@kaelio/ktx 0.1.0-rc.5 → 0.1.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 (180) hide show
  1. package/assets/python/kaelio_ktx-0.1.0-py3-none-any.whl +0 -0
  2. package/assets/python/manifest.json +2 -2
  3. package/dist/clack.d.ts +6 -0
  4. package/dist/clack.js +23 -0
  5. package/dist/cli-program.js +5 -2
  6. package/dist/cli-program.test.js +7 -1
  7. package/dist/cli-runtime.d.ts +4 -0
  8. package/dist/cli-runtime.js +8 -1
  9. package/dist/command-schemas.d.ts +1 -1
  10. package/dist/commands/ingest-commands.js +1 -0
  11. package/dist/commands/knowledge-commands.js +5 -0
  12. package/dist/commands/mcp-commands.js +11 -3
  13. package/dist/commands/mcp-commands.test.js +30 -1
  14. package/dist/commands/sql-commands.d.ts +3 -0
  15. package/dist/commands/sql-commands.js +43 -0
  16. package/dist/commands/sql-commands.test.d.ts +1 -0
  17. package/dist/commands/sql-commands.test.js +68 -0
  18. package/dist/context-build-view.js +5 -1
  19. package/dist/dev.test.js +27 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +1 -0
  22. package/dist/index.test.js +56 -21
  23. package/dist/ingest.js +123 -18
  24. package/dist/ingest.test.js +206 -0
  25. package/dist/io/print-list.d.ts +2 -1
  26. package/dist/io/print-list.js +7 -0
  27. package/dist/io/print-list.test.js +13 -11
  28. package/dist/io/symbols.d.ts +2 -2
  29. package/dist/knowledge.d.ts +1 -0
  30. package/dist/knowledge.js +34 -16
  31. package/dist/knowledge.test.js +27 -0
  32. package/dist/managed-python-command.d.ts +2 -0
  33. package/dist/managed-python-command.js +17 -9
  34. package/dist/managed-python-command.test.js +59 -4
  35. package/dist/next-steps.js +1 -1
  36. package/dist/next-steps.test.js +2 -0
  37. package/dist/print-command-tree.js +7 -1
  38. package/dist/public-ingest.d.ts +9 -1
  39. package/dist/public-ingest.js +50 -7
  40. package/dist/public-ingest.test.js +69 -2
  41. package/dist/release-version.d.ts +5 -0
  42. package/dist/release-version.js +44 -0
  43. package/dist/runtime-requirements.d.ts +23 -0
  44. package/dist/runtime-requirements.js +99 -0
  45. package/dist/runtime-requirements.test.d.ts +1 -0
  46. package/dist/runtime-requirements.test.js +63 -0
  47. package/dist/setup-agents.d.ts +11 -3
  48. package/dist/setup-agents.js +397 -134
  49. package/dist/setup-agents.test.js +359 -61
  50. package/dist/setup-embeddings.js +3 -6
  51. package/dist/setup-embeddings.test.js +18 -2
  52. package/dist/setup-models.js +2 -2
  53. package/dist/setup-models.test.js +5 -3
  54. package/dist/setup-ready-menu.d.ts +1 -1
  55. package/dist/setup-ready-menu.js +2 -0
  56. package/dist/setup-ready-menu.test.js +3 -0
  57. package/dist/setup-runtime.d.ts +45 -0
  58. package/dist/setup-runtime.js +47 -0
  59. package/dist/setup-runtime.test.d.ts +1 -0
  60. package/dist/setup-runtime.test.js +110 -0
  61. package/dist/setup-sources-notion.test.d.ts +1 -0
  62. package/dist/setup-sources-notion.test.js +107 -0
  63. package/dist/setup-sources.js +5 -2
  64. package/dist/setup.d.ts +19 -1
  65. package/dist/setup.js +104 -29
  66. package/dist/setup.test.js +221 -57
  67. package/dist/sl.js +2 -2
  68. package/dist/sl.test.js +10 -0
  69. package/dist/source-mapping.js +9 -1
  70. package/dist/source-mapping.test.d.ts +1 -0
  71. package/dist/source-mapping.test.js +65 -0
  72. package/dist/sql.d.ts +22 -0
  73. package/dist/sql.js +125 -0
  74. package/dist/sql.test.d.ts +1 -0
  75. package/dist/sql.test.js +226 -0
  76. package/node_modules/@ktx/connector-clickhouse/dist/package-exports.test.js +1 -1
  77. package/node_modules/@ktx/context/dist/connections/connection-type.d.ts +4 -4
  78. package/node_modules/@ktx/context/dist/core/git.service.d.ts +3 -0
  79. package/node_modules/@ktx/context/dist/core/git.service.js +47 -1
  80. package/node_modules/@ktx/context/dist/core/git.service.patch.test.d.ts +1 -0
  81. package/node_modules/@ktx/context/dist/core/git.service.patch.test.js +40 -0
  82. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/types.d.ts +5 -5
  83. package/node_modules/@ktx/context/dist/ingest/adapters/looker/looker.adapter.d.ts +2 -2
  84. package/node_modules/@ktx/context/dist/ingest/adapters/looker/tools/looker-query-to-sl.tool.d.ts +2 -2
  85. package/node_modules/@ktx/context/dist/ingest/adapters/looker/types.d.ts +16 -16
  86. package/node_modules/@ktx/context/dist/ingest/adapters/lookml/pull-config.d.ts +1 -1
  87. package/node_modules/@ktx/context/dist/ingest/adapters/metabase/fetch.js +16 -0
  88. package/node_modules/@ktx/context/dist/ingest/adapters/metabase/fetch.test.js +41 -0
  89. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/metricflow.adapter.d.ts +2 -1
  90. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/metricflow.adapter.js +40 -0
  91. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/metricflow.adapter.test.js +116 -1
  92. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/projection-config.d.ts +29 -0
  93. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/projection-config.js +40 -0
  94. package/node_modules/@ktx/context/dist/ingest/adapters/metricflow/pull-config.d.ts +1 -1
  95. package/node_modules/@ktx/context/dist/ingest/artifact-gates.d.ts +25 -0
  96. package/node_modules/@ktx/context/dist/ingest/artifact-gates.js +149 -0
  97. package/node_modules/@ktx/context/dist/ingest/artifact-gates.test.d.ts +1 -0
  98. package/node_modules/@ktx/context/dist/ingest/artifact-gates.test.js +167 -0
  99. package/node_modules/@ktx/context/dist/ingest/final-gate-repair.d.ts +29 -0
  100. package/node_modules/@ktx/context/dist/ingest/final-gate-repair.js +178 -0
  101. package/node_modules/@ktx/context/dist/ingest/final-gate-repair.test.d.ts +1 -0
  102. package/node_modules/@ktx/context/dist/ingest/final-gate-repair.test.js +109 -0
  103. package/node_modules/@ktx/context/dist/ingest/index.d.ts +8 -1
  104. package/node_modules/@ktx/context/dist/ingest/index.js +7 -0
  105. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.d.ts +18 -2
  106. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.isolated-diff.test.d.ts +1 -0
  107. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.isolated-diff.test.js +1761 -0
  108. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.js +1695 -901
  109. package/node_modules/@ktx/context/dist/ingest/ingest-bundle.runner.test.js +135 -118
  110. package/node_modules/@ktx/context/dist/ingest/ingest-trace.d.ts +50 -0
  111. package/node_modules/@ktx/context/dist/ingest/ingest-trace.js +88 -0
  112. package/node_modules/@ktx/context/dist/ingest/ingest-trace.test.d.ts +1 -0
  113. package/node_modules/@ktx/context/dist/ingest/ingest-trace.test.js +76 -0
  114. package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.d.ts +16 -0
  115. package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.js +78 -0
  116. package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.test.d.ts +1 -0
  117. package/node_modules/@ktx/context/dist/ingest/isolated-diff/git-patch.test.js +76 -0
  118. package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.d.ts +58 -0
  119. package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.js +223 -0
  120. package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.test.d.ts +1 -0
  121. package/node_modules/@ktx/context/dist/ingest/isolated-diff/patch-integrator.test.js +369 -0
  122. package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.d.ts +23 -0
  123. package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.js +190 -0
  124. package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.test.d.ts +1 -0
  125. package/node_modules/@ktx/context/dist/ingest/isolated-diff/textual-conflict-resolver.test.js +101 -0
  126. package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.d.ts +15 -0
  127. package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.js +61 -0
  128. package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.test.d.ts +1 -0
  129. package/node_modules/@ktx/context/dist/ingest/isolated-diff/work-unit-executor.test.js +137 -0
  130. package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +7 -0
  131. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +54 -10
  132. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.test.js +65 -0
  133. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +23 -5
  134. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.js +17 -0
  135. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.test.js +1 -0
  136. package/node_modules/@ktx/context/dist/ingest/memory-flow/types.d.ts +6 -0
  137. package/node_modules/@ktx/context/dist/ingest/parsed-target-table.d.ts +1 -1
  138. package/node_modules/@ktx/context/dist/ingest/ports.d.ts +3 -0
  139. package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +32 -7
  140. package/node_modules/@ktx/context/dist/ingest/report-snapshot.js +25 -0
  141. package/node_modules/@ktx/context/dist/ingest/report-snapshot.test.js +124 -0
  142. package/node_modules/@ktx/context/dist/ingest/reports.d.ts +23 -0
  143. package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.d.ts +11 -0
  144. package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.js +26 -0
  145. package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.test.d.ts +1 -0
  146. package/node_modules/@ktx/context/dist/ingest/semantic-layer-target-policy.test.js +25 -0
  147. package/node_modules/@ktx/context/dist/ingest/stages/stage-3-work-units.d.ts +4 -0
  148. package/node_modules/@ktx/context/dist/ingest/stages/stage-3-work-units.js +4 -0
  149. package/node_modules/@ktx/context/dist/ingest/stages/stage-3-work-units.test.js +29 -0
  150. package/node_modules/@ktx/context/dist/ingest/tools/emit-unmapped-fallback.tool.d.ts +1 -1
  151. package/node_modules/@ktx/context/dist/ingest/types.d.ts +24 -0
  152. package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.d.ts +24 -0
  153. package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.js +111 -0
  154. package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.test.d.ts +1 -0
  155. package/node_modules/@ktx/context/dist/ingest/wiki-body-refs.test.js +138 -0
  156. package/node_modules/@ktx/context/dist/llm/claude-code-runtime.js +19 -2
  157. package/node_modules/@ktx/context/dist/llm/claude-code-runtime.test.js +33 -0
  158. package/node_modules/@ktx/context/dist/project/setup-config.d.ts +1 -1
  159. package/node_modules/@ktx/context/dist/project/setup-config.js +10 -1
  160. package/node_modules/@ktx/context/dist/project/setup-config.test.js +3 -2
  161. package/node_modules/@ktx/context/dist/sl/tools/sl-edit-source.tool.js +5 -1
  162. package/node_modules/@ktx/context/dist/sl/tools/sl-edit-source.tool.test.js +15 -0
  163. package/node_modules/@ktx/context/dist/sl/tools/sl-write-source.tool.js +5 -1
  164. package/node_modules/@ktx/context/dist/sl/tools/sl-write-source.tool.test.js +22 -0
  165. package/node_modules/@ktx/context/dist/tools/action-target-connection.d.ts +9 -0
  166. package/node_modules/@ktx/context/dist/tools/action-target-connection.js +14 -0
  167. package/node_modules/@ktx/context/dist/tools/context-candidate-write.tool.d.ts +4 -4
  168. package/node_modules/@ktx/context/dist/tools/index.d.ts +1 -0
  169. package/node_modules/@ktx/context/dist/tools/index.js +1 -0
  170. package/node_modules/@ktx/context/dist/wiki/local-knowledge.js +4 -1
  171. package/node_modules/@ktx/context/dist/wiki/local-knowledge.test.js +44 -0
  172. package/node_modules/@ktx/context/dist/wiki/tools/wiki-write.tool.js +3 -48
  173. package/node_modules/@ktx/context/dist/wiki/tools/wiki-write.tool.test.js +28 -0
  174. package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.d.ts +17 -0
  175. package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.js +79 -0
  176. package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.test.d.ts +1 -0
  177. package/node_modules/@ktx/context/dist/wiki/wiki-ref-validation.test.js +64 -0
  178. package/node_modules/@ktx/context/prompts/memory_agent_bundle_ingest_work_unit.md +23 -4
  179. package/node_modules/@ktx/context/skills/ingest_triage/SKILL.md +7 -3
  180. package/package.json +4 -4
@@ -8,7 +8,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8
8
  import { localFakeBundleReport, persistLocalBundleReport } from './ingest.test-utils.js';
9
9
  import { contextBuildCommands, writeKtxSetupContextState } from './setup-context.js';
10
10
  import { runDemoTour } from './setup-demo-tour.js';
11
- import { formatKtxSetupStatus, readKtxSetupStatus, runKtxSetup } from './setup.js';
11
+ import { formatKtxSetupCompletionSummary, formatKtxSetupStatus, readKtxSetupStatus, runKtxSetup } from './setup.js';
12
12
  vi.mock('./setup-demo-tour.js', () => ({
13
13
  runDemoTour: vi.fn(async () => 0),
14
14
  }));
@@ -33,6 +33,41 @@ function makeIo() {
33
33
  stderr: () => stderr,
34
34
  };
35
35
  }
36
+ function runtimeReady(projectDir) {
37
+ return { status: 'ready', projectDir, requirements: { features: ['core'], requirements: [] } };
38
+ }
39
+ async function writeReadyRuntime(rootDir, cliVersion = '0.2.0') {
40
+ const runtimeRoot = join(rootDir, '.runtime');
41
+ const versionDir = join(runtimeRoot, cliVersion);
42
+ const pythonPath = join(versionDir, '.venv', 'bin', 'python');
43
+ const daemonPath = join(versionDir, '.venv', 'bin', 'ktx-daemon');
44
+ await mkdir(join(versionDir, '.venv', 'bin'), { recursive: true });
45
+ await writeFile(pythonPath, '', 'utf-8');
46
+ await writeFile(daemonPath, '', 'utf-8');
47
+ await writeFile(join(versionDir, 'manifest.json'), `${JSON.stringify({
48
+ schemaVersion: 1,
49
+ cliVersion,
50
+ installedAt: '2026-05-09T10:02:00.000Z',
51
+ asset: {
52
+ schemaVersion: 1,
53
+ distributionName: 'kaelio-ktx',
54
+ normalizedName: 'kaelio_ktx',
55
+ version: '0.1.0',
56
+ wheel: {
57
+ file: 'kaelio_ktx-0.1.0-py3-none-any.whl',
58
+ sha256: '0'.repeat(64),
59
+ bytes: 0,
60
+ },
61
+ },
62
+ features: ['core'],
63
+ python: {
64
+ executable: pythonPath,
65
+ daemonExecutable: daemonPath,
66
+ },
67
+ installLog: join(versionDir, 'install.log'),
68
+ }, null, 2)}\n`, 'utf-8');
69
+ return runtimeRoot;
70
+ }
36
71
  describe('setup status', () => {
37
72
  let tempDir;
38
73
  beforeEach(async () => {
@@ -302,6 +337,98 @@ describe('setup status', () => {
302
337
  expect(rendered).toContain('KTX context built: no');
303
338
  expect(rendered).not.toContain('No KTX project found.');
304
339
  });
340
+ it('formats a concise ready summary for completed agent setup', () => {
341
+ const rendered = formatKtxSetupCompletionSummary({
342
+ project: { path: tempDir, ready: true },
343
+ llm: { ready: true, model: 'sonnet' },
344
+ embeddings: { ready: true, model: 'text-embedding-3-small' },
345
+ databases: [{ connectionId: 'postgres-warehouse', ready: true }],
346
+ sources: [{ connectionId: 'dbt-main', type: 'dbt', ready: true }],
347
+ runtime: { required: true, ready: true, features: ['core'] },
348
+ context: { ready: true, status: 'completed' },
349
+ agents: [
350
+ { target: 'claude-code', scope: 'project', ready: true },
351
+ { target: 'claude-desktop', scope: 'global', ready: true },
352
+ ],
353
+ }, {
354
+ agentNextActions: [
355
+ '1. Start MCP',
356
+ ' Run this command before using Claude Code:',
357
+ '',
358
+ ' RUN:',
359
+ ` ktx mcp start --project-dir ${tempDir}`,
360
+ '',
361
+ ' If you need to stop MCP later:',
362
+ ` ktx mcp stop --project-dir ${tempDir}`,
363
+ '',
364
+ '2. Open Claude Code',
365
+ ' Open Claude Code from the KTX project directory:',
366
+ '',
367
+ ' RUN:',
368
+ ` cd '${tempDir}'`,
369
+ ' claude',
370
+ ].join('\n'),
371
+ });
372
+ expect(rendered).toContain(`Project\n ${tempDir}`);
373
+ expect(rendered).toContain('Context\n built');
374
+ expect(rendered).toContain('Agents configured\n Claude Code, Claude Desktop');
375
+ expect(rendered).toContain('REQUIRED BEFORE USING AGENTS\n\n 1. Start MCP');
376
+ expect(rendered).toContain(' Run this command before using Claude Code:');
377
+ expect(rendered).toContain(' RUN:');
378
+ expect(rendered).toContain(' If you need to stop MCP later:');
379
+ expect(rendered).toContain(`ktx mcp stop --project-dir ${tempDir}`);
380
+ expect(rendered).toContain('After that, try\n Ask your agent: "Use KTX to show me the available tables."');
381
+ expect(rendered).not.toContain('Verify');
382
+ expect(rendered).not.toContain('Project ready: yes');
383
+ expect(rendered).not.toContain('What you can do next');
384
+ });
385
+ it('prints agent next actions inside the final ready summary during full setup', async () => {
386
+ const testIo = makeIo();
387
+ await expect(runKtxSetup({
388
+ command: 'run',
389
+ projectDir: tempDir,
390
+ mode: 'new',
391
+ agents: false,
392
+ target: 'claude-code',
393
+ skipAgents: false,
394
+ inputMode: 'disabled',
395
+ yes: true,
396
+ cliVersion: '0.2.0',
397
+ skipLlm: true,
398
+ skipEmbeddings: true,
399
+ skipDatabases: true,
400
+ skipSources: true,
401
+ databaseSchemas: [],
402
+ }, testIo.io, {
403
+ runtime: async () => runtimeReady(tempDir),
404
+ context: async () => {
405
+ await writeKtxSetupContextState(tempDir, {
406
+ runId: 'setup-context-local-test',
407
+ status: 'completed',
408
+ primarySourceConnectionIds: [],
409
+ contextSourceConnectionIds: [],
410
+ reportIds: [],
411
+ artifactPaths: [],
412
+ retryableFailedTargets: [],
413
+ commands: contextBuildCommands(tempDir, 'setup-context-local-test'),
414
+ });
415
+ await writeKtxSetupState(tempDir, { completed_steps: ['project', 'context'] });
416
+ return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
417
+ },
418
+ })).resolves.toBe(0);
419
+ const output = testIo.stdout();
420
+ expect(output).toContain('Claude Code · Project scope');
421
+ expect(output).toContain(join(tempDir, '.mcp.json'));
422
+ expect(output).toContain('Requires MCP to be started.');
423
+ expect(output).toContain('Analytics skill installed.');
424
+ expect(output).not.toContain('Agent integration complete');
425
+ expect(output).toContain('Finish KTX agent setup');
426
+ expect(output).not.toContain('KTX project ready');
427
+ expect(output).toContain('REQUIRED BEFORE USING AGENTS');
428
+ expect(output).toContain('Run this command before using Claude Code:');
429
+ expect(output).toContain(`ktx mcp start --project-dir ${tempDir}`);
430
+ expect(output).not.toContain('Finish agent setup');
431
+ });
305
432
  it('prints the setup shell intro for auto-created run mode', async () => {
306
433
  const testIo = makeIo();
307
434
  await expect(runKtxSetup({
@@ -805,7 +932,7 @@ describe('setup status', () => {
805
932
  runtimeInstallPolicy: 'never',
806
933
  }), io.io);
807
934
  });
808
- it('auto-installs the managed runtime by default during setup', async () => {
935
+ it('prompts before installing the managed runtime by default during setup', async () => {
809
936
  const io = makeIo();
810
937
  const embeddings = vi.fn(async () => ({ status: 'ready', projectDir: tempDir }));
811
938
  const context = vi.fn(async () => ({ status: 'failed', projectDir: tempDir }));
@@ -830,11 +957,11 @@ describe('setup status', () => {
830
957
  })).resolves.toBe(1);
831
958
  expect(embeddings).toHaveBeenCalledWith(expect.objectContaining({
832
959
  cliVersion: '0.2.0',
833
- runtimeInstallPolicy: 'auto',
960
+ runtimeInstallPolicy: 'prompt',
834
961
  }), io.io);
835
962
  expect(context).toHaveBeenCalledWith(expect.objectContaining({
836
963
  cliVersion: '0.2.0',
837
- runtimeInstallPolicy: 'auto',
964
+ runtimeInstallPolicy: 'prompt',
838
965
  }), io.io);
839
966
  });
840
967
  it('lets Back from embedding setup return to the model step instead of exiting', async () => {
@@ -1164,6 +1291,10 @@ describe('setup status', () => {
1164
1291
  calls.push('sources');
1165
1292
  return { status: 'skipped', projectDir: tempDir };
1166
1293
  },
1294
+ runtime: async () => {
1295
+ calls.push('runtime');
1296
+ return runtimeReady(tempDir);
1297
+ },
1167
1298
  context: async () => {
1168
1299
  calls.push('context');
1169
1300
  return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
@@ -1177,7 +1308,7 @@ describe('setup status', () => {
1177
1308
  };
1178
1309
  },
1179
1310
  })).resolves.toBe(0);
1180
- expect(calls).toEqual(['model', 'embeddings', 'databases', 'sources', 'context', 'agents']);
1311
+ expect(calls).toEqual(['model', 'embeddings', 'databases', 'sources', 'runtime', 'context', 'agents']);
1181
1312
  });
1182
1313
  it('commits setup config changes written by later setup steps', async () => {
1183
1314
  const io = makeIo();
@@ -1205,6 +1336,7 @@ describe('setup status', () => {
1205
1336
  return { status: 'skipped', projectDir: tempDir };
1206
1337
  },
1207
1338
  sources: async () => ({ status: 'skipped', projectDir: tempDir }),
1339
+ runtime: async () => runtimeReady(tempDir),
1208
1340
  context: async () => ({ status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' }),
1209
1341
  agents: async () => ({
1210
1342
  status: 'ready',
@@ -1217,7 +1349,7 @@ describe('setup status', () => {
1217
1349
  const committedConfig = await execFileAsync('git', ['-C', tempDir, 'show', 'HEAD:ktx.yaml']);
1218
1350
  expect(committedConfig.stdout).toContain('warehouse:');
1219
1351
  });
1220
- it('runs agent setup after context succeeds in --agents mode', async () => {
1352
+ it('runs agent setup without runtime or context in --agents mode', async () => {
1221
1353
  const calls = [];
1222
1354
  const io = makeIo();
1223
1355
  await writeFile(join(tempDir, 'ktx.yaml'), ['connections: {}', ''].join('\n'), 'utf-8');
@@ -1242,9 +1374,13 @@ describe('setup status', () => {
1242
1374
  embeddings: async () => ({ status: 'skipped', projectDir: tempDir }),
1243
1375
  databases: async () => ({ status: 'skipped', projectDir: tempDir }),
1244
1376
  sources: async () => ({ status: 'skipped', projectDir: tempDir }),
1377
+ runtime: async () => {
1378
+ calls.push('runtime');
1379
+ throw new Error('runtime should not run');
1380
+ },
1245
1381
  context: async () => {
1246
1382
  calls.push('context');
1247
- return { status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' };
1383
+ throw new Error('context should not run');
1248
1384
  },
1249
1385
  agents: async () => {
1250
1386
  calls.push('agents');
@@ -1255,10 +1391,12 @@ describe('setup status', () => {
1255
1391
  };
1256
1392
  },
1257
1393
  })).resolves.toBe(0);
1258
- expect(calls).toEqual(['context', 'agents']);
1394
+ expect(calls).toEqual(['agents']);
1259
1395
  });
1260
- it('does not install agents when non-interactive --agents finds context incomplete', async () => {
1396
+ it('installs agents when non-interactive --agents finds context incomplete', async () => {
1261
1397
  const io = makeIo();
1398
+ const runtime = vi.fn(async () => runtimeReady(tempDir));
1399
+ const context = vi.fn(async () => ({ status: 'skipped', projectDir: tempDir }));
1262
1400
  const agents = vi.fn(async () => ({
1263
1401
  status: 'ready',
1264
1402
  projectDir: tempDir,
@@ -1282,11 +1420,14 @@ describe('setup status', () => {
1282
1420
  skipAgents: false,
1283
1421
  databaseSchemas: [],
1284
1422
  }, io.io, {
1285
- context: async () => ({ status: 'skipped', projectDir: tempDir }),
1423
+ runtime,
1424
+ context,
1286
1425
  agents,
1287
- })).resolves.toBe(1);
1288
- expect(agents).not.toHaveBeenCalled();
1289
- expect(io.stderr()).toContain('KTX context is not ready for agents.');
1426
+ })).resolves.toBe(0);
1427
+ expect(runtime).not.toHaveBeenCalled();
1428
+ expect(context).not.toHaveBeenCalled();
1429
+ expect(agents).toHaveBeenCalledTimes(1);
1430
+ expect(io.stderr()).not.toContain('KTX context is not ready for agents.');
1290
1431
  });
1291
1432
  it('routes a ready project menu selection to agent setup', async () => {
1292
1433
  const calls = [];
@@ -1309,7 +1450,7 @@ describe('setup status', () => {
1309
1450
  '',
1310
1451
  ].join('\n'), 'utf-8');
1311
1452
  await writeKtxSetupState(tempDir, {
1312
- completed_steps: ['project', 'llm', 'embeddings', 'sources', 'context', 'agents'],
1453
+ completed_steps: ['project', 'llm', 'embeddings', 'sources', 'runtime', 'context', 'agents'],
1313
1454
  });
1314
1455
  await writeFile(join(tempDir, '.ktx/agents/install-manifest.json'), JSON.stringify({
1315
1456
  version: 1,
@@ -1331,47 +1472,63 @@ describe('setup status', () => {
1331
1472
  retryableFailedTargets: [],
1332
1473
  commands: contextBuildCommands(tempDir, 'setup-context-local-ready'),
1333
1474
  });
1334
- await expect(runKtxSetup({
1335
- command: 'run',
1336
- projectDir: tempDir,
1337
- mode: 'existing',
1338
- agents: false,
1339
- inputMode: 'auto',
1340
- yes: false,
1341
- cliVersion: '0.2.0',
1342
- skipLlm: false,
1343
- skipEmbeddings: false,
1344
- skipDatabases: false,
1345
- skipSources: false,
1346
- skipAgents: false,
1347
- databaseSchemas: [],
1348
- }, io.io, {
1349
- readyMenuDeps: { prompts: { select: vi.fn(async () => 'agents'), cancel: vi.fn() } },
1350
- model: async (args) => {
1351
- expect(args.skipLlm).toBe(true);
1352
- return { status: 'skipped', projectDir: tempDir };
1353
- },
1354
- embeddings: async (args) => {
1355
- expect(args.skipEmbeddings).toBe(true);
1356
- return { status: 'skipped', projectDir: tempDir };
1357
- },
1358
- databases: async (args) => {
1359
- expect(args.skipDatabases).toBe(true);
1360
- return { status: 'skipped', projectDir: tempDir };
1361
- },
1362
- sources: async (args) => {
1363
- expect(args.skipSources).toBe(true);
1364
- return { status: 'skipped', projectDir: tempDir };
1365
- },
1366
- agents: async () => {
1367
- calls.push('agents');
1368
- return {
1369
- status: 'ready',
1370
- projectDir: tempDir,
1371
- installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
1372
- };
1373
- },
1374
- })).resolves.toBe(0);
1475
+ const previousRuntimeRoot = process.env.KTX_RUNTIME_ROOT;
1476
+ process.env.KTX_RUNTIME_ROOT = await writeReadyRuntime(tempDir);
1477
+ try {
1478
+ await expect(runKtxSetup({
1479
+ command: 'run',
1480
+ projectDir: tempDir,
1481
+ mode: 'existing',
1482
+ agents: false,
1483
+ inputMode: 'auto',
1484
+ yes: false,
1485
+ cliVersion: '0.2.0',
1486
+ skipLlm: false,
1487
+ skipEmbeddings: false,
1488
+ skipDatabases: false,
1489
+ skipSources: false,
1490
+ skipAgents: false,
1491
+ databaseSchemas: [],
1492
+ }, io.io, {
1493
+ readyMenuDeps: { prompts: { select: vi.fn(async () => 'agents'), cancel: vi.fn() } },
1494
+ model: async (args) => {
1495
+ expect(args.skipLlm).toBe(true);
1496
+ return { status: 'skipped', projectDir: tempDir };
1497
+ },
1498
+ embeddings: async (args) => {
1499
+ expect(args.skipEmbeddings).toBe(true);
1500
+ return { status: 'skipped', projectDir: tempDir };
1501
+ },
1502
+ databases: async (args) => {
1503
+ expect(args.skipDatabases).toBe(true);
1504
+ return { status: 'skipped', projectDir: tempDir };
1505
+ },
1506
+ sources: async (args) => {
1507
+ expect(args.skipSources).toBe(true);
1508
+ return { status: 'skipped', projectDir: tempDir };
1509
+ },
1510
+ runtime: async () => {
1511
+ calls.push('runtime');
1512
+ return runtimeReady(tempDir);
1513
+ },
1514
+ agents: async () => {
1515
+ calls.push('agents');
1516
+ return {
1517
+ status: 'ready',
1518
+ projectDir: tempDir,
1519
+ installs: [{ target: 'codex', scope: 'project', mode: 'mcp-cli' }],
1520
+ };
1521
+ },
1522
+ })).resolves.toBe(0);
1523
+ }
1524
+ finally {
1525
+ if (previousRuntimeRoot === undefined) {
1526
+ delete process.env.KTX_RUNTIME_ROOT;
1527
+ }
1528
+ else {
1529
+ process.env.KTX_RUNTIME_ROOT = previousRuntimeRoot;
1530
+ }
1531
+ }
1375
1532
  expect(calls).toEqual(['agents']);
1376
1533
  });
1377
1534
  it('skips to agent setup when context is ready but agents are not configured', async () => {
@@ -1442,6 +1599,10 @@ describe('setup status', () => {
1442
1599
  expect(args.skipSources).toBe(true);
1443
1600
  return { status: 'skipped', projectDir: tempDir };
1444
1601
  },
1602
+ runtime: async () => {
1603
+ calls.push('runtime');
1604
+ return runtimeReady(tempDir);
1605
+ },
1445
1606
  agents: async () => {
1446
1607
  calls.push('agents');
1447
1608
  return {
@@ -1454,8 +1615,9 @@ describe('setup status', () => {
1454
1615
  expect(readyMenuSelect).not.toHaveBeenCalled();
1455
1616
  expect(calls).toEqual(['agents']);
1456
1617
  });
1457
- it('runs only project resolution, context gate, and agent setup in --agents mode', async () => {
1618
+ it('runs only project resolution and agent setup in --agents mode', async () => {
1458
1619
  const io = makeIo();
1620
+ const runtime = vi.fn(async () => runtimeReady(tempDir));
1459
1621
  const context = vi.fn(async () => ({ status: 'ready', projectDir: tempDir, runId: 'setup-context-local-test' }));
1460
1622
  const agents = vi.fn(async () => ({
1461
1623
  status: 'ready',
@@ -1482,10 +1644,12 @@ describe('setup status', () => {
1482
1644
  model: async () => {
1483
1645
  throw new Error('model should not run');
1484
1646
  },
1647
+ runtime,
1485
1648
  context,
1486
1649
  agents,
1487
1650
  })).resolves.toBe(0);
1488
- expect(context).toHaveBeenCalledTimes(1);
1651
+ expect(runtime).not.toHaveBeenCalled();
1652
+ expect(context).not.toHaveBeenCalled();
1489
1653
  expect(agents).toHaveBeenCalledTimes(1);
1490
1654
  });
1491
1655
  it('does not run embedding setup when the model step fails', async () => {
package/dist/sl.js CHANGED
@@ -15,7 +15,7 @@ function slSearchEmbeddingService(project, deps) {
15
15
  }
16
16
  async function printSlSources(input) {
17
17
  const { resolveOutputMode } = await import('./io/mode.js');
18
- const { printList } = await import('./io/print-list.js');
18
+ const { createRankBadgeFormatter, printList } = await import('./io/print-list.js');
19
19
  const mode = resolveOutputMode({ explicit: input.output, json: input.json, io: input.io });
20
20
  if (input.command === 'sl search') {
21
21
  const searchColumns = [
@@ -24,7 +24,7 @@ async function printSlSources(input) {
24
24
  label: 'SCORE',
25
25
  plain: 'score=',
26
26
  role: 'badge',
27
- prettyFormat: (value) => `${Math.round(Number(value) * 100)}%`,
27
+ prettyFormat: createRankBadgeFormatter(input.rows),
28
28
  dim: true,
29
29
  },
30
30
  { key: 'connectionId', label: 'CONNECTION', plain: '' },
package/dist/sl.test.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
+ import { stripVTControlCharacters } from 'node:util';
4
5
  import Database from 'better-sqlite3';
5
6
  import { initKtxProject } from '@ktx/context/project';
6
7
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
@@ -72,6 +73,15 @@ describe('runKtxSl', () => {
72
73
  meta: { command: 'sl search' },
73
74
  });
74
75
  });
76
+ it('prints semantic-layer search rank badges in pretty output', async () => {
77
+ const projectDir = join(tempDir, 'rank-project');
78
+ await seedSlSource({ projectDir });
79
+ const searchIo = makeIo();
80
+ await expect(runKtxSl({ command: 'search', projectDir, connectionId: 'warehouse', query: 'order', output: 'pretty' }, searchIo.io)).resolves.toBe(0);
81
+ const stdout = stripVTControlCharacters(searchIo.stdout());
82
+ expect(stdout).toMatch(/#1\s+orders/);
83
+ expect(stdout).not.toContain('%');
84
+ });
75
85
  it('prints semantic-layer list and search as public JSON envelopes', async () => {
76
86
  const projectDir = join(tempDir, 'project');
77
87
  await seedSlSource({
@@ -1,5 +1,5 @@
1
1
  import { localConnectionToWarehouseDescriptor } from '@ktx/context/connections';
2
- import { DEFAULT_METABASE_CLIENT_CONFIG, DefaultLookerConnectionClientFactory, DefaultMetabaseConnectionClientFactory, KtxYamlMetabaseSourceStateReader, LocalLookerRuntimeStore, LocalMetabaseDiscoveryCache, computeLookerMappingDrift, computeMetabaseMappingDrift, discoverLookerConnections, discoverMetabaseDatabases, lookerCredentialsFromLocalConnection, metabaseRuntimeConfigFromLocalConnection, seedLocalMappingStateFromKtxYaml, validateLookerMappings, validateMappingPhysicalMatch, } from '@ktx/context/ingest';
2
+ import { DEFAULT_METABASE_CLIENT_CONFIG, DefaultLookerConnectionClientFactory, DefaultMetabaseConnectionClientFactory, KtxYamlMetabaseSourceStateReader, LocalLookerRuntimeStore, LocalMetabaseDiscoveryCache, computeLookerMappingDrift, computeMetabaseMappingDrift, discoverLookerConnections, discoverMetabaseDatabases, lookerCredentialsFromLocalConnection, metabaseRuntimeConfigFromLocalConnection, planMetabaseFanoutChildren, seedLocalMappingStateFromKtxYaml, validateLookerMappings, validateMappingPhysicalMatch, } from '@ktx/context/ingest';
3
3
  import { ktxLocalStateDbPath, loadKtxProject } from '@ktx/context/project';
4
4
  import { profileMark } from './startup-profile.js';
5
5
  profileMark('module:source-mapping');
@@ -126,6 +126,14 @@ export async function runKtxSourceMapping(args, io = process, deps = {}) {
126
126
  }
127
127
  }
128
128
  const rows = await store.listDatabaseMappings(args.connectionId);
129
+ planMetabaseFanoutChildren({
130
+ metabaseConnectionId: args.connectionId,
131
+ mappings: rows.map((row) => ({
132
+ metabaseDatabaseId: row.metabaseDatabaseId,
133
+ targetConnectionId: row.targetConnectionId,
134
+ syncEnabled: row.syncEnabled,
135
+ })),
136
+ });
129
137
  const failures = rows.flatMap((row) => {
130
138
  if (!row.targetConnectionId) {
131
139
  return [];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { runKtxSourceMapping } from './source-mapping.js';
6
+ function makeIo() {
7
+ let stdout = '';
8
+ let stderr = '';
9
+ return {
10
+ io: {
11
+ stdout: {
12
+ write: (chunk) => {
13
+ stdout += chunk;
14
+ },
15
+ },
16
+ stderr: {
17
+ write: (chunk) => {
18
+ stderr += chunk;
19
+ },
20
+ },
21
+ },
22
+ stdout: () => stdout,
23
+ stderr: () => stderr,
24
+ };
25
+ }
26
+ describe('source mapping commands', () => {
27
+ let tempDir;
28
+ beforeEach(async () => {
29
+ tempDir = await mkdtemp(join(tmpdir(), 'ktx-source-mapping-'));
30
+ });
31
+ afterEach(async () => {
32
+ await rm(tempDir, { recursive: true, force: true });
33
+ });
34
+ async function writeConfig(metabaseMappings) {
35
+ await writeFile(join(tempDir, 'ktx.yaml'), [
36
+ 'connections:',
37
+ ' warehouse:',
38
+ ' driver: postgres',
39
+ ' url: env:DATABASE_URL',
40
+ ' metabase:',
41
+ ' driver: metabase',
42
+ ' api_url: https://metabase.example.com',
43
+ ...metabaseMappings,
44
+ '',
45
+ ].join('\n'), 'utf-8');
46
+ }
47
+ it('fails Metabase validation when no sync-enabled target mapping exists', async () => {
48
+ await writeConfig([]);
49
+ const io = makeIo();
50
+ await expect(runKtxSourceMapping({ command: 'validate', projectDir: tempDir, connectionId: 'metabase' }, io.io)).resolves.toBe(1);
51
+ expect(io.stderr()).toContain('no sync-enabled mappings with a target connection for Metabase connection metabase');
52
+ });
53
+ it('passes Metabase validation when a sync-enabled target mapping exists', async () => {
54
+ await writeConfig([
55
+ ' mappings:',
56
+ ' databaseMappings:',
57
+ ' "3": warehouse',
58
+ ' syncEnabled:',
59
+ ' "3": true',
60
+ ]);
61
+ const io = makeIo();
62
+ await expect(runKtxSourceMapping({ command: 'validate', projectDir: tempDir, connectionId: 'metabase' }, io.io)).resolves.toBe(0);
63
+ expect(io.stdout()).toContain('Mapping validation passed: metabase');
64
+ });
65
+ });
package/dist/sql.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { loadKtxProject } from '@ktx/context/project';
2
+ import type { SqlAnalysisPort } from '@ktx/context/sql-analysis';
3
+ import type { KtxCliIo } from './cli-runtime.js';
4
+ import { createKtxCliScanConnector } from './local-scan-connectors.js';
5
+ type KtxSqlOutputMode = 'pretty' | 'plain' | 'json';
6
+ export type KtxSqlArgs = {
7
+ command: 'execute';
8
+ projectDir: string;
9
+ connectionId: string;
10
+ sql: string;
11
+ maxRows: number;
12
+ output?: KtxSqlOutputMode;
13
+ json?: boolean;
14
+ cliVersion: string;
15
+ };
16
+ export interface KtxSqlDeps {
17
+ loadProject?: typeof loadKtxProject;
18
+ createSqlAnalysis?: () => SqlAnalysisPort;
19
+ createScanConnector?: typeof createKtxCliScanConnector;
20
+ }
21
+ export declare function runKtxSql(args: KtxSqlArgs, io?: KtxCliIo, deps?: KtxSqlDeps): Promise<number>;
22
+ export {};