@kaelio/ktx 0.8.0 → 0.10.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 (183) hide show
  1. package/assets/python/{kaelio_ktx-0.8.0-py3-none-any.whl → kaelio_ktx-0.10.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 +42 -2
  8. package/dist/cli-runtime.d.ts +3 -0
  9. package/dist/cli-runtime.js +94 -3
  10. package/dist/commands/setup-commands.js +3 -4
  11. package/dist/connection-recovery.d.ts +34 -0
  12. package/dist/connection-recovery.js +82 -0
  13. package/dist/connection.js +26 -2
  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/ingest/adapters/historic-sql/bigquery-query-history-reader.js +71 -20
  36. package/dist/context/ingest/adapters/historic-sql/chunk-unified.js +2 -1
  37. package/dist/context/ingest/adapters/historic-sql/connection-dialect.d.ts +9 -0
  38. package/dist/context/ingest/adapters/historic-sql/connection-dialect.js +15 -4
  39. package/dist/context/ingest/adapters/historic-sql/pattern-inputs.js +8 -2
  40. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.d.ts +30 -0
  41. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +194 -0
  42. package/dist/context/ingest/adapters/historic-sql/scope-floor.d.ts +18 -0
  43. package/dist/context/ingest/adapters/historic-sql/scope-floor.js +229 -0
  44. package/dist/context/ingest/adapters/historic-sql/scope-membership.d.ts +8 -0
  45. package/dist/context/ingest/adapters/historic-sql/scope-membership.js +29 -0
  46. package/dist/context/ingest/adapters/historic-sql/snowflake-query-history-reader.js +68 -19
  47. package/dist/context/ingest/adapters/historic-sql/stage-unified.js +57 -50
  48. package/dist/context/ingest/adapters/historic-sql/types.d.ts +36 -3
  49. package/dist/context/ingest/adapters/historic-sql/types.js +14 -2
  50. package/dist/context/ingest/context-candidates/curator-pagination.service.d.ts +1 -5
  51. package/dist/context/ingest/context-candidates/curator-pagination.service.js +1 -3
  52. package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
  53. package/dist/context/ingest/final-gate-repair.d.ts +1 -0
  54. package/dist/context/ingest/final-gate-repair.js +1 -0
  55. package/dist/context/ingest/ingest-bundle.runner.d.ts +3 -0
  56. package/dist/context/ingest/ingest-bundle.runner.js +127 -53
  57. package/dist/context/ingest/isolated-diff/patch-integrator.js +75 -5
  58. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +1 -0
  59. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +1 -0
  60. package/dist/context/ingest/isolated-diff/work-unit-executor.d.ts +1 -0
  61. package/dist/context/ingest/local-adapters.js +21 -4
  62. package/dist/context/ingest/local-bundle-runtime.js +13 -5
  63. package/dist/context/ingest/local-ingest.d.ts +1 -0
  64. package/dist/context/ingest/local-ingest.js +13 -3
  65. package/dist/context/ingest/memory-flow/events.js +1 -1
  66. package/dist/context/ingest/memory-flow/schema.js +8 -3
  67. package/dist/context/ingest/memory-flow/types.d.ts +7 -3
  68. package/dist/context/ingest/ports.d.ts +3 -5
  69. package/dist/context/ingest/stages/stage-3-work-units.d.ts +1 -4
  70. package/dist/context/ingest/stages/stage-3-work-units.js +5 -1
  71. package/dist/context/ingest/stages/stage-4-reconciliation.d.ts +1 -4
  72. package/dist/context/ingest/stages/stage-4-reconciliation.js +1 -1
  73. package/dist/context/ingest/types.d.ts +1 -0
  74. package/dist/context/llm/ai-sdk-runtime.d.ts +3 -0
  75. package/dist/context/llm/ai-sdk-runtime.js +152 -16
  76. package/dist/context/llm/claude-code-runtime.d.ts +6 -4
  77. package/dist/context/llm/claude-code-runtime.js +127 -48
  78. package/dist/context/llm/codex-exec-events.d.ts +20 -0
  79. package/dist/context/llm/codex-exec-events.js +155 -0
  80. package/dist/context/llm/codex-isolation.d.ts +3 -0
  81. package/dist/context/llm/codex-isolation.js +5 -0
  82. package/dist/context/llm/codex-mcp-runtime-server.d.ts +24 -0
  83. package/dist/context/llm/codex-mcp-runtime-server.js +51 -0
  84. package/dist/context/llm/codex-models.d.ts +2 -0
  85. package/dist/context/llm/codex-models.js +17 -0
  86. package/dist/context/llm/codex-runtime-config.d.ts +16 -0
  87. package/dist/context/llm/codex-runtime-config.js +19 -0
  88. package/dist/context/llm/codex-runtime.d.ts +37 -0
  89. package/dist/context/llm/codex-runtime.js +347 -0
  90. package/dist/context/llm/codex-sdk-runner.d.ts +21 -0
  91. package/dist/context/llm/codex-sdk-runner.js +63 -0
  92. package/dist/context/llm/local-config.d.ts +16 -4
  93. package/dist/context/llm/local-config.js +18 -2
  94. package/dist/context/llm/rate-limit-governor.d.ts +103 -0
  95. package/dist/context/llm/rate-limit-governor.js +285 -0
  96. package/dist/context/llm/runtime-port.d.ts +3 -6
  97. package/dist/context/mcp/context-tools.js +43 -13
  98. package/dist/context/project/config.d.ts +14 -0
  99. package/dist/context/project/config.js +37 -2
  100. package/dist/context/scan/types.d.ts +15 -2
  101. package/dist/context/scan/types.js +12 -0
  102. package/dist/context/sl/description-normalization.js +4 -14
  103. package/dist/context/sql-analysis/http-sql-analysis-port.js +32 -2
  104. package/dist/context/sql-analysis/ports.d.ts +12 -2
  105. package/dist/context/tools/context-candidate-mark.tool.d.ts +2 -2
  106. package/dist/context-build-view.d.ts +13 -0
  107. package/dist/context-build-view.js +63 -32
  108. package/dist/demo-metrics.d.ts +0 -2
  109. package/dist/demo-metrics.js +1 -11
  110. package/dist/ingest.d.ts +1 -0
  111. package/dist/ingest.js +32 -3
  112. package/dist/io/buffered-command-io.d.ts +11 -0
  113. package/dist/io/buffered-command-io.js +28 -0
  114. package/dist/io/symbols.d.ts +2 -0
  115. package/dist/io/symbols.js +2 -0
  116. package/dist/llm/types.d.ts +1 -1
  117. package/dist/local-adapters.d.ts +10 -2
  118. package/dist/local-adapters.js +19 -3
  119. package/dist/memory-flow-hud.js +8 -16
  120. package/dist/next-steps.js +1 -2
  121. package/dist/progress-port-adapter.d.ts +6 -0
  122. package/dist/progress-port-adapter.js +18 -0
  123. package/dist/public-ingest.d.ts +20 -1
  124. package/dist/public-ingest.js +228 -42
  125. package/dist/reveal-password-prompt.d.ts +24 -0
  126. package/dist/reveal-password-prompt.js +78 -0
  127. package/dist/scan.js +21 -3
  128. package/dist/setup-context.d.ts +2 -0
  129. package/dist/setup-context.js +133 -27
  130. package/dist/setup-databases.d.ts +18 -1
  131. package/dist/setup-databases.js +378 -249
  132. package/dist/setup-demo-tour.js +1 -0
  133. package/dist/setup-embeddings.js +1 -1
  134. package/dist/setup-models.d.ts +11 -15
  135. package/dist/setup-models.js +140 -276
  136. package/dist/setup-prompts.js +3 -2
  137. package/dist/setup-ready-menu.d.ts +16 -2
  138. package/dist/setup-ready-menu.js +37 -5
  139. package/dist/setup-sources.js +115 -35
  140. package/dist/setup.d.ts +1 -1
  141. package/dist/setup.js +23 -11
  142. package/dist/sl.d.ts +2 -2
  143. package/dist/sl.js +20 -4
  144. package/dist/sql.js +18 -2
  145. package/dist/star-prompt/cache.d.ts +16 -0
  146. package/dist/star-prompt/cache.js +45 -0
  147. package/dist/star-prompt/star-count.d.ts +7 -0
  148. package/dist/star-prompt/star-count.js +66 -0
  149. package/dist/star-prompt/star-line.d.ts +12 -0
  150. package/dist/star-prompt/star-line.js +26 -0
  151. package/dist/status-project.d.ts +11 -0
  152. package/dist/status-project.js +50 -1
  153. package/dist/telemetry/command-hook.d.ts +1 -0
  154. package/dist/telemetry/command-hook.js +3 -1
  155. package/dist/telemetry/emitter.d.ts +10 -0
  156. package/dist/telemetry/emitter.js +31 -0
  157. package/dist/telemetry/events.d.ts +35 -6
  158. package/dist/telemetry/events.js +25 -2
  159. package/dist/telemetry/exception.d.ts +18 -0
  160. package/dist/telemetry/exception.js +162 -0
  161. package/dist/telemetry/identity.d.ts +0 -1
  162. package/dist/telemetry/identity.js +6 -6
  163. package/dist/telemetry/index.d.ts +15 -2
  164. package/dist/telemetry/index.js +15 -3
  165. package/dist/telemetry/redaction-secrets.d.ts +11 -0
  166. package/dist/telemetry/redaction-secrets.js +92 -0
  167. package/dist/telemetry/scrubber.d.ts +10 -0
  168. package/dist/telemetry/scrubber.js +20 -0
  169. package/dist/update-check/cache.d.ts +21 -0
  170. package/dist/update-check/cache.js +38 -0
  171. package/dist/update-check/channel.d.ts +15 -0
  172. package/dist/update-check/channel.js +30 -0
  173. package/dist/update-check/registry.d.ts +1 -0
  174. package/dist/update-check/registry.js +45 -0
  175. package/dist/update-check/update-check.d.ts +43 -0
  176. package/dist/update-check/update-check.js +116 -0
  177. package/package.json +12 -4
  178. package/dist/context/connections/local-query-executor.d.ts +0 -6
  179. package/dist/context/connections/local-query-executor.js +0 -39
  180. package/dist/context/connections/postgres-query-executor.d.ts +0 -25
  181. package/dist/context/connections/postgres-query-executor.js +0 -53
  182. package/dist/context/connections/sqlite-query-executor.d.ts +0 -4
  183. package/dist/context/connections/sqlite-query-executor.js +0 -74
@@ -191,6 +191,7 @@ async function runDemoContextReplay(io, stdin) {
191
191
  frame: 0,
192
192
  startedAt: Date.now(),
193
193
  totalElapsedMs: 0,
194
+ starCount: null,
194
195
  };
195
196
  const allTargets = [...allPrimary, ...allContext];
196
197
  const timeline = buildDemoReplayTimeline();
@@ -138,8 +138,8 @@ async function chooseCredentialRef(backend, args, io, deps) {
138
138
  const choice = await prompts.select({
139
139
  message: `How should KTX find your ${embeddingBackendDisplayName(backend)} embedding API key?`,
140
140
  options: [
141
- { value: 'env', label: `Use ${defaultEnv} from the environment` },
142
141
  { value: 'paste', label: 'Paste a key and save it as a local secret file' },
142
+ { value: 'env', label: `Use ${defaultEnv} from the environment` },
143
143
  { value: 'back', label: 'Back' },
144
144
  ],
145
145
  });
@@ -1,5 +1,5 @@
1
1
  import { type KtxProjectLlmConfig } from './context/project/config.js';
2
- import type { KtxLlmConfig } from './llm/types.js';
2
+ import { type KtxLlmConfig } from './llm/types.js';
3
3
  import { type KtxLlmHealthCheckResult } from './llm/model-health.js';
4
4
  import { type KtxCliSpinner } from './clack.js';
5
5
  import type { KtxCliIo } from './cli-runtime.js';
@@ -10,7 +10,6 @@ export interface KtxSetupModelArgs {
10
10
  llmBackend?: KtxSetupLlmBackend;
11
11
  anthropicApiKeyEnv?: string;
12
12
  anthropicApiKeyFile?: string;
13
- llmModel?: string;
14
13
  vertexProject?: string;
15
14
  vertexLocation?: string;
16
15
  forcePrompt?: boolean;
@@ -33,13 +32,7 @@ export type KtxSetupModelResult = {
33
32
  status: 'failed';
34
33
  projectDir: string;
35
34
  };
36
- /** @internal */
37
- export interface AnthropicModelChoice {
38
- id: string;
39
- label: string;
40
- recommended: boolean;
41
- }
42
- export type KtxSetupLlmBackend = 'anthropic' | 'vertex' | 'claude-code';
35
+ export type KtxSetupLlmBackend = 'anthropic' | 'vertex' | 'claude-code' | 'codex';
43
36
  /** @internal */
44
37
  export interface KtxSetupModelPromptAdapter {
45
38
  select(options: {
@@ -62,9 +55,7 @@ export interface KtxSetupModelPromptAdapter {
62
55
  }
63
56
  export interface KtxSetupModelDeps {
64
57
  env?: NodeJS.ProcessEnv;
65
- fetch?: typeof fetch;
66
58
  prompts?: KtxSetupModelPromptAdapter;
67
- listModels?: (apiKey: string) => Promise<AnthropicModelChoice[]>;
68
59
  healthCheck?: (config: KtxLlmConfig) => Promise<KtxLlmHealthCheckResult>;
69
60
  claudeCodeAuthProbe?: (input: {
70
61
  projectDir: string;
@@ -76,18 +67,23 @@ export interface KtxSetupModelDeps {
76
67
  ok: false;
77
68
  message: string;
78
69
  }>;
70
+ codexAuthProbe?: (input: {
71
+ projectDir: string;
72
+ model: string;
73
+ }) => Promise<{
74
+ ok: true;
75
+ } | {
76
+ ok: false;
77
+ message: string;
78
+ }>;
79
79
  readGcloudProject?: () => Promise<string | undefined>;
80
80
  listGcloudProjects?: () => Promise<GcloudProjectChoice[]>;
81
81
  spinner?: () => KtxCliSpinner;
82
82
  }
83
- /** @internal */
84
- export declare const BUNDLED_ANTHROPIC_MODELS: AnthropicModelChoice[];
85
83
  interface GcloudProjectChoice {
86
84
  projectId: string;
87
85
  name?: string;
88
86
  }
89
- /** @internal */
90
- export declare function fetchAnthropicModels(apiKey: string, fetchFn?: typeof fetch): Promise<AnthropicModelChoice[]>;
91
87
  export declare function isKtxSetupLlmConfigReady(config: KtxProjectLlmConfig): boolean;
92
88
  export declare function runKtxSetupAnthropicModelStep(args: KtxSetupModelArgs, io: KtxCliIo, deps?: KtxSetupModelDeps): Promise<KtxSetupModelResult>;
93
89
  export {};
@@ -3,10 +3,14 @@ import { writeFile } from 'node:fs/promises';
3
3
  import { promisify } from 'node:util';
4
4
  import { resolveLocalKtxLlmConfig } from './context/llm/local-config.js';
5
5
  import { runClaudeCodeAuthProbe } from './context/llm/claude-code-runtime.js';
6
+ import { formatCodexIsolationWarning } from './context/llm/codex-isolation.js';
7
+ import { runCodexAuthProbe } from './context/llm/codex-runtime.js';
8
+ import { DEFAULT_CODEX_MODEL } from './context/llm/codex-models.js';
6
9
  import { resolveKtxConfigReference } from './context/core/config-reference.js';
7
10
  import { serializeKtxProjectConfig } from './context/project/config.js';
8
11
  import { loadKtxProject } from './context/project/project.js';
9
12
  import { markKtxSetupStateStepComplete } from './context/project/setup-config.js';
13
+ import { KTX_MODEL_ROLES } from './llm/types.js';
10
14
  import { runKtxLlmHealthCheck } from './llm/model-health.js';
11
15
  import { formatClaudeCodePromptCachingWarning, ignoredClaudeCodePromptCachingFields, } from './claude-code-prompt-caching.js';
12
16
  import { createClackSpinner } from './clack.js';
@@ -17,57 +21,46 @@ const ESC = String.fromCharCode(0x1b);
17
21
  function yellow(text) {
18
22
  return `${ESC}[33m${text}${ESC}[39m`;
19
23
  }
20
- /** @internal */
21
- export const BUNDLED_ANTHROPIC_MODELS = [
22
- { id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', recommended: true },
23
- { id: 'claude-opus-4-6', label: 'Claude Opus 4.6', recommended: false },
24
- { id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5', recommended: false },
25
- ];
26
- const VERTEX_ANTHROPIC_MODELS = [
27
- { id: 'claude-opus-4-7', label: 'Claude Opus 4.7', recommended: false },
28
- { id: 'claude-sonnet-4-6', label: 'Claude Sonnet 4.6', recommended: false },
29
- { id: 'claude-opus-4-6', label: 'Claude Opus 4.6', recommended: false },
30
- { id: 'claude-opus-4-5', label: 'Claude Opus 4.5', recommended: false },
31
- { id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5', recommended: false },
32
- { id: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5', recommended: false },
33
- { id: 'claude-opus-4-1', label: 'Claude Opus 4.1', recommended: false },
34
- ];
35
- const CLAUDE_CODE_MODELS = [
36
- { id: 'sonnet', label: 'Claude Sonnet', recommended: true },
37
- { id: 'opus', label: 'Claude Opus', recommended: false },
38
- { id: 'haiku', label: 'Claude Haiku', recommended: false },
39
- ];
40
- const HIDDEN_ANTHROPIC_MODEL_PATTERNS = [
41
- /^claude-sonnet-4$/i,
42
- /^claude-opus-4$/i,
43
- /^Claude Sonnet 4$/i,
44
- /^Claude Opus 4$/i,
45
- ];
46
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, ' +
47
25
  'BI metadata, and docs into semantic-layer sources and wiki context. ktx.yaml stores an env: or file: ' +
48
26
  'reference, not the raw key.';
49
- const ANTHROPIC_MODEL_PROMPT_CONTEXT = 'KTX uses this as the default model for ingest agents that turn schemas, SQL, BI metadata, and docs ' +
50
- 'into semantic-layer sources and wiki context.';
51
27
  const VERTEX_PROJECT_PROMPT_CONTEXT = 'KTX stores the selected Google Cloud project ID in ktx.yaml and uses Application Default Credentials for ' +
52
28
  'access. Project visibility depends on the signed-in Google account and organization permissions.';
53
29
  const DEFAULT_VERTEX_LOCATION = 'us-east5';
54
- const execFileAsync = promisify(execFile);
55
- class AnthropicModelDiscoveryError extends Error {
56
- reason;
57
- status;
58
- constructor(message, reason, status) {
59
- super(message);
60
- this.reason = reason;
61
- this.status = status;
62
- this.name = 'AnthropicModelDiscoveryError';
63
- }
64
- }
65
- function isAnthropicModelAuthenticationError(error) {
66
- return error instanceof AnthropicModelDiscoveryError && error.reason === 'authentication';
67
- }
68
- function isSelectableAnthropicModel(model) {
69
- 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];
70
62
  }
63
+ const execFileAsync = promisify(execFile);
71
64
  function createPromptAdapter() {
72
65
  return createKtxSetupPromptAdapter({ selectCancelValue: 'back' });
73
66
  }
@@ -106,35 +99,6 @@ async function defaultListGcloudProjects() {
106
99
  })
107
100
  .filter((project) => Boolean(project));
108
101
  }
109
- /** @internal */
110
- export async function fetchAnthropicModels(apiKey, fetchFn = fetch) {
111
- const response = await fetchFn('https://api.anthropic.com/v1/models?limit=1000', {
112
- headers: {
113
- 'anthropic-version': '2023-06-01',
114
- 'x-api-key': apiKey,
115
- },
116
- });
117
- if (!response.ok) {
118
- if (response.status === 401 || response.status === 403) {
119
- throw new AnthropicModelDiscoveryError(`Anthropic model discovery failed with HTTP ${response.status}`, 'authentication', response.status);
120
- }
121
- throw new AnthropicModelDiscoveryError(`Anthropic model discovery failed with HTTP ${response.status}`, 'http', response.status);
122
- }
123
- const body = (await response.json());
124
- const models = (body.data ?? [])
125
- .map((item) => ({
126
- id: typeof item.id === 'string' ? item.id : '',
127
- label: typeof item.display_name === 'string' ? item.display_name : typeof item.id === 'string' ? item.id : '',
128
- recommended: false,
129
- }))
130
- .filter((item) => item.id.startsWith('claude-'))
131
- .filter(isSelectableAnthropicModel);
132
- if (models.length === 0) {
133
- throw new AnthropicModelDiscoveryError('Anthropic model discovery returned no Claude models', 'empty-response');
134
- }
135
- const recommendedIndex = models.findIndex((item) => item.id.includes('sonnet'));
136
- return models.map((item, index) => ({ ...item, recommended: index === Math.max(recommendedIndex, 0) }));
137
- }
138
102
  export function isKtxSetupLlmConfigReady(config) {
139
103
  let resolved;
140
104
  try {
@@ -149,16 +113,26 @@ export function isKtxSetupLlmConfigReady(config) {
149
113
  if (resolved.backend === 'vertex') {
150
114
  return typeof resolved.vertex?.location === 'string' && resolved.vertex.location.trim().length > 0;
151
115
  }
152
- return resolved.backend === 'anthropic' || resolved.backend === 'gateway' || resolved.backend === 'claude-code';
116
+ return (resolved.backend === 'anthropic' ||
117
+ resolved.backend === 'gateway' ||
118
+ resolved.backend === 'claude-code' ||
119
+ resolved.backend === 'codex');
153
120
  }
154
121
  function hasUsableConfiguredLlm(config) {
155
122
  return isKtxSetupLlmConfigReady(config.llm);
156
123
  }
157
- function buildProjectLlmConfig(existing, provider, model) {
124
+ function buildProjectLlmConfig(existing, provider, models) {
158
125
  if (provider.backend === 'claude-code') {
159
126
  return {
160
127
  provider: { backend: 'claude-code' },
161
- models: { ...existing.models, default: model },
128
+ models,
129
+ promptCaching: existing.promptCaching,
130
+ };
131
+ }
132
+ if (provider.backend === 'codex') {
133
+ return {
134
+ provider: { backend: 'codex' },
135
+ models,
162
136
  promptCaching: existing.promptCaching,
163
137
  };
164
138
  }
@@ -168,7 +142,7 @@ function buildProjectLlmConfig(existing, provider, model) {
168
142
  backend: 'vertex',
169
143
  vertex: provider.vertex,
170
144
  },
171
- models: { ...existing.models, default: model },
145
+ models,
172
146
  promptCaching: { ...(existing.promptCaching ?? {}), enabled: true, vertexFallbackTo5m: true },
173
147
  };
174
148
  }
@@ -177,7 +151,7 @@ function buildProjectLlmConfig(existing, provider, model) {
177
151
  backend: 'anthropic',
178
152
  anthropic: { api_key: provider.credentialRef },
179
153
  },
180
- models: { ...existing.models, default: model },
154
+ models,
181
155
  promptCaching: { ...(existing.promptCaching ?? {}), enabled: true },
182
156
  };
183
157
  }
@@ -276,8 +250,8 @@ async function chooseCredentialRef(args, io, deps) {
276
250
  const choice = await prompts.select({
277
251
  message: `How should KTX find your Anthropic API key?\n\n${ANTHROPIC_CREDENTIAL_PROMPT_CONTEXT}`,
278
252
  options: [
279
- { value: 'env', label: 'Use ANTHROPIC_API_KEY from the environment' },
280
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' },
281
255
  { value: 'back', label: 'Back' },
282
256
  ],
283
257
  });
@@ -316,14 +290,11 @@ function requestedBackend(args) {
316
290
  if (args.vertexProject || args.vertexLocation) {
317
291
  return 'vertex';
318
292
  }
319
- if (args.anthropicApiKeyEnv || args.anthropicApiKeyFile || args.llmModel) {
293
+ if (args.anthropicApiKeyEnv || args.anthropicApiKeyFile) {
320
294
  return 'anthropic';
321
295
  }
322
296
  return undefined;
323
297
  }
324
- function requestedModel(args) {
325
- return args.llmModel;
326
- }
327
298
  async function chooseBackend(args, io, deps) {
328
299
  const explicit = requestedBackend(args);
329
300
  if (explicit) {
@@ -340,6 +311,7 @@ async function chooseBackend(args, io, deps) {
340
311
  message: 'Which LLM provider should KTX use?',
341
312
  options: [
342
313
  { value: 'claude-code', label: 'Claude subscription (Pro/Max)' },
314
+ { value: 'codex', label: 'Codex subscription' },
343
315
  { value: 'anthropic', label: 'Anthropic API key' },
344
316
  { value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' },
345
317
  { value: 'back', label: 'Back' },
@@ -350,7 +322,7 @@ async function chooseBackend(args, io, deps) {
350
322
  }
351
323
  return {
352
324
  status: 'ready',
353
- backend: choice === 'vertex' || choice === 'claude-code' ? choice : 'anthropic',
325
+ backend: choice === 'vertex' || choice === 'claude-code' || choice === 'codex' ? choice : 'anthropic',
354
326
  prompted: true,
355
327
  };
356
328
  }
@@ -541,141 +513,11 @@ async function chooseVertexConfig(args, io, deps) {
541
513
  },
542
514
  };
543
515
  }
544
- async function chooseModel(args, credentialValue, io, deps) {
545
- const providedModel = requestedModel(args);
546
- if (providedModel) {
547
- return { status: 'ready', model: providedModel };
548
- }
549
- if (args.inputMode === 'disabled') {
550
- io.stderr.write('Missing LLM model: pass --llm-model.\n');
551
- return { status: 'missing-input' };
552
- }
553
- let models;
554
- try {
555
- models = deps.listModels
556
- ? await deps.listModels(credentialValue)
557
- : await fetchAnthropicModels(credentialValue, deps.fetch);
558
- }
559
- catch (error) {
560
- if (isAnthropicModelAuthenticationError(error)) {
561
- const statusSuffix = error.status ? ` (HTTP ${error.status})` : '';
562
- io.stderr.write(`Anthropic API key is invalid or unauthorized${statusSuffix}. Check the key and try again.\n`);
563
- return { status: 'invalid-credential' };
564
- }
565
- io.stderr.write('Could not fetch live Anthropic models. Showing bundled defaults. Setup will still test the selected model before saving it.\n');
566
- models = BUNDLED_ANTHROPIC_MODELS;
567
- }
568
- const selectableModels = models.filter(isSelectableAnthropicModel);
569
- const prompts = deps.prompts ?? createPromptAdapter();
570
- const modelOptions = [
571
- ...selectableModels.map((model) => ({
572
- value: model.id,
573
- label: model.label || model.id,
574
- ...(model.recommended ? { hint: 'recommended' } : {}),
575
- })),
576
- { value: 'manual', label: 'Enter a model ID manually' },
577
- { value: 'back', label: 'Back' },
578
- ];
579
- const choice = await prompts.autocomplete({
580
- message: `Which Anthropic model should KTX use?\n\n${ANTHROPIC_MODEL_PROMPT_CONTEXT}`,
581
- placeholder: 'Type to search models',
582
- options: modelOptions,
583
- });
584
- if (choice === 'back') {
585
- return { status: 'back' };
586
- }
587
- if (choice === 'manual') {
588
- const manual = await prompts.text({
589
- message: withTextInputNavigation('Anthropic model ID'),
590
- placeholder: selectableModels.find((model) => model.recommended)?.id ?? selectableModels[0]?.id,
591
- });
592
- if (manual === undefined) {
593
- return { status: 'back' };
594
- }
595
- return manual.trim() ? { status: 'ready', model: manual.trim() } : { status: 'missing-input' };
596
- }
597
- return { status: 'ready', model: choice };
598
- }
599
- async function chooseVertexModel(args, io, deps) {
600
- const providedModel = requestedModel(args);
601
- if (providedModel) {
602
- return { status: 'ready', model: providedModel };
603
- }
604
- if (args.inputMode === 'disabled') {
605
- io.stderr.write('Missing LLM model: pass --llm-model.\n');
606
- return { status: 'missing-input' };
607
- }
608
- const selectableModels = VERTEX_ANTHROPIC_MODELS.filter(isSelectableAnthropicModel);
609
- const prompts = deps.prompts ?? createPromptAdapter();
610
- const choice = await prompts.autocomplete({
611
- message: `Which Anthropic model should KTX use?\n\n${ANTHROPIC_MODEL_PROMPT_CONTEXT}`,
612
- placeholder: 'Type to search models',
613
- options: [
614
- ...selectableModels.map((model) => ({
615
- value: model.id,
616
- label: model.label || model.id,
617
- ...(model.recommended ? { hint: 'recommended' } : {}),
618
- })),
619
- { value: 'manual', label: 'Enter a model ID manually' },
620
- { value: 'back', label: 'Back' },
621
- ],
622
- });
623
- if (choice === 'back') {
624
- return { status: 'back' };
625
- }
626
- if (choice === 'manual') {
627
- const manual = await prompts.text({
628
- message: withTextInputNavigation('Anthropic model ID'),
629
- placeholder: selectableModels.find((model) => model.recommended)?.id ?? selectableModels[0]?.id,
630
- });
631
- if (manual === undefined) {
632
- return { status: 'back' };
633
- }
634
- return manual.trim() ? { status: 'ready', model: manual.trim() } : { status: 'missing-input' };
635
- }
636
- return { status: 'ready', model: choice };
637
- }
638
- async function chooseClaudeCodeModel(args, deps) {
639
- const providedModel = requestedModel(args);
640
- if (providedModel) {
641
- return { status: 'ready', model: providedModel };
642
- }
643
- if (args.inputMode === 'disabled') {
644
- return { status: 'ready', model: 'sonnet' };
645
- }
646
- const prompts = deps.prompts ?? createPromptAdapter();
647
- const choice = await prompts.select({
648
- message: `Which Claude Code model should KTX use?\n\n${ANTHROPIC_MODEL_PROMPT_CONTEXT}`,
649
- options: [
650
- ...CLAUDE_CODE_MODELS.map((model) => ({
651
- value: model.id,
652
- label: model.label,
653
- ...(model.recommended ? { hint: 'recommended' } : {}),
654
- })),
655
- { value: 'manual', label: 'Enter a Claude Code model ID manually' },
656
- { value: 'back', label: 'Back' },
657
- ],
658
- });
659
- if (choice === 'back') {
660
- return { status: 'back' };
661
- }
662
- if (choice === 'manual') {
663
- const manual = await prompts.text({
664
- message: withTextInputNavigation('Claude Code model ID'),
665
- placeholder: CLAUDE_CODE_MODELS.find((model) => model.recommended)?.id ?? CLAUDE_CODE_MODELS[0]?.id,
666
- });
667
- if (manual === undefined) {
668
- return { status: 'back' };
669
- }
670
- return manual.trim() ? { status: 'ready', model: manual.trim() } : { status: 'missing-input' };
671
- }
672
- return { status: 'ready', model: choice };
673
- }
674
- async function persistLlmConfig(projectDir, provider, model) {
516
+ async function persistLlmConfig(projectDir, provider, models) {
675
517
  const project = await loadKtxProject({ projectDir });
676
518
  const config = {
677
519
  ...project.config,
678
- llm: buildProjectLlmConfig(project.config.llm, provider, model),
520
+ llm: buildProjectLlmConfig(project.config.llm, provider, models),
679
521
  scan: {
680
522
  ...project.config.scan,
681
523
  enrichment: {
@@ -696,6 +538,48 @@ function buildInteractiveRetryArgs(args, backend) {
696
538
  skipLlm: args.skipLlm,
697
539
  };
698
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
+ }
699
583
  export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
700
584
  if (args.skipLlm) {
701
585
  io.stdout.write('│ LLM setup skipped.\n');
@@ -707,7 +591,6 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
707
591
  !args.llmBackend &&
708
592
  !args.anthropicApiKeyEnv &&
709
593
  !args.anthropicApiKeyFile &&
710
- !args.llmModel &&
711
594
  !args.vertexProject &&
712
595
  !args.vertexLocation) {
713
596
  io.stdout.write(`│ LLM ready: yes (${project.config.llm.models.default})\n`);
@@ -732,55 +615,50 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
732
615
  if (vertex.status !== 'ready') {
733
616
  return { status: vertex.status, projectDir: args.projectDir };
734
617
  }
735
- const model = await chooseVertexModel(backendArgs, io, deps);
736
- 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');
737
626
  attemptArgs = buildInteractiveRetryArgs(args, backendChoice.backend);
738
627
  continue;
739
628
  }
740
- if (model.status === 'invalid-credential') {
741
- return { status: 'failed', projectDir: args.projectDir };
742
- }
743
- if (model.status !== 'ready') {
744
- return { status: model.status, projectDir: args.projectDir };
745
- }
746
- const health = await runLlmHealthCheckWithProgress(buildVertexHealthConfig(vertex.values, model.model), 'Vertex AI', model.model, healthCheck, deps);
747
- if (health.ok) {
748
- await persistLlmConfig(args.projectDir, { backend: 'vertex', vertex: vertex.refs }, model.model);
749
- io.stdout.write(`│ LLM ready: yes (${model.model})\n`);
750
- return { status: 'ready', projectDir: args.projectDir };
751
- }
752
- io.stderr.write(`Vertex AI Anthropic model health check failed: ${formatVertexHealthFailure(health.message, vertex.values)}\n`);
753
- if (args.inputMode === 'disabled') {
754
- return { status: 'failed', projectDir: args.projectDir };
755
- }
756
- io.stderr.write('Choose a different Vertex AI project, location, or model, or Back.\n');
757
- attemptArgs = buildInteractiveRetryArgs(args, backendChoice.backend);
758
- 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 };
759
632
  }
760
633
  if (backendChoice.backend === 'claude-code') {
761
- const model = await chooseClaudeCodeModel(backendArgs, deps);
762
- if (model.status === 'back' && backendChoice.prompted) {
763
- attemptArgs = buildInteractiveRetryArgs(args);
764
- continue;
765
- }
766
- if (model.status === 'invalid-credential') {
767
- return { status: 'failed', projectDir: args.projectDir };
768
- }
769
- if (model.status !== 'ready') {
770
- return { status: model.status, projectDir: args.projectDir };
771
- }
634
+ const preset = presetForBackend('claude-code');
772
635
  const probe = deps.claudeCodeAuthProbe ?? runClaudeCodeAuthProbe;
773
- const health = await probe({ projectDir: args.projectDir, model: model.model, env: deps.env ?? process.env });
774
- if (!health.ok) {
775
- 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`);
776
639
  return { status: 'failed', projectDir: args.projectDir };
777
640
  }
778
- 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)));
779
642
  if (warning) {
780
643
  io.stderr.write(`${warning}\n`);
781
644
  }
782
- await persistLlmConfig(args.projectDir, { backend: 'claude-code' }, model.model);
783
- 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`);
647
+ return { status: 'ready', projectDir: args.projectDir };
648
+ }
649
+ if (backendChoice.backend === 'codex') {
650
+ const preset = presetForBackend('codex');
651
+ const probe = deps.codexAuthProbe ?? runCodexAuthProbe;
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`);
655
+ return { status: 'failed', projectDir: args.projectDir };
656
+ }
657
+ // Prefix the clack gutter so the warning sits inside the setup frame
658
+ // instead of breaking out of it; kept on stderr for scripted runs.
659
+ io.stderr.write(`│ ${formatCodexIsolationWarning()}\n`);
660
+ await persistLlmConfig(args.projectDir, { backend: 'codex' }, validation.models);
661
+ io.stdout.write(`│ LLM ready: yes (codex, ${validation.models.default})\n`);
784
662
  return { status: 'ready', projectDir: args.projectDir };
785
663
  }
786
664
  const credential = await chooseCredentialRef(backendArgs, io, deps);
@@ -791,8 +669,10 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
791
669
  if (credential.status !== 'ready') {
792
670
  return { status: credential.status, projectDir: args.projectDir };
793
671
  }
794
- const model = await chooseModel(backendArgs, credential.value, io, deps);
795
- 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`);
796
676
  if (args.inputMode === 'disabled') {
797
677
  return { status: 'failed', projectDir: args.projectDir };
798
678
  }
@@ -800,24 +680,8 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
800
680
  attemptArgs = buildInteractiveRetryArgs(args, backendChoice.backend);
801
681
  continue;
802
682
  }
803
- if (model.status === 'back' && !backendArgs.anthropicApiKeyEnv && !backendArgs.anthropicApiKeyFile) {
804
- attemptArgs = buildInteractiveRetryArgs(args, backendChoice.backend);
805
- continue;
806
- }
807
- if (model.status !== 'ready') {
808
- return { status: model.status, projectDir: args.projectDir };
809
- }
810
- const health = await runLlmHealthCheckWithProgress(buildAnthropicHealthConfig(credential.value, model.model), 'Anthropic API', model.model, healthCheck, deps);
811
- if (health.ok) {
812
- await persistLlmConfig(args.projectDir, { backend: 'anthropic', credentialRef: credential.ref }, model.model);
813
- io.stdout.write(`│ LLM ready: yes (${model.model})\n`);
814
- return { status: 'ready', projectDir: args.projectDir };
815
- }
816
- io.stderr.write(`Anthropic model health check failed: ${health.message}\n`);
817
- if (args.inputMode === 'disabled') {
818
- return { status: 'failed', projectDir: args.projectDir };
819
- }
820
- io.stderr.write('Choose a different credential source or model, or Back.\n');
821
- 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 };
822
686
  }
823
687
  }
@@ -1,5 +1,6 @@
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
2
  import { withMenuOptionsSpacing, withTextInputNavigation } from './prompt-navigation.js';
3
+ import { revealPassword } from './reveal-password-prompt.js';
3
4
  import { withSetupInterruptConfirmation } from './setup-interrupt.js';
4
5
  const DEFAULT_SETUP_CANCEL_MESSAGE = 'Setup cancelled.';
5
6
  export function createKtxSetupPromptAdapter(options) {
@@ -89,7 +90,7 @@ export function createKtxSetupPromptAdapter(options) {
89
90
  return isCancel(value) ? undefined : String(value);
90
91
  },
91
92
  async password(promptOptions) {
92
- const value = await withSetupInterruptConfirmation(() => password({ ...promptOptions, message: withTextInputNavigation(promptOptions.message) }));
93
+ const value = await withSetupInterruptConfirmation(() => revealPassword({ ...promptOptions, message: withTextInputNavigation(promptOptions.message) }));
93
94
  return isCancel(value) ? undefined : String(value);
94
95
  },
95
96
  cancel(message) {