@kaelio/ktx 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/assets/python/{kaelio_ktx-0.1.1-py3-none-any.whl → kaelio_ktx-0.2.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/admin-reindex.d.ts +15 -0
  4. package/dist/admin-reindex.js +168 -0
  5. package/dist/admin-reindex.test.js +116 -0
  6. package/dist/{dev.d.ts → admin.d.ts} +1 -1
  7. package/dist/{dev.js → admin.js} +14 -12
  8. package/dist/admin.test.d.ts +1 -0
  9. package/dist/{dev.test.js → admin.test.js} +36 -31
  10. package/dist/cli-program.js +7 -7
  11. package/dist/cli-program.test.js +1 -1
  12. package/dist/cli-runtime.d.ts +2 -0
  13. package/dist/commands/connection-commands.js +11 -10
  14. package/dist/commands/connection-selection.d.ts +11 -0
  15. package/dist/commands/connection-selection.js +9 -0
  16. package/dist/commands/ingest-commands.js +32 -26
  17. package/dist/commands/knowledge-commands.js +17 -28
  18. package/dist/commands/mcp-commands.js +17 -11
  19. package/dist/commands/sl-commands.js +27 -32
  20. package/dist/doctor.test.js +4 -4
  21. package/dist/example-smoke.test.js +3 -3
  22. package/dist/index.test.js +76 -60
  23. package/dist/io/print-list.test.js +4 -4
  24. package/dist/knowledge.js +1 -1
  25. package/dist/managed-python-command.js +2 -2
  26. package/dist/managed-python-command.test.js +3 -3
  27. package/dist/managed-python-runtime.d.ts +1 -1
  28. package/dist/managed-python-runtime.js +3 -3
  29. package/dist/managed-python-runtime.test.js +2 -2
  30. package/dist/memory-flow-tui.test.js +2 -2
  31. package/dist/next-steps.d.ts +6 -6
  32. package/dist/next-steps.js +4 -4
  33. package/dist/next-steps.test.js +5 -5
  34. package/dist/print-command-tree.test.js +1 -1
  35. package/dist/public-ingest.js +3 -5
  36. package/dist/public-ingest.test.js +7 -3
  37. package/dist/runtime.test.js +1 -1
  38. package/dist/scan.test.js +2 -2
  39. package/dist/setup-agents.js +3 -3
  40. package/dist/setup-agents.test.js +1 -1
  41. package/dist/setup-embeddings.js +1 -1
  42. package/dist/setup-embeddings.test.js +3 -3
  43. package/dist/setup-runtime.test.js +2 -2
  44. package/dist/sl.js +1 -1
  45. package/dist/standalone-smoke.test.js +6 -2
  46. package/node_modules/@ktx/context/dist/index-sync/index.d.ts +2 -0
  47. package/node_modules/@ktx/context/dist/index-sync/index.js +1 -0
  48. package/node_modules/@ktx/context/dist/index-sync/reindex.d.ts +20 -0
  49. package/node_modules/@ktx/context/dist/index-sync/reindex.js +141 -0
  50. package/node_modules/@ktx/context/dist/index-sync/reindex.test.d.ts +1 -0
  51. package/node_modules/@ktx/context/dist/index-sync/reindex.test.js +139 -0
  52. package/node_modules/@ktx/context/dist/index-sync/types.d.ts +29 -0
  53. package/node_modules/@ktx/context/dist/index-sync/types.js +1 -0
  54. package/node_modules/@ktx/context/dist/index.d.ts +1 -0
  55. package/node_modules/@ktx/context/dist/index.js +1 -0
  56. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +3 -0
  57. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +2 -2
  58. package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +2 -2
  59. package/node_modules/@ktx/context/dist/memory/local-memory.js +9 -3
  60. package/node_modules/@ktx/context/dist/sl/ports.d.ts +3 -3
  61. package/node_modules/@ktx/context/dist/sl/sl-search.service.d.ts +3 -2
  62. package/node_modules/@ktx/context/dist/sl/sl-search.service.js +47 -45
  63. package/node_modules/@ktx/context/dist/sl/sl-search.service.test.js +61 -0
  64. package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.d.ts +4 -3
  65. package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.js +15 -5
  66. package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.test.js +24 -0
  67. package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.d.ts +3 -2
  68. package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.js +62 -51
  69. package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.test.js +59 -3
  70. package/node_modules/@ktx/context/dist/wiki/ports.d.ts +3 -3
  71. package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.d.ts +33 -0
  72. package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.js +155 -2
  73. package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.test.js +26 -0
  74. package/node_modules/@ktx/context/package.json +5 -0
  75. package/package.json +1 -1
  76. /package/dist/{dev.test.d.ts → admin-reindex.test.d.ts} +0 -0
@@ -0,0 +1,9 @@
1
+ export function resolveConnectionSelection(input) {
2
+ if (input.all && input.connectionId !== undefined) {
3
+ throw new Error('--all cannot be combined with a connection id argument');
4
+ }
5
+ if (input.connectionId !== undefined) {
6
+ return { kind: 'single', connectionId: input.connectionId };
7
+ }
8
+ return { kind: 'all' };
9
+ }
@@ -2,32 +2,59 @@ import { Option } from '@commander-js/extra-typings';
2
2
  import { collectOption, parsePositiveIntegerOption, resolveCommandProjectDir, } from '../cli-program.js';
3
3
  import { runtimeInstallPolicyFromFlags } from '../managed-python-command.js';
4
4
  import { profileMark } from '../startup-profile.js';
5
+ import { resolveConnectionSelection } from './connection-selection.js';
5
6
  profileMark('module:commands/ingest-commands');
6
7
  export function registerIngestCommands(program, context, commandOptions) {
7
8
  const ingest = program
8
9
  .command('ingest')
9
- .description('Build or inspect KTX context')
10
+ .description('Build or inspect KTX context, or capture text into memory')
10
11
  .usage('[options] [connectionId]')
11
- .argument('[connectionId]', 'Configured connection id to ingest')
12
+ .argument('[connectionId]', 'Configured connection id to ingest (omit to ingest all)')
12
13
  .option('--all', 'Ingest all configured connections', false)
13
14
  .addOption(new Option('--fast', 'Use deterministic database schema ingest').conflicts('deep'))
14
15
  .addOption(new Option('--deep', 'Use AI-enriched database ingest').conflicts('fast'))
15
16
  .addOption(new Option('--query-history', 'Include database query-history usage patterns').conflicts('noQueryHistory'))
16
17
  .addOption(new Option('--no-query-history', 'Skip database query-history usage patterns'))
17
18
  .option('--query-history-window-days <days>', 'Query-history lookback window for this run', parsePositiveIntegerOption)
19
+ .option('--text <content>', 'Capture inline text into KTX memory; repeatable', collectOption, [])
20
+ .option('--file <path>', 'Capture a text file into KTX memory; use - for stdin; repeatable', collectOption, [])
21
+ .option('--connection-id <connectionId>', 'KTX connection id to tag captured text/file notes')
22
+ .option('--user-id <id>', 'Memory user id for text/file capture attribution', 'local-cli')
23
+ .option('--fail-fast', 'Stop after the first failed text/file item', false)
18
24
  .addOption(new Option('--plain', 'Print plain text output').conflicts(['json']))
19
25
  .addOption(new Option('--json', 'Print JSON output').conflicts(['plain']))
20
26
  .option('--yes', 'Install required managed runtime features without prompting')
21
27
  .option('--no-input', 'Disable interactive terminal input')
22
28
  .showHelpAfterError();
23
29
  ingest.action(async (connectionId, options, command) => {
30
+ const projectDir = resolveCommandProjectDir(command);
31
+ const hasTextCapture = options.text.length > 0 || options.file.length > 0;
32
+ if (hasTextCapture) {
33
+ if (connectionId !== undefined) {
34
+ command.error('error: --text/--file does not accept a positional connection id; use --connection-id <id> to tag captured notes');
35
+ }
36
+ if (options.all === true) {
37
+ command.error('error: --all cannot be combined with --text or --file');
38
+ }
39
+ context.setExitCode(await commandOptions.runTextIngest({
40
+ projectDir,
41
+ texts: options.text,
42
+ files: options.file,
43
+ ...(options.connectionId ? { connectionId: options.connectionId } : {}),
44
+ userId: options.userId,
45
+ json: options.json === true,
46
+ failFast: options.failFast === true,
47
+ }, context.io, context.deps));
48
+ return;
49
+ }
50
+ const selection = resolveConnectionSelection({ connectionId, all: options.all === true });
24
51
  const { runKtxPublicIngest } = await import('../public-ingest.js');
25
52
  const queryHistory = options.queryHistory === true ? 'enabled' : options.queryHistory === false ? 'disabled' : 'default';
26
53
  const args = {
27
54
  command: 'run',
28
- projectDir: resolveCommandProjectDir(command),
29
- ...(connectionId ? { targetConnectionId: connectionId } : {}),
30
- all: options.all === true,
55
+ projectDir,
56
+ ...(selection.kind === 'single' ? { targetConnectionId: selection.connectionId } : {}),
57
+ all: selection.kind === 'all',
31
58
  json: options.json === true,
32
59
  inputMode: options.input === false ? 'disabled' : 'auto',
33
60
  ...(options.fast === true ? { depth: 'fast' } : {}),
@@ -42,25 +69,4 @@ export function registerIngestCommands(program, context, commandOptions) {
42
69
  ingest.hook('preAction', (_thisCommand, actionCommand) => {
43
70
  context.writeDebug?.('ingest', actionCommand);
44
71
  });
45
- ingest
46
- .command('text')
47
- .description('Ingest free-form text artifacts into KTX memory')
48
- .argument('[files...]', 'Files to ingest; use - to read one item from stdin')
49
- .option('--text <content>', 'Text content to ingest; repeat for a batch', collectOption, [])
50
- .option('--connection-id <connectionId>', 'Optional KTX connection id for semantic-layer capture')
51
- .option('--user-id <id>', 'Memory user id for capture attribution', 'local-cli')
52
- .option('--json', 'Print JSON output')
53
- .option('--fail-fast', 'Stop after the first failed text item', false)
54
- .action(async (files, options, command) => {
55
- const parentOptions = command.parent?.opts();
56
- context.setExitCode(await commandOptions.runTextIngest({
57
- projectDir: resolveCommandProjectDir(command),
58
- texts: options.text,
59
- files,
60
- ...(options.connectionId ? { connectionId: options.connectionId } : {}),
61
- userId: options.userId,
62
- json: options.json === true || parentOptions?.json === true,
63
- failFast: options.failFast === true,
64
- }, context.io, context.deps));
65
- });
66
72
  }
@@ -11,47 +11,36 @@ function isDebugEnabled(command) {
11
11
  return options.debug === true;
12
12
  }
13
13
  export function registerWikiCommands(program, context) {
14
- const wiki = program
14
+ program
15
15
  .command('wiki')
16
16
  .description('List or search local wiki pages')
17
- .showHelpAfterError()
18
- .addHelpText('after', '\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n');
19
- wiki
20
- .command('list')
21
- .description('List local wiki pages')
22
- .option('--user-id <id>', 'Local user id', 'local')
23
- .addOption(new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
24
- 'pretty',
25
- 'plain',
26
- 'json',
27
- ]))
28
- .option('--json', 'Shortcut for --output=json (overrides --output)', false)
29
- .action(async (options, command) => {
30
- await runKnowledgeArgs(context, {
31
- command: 'list',
32
- projectDir: resolveCommandProjectDir(command),
33
- userId: options.userId,
34
- output: options.output,
35
- json: options.json,
36
- });
37
- });
38
- wiki
39
- .command('search')
40
- .description('Search local wiki pages')
41
- .argument('<query>', 'Search query')
17
+ .usage('[options] [query...]')
18
+ .argument('[query...]', 'Search query; omit to list all pages')
42
19
  .option('--user-id <id>', 'Local user id', 'local')
43
- .option('--limit <number>', 'Maximum search results', parsePositiveIntegerOption)
20
+ .option('--limit <number>', 'Maximum search results (search mode only)', parsePositiveIntegerOption)
44
21
  .addOption(new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
45
22
  'pretty',
46
23
  'plain',
47
24
  'json',
48
25
  ]))
49
26
  .option('--json', 'Shortcut for --output=json (overrides --output)', false)
27
+ .showHelpAfterError()
28
+ .addHelpText('after', '\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n')
50
29
  .action(async (query, options, command) => {
30
+ if (query.length === 0) {
31
+ await runKnowledgeArgs(context, {
32
+ command: 'list',
33
+ projectDir: resolveCommandProjectDir(command),
34
+ userId: options.userId,
35
+ output: options.output,
36
+ json: options.json,
37
+ });
38
+ return;
39
+ }
51
40
  await runKnowledgeArgs(context, {
52
41
  command: 'search',
53
42
  projectDir: resolveCommandProjectDir(command),
54
- query,
43
+ query: query.join(' '),
55
44
  userId: options.userId,
56
45
  output: options.output,
57
46
  json: options.json,
@@ -21,8 +21,23 @@ function formatMcpStartResultMessage(input) {
21
21
  '',
22
22
  ].join('\n');
23
23
  }
24
+ async function printMcpStatus(context, projectDir) {
25
+ const status = await (context.deps.mcp?.readStatus ?? readKtxMcpDaemonStatus)({ projectDir });
26
+ context.io.stdout.write(`${status.detail}\n`);
27
+ if (status.kind === 'running') {
28
+ context.io.stdout.write(`URL: ${status.url}\n`);
29
+ context.io.stdout.write(`PID: ${status.state.pid}\n`);
30
+ context.io.stdout.write(`Token auth: ${status.state.tokenAuth ? 'enabled' : 'disabled'}\n`);
31
+ context.io.stdout.write(`Project: ${status.state.projectDir}\n`);
32
+ }
33
+ }
24
34
  export function registerMcpCommands(program, context) {
25
- const mcp = program.command('mcp').description('Run the KTX MCP HTTP server');
35
+ const mcp = program
36
+ .command('mcp')
37
+ .description('Manage the KTX MCP HTTP server (bare command: show status)')
38
+ .action(async (_options, command) => {
39
+ await printMcpStatus(context, resolveCommandProjectDir(command));
40
+ });
26
41
  mcp
27
42
  .command('stdio')
28
43
  .description('Run the KTX MCP server over stdio')
@@ -91,16 +106,7 @@ export function registerMcpCommands(program, context) {
91
106
  .command('status')
92
107
  .description('Show KTX MCP daemon status')
93
108
  .action(async (_options, command) => {
94
- const status = await (context.deps.mcp?.readStatus ?? readKtxMcpDaemonStatus)({
95
- projectDir: resolveCommandProjectDir(command),
96
- });
97
- context.io.stdout.write(`${status.detail}\n`);
98
- if (status.kind === 'running') {
99
- context.io.stdout.write(`URL: ${status.url}\n`);
100
- context.io.stdout.write(`PID: ${status.state.pid}\n`);
101
- context.io.stdout.write(`Token auth: ${status.state.tokenAuth ? 'enabled' : 'disabled'}\n`);
102
- context.io.stdout.write(`Project: ${status.state.projectDir}\n`);
103
- }
109
+ await printMcpStatus(context, resolveCommandProjectDir(command));
104
110
  });
105
111
  mcp
106
112
  .command('logs')
@@ -28,63 +28,57 @@ export function registerSlCommands(program, context, commandName = 'sl') {
28
28
  const sl = program
29
29
  .command(commandName)
30
30
  .description('List, search, validate, or query local semantic-layer sources')
31
- .showHelpAfterError()
32
- .addHelpText('after', '\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n');
33
- sl.command('list')
34
- .description('List semantic-layer sources')
35
- .option('--connection-id <id>', 'KTX connection id')
36
- .addOption(new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
37
- 'pretty',
38
- 'plain',
39
- 'json',
40
- ]))
41
- .option('--json', 'Shortcut for --output=json (overrides --output)', false)
42
- .action(async (options, command) => {
43
- await runSlArgs(context, {
44
- command: 'list',
45
- projectDir: resolveCommandProjectDir(command),
46
- connectionId: options.connectionId,
47
- output: options.output,
48
- json: options.json,
49
- });
50
- });
51
- sl.command('search')
52
- .description('Search semantic-layer sources')
53
- .argument('<query>', 'Search query')
31
+ .usage('[options] [query...]')
32
+ .argument('[query...]', 'Search query; omit to list all sources')
54
33
  .option('--connection-id <id>', 'KTX connection id')
55
- .option('--limit <number>', 'Maximum search results', parsePositiveIntegerOption)
34
+ .option('--limit <number>', 'Maximum search results (search mode only)', parsePositiveIntegerOption)
56
35
  .addOption(new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
57
36
  'pretty',
58
37
  'plain',
59
38
  'json',
60
39
  ]))
61
40
  .option('--json', 'Shortcut for --output=json (overrides --output)', false)
41
+ .showHelpAfterError()
42
+ .addHelpText('after', '\nProject directory defaults to KTX_PROJECT_DIR when set, otherwise the current working directory.\n')
62
43
  .action(async (query, options, command) => {
44
+ if (query.length === 0) {
45
+ await runSlArgs(context, {
46
+ command: 'list',
47
+ projectDir: resolveCommandProjectDir(command),
48
+ connectionId: options.connectionId,
49
+ output: options.output,
50
+ json: options.json,
51
+ });
52
+ return;
53
+ }
63
54
  await runSlArgs(context, {
64
55
  command: 'search',
65
56
  projectDir: resolveCommandProjectDir(command),
66
57
  connectionId: options.connectionId,
67
- query,
58
+ query: query.join(' '),
68
59
  ...(options.limit !== undefined ? { limit: options.limit } : {}),
69
60
  output: options.output,
70
61
  json: options.json,
71
62
  });
72
63
  });
73
64
  sl.command('validate')
74
- .description('Validate a semantic-layer source')
65
+ .description('Validate a semantic-layer source (set --connection-id on `ktx sl`)')
75
66
  .argument('<sourceName>', 'Semantic-layer source name')
76
- .requiredOption('--connection-id <id>', 'KTX connection id')
77
- .action(async (sourceName, options, command) => {
67
+ .action(async (sourceName, _options, command) => {
68
+ const parentOpts = command.parent?.opts();
69
+ const connectionId = parentOpts?.connectionId;
70
+ if (connectionId === undefined) {
71
+ command.error("error: required option '--connection-id <id>' not specified");
72
+ }
78
73
  await runSlArgs(context, {
79
74
  command: 'validate',
80
75
  projectDir: resolveCommandProjectDir(command),
81
- connectionId: options.connectionId,
76
+ connectionId: connectionId,
82
77
  sourceName,
83
78
  });
84
79
  });
85
80
  sl.command('query')
86
- .description('Compile or execute a semantic-layer query')
87
- .option('--connection-id <id>', 'KTX connection id')
81
+ .description('Compile or execute a semantic-layer query (set --connection-id on `ktx sl`)')
88
82
  .option('--query-file <path>', 'JSON semantic-layer query file')
89
83
  .option('--measure <measure>', 'Measure to query; repeatable', collectOption, [])
90
84
  .option('--dimension <dimension>', 'Dimension to include; repeatable', collectOption, [])
@@ -102,10 +96,11 @@ export function registerSlCommands(program, context, commandName = 'sl') {
102
96
  if (options.measure.length === 0 && !options.queryFile) {
103
97
  throw new Error('sl query requires at least one --measure');
104
98
  }
99
+ const parentOpts = command.parent?.opts();
105
100
  const args = slQueryCommandSchema.parse({
106
101
  command: 'query',
107
102
  projectDir: resolveCommandProjectDir(command),
108
- connectionId: options.connectionId,
103
+ connectionId: parentOpts?.connectionId,
109
104
  ...(options.queryFile
110
105
  ? { queryFile: options.queryFile }
111
106
  : {
@@ -55,8 +55,8 @@ describe('formatDoctorReport', () => {
55
55
  expect(output).not.toContain('v22.16.0');
56
56
  expect(output).toContain('Everything ready.');
57
57
  expect(output).toContain('ktx status --json');
58
- expect(output).toContain('ktx sl list');
59
- expect(output).toContain('ktx wiki list');
58
+ expect(output).toContain('ktx sl');
59
+ expect(output).toContain('ktx wiki');
60
60
  expect(output).not.toContain('ktx scan');
61
61
  expect(output).not.toContain('ktx sl ask');
62
62
  });
@@ -412,8 +412,8 @@ describe('runKtxDoctor', () => {
412
412
  expect(out).toContain('info: pg_stat_statements.max is 1000');
413
413
  expect(out).not.toContain('Update the Postgres parameter group or config');
414
414
  expect(out).toContain('ktx status --json');
415
- expect(out).toContain('ktx sl list');
416
- expect(out).toContain('ktx wiki list');
415
+ expect(out).toContain('ktx sl');
416
+ expect(out).toContain('ktx wiki');
417
417
  expect(out).not.toContain('ktx scan');
418
418
  expect(out).not.toContain('ktx sl ask');
419
419
  delete process.env.ANTHROPIC_API_KEY;
@@ -51,10 +51,10 @@ describe('standalone local warehouse example', () => {
51
51
  });
52
52
  it('runs local CLI commands against the copied example project', async () => {
53
53
  const projectDir = await copyExampleProject(tempDir);
54
- const knowledgeList = await runBuiltCli(['wiki', 'search', 'revenue', '--json', '--project-dir', projectDir]);
54
+ const knowledgeList = await runBuiltCli(['wiki', 'revenue', '--json', '--project-dir', projectDir]);
55
55
  expect(knowledgeList).toMatchObject({ code: 0, stderr: '' });
56
56
  expect(parseJsonOutput(knowledgeList.stdout).data.items).toContainEqual(expect.objectContaining({ key: 'revenue', summary: 'Paid order value after refunds' }));
57
- const slList = await runBuiltCli(['sl', 'list', '--json', '--project-dir', projectDir, '--connection-id', 'warehouse']);
57
+ const slList = await runBuiltCli(['sl', '--json', '--project-dir', projectDir, '--connection-id', 'warehouse']);
58
58
  expect(slList).toMatchObject({ code: 0, stderr: '' });
59
59
  expect(parseJsonOutput(slList.stdout).data.items).toContainEqual(expect.objectContaining({ connectionId: 'warehouse', name: 'orders', columnCount: 3 }));
60
60
  const slSearch = await runBuiltCli([
@@ -78,6 +78,6 @@ describe('standalone local warehouse example', () => {
78
78
  'fake',
79
79
  ]);
80
80
  expect(ingest).toMatchObject({ code: 1, stdout: '' });
81
- expect(ingest.stderr).toContain("unknown option '--connection-id'");
81
+ expect(ingest.stderr).toContain("unknown option '--adapter'");
82
82
  }, 30_000);
83
83
  });
@@ -98,9 +98,10 @@ describe('runKtxCli', () => {
98
98
  await expect(runKtxCli(['--help'], testIo.io)).resolves.toBe(0);
99
99
  expect(testIo.stdout()).toContain('Usage: ktx [options] [command]');
100
100
  expect(testIo.stdout()).toContain('KTX data agent context layer CLI');
101
- for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'status', 'dev']) {
101
+ for (const command of ['setup', 'connection', 'ingest', 'wiki', 'sl', 'status', 'admin']) {
102
102
  expect(testIo.stdout()).toContain(`${command}`);
103
103
  }
104
+ expect(testIo.stdout()).not.toMatch(/^ dev\s/m);
104
105
  expect(testIo.stdout()).not.toMatch(/^ scan\s/m);
105
106
  for (const removed of ['demo', 'init', 'connect', 'ask', 'knowledge', 'agent', 'completion', 'serve']) {
106
107
  expect(testIo.stdout()).not.toMatch(new RegExp(`^\\s+${removed}(?:\\s|\\[|$)`, 'm'));
@@ -115,7 +116,7 @@ describe('runKtxCli', () => {
115
116
  it('routes supported public wiki commands', async () => {
116
117
  const knowledge = vi.fn(async () => 0);
117
118
  const listIo = makeIo();
118
- await expect(runKtxCli(['--project-dir', tempDir, 'wiki', 'list', '--json'], listIo.io, { knowledge }))
119
+ await expect(runKtxCli(['--project-dir', tempDir, 'wiki', '--json'], listIo.io, { knowledge }))
119
120
  .resolves.toBe(0);
120
121
  expect(knowledge).toHaveBeenCalledWith({
121
122
  command: 'list',
@@ -124,7 +125,7 @@ describe('runKtxCli', () => {
124
125
  json: true,
125
126
  }, listIo.io);
126
127
  const searchIo = makeIo();
127
- await expect(runKtxCli(['--project-dir', tempDir, 'wiki', 'search', 'revenue', '--limit', '5'], searchIo.io, { knowledge })).resolves.toBe(0);
128
+ await expect(runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', '--limit', '5'], searchIo.io, { knowledge })).resolves.toBe(0);
128
129
  expect(knowledge).toHaveBeenLastCalledWith({
129
130
  command: 'search',
130
131
  projectDir: tempDir,
@@ -134,7 +135,7 @@ describe('runKtxCli', () => {
134
135
  limit: 5,
135
136
  }, searchIo.io);
136
137
  const debugSearchIo = makeIo();
137
- await expect(runKtxCli(['--project-dir', tempDir, '--debug', 'wiki', 'search', 'revenue'], debugSearchIo.io, { knowledge })).resolves.toBe(0);
138
+ await expect(runKtxCli(['--project-dir', tempDir, '--debug', 'wiki', 'revenue'], debugSearchIo.io, { knowledge })).resolves.toBe(0);
138
139
  expect(knowledge).toHaveBeenLastCalledWith({
139
140
  command: 'search',
140
141
  projectDir: tempDir,
@@ -143,35 +144,32 @@ describe('runKtxCli', () => {
143
144
  json: false,
144
145
  debug: true,
145
146
  }, debugSearchIo.io);
147
+ const multiWordIo = makeIo();
148
+ await expect(runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', 'policy'], multiWordIo.io, { knowledge })).resolves.toBe(0);
149
+ expect(knowledge).toHaveBeenLastCalledWith({
150
+ command: 'search',
151
+ projectDir: tempDir,
152
+ query: 'revenue policy',
153
+ userId: 'local',
154
+ json: false,
155
+ }, multiWordIo.io);
146
156
  });
147
- it('rejects removed public wiki read and write commands', async () => {
157
+ it('rejects unknown write-style flags on the flattened wiki and sl commands', async () => {
148
158
  const knowledge = vi.fn(async () => 0);
149
- for (const argv of [
150
- ['--project-dir', tempDir, 'wiki', 'read', 'revenue', '--json'],
151
- ['--project-dir', tempDir, 'wiki', 'write', 'revenue', '--summary', 'Revenue', '--content', 'Revenue.'],
152
- ]) {
153
- const io = makeIo();
154
- await expect(runKtxCli(argv, io.io, { knowledge })).resolves.toBe(1);
155
- expect(io.stderr()).toMatch(/unknown command|error:/);
156
- }
157
- expect(knowledge).not.toHaveBeenCalled();
158
- });
159
- it('rejects removed public sl read/write commands', async () => {
160
159
  const sl = vi.fn(async () => 0);
161
- for (const argv of [
162
- ['--project-dir', tempDir, 'sl', 'read', 'orders', '--connection-id', 'warehouse'],
163
- ['--project-dir', tempDir, 'sl', 'write', 'orders', '--connection-id', 'warehouse', '--yaml', 'name: orders'],
164
- ]) {
165
- const io = makeIo();
166
- await expect(runKtxCli(argv, io.io, { sl })).resolves.toBe(1);
167
- expect(io.stderr()).toMatch(/unknown command|error:/);
168
- }
160
+ const wikiIo = makeIo();
161
+ await expect(runKtxCli(['--project-dir', tempDir, 'wiki', 'revenue', '--summary', 'Revenue', '--content', 'Revenue.'], wikiIo.io, { knowledge })).resolves.toBe(1);
162
+ expect(wikiIo.stderr()).toMatch(/unknown option|error:/);
163
+ expect(knowledge).not.toHaveBeenCalled();
164
+ const slIo = makeIo();
165
+ await expect(runKtxCli(['--project-dir', tempDir, 'sl', 'orders', '--yaml', 'name: orders'], slIo.io, { sl })).resolves.toBe(1);
166
+ expect(slIo.stderr()).toMatch(/unknown option|error:/);
169
167
  expect(sl).not.toHaveBeenCalled();
170
168
  });
171
- it('routes sl search and rejects the old sl list --query flag', async () => {
169
+ it('routes sl search via the flattened query positional and rejects unknown flags', async () => {
172
170
  const sl = vi.fn(async () => 0);
173
171
  const searchIo = makeIo();
174
- await expect(runKtxCli(['--project-dir', tempDir, 'sl', 'search', 'revenue', '--connection-id', 'warehouse', '--limit', '5', '--json'], searchIo.io, { sl })).resolves.toBe(0);
172
+ await expect(runKtxCli(['--project-dir', tempDir, 'sl', 'revenue', '--connection-id', 'warehouse', '--limit', '5', '--json'], searchIo.io, { sl })).resolves.toBe(0);
175
173
  expect(sl).toHaveBeenCalledWith({
176
174
  command: 'search',
177
175
  projectDir: tempDir,
@@ -181,9 +179,18 @@ describe('runKtxCli', () => {
181
179
  json: true,
182
180
  output: undefined,
183
181
  }, searchIo.io);
184
- const listIo = makeIo();
185
- await expect(runKtxCli(['--project-dir', tempDir, 'sl', 'list', '--query', 'revenue'], listIo.io, { sl })).resolves.toBe(1);
186
- expect(listIo.stderr()).toContain("unknown option '--query'");
182
+ const bareIo = makeIo();
183
+ await expect(runKtxCli(['--project-dir', tempDir, 'sl', '--connection-id', 'warehouse', '--json'], bareIo.io, { sl })).resolves.toBe(0);
184
+ expect(sl).toHaveBeenLastCalledWith({
185
+ command: 'list',
186
+ projectDir: tempDir,
187
+ connectionId: 'warehouse',
188
+ json: true,
189
+ output: undefined,
190
+ }, bareIo.io);
191
+ const unknownIo = makeIo();
192
+ await expect(runKtxCli(['--project-dir', tempDir, 'sl', '--query', 'revenue'], unknownIo.io, { sl })).resolves.toBe(1);
193
+ expect(unknownIo.stderr()).toContain("unknown option '--query'");
187
194
  });
188
195
  it('routes runtime management commands with the release runtime version', async () => {
189
196
  const runtime = vi.fn(async () => 0);
@@ -193,14 +200,14 @@ describe('runKtxCli', () => {
193
200
  const stopAllIo = makeIo();
194
201
  const statusIo = makeIo();
195
202
  const pruneIo = makeIo();
196
- await expect(runKtxCli(['dev', 'runtime', 'install', '--feature', 'local-embeddings', '--force', '--yes'], installIo.io, {
203
+ await expect(runKtxCli(['admin', 'runtime', 'install', '--feature', 'local-embeddings', '--force', '--yes'], installIo.io, {
197
204
  runtime,
198
205
  })).resolves.toBe(0);
199
- await expect(runKtxCli(['dev', 'runtime', 'start', '--feature', 'local-embeddings', '--force'], startIo.io, { runtime })).resolves.toBe(0);
200
- await expect(runKtxCli(['dev', 'runtime', 'stop'], stopIo.io, { runtime })).resolves.toBe(0);
201
- await expect(runKtxCli(['dev', 'runtime', 'stop', '--all'], stopAllIo.io, { runtime })).resolves.toBe(0);
202
- await expect(runKtxCli(['dev', 'runtime', 'status', '--json'], statusIo.io, { runtime })).resolves.toBe(0);
203
- await expect(runKtxCli(['dev', 'runtime', 'prune', '--dry-run'], pruneIo.io, { runtime })).resolves.toBe(1);
206
+ await expect(runKtxCli(['admin', 'runtime', 'start', '--feature', 'local-embeddings', '--force'], startIo.io, { runtime })).resolves.toBe(0);
207
+ await expect(runKtxCli(['admin', 'runtime', 'stop'], stopIo.io, { runtime })).resolves.toBe(0);
208
+ await expect(runKtxCli(['admin', 'runtime', 'stop', '--all'], stopAllIo.io, { runtime })).resolves.toBe(0);
209
+ await expect(runKtxCli(['admin', 'runtime', 'status', '--json'], statusIo.io, { runtime })).resolves.toBe(0);
210
+ await expect(runKtxCli(['admin', 'runtime', 'prune', '--dry-run'], pruneIo.io, { runtime })).resolves.toBe(1);
204
211
  expect(runtime).toHaveBeenNthCalledWith(1, {
205
212
  command: 'install',
206
213
  cliVersion: '0.1.0-rc.1',
@@ -262,7 +269,7 @@ describe('runKtxCli', () => {
262
269
  });
263
270
  it('documents runtime stop all in command help', async () => {
264
271
  const testIo = makeIo();
265
- await expect(runKtxCli(['dev', 'runtime', 'stop', '--help'], testIo.io)).resolves.toBe(0);
272
+ await expect(runKtxCli(['admin', 'runtime', 'stop', '--help'], testIo.io)).resolves.toBe(0);
266
273
  expect(testIo.stdout()).toContain('--all');
267
274
  expect(testIo.stdout()).toContain('Stop all KTX daemon processes recorded or discoverable');
268
275
  expect(testIo.stdout()).toContain('on this machine');
@@ -367,7 +374,7 @@ describe('runKtxCli', () => {
367
374
  await initKtxProject({ projectDir });
368
375
  const commands = [
369
376
  ['--project-dir', projectDir, 'status', '--json'],
370
- ['--project-dir', projectDir, 'sl', 'list', '--json'],
377
+ ['--project-dir', projectDir, 'sl', '--json'],
371
378
  ];
372
379
  for (const argv of commands) {
373
380
  const testIo = makeIo();
@@ -483,8 +490,8 @@ describe('runKtxCli', () => {
483
490
  it('rejects removed shell completion commands', async () => {
484
491
  const completionIo = makeIo();
485
492
  const hiddenIo = makeIo();
486
- await expect(runKtxCli(['dev', 'completion', 'zsh'], completionIo.io)).resolves.toBe(1);
487
- await expect(runKtxCli(['dev', '__complete', '--shell', 'zsh', '--position', '2', '--', 'ktx', 'co'], hiddenIo.io)).resolves.toBe(1);
493
+ await expect(runKtxCli(['admin', 'completion', 'zsh'], completionIo.io)).resolves.toBe(1);
494
+ await expect(runKtxCli(['admin', '__complete', '--shell', 'zsh', '--position', '2', '--', 'ktx', 'co'], hiddenIo.io)).resolves.toBe(1);
488
495
  expect(completionIo.stderr()).toMatch(/unknown command|error:/);
489
496
  expect(hiddenIo.stderr()).toMatch(/unknown command|error:/);
490
497
  });
@@ -636,7 +643,8 @@ describe('runKtxCli', () => {
636
643
  expect(testIo.stdout()).toContain('--query-history');
637
644
  expect(testIo.stdout()).toContain('--no-query-history');
638
645
  expect(testIo.stdout()).toContain('--query-history-window-days <days>');
639
- expect(testIo.stdout()).toContain('text');
646
+ expect(testIo.stdout()).toContain('--text');
647
+ expect(testIo.stdout()).toContain('--file');
640
648
  expect(testIo.stdout()).not.toMatch(/^ status\s/m);
641
649
  expect(testIo.stdout()).not.toMatch(/^ replay\s/m);
642
650
  expect(testIo.stdout()).not.toMatch(/^ run\s/m);
@@ -652,7 +660,6 @@ describe('runKtxCli', () => {
652
660
  '--project-dir',
653
661
  tempDir,
654
662
  'ingest',
655
- 'text',
656
663
  '--text',
657
664
  'Revenue means gross receipts.',
658
665
  '--text',
@@ -675,37 +682,46 @@ describe('runKtxCli', () => {
675
682
  }, testIo.io);
676
683
  expect(testIo.stderr()).toBe('');
677
684
  });
678
- it('documents text ingest inputs without a manifest option', async () => {
685
+ it('rejects a positional connection id when --text is supplied', async () => {
679
686
  const textIngest = vi.fn(async () => 0);
687
+ const publicIngest = vi.fn(async () => 0);
680
688
  const testIo = makeIo();
681
- await expect(runKtxCli(['ingest', 'text', '--help'], testIo.io, { textIngest })).resolves.toBe(0);
682
- expect(testIo.stdout()).toContain('Usage: ktx ingest text [options] [files...]');
683
- expect(testIo.stdout()).toContain('--text <content>');
684
- expect(testIo.stdout()).toContain('--connection-id <connectionId>');
685
- expect(testIo.stdout()).toContain('--user-id <id>');
686
- expect(testIo.stdout()).toContain('--fail-fast');
687
- expect(testIo.stdout()).not.toContain('--manifest');
689
+ await expect(runKtxCli(['--project-dir', tempDir, 'ingest', 'warehouse', '--text', 'hello'], testIo.io, { textIngest, publicIngest })).resolves.toBe(1);
688
690
  expect(textIngest).not.toHaveBeenCalled();
691
+ expect(publicIngest).not.toHaveBeenCalled();
692
+ expect(testIo.stderr()).toMatch(/--text\/--file does not accept a positional connection id/);
693
+ });
694
+ it('treats bare ingest as ingest --all', async () => {
695
+ const publicIngest = vi.fn().mockResolvedValue(0);
696
+ const testIo = makeIo();
697
+ await expect(runKtxCli(['--project-dir', tempDir, 'ingest', '--no-input'], testIo.io, { publicIngest })).resolves.toBe(0);
698
+ expect(publicIngest).toHaveBeenCalledWith(expect.objectContaining({
699
+ command: 'run',
700
+ projectDir: tempDir,
701
+ all: true,
702
+ }), testIo.io);
703
+ const args = publicIngest.mock.calls[0]?.[0];
704
+ expect(args.targetConnectionId).toBeUndefined();
689
705
  });
690
- it('rejects old adapter-backed ingest flags at the top level and under dev', async () => {
706
+ it('rejects old adapter-backed ingest flags at the top level and under admin', async () => {
691
707
  const rootRunIo = makeIo();
692
708
  const devRunIo = makeIo();
693
709
  const publicIngest = vi.fn(async () => 0);
694
710
  await expect(runKtxCli(['ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], rootRunIo.io, {
695
711
  publicIngest,
696
712
  })).resolves.toBe(1);
697
- await expect(runKtxCli(['dev', 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], devRunIo.io, {
713
+ await expect(runKtxCli(['admin', 'ingest', 'run', '--connection-id', 'warehouse', '--adapter', 'metabase'], devRunIo.io, {
698
714
  publicIngest,
699
715
  })).resolves.toBe(1);
700
716
  expect(publicIngest).not.toHaveBeenCalled();
701
717
  expect(rootRunIo.stderr()).toMatch(/unknown option '--connection-id'|error:/);
702
718
  expect(devRunIo.stderr()).toMatch(/unknown command|error:/);
703
719
  });
704
- it('rejects removed dev doctor and removed ingest parser cases', async () => {
720
+ it('rejects removed admin doctor and removed ingest parser cases', async () => {
705
721
  const doctor = vi.fn(async () => 0);
706
722
  const doctorIo = makeIo();
707
723
  const ingestRunIo = makeIo();
708
- await expect(runKtxCli(['dev', 'doctor', 'setup', '--json', '--no-input'], doctorIo.io, { doctor })).resolves.toBe(1);
724
+ await expect(runKtxCli(['admin', 'doctor', 'setup', '--json', '--no-input'], doctorIo.io, { doctor })).resolves.toBe(1);
709
725
  await expect(runKtxCli([
710
726
  'ingest',
711
727
  'run',
@@ -1237,10 +1253,10 @@ describe('runKtxCli', () => {
1237
1253
  ], serveIo.io)).resolves.toBe(1);
1238
1254
  expect(serveIo.stderr()).toMatch(/unknown command|error:/);
1239
1255
  });
1240
- it('prints dev help for bare dev commands', async () => {
1256
+ it('prints admin help for bare admin commands', async () => {
1241
1257
  const testIo = makeIo();
1242
- await expect(runKtxCli(['dev'], testIo.io)).resolves.toBe(0);
1243
- expect(testIo.stdout()).toContain('Usage: ktx dev [options] [command]');
1258
+ await expect(runKtxCli(['admin'], testIo.io)).resolves.toBe(0);
1259
+ expect(testIo.stdout()).toContain('Usage: ktx admin [options] [command]');
1244
1260
  expect(testIo.stdout()).toContain('Low-level project initialization');
1245
1261
  expect(testIo.stdout()).toContain('init');
1246
1262
  expect(testIo.stdout()).toContain('runtime');
@@ -1251,20 +1267,20 @@ describe('runKtxCli', () => {
1251
1267
  expect(testIo.stdout()).not.toContain('knowledge');
1252
1268
  expect(testIo.stderr()).toBe('');
1253
1269
  });
1254
- it('rejects removed dev command groups without invoking execution', async () => {
1270
+ it('rejects removed admin command groups without invoking execution', async () => {
1255
1271
  for (const command of ['scan', 'ingest', 'mapping']) {
1256
1272
  const testIo = makeIo();
1257
1273
  const publicIngest = vi.fn().mockResolvedValue(0);
1258
1274
  const sl = vi.fn().mockResolvedValue(0);
1259
- await expect(runKtxCli(['dev', command], testIo.io, { publicIngest, sl })).resolves.toBe(1);
1275
+ await expect(runKtxCli(['admin', command], testIo.io, { publicIngest, sl })).resolves.toBe(1);
1260
1276
  expect(testIo.stderr()).toMatch(/unknown command|error:/);
1261
1277
  expect(publicIngest).not.toHaveBeenCalled();
1262
1278
  expect(sl).not.toHaveBeenCalled();
1263
1279
  }
1264
1280
  });
1265
- it('rejects removed reserved dev subcommands', async () => {
1281
+ it('rejects removed reserved admin subcommands', async () => {
1266
1282
  const testIo = makeIo();
1267
- await expect(runKtxCli(['dev', 'artifacts'], testIo.io)).resolves.toBe(1);
1283
+ await expect(runKtxCli(['admin', 'artifacts'], testIo.io)).resolves.toBe(1);
1268
1284
  expect(testIo.stderr()).toMatch(/unknown command|error:/);
1269
1285
  });
1270
1286
  it('rejects mutually exclusive public ingest output modes before invoking runners', async () => {