@kaelio/ktx 0.9.0 → 0.11.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 (143) hide show
  1. package/assets/python/{kaelio_ktx-0.9.0-py3-none-any.whl → kaelio_ktx-0.11.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/clack.d.ts +6 -0
  5. package/dist/clack.js +17 -2
  6. package/dist/cli-program.d.ts +3 -0
  7. package/dist/cli-program.js +46 -2
  8. package/dist/cli-runtime.d.ts +5 -0
  9. package/dist/cli-runtime.js +50 -0
  10. package/dist/commands/setup-commands.js +2 -3
  11. package/dist/community-cta.d.ts +11 -0
  12. package/dist/community-cta.js +19 -0
  13. package/dist/connection.js +23 -1
  14. package/dist/connectors/bigquery/connector.d.ts +2 -5
  15. package/dist/connectors/bigquery/connector.js +2 -2
  16. package/dist/connectors/clickhouse/connector.d.ts +2 -5
  17. package/dist/connectors/clickhouse/connector.js +2 -2
  18. package/dist/connectors/mysql/connector.d.ts +7 -6
  19. package/dist/connectors/mysql/connector.js +25 -5
  20. package/dist/connectors/mysql/dialect.d.ts +1 -1
  21. package/dist/connectors/mysql/dialect.js +12 -2
  22. package/dist/connectors/postgres/connector.d.ts +2 -5
  23. package/dist/connectors/postgres/connector.js +2 -2
  24. package/dist/connectors/snowflake/connector.d.ts +2 -5
  25. package/dist/connectors/snowflake/connector.js +2 -2
  26. package/dist/connectors/sqlite/connector.d.ts +2 -5
  27. package/dist/connectors/sqlite/connector.js +2 -2
  28. package/dist/connectors/sqlserver/connector.d.ts +2 -5
  29. package/dist/connectors/sqlserver/connector.js +2 -2
  30. package/dist/context/connections/drivers.d.ts +0 -1
  31. package/dist/context/connections/drivers.js +0 -7
  32. package/dist/context/connections/query-executor.d.ts +2 -1
  33. package/dist/context/core/abort.d.ts +9 -0
  34. package/dist/context/core/abort.js +36 -0
  35. package/dist/context/core/git-env.d.ts +12 -1
  36. package/dist/context/core/git-env.js +17 -2
  37. package/dist/context/core/git.service.js +15 -7
  38. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.d.ts +1 -0
  39. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +6 -2
  40. package/dist/context/ingest/context-candidates/curator-pagination.service.d.ts +1 -5
  41. package/dist/context/ingest/context-candidates/curator-pagination.service.js +1 -3
  42. package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
  43. package/dist/context/ingest/final-gate-repair.d.ts +1 -0
  44. package/dist/context/ingest/final-gate-repair.js +1 -0
  45. package/dist/context/ingest/ingest-bundle.runner.d.ts +3 -0
  46. package/dist/context/ingest/ingest-bundle.runner.js +127 -53
  47. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +1 -0
  48. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +1 -0
  49. package/dist/context/ingest/isolated-diff/work-unit-executor.d.ts +1 -0
  50. package/dist/context/ingest/local-bundle-runtime.js +11 -4
  51. package/dist/context/ingest/local-ingest.d.ts +1 -0
  52. package/dist/context/ingest/local-ingest.js +13 -3
  53. package/dist/context/ingest/memory-flow/events.js +1 -1
  54. package/dist/context/ingest/memory-flow/schema.js +8 -3
  55. package/dist/context/ingest/memory-flow/types.d.ts +7 -3
  56. package/dist/context/ingest/ports.d.ts +3 -5
  57. package/dist/context/ingest/stages/stage-3-work-units.d.ts +1 -4
  58. package/dist/context/ingest/stages/stage-3-work-units.js +5 -1
  59. package/dist/context/ingest/stages/stage-4-reconciliation.d.ts +1 -4
  60. package/dist/context/ingest/stages/stage-4-reconciliation.js +1 -1
  61. package/dist/context/ingest/types.d.ts +1 -0
  62. package/dist/context/llm/ai-sdk-runtime.d.ts +3 -0
  63. package/dist/context/llm/ai-sdk-runtime.js +152 -16
  64. package/dist/context/llm/claude-code-runtime.d.ts +6 -4
  65. package/dist/context/llm/claude-code-runtime.js +127 -48
  66. package/dist/context/llm/codex-runtime.d.ts +3 -3
  67. package/dist/context/llm/codex-runtime.js +90 -47
  68. package/dist/context/llm/local-config.d.ts +15 -5
  69. package/dist/context/llm/local-config.js +6 -1
  70. package/dist/context/llm/rate-limit-governor.d.ts +103 -0
  71. package/dist/context/llm/rate-limit-governor.js +285 -0
  72. package/dist/context/llm/runtime-port.d.ts +3 -6
  73. package/dist/context/mcp/context-tools.js +43 -13
  74. package/dist/context/project/config.d.ts +12 -0
  75. package/dist/context/project/config.js +35 -0
  76. package/dist/context/scan/types.d.ts +15 -2
  77. package/dist/context/scan/types.js +12 -0
  78. package/dist/context/sl/description-normalization.js +4 -14
  79. package/dist/context/tools/context-candidate-mark.tool.d.ts +2 -2
  80. package/dist/context-build-view.d.ts +13 -0
  81. package/dist/context-build-view.js +60 -1
  82. package/dist/demo-metrics.d.ts +0 -2
  83. package/dist/demo-metrics.js +1 -11
  84. package/dist/ingest.d.ts +1 -0
  85. package/dist/ingest.js +32 -3
  86. package/dist/io/symbols.d.ts +2 -0
  87. package/dist/io/symbols.js +2 -0
  88. package/dist/io/tty.d.ts +9 -0
  89. package/dist/io/tty.js +5 -0
  90. package/dist/links.d.ts +1 -0
  91. package/dist/links.js +1 -0
  92. package/dist/memory-flow-hud.js +8 -16
  93. package/dist/public-ingest.js +50 -15
  94. package/dist/reveal-password-prompt.d.ts +24 -0
  95. package/dist/reveal-password-prompt.js +78 -0
  96. package/dist/scan.js +18 -2
  97. package/dist/setup-agents.js +1 -5
  98. package/dist/setup-databases.d.ts +1 -0
  99. package/dist/setup-databases.js +23 -3
  100. package/dist/setup-demo-tour.js +1 -0
  101. package/dist/setup-embeddings.js +1 -1
  102. package/dist/setup-models.d.ts +1 -14
  103. package/dist/setup-models.js +116 -340
  104. package/dist/setup-prompts.js +4 -7
  105. package/dist/setup-sources.js +7 -7
  106. package/dist/setup.d.ts +26 -1
  107. package/dist/setup.js +78 -7
  108. package/dist/sl.d.ts +2 -2
  109. package/dist/sl.js +20 -4
  110. package/dist/sql.js +18 -2
  111. package/dist/star-prompt/cache.d.ts +16 -0
  112. package/dist/star-prompt/cache.js +45 -0
  113. package/dist/star-prompt/star-count.d.ts +7 -0
  114. package/dist/star-prompt/star-count.js +66 -0
  115. package/dist/star-prompt/star-line.d.ts +12 -0
  116. package/dist/star-prompt/star-line.js +26 -0
  117. package/dist/telemetry/command-hook.d.ts +24 -0
  118. package/dist/telemetry/command-hook.js +37 -3
  119. package/dist/telemetry/emitter.d.ts +10 -0
  120. package/dist/telemetry/emitter.js +31 -0
  121. package/dist/telemetry/events.d.ts +24 -0
  122. package/dist/telemetry/events.js +15 -0
  123. package/dist/telemetry/exception.d.ts +18 -0
  124. package/dist/telemetry/exception.js +162 -0
  125. package/dist/telemetry/index.d.ts +4 -3
  126. package/dist/telemetry/index.js +3 -2
  127. package/dist/telemetry/redaction-secrets.d.ts +11 -0
  128. package/dist/telemetry/redaction-secrets.js +92 -0
  129. package/dist/update-check/cache.d.ts +21 -0
  130. package/dist/update-check/cache.js +38 -0
  131. package/dist/update-check/channel.d.ts +15 -0
  132. package/dist/update-check/channel.js +30 -0
  133. package/dist/update-check/registry.d.ts +1 -0
  134. package/dist/update-check/registry.js +45 -0
  135. package/dist/update-check/update-check.d.ts +43 -0
  136. package/dist/update-check/update-check.js +116 -0
  137. package/package.json +8 -1
  138. package/dist/context/connections/local-query-executor.d.ts +0 -6
  139. package/dist/context/connections/local-query-executor.js +0 -39
  140. package/dist/context/connections/postgres-query-executor.d.ts +0 -25
  141. package/dist/context/connections/postgres-query-executor.js +0 -53
  142. package/dist/context/connections/sqlite-query-executor.d.ts +0 -4
  143. package/dist/context/connections/sqlite-query-executor.js +0 -74
@@ -10,6 +10,7 @@ import { resolveKtxConfigReference } from './context/core/config-reference.js';
10
10
  import { serializeKtxProjectConfig } from './context/project/config.js';
11
11
  import { loadKtxProject } from './context/project/project.js';
12
12
  import { markKtxSetupStateStepComplete } from './context/project/setup-config.js';
13
+ import { KTX_MODEL_ROLES } from './llm/types.js';
13
14
  import { runKtxLlmHealthCheck } from './llm/model-health.js';
14
15
  import { formatClaudeCodePromptCachingWarning, ignoredClaudeCodePromptCachingFields, } from './claude-code-prompt-caching.js';
15
16
  import { createClackSpinner } from './clack.js';
@@ -20,70 +21,46 @@ const ESC = String.fromCharCode(0x1b);
20
21
  function yellow(text) {
21
22
  return `${ESC}[33m${text}${ESC}[39m`;
22
23
  }
23
- /** @internal */
24
- export const BUNDLED_ANTHROPIC_MODELS = [
25
- { id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', recommended: true },
26
- { id: 'claude-opus-4-6', label: 'Claude Opus 4.6', recommended: false },
27
- { id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5', recommended: false },
28
- ];
29
- const VERTEX_ANTHROPIC_MODELS = [
30
- { id: 'claude-opus-4-7', label: 'Claude Opus 4.7', recommended: false },
31
- { id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', recommended: false },
32
- { id: 'claude-opus-4-6', label: 'Claude Opus 4.6', recommended: false },
33
- { id: 'claude-opus-4-5', label: 'Claude Opus 4.5', recommended: false },
34
- { id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5', recommended: false },
35
- { id: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5', recommended: false },
36
- { id: 'claude-opus-4-1', label: 'Claude Opus 4.1', recommended: false },
37
- ];
38
- const CLAUDE_CODE_MODELS = [
39
- { id: 'sonnet', label: 'Claude Sonnet', recommended: true },
40
- { id: 'opus', label: 'Claude Opus', recommended: false },
41
- { id: 'haiku', label: 'Claude Haiku', recommended: false },
42
- ];
43
- // Curated Codex models from OpenAI's current lineup that work under both
44
- // ChatGPT-account (subscription) and API-key auth. Intentionally omitted:
45
- // the `*-codex` ids (e.g. gpt-5.3-codex, gpt-5.2-codex) are API-key-only and
46
- // fail on ChatGPT-account auth, and gpt-5.3-codex-spark is a ChatGPT-Pro-only
47
- // research preview. Codex resolves real availability per account at runtime
48
- // (its binary remote-fetches the model list), so this is a convenience
49
- // shortlist only — the manual-entry option accepts any id your account's
50
- // `codex` picker exposes, and the auth probe reports an unsupported choice.
51
- const CODEX_MODELS = [
52
- { id: 'gpt-5.5', label: 'GPT-5.5', recommended: true },
53
- { id: 'gpt-5.4', label: 'GPT-5.4', recommended: false },
54
- { id: 'gpt-5.4-mini', label: 'GPT-5.4 mini', recommended: false },
55
- ];
56
- const HIDDEN_ANTHROPIC_MODEL_PATTERNS = [
57
- /^claude-sonnet-4$/i,
58
- /^claude-opus-4$/i,
59
- /^Claude Sonnet 4$/i,
60
- /^Claude Opus 4$/i,
61
- ];
62
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, ' +
63
25
  'BI metadata, and docs into semantic-layer sources and wiki context. ktx.yaml stores an env: or file: ' +
64
26
  'reference, not the raw key.';
65
- const ANTHROPIC_MODEL_PROMPT_CONTEXT = 'KTX uses this as the default model for ingest agents that turn schemas, SQL, BI metadata, and docs ' +
66
- 'into semantic-layer sources and wiki context.';
67
27
  const VERTEX_PROJECT_PROMPT_CONTEXT = 'KTX stores the selected Google Cloud project ID in ktx.yaml and uses Application Default Credentials for ' +
68
28
  'access. Project visibility depends on the signed-in Google account and organization permissions.';
69
29
  const DEFAULT_VERTEX_LOCATION = 'us-east5';
70
- const execFileAsync = promisify(execFile);
71
- class AnthropicModelDiscoveryError extends Error {
72
- reason;
73
- status;
74
- constructor(message, reason, status) {
75
- super(message);
76
- this.reason = reason;
77
- this.status = status;
78
- this.name = 'AnthropicModelDiscoveryError';
79
- }
80
- }
81
- function isAnthropicModelAuthenticationError(error) {
82
- return error instanceof AnthropicModelDiscoveryError && error.reason === 'authentication';
83
- }
84
- function isSelectableAnthropicModel(model) {
85
- return !HIDDEN_ANTHROPIC_MODEL_PATTERNS.some((pattern) => pattern.test(model.id) || pattern.test(model.label));
30
+ const ANTHROPIC_PRESET = {
31
+ default: 'claude-sonnet-4-6',
32
+ triage: 'claude-haiku-4-5',
33
+ candidateExtraction: 'claude-sonnet-4-6',
34
+ curator: 'claude-opus-4-7',
35
+ reconcile: 'claude-opus-4-7',
36
+ repair: 'claude-haiku-4-5',
37
+ };
38
+ const CLAUDE_CODE_PRESET = {
39
+ default: 'sonnet',
40
+ triage: 'haiku',
41
+ candidateExtraction: 'sonnet',
42
+ curator: 'opus',
43
+ reconcile: 'opus',
44
+ repair: 'haiku',
45
+ };
46
+ const CODEX_PRESET = {
47
+ default: DEFAULT_CODEX_MODEL,
48
+ triage: DEFAULT_CODEX_MODEL,
49
+ candidateExtraction: DEFAULT_CODEX_MODEL,
50
+ curator: DEFAULT_CODEX_MODEL,
51
+ reconcile: DEFAULT_CODEX_MODEL,
52
+ repair: DEFAULT_CODEX_MODEL,
53
+ };
54
+ const MODEL_PRESETS = {
55
+ anthropic: ANTHROPIC_PRESET,
56
+ vertex: ANTHROPIC_PRESET,
57
+ 'claude-code': CLAUDE_CODE_PRESET,
58
+ codex: CODEX_PRESET,
59
+ };
60
+ function presetForBackend(backend) {
61
+ return MODEL_PRESETS[backend];
86
62
  }
63
+ const execFileAsync = promisify(execFile);
87
64
  function createPromptAdapter() {
88
65
  return createKtxSetupPromptAdapter({ selectCancelValue: 'back' });
89
66
  }
@@ -122,35 +99,6 @@ async function defaultListGcloudProjects() {
122
99
  })
123
100
  .filter((project) => Boolean(project));
124
101
  }
125
- /** @internal */
126
- export async function fetchAnthropicModels(apiKey, fetchFn = fetch) {
127
- const response = await fetchFn('https://api.anthropic.com/v1/models?limit=1000', {
128
- headers: {
129
- 'anthropic-version': '2023-06-01',
130
- 'x-api-key': apiKey,
131
- },
132
- });
133
- if (!response.ok) {
134
- if (response.status === 401 || response.status === 403) {
135
- throw new AnthropicModelDiscoveryError(`Anthropic model discovery failed with HTTP ${response.status}`, 'authentication', response.status);
136
- }
137
- throw new AnthropicModelDiscoveryError(`Anthropic model discovery failed with HTTP ${response.status}`, 'http', response.status);
138
- }
139
- const body = (await response.json());
140
- const models = (body.data ?? [])
141
- .map((item) => ({
142
- id: typeof item.id === 'string' ? item.id : '',
143
- label: typeof item.display_name === 'string' ? item.display_name : typeof item.id === 'string' ? item.id : '',
144
- recommended: false,
145
- }))
146
- .filter((item) => item.id.startsWith('claude-'))
147
- .filter(isSelectableAnthropicModel);
148
- if (models.length === 0) {
149
- throw new AnthropicModelDiscoveryError('Anthropic model discovery returned no Claude models', 'empty-response');
150
- }
151
- const recommendedIndex = models.findIndex((item) => item.id.includes('sonnet'));
152
- return models.map((item, index) => ({ ...item, recommended: index === Math.max(recommendedIndex, 0) }));
153
- }
154
102
  export function isKtxSetupLlmConfigReady(config) {
155
103
  let resolved;
156
104
  try {
@@ -173,18 +121,18 @@ export function isKtxSetupLlmConfigReady(config) {
173
121
  function hasUsableConfiguredLlm(config) {
174
122
  return isKtxSetupLlmConfigReady(config.llm);
175
123
  }
176
- function buildProjectLlmConfig(existing, provider, model) {
124
+ function buildProjectLlmConfig(existing, provider, models) {
177
125
  if (provider.backend === 'claude-code') {
178
126
  return {
179
127
  provider: { backend: 'claude-code' },
180
- models: { ...existing.models, default: model },
128
+ models,
181
129
  promptCaching: existing.promptCaching,
182
130
  };
183
131
  }
184
132
  if (provider.backend === 'codex') {
185
133
  return {
186
134
  provider: { backend: 'codex' },
187
- models: { ...existing.models, default: model },
135
+ models,
188
136
  promptCaching: existing.promptCaching,
189
137
  };
190
138
  }
@@ -194,7 +142,7 @@ function buildProjectLlmConfig(existing, provider, model) {
194
142
  backend: 'vertex',
195
143
  vertex: provider.vertex,
196
144
  },
197
- models: { ...existing.models, default: model },
145
+ models,
198
146
  promptCaching: { ...(existing.promptCaching ?? {}), enabled: true, vertexFallbackTo5m: true },
199
147
  };
200
148
  }
@@ -203,7 +151,7 @@ function buildProjectLlmConfig(existing, provider, model) {
203
151
  backend: 'anthropic',
204
152
  anthropic: { api_key: provider.credentialRef },
205
153
  },
206
- models: { ...existing.models, default: model },
154
+ models,
207
155
  promptCaching: { ...(existing.promptCaching ?? {}), enabled: true },
208
156
  };
209
157
  }
@@ -302,8 +250,8 @@ async function chooseCredentialRef(args, io, deps) {
302
250
  const choice = await prompts.select({
303
251
  message: `How should KTX find your Anthropic API key?\n\n${ANTHROPIC_CREDENTIAL_PROMPT_CONTEXT}`,
304
252
  options: [
305
- { value: 'env', label: 'Use ANTHROPIC_API_KEY from the environment' },
306
253
  { value: 'paste', label: 'Paste a key and save it as a local secret file' },
254
+ { value: 'env', label: 'Use ANTHROPIC_API_KEY from the environment' },
307
255
  { value: 'back', label: 'Back' },
308
256
  ],
309
257
  });
@@ -342,14 +290,11 @@ function requestedBackend(args) {
342
290
  if (args.vertexProject || args.vertexLocation) {
343
291
  return 'vertex';
344
292
  }
345
- if (args.anthropicApiKeyEnv || args.anthropicApiKeyFile || args.llmModel) {
293
+ if (args.anthropicApiKeyEnv || args.anthropicApiKeyFile) {
346
294
  return 'anthropic';
347
295
  }
348
296
  return undefined;
349
297
  }
350
- function requestedModel(args) {
351
- return args.llmModel;
352
- }
353
298
  async function chooseBackend(args, io, deps) {
354
299
  const explicit = requestedBackend(args);
355
300
  if (explicit) {
@@ -568,177 +513,11 @@ async function chooseVertexConfig(args, io, deps) {
568
513
  },
569
514
  };
570
515
  }
571
- async function chooseModel(args, credentialValue, io, deps) {
572
- const providedModel = requestedModel(args);
573
- if (providedModel) {
574
- return { status: 'ready', model: providedModel };
575
- }
576
- if (args.inputMode === 'disabled') {
577
- io.stderr.write('Missing LLM model: pass --llm-model.\n');
578
- return { status: 'missing-input' };
579
- }
580
- let models;
581
- try {
582
- models = deps.listModels
583
- ? await deps.listModels(credentialValue)
584
- : await fetchAnthropicModels(credentialValue, deps.fetch);
585
- }
586
- catch (error) {
587
- if (isAnthropicModelAuthenticationError(error)) {
588
- const statusSuffix = error.status ? ` (HTTP ${error.status})` : '';
589
- io.stderr.write(`Anthropic API key is invalid or unauthorized${statusSuffix}. Check the key and try again.\n`);
590
- return { status: 'invalid-credential' };
591
- }
592
- io.stderr.write('Could not fetch live Anthropic models. Showing bundled defaults. Setup will still test the selected model before saving it.\n');
593
- models = BUNDLED_ANTHROPIC_MODELS;
594
- }
595
- const selectableModels = models.filter(isSelectableAnthropicModel);
596
- const prompts = deps.prompts ?? createPromptAdapter();
597
- const modelOptions = [
598
- ...selectableModels.map((model) => ({
599
- value: model.id,
600
- label: model.label || model.id,
601
- ...(model.recommended ? { hint: 'recommended' } : {}),
602
- })),
603
- { value: 'manual', label: 'Enter a model ID manually' },
604
- { value: 'back', label: 'Back' },
605
- ];
606
- const choice = await prompts.autocomplete({
607
- message: `Which Anthropic model should KTX use?\n\n${ANTHROPIC_MODEL_PROMPT_CONTEXT}`,
608
- placeholder: 'Type to search models',
609
- options: modelOptions,
610
- });
611
- if (choice === 'back') {
612
- return { status: 'back' };
613
- }
614
- if (choice === 'manual') {
615
- const manual = await prompts.text({
616
- message: withTextInputNavigation('Anthropic model ID'),
617
- placeholder: selectableModels.find((model) => model.recommended)?.id ?? selectableModels[0]?.id,
618
- });
619
- if (manual === undefined) {
620
- return { status: 'back' };
621
- }
622
- return manual.trim() ? { status: 'ready', model: manual.trim() } : { status: 'missing-input' };
623
- }
624
- return { status: 'ready', model: choice };
625
- }
626
- async function chooseVertexModel(args, io, deps) {
627
- const providedModel = requestedModel(args);
628
- if (providedModel) {
629
- return { status: 'ready', model: providedModel };
630
- }
631
- if (args.inputMode === 'disabled') {
632
- io.stderr.write('Missing LLM model: pass --llm-model.\n');
633
- return { status: 'missing-input' };
634
- }
635
- const selectableModels = VERTEX_ANTHROPIC_MODELS.filter(isSelectableAnthropicModel);
636
- const prompts = deps.prompts ?? createPromptAdapter();
637
- const choice = await prompts.autocomplete({
638
- message: `Which Anthropic model should KTX use?\n\n${ANTHROPIC_MODEL_PROMPT_CONTEXT}`,
639
- placeholder: 'Type to search models',
640
- options: [
641
- ...selectableModels.map((model) => ({
642
- value: model.id,
643
- label: model.label || model.id,
644
- ...(model.recommended ? { hint: 'recommended' } : {}),
645
- })),
646
- { value: 'manual', label: 'Enter a model ID manually' },
647
- { value: 'back', label: 'Back' },
648
- ],
649
- });
650
- if (choice === 'back') {
651
- return { status: 'back' };
652
- }
653
- if (choice === 'manual') {
654
- const manual = await prompts.text({
655
- message: withTextInputNavigation('Anthropic model ID'),
656
- placeholder: selectableModels.find((model) => model.recommended)?.id ?? selectableModels[0]?.id,
657
- });
658
- if (manual === undefined) {
659
- return { status: 'back' };
660
- }
661
- return manual.trim() ? { status: 'ready', model: manual.trim() } : { status: 'missing-input' };
662
- }
663
- return { status: 'ready', model: choice };
664
- }
665
- async function chooseClaudeCodeModel(args, deps) {
666
- const providedModel = requestedModel(args);
667
- if (providedModel) {
668
- return { status: 'ready', model: providedModel };
669
- }
670
- if (args.inputMode === 'disabled') {
671
- return { status: 'ready', model: 'sonnet' };
672
- }
673
- const prompts = deps.prompts ?? createPromptAdapter();
674
- const choice = await prompts.select({
675
- message: `Which Claude Code model should KTX use?\n\n${ANTHROPIC_MODEL_PROMPT_CONTEXT}`,
676
- options: [
677
- ...CLAUDE_CODE_MODELS.map((model) => ({
678
- value: model.id,
679
- label: model.label,
680
- ...(model.recommended ? { hint: 'recommended' } : {}),
681
- })),
682
- { value: 'manual', label: 'Enter a Claude Code model ID manually' },
683
- { value: 'back', label: 'Back' },
684
- ],
685
- });
686
- if (choice === 'back') {
687
- return { status: 'back' };
688
- }
689
- if (choice === 'manual') {
690
- const manual = await prompts.text({
691
- message: withTextInputNavigation('Claude Code model ID'),
692
- placeholder: CLAUDE_CODE_MODELS.find((model) => model.recommended)?.id ?? CLAUDE_CODE_MODELS[0]?.id,
693
- });
694
- if (manual === undefined) {
695
- return { status: 'back' };
696
- }
697
- return manual.trim() ? { status: 'ready', model: manual.trim() } : { status: 'missing-input' };
698
- }
699
- return { status: 'ready', model: choice };
700
- }
701
- async function chooseCodexModel(args, deps) {
702
- const providedModel = requestedModel(args);
703
- if (providedModel) {
704
- return { status: 'ready', model: providedModel };
705
- }
706
- if (args.inputMode === 'disabled') {
707
- return { status: 'ready', model: DEFAULT_CODEX_MODEL };
708
- }
709
- const prompts = deps.prompts ?? createPromptAdapter();
710
- const choice = await prompts.select({
711
- message: `Which Codex model should KTX use?\n\n${ANTHROPIC_MODEL_PROMPT_CONTEXT}`,
712
- options: [
713
- ...CODEX_MODELS.map((model) => ({
714
- value: model.id,
715
- label: model.label,
716
- ...(model.recommended ? { hint: 'recommended' } : {}),
717
- })),
718
- { value: 'manual', label: 'Enter a Codex model ID manually' },
719
- { value: 'back', label: 'Back' },
720
- ],
721
- });
722
- if (choice === 'back') {
723
- return { status: 'back' };
724
- }
725
- if (choice === 'manual') {
726
- const manual = await prompts.text({
727
- message: withTextInputNavigation('Codex model ID'),
728
- placeholder: CODEX_MODELS.find((model) => model.recommended)?.id ?? CODEX_MODELS[0]?.id,
729
- });
730
- if (manual === undefined) {
731
- return { status: 'back' };
732
- }
733
- return manual.trim() ? { status: 'ready', model: manual.trim() } : { status: 'missing-input' };
734
- }
735
- return { status: 'ready', model: choice };
736
- }
737
- async function persistLlmConfig(projectDir, provider, model) {
516
+ async function persistLlmConfig(projectDir, provider, models) {
738
517
  const project = await loadKtxProject({ projectDir });
739
518
  const config = {
740
519
  ...project.config,
741
- llm: buildProjectLlmConfig(project.config.llm, provider, model),
520
+ llm: buildProjectLlmConfig(project.config.llm, provider, models),
742
521
  scan: {
743
522
  ...project.config.scan,
744
523
  enrichment: {
@@ -759,6 +538,48 @@ function buildInteractiveRetryArgs(args, backend) {
759
538
  skipLlm: args.skipLlm,
760
539
  };
761
540
  }
541
+ function distinctPresetModels(preset) {
542
+ const models = [];
543
+ const seen = new Set();
544
+ for (const role of KTX_MODEL_ROLES) {
545
+ const model = preset[role];
546
+ if (!seen.has(model)) {
547
+ seen.add(model);
548
+ models.push(model);
549
+ }
550
+ }
551
+ return models;
552
+ }
553
+ function rolesUsingModel(preset, model) {
554
+ return KTX_MODEL_ROLES.filter((role) => preset[role] === model);
555
+ }
556
+ function formatPresetFallbackWarning(roles, unavailableModel, anchorModel) {
557
+ return `LLM model ${unavailableModel} is unavailable for ${roles.join(', ')}; using ${anchorModel} for those roles.`;
558
+ }
559
+ async function validatePresetModels(preset, validateModel, io) {
560
+ const anchorModel = preset.default;
561
+ const degraded = { ...preset };
562
+ const models = distinctPresetModels(preset);
563
+ const anchorResult = await validateModel(anchorModel);
564
+ if (!anchorResult.ok) {
565
+ return { status: 'failed', message: anchorResult.message };
566
+ }
567
+ for (const model of models) {
568
+ if (model === anchorModel) {
569
+ continue;
570
+ }
571
+ const result = await validateModel(model);
572
+ if (result.ok) {
573
+ continue;
574
+ }
575
+ const affectedRoles = rolesUsingModel(degraded, model);
576
+ for (const role of affectedRoles) {
577
+ degraded[role] = anchorModel;
578
+ }
579
+ io.stderr.write(`${formatPresetFallbackWarning(affectedRoles, model, anchorModel)}\n`);
580
+ }
581
+ return { status: 'ready', models: degraded };
582
+ }
762
583
  export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
763
584
  if (args.skipLlm) {
764
585
  io.stdout.write('│ LLM setup skipped.\n');
@@ -770,7 +591,6 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
770
591
  !args.llmBackend &&
771
592
  !args.anthropicApiKeyEnv &&
772
593
  !args.anthropicApiKeyFile &&
773
- !args.llmModel &&
774
594
  !args.vertexProject &&
775
595
  !args.vertexLocation) {
776
596
  io.stdout.write(`│ LLM ready: yes (${project.config.llm.models.default})\n`);
@@ -795,80 +615,50 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
795
615
  if (vertex.status !== 'ready') {
796
616
  return { status: vertex.status, projectDir: args.projectDir };
797
617
  }
798
- const model = await chooseVertexModel(backendArgs, io, deps);
799
- if (model.status === 'back' && !backendArgs.vertexLocation) {
618
+ const preset = presetForBackend('vertex');
619
+ const validation = await validatePresetModels(preset, async (model) => runLlmHealthCheckWithProgress(buildVertexHealthConfig(vertex.values, model), 'Vertex AI', model, healthCheck, deps), io);
620
+ if (validation.status !== 'ready') {
621
+ io.stderr.write(`Vertex AI Anthropic model health check failed: ${formatVertexHealthFailure(validation.message, vertex.values)}\n`);
622
+ if (args.inputMode === 'disabled') {
623
+ return { status: 'failed', projectDir: args.projectDir };
624
+ }
625
+ io.stderr.write('Choose a different Vertex AI project or location, or Back.\n');
800
626
  attemptArgs = buildInteractiveRetryArgs(args, backendChoice.backend);
801
627
  continue;
802
628
  }
803
- if (model.status === 'invalid-credential') {
804
- return { status: 'failed', projectDir: args.projectDir };
805
- }
806
- if (model.status !== 'ready') {
807
- return { status: model.status, projectDir: args.projectDir };
808
- }
809
- const health = await runLlmHealthCheckWithProgress(buildVertexHealthConfig(vertex.values, model.model), 'Vertex AI', model.model, healthCheck, deps);
810
- if (health.ok) {
811
- await persistLlmConfig(args.projectDir, { backend: 'vertex', vertex: vertex.refs }, model.model);
812
- io.stdout.write(`│ LLM ready: yes (${model.model})\n`);
813
- return { status: 'ready', projectDir: args.projectDir };
814
- }
815
- io.stderr.write(`Vertex AI Anthropic model health check failed: ${formatVertexHealthFailure(health.message, vertex.values)}\n`);
816
- if (args.inputMode === 'disabled') {
817
- return { status: 'failed', projectDir: args.projectDir };
818
- }
819
- io.stderr.write('Choose a different Vertex AI project, location, or model, or Back.\n');
820
- attemptArgs = buildInteractiveRetryArgs(args, backendChoice.backend);
821
- continue;
629
+ await persistLlmConfig(args.projectDir, { backend: 'vertex', vertex: vertex.refs }, validation.models);
630
+ io.stdout.write(`│ LLM ready: yes (${validation.models.default})\n`);
631
+ return { status: 'ready', projectDir: args.projectDir };
822
632
  }
823
633
  if (backendChoice.backend === 'claude-code') {
824
- const model = await chooseClaudeCodeModel(backendArgs, deps);
825
- if (model.status === 'back' && backendChoice.prompted) {
826
- attemptArgs = buildInteractiveRetryArgs(args);
827
- continue;
828
- }
829
- if (model.status === 'invalid-credential') {
830
- return { status: 'failed', projectDir: args.projectDir };
831
- }
832
- if (model.status !== 'ready') {
833
- return { status: model.status, projectDir: args.projectDir };
834
- }
634
+ const preset = presetForBackend('claude-code');
835
635
  const probe = deps.claudeCodeAuthProbe ?? runClaudeCodeAuthProbe;
836
- const health = await probe({ projectDir: args.projectDir, model: model.model, env: deps.env ?? process.env });
837
- if (!health.ok) {
838
- io.stderr.write(`${health.message}\n`);
636
+ const validation = await validatePresetModels(preset, async (model) => probe({ projectDir: args.projectDir, model, env: deps.env ?? process.env }), io);
637
+ if (validation.status !== 'ready') {
638
+ io.stderr.write(`${validation.message}\n`);
839
639
  return { status: 'failed', projectDir: args.projectDir };
840
640
  }
841
- const warning = formatClaudeCodePromptCachingWarning(ignoredClaudeCodePromptCachingFields(buildProjectLlmConfig(project.config.llm, { backend: 'claude-code' }, model.model)));
641
+ const warning = formatClaudeCodePromptCachingWarning(ignoredClaudeCodePromptCachingFields(buildProjectLlmConfig(project.config.llm, { backend: 'claude-code' }, validation.models)));
842
642
  if (warning) {
843
643
  io.stderr.write(`${warning}\n`);
844
644
  }
845
- await persistLlmConfig(args.projectDir, { backend: 'claude-code' }, model.model);
846
- io.stdout.write(`│ LLM ready: yes (${model.model})\n`);
645
+ await persistLlmConfig(args.projectDir, { backend: 'claude-code' }, validation.models);
646
+ io.stdout.write(`│ LLM ready: yes (${validation.models.default})\n`);
847
647
  return { status: 'ready', projectDir: args.projectDir };
848
648
  }
849
649
  if (backendChoice.backend === 'codex') {
850
- const model = await chooseCodexModel(backendArgs, deps);
851
- if (model.status === 'back' && backendChoice.prompted) {
852
- attemptArgs = buildInteractiveRetryArgs(args);
853
- continue;
854
- }
855
- if (model.status === 'invalid-credential') {
856
- return { status: 'failed', projectDir: args.projectDir };
857
- }
858
- if (model.status !== 'ready') {
859
- return { status: model.status, projectDir: args.projectDir };
860
- }
650
+ const preset = presetForBackend('codex');
861
651
  const probe = deps.codexAuthProbe ?? runCodexAuthProbe;
862
- const health = await probe({ projectDir: args.projectDir, model: model.model });
863
- if (!health.ok) {
864
- io.stderr.write(`${health.message}\n`);
652
+ const validation = await validatePresetModels(preset, async (model) => probe({ projectDir: args.projectDir, model }), io);
653
+ if (validation.status !== 'ready') {
654
+ io.stderr.write(`${validation.message}\n`);
865
655
  return { status: 'failed', projectDir: args.projectDir };
866
656
  }
867
657
  // Prefix the clack gutter so the warning sits inside the setup frame
868
658
  // instead of breaking out of it; kept on stderr for scripted runs.
869
659
  io.stderr.write(`│ ${formatCodexIsolationWarning()}\n`);
870
- await persistLlmConfig(args.projectDir, { backend: 'codex' }, model.model);
871
- io.stdout.write(`│ LLM ready: yes (codex, ${model.model})\n`);
660
+ await persistLlmConfig(args.projectDir, { backend: 'codex' }, validation.models);
661
+ io.stdout.write(`│ LLM ready: yes (codex, ${validation.models.default})\n`);
872
662
  return { status: 'ready', projectDir: args.projectDir };
873
663
  }
874
664
  const credential = await chooseCredentialRef(backendArgs, io, deps);
@@ -879,8 +669,10 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
879
669
  if (credential.status !== 'ready') {
880
670
  return { status: credential.status, projectDir: args.projectDir };
881
671
  }
882
- const model = await chooseModel(backendArgs, credential.value, io, deps);
883
- if (model.status === 'invalid-credential') {
672
+ const preset = presetForBackend('anthropic');
673
+ const validation = await validatePresetModels(preset, async (model) => runLlmHealthCheckWithProgress(buildAnthropicHealthConfig(credential.value, model), 'Anthropic API', model, healthCheck, deps), io);
674
+ if (validation.status !== 'ready') {
675
+ io.stderr.write(`Anthropic model health check failed: ${validation.message}\n`);
884
676
  if (args.inputMode === 'disabled') {
885
677
  return { status: 'failed', projectDir: args.projectDir };
886
678
  }
@@ -888,24 +680,8 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
888
680
  attemptArgs = buildInteractiveRetryArgs(args, backendChoice.backend);
889
681
  continue;
890
682
  }
891
- if (model.status === 'back' && !backendArgs.anthropicApiKeyEnv && !backendArgs.anthropicApiKeyFile) {
892
- attemptArgs = buildInteractiveRetryArgs(args, backendChoice.backend);
893
- continue;
894
- }
895
- if (model.status !== 'ready') {
896
- return { status: model.status, projectDir: args.projectDir };
897
- }
898
- const health = await runLlmHealthCheckWithProgress(buildAnthropicHealthConfig(credential.value, model.model), 'Anthropic API', model.model, healthCheck, deps);
899
- if (health.ok) {
900
- await persistLlmConfig(args.projectDir, { backend: 'anthropic', credentialRef: credential.ref }, model.model);
901
- io.stdout.write(`│ LLM ready: yes (${model.model})\n`);
902
- return { status: 'ready', projectDir: args.projectDir };
903
- }
904
- io.stderr.write(`Anthropic model health check failed: ${health.message}\n`);
905
- if (args.inputMode === 'disabled') {
906
- return { status: 'failed', projectDir: args.projectDir };
907
- }
908
- io.stderr.write('Choose a different credential source or model, or Back.\n');
909
- attemptArgs = buildInteractiveRetryArgs(args, backendChoice.backend);
683
+ await persistLlmConfig(args.projectDir, { backend: 'anthropic', credentialRef: credential.ref }, validation.models);
684
+ io.stdout.write(`│ LLM ready: yes (${validation.models.default})\n`);
685
+ return { status: 'ready', projectDir: args.projectDir };
910
686
  }
911
687
  }
@@ -1,5 +1,7 @@
1
- import { autocomplete, autocompleteMultiselect, cancel, confirm, intro, isCancel, log, multiselect, note, password, select, text, } from '@clack/prompts';
1
+ import { autocomplete, autocompleteMultiselect, cancel, confirm, intro, isCancel, log, multiselect, note, select, text, } from '@clack/prompts';
2
+ import { isWritableTtyOutput } from './io/tty.js';
2
3
  import { withMenuOptionsSpacing, withTextInputNavigation } from './prompt-navigation.js';
4
+ import { revealPassword } from './reveal-password-prompt.js';
3
5
  import { withSetupInterruptConfirmation } from './setup-interrupt.js';
4
6
  const DEFAULT_SETUP_CANCEL_MESSAGE = 'Setup cancelled.';
5
7
  export function createKtxSetupPromptAdapter(options) {
@@ -89,7 +91,7 @@ export function createKtxSetupPromptAdapter(options) {
89
91
  return isCancel(value) ? undefined : String(value);
90
92
  },
91
93
  async password(promptOptions) {
92
- const value = await withSetupInterruptConfirmation(() => password({ ...promptOptions, message: withTextInputNavigation(promptOptions.message) }));
94
+ const value = await withSetupInterruptConfirmation(() => revealPassword({ ...promptOptions, message: withTextInputNavigation(promptOptions.message) }));
93
95
  return isCancel(value) ? undefined : String(value);
94
96
  },
95
97
  cancel(message) {
@@ -100,11 +102,6 @@ export function createKtxSetupPromptAdapter(options) {
100
102
  },
101
103
  };
102
104
  }
103
- function isWritableTtyOutput(output) {
104
- return (output.isTTY === true &&
105
- typeof output.on === 'function' &&
106
- typeof output.columns !== 'undefined');
107
- }
108
105
  export function createKtxSetupUiAdapter() {
109
106
  return {
110
107
  intro(title, io) {