diff --git a/README.md b/README.md
index 8a422dc6e..1e683aff8 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Both versions exist purely as research projects used to explore new ideas and ga
# What’s next
-An important aspect of KM² is how we are building the next memory prototype. In parallel, our team is developing [Amplifier](https://github.com/microsoft/amplifier/tree/next), a platform for metacognitive AI engineering. We use Amplifier to build Amplifier itself — and in the same way, we are using Amplifier to build the next generation of Kernel Memory.
+An important aspect of KM² is how we are building the next memory prototype. In parallel, our team is developing [Amplifier](https://github.com/microsoft/amplifier), a platform for metacognitive AI engineering. We use Amplifier to build Amplifier itself — and in a similar way, we are using AI and Amplifier concepts to build the next generation of Kernel Memory.
KM² will focus on the following areas, which will be documented in more detail when ready:
- quality of content generated
@@ -76,4 +76,4 @@ gh api repos/:owner/:repo/contributors --paginate --jq '
|
|
|
|
|
|
|
| [Valkozaur](https://github.com/Valkozaur) | [vicperdana](https://github.com/vicperdana) | [walexee](https://github.com/walexee) | [aportillo83](https://github.com/aportillo83) | [carlodek](https://github.com/carlodek) | [KSemenenko](https://github.com/KSemenenko) |
|
|
|
-| [roldengarm](https://github.com/roldengarm) | [snakex64](https://github.com/snakex64) |
\ No newline at end of file
+| [roldengarm](https://github.com/roldengarm) | [snakex64](https://github.com/snakex64) |
diff --git a/src/Core/Storage/Models/ContentDtoWithNode.cs b/src/Core/Storage/Models/ContentDtoWithNode.cs
new file mode 100644
index 000000000..7cc168bfe
--- /dev/null
+++ b/src/Core/Storage/Models/ContentDtoWithNode.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft. All rights reserved.
+namespace KernelMemory.Core.Storage.Models;
+
+///
+/// Content DTO with node information included.
+/// Used by CLI commands to show which node the content came from.
+///
+public class ContentDtoWithNode
+{
+ public string Id { get; set; } = string.Empty;
+ public string Node { get; set; } = string.Empty;
+ public string Content { get; set; } = string.Empty;
+ public string MimeType { get; set; } = string.Empty;
+ public long ByteSize { get; set; }
+ public DateTimeOffset ContentCreatedAt { get; set; }
+ public DateTimeOffset RecordCreatedAt { get; set; }
+ public DateTimeOffset RecordUpdatedAt { get; set; }
+ public string Title { get; set; } = string.Empty;
+ public string Description { get; set; } = string.Empty;
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays")]
+ public string[] Tags { get; set; } = [];
+ public Dictionary Metadata { get; set; } = new();
+
+ ///
+ /// Creates a ContentDtoWithNode from a ContentDto and node ID.
+ ///
+ /// The content DTO to wrap.
+ /// The node ID to include.
+ /// A new ContentDtoWithNode instance.
+ public static ContentDtoWithNode FromContentDto(ContentDto content, string nodeId)
+ {
+ return new ContentDtoWithNode
+ {
+ Id = content.Id,
+ Node = nodeId,
+ Content = content.Content,
+ MimeType = content.MimeType,
+ ByteSize = content.ByteSize,
+ ContentCreatedAt = content.ContentCreatedAt,
+ RecordCreatedAt = content.RecordCreatedAt,
+ RecordUpdatedAt = content.RecordUpdatedAt,
+ Title = content.Title,
+ Description = content.Description,
+ Tags = content.Tags,
+ Metadata = content.Metadata
+ };
+ }
+}
diff --git a/src/Main/CLI/Commands/GetCommand.cs b/src/Main/CLI/Commands/GetCommand.cs
index b8bd656b1..0bfc1095d 100644
--- a/src/Main/CLI/Commands/GetCommand.cs
+++ b/src/Main/CLI/Commands/GetCommand.cs
@@ -70,24 +70,8 @@ public override async Task ExecuteAsync(
}
// Wrap result with node information
- var response = new
- {
- id = result.Id,
- node = node.Id,
- content = result.Content,
- mimeType = result.MimeType,
- byteSize = result.ByteSize,
- contentCreatedAt = result.ContentCreatedAt,
- recordCreatedAt = result.RecordCreatedAt,
- recordUpdatedAt = result.RecordUpdatedAt,
- title = result.Title,
- description = result.Description,
- tags = result.Tags,
- metadata = result.Metadata
- };
-
- // If --full flag is set, ensure verbose mode for human formatter
- // For JSON/YAML, all fields are always included
+ var response = Core.Storage.Models.ContentDtoWithNode.FromContentDto(result, node.Id);
+
formatter.Format(response);
return Constants.ExitCodeSuccess;
diff --git a/src/Main/CLI/Commands/ListCommand.cs b/src/Main/CLI/Commands/ListCommand.cs
index 2a1a4e98f..403b3649a 100644
--- a/src/Main/CLI/Commands/ListCommand.cs
+++ b/src/Main/CLI/Commands/ListCommand.cs
@@ -75,21 +75,8 @@ public override async Task ExecuteAsync(
var items = await service.ListAsync(settings.Skip, settings.Take, CancellationToken.None).ConfigureAwait(false);
// Wrap items with node information
- var itemsWithNode = items.Select(item => new
- {
- id = item.Id,
- node = node.Id,
- content = item.Content,
- mimeType = item.MimeType,
- byteSize = item.ByteSize,
- contentCreatedAt = item.ContentCreatedAt,
- recordCreatedAt = item.RecordCreatedAt,
- recordUpdatedAt = item.RecordUpdatedAt,
- title = item.Title,
- description = item.Description,
- tags = item.Tags,
- metadata = item.Metadata
- });
+ var itemsWithNode = items.Select(item =>
+ Core.Storage.Models.ContentDtoWithNode.FromContentDto(item, node.Id));
// Format list with pagination info
formatter.FormatList(itemsWithNode, totalCount, settings.Skip, settings.Take);
diff --git a/src/Main/CLI/OutputFormatters/HumanOutputFormatter.cs b/src/Main/CLI/OutputFormatters/HumanOutputFormatter.cs
index 5e92198d2..bbff39234 100644
--- a/src/Main/CLI/OutputFormatters/HumanOutputFormatter.cs
+++ b/src/Main/CLI/OutputFormatters/HumanOutputFormatter.cs
@@ -41,6 +41,9 @@ public void Format(object data)
switch (data)
{
+ case Core.Storage.Models.ContentDtoWithNode contentWithNode:
+ this.FormatContentWithNode(contentWithNode);
+ break;
case ContentDto content:
this.FormatContent(content);
break;
@@ -82,7 +85,11 @@ public void FormatList(IEnumerable items, long totalCount, int skip, int t
var itemsList = items.ToList();
- if (typeof(T) == typeof(ContentDto))
+ if (typeof(T) == typeof(Core.Storage.Models.ContentDtoWithNode))
+ {
+ this.FormatContentWithNodeList(itemsList.Cast(), totalCount, skip, take);
+ }
+ else if (typeof(T) == typeof(ContentDto))
{
this.FormatContentList(itemsList.Cast(), totalCount, skip, take);
}
@@ -278,4 +285,121 @@ private void FormatGenericList(IEnumerable items, long totalCount, int ski
AnsiConsole.WriteLine(item?.ToString() ?? string.Empty);
}
}
+
+ private void FormatContentWithNode(Core.Storage.Models.ContentDtoWithNode content)
+ {
+ var isQuiet = this.Verbosity.Equals("quiet", StringComparison.OrdinalIgnoreCase);
+ var isVerbose = this.Verbosity.Equals("verbose", StringComparison.OrdinalIgnoreCase);
+
+ if (isQuiet)
+ {
+ // Quiet mode: just the ID
+ AnsiConsole.WriteLine(content.Id);
+ return;
+ }
+
+ var table = new Table();
+ table.Border(TableBorder.Rounded);
+ table.AddColumn("Property");
+ table.AddColumn("Value");
+
+ table.AddRow("[yellow]Node[/]", Markup.Escape(content.Node));
+ table.AddRow("[yellow]ID[/]", Markup.Escape(content.Id));
+
+ // Truncate content unless verbose
+ var displayContent = content.Content;
+ if (!isVerbose && displayContent.Length > Constants.MaxContentDisplayLength)
+ {
+ displayContent = string.Concat(displayContent.AsSpan(0, Constants.MaxContentDisplayLength), "...");
+ }
+ table.AddRow("[yellow]Content[/]", Markup.Escape(displayContent));
+
+ if (!string.IsNullOrEmpty(content.Title))
+ {
+ table.AddRow("[yellow]Title[/]", Markup.Escape(content.Title));
+ }
+
+ if (!string.IsNullOrEmpty(content.Description))
+ {
+ table.AddRow("[yellow]Description[/]", Markup.Escape(content.Description));
+ }
+
+ if (content.Tags.Length > 0)
+ {
+ table.AddRow("[yellow]Tags[/]", Markup.Escape(string.Join(", ", content.Tags)));
+ }
+
+ if (isVerbose)
+ {
+ table.AddRow("[yellow]MimeType[/]", Markup.Escape(content.MimeType));
+ table.AddRow("[yellow]Size[/]", $"{content.ByteSize} bytes");
+ table.AddRow("[yellow]ContentCreatedAt[/]", content.ContentCreatedAt.ToString("O"));
+ table.AddRow("[yellow]RecordCreatedAt[/]", content.RecordCreatedAt.ToString("O"));
+ table.AddRow("[yellow]RecordUpdatedAt[/]", content.RecordUpdatedAt.ToString("O"));
+
+ if (content.Metadata.Count > 0)
+ {
+ var metadataStr = string.Join(", ", content.Metadata.Select(kvp => $"{kvp.Key}={kvp.Value}"));
+ table.AddRow("[yellow]Metadata[/]", Markup.Escape(metadataStr));
+ }
+ }
+
+ AnsiConsole.Write(table);
+ }
+
+ private void FormatContentWithNodeList(IEnumerable contents, long totalCount, int skip, int take)
+ {
+ var isQuiet = this.Verbosity.Equals("quiet", StringComparison.OrdinalIgnoreCase);
+ var contentsList = contents.ToList();
+
+ // Check if list is empty
+ if (contentsList.Count == 0)
+ {
+ if (this._useColors)
+ {
+ AnsiConsole.MarkupLine("[dim]No content found[/]");
+ }
+ else
+ {
+ AnsiConsole.WriteLine("No content found");
+ }
+ return;
+ }
+
+ if (isQuiet)
+ {
+ // Quiet mode: just IDs
+ foreach (var content in contentsList)
+ {
+ AnsiConsole.WriteLine(content.Id);
+ }
+ return;
+ }
+
+ // Show pagination info
+ AnsiConsole.MarkupLine($"[cyan]Showing {contentsList.Count} of {totalCount} items (skip: {skip})[/]");
+ AnsiConsole.WriteLine();
+
+ // Create table
+ var table = new Table();
+ table.Border(TableBorder.Rounded);
+ table.AddColumn("[yellow]Node[/]");
+ table.AddColumn("[yellow]ID[/]");
+ table.AddColumn("[yellow]Content Preview[/]");
+
+ foreach (var content in contentsList)
+ {
+ var preview = content.Content.Length > 50
+ ? string.Concat(content.Content.AsSpan(0, 50), "...")
+ : content.Content;
+
+ table.AddRow(
+ Markup.Escape(content.Node),
+ Markup.Escape(content.Id),
+ Markup.Escape(preview)
+ );
+ }
+
+ AnsiConsole.Write(table);
+ }
}