@kaelio/ktx 0.8.0 → 0.9.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 (83) hide show
  1. package/assets/python/{kaelio_ktx-0.8.0-py3-none-any.whl → kaelio_ktx-0.9.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/cli-runtime.js +50 -3
  5. package/dist/commands/setup-commands.js +1 -1
  6. package/dist/connection-recovery.d.ts +34 -0
  7. package/dist/connection-recovery.js +82 -0
  8. package/dist/connection.js +3 -1
  9. package/dist/context/ingest/adapters/historic-sql/bigquery-query-history-reader.js +71 -20
  10. package/dist/context/ingest/adapters/historic-sql/chunk-unified.js +2 -1
  11. package/dist/context/ingest/adapters/historic-sql/connection-dialect.d.ts +9 -0
  12. package/dist/context/ingest/adapters/historic-sql/connection-dialect.js +15 -4
  13. package/dist/context/ingest/adapters/historic-sql/pattern-inputs.js +8 -2
  14. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.d.ts +29 -0
  15. package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +190 -0
  16. package/dist/context/ingest/adapters/historic-sql/scope-floor.d.ts +18 -0
  17. package/dist/context/ingest/adapters/historic-sql/scope-floor.js +229 -0
  18. package/dist/context/ingest/adapters/historic-sql/scope-membership.d.ts +8 -0
  19. package/dist/context/ingest/adapters/historic-sql/scope-membership.js +29 -0
  20. package/dist/context/ingest/adapters/historic-sql/snowflake-query-history-reader.js +68 -19
  21. package/dist/context/ingest/adapters/historic-sql/stage-unified.js +57 -50
  22. package/dist/context/ingest/adapters/historic-sql/types.d.ts +36 -3
  23. package/dist/context/ingest/adapters/historic-sql/types.js +14 -2
  24. package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
  25. package/dist/context/ingest/isolated-diff/patch-integrator.js +75 -5
  26. package/dist/context/ingest/local-adapters.js +21 -4
  27. package/dist/context/ingest/local-bundle-runtime.js +3 -2
  28. package/dist/context/llm/codex-exec-events.d.ts +20 -0
  29. package/dist/context/llm/codex-exec-events.js +155 -0
  30. package/dist/context/llm/codex-isolation.d.ts +3 -0
  31. package/dist/context/llm/codex-isolation.js +5 -0
  32. package/dist/context/llm/codex-mcp-runtime-server.d.ts +24 -0
  33. package/dist/context/llm/codex-mcp-runtime-server.js +51 -0
  34. package/dist/context/llm/codex-models.d.ts +2 -0
  35. package/dist/context/llm/codex-models.js +17 -0
  36. package/dist/context/llm/codex-runtime-config.d.ts +16 -0
  37. package/dist/context/llm/codex-runtime-config.js +19 -0
  38. package/dist/context/llm/codex-runtime.d.ts +37 -0
  39. package/dist/context/llm/codex-runtime.js +304 -0
  40. package/dist/context/llm/codex-sdk-runner.d.ts +21 -0
  41. package/dist/context/llm/codex-sdk-runner.js +63 -0
  42. package/dist/context/llm/local-config.d.ts +2 -0
  43. package/dist/context/llm/local-config.js +12 -1
  44. package/dist/context/project/config.d.ts +2 -0
  45. package/dist/context/project/config.js +2 -2
  46. package/dist/context/sql-analysis/http-sql-analysis-port.js +32 -2
  47. package/dist/context/sql-analysis/ports.d.ts +12 -2
  48. package/dist/context/tools/context-candidate-mark.tool.d.ts +2 -2
  49. package/dist/context-build-view.js +4 -32
  50. package/dist/io/buffered-command-io.d.ts +11 -0
  51. package/dist/io/buffered-command-io.js +28 -0
  52. package/dist/llm/types.d.ts +1 -1
  53. package/dist/local-adapters.d.ts +10 -2
  54. package/dist/local-adapters.js +19 -3
  55. package/dist/next-steps.js +1 -2
  56. package/dist/progress-port-adapter.d.ts +6 -0
  57. package/dist/progress-port-adapter.js +18 -0
  58. package/dist/public-ingest.d.ts +20 -1
  59. package/dist/public-ingest.js +178 -27
  60. package/dist/scan.js +3 -1
  61. package/dist/setup-context.d.ts +2 -0
  62. package/dist/setup-context.js +133 -27
  63. package/dist/setup-databases.d.ts +17 -1
  64. package/dist/setup-databases.js +358 -249
  65. package/dist/setup-models.d.ts +10 -1
  66. package/dist/setup-models.js +90 -2
  67. package/dist/setup-ready-menu.d.ts +16 -2
  68. package/dist/setup-ready-menu.js +37 -5
  69. package/dist/setup-sources.js +108 -28
  70. package/dist/setup.js +22 -10
  71. package/dist/status-project.d.ts +11 -0
  72. package/dist/status-project.js +50 -1
  73. package/dist/telemetry/command-hook.d.ts +1 -0
  74. package/dist/telemetry/command-hook.js +3 -1
  75. package/dist/telemetry/events.d.ts +11 -6
  76. package/dist/telemetry/events.js +10 -2
  77. package/dist/telemetry/identity.d.ts +0 -1
  78. package/dist/telemetry/identity.js +6 -6
  79. package/dist/telemetry/index.d.ts +12 -0
  80. package/dist/telemetry/index.js +13 -2
  81. package/dist/telemetry/scrubber.d.ts +10 -0
  82. package/dist/telemetry/scrubber.js +20 -0
  83. package/package.json +5 -4
@@ -39,7 +39,7 @@ export interface AnthropicModelChoice {
39
39
  label: string;
40
40
  recommended: boolean;
41
41
  }
42
- export type KtxSetupLlmBackend = 'anthropic' | 'vertex' | 'claude-code';
42
+ export type KtxSetupLlmBackend = 'anthropic' | 'vertex' | 'claude-code' | 'codex';
43
43
  /** @internal */
44
44
  export interface KtxSetupModelPromptAdapter {
45
45
  select(options: {
@@ -76,6 +76,15 @@ export interface KtxSetupModelDeps {
76
76
  ok: false;
77
77
  message: string;
78
78
  }>;
79
+ codexAuthProbe?: (input: {
80
+ projectDir: string;
81
+ model: string;
82
+ }) => Promise<{
83
+ ok: true;
84
+ } | {
85
+ ok: false;
86
+ message: string;
87
+ }>;
79
88
  readGcloudProject?: () => Promise<string | undefined>;
80
89
  listGcloudProjects?: () => Promise<GcloudProjectChoice[]>;
81
90
  spinner?: () => KtxCliSpinner;
@@ -3,6 +3,9 @@ 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';
@@ -37,6 +40,19 @@ const CLAUDE_CODE_MODELS = [
37
40
  { id: 'opus', label: 'Claude Opus', recommended: false },
38
41
  { id: 'haiku', label: 'Claude Haiku', recommended: false },
39
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
+ ];
40
56
  const HIDDEN_ANTHROPIC_MODEL_PATTERNS = [
41
57
  /^claude-sonnet-4$/i,
42
58
  /^claude-opus-4$/i,
@@ -149,7 +165,10 @@ export function isKtxSetupLlmConfigReady(config) {
149
165
  if (resolved.backend === 'vertex') {
150
166
  return typeof resolved.vertex?.location === 'string' && resolved.vertex.location.trim().length > 0;
151
167
  }
152
- return resolved.backend === 'anthropic' || resolved.backend === 'gateway' || resolved.backend === 'claude-code';
168
+ return (resolved.backend === 'anthropic' ||
169
+ resolved.backend === 'gateway' ||
170
+ resolved.backend === 'claude-code' ||
171
+ resolved.backend === 'codex');
153
172
  }
154
173
  function hasUsableConfiguredLlm(config) {
155
174
  return isKtxSetupLlmConfigReady(config.llm);
@@ -162,6 +181,13 @@ function buildProjectLlmConfig(existing, provider, model) {
162
181
  promptCaching: existing.promptCaching,
163
182
  };
164
183
  }
184
+ if (provider.backend === 'codex') {
185
+ return {
186
+ provider: { backend: 'codex' },
187
+ models: { ...existing.models, default: model },
188
+ promptCaching: existing.promptCaching,
189
+ };
190
+ }
165
191
  if (provider.backend === 'vertex') {
166
192
  return {
167
193
  provider: {
@@ -340,6 +366,7 @@ async function chooseBackend(args, io, deps) {
340
366
  message: 'Which LLM provider should KTX use?',
341
367
  options: [
342
368
  { value: 'claude-code', label: 'Claude subscription (Pro/Max)' },
369
+ { value: 'codex', label: 'Codex subscription' },
343
370
  { value: 'anthropic', label: 'Anthropic API key' },
344
371
  { value: 'vertex', label: 'Google Vertex AI for Anthropic Claude' },
345
372
  { value: 'back', label: 'Back' },
@@ -350,7 +377,7 @@ async function chooseBackend(args, io, deps) {
350
377
  }
351
378
  return {
352
379
  status: 'ready',
353
- backend: choice === 'vertex' || choice === 'claude-code' ? choice : 'anthropic',
380
+ backend: choice === 'vertex' || choice === 'claude-code' || choice === 'codex' ? choice : 'anthropic',
354
381
  prompted: true,
355
382
  };
356
383
  }
@@ -671,6 +698,42 @@ async function chooseClaudeCodeModel(args, deps) {
671
698
  }
672
699
  return { status: 'ready', model: choice };
673
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
+ }
674
737
  async function persistLlmConfig(projectDir, provider, model) {
675
738
  const project = await loadKtxProject({ projectDir });
676
739
  const config = {
@@ -783,6 +846,31 @@ export async function runKtxSetupAnthropicModelStep(args, io, deps = {}) {
783
846
  io.stdout.write(`│ LLM ready: yes (${model.model})\n`);
784
847
  return { status: 'ready', projectDir: args.projectDir };
785
848
  }
849
+ 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
+ }
861
+ 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`);
865
+ return { status: 'failed', projectDir: args.projectDir };
866
+ }
867
+ // Prefix the clack gutter so the warning sits inside the setup frame
868
+ // instead of breaking out of it; kept on stderr for scripted runs.
869
+ 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`);
872
+ return { status: 'ready', projectDir: args.projectDir };
873
+ }
786
874
  const credential = await chooseCredentialRef(backendArgs, io, deps);
787
875
  if (credential.status === 'back' && backendChoice.prompted) {
788
876
  attemptArgs = buildInteractiveRetryArgs(args);
@@ -1,6 +1,11 @@
1
1
  import { type KtxSetupPromptOption } from './setup-prompts.js';
2
2
  import type { KtxSetupStatus } from './setup.js';
3
3
  export type KtxSetupReadyAction = 'models' | 'embeddings' | 'databases' | 'sources' | 'runtime' | 'context' | 'agents' | 'exit';
4
+ /**
5
+ * Where a project stands once its `ktx.yaml` exists. Single source of truth for the
6
+ * end-of-setup interception: each state maps to exactly one obvious next action.
7
+ */
8
+ export type KtxSetupCompletion = 'incomplete' | 'needs-context' | 'needs-agents' | 'ready';
4
9
  interface KtxSetupReadyMenuPromptAdapter {
5
10
  select(options: {
6
11
  message: string;
@@ -11,8 +16,17 @@ interface KtxSetupReadyMenuPromptAdapter {
11
16
  export interface KtxSetupReadyMenuDeps {
12
17
  prompts?: KtxSetupReadyMenuPromptAdapter;
13
18
  }
14
- export declare function isKtxPreAgentSetupReady(status: KtxSetupStatus): boolean;
15
- export declare function isKtxSetupReady(status: KtxSetupStatus): boolean;
19
+ export declare function setupHasContextTargets(status: KtxSetupStatus): boolean;
20
+ export declare function classifyKtxSetupCompletion(status: KtxSetupStatus): KtxSetupCompletion;
21
+ /**
22
+ * Shown when a returning user re-runs `ktx setup` on a fully-ready project. Leads with
23
+ * "you're done" (the readiness note is printed by the caller first) and keeps the
24
+ * section editor one explicit step away rather than defaulting into it.
25
+ */
26
+ export declare function runKtxSetupReadyMenu(status: KtxSetupStatus, deps?: KtxSetupReadyMenuDeps): Promise<{
27
+ action: KtxSetupReadyAction;
28
+ }>;
29
+ /** @internal Reached only through {@link runKtxSetupReadyMenu}; exported for unit tests. */
16
30
  export declare function runKtxSetupReadyChangeMenu(status: KtxSetupStatus, deps?: KtxSetupReadyMenuDeps): Promise<{
17
31
  action: KtxSetupReadyAction;
18
32
  }>;
@@ -1,23 +1,55 @@
1
1
  import { createKtxSetupPromptAdapter, } from './setup-prompts.js';
2
- export function isKtxPreAgentSetupReady(status) {
2
+ export function setupHasContextTargets(status) {
3
+ return status.databases.length > 0 || status.sources.length > 0;
4
+ }
5
+ function setupConfigReady(status) {
3
6
  return (status.project.ready &&
4
7
  status.llm.ready &&
5
8
  status.embeddings.ready &&
6
9
  status.databases.every((database) => database.ready) &&
7
10
  status.sources.every((source) => source.ready) &&
8
11
  status.runtime.ready &&
9
- status.context.ready);
12
+ setupHasContextTargets(status));
10
13
  }
11
- export function isKtxSetupReady(status) {
12
- return isKtxPreAgentSetupReady(status) && status.agents.some((agent) => agent.ready);
14
+ export function classifyKtxSetupCompletion(status) {
15
+ if (!setupConfigReady(status)) {
16
+ return 'incomplete';
17
+ }
18
+ if (!status.context.ready) {
19
+ return 'needs-context';
20
+ }
21
+ if (!status.agents.some((agent) => agent.ready)) {
22
+ return 'needs-agents';
23
+ }
24
+ return 'ready';
13
25
  }
14
26
  function createPromptAdapter() {
15
27
  return createKtxSetupPromptAdapter({ selectCancelValue: 'exit' });
16
28
  }
29
+ /**
30
+ * Shown when a returning user re-runs `ktx setup` on a fully-ready project. Leads with
31
+ * "you're done" (the readiness note is printed by the caller first) and keeps the
32
+ * section editor one explicit step away rather than defaulting into it.
33
+ */
34
+ export async function runKtxSetupReadyMenu(status, deps = {}) {
35
+ const prompts = deps.prompts ?? createPromptAdapter();
36
+ const choice = await prompts.select({
37
+ message: 'Anything else?',
38
+ options: [
39
+ { value: 'done', label: "Done — I'll start using ktx" },
40
+ { value: 'change', label: 'Change a setting' },
41
+ ],
42
+ });
43
+ if (choice !== 'change') {
44
+ return { action: 'exit' };
45
+ }
46
+ return runKtxSetupReadyChangeMenu(status, { prompts });
47
+ }
48
+ /** @internal Reached only through {@link runKtxSetupReadyMenu}; exported for unit tests. */
17
49
  export async function runKtxSetupReadyChangeMenu(status, deps = {}) {
18
50
  const prompts = deps.prompts ?? createPromptAdapter();
19
51
  const action = (await prompts.select({
20
- message: `KTX is already set up for ${status.project.name ?? status.project.path}. What would you like to change?`,
52
+ message: 'What would you like to change?',
21
53
  options: [
22
54
  { value: 'models', label: 'Models' },
23
55
  { value: 'embeddings', label: 'Embeddings' },
@@ -19,6 +19,7 @@ import { markKtxSetupStateStepComplete } from './context/project/setup-config.js
19
19
  import { errorMessage, writePrefixedLines } from './clack.js';
20
20
  import { pickNotionRootPages } from './notion-page-picker.js';
21
21
  import { runKtxSourceMapping } from './source-mapping.js';
22
+ import { runConnectionSetupWithRecovery, } from './connection-recovery.js';
22
23
  import { withMultiselectNavigation, withTextInputNavigation } from './prompt-navigation.js';
23
24
  import { runKtxPublicIngest } from './public-ingest.js';
24
25
  import { writeProjectLocalSecretReference } from './setup-secrets.js';
@@ -1434,56 +1435,125 @@ async function validateSource(source, args, deps) {
1434
1435
  }
1435
1436
  return await (deps.validateNotion ?? defaultValidateNotion)(args.connection);
1436
1437
  }
1437
- async function saveValidateAndMaybeBuildSource(input) {
1438
- const connectionId = input.sourceChoice.kind === 'existing'
1438
+ async function createSourceSetupRollback(projectDir) {
1439
+ const project = await loadKtxProject({ projectDir });
1440
+ const previousConfig = project.config;
1441
+ const configPath = project.configPath;
1442
+ return async () => {
1443
+ await writeFile(configPath, serializeKtxProjectConfig(previousConfig), 'utf-8');
1444
+ };
1445
+ }
1446
+ function sourceConnectionId(input) {
1447
+ return input.sourceChoice.kind === 'existing' || input.sourceChoice.kind === 'edited'
1439
1448
  ? input.sourceChoice.connectionId
1440
- : input.sourceChoice.kind === 'edited'
1441
- ? input.sourceChoice.connectionId
1442
- : (input.sourceChoice.args.sourceConnectionId ?? `${input.source}-main`);
1443
- const connection = input.sourceChoice.kind === 'existing'
1444
- ? input.sourceChoice.connection
1445
- : buildConnection(input.source, input.sourceChoice.args);
1446
- const rollback = input.sourceChoice.kind === 'existing'
1447
- ? undefined
1448
- : await writeSourceConnection(input.args.projectDir, connectionId, connection, sourceAdapter(input.source), input.io);
1449
- if (input.sourceChoice.kind === 'existing') {
1450
- await ensureSourceAdapterEnabled(input.args.projectDir, input.source);
1451
- }
1452
- const validation = await validateSource(input.source, { projectDir: input.args.projectDir, connectionId, connection }, input.deps);
1449
+ : (input.sourceChoice.args.sourceConnectionId ?? `${input.source}-main`);
1450
+ }
1451
+ async function validateSourceConnectionAndMapping(input) {
1452
+ const validation = await validateSource(input.source, { projectDir: input.args.projectDir, connectionId: input.connectionId, connection: input.connection }, input.deps);
1453
1453
  if (!validation.ok) {
1454
- await rollback?.();
1455
1454
  input.io.stderr.write(`${validation.message}\n`);
1456
1455
  return { status: 'failed' };
1457
1456
  }
1458
1457
  if (input.source === 'metabase' || input.source === 'looker') {
1459
- input.prompts.log?.(`Validating ${sourceLabel(input.source)} mapping…`);
1460
- const mappingCode = await (input.deps.runMapping ?? defaultRunMapping)(input.args.projectDir, connectionId, createSetupPrefixedIo(input.io));
1458
+ input.prompts.log?.(`Validating ${sourceLabel(input.source)} mapping...`);
1459
+ const mappingCode = await (input.deps.runMapping ?? defaultRunMapping)(input.args.projectDir, input.connectionId, createSetupPrefixedIo(input.io));
1461
1460
  if (mappingCode !== 0) {
1462
- await rollback?.();
1463
1461
  return { status: 'failed' };
1464
1462
  }
1465
1463
  }
1464
+ return { status: 'ok' };
1465
+ }
1466
+ async function saveValidateAndMaybeBuildSource(input) {
1467
+ let latestChoice = input.sourceChoice;
1468
+ let latestConnectionId = sourceConnectionId({ source: input.source, sourceChoice: latestChoice });
1469
+ let latestConnection = latestChoice.kind === 'existing'
1470
+ ? latestChoice.connection
1471
+ : buildConnection(input.source, latestChoice.args);
1472
+ let configureCount = 0;
1473
+ let rollbackAfterConfigure;
1474
+ const outcome = await runConnectionSetupWithRecovery({
1475
+ label: latestConnectionId,
1476
+ interactive: input.args.inputMode !== 'disabled',
1477
+ allowSkip: true,
1478
+ io: input.io,
1479
+ prompts: input.prompts,
1480
+ snapshot: async () => {
1481
+ rollbackAfterConfigure = await createSourceSetupRollback(input.args.projectDir);
1482
+ return rollbackAfterConfigure;
1483
+ },
1484
+ configure: async () => {
1485
+ configureCount += 1;
1486
+ if (latestChoice.kind === 'existing' && configureCount === 1) {
1487
+ await ensureSourceAdapterEnabled(input.args.projectDir, input.source);
1488
+ return 'configured';
1489
+ }
1490
+ const project = await loadKtxProject({ projectDir: input.args.projectDir });
1491
+ const currentConnection = project.config.connections[latestConnectionId] ?? latestConnection;
1492
+ const useAlreadyPromptedArgs = configureCount === 1 && latestChoice.kind !== 'existing';
1493
+ const sourceArgs = useAlreadyPromptedArgs && latestChoice.kind !== 'existing'
1494
+ ? latestChoice.args
1495
+ : input.args.inputMode === 'disabled'
1496
+ ? sourceArgsFromExistingConnection({
1497
+ args: input.args,
1498
+ source: input.source,
1499
+ connectionId: latestConnectionId,
1500
+ connection: currentConnection,
1501
+ })
1502
+ : await promptForInteractiveSource(sourceArgsFromExistingConnection({
1503
+ args: input.args,
1504
+ source: input.source,
1505
+ connectionId: latestConnectionId,
1506
+ connection: currentConnection,
1507
+ }), input.source, input.prompts, input.io, {
1508
+ pickNotionRootPages: input.deps.pickNotionRootPages,
1509
+ discoverMetabaseDatabases: input.deps.discoverMetabaseDatabases,
1510
+ }, latestConnectionId, input.deps.testGitRepo, input.deps.discoverMetabaseDatabases);
1511
+ if (sourceArgs === 'back') {
1512
+ return 'back';
1513
+ }
1514
+ latestConnectionId = sourceArgs.sourceConnectionId ?? latestConnectionId;
1515
+ latestConnection = buildConnection(input.source, sourceArgs);
1516
+ latestChoice =
1517
+ latestChoice.kind === 'new'
1518
+ ? { kind: 'new', args: sourceArgs }
1519
+ : { kind: 'edited', connectionId: latestConnectionId, args: sourceArgs };
1520
+ await writeSourceConnection(input.args.projectDir, latestConnectionId, latestConnection, sourceAdapter(input.source), input.io);
1521
+ return 'configured';
1522
+ },
1523
+ validate: () => validateSourceConnectionAndMapping({
1524
+ args: input.args,
1525
+ source: input.source,
1526
+ connectionId: latestConnectionId,
1527
+ connection: latestConnection,
1528
+ prompts: input.prompts,
1529
+ io: input.io,
1530
+ deps: input.deps,
1531
+ }),
1532
+ });
1533
+ if (outcome !== 'ready') {
1534
+ return { status: outcome };
1535
+ }
1466
1536
  if (input.args.runInitialSourceIngest) {
1467
1537
  const ingestResult = await runInitialSourceIngestWithRecovery({
1468
1538
  args: input.args,
1469
- connectionId,
1539
+ connectionId: latestConnectionId,
1470
1540
  io: input.io,
1471
1541
  prompts: input.prompts,
1472
1542
  deps: input.deps,
1473
1543
  });
1474
1544
  if (ingestResult === 'failed') {
1475
- await rollback?.();
1545
+ await rollbackAfterConfigure?.();
1476
1546
  return { status: 'failed' };
1477
1547
  }
1478
1548
  if (ingestResult === 'back') {
1479
- await rollback?.();
1549
+ await rollbackAfterConfigure?.();
1480
1550
  return { status: 'back' };
1481
1551
  }
1482
1552
  }
1483
1553
  else {
1484
- input.io.stdout.write(`│ Context source ${connectionId} saved. It will be built during the context build step.\n`);
1554
+ input.io.stdout.write(`│ Context source ${latestConnectionId} saved. It will be built during the context build step.\n`);
1485
1555
  }
1486
- return { status: 'ready', connectionId };
1556
+ return { status: 'ready', connectionId: latestConnectionId };
1487
1557
  }
1488
1558
  export async function runKtxSetupSourcesStep(args, io, deps = {}) {
1489
1559
  try {
@@ -1579,8 +1649,13 @@ export async function runKtxSetupSourcesStep(args, io, deps = {}) {
1579
1649
  returnToSourceSelection = true;
1580
1650
  break;
1581
1651
  }
1582
- if (!readyConnectionIds.includes(choiceResult.connectionId)) {
1583
- readyConnectionIds.push(choiceResult.connectionId);
1652
+ if (choiceResult.status === 'skip') {
1653
+ continue;
1654
+ }
1655
+ if (choiceResult.status === 'ready') {
1656
+ if (!readyConnectionIds.includes(choiceResult.connectionId)) {
1657
+ readyConnectionIds.push(choiceResult.connectionId);
1658
+ }
1584
1659
  }
1585
1660
  }
1586
1661
  if (returnToSourceSelection) {
@@ -1640,8 +1715,13 @@ export async function runKtxSetupSourcesStep(args, io, deps = {}) {
1640
1715
  if (choiceResult.status === 'back') {
1641
1716
  continue;
1642
1717
  }
1643
- if (!readyConnectionIds.includes(choiceResult.connectionId)) {
1644
- readyConnectionIds.push(choiceResult.connectionId);
1718
+ if (choiceResult.status === 'skip') {
1719
+ continue;
1720
+ }
1721
+ if (choiceResult.status === 'ready') {
1722
+ if (!readyConnectionIds.includes(choiceResult.connectionId)) {
1723
+ readyConnectionIds.push(choiceResult.connectionId);
1724
+ }
1645
1725
  }
1646
1726
  continue;
1647
1727
  }
package/dist/setup.js CHANGED
@@ -6,7 +6,7 @@ import { ktxLocalStateDbPath } from './context/project/local-state-db.js';
6
6
  import { loadKtxProject } from './context/project/project.js';
7
7
  import { readKtxSetupState } from './context/project/setup-config.js';
8
8
  import { getKtxCliPackageInfo } from './cli-runtime.js';
9
- import { formatSetupNextStepLines } from './next-steps.js';
9
+ import { formatNextStepLines, formatSetupNextStepLines } from './next-steps.js';
10
10
  import { runtimeInstallPolicyFromFlags } from './managed-python-command.js';
11
11
  import { readManagedPythonRuntimeStatus } from './managed-python-runtime.js';
12
12
  import { resolveProjectRuntimeRequirements } from './runtime-requirements.js';
@@ -16,7 +16,7 @@ import { runKtxSetupDatabasesStep, } from './setup-databases.js';
16
16
  import { runKtxSetupEmbeddingsStep } from './setup-embeddings.js';
17
17
  import { isKtxSetupLlmConfigReady, runKtxSetupAnthropicModelStep, } from './setup-models.js';
18
18
  import { runKtxSetupProjectStep } from './setup-project.js';
19
- import { isKtxPreAgentSetupReady, isKtxSetupReady, runKtxSetupReadyChangeMenu, } from './setup-ready-menu.js';
19
+ import { classifyKtxSetupCompletion, runKtxSetupReadyMenu, setupHasContextTargets, } from './setup-ready-menu.js';
20
20
  import { runKtxSetupSourcesStep } from './setup-sources.js';
21
21
  import { runKtxSetupRuntimeStep, } from './setup-runtime.js';
22
22
  import { createKtxSetupPromptAdapter, createKtxSetupUiAdapter, } from './setup-prompts.js';
@@ -47,6 +47,7 @@ async function recordSetupStep(input) {
47
47
  step: input.step,
48
48
  outcome: setupTelemetryOutcome(input.status),
49
49
  durationMs: Math.max(0, performance.now() - input.startedAt),
50
+ ...(input.errorDetail ? { errorDetail: input.errorDetail } : {}),
50
51
  },
51
52
  });
52
53
  }
@@ -286,9 +287,6 @@ function setupStatusReady(status) {
286
287
  status.sources.every((source) => source.ready) &&
287
288
  status.runtime.ready);
288
289
  }
289
- function setupHasContextTargets(status) {
290
- return status.databases.length > 0 || status.sources.length > 0;
291
- }
292
290
  function setupContextReady(status) {
293
291
  return status.context.ready;
294
292
  }
@@ -371,14 +369,22 @@ async function runKtxSetupInner(args, io, deps = {}) {
371
369
  const currentStatus = await readKtxSetupStatus(projectResult.projectDir, { cliVersion: args.cliVersion });
372
370
  let readyAction;
373
371
  if (args.inputMode !== 'disabled' && !agentsRequested) {
374
- if (isKtxSetupReady(currentStatus)) {
375
- readyAction = (await runKtxSetupReadyChangeMenu(currentStatus, deps.readyMenuDeps)).action;
376
- if (readyAction === 'exit')
372
+ const completion = classifyKtxSetupCompletion(currentStatus);
373
+ if (completion === 'ready') {
374
+ setupUi.note(formatNextStepLines().join('\n'), 'ktx is ready', io);
375
+ const choice = (await runKtxSetupReadyMenu(currentStatus, deps.readyMenuDeps)).action;
376
+ if (choice === 'exit')
377
377
  return 0;
378
+ readyAction = choice;
379
+ }
380
+ else if (completion === 'needs-context') {
381
+ // Config is done; skip the re-walk and land straight on the build prompt.
382
+ readyAction = 'context';
378
383
  }
379
- else if (isKtxPreAgentSetupReady(currentStatus)) {
384
+ else if (completion === 'needs-agents') {
380
385
  readyAction = 'agents';
381
386
  }
387
+ // 'incomplete' → readyAction stays undefined → run the full setup walk.
382
388
  }
383
389
  const runOnly = readyAction;
384
390
  const agentOnlySetup = agentsRequested || runOnly === 'agents';
@@ -467,6 +473,9 @@ async function runKtxSetupInner(args, io, deps = {}) {
467
473
  const databaseResult = await databasesRunner({
468
474
  projectDir: projectResult.projectDir,
469
475
  inputMode: args.inputMode,
476
+ yes: args.yes,
477
+ cliVersion: args.cliVersion,
478
+ runtimeInstallPolicy: setupRuntimeInstallPolicy(args),
470
479
  ...(args.databaseDrivers ? { databaseDrivers: args.databaseDrivers } : {}),
471
480
  ...(args.databaseConnectionIds ? { databaseConnectionIds: args.databaseConnectionIds } : {}),
472
481
  ...(args.databaseConnectionId ? { databaseConnectionId: args.databaseConnectionId } : {}),
@@ -565,6 +574,7 @@ async function runKtxSetupInner(args, io, deps = {}) {
565
574
  startedAt: stepStartedAt,
566
575
  io,
567
576
  cliVersion: args.cliVersion,
577
+ ...(stepResult.errorDetail ? { errorDetail: stepResult.errorDetail } : {}),
568
578
  });
569
579
  if (stepResult.status === 'failed') {
570
580
  return 1;
@@ -589,7 +599,9 @@ async function runKtxSetupInner(args, io, deps = {}) {
589
599
  }
590
600
  if (step === 'context' && stepResult.status !== 'ready') {
591
601
  if (shouldRunAgents && args.skipAgents !== true) {
592
- return 0;
602
+ // Context isn't built, so skip agent install — but still reach the
603
+ // completion screen, which states readiness and points at `ktx ingest`.
604
+ break setupLoop;
593
605
  }
594
606
  }
595
607
  forcePromptSteps.delete(step);
@@ -62,6 +62,16 @@ type ClaudeCodeAuthProbe = (input: {
62
62
  ok: false;
63
63
  message: string;
64
64
  }>;
65
+ type CodexAuthProbe = (input: {
66
+ projectDir: string;
67
+ model: string;
68
+ }) => Promise<{
69
+ ok: true;
70
+ } | {
71
+ ok: false;
72
+ message: string;
73
+ fix: string;
74
+ }>;
65
75
  interface LocalStatsIngestPerConnection {
66
76
  connectionId: string;
67
77
  adapter: string;
@@ -135,6 +145,7 @@ export interface BuildProjectStatusOptions {
135
145
  env?: NodeJS.ProcessEnv;
136
146
  queryHistoryReadinessProbe?: HistoricSqlReadinessProbe;
137
147
  claudeCodeAuthProbe?: ClaudeCodeAuthProbe;
148
+ codexAuthProbe?: CodexAuthProbe;
138
149
  configIssues?: KtxConfigIssue[];
139
150
  fast?: boolean;
140
151
  useSpinner?: boolean;
@@ -1,6 +1,8 @@
1
1
  import { stat as statAsync, readdir as readdirAsync } from 'node:fs/promises';
2
2
  import { basename, join } from 'node:path';
3
3
  import { runClaudeCodeAuthProbe } from './context/llm/claude-code-runtime.js';
4
+ import { CODEX_ISOLATION_WARNING, CODEX_ISOLATION_WARNING_FIX, } from './context/llm/codex-isolation.js';
5
+ import { runCodexAuthProbe } from './context/llm/codex-runtime.js';
4
6
  import { ktxLocalStateDbPath } from './context/project/local-state-db.js';
5
7
  import { isQueryHistoryEnabled, queryHistoryDialectForConnection, } from './context/ingest/adapters/historic-sql/connection-dialect.js';
6
8
  import { historicSqlProbeCatalogName, runHistoricSqlReadinessProbe, } from './context/ingest/historic-sql-probes.js';
@@ -49,6 +51,18 @@ async function buildLlmStatus(config, options) {
49
51
  fix: 'Run: ktx setup (choose an LLM provider)',
50
52
  };
51
53
  }
54
+ // The runtime (resolveModelSlots) hard-requires llm.models.default for every
55
+ // non-none backend; without it ingest/scan/memory throw. Report that here so
56
+ // status never marks a project ready that the runtime would refuse to run.
57
+ if (!model || model.trim().length === 0) {
58
+ return {
59
+ backend,
60
+ model,
61
+ status: 'fail',
62
+ detail: `llm.models.default is required for backend "${backend}"`,
63
+ fix: 'Set llm.models.default in ktx.yaml, then rerun `ktx status` (or rerun `ktx setup`).',
64
+ };
65
+ }
52
66
  if (backend === 'anthropic') {
53
67
  const ref = config.provider.anthropic?.api_key;
54
68
  const resolved = resolveRef(ref, env);
@@ -90,7 +104,7 @@ async function buildLlmStatus(config, options) {
90
104
  };
91
105
  }
92
106
  if (backend === 'claude-code') {
93
- const modelName = model ?? 'sonnet';
107
+ const modelName = model;
94
108
  if (options.fast === true) {
95
109
  return {
96
110
  backend,
@@ -117,6 +131,34 @@ async function buildLlmStatus(config, options) {
117
131
  fix: 'Authenticate Claude Code locally with the Claude Code CLI, then rerun `ktx status`.',
118
132
  };
119
133
  }
134
+ if (backend === 'codex') {
135
+ const modelName = model;
136
+ if (options.fast === true) {
137
+ return {
138
+ backend,
139
+ model: modelName,
140
+ status: 'skipped',
141
+ detail: 'auth probe skipped (--fast)',
142
+ };
143
+ }
144
+ const probe = options.codexAuthProbe ?? runCodexAuthProbe;
145
+ const auth = await withSpinner(options.useSpinner === true, 'Probing Codex authentication', () => probe({ projectDir: options.projectDir, model: modelName }));
146
+ if (auth.ok) {
147
+ return {
148
+ backend,
149
+ model: modelName,
150
+ status: 'ok',
151
+ detail: 'local Codex session authenticated',
152
+ };
153
+ }
154
+ return {
155
+ backend,
156
+ model: modelName,
157
+ status: 'fail',
158
+ detail: auth.message,
159
+ fix: auth.fix,
160
+ };
161
+ }
120
162
  return { backend, model, status: 'warn', detail: 'unknown LLM backend' };
121
163
  }
122
164
  function buildEmbeddingsStatus(config, env) {
@@ -378,6 +420,12 @@ function buildWarnings(config, connections, llm, embeddings) {
378
420
  fix: formatClaudeCodePromptCachingFix(),
379
421
  });
380
422
  }
423
+ if (llm.backend === 'codex') {
424
+ warnings.push({
425
+ message: CODEX_ISOLATION_WARNING,
426
+ fix: CODEX_ISOLATION_WARNING_FIX,
427
+ });
428
+ }
381
429
  return warnings;
382
430
  }
383
431
  function buildVerdict(llm, embeddings, connections, queryHistory, warnings) {
@@ -625,6 +673,7 @@ export async function buildProjectStatus(project, options = {}) {
625
673
  projectDir: project.projectDir,
626
674
  env,
627
675
  claudeCodeAuthProbe: options.claudeCodeAuthProbe,
676
+ codexAuthProbe: options.codexAuthProbe,
628
677
  fast: options.fast,
629
678
  useSpinner: options.useSpinner,
630
679
  });