@kaelio/ktx 0.1.0 → 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 (122) hide show
  1. package/assets/python/{kaelio_ktx-0.1.0-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/setup-commands.js +14 -26
  20. package/dist/commands/sl-commands.js +27 -32
  21. package/dist/doctor.test.js +7 -8
  22. package/dist/example-smoke.test.js +3 -3
  23. package/dist/index.test.js +102 -70
  24. package/dist/ingest-depth.js +0 -1
  25. package/dist/ingest.test-utils.js +2 -2
  26. package/dist/ingest.test.js +4 -4
  27. package/dist/io/print-list.test.js +4 -4
  28. package/dist/knowledge.js +1 -1
  29. package/dist/managed-local-embeddings.d.ts +2 -0
  30. package/dist/managed-local-embeddings.js +2 -0
  31. package/dist/managed-local-embeddings.test.js +2 -0
  32. package/dist/managed-mcp-daemon.js +3 -2
  33. package/dist/managed-mcp-daemon.test.js +25 -0
  34. package/dist/managed-python-command.js +2 -2
  35. package/dist/managed-python-command.test.js +4 -3
  36. package/dist/managed-python-daemon.js +3 -2
  37. package/dist/managed-python-daemon.test.js +20 -0
  38. package/dist/managed-python-runtime.d.ts +5 -1
  39. package/dist/managed-python-runtime.js +50 -6
  40. package/dist/managed-python-runtime.test.js +53 -23
  41. package/dist/memory-flow-tui.test.js +2 -2
  42. package/dist/next-steps.d.ts +6 -6
  43. package/dist/next-steps.js +4 -4
  44. package/dist/next-steps.test.js +5 -5
  45. package/dist/print-command-tree.test.js +1 -1
  46. package/dist/proxy-env.d.ts +1 -0
  47. package/dist/proxy-env.js +23 -0
  48. package/dist/proxy-env.test.d.ts +1 -0
  49. package/dist/proxy-env.test.js +17 -0
  50. package/dist/public-ingest.js +3 -5
  51. package/dist/public-ingest.test.js +7 -3
  52. package/dist/runtime.test.js +2 -1
  53. package/dist/scan.test.js +2 -2
  54. package/dist/setup-agents.js +6 -4
  55. package/dist/setup-agents.test.js +35 -1
  56. package/dist/setup-embeddings.d.ts +1 -0
  57. package/dist/setup-embeddings.js +29 -7
  58. package/dist/setup-embeddings.test.js +49 -7
  59. package/dist/setup-models.d.ts +0 -1
  60. package/dist/setup-models.js +2 -3
  61. package/dist/setup-models.test.js +8 -10
  62. package/dist/setup-project.d.ts +9 -1
  63. package/dist/setup-project.js +52 -25
  64. package/dist/setup-project.test.js +8 -8
  65. package/dist/setup-runtime.test.js +4 -2
  66. package/dist/setup.d.ts +1 -2
  67. package/dist/setup.js +21 -5
  68. package/dist/setup.test.js +160 -43
  69. package/dist/sl.js +1 -1
  70. package/dist/sl.test.js +2 -1
  71. package/dist/standalone-smoke.test.js +8 -5
  72. package/dist/status-project.js +1 -10
  73. package/node_modules/@ktx/context/dist/index-sync/index.d.ts +2 -0
  74. package/node_modules/@ktx/context/dist/index-sync/index.js +1 -0
  75. package/node_modules/@ktx/context/dist/index-sync/reindex.d.ts +20 -0
  76. package/node_modules/@ktx/context/dist/index-sync/reindex.js +141 -0
  77. package/node_modules/@ktx/context/dist/index-sync/reindex.test.d.ts +1 -0
  78. package/node_modules/@ktx/context/dist/index-sync/reindex.test.js +139 -0
  79. package/node_modules/@ktx/context/dist/index-sync/types.d.ts +29 -0
  80. package/node_modules/@ktx/context/dist/index-sync/types.js +1 -0
  81. package/node_modules/@ktx/context/dist/index.d.ts +1 -0
  82. package/node_modules/@ktx/context/dist/index.js +1 -0
  83. package/node_modules/@ktx/context/dist/ingest/adapters/historic-sql/local-ingest-acceptance.test.js +1 -1
  84. package/node_modules/@ktx/context/dist/ingest/local-bundle-ingest.test.js +8 -8
  85. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.js +4 -1
  86. package/node_modules/@ktx/context/dist/ingest/local-bundle-runtime.test.js +3 -3
  87. package/node_modules/@ktx/context/dist/ingest/local-embedding-provider.integration.test.js +9 -10
  88. package/node_modules/@ktx/context/dist/ingest/memory-flow/schema.d.ts +2 -2
  89. package/node_modules/@ktx/context/dist/ingest/report-snapshot.d.ts +2 -2
  90. package/node_modules/@ktx/context/dist/llm/local-config.js +2 -15
  91. package/node_modules/@ktx/context/dist/llm/local-config.test.js +3 -7
  92. package/node_modules/@ktx/context/dist/memory/local-memory.js +9 -3
  93. package/node_modules/@ktx/context/dist/project/config.d.ts +0 -5
  94. package/node_modules/@ktx/context/dist/project/config.js +5 -5
  95. package/node_modules/@ktx/context/dist/project/config.test.js +4 -7
  96. package/node_modules/@ktx/context/dist/scan/enrichment-state.test.js +4 -4
  97. package/node_modules/@ktx/context/dist/scan/index.d.ts +1 -1
  98. package/node_modules/@ktx/context/dist/scan/local-enrichment.d.ts +2 -6
  99. package/node_modules/@ktx/context/dist/scan/local-enrichment.js +31 -47
  100. package/node_modules/@ktx/context/dist/scan/local-enrichment.test.js +35 -18
  101. package/node_modules/@ktx/context/dist/scan/local-scan.test.js +2 -3
  102. package/node_modules/@ktx/context/dist/sl/ports.d.ts +3 -3
  103. package/node_modules/@ktx/context/dist/sl/sl-search.service.d.ts +3 -2
  104. package/node_modules/@ktx/context/dist/sl/sl-search.service.js +47 -45
  105. package/node_modules/@ktx/context/dist/sl/sl-search.service.test.js +61 -0
  106. package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.d.ts +4 -3
  107. package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.js +15 -5
  108. package/node_modules/@ktx/context/dist/sl/sqlite-sl-sources-index.test.js +24 -0
  109. package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.d.ts +3 -2
  110. package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.js +62 -51
  111. package/node_modules/@ktx/context/dist/wiki/knowledge-wiki.service.test.js +59 -3
  112. package/node_modules/@ktx/context/dist/wiki/ports.d.ts +3 -3
  113. package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.d.ts +33 -0
  114. package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.js +155 -2
  115. package/node_modules/@ktx/context/dist/wiki/sqlite-knowledge-index.test.js +26 -0
  116. package/node_modules/@ktx/context/package.json +5 -0
  117. package/node_modules/@ktx/llm/dist/embedding-provider.d.ts +0 -7
  118. package/node_modules/@ktx/llm/dist/embedding-provider.js +12 -138
  119. package/node_modules/@ktx/llm/dist/embedding-provider.test.js +10 -25
  120. package/node_modules/@ktx/llm/dist/types.d.ts +1 -1
  121. package/package.json +1 -1
  122. /package/dist/{dev.test.d.ts → admin-reindex.test.d.ts} +0 -0
package/dist/scan.test.js CHANGED
@@ -287,8 +287,8 @@ describe('runKtxScan', () => {
287
287
  expect(io.stdout()).toContain('Report: raw-sources/warehouse/live-database/sync-1/scan-report.json');
288
288
  expect(io.stdout()).toContain('Next:\n');
289
289
  expect(io.stdout()).toContain('ktx status --project-dir ');
290
- expect(io.stdout()).not.toContain('ktx dev scan status');
291
- expect(io.stdout()).not.toContain('ktx dev scan report');
290
+ expect(io.stdout()).not.toContain('ktx admin scan status');
291
+ expect(io.stdout()).not.toContain('ktx admin scan report');
292
292
  expect(io.stdout()).not.toContain('\u001b[');
293
293
  expect(io.stdout()).not.toContain('✓');
294
294
  expect(io.stdout()).not.toContain('+1');
@@ -426,8 +426,8 @@ function cliInstructionContent(input) {
426
426
  'Available commands:',
427
427
  '',
428
428
  `- \`${ktxCommandLine(input.launcher, ['status', ...jsonProjectDirArgs])}\``,
429
- `- \`${ktxCommandLine(input.launcher, ['sl', 'list', ...jsonProjectDirArgs])}\``,
430
- `- \`${ktxCommandLine(input.launcher, ['sl', 'search', '<text>', ...jsonProjectDirArgs, '--connection-id', '<id>'])}\``,
429
+ `- \`${ktxCommandLine(input.launcher, ['sl', ...jsonProjectDirArgs])}\``,
430
+ `- \`${ktxCommandLine(input.launcher, ['sl', '<text>', ...jsonProjectDirArgs, '--connection-id', '<id>'])}\``,
431
431
  `- \`${ktxCommandLine(input.launcher, [
432
432
  'sl',
433
433
  'query',
@@ -442,7 +442,7 @@ function cliInstructionContent(input) {
442
442
  '--max-rows',
443
443
  '100',
444
444
  ])}\``,
445
- `- \`${ktxCommandLine(input.launcher, ['wiki', 'search', '<query>', ...jsonProjectDirArgs, '--limit', '10'])}\``,
445
+ `- \`${ktxCommandLine(input.launcher, ['wiki', '<query>', ...jsonProjectDirArgs, '--limit', '10'])}\``,
446
446
  '',
447
447
  'Use semantic-layer queries before direct database access. Do not print secrets or credential references.',
448
448
  '',
@@ -965,7 +965,9 @@ export async function runKtxSetupAgentsStep(args, io, deps = {}) {
965
965
  if (targets.includes('back'))
966
966
  return { status: 'back', projectDir: args.projectDir };
967
967
  if (targets.length === 0) {
968
- io.stderr.write('Missing agent target: pass --target or use interactive setup.\n');
968
+ io.stderr.write(args.inputMode === 'disabled'
969
+ ? 'Run in a TTY, or pass --target <target>.\n'
970
+ : 'Missing agent target: pass --target or use interactive setup.\n');
969
971
  return { status: 'missing-input', projectDir: args.projectDir };
970
972
  }
971
973
  const scopeTargets = targets.filter((target) => target !== 'claude-desktop');
@@ -149,7 +149,7 @@ describe('setup agents', () => {
149
149
  expect(skill).toContain(`--project-dir ${tempDir}`);
150
150
  expect(skill).toContain('must not print secrets');
151
151
  expect(skill).toContain('status --json');
152
- expect(skill).toContain('sl list --json');
152
+ expect(skill).toContain('sl --json');
153
153
  expect(skill).toContain('sl query');
154
154
  expect(skill).toContain('--format json');
155
155
  expect(skill).not.toContain('sl query --json');
@@ -163,6 +163,40 @@ describe('setup agents', () => {
163
163
  expect(await readKtxSetupState(tempDir)).toEqual({ completed_steps: ['agents'] });
164
164
  expect(io.stderr()).toBe('');
165
165
  });
166
+ it('installs a specified target in non-interactive mode without --yes', async () => {
167
+ const io = makeIo();
168
+ await expect(runKtxSetupAgentsStep({
169
+ projectDir: tempDir,
170
+ inputMode: 'disabled',
171
+ yes: false,
172
+ agents: true,
173
+ target: 'claude-code',
174
+ scope: 'project',
175
+ mode: 'mcp',
176
+ skipAgents: false,
177
+ }, io.io)).resolves.toMatchObject({
178
+ status: 'ready',
179
+ projectDir: tempDir,
180
+ installs: [{ target: 'claude-code', scope: 'project', mode: 'mcp' }],
181
+ });
182
+ await expect(stat(join(tempDir, '.claude/skills/ktx-analytics/SKILL.md'))).resolves.toBeDefined();
183
+ const mcpConfig = JSON.parse(await readFile(join(tempDir, '.mcp.json'), 'utf-8'));
184
+ expect(mcpConfig.mcpServers).toHaveProperty('ktx');
185
+ expect(io.stderr()).toBe('');
186
+ });
187
+ it('prints concrete target guidance when non-interactive agent setup has no target', async () => {
188
+ const io = makeIo();
189
+ await expect(runKtxSetupAgentsStep({
190
+ projectDir: tempDir,
191
+ inputMode: 'disabled',
192
+ yes: false,
193
+ agents: true,
194
+ scope: 'project',
195
+ mode: 'mcp',
196
+ skipAgents: false,
197
+ }, io.io)).resolves.toEqual({ status: 'missing-input', projectDir: tempDir });
198
+ expect(io.stderr()).toBe('Run in a TTY, or pass --target <target>.\n');
199
+ });
166
200
  it('prints standalone agent next actions after successful installation', async () => {
167
201
  const io = makeIo();
168
202
  const result = await runKtxSetupAgentsStep({
@@ -49,6 +49,7 @@ export interface KtxSetupEmbeddingsDeps {
49
49
  healthCheck?: (config: KtxEmbeddingConfig) => Promise<KtxEmbeddingHealthCheckResult>;
50
50
  ensureLocalEmbeddings?: (options: {
51
51
  cliVersion: string;
52
+ projectDir: string;
52
53
  installPolicy: KtxManagedPythonInstallPolicy;
53
54
  io: KtxCliIo;
54
55
  }) => Promise<ManagedLocalEmbeddingsDaemon>;
@@ -1,4 +1,4 @@
1
- import { writeFile } from 'node:fs/promises';
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
2
  import { resolveKtxConfigReference } from '@ktx/context/core';
3
3
  import { loadKtxProject, markKtxSetupStateStepComplete, readKtxSetupState, serializeKtxProjectConfig, } from '@ktx/context/project';
4
4
  import { runKtxEmbeddingHealthCheck } from '@ktx/llm';
@@ -20,13 +20,13 @@ const LOCAL_EMBEDDING_BACKEND = 'sentence-transformers';
20
20
  const EMBEDDING_OPTION_PROMPT_CONTEXT = 'KTX uses embeddings for semantic search over semantic-layer sources, wiki context, schema metadata, ' +
21
21
  'and relationship evidence.';
22
22
  const LOCAL_EMBEDDING_HEALTH_TIMEOUT_MS = 120_000;
23
+ const LOCAL_EMBEDDING_STDERR_TAIL_LINES = 40;
23
24
  function createPromptAdapter() {
24
25
  return createKtxSetupPromptAdapter({ selectCancelValue: 'back' });
25
26
  }
26
27
  async function hasCompletedEmbeddings(projectDir, config) {
27
28
  return ((await readKtxSetupState(projectDir)).completed_steps.includes('embeddings') &&
28
29
  config.ingest.embeddings.backend !== 'none' &&
29
- config.ingest.embeddings.backend !== 'deterministic' &&
30
30
  typeof config.ingest.embeddings.model === 'string' &&
31
31
  config.ingest.embeddings.model.length > 0 &&
32
32
  config.ingest.embeddings.dimensions > 0);
@@ -190,14 +190,33 @@ async function chooseEmbeddingBackend(args, deps) {
190
190
  }
191
191
  return 'back';
192
192
  }
193
- function localEmbeddingSetupMessage(message) {
194
- return [
193
+ async function readLocalEmbeddingDaemonStderrTail(stderrLog) {
194
+ if (!stderrLog) {
195
+ return [];
196
+ }
197
+ try {
198
+ const lines = (await readFile(stderrLog, 'utf8'))
199
+ .split(/\r?\n/)
200
+ .map((line) => line.trimEnd())
201
+ .filter((line) => line.trim().length > 0);
202
+ return lines.slice(-LOCAL_EMBEDDING_STDERR_TAIL_LINES);
203
+ }
204
+ catch {
205
+ return [];
206
+ }
207
+ }
208
+ function localEmbeddingSetupMessage(message, stderrTail = []) {
209
+ const lines = [
195
210
  `Local embedding health check failed: ${message}`,
196
211
  'Local embeddings use the KTX-managed Python runtime.',
197
- 'Prepare the runtime with: ktx dev runtime start --feature local-embeddings',
212
+ 'Prepare the runtime with: ktx admin runtime start --feature local-embeddings',
198
213
  'Use --yes with setup to install and start the runtime without prompting.',
199
214
  'The first run may download Python packages and the all-MiniLM-L6-v2 model.',
200
- ].join('\n');
215
+ ];
216
+ if (stderrTail.length > 0) {
217
+ lines.push('Recent local embeddings daemon stderr:', ...stderrTail);
218
+ }
219
+ return lines.join('\n');
201
220
  }
202
221
  async function promptAfterLocalEmbeddingFailure(deps) {
203
222
  const choice = await (deps.prompts ?? createPromptAdapter()).select({
@@ -324,8 +343,11 @@ export async function runKtxSetupEmbeddingsStep(args, io, deps = {}) {
324
343
  return { status: 'ready', projectDir: args.projectDir };
325
344
  }
326
345
  progress.fail('Embedding test failed');
346
+ const stderrTail = selectedBackend === 'sentence-transformers'
347
+ ? await readLocalEmbeddingDaemonStderrTail(managedLocalEmbeddings?.stderrLog)
348
+ : [];
327
349
  io.stderr.write(selectedBackend === 'sentence-transformers'
328
- ? `${localEmbeddingSetupMessage(health.message)}\n`
350
+ ? `${localEmbeddingSetupMessage(health.message, stderrTail)}\n`
329
351
  : `Embedding health check failed: ${health.message}\n`);
330
352
  if (args.inputMode === 'disabled') {
331
353
  return { status: 'failed', projectDir: args.projectDir };
@@ -39,9 +39,11 @@ function makePromptAdapter(options) {
39
39
  cancel: vi.fn(),
40
40
  };
41
41
  }
42
- function managedDaemon(baseUrl = 'http://127.0.0.1:61234') {
42
+ function managedDaemon(baseUrl = 'http://127.0.0.1:61234', logs = {}) {
43
43
  return {
44
44
  baseUrl,
45
+ stdoutLog: logs.stdoutLog ?? '/tmp/ktx-daemon.stdout.log',
46
+ stderrLog: logs.stderrLog ?? '/tmp/ktx-daemon.stderr.log',
45
47
  env: {
46
48
  KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL: baseUrl,
47
49
  },
@@ -223,7 +225,7 @@ describe('setup embeddings step', () => {
223
225
  it('fails non-interactive local setup when the managed local embeddings runtime is missing', async () => {
224
226
  const io = makeIo();
225
227
  const ensureLocalEmbeddings = vi.fn(async () => {
226
- throw new Error('KTX Python runtime is required for this command. Run: ktx dev runtime install --feature local-embeddings --yes');
228
+ throw new Error('KTX Python runtime is required for this command. Run: ktx admin runtime install --feature local-embeddings --yes');
227
229
  });
228
230
  const result = await runKtxSetupEmbeddingsStep({
229
231
  projectDir: tempDir,
@@ -233,7 +235,7 @@ describe('setup embeddings step', () => {
233
235
  skipEmbeddings: false,
234
236
  }, io.io, { env: {}, ensureLocalEmbeddings });
235
237
  expect(result.status).toBe('failed');
236
- expect(io.stderr()).toContain('KTX Python runtime is required for this command. Run: ktx dev runtime install --feature local-embeddings --yes');
238
+ expect(io.stderr()).toContain('KTX Python runtime is required for this command. Run: ktx admin runtime install --feature local-embeddings --yes');
237
239
  });
238
240
  it('does not persist embedding completion when the health check fails', async () => {
239
241
  const io = makeIo();
@@ -251,11 +253,51 @@ describe('setup embeddings step', () => {
251
253
  expect(result.status).toBe('failed');
252
254
  const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
253
255
  expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
254
- expect(config.ingest.embeddings.backend).toBe('deterministic');
256
+ expect(config.ingest.embeddings.backend).toBe('none');
255
257
  expect(io.stderr()).toContain('Local embedding health check failed: 401 invalid api key [redacted]');
256
- expect(io.stderr()).toContain('Prepare the runtime with: ktx dev runtime start --feature local-embeddings');
258
+ expect(io.stderr()).toContain('Prepare the runtime with: ktx admin runtime start --feature local-embeddings');
257
259
  expect(io.stderr()).not.toContain('skip for now');
258
260
  });
261
+ it('prints the recent daemon stderr tail when local embedding health check fails', async () => {
262
+ const io = makeIo();
263
+ const stderrLog = join(tempDir, '.ktx', 'runtime', 'daemon.stderr.log');
264
+ await mkdir(join(tempDir, '.ktx', 'runtime'), { recursive: true });
265
+ await writeFile(stderrLog, Array.from({ length: 45 }, (_value, index) => `daemon traceback line ${index + 1}`).join('\n'));
266
+ const result = await runKtxSetupEmbeddingsStep({
267
+ projectDir: tempDir,
268
+ inputMode: 'disabled',
269
+ cliVersion: '0.2.0',
270
+ runtimeInstallPolicy: 'auto',
271
+ skipEmbeddings: false,
272
+ }, io.io, {
273
+ env: {},
274
+ ensureLocalEmbeddings: vi.fn(async () => managedDaemon('http://127.0.0.1:61234', { stderrLog })),
275
+ healthCheck: vi.fn(async () => ({ ok: false, message: 'HTTP 500' })),
276
+ });
277
+ expect(result.status).toBe('failed');
278
+ expect(io.stderr()).toContain('Recent local embeddings daemon stderr:');
279
+ expect(io.stderr()).toContain('daemon traceback line 6');
280
+ expect(io.stderr()).toContain('daemon traceback line 45');
281
+ expect(io.stderr()).not.toContain('daemon traceback line 5');
282
+ });
283
+ it('does not print daemon stderr diagnostics when the log is unavailable or empty', async () => {
284
+ const io = makeIo();
285
+ const result = await runKtxSetupEmbeddingsStep({
286
+ projectDir: tempDir,
287
+ inputMode: 'disabled',
288
+ cliVersion: '0.2.0',
289
+ runtimeInstallPolicy: 'auto',
290
+ skipEmbeddings: false,
291
+ }, io.io, {
292
+ env: {},
293
+ ensureLocalEmbeddings: vi.fn(async () => managedDaemon('http://127.0.0.1:61234', {
294
+ stderrLog: join(tempDir, '.ktx', 'runtime', 'missing.stderr.log'),
295
+ })),
296
+ healthCheck: vi.fn(async () => ({ ok: false, message: 'HTTP 500' })),
297
+ });
298
+ expect(result.status).toBe('failed');
299
+ expect(io.stderr()).not.toContain('Recent local embeddings daemon stderr:');
300
+ });
259
301
  it('uses fixed OpenAI defaults and only asks for credentials when OpenAI is selected', async () => {
260
302
  const io = makeIo();
261
303
  const healthCheck = vi.fn(async () => ({ ok: true }));
@@ -342,7 +384,7 @@ describe('setup embeddings step', () => {
342
384
  expect(result.status).toBe('skipped');
343
385
  const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
344
386
  expect(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
345
- expect(config.ingest.embeddings.backend).toBe('deterministic');
387
+ expect(config.ingest.embeddings.backend).toBe('none');
346
388
  });
347
389
  it('returns back without writing config when the local health check fails and Back is selected', async () => {
348
390
  const prompts = makePromptAdapter({ selectValues: ['sentence-transformers', 'back'] });
@@ -360,7 +402,7 @@ describe('setup embeddings step', () => {
360
402
  });
361
403
  expect(result.status).toBe('back');
362
404
  const config = parseKtxProjectConfig(await readFile(join(tempDir, 'ktx.yaml'), 'utf-8'));
363
- expect(config.ingest.embeddings.backend).toBe('deterministic');
405
+ expect(config.ingest.embeddings.backend).toBe('none');
364
406
  });
365
407
  it('preserves already completed embeddings setup when no embedding args request changes', async () => {
366
408
  await mkdir(join(tempDir, '.ktx'), { recursive: true });
@@ -10,7 +10,6 @@ export interface KtxSetupModelArgs {
10
10
  anthropicApiKeyEnv?: string;
11
11
  anthropicApiKeyFile?: string;
12
12
  llmModel?: string;
13
- anthropicModel?: string;
14
13
  vertexProject?: string;
15
14
  vertexLocation?: string;
16
15
  forcePrompt?: boolean;
@@ -312,13 +312,13 @@ function requestedBackend(args) {
312
312
  if (args.vertexProject || args.vertexLocation) {
313
313
  return 'vertex';
314
314
  }
315
- if (args.anthropicApiKeyEnv || args.anthropicApiKeyFile || args.llmModel || args.anthropicModel) {
315
+ if (args.anthropicApiKeyEnv || args.anthropicApiKeyFile || args.llmModel) {
316
316
  return 'anthropic';
317
317
  }
318
318
  return undefined;
319
319
  }
320
320
  function requestedModel(args) {
321
- return args.llmModel ?? args.anthropicModel;
321
+ return args.llmModel;
322
322
  }
323
323
  async function chooseBackend(args, io, deps) {
324
324
  const explicit = requestedBackend(args);
@@ -701,7 +701,6 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
701
701
  !args.anthropicApiKeyEnv &&
702
702
  !args.anthropicApiKeyFile &&
703
703
  !args.llmModel &&
704
- !args.anthropicModel &&
705
704
  !args.vertexProject &&
706
705
  !args.vertexLocation) {
707
706
  io.stdout.write(`│ LLM ready: yes (${project.config.llm.models.default})\n`);
@@ -216,7 +216,7 @@ describe('setup Anthropic model step', () => {
216
216
  projectDir: tempDir,
217
217
  inputMode: 'disabled',
218
218
  anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
219
- anthropicModel: 'claude-sonnet-4-6',
219
+ llmModel: 'claude-sonnet-4-6',
220
220
  skipLlm: false,
221
221
  }, io.io, {
222
222
  env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, // pragma: allowlist secret
@@ -253,7 +253,7 @@ describe('setup Anthropic model step', () => {
253
253
  llmBackend: 'vertex',
254
254
  vertexProject: 'local-gcp-project',
255
255
  vertexLocation: 'us-east5',
256
- anthropicModel: 'claude-sonnet-4-6',
256
+ llmModel: 'claude-sonnet-4-6',
257
257
  skipLlm: false,
258
258
  }, io.io, { env: {}, healthCheck, spinner });
259
259
  expect(result.status).toBe('ready');
@@ -476,7 +476,7 @@ describe('setup Anthropic model step', () => {
476
476
  llmBackend: 'vertex',
477
477
  vertexProject: 'kaelio-orbit-looker-20260430',
478
478
  vertexLocation: 'us-east5',
479
- anthropicModel: 'claude-sonnet-4-6',
479
+ llmModel: 'claude-sonnet-4-6',
480
480
  skipLlm: false,
481
481
  }, io.io, {
482
482
  env: {},
@@ -497,7 +497,7 @@ describe('setup Anthropic model step', () => {
497
497
  projectDir: tempDir,
498
498
  inputMode: 'disabled',
499
499
  anthropicApiKeyFile: secretPath,
500
- anthropicModel: 'claude-sonnet-4-6',
500
+ llmModel: 'claude-sonnet-4-6',
501
501
  skipLlm: false,
502
502
  }, io.io, { env: {}, healthCheck });
503
503
  expect(result.status).toBe('ready');
@@ -525,7 +525,7 @@ describe('setup Anthropic model step', () => {
525
525
  projectDir: tempDir,
526
526
  inputMode: 'disabled',
527
527
  anthropicApiKeyFile: missingSecretPath,
528
- anthropicModel: 'claude-sonnet-4-6',
528
+ llmModel: 'claude-sonnet-4-6',
529
529
  skipLlm: false,
530
530
  }, io.io, { env: {}, healthCheck });
531
531
  expect(result.status).toBe('missing-input');
@@ -720,7 +720,7 @@ describe('setup Anthropic model step', () => {
720
720
  projectDir: tempDir,
721
721
  inputMode: 'disabled',
722
722
  anthropicApiKeyEnv: 'ANTHROPIC_API_KEY', // pragma: allowlist secret
723
- anthropicModel: 'claude-sonnet-4-6',
723
+ llmModel: 'claude-sonnet-4-6',
724
724
  skipLlm: false,
725
725
  }, io.io, {
726
726
  env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, // pragma: allowlist secret
@@ -830,8 +830,7 @@ describe('setup Anthropic model step', () => {
830
830
  ' default: claude-sonnet-4-6',
831
831
  'ingest:',
832
832
  ' embeddings:',
833
- ' backend: deterministic',
834
- ' model: deterministic',
833
+ ' backend: none',
835
834
  ' dimensions: 8',
836
835
  ].join('\n'), 'utf-8');
837
836
  await writeKtxSetupState(tempDir, { completed_steps: ['project', 'llm'] });
@@ -865,8 +864,7 @@ describe('setup Anthropic model step', () => {
865
864
  ` default: ${fixture.model}`,
866
865
  'ingest:',
867
866
  ' embeddings:',
868
- ' backend: deterministic',
869
- ' model: deterministic',
867
+ ' backend: none',
870
868
  ' dimensions: 8',
871
869
  ].join('\n'), 'utf-8');
872
870
  await writeKtxSetupState(tempDir, { completed_steps: ['project', 'llm'] });
@@ -1,7 +1,7 @@
1
1
  import { initKtxProject, type KtxLocalProject, loadKtxProject } from '@ktx/context/project';
2
2
  import type { KtxCliIo } from './cli-runtime.js';
3
3
  import { type KtxSetupPromptOption } from './setup-prompts.js';
4
- export type KtxSetupProjectMode = 'auto' | 'new' | 'existing' | 'prompt-new';
4
+ export type KtxSetupProjectMode = 'auto' | 'prompt-new';
5
5
  export type KtxSetupInputMode = 'auto' | 'disabled';
6
6
  export interface KtxSetupProjectArgs {
7
7
  projectDir: string;
@@ -10,11 +10,19 @@ export interface KtxSetupProjectArgs {
10
10
  yes: boolean;
11
11
  allowBack?: boolean;
12
12
  }
13
+ export type KtxSetupCreatedProjectCleanup = {
14
+ kind: 'remove-project-dir';
15
+ projectDir: string;
16
+ } | {
17
+ kind: 'remove-ktx-scaffold';
18
+ projectDir: string;
19
+ };
13
20
  export type KtxSetupProjectResult = {
14
21
  status: 'ready';
15
22
  projectDir: string;
16
23
  project: KtxLocalProject;
17
24
  confirmedCreation?: boolean;
25
+ createdProjectCleanup?: KtxSetupCreatedProjectCleanup;
18
26
  } | {
19
27
  status: 'back';
20
28
  projectDir: string;
@@ -37,6 +37,15 @@ async function existingFolderState(projectDir) {
37
37
  throw error;
38
38
  }
39
39
  }
40
+ function cleanupForFolderState(projectDir, state) {
41
+ if (state === 'missing') {
42
+ return { kind: 'remove-project-dir', projectDir };
43
+ }
44
+ if (state === 'empty-directory') {
45
+ return { kind: 'remove-ktx-scaffold', projectDir };
46
+ }
47
+ return undefined;
48
+ }
40
49
  async function confirmProjectDir(selectedDir, io, prompts) {
41
50
  const state = await existingFolderState(selectedDir);
42
51
  if (state === 'not-directory') {
@@ -75,7 +84,7 @@ async function confirmProjectDir(selectedDir, io, prompts) {
75
84
  return { status: 'back' };
76
85
  if (action !== 'create')
77
86
  return { status: 'cancelled' };
78
- return { status: 'confirmed', confirmedCreation: true };
87
+ return { status: 'confirmed', confirmedCreation: true, createdProjectCleanup: cleanupForFolderState(selectedDir, state) };
79
88
  }
80
89
  async function normalizeSetupGitignore(projectDir) {
81
90
  const gitignorePath = join(projectDir, '.ktx/.gitignore');
@@ -147,34 +156,34 @@ async function promptForNewProjectDir(projectDir, homeDir, io, prompts) {
147
156
  return { status: 'back', projectDir };
148
157
  if (confirmed.status === 'cancelled')
149
158
  return { status: 'cancelled', projectDir };
150
- return { status: 'selected', projectDir: selectedDir, confirmedCreation: confirmed.confirmedCreation };
159
+ return {
160
+ status: 'selected',
161
+ projectDir: selectedDir,
162
+ confirmedCreation: confirmed.confirmedCreation,
163
+ ...(confirmed.createdProjectCleanup ? { createdProjectCleanup: confirmed.createdProjectCleanup } : {}),
164
+ };
151
165
  }
152
166
  }
167
+ async function createProjectWithCleanup(projectDir, deps) {
168
+ const state = await existingFolderState(projectDir);
169
+ const project = await createProject(projectDir, deps);
170
+ const createdProjectCleanup = cleanupForFolderState(projectDir, state);
171
+ return {
172
+ project,
173
+ ...(createdProjectCleanup ? { createdProjectCleanup } : {}),
174
+ };
175
+ }
153
176
  export async function runKtxSetupProjectStep(args, io, deps = {}) {
154
177
  const projectDir = resolve(args.projectDir);
155
178
  const homeDir = deps.homeDir ?? homedir();
156
179
  const exists = hasProjectConfig(projectDir);
157
- if (args.mode === 'existing') {
158
- if (!exists) {
159
- io.stderr.write(`No existing KTX project found at ${projectDir}. Pass --new to create it.\n`);
160
- return { status: 'missing-input', projectDir };
161
- }
162
- const project = await loadExistingProject(projectDir, deps);
163
- printProjectSummary(io, projectDir);
164
- return { status: 'ready', projectDir, project };
165
- }
166
- if (args.mode === 'new') {
167
- const project = await createProject(projectDir, deps);
168
- printProjectSummary(io, projectDir);
169
- return { status: 'ready', projectDir, project };
170
- }
171
180
  if (args.mode === 'prompt-new') {
172
181
  if (args.inputMode === 'disabled') {
173
- io.stderr.write('Missing new project folder: pass --new --project-dir to create a project without prompts.\n');
182
+ io.stderr.write('Missing new project folder: pass --project-dir and --yes to create a project without prompts.\n');
174
183
  return { status: 'missing-input', projectDir };
175
184
  }
176
185
  if (!io.stdout.isTTY && !deps.prompts) {
177
- io.stderr.write('Missing new project folder: pass --new --project-dir to create a project outside an interactive terminal.\n');
186
+ io.stderr.write('Missing new project folder: pass --project-dir and --yes to create a project outside an interactive terminal.\n');
178
187
  return { status: 'missing-input', projectDir };
179
188
  }
180
189
  const prompts = deps.prompts ?? createClackSetupProjectPromptAdapter();
@@ -192,6 +201,7 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
192
201
  projectDir: selected.projectDir,
193
202
  project,
194
203
  confirmedCreation: selected.confirmedCreation,
204
+ ...(selected.createdProjectCleanup ? { createdProjectCleanup: selected.createdProjectCleanup } : {}),
195
205
  };
196
206
  }
197
207
  if (exists) {
@@ -201,15 +211,20 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
201
211
  }
202
212
  if (args.inputMode === 'disabled') {
203
213
  if (!args.yes) {
204
- io.stderr.write('Missing setup choice: pass --new or --yes to create a project in non-interactive setup.\n');
214
+ io.stderr.write('Missing setup choice: pass --yes to create a project in non-interactive setup.\n');
205
215
  return { status: 'missing-input', projectDir };
206
216
  }
207
- const project = await createProject(projectDir, deps);
217
+ const { project, createdProjectCleanup } = await createProjectWithCleanup(projectDir, deps);
208
218
  printProjectSummary(io, projectDir);
209
- return { status: 'ready', projectDir, project };
219
+ return {
220
+ status: 'ready',
221
+ projectDir,
222
+ project,
223
+ ...(createdProjectCleanup ? { createdProjectCleanup } : {}),
224
+ };
210
225
  }
211
226
  if (!io.stdout.isTTY && !deps.prompts) {
212
- io.stderr.write('Missing setup choice: pass --new or --yes to create a project outside an interactive terminal.\n');
227
+ io.stderr.write('Missing setup choice: pass --yes to create a project outside an interactive terminal.\n');
213
228
  return { status: 'missing-input', projectDir };
214
229
  }
215
230
  const prompts = deps.prompts ?? createClackSetupProjectPromptAdapter();
@@ -238,9 +253,14 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
238
253
  return { status: 'cancelled', projectDir };
239
254
  }
240
255
  if (choice === 'current') {
241
- const project = await createProject(projectDir, deps);
256
+ const { project, createdProjectCleanup } = await createProjectWithCleanup(projectDir, deps);
242
257
  printProjectSummary(io, projectDir);
243
- return { status: 'ready', projectDir, project };
258
+ return {
259
+ status: 'ready',
260
+ projectDir,
261
+ project,
262
+ ...(createdProjectCleanup ? { createdProjectCleanup } : {}),
263
+ };
244
264
  }
245
265
  if (choice === 'new-default') {
246
266
  const confirmed = await confirmProjectDir(defaultProjectDir, io, prompts);
@@ -257,6 +277,7 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
257
277
  projectDir: defaultProjectDir,
258
278
  project,
259
279
  confirmedCreation: confirmed.confirmedCreation,
280
+ ...(confirmed.createdProjectCleanup ? { createdProjectCleanup: confirmed.createdProjectCleanup } : {}),
260
281
  };
261
282
  }
262
283
  if (choice === 'new-custom') {
@@ -281,7 +302,13 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
281
302
  return { status: 'cancelled', projectDir };
282
303
  const project = await createProject(customDir, deps);
283
304
  printProjectSummary(io, customDir);
284
- return { status: 'ready', projectDir: customDir, project, confirmedCreation: confirmed.confirmedCreation };
305
+ return {
306
+ status: 'ready',
307
+ projectDir: customDir,
308
+ project,
309
+ confirmedCreation: confirmed.confirmedCreation,
310
+ ...(confirmed.createdProjectCleanup ? { createdProjectCleanup: confirmed.createdProjectCleanup } : {}),
311
+ };
285
312
  }
286
313
  prompts.cancel('Setup cancelled.');
287
314
  return { status: 'cancelled', projectDir };
@@ -48,10 +48,10 @@ describe('setup project step', () => {
48
48
  afterEach(async () => {
49
49
  await rm(tempDir, { recursive: true, force: true });
50
50
  });
51
- it('creates a new project with --new and marks the project step complete', async () => {
51
+ it('creates a new project in non-interactive auto mode with --yes and marks the project step complete', async () => {
52
52
  const projectDir = join(tempDir, 'warehouse');
53
53
  const testIo = makeIo();
54
- const result = await runKtxSetupProjectStep({ projectDir, mode: 'new', inputMode: 'disabled', yes: false }, testIo.io);
54
+ const result = await runKtxSetupProjectStep({ projectDir, mode: 'auto', inputMode: 'disabled', yes: true }, testIo.io);
55
55
  expect(result.status).toBe('ready');
56
56
  expect(result.projectDir).toBe(projectDir);
57
57
  expect(await readFile(join(projectDir, 'ktx.yaml'), 'utf-8')).not.toContain('completed_steps:');
@@ -61,7 +61,7 @@ describe('setup project step', () => {
61
61
  expect(testIo.stdout()).toContain(`Project: ${projectDir}`);
62
62
  expect(testIo.stderr()).toBe('');
63
63
  });
64
- it('loads an existing project with --existing and drops config setup progress', async () => {
64
+ it('loads an existing project in auto mode and drops config setup progress', async () => {
65
65
  const projectDir = join(tempDir, 'warehouse');
66
66
  await initKtxProject({ projectDir });
67
67
  await writeFile(join(projectDir, 'ktx.yaml'), [
@@ -72,7 +72,7 @@ describe('setup project step', () => {
72
72
  ' - llm',
73
73
  'connections: {}',
74
74
  ].join('\n'), 'utf-8');
75
- const result = await runKtxSetupProjectStep({ projectDir, mode: 'existing', inputMode: 'disabled', yes: false }, makeIo().io);
75
+ const result = await runKtxSetupProjectStep({ projectDir, mode: 'auto', inputMode: 'disabled', yes: false }, makeIo().io);
76
76
  expect(result.status).toBe('ready');
77
77
  const config = parseKtxProjectConfig(await readFile(join(projectDir, 'ktx.yaml'), 'utf-8'));
78
78
  expect(config.setup).toEqual({
@@ -86,16 +86,16 @@ describe('setup project step', () => {
86
86
  const rejectedIo = makeIo();
87
87
  const acceptedIo = makeIo();
88
88
  await expect(runKtxSetupProjectStep({ projectDir, mode: 'auto', inputMode: 'disabled', yes: false }, rejectedIo.io)).resolves.toMatchObject({ status: 'missing-input' });
89
- expect(rejectedIo.stderr()).toContain('Missing setup choice: pass --new or --yes');
89
+ expect(rejectedIo.stderr()).toContain('Missing setup choice: pass --yes');
90
90
  await expect(stat(join(projectDir, 'ktx.yaml'))).rejects.toThrow();
91
91
  await expect(runKtxSetupProjectStep({ projectDir, mode: 'auto', inputMode: 'disabled', yes: true }, acceptedIo.io)).resolves.toMatchObject({ status: 'ready', projectDir });
92
92
  await expect(stat(join(projectDir, 'ktx.yaml'))).resolves.toBeDefined();
93
93
  });
94
- it('fails --existing clearly when ktx.yaml is missing', async () => {
94
+ it('fails clearly in no-input auto mode when ktx.yaml is missing and --yes is absent', async () => {
95
95
  const projectDir = join(tempDir, 'warehouse');
96
96
  const testIo = makeIo();
97
- await expect(runKtxSetupProjectStep({ projectDir, mode: 'existing', inputMode: 'disabled', yes: false }, testIo.io)).resolves.toMatchObject({ status: 'missing-input' });
98
- expect(testIo.stderr()).toContain(`No existing KTX project found at ${projectDir}`);
97
+ await expect(runKtxSetupProjectStep({ projectDir, mode: 'auto', inputMode: 'disabled', yes: false }, testIo.io)).resolves.toMatchObject({ status: 'missing-input' });
98
+ expect(testIo.stderr()).toContain('Missing setup choice: pass --yes');
99
99
  });
100
100
  it('prompts to use the current directory and creates a project in interactive auto mode', async () => {
101
101
  const projectDir = join(tempDir, 'warehouse');
@@ -56,7 +56,7 @@ describe('runKtxSetupRuntimeStep', () => {
56
56
  it('fails fast when required runtime features cannot be installed in no-input mode', async () => {
57
57
  const io = makeIo();
58
58
  const ensureRuntime = vi.fn(async () => {
59
- throw new Error('KTX Python runtime is required for this command. Run: ktx dev runtime install --yes');
59
+ throw new Error('KTX Python runtime is required for this command. Run: ktx admin runtime install --yes');
60
60
  });
61
61
  await expect(runKtxSetupRuntimeStep({
62
62
  projectDir: tempDir,
@@ -71,12 +71,14 @@ describe('runKtxSetupRuntimeStep', () => {
71
71
  })).resolves.toMatchObject({ status: 'failed' });
72
72
  expect(ensureRuntime).toHaveBeenCalledWith(expect.objectContaining({ installPolicy: 'never' }));
73
73
  expect((await readKtxSetupState(tempDir)).completed_steps).not.toContain('runtime');
74
- expect(io.stderr()).toContain('ktx dev runtime install --yes');
74
+ expect(io.stderr()).toContain('ktx admin runtime install --yes');
75
75
  });
76
76
  it('starts the managed local embeddings daemon for configured sentence-transformers embeddings', async () => {
77
77
  const io = makeIo();
78
78
  const ensureLocalEmbeddings = vi.fn(async () => ({
79
79
  baseUrl: 'http://127.0.0.1:61234',
80
+ stdoutLog: join(tempDir, '.ktx', 'runtime', 'daemon.stdout.log'),
81
+ stderrLog: join(tempDir, '.ktx', 'runtime', 'daemon.stderr.log'),
80
82
  env: { KTX_MANAGED_SENTENCE_TRANSFORMERS_BASE_URL: 'http://127.0.0.1:61234' },
81
83
  }));
82
84
  const config = {