feat(cli): unify wiki and sl search output with clack-style box and score badge (#89)

Routes `ktx wiki list` and `ktx wiki search` through the shared printList()
renderer so all four list/search commands now produce the same Clack-style
pretty box, TSV plain output, and JSON envelope. Adds a `--output` flag to
the wiki commands mirroring sl, and surfaces relevance score as a leading
dim badge ("87%") in pretty mode and a `score=` prefix in plain mode for
both wiki search and sl search. Empty results now emit a consistent
actionable hint across commands.
This commit is contained in:
Andrey Avtomonov 2026-05-14 15:15:20 +02:00 committed by GitHub
parent 52dd89481c
commit 77cce79237
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 440 additions and 86 deletions

View file

@ -6,17 +6,28 @@ import {
import { loadKtxProject } from '@ktx/context/project';
import {
type LocalKnowledgeScope,
type LocalKnowledgeSearchResult,
type LocalKnowledgeSummary,
listLocalKnowledgePages,
readLocalKnowledgePage,
searchLocalKnowledgePages,
writeLocalKnowledgePage,
} from '@ktx/context/wiki';
import { writeJsonResult } from './io/print-list.js';
import { resolveOutputMode } from './io/mode.js';
import { printList, type PrintListColumn, writeJsonResult } from './io/print-list.js';
export type KtxKnowledgeArgs =
| { command: 'list'; projectDir: string; userId: string; json?: boolean }
| { command: 'list'; projectDir: string; userId: string; output?: string; json?: boolean }
| { command: 'read'; projectDir: string; key: string; userId: string; json?: boolean }
| { command: 'search'; projectDir: string; query: string; userId: string; json?: boolean; limit?: number }
| {
command: 'search';
projectDir: string;
query: string;
userId: string;
output?: string;
json?: boolean;
limit?: number;
}
| {
command: 'write';
projectDir: string;
@ -30,10 +41,27 @@ export type KtxKnowledgeArgs =
slRefs: string[];
};
interface KtxKnowledgeIo {
stdout: { write(chunk: string): void };
stderr: { write(chunk: string): void };
}
type KtxKnowledgeIo = import('./cli-runtime.js').KtxCliIo;
const WIKI_LIST_COLUMNS: ReadonlyArray<PrintListColumn<LocalKnowledgeSummary>> = [
{ key: 'scope', label: 'SCOPE', plain: '' },
{ key: 'key', label: 'KEY', plain: '' },
{ key: 'summary', label: 'SUMMARY', plain: '', optional: true, dim: true },
];
const WIKI_SEARCH_COLUMNS: ReadonlyArray<PrintListColumn<LocalKnowledgeSearchResult>> = [
{
key: 'score',
label: 'SCORE',
plain: 'score=',
role: 'badge',
prettyFormat: (value) => `${Math.round(Number(value) * 100)}%`,
dim: true,
},
{ key: 'scope', label: 'SCOPE', plain: '' },
{ key: 'key', label: 'KEY', plain: '' },
{ key: 'summary', label: 'SUMMARY', plain: '', optional: true, dim: true },
];
interface KtxKnowledgeDeps {
embeddingService?: KtxEmbeddingPort | null;
@ -62,17 +90,18 @@ export async function runKtxKnowledge(
const project = await loadKtxProject({ projectDir: args.projectDir });
if (args.command === 'list') {
const pages = await listLocalKnowledgePages(project, { userId: args.userId });
if (args.json) {
writeJsonResult(io, {
kind: 'list',
data: { items: pages },
meta: { command: 'wiki list' },
});
return 0;
}
for (const page of pages) {
io.stdout.write(`${page.scope}\t${page.key}\t${page.summary}\n`);
}
const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
printList<LocalKnowledgeSummary>({
rows: pages,
columns: WIKI_LIST_COLUMNS,
groupBy: 'scope',
emptyMessage: `No local wiki pages found in ${project.projectDir}`,
emptyHint: 'Add Markdown files under wiki/ or run `ktx ingest <connectionId>`.',
unit: 'page',
command: 'wiki list',
mode,
io,
});
return 0;
}
if (args.command === 'read') {
@ -101,30 +130,27 @@ export async function runKtxKnowledge(
embeddingService: wikiSearchEmbeddingService(project, deps),
limit: args.limit,
});
if (args.json) {
writeJsonResult(io, {
kind: 'list',
data: { items: results },
meta: { command: 'wiki search' },
});
return 0;
}
if (results.length === 0) {
const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
let emptyMessage = `No local wiki pages matched "${args.query}"`;
let emptyHint = 'Run `ktx wiki list` to inspect available pages.';
if (results.length === 0 && mode !== 'json') {
const pages = await listLocalKnowledgePages(project, { userId: args.userId });
if (pages.length === 0) {
io.stderr.write(
`No local wiki pages found in ${project.projectDir}. Add Markdown files under wiki/ or run \`ktx ingest <connectionId>\`.\n`,
);
} else {
io.stderr.write(
`No local wiki pages matched "${args.query}". Run \`ktx wiki list\` to inspect available pages.\n`,
);
emptyMessage = `No local wiki pages found in ${project.projectDir}`;
emptyHint = 'Add Markdown files under wiki/ or run `ktx ingest <connectionId>`.';
}
return 0;
}
for (const result of results) {
io.stdout.write(`${result.score}\t${result.scope}\t${result.key}\t${result.summary}\n`);
}
printList<LocalKnowledgeSearchResult>({
rows: results,
columns: WIKI_SEARCH_COLUMNS,
groupBy: 'scope',
emptyMessage,
emptyHint,
unit: 'page',
command: 'wiki search',
mode,
io,
});
return 0;
}