diff --git a/docs-site/content/docs/cli-reference/ktx-ingest.mdx b/docs-site/content/docs/cli-reference/ktx-ingest.mdx index 4ae2e6f1..e7b8bbe5 100644 --- a/docs-site/content/docs/cli-reference/ktx-ingest.mdx +++ b/docs-site/content/docs/cli-reference/ktx-ingest.mdx @@ -1,6 +1,6 @@ --- title: "ktx ingest" -description: "Build, inspect, and replay KTX context ingest runs." +description: "Build or refresh KTX context from configured connections." --- `ktx ingest` builds or refreshes KTX context from configured connections. @@ -36,16 +36,6 @@ connections when you use `--all`. database connections. Query-history flags apply only to database connections that support query history. -## Status and replay - -| Subcommand | Description | -|------------|-------------| -| `status [runId]` | Print status for the latest or selected stored ingest run or report file | -| `replay ` | Replay a stored ingest run or bundle report through memory-flow output | - -Both subcommands accept `--report-file `, `--plain`, `--json`, `--viz`, -and `--no-input`. - ## Examples ```bash @@ -57,14 +47,6 @@ ktx ingest warehouse --query-history-window-days 30 ktx ingest notion ktx ingest --all ktx ingest --all --deep - -ktx ingest status -ktx ingest status run-abc123 -ktx ingest status --json - -ktx ingest replay run-abc123 -ktx ingest replay run-abc123 --viz -ktx ingest replay run-abc123 --report-file /tmp/ingest-report.json ``` ## Common errors @@ -74,5 +56,4 @@ ktx ingest replay run-abc123 --report-file /tmp/ingest-report.json | Connection not configured | The connection id is not present in `ktx.yaml` | Add the connection with `ktx setup` or update `ktx.yaml` | | Deep readiness is missing | `--deep` or query history needs model, embedding, and scan-enrichment configuration | Run `ktx setup` or rerun with `--fast` | | Query history is unsupported | The selected database driver does not support query history | Run schema ingest without query-history flags | -| Latest run not found | No stored ingest report exists in this project | Run `ktx ingest ` first | -| Visual replay fails in a non-interactive shell | Visual report replay needs a terminal | Use `ktx ingest status --json` for agent and CI workflows | +| No ingest target was selected | No connection id was provided and `--all` was omitted | Run `ktx ingest ` or `ktx ingest --all` | diff --git a/docs-site/content/docs/cli-reference/ktx-wiki.mdx b/docs-site/content/docs/cli-reference/ktx-wiki.mdx index 1d57a93f..a6a0ca01 100644 --- a/docs-site/content/docs/cli-reference/ktx-wiki.mdx +++ b/docs-site/content/docs/cli-reference/ktx-wiki.mdx @@ -1,6 +1,6 @@ --- title: "ktx wiki" -description: "List, read, search, or write wiki pages." +description: "List or search wiki pages." --- Manage wiki pages in your KTX project. Wiki pages are Markdown documents that capture business definitions, rules, and gotchas. Agents search them for context when answering questions about your data. @@ -16,9 +16,7 @@ ktx wiki [options] | Subcommand | Description | |-----------|-------------| | `list` | List local wiki pages | -| `read ` | Read one local wiki page | | `search ` | Search local wiki pages | -| `write ` | Write one local wiki page | ## Options @@ -29,13 +27,6 @@ ktx wiki [options] | `--json` | Print JSON output | `false` | | `--user-id ` | Local user id | `local` | -### `wiki read` - -| Flag | Description | Default | -|------|-------------|---------| -| `--json` | Print JSON output | `false` | -| `--user-id ` | Local user id | `local` | - ### `wiki search` | Flag | Description | Default | @@ -44,18 +35,6 @@ ktx wiki [options] | `--user-id ` | Local user id | `local` | | `--limit ` | Maximum search results | — | -### `wiki write` - -| Flag | Description | Default | -|------|-------------|---------| -| `--user-id ` | Local user id | `local` | -| `--scope ` | Scope: `global` or `user` | `global` | -| `--summary ` | Wiki page summary (required) | — | -| `--content ` | Wiki page content (required) | — | -| `--tag ` | Wiki tag; repeatable | — | -| `--ref ` | Wiki ref; repeatable | — | -| `--sl-ref ` | Semantic-layer ref; repeatable | — | - ## Examples ```bash @@ -65,48 +44,17 @@ ktx wiki list # List all wiki pages as JSON ktx wiki list --json -# Read a specific wiki page -ktx wiki read revenue-definitions - -# Read a specific wiki page as JSON -ktx wiki read revenue-definitions --json - # Search wiki pages ktx wiki search "monthly recurring revenue" # Search wiki pages as JSON ktx wiki search "monthly recurring revenue" --json --limit 10 - -# Write a global wiki page -ktx wiki write revenue-definitions \ - --summary "Canonical revenue metric definitions" \ - --content "## MRR\nMonthly Recurring Revenue is calculated as..." - -# Write a user-scoped wiki page -ktx wiki write my-notes \ - --scope user \ - --summary "Personal analysis notes" \ - --content "Things to check when revenue numbers look off..." - -# Write a page with tags and references -ktx wiki write churn-rules \ - --summary "Churn calculation business rules" \ - --content "A customer is considered churned when..." \ - --tag finance \ - --tag retention \ - --sl-ref customers \ - --sl-ref subscriptions - -# Write a page with external references -ktx wiki write data-freshness \ - --summary "Data pipeline SLAs and freshness guarantees" \ - --content "The orders table refreshes every 15 minutes..." \ - --ref "https://wiki.example.com/data-pipelines" ``` ## Output -Wiki commands print local wiki pages and search results. Agents should search first, then read the most relevant page by key. +Wiki commands print local wiki page listings and search results. Open the +matching Markdown files directly when you need the full page contents. ```json { @@ -128,6 +76,4 @@ Wiki commands print local wiki pages and search results. Agents should search fi | Error | Cause | Recovery | |-------|-------|----------| | Search returns no results | The query terms do not match summaries, tags, or content | Retry with business synonyms, then create a page if the knowledge is missing | -| Read fails for a key | The page key is wrong or scoped to a different user | Run `ktx wiki list` or search again to get the exact key | -| Write fails due to missing fields | `--summary` or `--content` was omitted | Pass both fields, and keep the summary short enough for search results | -| Agent writes duplicate pages | It did not search existing pages first | Always run `ktx wiki search` before `ktx wiki write` | +| A page is missing | No Markdown file exists for that business context | Add a file under `wiki/` or run `ktx ingest ` | diff --git a/docs-site/content/docs/getting-started/quickstart.mdx b/docs-site/content/docs/getting-started/quickstart.mdx index 462c5dd4..46a45db4 100644 --- a/docs-site/content/docs/getting-started/quickstart.mdx +++ b/docs-site/content/docs/getting-started/quickstart.mdx @@ -211,8 +211,8 @@ KTX writes project state as plain files so agents can inspect and edit changes i | `ktx.yaml` | `ktx setup` | Main project configuration: connections, LLM settings, embeddings, and context sources | | `.ktx/secrets/*` | `ktx setup` when file-backed secrets are selected | Local secret files referenced from `ktx.yaml`; do not commit these | | `semantic-layer//*.yaml` | context build, ingestion, or direct file edits | Semantic source definitions agents use for SQL generation | -| `wiki/global/*.md` | ingestion, memory capture, `ktx wiki write --scope global`, or direct file edits | Shared business context and metric definitions | -| `wiki/user//*.md` | memory capture, `ktx wiki write --scope user`, or direct file edits | User-scoped notes for one agent/user context | +| `wiki/global/*.md` | ingestion, memory capture, or direct file edits | Shared business context and metric definitions | +| `wiki/user//*.md` | memory capture or direct file edits | User-scoped notes for one agent/user context | | `.claude/skills/ktx/SKILL.md`, `.agents/skills/ktx/SKILL.md` | CLI-mode agent integration setup | Agent instructions for calling public `ktx` commands | ## Verify it worked diff --git a/docs-site/content/docs/guides/building-context.mdx b/docs-site/content/docs/guides/building-context.mdx index 56fb7d1d..5dcf2422 100644 --- a/docs-site/content/docs/guides/building-context.mdx +++ b/docs-site/content/docs/guides/building-context.mdx @@ -87,21 +87,7 @@ Useful output flags: | `--json` | Output as JSON | | `--plain` | Plain text output | -### Inspecting stored reports - -```bash -# Check status of the latest ingest -ktx ingest status - -# Check a specific run -ktx ingest status - -# Replay a past ingest run -ktx ingest replay -``` - -`ktx ingest replay` opens the stored memory-flow output for a completed run. -Foreground context builds do not detach into background control sessions; if a +Foreground context builds do not detach into background control sessions. If a run is interrupted, rerun `ktx ingest ` or `ktx ingest --all`. ### Supported context sources @@ -178,12 +164,8 @@ sl_refs: [orders] Orders in "pending" status for more than 48 hours are flagged for review. ``` -### Deterministic replay +### Ingest transcripts -Every ingest session records a full transcript — tool calls, LLM responses, and write decisions. You can replay any session to debug why a source was written a certain way: - -```bash -ktx ingest replay --viz -``` - -This opens the same TUI view as the original run, letting you step through the agent's reasoning. +Every ingest session records a full transcript: tool calls, LLM responses, and +write decisions. Inspect the stored transcript files when you need to debug why +a source was written a certain way. diff --git a/docs-site/content/docs/guides/writing-context.mdx b/docs-site/content/docs/guides/writing-context.mdx index b6ca3597..b5a6db5c 100644 --- a/docs-site/content/docs/guides/writing-context.mdx +++ b/docs-site/content/docs/guides/writing-context.mdx @@ -248,8 +248,7 @@ wiki/ ### Editing pages Create and edit wiki pages directly as Markdown files in the `wiki/` -directory, or with `ktx wiki write`. Ingest and memory capture also create -these pages automatically. +directory. Ingest and memory capture also create these pages automatically. Wiki page fields: diff --git a/docs-site/content/docs/integrations/agent-clients.mdx b/docs-site/content/docs/integrations/agent-clients.mdx index 95786f52..6f7e7660 100644 --- a/docs-site/content/docs/integrations/agent-clients.mdx +++ b/docs-site/content/docs/integrations/agent-clients.mdx @@ -125,8 +125,6 @@ All supported agent clients call the same KTX CLI commands: |---------|-------------| | `ktx status --json` | Return project setup and context readiness | | `ktx wiki search --json` | Search wiki pages | -| `ktx wiki read --json` | Read a wiki page | -| `ktx wiki write ` | Write or update a wiki page | | `ktx sl list --json` | List semantic-layer sources | | `ktx sl search --json` | Search semantic-layer sources | | `ktx sl validate --connection-id ` | Validate semantic source definitions | diff --git a/docs-site/content/docs/integrations/context-sources.mdx b/docs-site/content/docs/integrations/context-sources.mdx index 0cd8bb8b..86c3135a 100644 --- a/docs-site/content/docs/integrations/context-sources.mdx +++ b/docs-site/content/docs/integrations/context-sources.mdx @@ -15,7 +15,7 @@ Agents must configure and ingest context sources in this order: 2. Store tokens as `env:NAME` or `file:/path/to/secret`. 3. Run `ktx ingest ` for one source or `ktx ingest --all` for every configured source. -4. Check progress with `ktx ingest status --json`. +4. Review the foreground ingest output. 5. Review generated `semantic-layer/` YAML and `wiki/` Markdown files in git. 6. Validate changed semantic sources with `ktx sl validate`. diff --git a/packages/cli/src/cli-program.ts b/packages/cli/src/cli-program.ts index e9265c09..35f059ab 100644 --- a/packages/cli/src/cli-program.ts +++ b/packages/cli/src/cli-program.ts @@ -53,7 +53,27 @@ type CommandPathNode = CommandWithGlobalOptions & { }; const PROJECT_AWARE_ROOT_COMMANDS = new Set(['setup', 'connection', 'ingest', 'wiki', 'sl', 'status']); -const REMOVED_ROOT_COMMANDS = new Set(['scan']); +const REMOVED_COMMAND_PATHS = new Set([ + 'scan', + 'ingest run', + 'ingest status', + 'ingest watch', + 'ingest replay', + 'wiki read', + 'wiki write', +]); +const GLOBAL_OPTIONS_WITH_VALUE = new Set(['--project-dir']); +const OPTIONS_WITH_VALUE = new Set([ + '--project-dir', + '--query-history-window-days', + '--user-id', + '--limit', + '--format', + '--connection-id', + '--source-name', + '--query-file', + '--max-rows', +]); export interface CommandWithGlobalOptions { opts: () => object; @@ -175,9 +195,6 @@ function shouldSuppressProjectDirLine(path: string[], options: Record= 0) { const demoCommand = path[demoIndex + 1]; @@ -222,10 +239,6 @@ function createBaseProgram(info: KtxCliPackageInfo, io: KtxCliIo): Command { .version(`${info.name} ${info.version}`, '-v, --version', 'Show CLI version') .helpOption('-h, --help', 'Show this help text') .configureHelp({ showGlobalOptions: true }) - .addHelpText( - 'after', - '\nAdvanced:\n ktx dev Low-level project initialization and runtime management.\n', - ) .showHelpAfterError() .exitOverride() .configureOutput({ @@ -255,6 +268,45 @@ function formatCliError(error: unknown): string { return error instanceof Error ? error.message : String(error); } +function commandPathFromArgv(argv: string[]): string[] { + const path: string[] = []; + for (let index = 0; index < argv.length && path.length < 2; index += 1) { + const arg = argv[index]; + if (arg === undefined) { + continue; + } + if (arg === '--') { + break; + } + if ((path.length === 0 ? GLOBAL_OPTIONS_WITH_VALUE : OPTIONS_WITH_VALUE).has(arg)) { + index += 1; + continue; + } + const optionsWithValue = path.length === 0 ? GLOBAL_OPTIONS_WITH_VALUE : OPTIONS_WITH_VALUE; + if ([...optionsWithValue].some((option) => arg.startsWith(`${option}=`))) { + continue; + } + if (path.length === 0 && arg === '--debug') { + continue; + } + if (arg.startsWith('-')) { + continue; + } + path.push(arg); + } + return path; +} + +function removedCommandName(argv: string[]): string | null { + const path = commandPathFromArgv(argv); + if (path.length === 0) { + return null; + } + + const pathKey = path.join(' '); + return REMOVED_COMMAND_PATHS.has(pathKey) ? path.at(-1) ?? null : null; +} + async function runBareInteractiveCommand( program: Command, io: KtxCliIo, @@ -309,10 +361,7 @@ export function buildKtxProgram(options: BuildKtxProgramOptions): Command { registerSetupCommands(program, context); registerConnectionCommands(program, context); - registerIngestCommands(program, context, { - runIngestWithProgress: async (ingestArgs, ingestIo, ingestDeps, defaultRunIngest) => - await (ingestDeps.ingest ?? defaultRunIngest)(ingestArgs, ingestIo), - }); + registerIngestCommands(program, context); registerWikiCommands(program, context); registerSlCommands(program, context); registerStatusCommands(program, context); @@ -366,8 +415,9 @@ export async function runCommanderKtxCli( return 0; } - if (REMOVED_ROOT_COMMANDS.has(argv[0] ?? '')) { - io.stderr.write(`error: unknown command '${argv[0]}'\n`); + const removedCommand = removedCommandName(argv); + if (removedCommand) { + io.stderr.write(`error: unknown command '${removedCommand}'\n`); return 1; } diff --git a/packages/cli/src/cli-runtime.ts b/packages/cli/src/cli-runtime.ts index 6f25204d..b3ab3ed0 100644 --- a/packages/cli/src/cli-runtime.ts +++ b/packages/cli/src/cli-runtime.ts @@ -2,7 +2,6 @@ import { createRequire } from 'node:module'; import type { KtxConnectionArgs } from './connection.js'; import type { KtxDoctorArgs } from './doctor.js'; -import type { KtxIngestArgs } from './ingest.js'; import type { KtxKnowledgeArgs } from './knowledge.js'; import type { KtxPublicIngestArgs } from './public-ingest.js'; import type { KtxRuntimeArgs } from './runtime.js'; @@ -29,7 +28,6 @@ export interface KtxCliDeps { setup?: (args: KtxSetupArgs, io: KtxCliIo) => Promise; connection?: (args: KtxConnectionArgs, io: KtxCliIo) => Promise; doctor?: (args: KtxDoctorArgs, io: KtxCliIo) => Promise; - ingest?: (args: KtxIngestArgs, io: KtxCliIo) => Promise; publicIngest?: (args: KtxPublicIngestArgs, io: KtxCliIo) => Promise; runtime?: (args: KtxRuntimeArgs, io: KtxCliIo) => Promise; knowledge?: (args: KtxKnowledgeArgs, io: KtxCliIo) => Promise; diff --git a/packages/cli/src/command-schemas.ts b/packages/cli/src/command-schemas.ts index 5caece1f..e1365d86 100644 --- a/packages/cli/src/command-schemas.ts +++ b/packages/cli/src/command-schemas.ts @@ -3,19 +3,6 @@ import { z } from 'zod'; const projectDirSchema = z.string().min(1); const stringArraySchema = z.array(z.string()); -export const wikiWriteCommandSchema = z.object({ - command: z.literal('write'), - projectDir: projectDirSchema, - key: z.string().min(1), - scope: z.enum(['GLOBAL', 'USER']), - userId: z.string().min(1), - summary: z.string().min(1), - content: z.string().min(1), - tags: stringArraySchema, - refs: stringArraySchema, - slRefs: stringArraySchema, -}); - const orderBySchema = z.union([ z.string().min(1), z.object({ diff --git a/packages/cli/src/command-tree.test.ts b/packages/cli/src/command-tree.test.ts index a23ebd1b..181fac77 100644 --- a/packages/cli/src/command-tree.test.ts +++ b/packages/cli/src/command-tree.test.ts @@ -53,7 +53,7 @@ describe('walkCommandTree', () => { expect(walkCommandTree(command).arguments).toEqual(['', '[schemas...]']); }); - it('omits Commander hidden commands from the public tree', () => { + it('walks registered commands without applying hidden-command policy', () => { const root = new Command('ktx'); root.command('scan', { hidden: true }).description('Run a standalone connection scan'); const ingest = root.command('ingest').description('Build or inspect KTX context'); @@ -64,10 +64,19 @@ describe('walkCommandTree', () => { const tree = walkCommandTree(root); - expect(tree.children.map((child) => child.name)).toEqual(['ingest', 'status']); + expect(tree.children.map((child) => child.name)).toEqual(['scan', 'ingest', 'status']); expect(tree.children[0]).toMatchObject({ + name: 'scan', + description: 'Run a standalone connection scan', + children: [], + }); + expect(tree.children[1]).toMatchObject({ name: 'ingest', - children: [{ name: 'status', description: 'Print status', aliases: [], arguments: [], children: [] }], + children: [ + { name: 'run', description: 'Run local ingest by adapter', aliases: [], arguments: [], children: [] }, + { name: 'watch', description: 'Open a stored visual report', aliases: [], arguments: [], children: [] }, + { name: 'status', description: 'Print status', aliases: [], arguments: [], children: [] }, + ], }); }); }); diff --git a/packages/cli/src/command-tree.ts b/packages/cli/src/command-tree.ts index 1ef9ba26..2eeb24e8 100644 --- a/packages/cli/src/command-tree.ts +++ b/packages/cli/src/command-tree.ts @@ -10,17 +10,13 @@ export interface CommandTreeNode { children: CommandTreeNode[]; } -function isHiddenCommand(command: CommandUnknownOpts): boolean { - return (command as CommandUnknownOpts & { _hidden?: boolean })._hidden === true; -} - export function walkCommandTree(command: CommandUnknownOpts): CommandTreeNode { return { name: command.name(), description: command.description(), aliases: command.aliases(), arguments: command.registeredArguments.map(formatArgumentDeclaration), - children: command.commands.filter((child) => !isHiddenCommand(child)).map((child) => walkCommandTree(child)), + children: command.commands.map((child) => walkCommandTree(child)), }; } diff --git a/packages/cli/src/commands/ingest-commands.ts b/packages/cli/src/commands/ingest-commands.ts index 482e301e..030fdd77 100644 --- a/packages/cli/src/commands/ingest-commands.ts +++ b/packages/cli/src/commands/ingest-commands.ts @@ -1,81 +1,15 @@ -import { resolve } from 'node:path'; import { type Command, Option } from '@commander-js/extra-typings'; import { type KtxCliCommandContext, - type OutputModeOptions, parsePositiveIntegerOption, resolveCommandProjectDir, } from '../cli-program.js'; -import type { KtxCliDeps, KtxCliIo } from '../index.js'; -import type { KtxIngestArgs, KtxIngestOutputMode } from '../ingest.js'; -import { runtimeInstallPolicyFromFlags } from '../managed-python-command.js'; import type { KtxPublicIngestArgs } from '../public-ingest.js'; import { profileMark } from '../startup-profile.js'; profileMark('module:commands/ingest-commands'); -interface IngestCommandOptions { - runIngestWithProgress: ( - args: KtxIngestArgs, - io: KtxCliIo, - deps: KtxCliDeps, - defaultRunIngest: (args: KtxIngestArgs, io: KtxCliIo) => Promise, - ) => Promise; -} - -function outputMode(options: OutputModeOptions): KtxIngestOutputMode { - if (options.json === true) { - return 'json'; - } - if (options.viz === true) { - return 'viz'; - } - return 'plain'; -} - -function watchOutputMode(options: OutputModeOptions): KtxIngestOutputMode { - if (options.json === true) { - return 'json'; - } - if (options.plain === true) { - return 'plain'; - } - return 'viz'; -} - -function inputMode(options: OutputModeOptions): Pick { - return options.input === false ? { inputMode: 'disabled' } : {}; -} - -function resolvedOptions(command: { optsWithGlobals?: () => object }, fallback: T): T { - return (command.optsWithGlobals ? command.optsWithGlobals() : fallback) as T; -} - -function assertOutputModeCompatible(options: OutputModeOptions): void { - const requested = [ - options.plain === true ? '--plain' : undefined, - options.json === true ? '--json' : undefined, - options.viz === true ? '--viz' : undefined, - ].filter((option): option is string => option !== undefined); - if (requested.length > 1) { - throw new Error(`Output mode options cannot be used together: ${requested.join(', ')}`); - } -} - -async function runIngestArgs( - context: KtxCliCommandContext, - args: KtxIngestArgs, - options: IngestCommandOptions, -): Promise { - const { runKtxIngest } = await import('../ingest.js'); - context.setExitCode(await options.runIngestWithProgress(args, context.io, context.deps, runKtxIngest)); -} - -export function registerIngestCommands( - program: Command, - context: KtxCliCommandContext, - commandOptions: IngestCommandOptions, -): void { +export function registerIngestCommands(program: Command, context: KtxCliCommandContext): void { const ingest = program .command('ingest') .description('Build or inspect KTX context') @@ -114,123 +48,4 @@ export function registerIngestCommands( ingest.hook('preAction', (_thisCommand, actionCommand) => { context.writeDebug?.('ingest', actionCommand); }); - - ingest - .command('run', { hidden: true }) - .description('Run local ingest for one configured connection and source adapter') - .requiredOption('--connection-id ', 'KTX connection id') - .requiredOption('--adapter ', 'Ingest source adapter name') - .option('--source-dir ', 'Directory containing source files') - .option('--database-introspection-url ', 'Daemon URL for live-database introspection') - .option('--debug-llm-request-file ', 'Write sanitized LLM request structure to a JSONL file') - .option('--report-file ', 'Unsupported for ingest run; use ingest status/watch instead') - .addOption(new Option('--plain', 'Print plain text output').conflicts(['json', 'viz'])) - .addOption(new Option('--json', 'Print JSON output').conflicts(['plain', 'viz'])) - .addOption(new Option('--viz', 'Render memory-flow TUI output').conflicts(['plain', 'json'])) - .option('--yes', 'Install the managed Python runtime without prompting when required', false) - .option('--no-input', 'Disable interactive terminal input for visualization') - .action(async (options, command) => { - const commandOptionsWithGlobals = resolvedOptions(command, options); - assertOutputModeCompatible(commandOptionsWithGlobals); - if (options.reportFile) { - throw new Error('--report-file is only supported for ingest status/watch'); - } - await runIngestArgs( - context, - { - command: 'run', - projectDir: resolveCommandProjectDir(command), - connectionId: commandOptionsWithGlobals.connectionId, - adapter: commandOptionsWithGlobals.adapter, - sourceDir: commandOptionsWithGlobals.sourceDir ? resolve(commandOptionsWithGlobals.sourceDir) : undefined, - databaseIntrospectionUrl: commandOptionsWithGlobals.databaseIntrospectionUrl || undefined, - cliVersion: context.packageInfo.version, - runtimeInstallPolicy: runtimeInstallPolicyFromFlags({ yes: commandOptionsWithGlobals.yes }), - ...(commandOptionsWithGlobals.debugLlmRequestFile - ? { debugLlmRequestFile: resolve(commandOptionsWithGlobals.debugLlmRequestFile) } - : {}), - outputMode: outputMode(commandOptionsWithGlobals), - ...inputMode(commandOptionsWithGlobals), - }, - commandOptions, - ); - }); - - ingest - .command('status') - .description('Print status for the latest or selected stored ingest report') - .argument('[runId]', 'Local ingest id, report id, run id, or job id') - .option('--report-file ', 'Bundle ingest report JSON file to render') - .addOption(new Option('--plain', 'Print plain text output').conflicts(['json', 'viz'])) - .addOption(new Option('--json', 'Print JSON output').conflicts(['plain', 'viz'])) - .addOption(new Option('--viz', 'Render memory-flow TUI output').conflicts(['plain', 'json'])) - .option('--no-input', 'Disable interactive terminal input for visualization') - .action(async (runId: string | undefined, options, command) => { - const commandOptionsWithGlobals = resolvedOptions(command, options); - assertOutputModeCompatible(commandOptionsWithGlobals); - await runIngestArgs( - context, - { - command: 'status', - projectDir: resolveCommandProjectDir(command), - ...(runId ? { runId } : {}), - ...(commandOptionsWithGlobals.reportFile ? { reportFile: resolve(commandOptionsWithGlobals.reportFile) } : {}), - outputMode: outputMode(commandOptionsWithGlobals), - ...inputMode(commandOptionsWithGlobals), - }, - commandOptions, - ); - }); - - ingest - .command('watch', { hidden: true }) - .description('Open the latest or selected stored ingest visual report') - .argument('[runId]', 'Local ingest id, report id, run id, or job id') - .option('--report-file ', 'Bundle ingest report JSON file to render') - .addOption(new Option('--plain', 'Print plain text output').conflicts(['json', 'viz'])) - .addOption(new Option('--json', 'Print JSON output').conflicts(['plain', 'viz'])) - .addOption(new Option('--viz', 'Render memory-flow TUI output').conflicts(['plain', 'json'])) - .option('--no-input', 'Disable interactive terminal input for visualization') - .action(async (runId: string | undefined, options, command) => { - const commandOptionsWithGlobals = resolvedOptions(command, options); - assertOutputModeCompatible(commandOptionsWithGlobals); - await runIngestArgs( - context, - { - command: 'watch', - projectDir: resolveCommandProjectDir(command), - ...(runId ? { runId } : {}), - ...(commandOptionsWithGlobals.reportFile ? { reportFile: resolve(commandOptionsWithGlobals.reportFile) } : {}), - outputMode: watchOutputMode(commandOptionsWithGlobals), - ...inputMode(commandOptionsWithGlobals), - }, - commandOptions, - ); - }); - - ingest - .command('replay') - .description('Replay a stored ingest report through memory-flow output') - .argument('', 'Local ingest id, report id, run id, or job id') - .option('--report-file ', 'Bundle ingest report JSON file to render') - .addOption(new Option('--plain', 'Print plain text output').conflicts(['json', 'viz'])) - .addOption(new Option('--json', 'Print JSON output').conflicts(['plain', 'viz'])) - .addOption(new Option('--viz', 'Render memory-flow TUI output').conflicts(['plain', 'json'])) - .option('--no-input', 'Disable interactive terminal input for visualization') - .action(async (runId: string, options, command) => { - const commandOptionsWithGlobals = resolvedOptions(command, options); - assertOutputModeCompatible(commandOptionsWithGlobals); - await runIngestArgs( - context, - { - command: 'replay', - projectDir: resolveCommandProjectDir(command), - runId, - ...(commandOptionsWithGlobals.reportFile ? { reportFile: resolve(commandOptionsWithGlobals.reportFile) } : {}), - outputMode: outputMode(commandOptionsWithGlobals), - ...inputMode(commandOptionsWithGlobals), - }, - commandOptions, - ); - }); } diff --git a/packages/cli/src/commands/knowledge-commands.ts b/packages/cli/src/commands/knowledge-commands.ts index f8d716f7..d0c04a32 100644 --- a/packages/cli/src/commands/knowledge-commands.ts +++ b/packages/cli/src/commands/knowledge-commands.ts @@ -1,11 +1,9 @@ -import { type Command, Option } from '@commander-js/extra-typings'; +import type { Command } from '@commander-js/extra-typings'; import { - collectOption, type KtxCliCommandContext, parsePositiveIntegerOption, resolveCommandProjectDir, } from '../cli-program.js'; -import { wikiWriteCommandSchema } from '../command-schemas.js'; import type { KtxKnowledgeArgs } from '../knowledge.js'; import { profileMark } from '../startup-profile.js'; @@ -19,7 +17,7 @@ async function runKnowledgeArgs(context: KtxCliCommandContext, args: KtxKnowledg export function registerWikiCommands(program: Command, context: KtxCliCommandContext): void { const wiki = program .command('wiki') - .description('List, read, search, or write local wiki pages') + .description('List or search local wiki pages') .showHelpAfterError() .addHelpText( 'after', @@ -40,22 +38,6 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon }); }); - wiki - .command('read') - .description('Read one local wiki page') - .argument('', 'Wiki page key') - .option('--json', 'Print JSON output', false) - .option('--user-id ', 'Local user id', 'local') - .action(async (key: string, options: { userId: string; json?: boolean }, command) => { - await runKnowledgeArgs(context, { - command: 'read', - projectDir: resolveCommandProjectDir(command), - key, - userId: options.userId, - json: options.json, - }); - }); - wiki .command('search') .description('Search local wiki pages') @@ -73,31 +55,4 @@ export function registerWikiCommands(program: Command, context: KtxCliCommandCon ...(options.limit !== undefined ? { limit: options.limit } : {}), }); }); - - wiki - .command('write') - .description('Write one local wiki page') - .argument('', 'Wiki page key') - .option('--user-id ', 'Local user id', 'local') - .addOption(new Option('--scope ', 'global or user').choices(['global', 'user']).default('global')) - .requiredOption('--summary ', 'Wiki summary') - .requiredOption('--content ', 'Wiki content') - .option('--tag ', 'Wiki tag; repeatable', collectOption, []) - .option('--ref ', 'Wiki ref; repeatable', collectOption, []) - .option('--sl-ref ', 'Semantic-layer ref; repeatable', collectOption, []) - .action(async (key: string, options, command) => { - const args = wikiWriteCommandSchema.parse({ - command: 'write', - projectDir: resolveCommandProjectDir(command), - key, - scope: options.scope === 'user' ? 'USER' : 'GLOBAL', - userId: options.userId, - summary: options.summary, - content: options.content, - tags: options.tag, - refs: options.ref, - slRefs: options.slRef, - }); - await runKnowledgeArgs(context, args); - }); } diff --git a/packages/cli/src/dev.test.ts b/packages/cli/src/dev.test.ts index 1f3c3db9..4ec7f0b3 100644 --- a/packages/cli/src/dev.test.ts +++ b/packages/cli/src/dev.test.ts @@ -52,14 +52,14 @@ describe('dev Commander tree', () => { expect(testIo.stderr()).toBe(''); }); - it('keeps dev callable while hiding it from root command rows', async () => { + it('lists dev in root command rows', async () => { const testIo = makeIo(); await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0); - expect(testIo.stdout()).toContain('Advanced:'); - expect(testIo.stdout()).toContain('ktx dev'); - expect(testIo.stdout()).not.toContain('dev Low-level diagnostics'); + expect(testIo.stdout()).not.toContain('Advanced:'); + expect(testIo.stdout()).toContain('dev'); + expect(testIo.stdout()).toMatch(/Low-level project initialization and runtime\s+management/); expect(testIo.stderr()).toBe(''); }); @@ -132,9 +132,8 @@ describe('dev Commander tree', () => { ])('prints generated nested help for $argv', async ({ argv, expected }) => { const io = makeIo(); const doctor = vi.fn(async () => 0); - const ingest = vi.fn(async () => 0); - await expect(runKtxCli(argv, io.io, { doctor, ingest })).resolves.toBe(0); + await expect(runKtxCli(argv, io.io, { doctor })).resolves.toBe(0); for (const text of expected) { expect(io.stdout()).toContain(text); @@ -145,28 +144,25 @@ describe('dev Commander tree', () => { } expect(io.stderr()).toBe(''); expect(doctor).not.toHaveBeenCalled(); - expect(ingest).not.toHaveBeenCalled(); }); - it('keeps legacy adapter-backed ingest run callable but hidden from ingest help', async () => { + it('rejects removed adapter-backed ingest run and keeps it out of ingest help', async () => { const helpIo = makeIo(); const runIo = makeIo(); - const ingest = vi.fn(async () => 0); + const publicIngest = vi.fn(async () => 0); - await expect(runKtxCli(['ingest', '--help'], helpIo.io, { ingest })).resolves.toBe(0); + await expect(runKtxCli(['ingest', '--help'], helpIo.io, { publicIngest })).resolves.toBe(0); await expect( runKtxCli( ['ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase', '--project-dir', '/tmp/project'], runIo.io, - { ingest }, + { publicIngest }, ), - ).resolves.toBe(0); + ).resolves.toBe(1); expect(helpIo.stdout()).not.toMatch(/^ run\s/m); - expect(ingest).toHaveBeenCalledWith( - expect.objectContaining({ command: 'run', connectionId: 'warehouse', adapter: 'metabase' }), - runIo.io, - ); + expect(runIo.stderr()).toMatch(/unknown command|error:/); + expect(publicIngest).not.toHaveBeenCalled(); }); it.each([ @@ -177,17 +173,17 @@ describe('dev Commander tree', () => { { argv: ['scan', 'warehouse', '--project-dir', '/tmp/project', '--mode', 'relationships'] }, ])('rejects removed top-level scan command $argv', async ({ argv }) => { const io = makeIo(); - const ingest = vi.fn(async () => 0); + const publicIngest = vi.fn(async () => 0); - await expect(runKtxCli(argv, io.io, { ingest })).resolves.toBe(1); + await expect(runKtxCli(argv, io.io, { publicIngest })).resolves.toBe(1); - expect(ingest).not.toHaveBeenCalled(); + expect(publicIngest).not.toHaveBeenCalled(); expect(io.stderr()).toMatch(/unknown command|error:/); }); - it('dispatches top-level ingest run through the low-level ingest Commander registration', async () => { + it('rejects top-level ingest run through the removed low-level ingest registration', async () => { const io = makeIo(); - const ingest = vi.fn(async () => 0); + const publicIngest = vi.fn(async () => 0); await expect( runKtxCli( @@ -203,24 +199,11 @@ describe('dev Commander tree', () => { '--json', ], io.io, - { ingest }, + { publicIngest }, ), - ).resolves.toBe(0); + ).resolves.toBe(1); - expect(ingest).toHaveBeenCalledWith( - { - command: 'run', - projectDir: '/tmp/project', - connectionId: 'warehouse', - adapter: 'metabase', - sourceDir: undefined, - databaseIntrospectionUrl: undefined, - cliVersion: '0.0.0-private', - runtimeInstallPolicy: 'prompt', - outputMode: 'json', - }, - io.io, - ); - expect(io.stderr()).toBe(''); + expect(publicIngest).not.toHaveBeenCalled(); + expect(io.stderr()).toMatch(/unknown command|error:/); }); }); diff --git a/packages/cli/src/dev.ts b/packages/cli/src/dev.ts index 9391cc43..37865c57 100644 --- a/packages/cli/src/dev.ts +++ b/packages/cli/src/dev.ts @@ -8,7 +8,7 @@ profileMark('module:dev'); export function registerDevCommands(program: Command, context: KtxCliCommandContext): void { const dev = program - .command('dev', { hidden: true }) + .command('dev') .description('Low-level project initialization and runtime management') .showHelpAfterError(); diff --git a/packages/cli/src/example-smoke.test.ts b/packages/cli/src/example-smoke.test.ts index f1670544..95c1031f 100644 --- a/packages/cli/src/example-smoke.test.ts +++ b/packages/cli/src/example-smoke.test.ts @@ -71,7 +71,6 @@ describe('standalone local warehouse example', () => { it('runs local CLI commands against the copied example project', async () => { const projectDir = await copyExampleProject(tempDir); - const sourceDir = join(projectDir, 'source'); const knowledgeList = await runBuiltCli(['wiki', 'search', 'revenue', '--json', '--project-dir', projectDir]); expect(knowledgeList).toMatchObject({ code: 0, stderr: '' }); @@ -105,19 +104,13 @@ describe('standalone local warehouse example', () => { const ingest = await runBuiltCli([ 'ingest', 'run', - '--project-dir', - projectDir, '--connection-id', 'warehouse', '--adapter', 'fake', - '--source-dir', - sourceDir, ]); expect(ingest).toMatchObject({ code: 1, stdout: '' }); - expect(ingest.stderr).toContain( - 'ktx ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner', - ); + expect(ingest.stderr).toContain("unknown command 'run'"); }, 30_000); }); diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts index bc16c5fd..8d257e0f 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/src/index.test.ts @@ -124,7 +124,7 @@ describe('runKtxCli', () => { expect(testIo.stdout()).toContain('Usage: ktx [options] [command]'); expect(testIo.stdout()).toContain('KTX data agent context layer CLI'); - for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'status']) { + for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'status', 'dev']) { expect(testIo.stdout()).toContain(`${command}`); } expect(testIo.stdout()).not.toMatch(/^ scan\s/m); @@ -135,71 +135,60 @@ describe('runKtxCli', () => { expect(testIo.stdout()).toContain('KTX_PROJECT_DIR'); expect(testIo.stdout()).toContain('--debug'); expect(testIo.stdout()).not.toContain('--' + 'verbose'); - expect(testIo.stdout()).toContain('Advanced:'); - expect(testIo.stdout()).toContain('ktx dev'); + expect(testIo.stdout()).not.toContain('Advanced:'); expect(testIo.stderr()).toBe(''); }); - it('routes public wiki read and write commands', async () => { + it('routes supported public wiki commands', async () => { const knowledge = vi.fn(async () => 0); - const readIo = makeIo(); - await expect(runKtxCli(['--project-dir', tempDir, 'wiki', 'read', 'revenue', '--json'], readIo.io, { knowledge })) + const listIo = makeIo(); + await expect(runKtxCli(['--project-dir', tempDir, 'wiki', 'list', '--json'], listIo.io, { knowledge })) .resolves.toBe(0); expect(knowledge).toHaveBeenCalledWith( { - command: 'read', + command: 'list', projectDir: tempDir, - key: 'revenue', userId: 'local', json: true, }, - readIo.io, + listIo.io, ); - const writeIo = makeIo(); + const searchIo = makeIo(); await expect( - runKtxCli( - [ - '--project-dir', - tempDir, - 'wiki', - 'write', - 'revenue', - '--scope', - 'user', - '--summary', - 'Revenue', - '--content', - 'Revenue.', - '--tag', - 'finance', - '--ref', - 'https://example.com/revenue', - '--sl-ref', - 'orders', - ], - writeIo.io, - { knowledge }, - ), + runKtxCli(['--project-dir', tempDir, 'wiki', 'search', 'revenue', '--limit', '5'], searchIo.io, { knowledge }), ).resolves.toBe(0); expect(knowledge).toHaveBeenLastCalledWith( { - command: 'write', + command: 'search', projectDir: tempDir, - key: 'revenue', - scope: 'USER', + query: 'revenue', userId: 'local', - summary: 'Revenue', - content: 'Revenue.', - tags: ['finance'], - refs: ['https://example.com/revenue'], - slRefs: ['orders'], + json: false, + limit: 5, }, - writeIo.io, + searchIo.io, ); }); + it('rejects removed public wiki read and write commands', async () => { + const knowledge = vi.fn(async () => 0); + + for (const argv of [ + ['--project-dir', tempDir, 'wiki', 'read', 'revenue', '--json'], + ['--project-dir', tempDir, 'wiki', 'write', 'revenue', '--summary', 'Revenue', '--content', 'Revenue.'], + ]) { + const io = makeIo(); + + await expect(runKtxCli(argv, io.io, { knowledge })).resolves.toBe(1); + + expect(io.stderr()).toMatch(/unknown command|error:/); + } + + expect(knowledge).not.toHaveBeenCalled(); + }); + it('rejects removed public sl read/write commands', async () => { const sl = vi.fn(async () => 0); @@ -334,23 +323,15 @@ describe('runKtxCli', () => { expect(testIo.stderr()).toBe(`Project: ${tempDir}\n`); }); - it('skips the project directory line for JSON and TUI output modes', async () => { - const ingest = vi.fn(async () => 0); + it('skips the project directory line for JSON output mode', async () => { + const publicIngest = vi.fn(async () => 0); const jsonIo = makeIo(); - const vizIo = makeIo({ stdoutIsTty: true }); - await expect(runKtxCli(['--project-dir', tempDir, 'ingest', 'status', 'run-1', '--json'], jsonIo.io, { ingest })) - .resolves.toBe(0); await expect( - runKtxCli( - ['--project-dir', tempDir, 'ingest', 'status', 'run-1', '--viz'], - vizIo.io, - { ingest }, - ), + runKtxCli(['--project-dir', tempDir, 'ingest', 'warehouse', '--json'], jsonIo.io, { publicIngest }), ).resolves.toBe(0); expect(jsonIo.stderr()).toBe(''); - expect(vizIo.stderr()).toBe(''); }); it('documents runtime stop all in command help', async () => { @@ -692,60 +673,24 @@ describe('runKtxCli', () => { expect(testIo.stderr()).toMatch(/option '--(deep|fast)' cannot be used with option '--(fast|deep)'/); }); - it('prints ingest watch help from Commander', async () => { + it.each([ + { argv: ['ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'] }, + { argv: ['ingest', 'run', '--help'] }, + { argv: ['ingest', 'status'] }, + { argv: ['ingest', 'status', 'run-1', '--json', '--no-input'] }, + { argv: ['ingest', 'watch'] }, + { argv: ['ingest', 'watch', '--help'] }, + { argv: ['ingest', 'replay', 'run-1'] }, + { argv: ['ingest', 'replay', '--help'] }, + { argv: ['--project-dir', '/tmp/project', 'ingest', 'status', 'run-1'] }, + ])('rejects removed ingest subcommand $argv', async ({ argv }) => { const testIo = makeIo(); - const ingest = vi.fn(async () => 0); + const publicIngest = vi.fn(async () => 0); - await expect(runKtxCli(['ingest', 'watch', '--help'], testIo.io, { ingest })).resolves.toBe(0); + await expect(runKtxCli(argv, testIo.io, { publicIngest })).resolves.toBe(1); - expect(testIo.stdout()).toContain('Usage: ktx ingest watch [options] [runId]'); - expect(testIo.stdout()).toContain('[runId]'); - expect(testIo.stdout()).toContain('--project-dir '); - expect(testIo.stdout()).toContain('--json'); - expect(testIo.stdout()).toContain('--no-input'); - expect(testIo.stderr()).toBe(''); - expect(ingest).not.toHaveBeenCalled(); - }); - - it('dispatches ingest status and watch through Commander', async () => { - const statusIo = makeIo(); - const watchIo = makeIo(); - const ingest = vi.fn(async () => 0); - - await expect( - runKtxCli(['--project-dir', tempDir, 'ingest', 'status', 'run-1', '--json', '--no-input'], statusIo.io, { - ingest, - }), - ).resolves.toBe(0); - await expect( - runKtxCli(['--project-dir', tempDir, 'ingest', 'watch', '--no-input'], watchIo.io, { - ingest, - }), - ).resolves.toBe(0); - - expect(ingest).toHaveBeenNthCalledWith( - 1, - { - command: 'status', - projectDir: tempDir, - runId: 'run-1', - outputMode: 'json', - inputMode: 'disabled', - }, - statusIo.io, - ); - expect(ingest).toHaveBeenNthCalledWith( - 2, - { - command: 'watch', - projectDir: tempDir, - outputMode: 'viz', - inputMode: 'disabled', - }, - watchIo.io, - ); - expect(statusIo.stderr()).toBe(''); - expect(watchIo.stderr()).toBe(''); + expect(testIo.stderr()).toMatch(/unknown command|error:/); + expect(publicIngest).not.toHaveBeenCalled(); }); it('rejects standalone demo commands', async () => { @@ -781,9 +726,9 @@ describe('runKtxCli', () => { it('prints ingest help without invoking ingest execution', async () => { const testIo = makeIo(); - const ingest = vi.fn(); + const publicIngest = vi.fn(); - await expect(runKtxCli(['ingest', '--help'], testIo.io, { ingest })).resolves.toBe(0); + await expect(runKtxCli(['ingest', '--help'], testIo.io, { publicIngest })).resolves.toBe(0); expect(testIo.stdout()).toContain('Usage: ktx ingest [options] [connectionId]'); expect(testIo.stdout()).toContain('Build or inspect KTX context'); @@ -793,37 +738,36 @@ describe('runKtxCli', () => { expect(testIo.stdout()).toContain('--query-history'); expect(testIo.stdout()).toContain('--no-query-history'); expect(testIo.stdout()).toContain('--query-history-window-days '); - expect(testIo.stdout()).toContain('status'); - expect(testIo.stdout()).toContain('replay'); + expect(testIo.stdout()).not.toMatch(/^ status\s/m); + expect(testIo.stdout()).not.toMatch(/^ replay\s/m); expect(testIo.stdout()).not.toMatch(/^ run\s/m); expect(testIo.stdout()).not.toMatch(/^ watch\s/m); expect(testIo.stderr()).toBe(''); - expect(ingest).not.toHaveBeenCalled(); + expect(publicIngest).not.toHaveBeenCalled(); }); - it('routes ingest run at the top level and rejects removed dev ingest', async () => { - const runIo = makeIo(); + it('rejects removed ingest run at the top level and under dev', async () => { + const rootRunIo = makeIo(); const devRunIo = makeIo(); - const ingest = vi.fn(async () => 0); + const publicIngest = vi.fn(async () => 0); await expect( - runKtxCli(['ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], runIo.io, { ingest }), - ).resolves.toBe(0); - await expect( - runKtxCli(['dev', 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], devRunIo.io, { - ingest, + runKtxCli(['ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], rootRunIo.io, { + publicIngest, }), ).resolves.toBe(1); - expect(ingest).toHaveBeenCalledWith( - expect.objectContaining({ command: 'run', connectionId: 'warehouse', adapter: 'metabase' }), - expect.anything(), - ); + await expect( + runKtxCli(['dev', 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], devRunIo.io, { + publicIngest, + }), + ).resolves.toBe(1); + expect(publicIngest).not.toHaveBeenCalled(); + expect(rootRunIo.stderr()).toMatch(/unknown command|error:/); expect(devRunIo.stderr()).toMatch(/unknown command|error:/); }); - it('rejects removed dev doctor while keeping ingest parser cases at the root', async () => { + it('rejects removed dev doctor and removed ingest parser cases', async () => { const doctor = vi.fn(async () => 0); - const ingest = vi.fn(async () => 0); const doctorIo = makeIo(); const ingestRunIo = makeIo(); const ingestReplayHelpIo = makeIo(); @@ -848,94 +792,15 @@ describe('runKtxCli', () => { '--no-input', ], ingestRunIo.io, - { ingest }, + {}, ), - ).resolves.toBe(0); - await expect(runKtxCli(['ingest', 'replay', '--help'], ingestReplayHelpIo.io, { ingest })).resolves.toBe(0); + ).resolves.toBe(1); + await expect(runKtxCli(['ingest', 'replay', '--help'], ingestReplayHelpIo.io)).resolves.toBe(1); expect(doctor).not.toHaveBeenCalled(); - expect(ingest).toHaveBeenCalledWith( - { - command: 'run', - projectDir: tempDir, - connectionId: 'warehouse', - adapter: 'fake', - sourceDir: tempDir, - databaseIntrospectionUrl: undefined, - cliVersion: '0.0.0-private', - runtimeInstallPolicy: 'prompt', - debugLlmRequestFile: `${tempDir}/debug.jsonl`, - outputMode: 'json', - inputMode: 'disabled', - }, - ingestRunIo.io, - ); - expect(ingestReplayHelpIo.stdout()).toContain('Usage: ktx ingest replay [options] '); - expect(ingestReplayHelpIo.stdout()).toContain(''); expect(doctorIo.stderr()).toMatch(/unknown command|error:/); - expect(ingestRunIo.stderr()).toBe(''); - expect(ingestReplayHelpIo.stderr()).toBe(''); - }); - - it('routes ingest managed runtime install policy separately from visualization input mode', async () => { - const autoIo = makeIo(); - const nonInteractiveIo = makeIo(); - const ingest = vi.fn(async () => 0); - - await expect( - runKtxCli( - [ - 'ingest', - 'run', - '--project-dir', - tempDir, - '--connection-id', - 'warehouse', - '--adapter', - 'looker', - '--yes', - ], - autoIo.io, - { ingest }, - ), - ).resolves.toBe(0); - await expect( - runKtxCli( - [ - 'ingest', - 'run', - '--project-dir', - tempDir, - '--connection-id', - 'warehouse', - '--adapter', - 'looker', - '--yes', - '--no-input', - ], - nonInteractiveIo.io, - { ingest }, - ), - ).resolves.toBe(0); - - expect(ingest).toHaveBeenCalledWith( - expect.objectContaining({ - command: 'run', - cliVersion: '0.0.0-private', - runtimeInstallPolicy: 'auto', - }), - autoIo.io, - ); - expect(ingest).toHaveBeenCalledWith( - expect.objectContaining({ - command: 'run', - cliVersion: '0.0.0-private', - runtimeInstallPolicy: 'auto', - inputMode: 'disabled', - }), - nonInteractiveIo.io, - ); - expect(nonInteractiveIo.stderr()).toBe(`Project: ${tempDir}\n`); + expect(ingestRunIo.stderr()).toMatch(/unknown command|error:/); + expect(ingestReplayHelpIo.stderr()).toMatch(/unknown command|error:/); }); it('dispatches public connection through the existing connection implementation', async () => { @@ -1199,7 +1064,9 @@ describe('runKtxCli', () => { ).resolves.toBe(1); expect(setup).not.toHaveBeenCalled(); - expect(testIo.stderr()).toContain('"status" is reserved for ktx ingest status; choose a different connection id.'); + expect(testIo.stderr()).toContain( + '"status" is reserved for the KTX ingest command namespace; choose a different connection id.', + ); }); it('dispatches setup source flags', async () => { @@ -1573,12 +1440,12 @@ describe('runKtxCli', () => { { argv: ['scan', 'warehouse', '--mode', 'relationships'] }, ])('rejects removed top-level scan command $argv', async ({ argv }) => { const testIo = makeIo(); - const ingest = vi.fn().mockResolvedValue(0); + const publicIngest = vi.fn().mockResolvedValue(0); - await expect(runKtxCli(argv, testIo.io, { ingest })).resolves.toBe(1); + await expect(runKtxCli(argv, testIo.io, { publicIngest })).resolves.toBe(1); expect(testIo.stderr()).toMatch(/unknown command|error:/); - expect(ingest).not.toHaveBeenCalled(); + expect(publicIngest).not.toHaveBeenCalled(); }); it('rejects removed public serve command options before dispatch', async () => { @@ -1626,13 +1493,13 @@ describe('runKtxCli', () => { it('rejects removed dev command groups without invoking execution', async () => { for (const command of ['scan', 'ingest', 'mapping']) { const testIo = makeIo(); - const ingest = vi.fn().mockResolvedValue(0); + const publicIngest = vi.fn().mockResolvedValue(0); const sl = vi.fn().mockResolvedValue(0); - await expect(runKtxCli(['dev', command], testIo.io, { ingest, sl })).resolves.toBe(1); + await expect(runKtxCli(['dev', command], testIo.io, { publicIngest, sl })).resolves.toBe(1); expect(testIo.stderr()).toMatch(/unknown command|error:/); - expect(ingest).not.toHaveBeenCalled(); + expect(publicIngest).not.toHaveBeenCalled(); expect(sl).not.toHaveBeenCalled(); } }); @@ -1645,19 +1512,16 @@ describe('runKtxCli', () => { expect(testIo.stderr()).toMatch(/unknown command|error:/); }); - it('rejects mutually exclusive output modes before invoking runners', async () => { - const ingest = vi.fn(async () => 0); + it('rejects mutually exclusive public ingest output modes before invoking runners', async () => { + const publicIngest = vi.fn(async () => 0); - for (const argv of [ - ['ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'fake', '--json', '--plain'], - ['ingest', 'status', 'run-1', '--json', '--viz'], - ]) { - const testIo = makeIo(); - await expect(runKtxCli(argv, testIo.io, { ingest })).resolves.toBe(1); - expect(testIo.stderr()).toMatch(/conflict|cannot be used/i); - } + const testIo = makeIo(); + await expect(runKtxCli(['ingest', 'warehouse', '--json', '--plain'], testIo.io, { publicIngest })).resolves.toBe( + 1, + ); - expect(ingest).not.toHaveBeenCalled(); + expect(testIo.stderr()).toMatch(/conflict|cannot be used/i); + expect(publicIngest).not.toHaveBeenCalled(); }); it('does not expose root init after setup owns project creation', async () => { diff --git a/packages/cli/src/ingest.test.ts b/packages/cli/src/ingest.test.ts index 5198cca2..eb41ea04 100644 --- a/packages/cli/src/ingest.test.ts +++ b/packages/cli/src/ingest.test.ts @@ -311,7 +311,7 @@ describe('runKtxIngest', () => { expect(runIo.stdout()).toBe(''); expect(runIo.stderr()).toContain( - 'ktx ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.', + 'ktx ingest requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.', ); expect(runIo.stderr()).toContain( `ktx setup --project-dir ${projectDir} --anthropic-api-key-env ANTHROPIC_API_KEY --anthropic-model claude-sonnet-4-6 --no-input`, @@ -854,7 +854,7 @@ describe('runKtxIngest', () => { ).resolves.toBe(1); expect(io.stderr()).toContain('source-dir uploads are not supported for the Metabase fan-out adapter'); - expect(io.stderr()).not.toContain('ktx ingest run requires llm.provider.backend'); + expect(io.stderr()).not.toContain('ktx ingest requires llm.provider.backend'); expect(io.stdout()).toBe(''); }); diff --git a/packages/cli/src/ingest.ts b/packages/cli/src/ingest.ts index 4cf25b05..4718a926 100644 --- a/packages/cli/src/ingest.ts +++ b/packages/cli/src/ingest.ts @@ -35,7 +35,7 @@ import { profileMark } from './startup-profile.js'; profileMark('module:ingest'); -export type KtxIngestOutputMode = 'plain' | 'json' | 'viz'; +type KtxIngestOutputMode = 'plain' | 'json' | 'viz'; type KtxIngestInputMode = 'auto' | 'disabled'; export type KtxIngestArgs = diff --git a/packages/cli/src/knowledge.test.ts b/packages/cli/src/knowledge.test.ts index c4b3fdd9..2486d621 100644 --- a/packages/cli/src/knowledge.test.ts +++ b/packages/cli/src/knowledge.test.ts @@ -192,7 +192,7 @@ describe('runKtxKnowledge', () => { expect(searchIo.stdout()).toBe(''); expect(searchIo.stderr()).toContain('No local wiki pages found'); - expect(searchIo.stderr()).toContain('ktx wiki write'); + expect(searchIo.stderr()).toContain('ktx ingest '); }); it('uses configured embeddings for semantic wiki search', async () => { diff --git a/packages/cli/src/knowledge.ts b/packages/cli/src/knowledge.ts index 2e039dea..b4585c0e 100644 --- a/packages/cli/src/knowledge.ts +++ b/packages/cli/src/knowledge.ts @@ -113,7 +113,7 @@ export async function runKtxKnowledge( const pages = await listLocalKnowledgePages(project, { userId: args.userId }); if (pages.length === 0) { io.stderr.write( - `No local wiki pages found in ${project.projectDir}. Create one with \`ktx wiki write --summary --content \` or run ingest.\n`, + `No local wiki pages found in ${project.projectDir}. Add Markdown files under wiki/ or run \`ktx ingest \`.\n`, ); } else { io.stderr.write( diff --git a/packages/cli/src/print-command-tree.test.ts b/packages/cli/src/print-command-tree.test.ts index 67ca7aeb..86ef451e 100644 --- a/packages/cli/src/print-command-tree.test.ts +++ b/packages/cli/src/print-command-tree.test.ts @@ -12,7 +12,7 @@ describe('renderKtxCommandTree', () => { .filter((line) => /^ {2}[├└]── \S/.test(line)) .map((line) => line.replace(/^ {2}[├└]── /, '').trim().split(' ')[0]); - for (const expected of ['setup', 'connection', 'ingest', 'sl']) { + for (const expected of ['setup', 'connection', 'ingest', 'sl', 'dev']) { expect(topLevel).toContain(expected); } @@ -24,9 +24,15 @@ describe('renderKtxCommandTree', () => { expect(output).not.toContain('│ ├── metabase'); expect(output).not.toContain('│ ├── notion'); expect(output).not.toContain('scan '); + expect(output).not.toContain('│ ├── status'); + expect(output).not.toContain('│ ├── replay'); + expect(output).not.toContain('│ └── replay'); expect(output).not.toContain('│ ├── run'); expect(output).not.toContain('│ ├── watch'); expect(output).not.toContain('│ └── watch'); + expect(output).not.toContain('│ ├── read'); + expect(output).not.toContain('│ ├── write'); + expect(output).not.toContain('│ └── write'); }); it('ends with a single trailing newline', () => { diff --git a/packages/cli/src/project-dir.test.ts b/packages/cli/src/project-dir.test.ts index ed8bdeb3..9a6d0c5a 100644 --- a/packages/cli/src/project-dir.test.ts +++ b/packages/cli/src/project-dir.test.ts @@ -32,10 +32,9 @@ describe('project directory defaults', () => { const connection = vi.fn(async () => 0); const doctor = vi.fn(async () => 0); - const ingest = vi.fn(async () => 0); const publicIngest = vi.fn(async () => 0); const setup = vi.fn(async () => 0); - const deps: KtxCliDeps = { connection, doctor, ingest, publicIngest, setup }; + const deps: KtxCliDeps = { connection, doctor, publicIngest, setup }; const cases: Array<{ argv: string[]; @@ -55,12 +54,6 @@ describe('project directory defaults', () => { expected: { command: 'project', projectDir: '/tmp/ktx-env-project' }, expectedStderr: 'Project: /tmp/ktx-env-project\n', }, - { - argv: ['ingest', 'status', 'run-1'], - spy: ingest, - expected: { command: 'status', projectDir: '/tmp/ktx-env-project', runId: 'run-1', outputMode: 'plain' }, - expectedStderr: 'Project: /tmp/ktx-env-project\n', - }, { argv: ['setup', '--no-input'], spy: setup, @@ -87,31 +80,32 @@ describe('project directory defaults', () => { process.env.KTX_PROJECT_DIR = '/tmp/ktx-env-project'; const publicIngest = vi.fn(async () => 0); - const ingest = vi.fn(async () => 0); - const publicIngestIo = makeIo(); - const ingestIo = makeIo(); + const beforeCommandIo = makeIo(); + const afterCommandIo = makeIo(); await expect( - runKtxCli(['--project-dir', '/tmp/ktx-explicit-project', 'ingest', 'warehouse', '--no-input'], publicIngestIo.io, { + runKtxCli(['--project-dir', '/tmp/ktx-explicit-project', 'ingest', 'warehouse', '--no-input'], beforeCommandIo.io, { publicIngest, }), ).resolves.toBe(0); await expect( - runKtxCli(['ingest', 'status', 'run-1', '--project-dir=/tmp/ktx-explicit-project'], ingestIo.io, { - ingest, + runKtxCli(['ingest', 'warehouse', '--project-dir=/tmp/ktx-explicit-project', '--no-input'], afterCommandIo.io, { + publicIngest, }), ).resolves.toBe(0); - expect(publicIngest).toHaveBeenCalledWith( + expect(publicIngest).toHaveBeenNthCalledWith( + 1, expect.objectContaining({ command: 'run', projectDir: '/tmp/ktx-explicit-project' }), - publicIngestIo.io, + beforeCommandIo.io, ); - expect(ingest).toHaveBeenCalledWith( - expect.objectContaining({ command: 'status', projectDir: '/tmp/ktx-explicit-project' }), - ingestIo.io, + expect(publicIngest).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ command: 'run', projectDir: '/tmp/ktx-explicit-project' }), + afterCommandIo.io, ); - expect(publicIngestIo.stderr()).toBe('Project: /tmp/ktx-explicit-project\n'); - expect(ingestIo.stderr()).toBe('Project: /tmp/ktx-explicit-project\n'); + expect(beforeCommandIo.stderr()).toBe('Project: /tmp/ktx-explicit-project\n'); + expect(afterCommandIo.stderr()).toBe('Project: /tmp/ktx-explicit-project\n'); }); it('uses nearest ancestor containing ktx.yaml when no explicit or environment project-dir exists', async () => { diff --git a/packages/cli/src/public-ingest.test.ts b/packages/cli/src/public-ingest.test.ts index e259f4ea..b26d76dc 100644 --- a/packages/cli/src/public-ingest.test.ts +++ b/packages/cli/src/public-ingest.test.ts @@ -984,46 +984,4 @@ describe('runKtxPublicIngest', () => { ); }); - it('routes public status and watch to the ingest status renderer', async () => { - const runIngest = vi.fn(async () => 0); - const statusIo = makeIo(); - const watchIo = makeIo(); - - await expect( - runKtxPublicIngest( - { command: 'status', projectDir: '/tmp/ktx', json: false, inputMode: 'disabled' }, - statusIo.io, - { runIngest }, - ), - ).resolves.toBe(0); - await expect( - runKtxPublicIngest( - { command: 'watch', projectDir: '/tmp/ktx', runId: 'run-1', json: false, inputMode: 'auto' }, - watchIo.io, - { runIngest }, - ), - ).resolves.toBe(0); - - expect(runIngest).toHaveBeenNthCalledWith( - 1, - { - command: 'status', - projectDir: '/tmp/ktx', - outputMode: 'plain', - inputMode: 'disabled', - }, - statusIo.io, - ); - expect(runIngest).toHaveBeenNthCalledWith( - 2, - { - command: 'watch', - projectDir: '/tmp/ktx', - runId: 'run-1', - outputMode: 'viz', - inputMode: 'auto', - }, - watchIo.io, - ); - }); }); diff --git a/packages/cli/src/public-ingest.ts b/packages/cli/src/public-ingest.ts index fe041a0d..dd9687d7 100644 --- a/packages/cli/src/public-ingest.ts +++ b/packages/cli/src/public-ingest.ts @@ -23,26 +23,19 @@ type KtxPublicIngestQueryHistoryFlag = 'default' | 'enabled' | 'disabled'; type HistoricSqlDialect = 'postgres' | 'bigquery' | 'snowflake'; export type KtxPublicIngestArgs = - | { - command: 'run'; - projectDir: string; - targetConnectionId?: string; - all: boolean; - json: boolean; - inputMode: KtxPublicIngestInputMode; - depth?: KtxPublicIngestDepth; - queryHistory?: KtxPublicIngestQueryHistoryFlag; - queryHistoryWindowDays?: number; - scanMode?: Extract['mode']; - detectRelationships?: boolean; - } - | { - command: 'status' | 'watch'; - projectDir: string; - runId?: string; - json: boolean; - inputMode: KtxPublicIngestInputMode; - }; + { + command: 'run'; + projectDir: string; + targetConnectionId?: string; + all: boolean; + json: boolean; + inputMode: KtxPublicIngestInputMode; + depth?: KtxPublicIngestDepth; + queryHistory?: KtxPublicIngestQueryHistoryFlag; + queryHistoryWindowDays?: number; + scanMode?: Extract['mode']; + detectRelationships?: boolean; + }; export interface KtxPublicIngestPlanTarget { connectionId: string; @@ -775,20 +768,6 @@ export async function runKtxPublicIngest( io: KtxCliIo, deps: KtxPublicIngestDeps = {}, ): Promise { - if (args.command !== 'run') { - const { runKtxIngest } = await import('./ingest.js'); - return await (deps.runIngest ?? runKtxIngest)( - { - command: args.command, - projectDir: args.projectDir, - ...(args.runId ? { runId: args.runId } : {}), - outputMode: args.json ? 'json' : args.command === 'watch' ? 'viz' : 'plain', - inputMode: args.inputMode, - }, - io, - ); - } - const loadProject = deps.loadProject ?? loadKtxProject; const project = await loadProject({ projectDir: args.projectDir }); if (shouldUseForegroundContextBuildView(args, io)) { diff --git a/packages/cli/src/setup-databases.test.ts b/packages/cli/src/setup-databases.test.ts index 6fe577af..05f8aae0 100644 --- a/packages/cli/src/setup-databases.test.ts +++ b/packages/cli/src/setup-databases.test.ts @@ -1839,7 +1839,9 @@ describe('setup databases step', () => { ); expect(result.status).toBe('failed'); - expect(io.stderr()).toContain('"replay" is reserved for ktx ingest replay; choose a different connection id.'); + expect(io.stderr()).toContain( + '"replay" is reserved for the KTX ingest command namespace; choose a different connection id.', + ); }); it('leaves setup incomplete when databases are skipped', async () => { diff --git a/packages/cli/src/setup-sources.test.ts b/packages/cli/src/setup-sources.test.ts index afe6ec07..77c2342f 100644 --- a/packages/cli/src/setup-sources.test.ts +++ b/packages/cli/src/setup-sources.test.ts @@ -276,7 +276,9 @@ describe('setup sources step', () => { ); expect(result.status).toBe('failed'); - expect(io.stderr()).toContain('"status" is reserved for ktx ingest status; choose a different connection id.'); + expect(io.stderr()).toContain( + '"status" is reserved for the KTX ingest command namespace; choose a different connection id.', + ); }); it('uses selected Notion roots when root page ids are provided even if crawl mode says all accessible', async () => { diff --git a/packages/cli/src/standalone-smoke.test.ts b/packages/cli/src/standalone-smoke.test.ts index 1302953c..97ab259b 100644 --- a/packages/cli/src/standalone-smoke.test.ts +++ b/packages/cli/src/standalone-smoke.test.ts @@ -50,28 +50,6 @@ async function runBuiltCli(args: string[], options: { env?: NodeJS.ProcessEnv } } } -async function writeWarehouseConfig(projectDir: string): Promise { - await writeFile( - join(projectDir, 'ktx.yaml'), - [ - 'project: warehouse', - 'connections:', - ' warehouse:', - ' driver: postgres', - 'ingest:', - ' adapters:', - ' - fake', - '', - ].join('\n'), - 'utf-8', - ); -} - -async function writeSourceFixture(sourceDir: string): Promise { - await mkdir(join(sourceDir, 'orders'), { recursive: true }); - await writeFile(join(sourceDir, 'orders', 'orders.json'), '{"name":"orders"}\n', 'utf-8'); -} - function createSqliteWarehouse(dbPath: string): void { const db = new Database(dbPath); try { @@ -157,33 +135,23 @@ describe('standalone built ktx CLI smoke', () => { await rm(tempDir, { recursive: true, force: true }); }); - it('reports missing local ingest LLM config through the built binary', async () => { + it('rejects removed low-level ingest run through the built binary', async () => { const projectDir = join(tempDir, 'project'); - const sourceDir = join(tempDir, 'source'); const init = await runSetupNewProject(projectDir); expectProjectStderr(init, projectDir); expect(init.stdout).toContain(`Project: ${projectDir}`); - await writeWarehouseConfig(projectDir); - await writeSourceFixture(sourceDir); - const run = await runBuiltCli([ 'ingest', 'run', - '--project-dir', - projectDir, '--connection-id', 'warehouse', '--adapter', 'fake', - '--source-dir', - sourceDir, ]); expect(run).toMatchObject({ code: 1, stdout: '' }); - expect(run.stderr).toContain( - 'ktx ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner', - ); + expect(run.stderr).toContain("unknown command 'run'"); }); it('rejects the removed agent command through the built binary', async () => { diff --git a/packages/context/src/ingest/local-adapters.ts b/packages/context/src/ingest/local-adapters.ts index 3c503192..9c86b61e 100644 --- a/packages/context/src/ingest/local-adapters.ts +++ b/packages/context/src/ingest/local-adapters.ts @@ -228,7 +228,7 @@ export async function localPullConfigForAdapter( ): Promise { if (adapter.source === 'metabase') { throw new Error( - 'Metabase scheduled pulls fan out by mapping. Call runLocalMetabaseIngest() or use `ktx ingest run --adapter metabase --connection-id ` from the CLI.', + 'Metabase scheduled pulls fan out by mapping. Call runLocalMetabaseIngest() or use `ktx ingest ` from the CLI.', ); } const connection = project.config.connections[connectionId]; diff --git a/packages/context/src/ingest/local-bundle-runtime.test.ts b/packages/context/src/ingest/local-bundle-runtime.test.ts index e9be5a14..c6bd0539 100644 --- a/packages/context/src/ingest/local-bundle-runtime.test.ts +++ b/packages/context/src/ingest/local-bundle-runtime.test.ts @@ -56,7 +56,7 @@ describe('createLocalBundleIngestRuntime', () => { }), ).toThrow( [ - 'ktx ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.', + 'ktx ingest requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.', `Configure an Anthropic provider, then rerun ingest:`, ` ktx setup --project-dir ${project.projectDir} --anthropic-api-key-env ANTHROPIC_API_KEY --anthropic-model claude-sonnet-4-6 --no-input`, ].join('\n'), diff --git a/packages/context/src/ingest/local-bundle-runtime.ts b/packages/context/src/ingest/local-bundle-runtime.ts index 2a3c9943..047b7ee6 100644 --- a/packages/context/src/ingest/local-bundle-runtime.ts +++ b/packages/context/src/ingest/local-bundle-runtime.ts @@ -571,7 +571,7 @@ function nextLocalJobId(): string { function localIngestLlmProviderGuardMessage(projectDir: string): string { return [ - 'ktx ingest run requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.', + 'ktx ingest requires llm.provider.backend: anthropic, vertex, or gateway, or an injected agentRunner.', 'Configure an Anthropic provider, then rerun ingest:', ` ktx setup --project-dir ${projectDir} --anthropic-api-key-env ANTHROPIC_API_KEY --anthropic-model claude-sonnet-4-6 --no-input`, ].join('\n'); diff --git a/packages/context/src/project/config.test.ts b/packages/context/src/project/config.test.ts index 137bd7b8..832dd5af 100644 --- a/packages/context/src/project/config.test.ts +++ b/packages/context/src/project/config.test.ts @@ -10,7 +10,7 @@ connections: ${connectionId}: driver: postgres `), - ).toThrow(`"${connectionId}" is reserved for ktx ingest ${connectionId}`); + ).toThrow(`"${connectionId}" is reserved for the KTX ingest command namespace`); }); it('builds the default standalone project config', () => { diff --git a/packages/context/src/project/config.ts b/packages/context/src/project/config.ts index e655ca02..0c097e4f 100644 --- a/packages/context/src/project/config.ts +++ b/packages/context/src/project/config.ts @@ -113,10 +113,10 @@ function isRecord(value: unknown): value is Record { } const RESERVED_INGEST_CONNECTION_IDS = new Map([ - ['status', 'ktx ingest status'], - ['replay', 'ktx ingest replay'], - ['run', 'ktx ingest run'], - ['watch', 'ktx ingest watch'], + ['status', 'the KTX ingest command namespace'], + ['replay', 'the KTX ingest command namespace'], + ['run', 'the KTX ingest command namespace'], + ['watch', 'the KTX ingest command namespace'], ]); export function reservedKtxIngestConnectionIdMessage(connectionId: string): string | null { diff --git a/packages/context/src/scan/local-scan.test.ts b/packages/context/src/scan/local-scan.test.ts index 6c3e877f..2d912c54 100644 --- a/packages/context/src/scan/local-scan.test.ts +++ b/packages/context/src/scan/local-scan.test.ts @@ -120,6 +120,22 @@ async function writeLiveDatabaseConfig(projectDir: string): Promise { ); } +async function writeDatabaseConfigWithoutIngestAdapters(projectDir: string): Promise { + await writeFile( + join(projectDir, 'ktx.yaml'), + [ + 'project: warehouse', + 'connections:', + ' warehouse:', + ' driver: postgres', + ' url: env:DATABASE_URL', + ' readonly: true', + '', + ].join('\n'), + 'utf-8', + ); +} + function fetchOnlyAdapter(options: { extractedAt?: () => string } = {}): SourceAdapter { return { source: 'live-database', @@ -244,6 +260,27 @@ describe('local scan', () => { }); }); + it('runs a structural database scan when live-database is not listed in ktx.yaml', async () => { + await writeDatabaseConfigWithoutIngestAdapters(project.projectDir); + project = await loadKtxProject({ projectDir: project.projectDir }); + + const result = await runLocalScan({ + project, + adapters: [fetchOnlyAdapter()], + connectionId: 'warehouse', + jobId: 'scan-run-without-public-adapter', + now: () => new Date('2026-04-29T09:10:00.000Z'), + }); + + expect(result.report).toMatchObject({ + connectionId: 'warehouse', + runId: 'scan-run-without-public-adapter', + artifactPaths: { + reportPath: 'raw-sources/warehouse/live-database/2026-04-29-091000-scan-run-without-public-adapter/scan-report.json', + }, + }); + }); + it('reuses scan report and raw-source paths when the same local scan run id is retried', async () => { const first = await runLocalScan({ project, diff --git a/packages/context/src/scan/local-scan.ts b/packages/context/src/scan/local-scan.ts index 7f3c00a0..362c3b2c 100644 --- a/packages/context/src/scan/local-scan.ts +++ b/packages/context/src/scan/local-scan.ts @@ -342,6 +342,22 @@ function createFilteredConnector(connector: KtxScanConnector, enabledTables: Set }; } +function withInternalLiveDatabaseAdapter(project: KtxLocalProject): KtxLocalProject { + if (project.config.ingest.adapters.includes(LIVE_DATABASE_ADAPTER)) { + return project; + } + return { + ...project, + config: { + ...project.config, + ingest: { + ...project.config.ingest, + adapters: [...project.config.ingest.adapters, LIVE_DATABASE_ADAPTER], + }, + }, + }; +} + export async function runLocalScan(options: RunLocalScanOptions): Promise { const mode = options.mode ?? 'structural'; assertSupportedMode(mode); @@ -367,7 +383,7 @@ export async function runLocalScan(options: RunLocalScanOptions): Promise { assert.match(ingestReference, /--query-history-window-days /); assert.match(buildingContext, /ktx ingest /); assert.match(buildingContext, /ktx ingest --all/); - assert.match(buildingContext, /ktx ingest replay /); assert.match(contextSources, /ktx ingest /); assert.match(contextAsCode, /ktx ingest --all --no-input/); assert.match(quickstart, /schema context/); @@ -272,11 +271,15 @@ describe('standalone example docs', () => { assert.doesNotMatch(cliMeta, /ktx-scan/); assert.doesNotMatch(ingestReference, /ktx ingest run/); + assert.doesNotMatch(ingestReference, /ktx ingest status/); + assert.doesNotMatch(ingestReference, /ktx ingest replay/); assert.doesNotMatch(ingestReference, /--adapter/); assert.doesNotMatch(ingestReference, /ktx ingest watch/); assert.doesNotMatch(ingestReference, /live-database/); assert.doesNotMatch(devReference, /ktx scan/); assert.doesNotMatch(buildingContext, /ktx ingest watch/); + assert.doesNotMatch(buildingContext, /ktx ingest status/); + assert.doesNotMatch(buildingContext, /ktx ingest replay/); assert.doesNotMatch(buildingContext, /historic-sql/); assert.doesNotMatch(buildingContext, /live-database/); assert.doesNotMatch(contextSources, /ktx ingest run --connection-id/); diff --git a/scripts/installed-live-database-smoke.mjs b/scripts/installed-live-database-smoke.mjs index bcc5fb0b..f31d413a 100644 --- a/scripts/installed-live-database-smoke.mjs +++ b/scripts/installed-live-database-smoke.mjs @@ -113,10 +113,6 @@ export function buildLiveDatabaseIngestArgs(projectDir, _databaseIntrospectionUr ]; } -export function buildLiveDatabaseStatusArgs(projectDir, runId) { - return ['exec', 'ktx', 'ingest', 'status', '--project-dir', projectDir, runId]; -} - async function run(command, args, options = {}) { process.stdout.write(`$ ${command} ${args.join(' ')}\n`); return new Promise((resolve) => { @@ -167,7 +163,7 @@ function requireOutput(label, result, pattern) { function getRunId(stdout) { const match = stdout.match(/^Run: (.+)$/m); if (!match) { - throw new Error(`ingest run output did not include a run id\nstdout:\n${stdout}`); + throw new Error(`ingest output did not include a run id\nstdout:\n${stdout}`); } return match[1]; } @@ -322,16 +318,6 @@ async function main() { requireOutput('ktx ingest warehouse --fast', ingestRun, /Database schema/); const runId = getRunId(ingestRun.stdout); - const ingestStatus = await run('pnpm', buildLiveDatabaseStatusArgs(projectDir, runId), { - cwd: cleanInstallDir, - env: managedRuntimeEnv(cleanInstallDir), - timeout: 30_000, - }); - requireSuccess('ktx ingest status live-database', ingestStatus); - requireOutput('ktx ingest status live-database', ingestStatus, new RegExp(`Run: ${runId}`)); - requireOutput('ktx ingest status live-database', ingestStatus, /Status: done/); - requireOutput('ktx ingest status live-database', ingestStatus, /Raw files: 4/); - requireOutput('ktx ingest status live-database', ingestStatus, /Work units: 2/); await assertPathExists(join(projectDir, '.ktx', 'db.sqlite'), 'SQLite local ingest state'); process.stdout.write(`Installed live-database artifact smoke passed: ${runId}\n`); } finally { diff --git a/scripts/installed-live-database-smoke.test.mjs b/scripts/installed-live-database-smoke.test.mjs index aad69d8b..3eecf053 100644 --- a/scripts/installed-live-database-smoke.test.mjs +++ b/scripts/installed-live-database-smoke.test.mjs @@ -5,7 +5,6 @@ import { buildDockerRunArgs, buildKtxYaml, buildLiveDatabaseIngestArgs, - buildLiveDatabaseStatusArgs, buildPostgresUrl, buildPostgresReadyArgs, buildSeedSql, @@ -95,7 +94,7 @@ describe('installed live-database artifact smoke helpers', () => { ]); }); - it('builds installed CLI public database ingest and status commands', () => { + it('builds the installed CLI public database ingest command', () => { assert.deepEqual(buildLiveDatabaseIngestArgs('/tmp/project', 'http://127.0.0.1:8765'), [ 'exec', 'ktx', @@ -107,14 +106,5 @@ describe('installed live-database artifact smoke helpers', () => { '--no-input', ]); - assert.deepEqual(buildLiveDatabaseStatusArgs('/tmp/project', 'local-run-1'), [ - 'exec', - 'ktx', - 'ingest', - 'status', - '--project-dir', - '/tmp/project', - 'local-run-1', - ]); }); }); diff --git a/scripts/package-artifacts.mjs b/scripts/package-artifacts.mjs index c7575516..66fe1e5d 100644 --- a/scripts/package-artifacts.mjs +++ b/scripts/package-artifacts.mjs @@ -561,7 +561,7 @@ function requireIncludes(values, expected, label) { function getRunId(stdout) { const match = stdout.match(/^Run: (.+)$/m); - assert.ok(match, 'ingest run output did not include a run id'); + assert.ok(match, 'ingest output did not include a run id'); return match[1]; } @@ -588,7 +588,6 @@ process.env.KTX_RUNTIME_ROOT = join(root, 'managed-runtime'); let daemonStarted = false; try { const projectDir = join(root, 'project'); - const sourceDir = join(root, 'source'); const version = await run('pnpm', ['exec', 'ktx', '--version']); requireSuccess('ktx public package version', version); @@ -842,27 +841,8 @@ try { const enrichedScanRunId = getRunId(enrichedScan.stdout); process.stdout.write('ktx ingest deep verified: ' + enrichedScanRunId + '\\n'); - await mkdir(join(sourceDir, 'orders'), { recursive: true }); - await writeFile(join(sourceDir, 'orders', 'orders.json'), '{"name":"orders"}\\n', 'utf-8'); - - const ingestRun = await run('pnpm', ['exec', 'ktx', 'ingest', 'run', - '--project-dir', - projectDir, - '--connection-id', - 'warehouse', - '--adapter', - 'fake', - '--source-dir', - sourceDir, - ]); - assert.equal(ingestRun.code, 1, 'ktx ingest run without an LLM provider must fail'); - assert.match( - ingestRun.stderr, - /ktx ingest run requires llm\\.provider\\.backend: anthropic, vertex, or gateway, or an injected agentRunner/, - ); - await access(join(projectDir, '.ktx', 'db.sqlite')); - process.stdout.write('ktx ingest provider guard verified\\n'); + process.stdout.write('ktx ingest state verified\\n'); } finally { if (daemonStarted) { await run('pnpm', ['exec', 'ktx', 'dev', 'runtime', 'stop']); diff --git a/scripts/package-artifacts.test.mjs b/scripts/package-artifacts.test.mjs index 53308d35..0d3c71d6 100644 --- a/scripts/package-artifacts.test.mjs +++ b/scripts/package-artifacts.test.mjs @@ -495,11 +495,11 @@ describe('verification snippets', () => { assert.match(source, /ktx ingest deep verified/); assert.match(source, /enrichment:/); assert.match(source, /mode: deterministic/); - assert.match(source, /run\('pnpm', \['exec', 'ktx', 'ingest', 'run'/); + assert.doesNotMatch(source, /run\('pnpm', \['exec', 'ktx', 'ingest', 'run'/); assert.match(source, /access\(join\(projectDir, '\.ktx', 'db\.sqlite'\)\)/); assert.match(source, /SQLite wiki index/); - assert.match(source, /ktx ingest run requires llm\\.provider\\.backend: anthropic, vertex, or gateway/); - assert.match(source, /ktx ingest provider guard verified/); + assert.doesNotMatch(source, /ktx ingest run requires llm\\.provider\\.backend: anthropic, vertex, or gateway/); + assert.match(source, /ktx ingest state verified/); }); describe('npmCliSmokeSource', () => {