Skip to content

Commit c376e9a

Browse files
authored
fix: restore human format table display for get and list commands (#1101)
## Summary Fixes broken human format output in `km get` and `km list` commands caused by #1100, which wrapped ContentDto objects in anonymous types that broke HumanOutputFormatter's type checking. This PR restores the table display functionality and improves the human format by: - Creating a proper `ContentDtoWithNode` wrapper class instead of using anonymous objects - Repositioning node field before ID for better visibility - Simplifying human format output by moving technical details (mimeType, size, dates) to verbose mode only **Before:** Human format was broken (displayed as JSON instead of tables) **After:** Clean tables showing only essential information: Node, ID, Content, Title, Description, Tags ## Changes - **New file:** `src/Core/Storage/Models/ContentDtoWithNode.cs` - Proper wrapper class for content with node information - **Modified:** `src/Main/CLI/Commands/GetCommand.cs` - Uses `ContentDtoWithNode.FromContentDto()` instead of anonymous object - **Modified:** `src/Main/CLI/Commands/ListCommand.cs` - Uses `ContentDtoWithNode.FromContentDto()` for list items - **Modified:** `src/Main/CLI/OutputFormatters/HumanOutputFormatter.cs` - Added `FormatContentWithNode()` and `FormatContentWithNodeList()` methods ### Human Format Display **km get (normal mode):** - Node - ID - Content - Title (if present) - Description (if present) - Tags (if present) **km get (verbose mode adds):** - MimeType, Size, ContentCreatedAt, RecordCreatedAt, RecordUpdatedAt, Metadata **km list (normal mode):** | Node | ID | Content Preview | ## Test Plan - [x] All 301 tests pass, 0 skipped - [x] Code coverage: 80.14% (exceeds 80% threshold) - [x] Build: 0 warnings, 0 errors - [x] Formatting: Clean - [x] Manual testing: `km get` displays table with node information - [x] Manual testing: `km list` displays table with node column - [x] JSON/YAML formats still include all fields including node ## Stats - **Files changed:** 5 - **Lines added:** 179 - **Lines removed:** 36 - **New classes:** 1 (`ContentDtoWithNode`) ## Breaking Changes None - this is a bug fix that restores expected functionality and improves UX
1 parent 2e2516f commit c376e9a

File tree

5 files changed

+179
-36
lines changed

5 files changed

+179
-36
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Both versions exist purely as research projects used to explore new ideas and ga
1414

1515
# What’s next
1616

17-
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.
17+
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.
1818

1919
KM² will focus on the following areas, which will be documented in more detail when ready:
2020
- quality of content generated
@@ -76,4 +76,4 @@ gh api repos/:owner/:repo/contributors --paginate --jq '
7676
| <img alt="Valkozaur" src="https://avatars.githubusercontent.com/u/58659526?v=4&s=110" width="110"> | <img alt="vicperdana" src="https://avatars.githubusercontent.com/u/7114832?v=4&s=110" width="110"> | <img alt="walexee" src="https://avatars.githubusercontent.com/u/12895846?v=4&s=110" width="110"> | <img alt="aportillo83" src="https://avatars.githubusercontent.com/u/72951744?v=4&s=110" width="110"> | <img alt="carlodek" src="https://avatars.githubusercontent.com/u/56030624?v=4&s=110" width="110"> | <img alt="KSemenenko" src="https://avatars.githubusercontent.com/u/4385716?v=4&s=110" width="110"> |
7777
| [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) |
7878
| <img alt="roldengarm" src="https://avatars.githubusercontent.com/u/37638588?v=4&s=110" width="110"> | <img alt="snakex64" src="https://avatars.githubusercontent.com/u/39806655?v=4&s=110" width="110"> |
79-
| [roldengarm](https://github.com/roldengarm) | [snakex64](https://github.com/snakex64) |
79+
| [roldengarm](https://github.com/roldengarm) | [snakex64](https://github.com/snakex64) |
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
namespace KernelMemory.Core.Storage.Models;
3+
4+
/// <summary>
5+
/// Content DTO with node information included.
6+
/// Used by CLI commands to show which node the content came from.
7+
/// </summary>
8+
public class ContentDtoWithNode
9+
{
10+
public string Id { get; set; } = string.Empty;
11+
public string Node { get; set; } = string.Empty;
12+
public string Content { get; set; } = string.Empty;
13+
public string MimeType { get; set; } = string.Empty;
14+
public long ByteSize { get; set; }
15+
public DateTimeOffset ContentCreatedAt { get; set; }
16+
public DateTimeOffset RecordCreatedAt { get; set; }
17+
public DateTimeOffset RecordUpdatedAt { get; set; }
18+
public string Title { get; set; } = string.Empty;
19+
public string Description { get; set; } = string.Empty;
20+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays")]
21+
public string[] Tags { get; set; } = [];
22+
public Dictionary<string, string> Metadata { get; set; } = new();
23+
24+
/// <summary>
25+
/// Creates a ContentDtoWithNode from a ContentDto and node ID.
26+
/// </summary>
27+
/// <param name="content">The content DTO to wrap.</param>
28+
/// <param name="nodeId">The node ID to include.</param>
29+
/// <returns>A new ContentDtoWithNode instance.</returns>
30+
public static ContentDtoWithNode FromContentDto(ContentDto content, string nodeId)
31+
{
32+
return new ContentDtoWithNode
33+
{
34+
Id = content.Id,
35+
Node = nodeId,
36+
Content = content.Content,
37+
MimeType = content.MimeType,
38+
ByteSize = content.ByteSize,
39+
ContentCreatedAt = content.ContentCreatedAt,
40+
RecordCreatedAt = content.RecordCreatedAt,
41+
RecordUpdatedAt = content.RecordUpdatedAt,
42+
Title = content.Title,
43+
Description = content.Description,
44+
Tags = content.Tags,
45+
Metadata = content.Metadata
46+
};
47+
}
48+
}

src/Main/CLI/Commands/GetCommand.cs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -70,24 +70,8 @@ public override async Task<int> ExecuteAsync(
7070
}
7171

7272
// Wrap result with node information
73-
var response = new
74-
{
75-
id = result.Id,
76-
node = node.Id,
77-
content = result.Content,
78-
mimeType = result.MimeType,
79-
byteSize = result.ByteSize,
80-
contentCreatedAt = result.ContentCreatedAt,
81-
recordCreatedAt = result.RecordCreatedAt,
82-
recordUpdatedAt = result.RecordUpdatedAt,
83-
title = result.Title,
84-
description = result.Description,
85-
tags = result.Tags,
86-
metadata = result.Metadata
87-
};
88-
89-
// If --full flag is set, ensure verbose mode for human formatter
90-
// For JSON/YAML, all fields are always included
73+
var response = Core.Storage.Models.ContentDtoWithNode.FromContentDto(result, node.Id);
74+
9175
formatter.Format(response);
9276

9377
return Constants.ExitCodeSuccess;

src/Main/CLI/Commands/ListCommand.cs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,21 +75,8 @@ public override async Task<int> ExecuteAsync(
7575
var items = await service.ListAsync(settings.Skip, settings.Take, CancellationToken.None).ConfigureAwait(false);
7676

7777
// Wrap items with node information
78-
var itemsWithNode = items.Select(item => new
79-
{
80-
id = item.Id,
81-
node = node.Id,
82-
content = item.Content,
83-
mimeType = item.MimeType,
84-
byteSize = item.ByteSize,
85-
contentCreatedAt = item.ContentCreatedAt,
86-
recordCreatedAt = item.RecordCreatedAt,
87-
recordUpdatedAt = item.RecordUpdatedAt,
88-
title = item.Title,
89-
description = item.Description,
90-
tags = item.Tags,
91-
metadata = item.Metadata
92-
});
78+
var itemsWithNode = items.Select(item =>
79+
Core.Storage.Models.ContentDtoWithNode.FromContentDto(item, node.Id));
9380

9481
// Format list with pagination info
9582
formatter.FormatList(itemsWithNode, totalCount, settings.Skip, settings.Take);

src/Main/CLI/OutputFormatters/HumanOutputFormatter.cs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public void Format(object data)
4141

4242
switch (data)
4343
{
44+
case Core.Storage.Models.ContentDtoWithNode contentWithNode:
45+
this.FormatContentWithNode(contentWithNode);
46+
break;
4447
case ContentDto content:
4548
this.FormatContent(content);
4649
break;
@@ -82,7 +85,11 @@ public void FormatList<T>(IEnumerable<T> items, long totalCount, int skip, int t
8285

8386
var itemsList = items.ToList();
8487

85-
if (typeof(T) == typeof(ContentDto))
88+
if (typeof(T) == typeof(Core.Storage.Models.ContentDtoWithNode))
89+
{
90+
this.FormatContentWithNodeList(itemsList.Cast<Core.Storage.Models.ContentDtoWithNode>(), totalCount, skip, take);
91+
}
92+
else if (typeof(T) == typeof(ContentDto))
8693
{
8794
this.FormatContentList(itemsList.Cast<ContentDto>(), totalCount, skip, take);
8895
}
@@ -278,4 +285,121 @@ private void FormatGenericList<T>(IEnumerable<T> items, long totalCount, int ski
278285
AnsiConsole.WriteLine(item?.ToString() ?? string.Empty);
279286
}
280287
}
288+
289+
private void FormatContentWithNode(Core.Storage.Models.ContentDtoWithNode content)
290+
{
291+
var isQuiet = this.Verbosity.Equals("quiet", StringComparison.OrdinalIgnoreCase);
292+
var isVerbose = this.Verbosity.Equals("verbose", StringComparison.OrdinalIgnoreCase);
293+
294+
if (isQuiet)
295+
{
296+
// Quiet mode: just the ID
297+
AnsiConsole.WriteLine(content.Id);
298+
return;
299+
}
300+
301+
var table = new Table();
302+
table.Border(TableBorder.Rounded);
303+
table.AddColumn("Property");
304+
table.AddColumn("Value");
305+
306+
table.AddRow("[yellow]Node[/]", Markup.Escape(content.Node));
307+
table.AddRow("[yellow]ID[/]", Markup.Escape(content.Id));
308+
309+
// Truncate content unless verbose
310+
var displayContent = content.Content;
311+
if (!isVerbose && displayContent.Length > Constants.MaxContentDisplayLength)
312+
{
313+
displayContent = string.Concat(displayContent.AsSpan(0, Constants.MaxContentDisplayLength), "...");
314+
}
315+
table.AddRow("[yellow]Content[/]", Markup.Escape(displayContent));
316+
317+
if (!string.IsNullOrEmpty(content.Title))
318+
{
319+
table.AddRow("[yellow]Title[/]", Markup.Escape(content.Title));
320+
}
321+
322+
if (!string.IsNullOrEmpty(content.Description))
323+
{
324+
table.AddRow("[yellow]Description[/]", Markup.Escape(content.Description));
325+
}
326+
327+
if (content.Tags.Length > 0)
328+
{
329+
table.AddRow("[yellow]Tags[/]", Markup.Escape(string.Join(", ", content.Tags)));
330+
}
331+
332+
if (isVerbose)
333+
{
334+
table.AddRow("[yellow]MimeType[/]", Markup.Escape(content.MimeType));
335+
table.AddRow("[yellow]Size[/]", $"{content.ByteSize} bytes");
336+
table.AddRow("[yellow]ContentCreatedAt[/]", content.ContentCreatedAt.ToString("O"));
337+
table.AddRow("[yellow]RecordCreatedAt[/]", content.RecordCreatedAt.ToString("O"));
338+
table.AddRow("[yellow]RecordUpdatedAt[/]", content.RecordUpdatedAt.ToString("O"));
339+
340+
if (content.Metadata.Count > 0)
341+
{
342+
var metadataStr = string.Join(", ", content.Metadata.Select(kvp => $"{kvp.Key}={kvp.Value}"));
343+
table.AddRow("[yellow]Metadata[/]", Markup.Escape(metadataStr));
344+
}
345+
}
346+
347+
AnsiConsole.Write(table);
348+
}
349+
350+
private void FormatContentWithNodeList(IEnumerable<Core.Storage.Models.ContentDtoWithNode> contents, long totalCount, int skip, int take)
351+
{
352+
var isQuiet = this.Verbosity.Equals("quiet", StringComparison.OrdinalIgnoreCase);
353+
var contentsList = contents.ToList();
354+
355+
// Check if list is empty
356+
if (contentsList.Count == 0)
357+
{
358+
if (this._useColors)
359+
{
360+
AnsiConsole.MarkupLine("[dim]No content found[/]");
361+
}
362+
else
363+
{
364+
AnsiConsole.WriteLine("No content found");
365+
}
366+
return;
367+
}
368+
369+
if (isQuiet)
370+
{
371+
// Quiet mode: just IDs
372+
foreach (var content in contentsList)
373+
{
374+
AnsiConsole.WriteLine(content.Id);
375+
}
376+
return;
377+
}
378+
379+
// Show pagination info
380+
AnsiConsole.MarkupLine($"[cyan]Showing {contentsList.Count} of {totalCount} items (skip: {skip})[/]");
381+
AnsiConsole.WriteLine();
382+
383+
// Create table
384+
var table = new Table();
385+
table.Border(TableBorder.Rounded);
386+
table.AddColumn("[yellow]Node[/]");
387+
table.AddColumn("[yellow]ID[/]");
388+
table.AddColumn("[yellow]Content Preview[/]");
389+
390+
foreach (var content in contentsList)
391+
{
392+
var preview = content.Content.Length > 50
393+
? string.Concat(content.Content.AsSpan(0, 50), "...")
394+
: content.Content;
395+
396+
table.AddRow(
397+
Markup.Escape(content.Node),
398+
Markup.Escape(content.Id),
399+
Markup.Escape(preview)
400+
);
401+
}
402+
403+
AnsiConsole.Write(table);
404+
}
281405
}

0 commit comments

Comments
 (0)