@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
@@ -0,0 +1,304 @@
1
+ import { z } from 'zod';
2
+ import { noopLogger } from '../core/config.js';
3
+ import { isCompletedAgentStep, summarizeCodexExecEvents } from './codex-exec-events.js';
4
+ import { startCodexRuntimeMcpServer, } from './codex-mcp-runtime-server.js';
5
+ import { resolveCodexModel } from './codex-models.js';
6
+ import { buildCodexRuntimeConfig } from './codex-runtime-config.js';
7
+ import { CodexSdkCliRunner } from './codex-sdk-runner.js';
8
+ function modelForRole(modelSlots, role) {
9
+ return resolveCodexModel(modelSlots[role] ?? modelSlots.default);
10
+ }
11
+ function promptWithSystem(system, prompt) {
12
+ return [system, prompt].filter(Boolean).join('\n\n');
13
+ }
14
+ function eventRecord(value) {
15
+ return value && typeof value === 'object' ? value : undefined;
16
+ }
17
+ function isTurnCompleted(event) {
18
+ return eventRecord(event)?.type === 'turn.completed';
19
+ }
20
+ /**
21
+ * Drains the Codex stream once, emitting a step as each agent action completes
22
+ * so callers see live progress and the step budget is enforced mid-run. Every
23
+ * completed agent-action item counts (see {@link isCompletedAgentStep}), so
24
+ * built-in `command_execution` steps decrement the budget the same as
25
+ * `mcp_tool_call`s. A turn that produced no actions still counts as one step,
26
+ * matching the metrics summary and the AI SDK backend.
27
+ */
28
+ async function collectEvents(events, options = {}) {
29
+ const collected = [];
30
+ let completedSteps = 0;
31
+ let sawActionStep = false;
32
+ let budgetExceeded = false;
33
+ let streamError;
34
+ // The SDK yields every stdout event, then throws on a non-zero codex exec
35
+ // exit. Catch that throw so the events already collected (which carry the
36
+ // real `turn.failed`/`error` reason) survive for the summary; the masked
37
+ // exit message is kept only as a fallback when no error event was emitted.
38
+ try {
39
+ for await (const event of events) {
40
+ collected.push(event);
41
+ const isActionStep = isCompletedAgentStep(event);
42
+ if (isActionStep) {
43
+ sawActionStep = true;
44
+ }
45
+ else if (sawActionStep || !isTurnCompleted(event)) {
46
+ // Only fall back to counting a bare turn as a step when the turn produced
47
+ // no agent actions; a completed turn is terminal, so it never aborts.
48
+ continue;
49
+ }
50
+ completedSteps += 1;
51
+ await options.onStep?.(completedSteps);
52
+ if (isActionStep && options.stepBudget !== undefined && completedSteps >= options.stepBudget) {
53
+ budgetExceeded = true;
54
+ options.abortController?.abort();
55
+ break;
56
+ }
57
+ }
58
+ }
59
+ catch (error) {
60
+ streamError = error instanceof Error ? error : new Error(String(error));
61
+ }
62
+ return { events: collected, budgetExceeded, ...(streamError ? { streamError } : {}) };
63
+ }
64
+ function metrics(summary, startedAt) {
65
+ return { totalMs: Date.now() - startedAt, usage: summary.usage };
66
+ }
67
+ function summaryError(summary, streamError) {
68
+ // A `turn.failed`/`error` event carries the real reason; prefer it over the
69
+ // SDK's generic non-zero-exit throw. Fall back to the stream error only when
70
+ // no event explained the failure (e.g. spawn failure or auth before a turn).
71
+ if (summary.error) {
72
+ return summary.error;
73
+ }
74
+ if (summary.toolFailures.length > 0) {
75
+ return new Error(`Codex runtime tool call failed: ${summary.toolFailures.join('; ')}`);
76
+ }
77
+ return streamError;
78
+ }
79
+ function assertSuccessfulText(summary, streamError) {
80
+ const error = summaryError(summary, streamError);
81
+ if (error) {
82
+ throw error;
83
+ }
84
+ if (!summary.finalText.trim()) {
85
+ throw new Error('Codex completed without an agent message');
86
+ }
87
+ return summary.finalText;
88
+ }
89
+ function parseStructuredOutput(schema, text) {
90
+ try {
91
+ return schema.parse(JSON.parse(text));
92
+ }
93
+ catch (error) {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ throw new Error(`Codex structured output failed validation: ${message}`);
96
+ }
97
+ }
98
+ async function mcpForTools(input) {
99
+ if (!input.toolSet || Object.keys(input.toolSet).length === 0) {
100
+ return undefined;
101
+ }
102
+ return (input.startMcpServer ?? startCodexRuntimeMcpServer)({
103
+ projectDir: input.projectDir,
104
+ toolSet: input.toolSet,
105
+ });
106
+ }
107
+ function runtimeToolNames(toolSet) {
108
+ return Object.values(toolSet ?? {}).map((descriptor) => descriptor.name);
109
+ }
110
+ export class CodexKtxLlmRuntime {
111
+ deps;
112
+ runner;
113
+ logger;
114
+ constructor(deps) {
115
+ this.deps = deps;
116
+ this.runner = deps.runner ?? new CodexSdkCliRunner();
117
+ this.logger = deps.logger ?? noopLogger;
118
+ }
119
+ async generateText(input) {
120
+ const startedAt = Date.now();
121
+ const model = modelForRole(this.deps.modelSlots, input.role);
122
+ const mcp = await mcpForTools({
123
+ projectDir: this.deps.projectDir,
124
+ toolSet: input.tools,
125
+ startMcpServer: this.deps.startMcpServer,
126
+ });
127
+ try {
128
+ const config = buildCodexRuntimeConfig({
129
+ model,
130
+ ...(mcp
131
+ ? {
132
+ mcp: {
133
+ url: mcp.url,
134
+ bearerTokenEnvVar: mcp.bearerTokenEnvVar,
135
+ bearerToken: mcp.bearerToken,
136
+ toolNames: runtimeToolNames(input.tools),
137
+ },
138
+ }
139
+ : {}),
140
+ });
141
+ const collected = await collectEvents(await this.runner.runStreamed({
142
+ projectDir: this.deps.projectDir,
143
+ model,
144
+ prompt: promptWithSystem(input.system, input.prompt),
145
+ configOverrides: config.configOverrides,
146
+ env: config.env,
147
+ }));
148
+ const summary = summarizeCodexExecEvents(collected.events, { startedAt });
149
+ input.onMetrics?.(metrics(summary, startedAt));
150
+ return assertSuccessfulText(summary, collected.streamError);
151
+ }
152
+ finally {
153
+ await mcp?.close();
154
+ }
155
+ }
156
+ async generateObject(input) {
157
+ const startedAt = Date.now();
158
+ const model = modelForRole(this.deps.modelSlots, input.role);
159
+ const mcp = await mcpForTools({
160
+ projectDir: this.deps.projectDir,
161
+ toolSet: input.tools,
162
+ startMcpServer: this.deps.startMcpServer,
163
+ });
164
+ try {
165
+ const config = buildCodexRuntimeConfig({
166
+ model,
167
+ ...(mcp
168
+ ? {
169
+ mcp: {
170
+ url: mcp.url,
171
+ bearerTokenEnvVar: mcp.bearerTokenEnvVar,
172
+ bearerToken: mcp.bearerToken,
173
+ toolNames: runtimeToolNames(input.tools),
174
+ },
175
+ }
176
+ : {}),
177
+ });
178
+ const collected = await collectEvents(await this.runner.runStreamed({
179
+ projectDir: this.deps.projectDir,
180
+ model,
181
+ prompt: promptWithSystem(input.system, input.prompt),
182
+ configOverrides: config.configOverrides,
183
+ env: config.env,
184
+ outputSchema: z.toJSONSchema(input.schema, { target: 'draft-7' }),
185
+ }));
186
+ const summary = summarizeCodexExecEvents(collected.events, { startedAt });
187
+ input.onMetrics?.(metrics(summary, startedAt));
188
+ return parseStructuredOutput(input.schema, assertSuccessfulText(summary, collected.streamError));
189
+ }
190
+ finally {
191
+ await mcp?.close();
192
+ }
193
+ }
194
+ async runAgentLoop(params) {
195
+ const startedAt = Date.now();
196
+ const model = modelForRole(this.deps.modelSlots, params.modelRole);
197
+ let mcp;
198
+ try {
199
+ mcp = await mcpForTools({
200
+ projectDir: this.deps.projectDir,
201
+ toolSet: params.toolSet,
202
+ startMcpServer: this.deps.startMcpServer,
203
+ });
204
+ const config = buildCodexRuntimeConfig({
205
+ model,
206
+ ...(mcp
207
+ ? {
208
+ mcp: {
209
+ url: mcp.url,
210
+ bearerTokenEnvVar: mcp.bearerTokenEnvVar,
211
+ bearerToken: mcp.bearerToken,
212
+ toolNames: runtimeToolNames(params.toolSet),
213
+ },
214
+ }
215
+ : {}),
216
+ });
217
+ const abortController = new AbortController();
218
+ const onStep = async (stepIndex) => {
219
+ try {
220
+ await params.onStepFinish?.({ stepIndex, stepBudget: params.stepBudget });
221
+ }
222
+ catch (error) {
223
+ this.logger.warn(`[codex-runner] onStepFinish callback threw; ignoring: ${error instanceof Error ? error.message : String(error)}`);
224
+ }
225
+ };
226
+ const collected = await collectEvents(await this.runner.runStreamed({
227
+ projectDir: this.deps.projectDir,
228
+ model,
229
+ prompt: promptWithSystem(params.systemPrompt, params.userPrompt),
230
+ configOverrides: config.configOverrides,
231
+ env: config.env,
232
+ signal: abortController.signal,
233
+ }), { stepBudget: params.stepBudget, abortController, onStep });
234
+ const summary = summarizeCodexExecEvents(collected.events, { startedAt });
235
+ const error = summaryError(summary, collected.streamError);
236
+ const stopReason = collected.budgetExceeded ? 'budget' : error ? 'error' : summary.stopReason;
237
+ return {
238
+ stopReason,
239
+ ...(stopReason === 'error' && error ? { error } : {}),
240
+ metrics: {
241
+ totalMs: Date.now() - startedAt,
242
+ usage: summary.usage,
243
+ stepCount: summary.stepCount,
244
+ stepBoundariesMs: summary.stepBoundariesMs,
245
+ },
246
+ };
247
+ }
248
+ catch (error) {
249
+ const err = error instanceof Error ? error : new Error(String(error));
250
+ return {
251
+ stopReason: 'error',
252
+ error: err,
253
+ metrics: { totalMs: Date.now() - startedAt, usage: {}, stepCount: 0, stepBoundariesMs: [] },
254
+ };
255
+ }
256
+ finally {
257
+ await mcp?.close();
258
+ }
259
+ }
260
+ }
261
+ // A rejected model is not an auth failure: Codex authenticated, connected, and
262
+ // the API refused the model id. These markers come from the API error envelope
263
+ // (e.g. "model is not supported", "invalid_request_error").
264
+ const MODEL_UNAVAILABLE_MARKERS = /\bnot supported\b|\bnot available\b|\bdoes not exist\b|invalid_request_error|\bunknown model\b|\bunsupported model\b/i;
265
+ function describeCodexProbeFailure(model, message) {
266
+ if (MODEL_UNAVAILABLE_MARKERS.test(message)) {
267
+ const fix = `Run \`codex\` to see the models your account supports, then set llm.models.default in ktx.yaml (or rerun \`ktx setup\`).`;
268
+ return {
269
+ message: `Codex is authenticated, but the configured model "${model}" is not available for this Codex account. ${fix} Details: ${message}`,
270
+ fix,
271
+ };
272
+ }
273
+ const fix = `Authenticate Codex locally with the Codex CLI, verify the Codex CLI is installed, then rerun setup or \`ktx status\`.`;
274
+ return {
275
+ message: `Codex authentication is not usable. ${fix} Details: ${message}`,
276
+ fix,
277
+ };
278
+ }
279
+ export async function runCodexAuthProbe(input) {
280
+ let model;
281
+ try {
282
+ model = resolveCodexModel(input.model);
283
+ }
284
+ catch (error) {
285
+ return {
286
+ ok: false,
287
+ message: error instanceof Error ? error.message : String(error),
288
+ fix: 'Set llm.models.default in ktx.yaml to a supported codex model (codex, default, or a gpt-* / codex-* id), or rerun `ktx setup`.',
289
+ };
290
+ }
291
+ const runtime = new CodexKtxLlmRuntime({
292
+ projectDir: input.projectDir,
293
+ modelSlots: { default: model },
294
+ ...(input.runner ? { runner: input.runner } : {}),
295
+ });
296
+ try {
297
+ await runtime.generateText({ role: 'default', prompt: 'Reply with exactly: ok' });
298
+ return { ok: true };
299
+ }
300
+ catch (error) {
301
+ const message = error instanceof Error ? error.message : String(error);
302
+ return { ok: false, ...describeCodexProbeFailure(model, message) };
303
+ }
304
+ }
@@ -0,0 +1,21 @@
1
+ export interface CodexSdkRunnerInput {
2
+ projectDir: string;
3
+ model: string;
4
+ prompt: string;
5
+ configOverrides?: Record<string, unknown>;
6
+ env?: Record<string, string>;
7
+ outputSchema?: Record<string, unknown>;
8
+ signal?: AbortSignal;
9
+ }
10
+ export interface CodexSdkRunner {
11
+ runStreamed(input: CodexSdkRunnerInput): Promise<AsyncIterable<unknown>>;
12
+ }
13
+ export interface CodexSdkCliRunnerOptions {
14
+ envBase?: NodeJS.ProcessEnv;
15
+ codexPathOverride?: string;
16
+ }
17
+ export declare class CodexSdkCliRunner implements CodexSdkRunner {
18
+ private readonly options;
19
+ constructor(options?: CodexSdkCliRunnerOptions);
20
+ runStreamed(input: CodexSdkRunnerInput): Promise<AsyncIterable<unknown>>;
21
+ }
@@ -0,0 +1,63 @@
1
+ import { Codex } from '@openai/codex-sdk';
2
+ const CODEX_ENV_ALLOWLIST = new Set([
3
+ 'HOME',
4
+ 'USERPROFILE',
5
+ 'APPDATA',
6
+ 'LOCALAPPDATA',
7
+ 'XDG_CONFIG_HOME',
8
+ 'CODEX_HOME',
9
+ 'CODEX_API_KEY',
10
+ 'OPENAI_API_KEY',
11
+ 'PATH',
12
+ 'Path',
13
+ 'SYSTEMROOT',
14
+ 'COMSPEC',
15
+ 'TMPDIR',
16
+ 'TMP',
17
+ 'TEMP',
18
+ 'SSL_CERT_FILE',
19
+ 'SSL_CERT_DIR',
20
+ 'NODE_EXTRA_CA_CERTS',
21
+ 'HTTPS_PROXY',
22
+ 'HTTP_PROXY',
23
+ 'ALL_PROXY',
24
+ 'NO_PROXY',
25
+ ]);
26
+ function buildCodexSdkEnv(baseEnv, overrides) {
27
+ const env = {};
28
+ for (const key of CODEX_ENV_ALLOWLIST) {
29
+ const value = baseEnv[key];
30
+ if (typeof value === 'string') {
31
+ env[key] = value;
32
+ }
33
+ }
34
+ return { ...env, ...(overrides ?? {}) };
35
+ }
36
+ export class CodexSdkCliRunner {
37
+ options;
38
+ constructor(options = {}) {
39
+ this.options = options;
40
+ }
41
+ async runStreamed(input) {
42
+ const CodexClass = Codex;
43
+ const codex = new CodexClass({
44
+ ...(input.configOverrides ? { config: input.configOverrides } : {}),
45
+ env: buildCodexSdkEnv(this.options.envBase ?? process.env, input.env),
46
+ ...(this.options.codexPathOverride ? { codexPathOverride: this.options.codexPathOverride } : {}),
47
+ });
48
+ const thread = codex.startThread({
49
+ workingDirectory: input.projectDir,
50
+ skipGitRepoCheck: true,
51
+ model: input.model,
52
+ sandboxMode: 'read-only',
53
+ webSearchMode: 'disabled',
54
+ approvalPolicy: 'never',
55
+ });
56
+ const turnOptions = {
57
+ ...(input.outputSchema ? { outputSchema: input.outputSchema } : {}),
58
+ ...(input.signal ? { signal: input.signal } : {}),
59
+ };
60
+ const streamed = await thread.runStreamed(input.prompt, Object.keys(turnOptions).length > 0 ? turnOptions : undefined);
61
+ return streamed.events;
62
+ }
63
+ }
@@ -3,6 +3,7 @@ import { createKtxLlmProvider } from '../../llm/model-provider.js';
3
3
  import type { KtxEmbeddingConfig, KtxEmbeddingProvider, KtxLlmConfig, KtxLlmProvider } from '../../llm/types.js';
4
4
  import type { KtxProjectEmbeddingConfig, KtxProjectLlmConfig } from '../project/config.js';
5
5
  import { ClaudeCodeKtxLlmRuntime } from './claude-code-runtime.js';
6
+ import { CodexKtxLlmRuntime } from './codex-runtime.js';
6
7
  import type { KtxLlmRuntimePort } from './runtime-port.js';
7
8
  interface LocalConfigDeps {
8
9
  env?: NodeJS.ProcessEnv;
@@ -10,6 +11,7 @@ interface LocalConfigDeps {
10
11
  createKtxLlmProvider?: typeof createKtxLlmProvider;
11
12
  createKtxEmbeddingProvider?: typeof createKtxEmbeddingProvider;
12
13
  createClaudeCodeRuntime?: (deps: ConstructorParameters<typeof ClaudeCodeKtxLlmRuntime>[0]) => KtxLlmRuntimePort;
14
+ createCodexRuntime?: (deps: ConstructorParameters<typeof CodexKtxLlmRuntime>[0]) => KtxLlmRuntimePort;
13
15
  createAiSdkRuntime?: (deps: {
14
16
  llmProvider: KtxLlmProvider;
15
17
  }) => KtxLlmRuntimePort;
@@ -3,6 +3,7 @@ import { createKtxLlmProvider } from '../../llm/model-provider.js';
3
3
  import { resolveKtxConfigReference } from '../core/config-reference.js';
4
4
  import { AiSdkKtxLlmRuntime } from './ai-sdk-runtime.js';
5
5
  import { ClaudeCodeKtxLlmRuntime } from './claude-code-runtime.js';
6
+ import { CodexKtxLlmRuntime } from './codex-runtime.js';
6
7
  function resolveOptional(value, env) {
7
8
  return resolveKtxConfigReference(value, env) || undefined;
8
9
  }
@@ -70,7 +71,7 @@ export function resolveLocalKtxLlmConfig(config, env) {
70
71
  /** @internal */
71
72
  export function createLocalKtxLlmProviderFromConfig(config, deps = {}) {
72
73
  const resolved = resolveLocalKtxLlmConfig(config, deps.env ?? process.env);
73
- if (!resolved || resolved.backend === 'claude-code') {
74
+ if (!resolved || resolved.backend === 'claude-code' || resolved.backend === 'codex') {
74
75
  return null;
75
76
  }
76
77
  return (deps.createKtxLlmProvider ?? createKtxLlmProvider)(resolved);
@@ -91,6 +92,16 @@ export function createLocalKtxLlmRuntimeFromConfig(config, deps = {}) {
91
92
  env: deps.env,
92
93
  });
93
94
  }
95
+ if (resolved.backend === 'codex') {
96
+ const projectDir = deps.projectDir;
97
+ if (!projectDir) {
98
+ throw new Error('projectDir is required when creating the codex LLM runtime');
99
+ }
100
+ return (deps.createCodexRuntime ?? ((runtimeDeps) => new CodexKtxLlmRuntime(runtimeDeps)))({
101
+ projectDir,
102
+ modelSlots: resolved.modelSlots,
103
+ });
104
+ }
94
105
  const llmProvider = (deps.createKtxLlmProvider ?? createKtxLlmProvider)(resolved);
95
106
  return (deps.createAiSdkRuntime ?? ((runtimeDeps) => new AiSdkKtxLlmRuntime(runtimeDeps)))({ llmProvider });
96
107
  }
@@ -6,6 +6,7 @@ declare const llmSchema: z.ZodObject<{
6
6
  vertex: "vertex";
7
7
  gateway: "gateway";
8
8
  "claude-code": "claude-code";
9
+ codex: "codex";
9
10
  none: "none";
10
11
  }>>;
11
12
  vertex: z.ZodOptional<z.ZodObject<{
@@ -327,6 +328,7 @@ declare const ktxProjectConfigSchema: z.ZodObject<{
327
328
  vertex: "vertex";
328
329
  gateway: "gateway";
329
330
  "claude-code": "claude-code";
331
+ codex: "codex";
330
332
  none: "none";
331
333
  }>>;
332
334
  vertex: z.ZodOptional<z.ZodObject<{
@@ -2,7 +2,7 @@ import { KTX_MODEL_ROLES } from '../../llm/types.js';
2
2
  import YAML from 'yaml';
3
3
  import * as z from 'zod';
4
4
  import { connectionConfigSchema } from './driver-schemas.js';
5
- const KTX_LLM_BACKENDS = ['none', 'anthropic', 'vertex', 'gateway', 'claude-code'];
5
+ const KTX_LLM_BACKENDS = ['none', 'anthropic', 'vertex', 'gateway', 'claude-code', 'codex'];
6
6
  const KTX_EMBEDDING_BACKENDS = ['none', 'openai', 'sentence-transformers'];
7
7
  const KTX_PROMPT_CACHE_TTLS = ['5m', '1h'];
8
8
  const KTX_ENRICHMENT_MODES = ['none', 'deterministic', 'llm'];
@@ -32,7 +32,7 @@ const llmProviderSchema = z
32
32
  backend: z
33
33
  .enum(KTX_LLM_BACKENDS)
34
34
  .default('none')
35
- .describe('LLM provider backend. "none" disables LLM features; "anthropic" / "vertex" / "gateway" require the matching nested credentials block; "claude-code" uses the local Claude Code session.'),
35
+ .describe('LLM provider backend. "none" disables LLM features; "anthropic" / "vertex" / "gateway" require the matching nested credentials block; "claude-code" uses the local Claude Code session; "codex" uses the local Codex session.'),
36
36
  vertex: vertexProviderSchema.optional().describe('Vertex AI credentials, used when backend is "vertex".'),
37
37
  anthropic: apiCredentialsSchema.optional().describe('Anthropic API credentials, used when backend is "anthropic".'),
38
38
  gateway: apiCredentialsSchema.optional().describe('AI Gateway credentials, used when backend is "gateway".'),
@@ -59,6 +59,13 @@ function optionalString(raw, field) {
59
59
  }
60
60
  throw new Error(`sql analysis response has invalid optional string field ${field}`);
61
61
  }
62
+ function optionalNullableStringField(raw, field) {
63
+ const value = raw[field];
64
+ if (value === null || value === undefined || typeof value === 'string') {
65
+ return value ?? null;
66
+ }
67
+ throw new Error(`sql analysis response has invalid optional nullable string field ${field}`);
68
+ }
62
69
  function requiredStringArray(raw, field) {
63
70
  const value = raw[field];
64
71
  if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
@@ -136,10 +143,32 @@ function mapColumnsByClause(raw) {
136
143
  }
137
144
  return result;
138
145
  }
146
+ function requiredTableRef(raw, field) {
147
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
148
+ throw new Error(`sql analysis response contains invalid table ref in ${field}`);
149
+ }
150
+ const record = raw;
151
+ const name = record.name;
152
+ if (typeof name !== 'string' || name.length === 0) {
153
+ throw new Error(`sql analysis response table ref in ${field} is missing name`);
154
+ }
155
+ return {
156
+ catalog: optionalNullableStringField(record, 'catalog'),
157
+ db: optionalNullableStringField(record, 'db'),
158
+ name,
159
+ };
160
+ }
161
+ function requiredTableRefArray(raw, field) {
162
+ const value = raw[field];
163
+ if (!Array.isArray(value)) {
164
+ throw new Error(`sql analysis response is missing table-ref[] field ${field}`);
165
+ }
166
+ return value.map((item, index) => requiredTableRef(item, `${field}.${index}`));
167
+ }
139
168
  function mapBatchResult(raw) {
140
169
  const error = optionalString(raw, 'error');
141
170
  return {
142
- tablesTouched: requiredStringArray(raw, 'tables_touched'),
171
+ tablesTouched: requiredTableRefArray(raw, 'tables_touched'),
143
172
  columnsByClause: mapColumnsByClause(raw),
144
173
  ...(error !== undefined ? { error } : {}),
145
174
  };
@@ -170,10 +199,11 @@ export function createHttpSqlAnalysisPort(options) {
170
199
  });
171
200
  return mapResult(raw);
172
201
  },
173
- async analyzeBatch(items, dialect) {
202
+ async analyzeBatch(items, dialect, options) {
174
203
  const raw = await requestJson('/sql/analyze-batch', {
175
204
  dialect,
176
205
  items,
206
+ ...(options?.catalog ? { catalog: options.catalog } : {}),
177
207
  });
178
208
  return mapBatchResponse(raw);
179
209
  },
@@ -1,3 +1,4 @@
1
+ import type { KtxTableRef } from '../scan/types.js';
1
2
  export type SqlAnalysisDialect = 'bigquery' | 'snowflake' | 'postgres' | 'redshift' | 'mysql' | 'sqlite' | 'tsql' | 'clickhouse' | (string & {});
2
3
  export type SqlAnalysisLiteralSlotType = 'string' | 'number' | 'timestamp' | 'date' | 'boolean' | 'null' | 'unknown';
3
4
  export interface SqlAnalysisLiteralSlot {
@@ -17,8 +18,17 @@ export interface SqlAnalysisBatchItem {
17
18
  id: string;
18
19
  sql: string;
19
20
  }
21
+ interface SqlAnalysisCatalogTable extends KtxTableRef {
22
+ columns?: string[];
23
+ }
24
+ interface SqlAnalysisCatalog {
25
+ tables: SqlAnalysisCatalogTable[];
26
+ }
27
+ export interface SqlAnalysisBatchOptions {
28
+ catalog?: SqlAnalysisCatalog;
29
+ }
20
30
  export interface SqlAnalysisBatchResult {
21
- tablesTouched: string[];
31
+ tablesTouched: KtxTableRef[];
22
32
  columnsByClause: Partial<Record<SqlAnalysisClause, string[]>>;
23
33
  error?: string | null;
24
34
  }
@@ -28,7 +38,7 @@ export interface SqlReadOnlyValidationResult {
28
38
  }
29
39
  export interface SqlAnalysisPort {
30
40
  analyzeForFingerprint(sql: string, dialect: SqlAnalysisDialect): Promise<SqlAnalysisFingerprintResult>;
31
- analyzeBatch(items: SqlAnalysisBatchItem[], dialect: SqlAnalysisDialect): Promise<Map<string, SqlAnalysisBatchResult>>;
41
+ analyzeBatch(items: SqlAnalysisBatchItem[], dialect: SqlAnalysisDialect, options?: SqlAnalysisBatchOptions): Promise<Map<string, SqlAnalysisBatchResult>>;
32
42
  validateReadOnly(sql: string, dialect: SqlAnalysisDialect): Promise<SqlReadOnlyValidationResult>;
33
43
  }
34
44
  export {};
@@ -8,8 +8,8 @@ declare const contextCandidateMarkInputSchema: z.ZodObject<{
8
8
  conflict: "conflict";
9
9
  merged: "merged";
10
10
  pending: "pending";
11
- promoted: "promoted";
12
11
  rejected: "rejected";
12
+ promoted: "promoted";
13
13
  }>;
14
14
  rejectionReason: z.ZodDefault<z.ZodNullable<z.ZodString>>;
15
15
  }, z.core.$strip>;
@@ -31,8 +31,8 @@ export declare class ContextCandidateMarkTool extends BaseTool<typeof contextCan
31
31
  conflict: "conflict";
32
32
  merged: "merged";
33
33
  pending: "pending";
34
- promoted: "promoted";
35
34
  rejected: "rejected";
35
+ promoted: "promoted";
36
36
  }>;
37
37
  rejectionReason: z.ZodDefault<z.ZodNullable<z.ZodString>>;
38
38
  }, z.core.$strip>;
@@ -1,5 +1,5 @@
1
- import { publicDatabaseIngestMessage, publicQueryHistoryMessage } from './public-ingest-copy.js';
2
- import { buildPublicIngestPlan, executePublicIngestTarget } from './public-ingest.js';
1
+ import { buildPublicIngestPlan, executePublicIngestTarget, publicProgressMessage } from './public-ingest.js';
2
+ import { createAggregateProgressPort } from './progress-port-adapter.js';
3
3
  import { formatDuration } from './demo-metrics.js';
4
4
  import { profileMark } from './startup-profile.js';
5
5
  profileMark('module:context-build-view');
@@ -618,38 +618,10 @@ export function initViewState(targets) {
618
618
  totalElapsedMs: 0,
619
619
  };
620
620
  }
621
- function publicProgressMessage(message, target) {
622
- let current = message;
623
- if (target.operation === 'database-ingest') {
624
- current = publicDatabaseIngestMessage(current);
625
- }
626
- if (target.steps.includes('query-history')) {
627
- current = publicQueryHistoryMessage(current, target.connectionId);
628
- }
629
- return current;
630
- }
631
621
  function formatProgressDetail(update, target) {
632
622
  const percent = Math.max(0, Math.min(100, Math.round(update.percent)));
633
623
  return `[${percent}%] ${publicProgressMessage(update.message, target)}`;
634
624
  }
635
- function createContextBuildProgressPort(onProgress, state = { progress: 0 }, start = 0, weight = 1) {
636
- return {
637
- async update(value, message, options) {
638
- const absoluteValue = start + Math.max(0, Math.min(1, value)) * weight;
639
- state.progress = Math.max(state.progress, Math.min(1, absoluteValue));
640
- if (!message)
641
- return;
642
- onProgress({
643
- percent: Math.max(0, Math.min(100, Math.round(state.progress * 100))),
644
- message,
645
- ...(options?.transient !== undefined ? { transient: options.transient } : {}),
646
- });
647
- },
648
- startPhase(phaseWeight) {
649
- return createContextBuildProgressPort(onProgress, state, state.progress, weight * phaseWeight);
650
- },
651
- };
652
- }
653
625
  export async function runContextBuild(project, args, io, deps = {}) {
654
626
  const plan = buildPublicIngestPlan(project, {
655
627
  projectDir: args.projectDir,
@@ -802,7 +774,7 @@ export async function runContextBuild(project, args, io, deps = {}) {
802
774
  hasPendingProgressPublish = !publishSourceProgress(false);
803
775
  };
804
776
  const progressDeps = {
805
- scanProgress: createContextBuildProgressPort(updateSchemaPhase),
777
+ scanProgress: createAggregateProgressPort(updateSchemaPhase),
806
778
  ingestProgress: updateIngestPhase,
807
779
  runtimeIo: io,
808
780
  onPhaseStart,
@@ -811,7 +783,7 @@ export async function runContextBuild(project, args, io, deps = {}) {
811
783
  let result = null;
812
784
  let thrownError = null;
813
785
  try {
814
- result = await execTarget(targetState.target, runArgs, capture.io, progressDeps);
786
+ result = await execTarget(targetState.target, runArgs, capture.io, progressDeps, project);
815
787
  }
816
788
  catch (error) {
817
789
  thrownError = error;
@@ -0,0 +1,11 @@
1
+ import type { KtxCliIo } from '../cli-runtime.js';
2
+ export interface BufferedCommandIo extends KtxCliIo {
3
+ stdoutText(): string;
4
+ stderrText(): string;
5
+ }
6
+ /**
7
+ * Captures stdout/stderr from a command (e.g. `runKtxConnection`) into buffers
8
+ * instead of the terminal. Callers decide whether to flush the captured text to
9
+ * the user or discard it.
10
+ */
11
+ export declare function createBufferedCommandIo(): BufferedCommandIo;