@kaelio/ktx 0.10.0 → 0.12.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 (193) hide show
  1. package/assets/python/{kaelio_ktx-0.10.0-py3-none-any.whl → kaelio_ktx-0.12.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/admin.js +1 -1
  5. package/dist/clack.d.ts +16 -0
  6. package/dist/clack.js +37 -6
  7. package/dist/claude-code-prompt-caching.js +1 -1
  8. package/dist/cli-program.js +7 -3
  9. package/dist/cli-runtime.d.ts +2 -0
  10. package/dist/cli-runtime.js +14 -8
  11. package/dist/commands/connection-commands.js +1 -1
  12. package/dist/commands/ingest-commands.js +4 -4
  13. package/dist/commands/mcp-commands.js +12 -12
  14. package/dist/commands/runtime-commands.js +4 -4
  15. package/dist/commands/setup-commands.js +6 -5
  16. package/dist/commands/sl-commands.js +1 -1
  17. package/dist/commands/sql-commands.js +1 -1
  18. package/dist/commands/status-commands.js +1 -1
  19. package/dist/community-cta.d.ts +11 -0
  20. package/dist/community-cta.js +19 -0
  21. package/dist/connection.js +1 -1
  22. package/dist/connectors/clickhouse/connector.js +1 -1
  23. package/dist/connectors/mysql/connector.js +1 -1
  24. package/dist/connectors/snowflake/connector.d.ts +1 -1
  25. package/dist/connectors/sqlite/connector.js +2 -25
  26. package/dist/connectors/sqlserver/connector.js +3 -3
  27. package/dist/context/connections/connection-type.d.ts +1 -1
  28. package/dist/context/connections/read-only-sql.d.ts +1 -0
  29. package/dist/context/connections/read-only-sql.js +116 -2
  30. package/dist/context/core/git-env.d.ts +12 -1
  31. package/dist/context/core/git-env.js +17 -2
  32. package/dist/context/core/git.service.d.ts +23 -0
  33. package/dist/context/core/git.service.js +86 -15
  34. package/dist/context/ingest/adapters/historic-sql/projection.js +2 -1
  35. package/dist/context/ingest/adapters/looker/client.js +7 -2
  36. package/dist/context/ingest/adapters/looker/factory.d.ts +8 -1
  37. package/dist/context/ingest/adapters/looker/factory.js +9 -0
  38. package/dist/context/ingest/adapters/looker/mapping.js +1 -1
  39. package/dist/context/ingest/adapters/looker/types.d.ts +1 -1
  40. package/dist/context/ingest/adapters/metabase/client.d.ts +1 -1
  41. package/dist/context/ingest/adapters/metabase/client.js +1 -1
  42. package/dist/context/ingest/adapters/metabase/local-metabase.adapter.js +1 -1
  43. package/dist/context/ingest/adapters/metabase/mapping.js +6 -6
  44. package/dist/context/ingest/artifact-gates.d.ts +2 -6
  45. package/dist/context/ingest/artifact-gates.js +5 -47
  46. package/dist/context/ingest/constrained-repair.d.ts +55 -0
  47. package/dist/context/ingest/constrained-repair.js +167 -0
  48. package/dist/context/ingest/final-gate-repair.d.ts +9 -11
  49. package/dist/context/ingest/final-gate-repair.js +40 -128
  50. package/dist/context/ingest/finalization-scope.d.ts +1 -1
  51. package/dist/context/ingest/finalization-scope.js +15 -15
  52. package/dist/context/ingest/ingest-bundle.runner.d.ts +1 -0
  53. package/dist/context/ingest/ingest-bundle.runner.js +101 -67
  54. package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +6 -13
  55. package/dist/context/ingest/isolated-diff/patch-integrator.js +32 -109
  56. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +8 -9
  57. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +63 -141
  58. package/dist/context/ingest/local-bundle-runtime.d.ts +2 -0
  59. package/dist/context/ingest/local-bundle-runtime.js +9 -10
  60. package/dist/context/ingest/local-ingest.d.ts +2 -0
  61. package/dist/context/ingest/local-ingest.js +2 -0
  62. package/dist/context/ingest/memory-flow/view-model.js +1 -1
  63. package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -6
  64. package/dist/context/ingest/stages/stage-3-work-units.js +2 -1
  65. package/dist/context/ingest/stages/validate-wu-sources.d.ts +7 -1
  66. package/dist/context/ingest/stages/validate-wu-sources.js +109 -4
  67. package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.d.ts +2 -0
  68. package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.js +1 -1
  69. package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +3 -3
  70. package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.d.ts +3 -1
  71. package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +15 -1
  72. package/dist/context/llm/ai-sdk-runtime.js +2 -2
  73. package/dist/context/llm/claude-code-runtime.js +1 -1
  74. package/dist/context/llm/local-config.js +1 -1
  75. package/dist/context/llm/runtime-tools.js +2 -2
  76. package/dist/context/mcp/context-tools.js +7 -7
  77. package/dist/context/mcp/local-project-ports.js +23 -54
  78. package/dist/context/memory/local-memory.js +4 -1
  79. package/dist/context/memory/memory-agent.service.js +1 -1
  80. package/dist/context/project/config.d.ts +11 -4
  81. package/dist/context/project/config.js +85 -30
  82. package/dist/context/project/driver-schemas.js +1 -1
  83. package/dist/context/project/mappings-yaml-schema.js +2 -2
  84. package/dist/context/project/project.js +12 -4
  85. package/dist/context/scan/description-generation.js +4 -4
  86. package/dist/context/scan/local-enrichment-artifacts.js +2 -1
  87. package/dist/context/scan/local-scan.js +2 -2
  88. package/dist/context/scan/local-structural-artifacts.js +5 -5
  89. package/dist/context/scan/relationship-benchmark-report.js +1 -1
  90. package/dist/context/scan/relationship-discovery.js +3 -3
  91. package/dist/context/scan/relationship-llm-proposal.js +3 -3
  92. package/dist/context/sl/local-query.js +3 -33
  93. package/dist/context/sl/local-sl.d.ts +0 -8
  94. package/dist/context/sl/local-sl.js +44 -69
  95. package/dist/context/sl/semantic-layer.service.d.ts +25 -8
  96. package/dist/context/sl/semantic-layer.service.js +109 -56
  97. package/dist/context/sl/source-files.d.ts +46 -0
  98. package/dist/context/sl/source-files.js +131 -0
  99. package/dist/context/sl/tools/base-semantic-layer.tool.d.ts +2 -2
  100. package/dist/context/sl/tools/base-semantic-layer.tool.js +2 -7
  101. package/dist/context/sl/tools/sl-edit-source.tool.js +10 -8
  102. package/dist/context/sl/tools/sl-warehouse-validation.js +55 -27
  103. package/dist/context/sl/tools/sl-write-source.tool.js +12 -9
  104. package/dist/context/sql-analysis/dialect.d.ts +2 -0
  105. package/dist/context/sql-analysis/dialect.js +20 -0
  106. package/dist/context/tools/base-tool.d.ts +6 -19
  107. package/dist/context/tools/base-tool.js +0 -14
  108. package/dist/context-build-view.js +5 -5
  109. package/dist/database-tree-picker.js +18 -3
  110. package/dist/demo-assets.js +0 -1
  111. package/dist/doctor.d.ts +1 -1
  112. package/dist/doctor.js +31 -23
  113. package/dist/errors.d.ts +31 -0
  114. package/dist/errors.js +44 -0
  115. package/dist/ingest.d.ts +1 -1
  116. package/dist/ingest.js +8 -2
  117. package/dist/io/symbols.d.ts +2 -0
  118. package/dist/io/symbols.js +2 -0
  119. package/dist/io/tty.d.ts +17 -0
  120. package/dist/io/tty.js +21 -0
  121. package/dist/links.d.ts +1 -0
  122. package/dist/links.js +1 -0
  123. package/dist/llm/embedding-health.js +1 -1
  124. package/dist/llm/embedding-provider.js +3 -3
  125. package/dist/llm/model-provider.js +1 -1
  126. package/dist/local-adapters.d.ts +1 -0
  127. package/dist/local-adapters.js +2 -2
  128. package/dist/local-scan-connectors.js +1 -1
  129. package/dist/managed-local-embeddings.js +17 -8
  130. package/dist/managed-mcp-daemon.js +3 -3
  131. package/dist/managed-python-command.d.ts +7 -0
  132. package/dist/managed-python-command.js +34 -8
  133. package/dist/managed-python-daemon.js +2 -2
  134. package/dist/managed-python-http.js +3 -3
  135. package/dist/managed-python-runtime.d.ts +30 -1
  136. package/dist/managed-python-runtime.js +134 -18
  137. package/dist/managed-uv-release.d.ts +7 -0
  138. package/dist/managed-uv-release.js +11 -0
  139. package/dist/mcp-http-server.js +4 -4
  140. package/dist/mcp-server-factory.js +3 -3
  141. package/dist/mcp-stdio-server.js +1 -1
  142. package/dist/memory-flow-hud.js +2 -2
  143. package/dist/next-steps.js +2 -2
  144. package/dist/prompt-navigation.d.ts +17 -0
  145. package/dist/prompt-navigation.js +49 -3
  146. package/dist/prompts/memory_agent_bundle_ingest_work_unit.md +2 -2
  147. package/dist/prompts/memory_agent_external_ingest.md +2 -2
  148. package/dist/public-ingest-copy.js +1 -1
  149. package/dist/public-ingest.js +3 -3
  150. package/dist/release-version.js +1 -1
  151. package/dist/runtime-requirements.js +1 -1
  152. package/dist/runtime.js +9 -9
  153. package/dist/scan.js +1 -1
  154. package/dist/setup-agents.js +22 -35
  155. package/dist/setup-banner.d.ts +20 -0
  156. package/dist/setup-banner.js +39 -0
  157. package/dist/setup-context.js +24 -15
  158. package/dist/setup-databases.js +31 -59
  159. package/dist/setup-demo-tour.js +12 -8
  160. package/dist/setup-embeddings.js +9 -9
  161. package/dist/setup-interrupt.js +1 -1
  162. package/dist/setup-models.d.ts +4 -1
  163. package/dist/setup-models.js +54 -28
  164. package/dist/setup-project.js +29 -5
  165. package/dist/setup-prompts.js +16 -5
  166. package/dist/setup-ready-menu.js +1 -1
  167. package/dist/setup-sources.js +27 -7
  168. package/dist/setup.d.ts +25 -0
  169. package/dist/setup.js +90 -19
  170. package/dist/skills/analytics/SKILL.md +3 -3
  171. package/dist/skills/dbt_ingest/SKILL.md +3 -3
  172. package/dist/skills/looker_ingest/SKILL.md +3 -3
  173. package/dist/skills/lookml_ingest/SKILL.md +7 -7
  174. package/dist/skills/metabase_ingest/SKILL.md +4 -4
  175. package/dist/skills/metricflow_ingest/SKILL.md +15 -15
  176. package/dist/skills/notion_synthesize/SKILL.md +1 -1
  177. package/dist/skills/sl/SKILL.md +3 -3
  178. package/dist/skills/sl_capture/SKILL.md +1 -1
  179. package/dist/skills/wiki_capture/SKILL.md +1 -1
  180. package/dist/source-mapping.js +1 -1
  181. package/dist/startup-profile.js +1 -1
  182. package/dist/status-project.d.ts +0 -2
  183. package/dist/status-project.js +4 -6
  184. package/dist/telemetry/command-hook.d.ts +24 -0
  185. package/dist/telemetry/command-hook.js +37 -3
  186. package/dist/telemetry/events.d.ts +1 -1
  187. package/dist/telemetry/exception.js +14 -0
  188. package/dist/telemetry/index.d.ts +2 -2
  189. package/dist/telemetry/index.js +2 -2
  190. package/dist/text-ingest.js +1 -1
  191. package/dist/tree-picker-tui.d.ts +0 -1
  192. package/dist/tree-picker-tui.js +2 -3
  193. package/package.json +1 -1
@@ -2,7 +2,7 @@ import { stdin } from 'node:process';
2
2
  import { cancel, confirm, isCancel as isClackCancel } from '@clack/prompts';
3
3
  export class KtxSetupExitError extends Error {
4
4
  constructor() {
5
- super('KTX setup exit requested');
5
+ super('ktx setup exit requested');
6
6
  this.name = 'KtxSetupExitError';
7
7
  }
8
8
  }
@@ -32,7 +32,10 @@ export type KtxSetupModelResult = {
32
32
  status: 'failed';
33
33
  projectDir: string;
34
34
  };
35
- export type KtxSetupLlmBackend = 'anthropic' | 'vertex' | 'claude-code' | 'codex';
35
+ declare const KTX_SETUP_LLM_BACKENDS: readonly ["claude-code", "codex", "anthropic", "vertex"];
36
+ export type KtxSetupLlmBackend = (typeof KTX_SETUP_LLM_BACKENDS)[number];
37
+ /** Validates a raw CLI or prompt value against the setup-selectable LLM backends. */
38
+ export declare function isKtxSetupLlmBackend(value: string): value is KtxSetupLlmBackend;
36
39
  /** @internal */
37
40
  export interface KtxSetupModelPromptAdapter {
38
41
  select(options: {
@@ -21,10 +21,28 @@ const ESC = String.fromCharCode(0x1b);
21
21
  function yellow(text) {
22
22
  return `${ESC}[33m${text}${ESC}[39m`;
23
23
  }
24
- const ANTHROPIC_CREDENTIAL_PROMPT_CONTEXT = 'KTX uses the key to verify Anthropic model access now and to run ingest agents that turn schemas, SQL, ' +
24
+ // Single source of truth for the LLM backends a user can pick during setup.
25
+ // The CLI arg parser, the interactive prompt, and the missing-backend error all
26
+ // derive from this list, so adding a backend is one edit. Order is the prompt's
27
+ // preference order (subscription backends first).
28
+ const KTX_SETUP_LLM_BACKENDS = ['claude-code', 'codex', 'anthropic', 'vertex'];
29
+ /** Validates a raw CLI or prompt value against the setup-selectable LLM backends. */
30
+ export function isKtxSetupLlmBackend(value) {
31
+ return KTX_SETUP_LLM_BACKENDS.some((backend) => backend === value);
32
+ }
33
+ // Display labels for the interactive provider prompt. The Record key type forces
34
+ // every backend to carry a label, so adding one to KTX_SETUP_LLM_BACKENDS fails
35
+ // to compile until its prompt option exists here.
36
+ const KTX_SETUP_LLM_BACKEND_LABELS = {
37
+ 'claude-code': 'Claude subscription (Pro/Max)',
38
+ codex: 'Codex subscription',
39
+ anthropic: 'Anthropic API key',
40
+ vertex: 'Google Vertex AI for Anthropic Claude',
41
+ };
42
+ const ANTHROPIC_CREDENTIAL_PROMPT_CONTEXT = 'ktx uses the key to verify Anthropic model access now and to run ingest agents that turn schemas, SQL, ' +
25
43
  'BI metadata, and docs into semantic-layer sources and wiki context. ktx.yaml stores an env: or file: ' +
26
44
  'reference, not the raw key.';
27
- const VERTEX_PROJECT_PROMPT_CONTEXT = 'KTX stores the selected Google Cloud project ID in ktx.yaml and uses Application Default Credentials for ' +
45
+ const VERTEX_PROJECT_PROMPT_CONTEXT = 'ktx stores the selected Google Cloud project ID in ktx.yaml and uses Application Default Credentials for ' +
28
46
  'access. Project visibility depends on the signed-in Google account and organization permissions.';
29
47
  const DEFAULT_VERTEX_LOCATION = 'us-east5';
30
48
  const ANTHROPIC_PRESET = {
@@ -61,6 +79,14 @@ function presetForBackend(backend) {
61
79
  return MODEL_PRESETS[backend];
62
80
  }
63
81
  const execFileAsync = promisify(execFile);
82
+ // Non-interactive setup cannot pick a provider safely: every backend needs
83
+ // something the user must supply (an API key, gcloud ADC, or a logged-in local
84
+ // CLI), so there is no credential-free default to fall back to. Name the hidden
85
+ // --llm-backend flag and its choices here instead, mirroring how the other
86
+ // automation errors guide users to the flag they need.
87
+ const MISSING_LLM_BACKEND_MESSAGE = `Missing LLM backend: pass --llm-backend with one of ${KTX_SETUP_LLM_BACKENDS.join(', ')}. ` +
88
+ 'claude-code and codex use local CLI authentication; anthropic also needs --anthropic-api-key-env or ' +
89
+ '--anthropic-api-key-file, and vertex also needs --vertex-project.';
64
90
  function createPromptAdapter() {
65
91
  return createKtxSetupPromptAdapter({ selectCancelValue: 'back' });
66
92
  }
@@ -171,10 +197,10 @@ function buildVertexHealthConfig(vertex, model) {
171
197
  promptCaching: { enabled: true, vertexFallbackTo5m: true },
172
198
  };
173
199
  }
174
- function llmHealthCheckStartText(provider, model) {
200
+ function llmCheckStartText(provider, model) {
175
201
  return `Checking ${provider} LLM (${model}).`;
176
202
  }
177
- function startLlmHealthCheckProgress(spinner, message) {
203
+ function startLlmCheckProgress(spinner, message) {
178
204
  spinner.start(message);
179
205
  return {
180
206
  succeed(msg) {
@@ -185,23 +211,23 @@ function startLlmHealthCheckProgress(spinner, message) {
185
211
  },
186
212
  };
187
213
  }
188
- async function runLlmHealthCheckWithProgress(config, provider, model, healthCheck, deps) {
189
- const progress = startLlmHealthCheckProgress((deps.spinner ?? createClackSpinner)(), llmHealthCheckStartText(provider, model));
190
- let health;
214
+ async function validateModelWithProgress(provider, model, deps, run) {
215
+ const progress = startLlmCheckProgress((deps.spinner ?? createClackSpinner)(), llmCheckStartText(provider, model));
216
+ let result;
191
217
  try {
192
- health = await healthCheck(config);
218
+ result = await run();
193
219
  }
194
220
  catch (error) {
195
221
  progress.fail('LLM test failed');
196
222
  throw error;
197
223
  }
198
- if (health.ok) {
224
+ if (result.ok) {
199
225
  progress.succeed(`LLM test passed (${provider}, ${model})`);
200
226
  }
201
227
  else {
202
228
  progress.fail('LLM test failed');
203
229
  }
204
- return health;
230
+ return result;
205
231
  }
206
232
  function formatVertexHealthFailure(message, vertex) {
207
233
  const trimmed = message.trim() || 'unknown error';
@@ -248,7 +274,7 @@ async function chooseCredentialRef(args, io, deps) {
248
274
  }
249
275
  while (true) {
250
276
  const choice = await prompts.select({
251
- message: `How should KTX find your Anthropic API key?\n\n${ANTHROPIC_CREDENTIAL_PROMPT_CONTEXT}`,
277
+ message: `How should ktx find your Anthropic API key?\n\n${ANTHROPIC_CREDENTIAL_PROMPT_CONTEXT}`,
252
278
  options: [
253
279
  { value: 'paste', label: 'Paste a key and save it as a local secret file' },
254
280
  { value: 'env', label: 'Use ANTHROPIC_API_KEY from the environment' },
@@ -259,7 +285,7 @@ async function chooseCredentialRef(args, io, deps) {
259
285
  return { status: 'back' };
260
286
  }
261
287
  if (choice === 'paste') {
262
- io.stdout.write('│ KTX will save the key in .ktx/secrets/anthropic-api-key with local file permissions, then write a file: reference in ktx.yaml.\n');
288
+ io.stdout.write('│ ktx will save the key in .ktx/secrets/anthropic-api-key with local file permissions, then write a file: reference in ktx.yaml.\n');
263
289
  const value = await prompts.password({ message: withTextInputNavigation('Anthropic API key') });
264
290
  if (value === undefined) {
265
291
  continue;
@@ -301,30 +327,30 @@ async function chooseBackend(args, io, deps) {
301
327
  return { status: 'ready', backend: explicit, prompted: false };
302
328
  }
303
329
  if (args.inputMode === 'disabled') {
304
- return { status: 'ready', backend: 'anthropic', prompted: false };
330
+ io.stderr.write(`${MISSING_LLM_BACKEND_MESSAGE}\n`);
331
+ return { status: 'missing-input' };
305
332
  }
306
333
  const prompts = deps.prompts ?? createPromptAdapter();
307
334
  if (args.showPromptInstructions !== false) {
308
335
  io.stdout.write('│ Use Up/Down to move, Enter to confirm the current selection, choose Back to return to the previous step, Ctrl+C to exit.\n');
309
336
  }
310
337
  const choice = await prompts.select({
311
- message: 'Which LLM provider should KTX use?',
338
+ message: 'Which LLM provider should ktx use?',
312
339
  options: [
313
- { value: 'claude-code', label: 'Claude subscription (Pro/Max)' },
314
- { value: 'codex', label: 'Codex subscription' },
315
- { value: 'anthropic', label: 'Anthropic API key' },
316
- { value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' },
340
+ ...KTX_SETUP_LLM_BACKENDS.map((backend) => ({ value: backend, label: KTX_SETUP_LLM_BACKEND_LABELS[backend] })),
317
341
  { value: 'back', label: 'Back' },
318
342
  ],
319
343
  });
320
344
  if (choice === 'back') {
321
345
  return { status: 'back' };
322
346
  }
323
- return {
324
- status: 'ready',
325
- backend: choice === 'vertex' || choice === 'claude-code' || choice === 'codex' ? choice : 'anthropic',
326
- prompted: true,
327
- };
347
+ if (isKtxSetupLlmBackend(choice)) {
348
+ return { status: 'ready', backend: choice, prompted: true };
349
+ }
350
+ // Options are derived from KTX_SETUP_LLM_BACKENDS, so the only other value is
351
+ // 'back' (handled above). Treat any unexpected value as a cancel rather than
352
+ // silently assuming a provider.
353
+ return { status: 'back' };
328
354
  }
329
355
  function resolveProvidedVertexRef(label, ref, env, io) {
330
356
  let value;
@@ -404,7 +430,7 @@ async function chooseInteractiveVertexProject(currentProject, io, deps) {
404
430
  io.stdout.write('│ gcloud did not return any visible Google Cloud projects. Enter a project ID manually or choose Back.\n');
405
431
  }
406
432
  const choice = await prompts.autocomplete({
407
- message: `Which Google Cloud project should KTX use for Vertex AI?\n\n${[
433
+ message: `Which Google Cloud project should ktx use for Vertex AI?\n\n${[
408
434
  VERTEX_PROJECT_PROMPT_CONTEXT,
409
435
  listFailureMessage,
410
436
  ]
@@ -616,7 +642,7 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
616
642
  return { status: vertex.status, projectDir: args.projectDir };
617
643
  }
618
644
  const preset = presetForBackend('vertex');
619
- const validation = await validatePresetModels(preset, async (model) => runLlmHealthCheckWithProgress(buildVertexHealthConfig(vertex.values, model), 'Vertex AI', model, healthCheck, deps), io);
645
+ const validation = await validatePresetModels(preset, (model) => validateModelWithProgress('Vertex AI', model, deps, () => healthCheck(buildVertexHealthConfig(vertex.values, model))), io);
620
646
  if (validation.status !== 'ready') {
621
647
  io.stderr.write(`Vertex AI Anthropic model health check failed: ${formatVertexHealthFailure(validation.message, vertex.values)}\n`);
622
648
  if (args.inputMode === 'disabled') {
@@ -633,7 +659,7 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
633
659
  if (backendChoice.backend === 'claude-code') {
634
660
  const preset = presetForBackend('claude-code');
635
661
  const probe = deps.claudeCodeAuthProbe ?? runClaudeCodeAuthProbe;
636
- const validation = await validatePresetModels(preset, async (model) => probe({ projectDir: args.projectDir, model, env: deps.env ?? process.env }), io);
662
+ const validation = await validatePresetModels(preset, (model) => validateModelWithProgress('Claude subscription', model, deps, () => probe({ projectDir: args.projectDir, model, env: deps.env ?? process.env })), io);
637
663
  if (validation.status !== 'ready') {
638
664
  io.stderr.write(`${validation.message}\n`);
639
665
  return { status: 'failed', projectDir: args.projectDir };
@@ -649,7 +675,7 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
649
675
  if (backendChoice.backend === 'codex') {
650
676
  const preset = presetForBackend('codex');
651
677
  const probe = deps.codexAuthProbe ?? runCodexAuthProbe;
652
- const validation = await validatePresetModels(preset, async (model) => probe({ projectDir: args.projectDir, model }), io);
678
+ const validation = await validatePresetModels(preset, (model) => validateModelWithProgress('Codex', model, deps, () => probe({ projectDir: args.projectDir, model })), io);
653
679
  if (validation.status !== 'ready') {
654
680
  io.stderr.write(`${validation.message}\n`);
655
681
  return { status: 'failed', projectDir: args.projectDir };
@@ -670,7 +696,7 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
670
696
  return { status: credential.status, projectDir: args.projectDir };
671
697
  }
672
698
  const preset = presetForBackend('anthropic');
673
- const validation = await validatePresetModels(preset, async (model) => runLlmHealthCheckWithProgress(buildAnthropicHealthConfig(credential.value, model), 'Anthropic API', model, healthCheck, deps), io);
699
+ const validation = await validatePresetModels(preset, (model) => validateModelWithProgress('Anthropic API', model, deps, () => healthCheck(buildAnthropicHealthConfig(credential.value, model))), io);
674
700
  if (validation.status !== 'ready') {
675
701
  io.stderr.write(`Anthropic model health check failed: ${validation.message}\n`);
676
702
  if (args.inputMode === 'disabled') {
@@ -2,6 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { join, resolve } from 'node:path';
5
+ import { classifyKtxRepoOwnership } from './context/core/git.service.js';
5
6
  import { initKtxProject, loadKtxProject } from './context/project/project.js';
6
7
  import { markKtxSetupStateStepComplete, mergeKtxSetupGitignoreEntries } from './context/project/setup-config.js';
7
8
  import { serializeKtxProjectConfig } from './context/project/config.js';
@@ -39,7 +40,24 @@ async function existingFolderState(projectDir) {
39
40
  throw error;
40
41
  }
41
42
  }
43
+ /**
44
+ * ktx owns the git repository at the project dir, so it refuses to create a
45
+ * project inside a repository it did not create (which it would otherwise have
46
+ * to adopt or fail on at first commit). Guides the user toward a dedicated
47
+ * directory instead of letting `GitService.initialize()` throw mid-setup.
48
+ */
49
+ async function ensureProjectDirIsOwnable(selectedDir, io) {
50
+ if ((await classifyKtxRepoOwnership(selectedDir)) === 'foreign') {
51
+ io.stderr.write(`${selectedDir} is already a git repository that ktx did not create.\n` +
52
+ 'ktx keeps its context in a repository it owns. Choose a new subfolder or an empty directory instead.\n');
53
+ return false;
54
+ }
55
+ return true;
56
+ }
42
57
  async function confirmProjectDir(selectedDir, io, prompts) {
58
+ if (!(await ensureProjectDirIsOwnable(selectedDir, io))) {
59
+ return { status: 'choose-another' };
60
+ }
43
61
  const state = await existingFolderState(selectedDir);
44
62
  if (state === 'not-directory') {
45
63
  io.stderr.write(`Project folder path exists and is not a directory: ${selectedDir}\n`);
@@ -49,7 +67,7 @@ async function confirmProjectDir(selectedDir, io, prompts) {
49
67
  const action = await prompts.select({
50
68
  message: `That folder already exists and is not empty: ${selectedDir}`,
51
69
  options: [
52
- { value: 'use-existing', label: 'Yes, create KTX files there' },
70
+ { value: 'use-existing', label: 'Yes, create ktx files there' },
53
71
  { value: 'choose-another', label: 'Choose another folder' },
54
72
  { value: 'back', label: 'Back' },
55
73
  ],
@@ -62,9 +80,9 @@ async function confirmProjectDir(selectedDir, io, prompts) {
62
80
  return { status: 'cancelled' };
63
81
  return { status: 'confirmed', confirmedCreation: true };
64
82
  }
65
- io.stdout.write(`│ KTX will create:\n│ ${selectedDir}\n`);
83
+ io.stdout.write(`│ ktx will create:\n│ ${selectedDir}\n`);
66
84
  const action = await prompts.select({
67
- message: `Create KTX project at ${selectedDir}?`,
85
+ message: `Create ktx project at ${selectedDir}?`,
68
86
  options: [
69
87
  { value: 'create', label: 'Create project' },
70
88
  { value: 'choose-another', label: 'Choose another folder' },
@@ -108,7 +126,7 @@ async function promptForNewProjectDir(projectDir, homeDir, io, prompts) {
108
126
  const defaultProjectDir = join(projectDir, DEFAULT_NEW_PROJECT_FOLDER_NAME);
109
127
  while (true) {
110
128
  const destinationChoice = await prompts.select({
111
- message: 'Where should KTX create the project?',
129
+ message: 'Where should ktx create the project?',
112
130
  options: [
113
131
  { value: 'default', label: `Create the default project folder: ${defaultProjectDir}` },
114
132
  { value: 'custom', label: 'Enter a custom path' },
@@ -196,6 +214,9 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
196
214
  io.stderr.write('Missing setup choice: pass --yes to create a project in non-interactive setup.\n');
197
215
  return { status: 'missing-input', projectDir };
198
216
  }
217
+ if (!(await ensureProjectDirIsOwnable(projectDir, io))) {
218
+ return { status: 'missing-input', projectDir };
219
+ }
199
220
  const project = await createProject(projectDir, deps);
200
221
  printProjectSummary(io, projectDir);
201
222
  return {
@@ -217,7 +238,7 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
217
238
  io.stdout.write('│ Use Up/Down to move, Enter to confirm the current selection, choose Back to return to the previous step, Ctrl+C to exit.\n');
218
239
  while (true) {
219
240
  const choice = await prompts.select({
220
- message: 'Where should KTX create the project?',
241
+ message: 'Where should ktx create the project?',
221
242
  options: [
222
243
  { value: 'current', label: `Current directory (${projectDir})` },
223
244
  { value: 'new-default', label: `New subfolder (${defaultProjectDirLabel})` },
@@ -234,6 +255,9 @@ export async function runKtxSetupProjectStep(args, io, deps = {}) {
234
255
  return { status: 'cancelled', projectDir };
235
256
  }
236
257
  if (choice === 'current') {
258
+ if (!(await ensureProjectDirIsOwnable(projectDir, io))) {
259
+ continue;
260
+ }
237
261
  const project = await createProject(projectDir, deps);
238
262
  printProjectSummary(io, projectDir);
239
263
  return {
@@ -1,7 +1,15 @@
1
+ import { updateSettings } from '@clack/core';
1
2
  import { autocomplete, autocompleteMultiselect, cancel, confirm, intro, isCancel, log, multiselect, note, select, text, } from '@clack/prompts';
3
+ import { unicodeSupported } from './io/symbols.js';
4
+ import { colorDepthForOutput, isWritableTtyOutput } from './io/tty.js';
2
5
  import { withMenuOptionsSpacing, withTextInputNavigation } from './prompt-navigation.js';
6
+ import { renderKtxSetupBanner } from './setup-banner.js';
3
7
  import { revealPassword } from './reveal-password-prompt.js';
4
8
  import { withSetupInterruptConfirmation } from './setup-interrupt.js';
9
+ // clack remaps Tab to Space only on non-text prompts (flat multiselect/select/
10
+ // confirm); text inputs and autocomplete search set _track, so typed Tab is
11
+ // untouched. This makes Tab the single documented select key across setup.
12
+ updateSettings({ aliases: { tab: 'space' } });
5
13
  const DEFAULT_SETUP_CANCEL_MESSAGE = 'Setup cancelled.';
6
14
  export function createKtxSetupPromptAdapter(options) {
7
15
  const cancelMessage = options.cancelMessage ?? DEFAULT_SETUP_CANCEL_MESSAGE;
@@ -101,15 +109,18 @@ export function createKtxSetupPromptAdapter(options) {
101
109
  },
102
110
  };
103
111
  }
104
- function isWritableTtyOutput(output) {
105
- return (output.isTTY === true &&
106
- typeof output.on === 'function' &&
107
- typeof output.columns !== 'undefined');
108
- }
109
112
  export function createKtxSetupUiAdapter() {
110
113
  return {
111
114
  intro(title, io) {
112
115
  if (isWritableTtyOutput(io.stdout)) {
116
+ const banner = renderKtxSetupBanner({
117
+ columns: io.stdout.columns ?? 80,
118
+ colorDepth: colorDepthForOutput(io.stdout),
119
+ unicode: unicodeSupported,
120
+ });
121
+ if (banner !== '') {
122
+ io.stdout.write(banner);
123
+ }
113
124
  intro(title, { output: io.stdout });
114
125
  return;
115
126
  }
@@ -56,7 +56,7 @@ export async function runKtxSetupReadyChangeMenu(status, deps = {}) {
56
56
  { value: 'databases', label: 'Databases' },
57
57
  { value: 'sources', label: 'Context sources' },
58
58
  ...(status.runtime.required ? [{ value: 'runtime', label: 'Runtime' }] : []),
59
- { value: 'context', label: 'Rebuild KTX context' },
59
+ { value: 'context', label: 'Rebuild ktx context' },
60
60
  { value: 'agents', label: 'Agent integration' },
61
61
  { value: 'exit', label: 'Exit' },
62
62
  ],
@@ -16,7 +16,7 @@ import { parseMetricflowFiles } from './context/ingest/adapters/metricflow/deep-
16
16
  import { serializeKtxProjectConfig } from './context/project/config.js';
17
17
  import { loadKtxProject } from './context/project/project.js';
18
18
  import { markKtxSetupStateStepComplete } from './context/project/setup-config.js';
19
- import { errorMessage, writePrefixedLines } from './clack.js';
19
+ import { createCliSpinner, errorMessage, writePrefixedLines } from './clack.js';
20
20
  import { pickNotionRootPages } from './notion-page-picker.js';
21
21
  import { runKtxSourceMapping } from './source-mapping.js';
22
22
  import { runConnectionSetupWithRecovery, } from './connection-recovery.js';
@@ -65,7 +65,7 @@ function sourceAdapter(source) {
65
65
  return source;
66
66
  }
67
67
  function connectionNamePrompt(label) {
68
- return `Name this ${label} connection\nKTX will use this short name in commands and config. You can rename it now.`;
68
+ return `Name this ${label} connection\nktx will use this short name in commands and config. You can rename it now.`;
69
69
  }
70
70
  function sourceSubpathPrompt(source) {
71
71
  if (source === 'dbt') {
@@ -143,7 +143,7 @@ function assertSourceCredentialFlags(source, args) {
143
143
  async function chooseSourceCredentialRef(input) {
144
144
  while (true) {
145
145
  const choice = await input.prompts.select({
146
- message: `How should KTX find your ${input.label}?`,
146
+ message: `How should ktx find your ${input.label}?`,
147
147
  options: [
148
148
  ...(input.existingRef ? [{ value: 'keep', label: 'Keep existing credential' }] : []),
149
149
  { value: 'paste', label: 'Paste a key and save it as a local secret file' },
@@ -733,12 +733,15 @@ async function chooseMetabaseDatabaseId(input) {
733
733
  const sourceUrl = input.state.sourceUrl;
734
734
  const sourceApiKeyRef = input.state.sourceApiKeyRef;
735
735
  if (sourceUrl && sourceApiKeyRef) {
736
+ const discoverSpinner = createCliSpinner(input.io);
737
+ discoverSpinner.start('Discovering Metabase databases…');
736
738
  try {
737
739
  const discovered = await (input.deps.discoverMetabaseDatabases ?? defaultDiscoverMetabaseDatabases)({
738
740
  sourceUrl,
739
741
  sourceApiKeyRef,
740
742
  sourceConnectionId: input.state.sourceConnectionId ?? 'metabase-main',
741
743
  });
744
+ discoverSpinner.stop(`Found ${discovered.length} ${discovered.length === 1 ? 'database' : 'databases'}`);
742
745
  if (discovered.length === 1) {
743
746
  return discovered[0].id;
744
747
  }
@@ -763,6 +766,7 @@ async function chooseMetabaseDatabaseId(input) {
763
766
  catch {
764
767
  // Discovery is a convenience. Fall back to the raw id prompt when credentials
765
768
  // are unavailable locally or the Metabase API cannot be reached yet.
769
+ discoverSpinner.error('Could not reach Metabase — enter the database id manually');
766
770
  }
767
771
  }
768
772
  const databaseId = await promptText(input.prompts, { message: 'Metabase database id' });
@@ -891,6 +895,8 @@ async function promptForInteractiveSource(args, source, prompts, io, deps, defau
891
895
  scanDir = currentState.sourcePath;
892
896
  }
893
897
  else if (currentState.sourceLocation === 'git' && currentState.sourceGitUrl) {
898
+ const cloneSpinner = createCliSpinner(io);
899
+ cloneSpinner.start('Cloning repository to scan for dbt projects…');
894
900
  try {
895
901
  const cacheDir = await mkdtemp(join(tmpdir(), 'ktx-setup-dbt-scan-'));
896
902
  const authToken = currentState.sourceAuthTokenRef
@@ -903,8 +909,10 @@ async function promptForInteractiveSource(args, source, prompts, io, deps, defau
903
909
  branch: currentState.sourceBranch ?? 'main',
904
910
  });
905
911
  scanDir = cacheDir;
912
+ cloneSpinner.stop('Repository cloned');
906
913
  }
907
914
  catch {
915
+ cloneSpinner.error('Could not clone repository');
908
916
  // Clone failed — fall through to manual prompt
909
917
  }
910
918
  }
@@ -924,7 +932,7 @@ async function promptForInteractiveSource(args, source, prompts, io, deps, defau
924
932
  }
925
933
  if (subpaths.length > 1) {
926
934
  const selected = await prompts.select({
927
- message: 'Multiple dbt projects found — which one should KTX use?',
935
+ message: 'Multiple dbt projects found — which one should ktx use?',
928
936
  options: [
929
937
  ...subpaths.map((p) => ({ value: p || '.', label: p || '(project root)' })),
930
938
  { value: 'back', label: 'Back' },
@@ -1008,6 +1016,7 @@ async function promptForInteractiveSource(args, source, prompts, io, deps, defau
1008
1016
  state,
1009
1017
  prompts,
1010
1018
  deps: { discoverMetabaseDatabases: discoverMetabaseDatabaseList },
1019
+ io,
1011
1020
  });
1012
1021
  if (databaseId === 'back')
1013
1022
  return 'back';
@@ -1099,7 +1108,7 @@ async function promptForInteractiveSource(args, source, prompts, io, deps, defau
1099
1108
  },
1100
1109
  async (currentState) => {
1101
1110
  const crawlMode = await prompts.select({
1102
- message: 'Which Notion pages should KTX ingest?',
1111
+ message: 'Which Notion pages should ktx ingest?',
1103
1112
  options: [
1104
1113
  { value: 'all_accessible', label: 'All pages the integration can access' },
1105
1114
  { value: 'selected_roots', label: 'Specific pages and their subpages (choose them in a picker)' },
@@ -1449,11 +1458,22 @@ function sourceConnectionId(input) {
1449
1458
  : (input.sourceChoice.args.sourceConnectionId ?? `${input.source}-main`);
1450
1459
  }
1451
1460
  async function validateSourceConnectionAndMapping(input) {
1452
- const validation = await validateSource(input.source, { projectDir: input.args.projectDir, connectionId: input.connectionId, connection: input.connection }, input.deps);
1461
+ const validateSpinner = createCliSpinner(input.io);
1462
+ validateSpinner.start(`Validating ${sourceLabel(input.source)} source…`);
1463
+ let validation;
1464
+ try {
1465
+ validation = await validateSource(input.source, { projectDir: input.args.projectDir, connectionId: input.connectionId, connection: input.connection }, input.deps);
1466
+ }
1467
+ catch (error) {
1468
+ validateSpinner.error(`${sourceLabel(input.source)} source validation failed`);
1469
+ throw error;
1470
+ }
1453
1471
  if (!validation.ok) {
1472
+ validateSpinner.error(`${sourceLabel(input.source)} source validation failed`);
1454
1473
  input.io.stderr.write(`${validation.message}\n`);
1455
1474
  return { status: 'failed' };
1456
1475
  }
1476
+ validateSpinner.stop(`${sourceLabel(input.source)} source validated`);
1457
1477
  if (input.source === 'metabase' || input.source === 'looker') {
1458
1478
  input.prompts.log?.(`Validating ${sourceLabel(input.source)} mapping...`);
1459
1479
  const mappingCode = await (input.deps.runMapping ?? defaultRunMapping)(input.args.projectDir, input.connectionId, createSetupPrefixedIo(input.io));
@@ -1585,7 +1605,7 @@ export async function runKtxSetupSourcesStep(args, io, deps = {}) {
1585
1605
  : args.inputMode === 'disabled'
1586
1606
  ? []
1587
1607
  : await prompts.multiselect({
1588
- message: withMultiselectNavigation('Which context sources should KTX ingest?'),
1608
+ message: withMultiselectNavigation('Which context sources should ktx ingest?'),
1589
1609
  options: contextSourceChecklist.options,
1590
1610
  ...(contextSourceChecklist.initialValues.length > 0
1591
1611
  ? { initialValues: contextSourceChecklist.initialValues }
package/dist/setup.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type KtxCliIo } from './cli-runtime.js';
2
2
  import { readManagedPythonRuntimeStatus } from './managed-python-runtime.js';
3
+ import type { CommandOutcome } from './telemetry/index.js';
3
4
  import { type KtxAgentScope, type KtxAgentTarget, type KtxSetupAgentsDeps, runKtxSetupAgentsStep } from './setup-agents.js';
4
5
  import { type KtxSetupDatabaseDriver, type KtxSetupDatabasesDeps, runKtxSetupDatabasesStep } from './setup-databases.js';
5
6
  import { type KtxSetupEmbeddingsDeps, runKtxSetupEmbeddingsStep } from './setup-embeddings.js';
@@ -125,6 +126,7 @@ export interface KtxSetupDeps {
125
126
  entryMenuDeps?: KtxSetupEntryMenuDeps;
126
127
  setupUi?: KtxSetupUiAdapter;
127
128
  }
129
+ type TelemetrySetupStep = 'project' | 'runtime' | 'models' | 'embeddings' | 'databases' | 'sources' | 'context' | 'agents' | 'demo-tour';
128
130
  export interface KtxSetupEntryMenuPromptAdapter {
129
131
  select(options: {
130
132
  message: string;
@@ -135,6 +137,28 @@ export interface KtxSetupEntryMenuPromptAdapter {
135
137
  export interface KtxSetupEntryMenuDeps {
136
138
  prompts?: KtxSetupEntryMenuPromptAdapter;
137
139
  }
140
+ interface SetupCommandAnnotation {
141
+ outcome: CommandOutcome;
142
+ errorClass?: string;
143
+ errorDetail?: string;
144
+ }
145
+ /**
146
+ * Single source of truth for how a non-ready setup step ends: the process exit
147
+ * code and the telemetry annotation are both derived from one classification,
148
+ * so they can never disagree. A genuine failure (`error`) exits non-zero; an
149
+ * abort — the user leaving an interactive wizard — exits 0, matching the entry
150
+ * menu's "Exit", a project cancellation, and a confirmed Ctrl+C.
151
+ */
152
+ /** @internal */
153
+ export declare function setupTerminalOutcome(input: {
154
+ status: 'failed' | 'missing-input' | 'cancelled';
155
+ step: TelemetrySetupStep;
156
+ interactive: boolean;
157
+ errorDetail?: string;
158
+ }): {
159
+ exitCode: number;
160
+ annotation: SetupCommandAnnotation;
161
+ };
138
162
  export interface ReadKtxSetupStatusOptions {
139
163
  cliVersion?: string;
140
164
  env?: NodeJS.ProcessEnv;
@@ -146,3 +170,4 @@ export declare function formatKtxSetupCompletionSummary(status: KtxSetupStatus,
146
170
  agentNextActions?: string;
147
171
  }): string;
148
172
  export declare function runKtxSetup(args: KtxSetupArgs, io: KtxCliIo, deps?: KtxSetupDeps): Promise<number>;
173
+ export {};