@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
package/dist/clack.d.ts CHANGED
@@ -1,4 +1,10 @@
1
1
  import type { KtxCliIo } from './cli-runtime.js';
2
+ export interface CliStyleEnv {
3
+ NO_COLOR?: string;
4
+ TERM?: string;
5
+ }
6
+ export declare function dim(text: string, env?: CliStyleEnv): string;
7
+ export declare function cyan(text: string, env?: CliStyleEnv): string;
2
8
  export interface RailBufferedSource {
3
9
  stdoutText(): string;
4
10
  stderrText(): string;
package/dist/clack.js CHANGED
@@ -1,5 +1,20 @@
1
1
  import { cancel, confirm, isCancel, log, spinner } from '@clack/prompts';
2
2
  const ESC = String.fromCharCode(0x1b);
3
+ function ansiEnabled(env = process.env) {
4
+ return !env.NO_COLOR && env.TERM !== 'dumb';
5
+ }
6
+ function ansiColor(text, open, close, env) {
7
+ if (!ansiEnabled(env)) {
8
+ return text;
9
+ }
10
+ return `${ESC}[${open}m${text}${ESC}[${close}m`;
11
+ }
12
+ export function dim(text, env) {
13
+ return ansiColor(text, 2, 22, env);
14
+ }
15
+ export function cyan(text, env) {
16
+ return ansiColor(text, 36, 39, env);
17
+ }
3
18
  export function errorMessage(error) {
4
19
  return error instanceof Error ? error.message : String(error);
5
20
  }
@@ -24,10 +39,10 @@ export function createClackSpinner() {
24
39
  return spinner();
25
40
  }
26
41
  function magenta(text) {
27
- return `${ESC}[35m${text}${ESC}[39m`;
42
+ return ansiColor(text, 35, 39);
28
43
  }
29
44
  function red(text) {
30
- return `${ESC}[31m${text}${ESC}[39m`;
45
+ return ansiColor(text, 31, 39);
31
46
  }
32
47
  export function createStaticCliSpinner(io) {
33
48
  return {
@@ -1,5 +1,6 @@
1
1
  import { Command, type CommandUnknownOpts } from '@commander-js/extra-typings';
2
2
  import type { KtxCliDeps, KtxCliIo, KtxCliPackageInfo } from './cli-runtime.js';
3
+ import { type PrepareUpdateCheckNoticeOptions } from './update-check/update-check.js';
3
4
  export interface KtxCliCommandContext {
4
5
  io: KtxCliIo;
5
6
  deps: KtxCliDeps;
@@ -23,6 +24,7 @@ interface KtxCommanderProgramOptions {
23
24
  force: boolean;
24
25
  }, io: KtxCliIo) => Promise<number>;
25
26
  }
27
+ type KtxCliUpdateCheckOptions = Pick<PrepareUpdateCheckNoticeOptions, 'env' | 'fetchDistTags' | 'homeDir' | 'now'>;
26
28
  export interface BuildKtxProgramOptions {
27
29
  io: KtxCliIo;
28
30
  deps: KtxCliDeps;
@@ -34,6 +36,7 @@ export interface BuildKtxProgramOptions {
34
36
  setExitCode?: (code: number) => void;
35
37
  argv?: string[];
36
38
  setTelemetryModule?: (telemetry: typeof import('./telemetry/index.js')) => void;
39
+ updateCheck?: KtxCliUpdateCheckOptions;
37
40
  }
38
41
  export interface CommandWithGlobalOptions {
39
42
  opts: () => object;
@@ -14,6 +14,7 @@ import { registerAdminCommands } from './admin.js';
14
14
  import { renderMissingProjectMessage } from './doctor.js';
15
15
  import { findNearestKtxProjectDir, resolveKtxProjectDir } from './project-resolver.js';
16
16
  import { profileMark, profileSpan } from './startup-profile.js';
17
+ import { prepareUpdateCheckNotice } from './update-check/update-check.js';
17
18
  profileMark('module:cli-program');
18
19
  const PROJECT_AWARE_ROOT_COMMANDS = new Set(['setup', 'connection', 'ingest', 'wiki', 'sl', 'sql', 'status', 'mcp']);
19
20
  const PROJECT_INDEPENDENT_ADMIN_COMMANDS = new Set(['runtime', 'schema']);
@@ -319,16 +320,27 @@ export function collectCommandFlagsPresent(command) {
319
320
  }
320
321
  export function buildKtxProgram(options) {
321
322
  const program = createBaseProgram(options.packageInfo, options.io);
323
+ let pendingUpdateNotice = null;
322
324
  program.hook('preAction', async (_thisCommand, actionCommand) => {
323
325
  // The hidden completion command must stay silent and side-effect free: skip
324
- // the telemetry notice, command span, and project checks entirely.
326
+ // the telemetry notice, command span, project checks, and update checks entirely.
325
327
  if (commandPath(actionCommand).includes('__complete')) {
326
328
  return;
327
329
  }
330
+ const commandNode = actionCommand;
331
+ const updateCheck = await prepareUpdateCheckNotice({
332
+ io: options.io,
333
+ env: options.updateCheck?.env,
334
+ fetchDistTags: options.updateCheck?.fetchDistTags,
335
+ homeDir: options.updateCheck?.homeDir,
336
+ installedVersion: options.packageInfo.version,
337
+ now: options.updateCheck?.now,
338
+ commandOptions: commandOptions(commandNode),
339
+ });
340
+ pendingUpdateNotice = updateCheck.notice;
328
341
  const telemetry = await import('./telemetry/index.js');
329
342
  options.setTelemetryModule?.(telemetry);
330
343
  await telemetry.showTelemetryNoticeIfNeeded(options.io, options.packageInfo);
331
- const commandNode = actionCommand;
332
344
  const path = commandPath(commandNode);
333
345
  const projectDir = resolveCommandProjectDir(commandNode);
334
346
  const hasProject = ktxYamlExists(projectDir);
@@ -344,6 +356,12 @@ export function buildKtxProgram(options) {
344
356
  writeProjectDir(options.io, commandNode);
345
357
  ensureProjectAvailable(options.io, commandNode);
346
358
  });
359
+ program.hook('postAction', () => {
360
+ if (pendingUpdateNotice) {
361
+ options.io.stderr.write(pendingUpdateNotice);
362
+ pendingUpdateNotice = null;
363
+ }
364
+ });
347
365
  const context = {
348
366
  io: options.io,
349
367
  deps: options.deps,
@@ -407,6 +425,13 @@ export async function runCommanderKtxCli(argv, io, deps, info, options) {
407
425
  return await runBareInteractiveCommand(program, io, context);
408
426
  }
409
427
  catch (error) {
428
+ const telemetry = await import('./telemetry/index.js');
429
+ await telemetry.reportException({
430
+ error,
431
+ context: { source: 'bare-interactive', handled: true, fatal: false },
432
+ packageInfo: info,
433
+ io,
434
+ });
410
435
  io.stderr.write(`${formatCliError(error)}\n`);
411
436
  return 1;
412
437
  }
@@ -443,6 +468,21 @@ export async function runCommanderKtxCli(argv, io, deps, info, options) {
443
468
  outcome: commandOutcomeForParseResult(parseError, exitCode),
444
469
  error: parseError,
445
470
  });
471
+ if (parseError &&
472
+ !isCommanderExit(parseError) &&
473
+ !isKtxProjectMissingAbortError(parseError)) {
474
+ await telemetryModule.reportException({
475
+ error: parseError,
476
+ context: {
477
+ source: completed?.commandPath.join(' ') ?? 'commander parseAsync',
478
+ handled: true,
479
+ fatal: false,
480
+ },
481
+ projectDir: completed?.projectGroupAttached ? completed.projectDir : undefined,
482
+ packageInfo: info,
483
+ io,
484
+ });
485
+ }
446
486
  await telemetryModule.emitCompletedCommand({ completed, packageInfo: info, io });
447
487
  await telemetryModule.shutdownTelemetryEmitter();
448
488
  }
@@ -47,4 +47,7 @@ export declare function runInitForCommander(args: {
47
47
  projectDir: string;
48
48
  force: boolean;
49
49
  }, io: KtxCliIo): Promise<number>;
50
+ /** @internal */
51
+ export declare function createGlobalExceptionReporter(io: KtxCliIo, info: KtxCliPackageInfo): (source: "uncaughtException" | "unhandledRejection", error: unknown) => Promise<void>;
52
+ export declare function installGlobalExceptionHandlers(io: KtxCliIo, info: KtxCliPackageInfo): () => void;
50
53
  export declare function runKtxCli(argv?: string[], io?: KtxCliIo, deps?: KtxCliDeps): Promise<number>;
@@ -35,11 +35,102 @@ async function runInit(args, io) {
35
35
  export async function runInitForCommander(args, io) {
36
36
  return await runInit(args, io);
37
37
  }
38
+ function signalExitCode(signal) {
39
+ // 128 + signal number: SIGINT (2) -> 130, SIGTERM (15) -> 143.
40
+ return signal === 'SIGTERM' ? 143 : 130;
41
+ }
42
+ /**
43
+ * Flush telemetry on interrupt for the real CLI process. `capture()` is
44
+ * fire-and-forget and the only flush guarantee lives in a `finally` a signal
45
+ * skips, so Ctrl-C / `kill` of a long-running command (ingest, `mcp stdio`)
46
+ * would otherwise drop its `command` event and queued events. Installed only
47
+ * when driving the actual process; programmatic/test callers pass their own
48
+ * `io` and never reach here. Returns a disposer that removes the listeners.
49
+ */
50
+ function installTelemetrySignalFlush(io, info) {
51
+ let handling = false;
52
+ const handle = (signal) => {
53
+ if (handling) {
54
+ process.exit(signalExitCode(signal));
55
+ }
56
+ handling = true;
57
+ void (async () => {
58
+ try {
59
+ const { emitAbortedCommandAndShutdown } = await import('./telemetry/index.js');
60
+ await emitAbortedCommandAndShutdown({ packageInfo: info, io });
61
+ }
62
+ catch {
63
+ // Best-effort: never let a telemetry hiccup block the interrupt exit.
64
+ }
65
+ process.exit(signalExitCode(signal));
66
+ })();
67
+ };
68
+ const onSigint = () => handle('SIGINT');
69
+ const onSigterm = () => handle('SIGTERM');
70
+ process.on('SIGINT', onSigint);
71
+ process.on('SIGTERM', onSigterm);
72
+ return () => {
73
+ process.off('SIGINT', onSigint);
74
+ process.off('SIGTERM', onSigterm);
75
+ };
76
+ }
77
+ /** @internal */
78
+ export function createGlobalExceptionReporter(io, info) {
79
+ return async (source, error) => {
80
+ const { reportException, shutdownTelemetryEmitter } = await import('./telemetry/index.js');
81
+ await reportException({
82
+ error,
83
+ context: { source, handled: false, fatal: true },
84
+ io,
85
+ packageInfo: info,
86
+ immediate: true,
87
+ });
88
+ await shutdownTelemetryEmitter();
89
+ };
90
+ }
91
+ export function installGlobalExceptionHandlers(io, info) {
92
+ const report = createGlobalExceptionReporter(io, info);
93
+ const handle = (source, error) => {
94
+ void (async () => {
95
+ try {
96
+ await report(source, error);
97
+ }
98
+ catch {
99
+ // Best-effort: preserve Node's process termination behavior.
100
+ }
101
+ if (error instanceof Error && error.stack) {
102
+ io.stderr.write(`${error.stack}\n`);
103
+ }
104
+ else {
105
+ io.stderr.write(`${String(error)}\n`);
106
+ }
107
+ process.exit(1);
108
+ })();
109
+ };
110
+ const onUncaught = (error) => handle('uncaughtException', error);
111
+ const onUnhandled = (reason) => handle('unhandledRejection', reason);
112
+ process.on('uncaughtException', onUncaught);
113
+ process.on('unhandledRejection', onUnhandled);
114
+ return () => {
115
+ process.off('uncaughtException', onUncaught);
116
+ process.off('unhandledRejection', onUnhandled);
117
+ };
118
+ }
38
119
  export async function runKtxCli(argv = process.argv.slice(2), io = process, deps = {}) {
39
120
  const info = getKtxCliPackageInfo();
40
121
  profileMark('runtime:runKtxCli');
41
122
  const { runCommanderKtxCli } = await profileSpan('import ./cli-program.js', () => import('./cli-program.js'));
42
- return await runCommanderKtxCli(argv, io, deps, info, {
43
- runInit: runInitForCommander,
44
- });
123
+ // Real-process entry only: flush telemetry if interrupted. Test/programmatic
124
+ // callers pass their own `io`, so they never install process-level handlers.
125
+ const removeSignalFlush = io === process ? installTelemetrySignalFlush(io, info) : undefined;
126
+ const removeGlobalExceptionHandlers = io === process ? installGlobalExceptionHandlers(io, info) : undefined;
127
+ try {
128
+ return await runCommanderKtxCli(argv, io, deps, info, {
129
+ runInit: runInitForCommander,
130
+ });
131
+ }
132
+ finally {
133
+ removeGlobalExceptionHandlers?.();
134
+ removeSignalFlush?.();
135
+ }
45
136
  }
@@ -18,7 +18,7 @@ function embeddingBackend(value) {
18
18
  throw new InvalidArgumentError(`invalid choice '${value}'`);
19
19
  }
20
20
  function llmBackend(value) {
21
- if (value === 'anthropic' || value === 'vertex' || value === 'claude-code') {
21
+ if (value === 'anthropic' || value === 'vertex' || value === 'claude-code' || value === 'codex') {
22
22
  return value;
23
23
  }
24
24
  throw new InvalidArgumentError(`invalid choice '${value}'`);
@@ -89,7 +89,6 @@ function shouldShowSetupEntryMenu(options, command) {
89
89
  'llmBackend',
90
90
  'anthropicApiKeyEnv',
91
91
  'anthropicApiKeyFile',
92
- 'llmModel',
93
92
  'vertexProject',
94
93
  'vertexLocation',
95
94
  'skipLlm',
@@ -145,7 +144,6 @@ export function registerSetupCommands(program, context) {
145
144
  .addOption(new Option('--llm-backend <backend>', 'LLM backend').argParser(llmBackend).hideHelp())
146
145
  .addOption(new Option('--anthropic-api-key-env <name>', 'Environment variable containing the Anthropic API key').hideHelp())
147
146
  .addOption(new Option('--anthropic-api-key-file <path>', 'File containing the Anthropic API key').hideHelp())
148
- .addOption(new Option('--llm-model <model>', 'LLM model ID or backend model alias').hideHelp())
149
147
  .addOption(new Option('--vertex-project <project>', 'Google Vertex AI project ID, env:NAME, or file:/path').hideHelp())
150
148
  .addOption(new Option('--vertex-location <location>', 'Google Vertex AI location, env:NAME, or file:/path').hideHelp())
151
149
  .addOption(new Option('--skip-llm', 'Leave LLM setup incomplete for now').hideHelp().default(false))
@@ -272,6 +270,7 @@ export function registerSetupCommands(program, context) {
272
270
  return;
273
271
  }
274
272
  const resolvedAgentScope = options.local ? 'local' : options.global ? 'global' : 'project';
273
+ const debugEnabled = (command.optsWithGlobals ? command.optsWithGlobals() : command.opts()).debug === true;
275
274
  await runSetupArgs(context, {
276
275
  command: 'run',
277
276
  projectDir: resolveCommandProjectDir(command),
@@ -281,12 +280,12 @@ export function registerSetupCommands(program, context) {
281
280
  agentScope: resolvedAgentScope,
282
281
  skipAgents: options.skipAgents === true,
283
282
  inputMode: options.input === false ? 'disabled' : 'auto',
283
+ ...(debugEnabled ? { debug: true } : {}),
284
284
  yes: options.yes === true,
285
285
  cliVersion: context.packageInfo.version,
286
286
  ...(options.llmBackend ? { llmBackend: options.llmBackend } : {}),
287
287
  ...(options.anthropicApiKeyEnv ? { anthropicApiKeyEnv: options.anthropicApiKeyEnv } : {}),
288
288
  ...(options.anthropicApiKeyFile ? { anthropicApiKeyFile: options.anthropicApiKeyFile } : {}),
289
- ...(options.llmModel ? { llmModel: options.llmModel } : {}),
290
289
  ...(options.vertexProject ? { vertexProject: options.vertexProject } : {}),
291
290
  ...(options.vertexLocation ? { vertexLocation: options.vertexLocation } : {}),
292
291
  skipLlm: options.skipLlm === true,
@@ -0,0 +1,34 @@
1
+ import type { KtxCliIo } from './cli-runtime.js';
2
+ import type { KtxSetupPromptOption } from './setup-prompts.js';
3
+ export type RecoveryOutcome = 'ready' | 'skip' | 'back' | 'failed';
4
+ /** @internal */
5
+ export interface RecoveryAction {
6
+ value: string;
7
+ label: string;
8
+ run: () => Promise<void>;
9
+ }
10
+ export type ConfigureResult = 'configured' | 'back' | 'cancelled';
11
+ export type ValidateResult = {
12
+ status: 'ok';
13
+ } | {
14
+ status: 'back';
15
+ } | {
16
+ status: 'failed';
17
+ extraActions?: RecoveryAction[];
18
+ };
19
+ export interface ConnectionRecoveryInput {
20
+ label: string;
21
+ interactive: boolean;
22
+ allowSkip: boolean;
23
+ io: KtxCliIo;
24
+ prompts: {
25
+ select(options: {
26
+ message: string;
27
+ options: KtxSetupPromptOption[];
28
+ }): Promise<string>;
29
+ };
30
+ snapshot: () => Promise<() => Promise<void>>;
31
+ configure: () => Promise<ConfigureResult>;
32
+ validate: () => Promise<ValidateResult>;
33
+ }
34
+ export declare function runConnectionSetupWithRecovery(input: ConnectionRecoveryInput): Promise<RecoveryOutcome>;
@@ -0,0 +1,82 @@
1
+ async function runRollbackOnce(input) {
2
+ if (input.state.rolledBack) {
3
+ return;
4
+ }
5
+ input.state.rolledBack = true;
6
+ await input.rollback();
7
+ }
8
+ function recoveryOptions(input) {
9
+ return [
10
+ { value: 'retry', label: 'Retry connection test' },
11
+ { value: 're-enter', label: 'Re-enter connection details' },
12
+ ...(input.extraActions ?? []).map((action) => ({
13
+ value: action.value,
14
+ label: action.label,
15
+ })),
16
+ ...(input.allowSkip ? [{ value: 'skip', label: 'Skip this connection' }] : []),
17
+ { value: 'back', label: 'Back' },
18
+ ];
19
+ }
20
+ export async function runConnectionSetupWithRecovery(input) {
21
+ const rollback = await input.snapshot();
22
+ const rollbackState = { rolledBack: false };
23
+ const firstConfig = await input.configure();
24
+ if (firstConfig === 'back') {
25
+ await runRollbackOnce({ rollback, state: rollbackState });
26
+ return 'back';
27
+ }
28
+ if (firstConfig === 'cancelled') {
29
+ await runRollbackOnce({ rollback, state: rollbackState });
30
+ return 'failed';
31
+ }
32
+ let validation = await input.validate();
33
+ while (validation.status !== 'ok') {
34
+ if (validation.status === 'back') {
35
+ await runRollbackOnce({ rollback, state: rollbackState });
36
+ return 'back';
37
+ }
38
+ if (!input.interactive) {
39
+ return 'failed';
40
+ }
41
+ const action = await input.prompts.select({
42
+ message: `Connection setup failed for ${input.label}`,
43
+ options: recoveryOptions({
44
+ allowSkip: input.allowSkip,
45
+ extraActions: validation.extraActions,
46
+ }),
47
+ });
48
+ if (action === 'back') {
49
+ await runRollbackOnce({ rollback, state: rollbackState });
50
+ return 'back';
51
+ }
52
+ if (action === 'skip' && input.allowSkip) {
53
+ await runRollbackOnce({ rollback, state: rollbackState });
54
+ return 'skip';
55
+ }
56
+ if (action === 're-enter') {
57
+ const nextConfig = await input.configure();
58
+ if (nextConfig === 'back') {
59
+ await runRollbackOnce({ rollback, state: rollbackState });
60
+ return 'back';
61
+ }
62
+ if (nextConfig === 'cancelled') {
63
+ await runRollbackOnce({ rollback, state: rollbackState });
64
+ return 'failed';
65
+ }
66
+ validation = await input.validate();
67
+ continue;
68
+ }
69
+ if (action === 'retry') {
70
+ validation = await input.validate();
71
+ continue;
72
+ }
73
+ const extraAction = validation.extraActions?.find((candidate) => candidate.value === action);
74
+ if (extraAction) {
75
+ await extraAction.run();
76
+ validation = await input.validate();
77
+ continue;
78
+ }
79
+ validation = await input.validate();
80
+ }
81
+ return 'ready';
82
+ }
@@ -12,8 +12,9 @@ import { bold, dim, green, red, SYMBOLS } from './io/symbols.js';
12
12
  import { createKtxCliScanConnector } from './local-scan-connectors.js';
13
13
  import { profileMark } from './startup-profile.js';
14
14
  import { isDemoConnection } from './telemetry/demo-detect.js';
15
- import { emitTelemetryEvent } from './telemetry/index.js';
16
- import { scrubErrorClass } from './telemetry/scrubber.js';
15
+ import { emitTelemetryEvent, reportException } from './telemetry/index.js';
16
+ import { collectTelemetryRedactionSecrets } from './telemetry/redaction-secrets.js';
17
+ import { formatErrorDetail, scrubErrorClass } from './telemetry/scrubber.js';
17
18
  profileMark('module:connection');
18
19
  const SUPPORTED_TEST_DRIVERS = [
19
20
  'sqlite',
@@ -44,6 +45,12 @@ async function testNativeConnection(project, connectionId, createScanConnector)
44
45
  }
45
46
  const result = await connector.testConnection();
46
47
  if (!result.success) {
48
+ // Re-throw the driver's original error so connection_test telemetry records
49
+ // its real class (e.g. ConnectionError) and code (e.g. ELOGIN) instead of
50
+ // collapsing every native failure to a generic Error with no code.
51
+ if (result.cause instanceof Error) {
52
+ throw result.cause;
53
+ }
47
54
  throw new Error(result.error ?? 'connection test failed');
48
55
  }
49
56
  return { driver: connector.driver };
@@ -168,6 +175,7 @@ async function testConnectionByDriver(project, connectionId, deps) {
168
175
  }
169
176
  async function emitConnectionTest(input) {
170
177
  const errorClass = input.error ? scrubErrorClass(input.error) : undefined;
178
+ const errorDetail = input.error ? formatErrorDetail(input.error) : undefined;
171
179
  await emitTelemetryEvent({
172
180
  name: 'connection_test',
173
181
  projectDir: input.project.projectDir,
@@ -178,8 +186,24 @@ async function emitConnectionTest(input) {
178
186
  outcome: input.outcome,
179
187
  durationMs: input.durationMs,
180
188
  ...(errorClass ? { errorClass } : {}),
189
+ ...(errorDetail ? { errorDetail } : {}),
181
190
  },
182
191
  });
192
+ if (input.error) {
193
+ await reportException({
194
+ error: input.error,
195
+ context: { source: 'connection test', handled: true, fatal: false },
196
+ projectDir: input.project.projectDir,
197
+ io: input.io,
198
+ redactionSecrets: await collectTelemetryRedactionSecrets({
199
+ project: input.project,
200
+ connectionId: input.connectionId,
201
+ includeLlm: false,
202
+ includeEmbeddings: false,
203
+ env: process.env,
204
+ }),
205
+ });
206
+ }
183
207
  }
184
208
  function visualWidth(text) {
185
209
  // styleText wraps content in ANSI escape sequences; strip them before measuring.
@@ -1,5 +1,5 @@
1
1
  import { type TableField } from '@google-cloud/bigquery';
2
- import { type KtxColumnSampleInput, type KtxColumnSampleResult, type KtxColumnStatsInput, type KtxColumnStatsResult, type KtxQueryResult, type KtxReadOnlyQueryInput, type KtxScanConnector, type KtxScanContext, type KtxScanInput, type KtxSchemaSnapshot, type KtxTableListEntry, type KtxTableRef, type KtxTableSampleInput, type KtxTableSampleResult } from '../../context/scan/types.js';
2
+ import { type KtxConnectorTestResult, type KtxColumnSampleInput, type KtxColumnSampleResult, type KtxColumnStatsInput, type KtxColumnStatsResult, type KtxQueryResult, type KtxReadOnlyQueryInput, type KtxScanConnector, type KtxScanContext, type KtxScanInput, type KtxSchemaSnapshot, type KtxTableListEntry, type KtxTableRef, type KtxTableSampleInput, type KtxTableSampleResult } from '../../context/scan/types.js';
3
3
  export interface KtxBigQueryConnectionConfig {
4
4
  driver?: string;
5
5
  dataset_id?: string;
@@ -121,10 +121,7 @@ export declare class KtxBigQueryScanConnector implements KtxScanConnector {
121
121
  private readonly dialect;
122
122
  private client;
123
123
  constructor(options: KtxBigQueryScanConnectorOptions);
124
- testConnection(): Promise<{
125
- success: boolean;
126
- error?: string;
127
- }>;
124
+ testConnection(): Promise<KtxConnectorTestResult>;
128
125
  introspect(input: KtxScanInput, _ctx: KtxScanContext): Promise<KtxSchemaSnapshot>;
129
126
  sampleTable(input: KtxTableSampleInput, _ctx: KtxScanContext): Promise<KtxTableSampleResult & {
130
127
  headerTypes?: string[];
@@ -4,7 +4,7 @@ import { getDialectForDriver } from '../../context/connections/dialects.js';
4
4
  import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js';
5
5
  import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js';
6
6
  import { scopedTableNames } from '../../context/scan/table-ref.js';
7
- import { createKtxConnectorCapabilities, } from '../../context/scan/types.js';
7
+ import { connectorTestFailure, createKtxConnectorCapabilities, } from '../../context/scan/types.js';
8
8
  import { readFileSync } from 'node:fs';
9
9
  import { homedir } from 'node:os';
10
10
  import { resolve } from 'node:path';
@@ -185,7 +185,7 @@ export class KtxBigQueryScanConnector {
185
185
  return { success: true };
186
186
  }
187
187
  catch (error) {
188
- return { success: false, error: error instanceof Error ? error.message : String(error) };
188
+ return connectorTestFailure(error);
189
189
  }
190
190
  }
191
191
  async introspect(input, _ctx) {
@@ -1,5 +1,5 @@
1
1
  import { createClient } from '@clickhouse/client';
2
- import { type KtxColumnSampleInput, type KtxColumnSampleResult, type KtxColumnStatsInput, type KtxColumnStatsResult, type KtxQueryResult, type KtxReadOnlyQueryInput, type KtxScanConnector, type KtxScanContext, type KtxScanInput, type KtxSchemaSnapshot, type KtxTableRef, type KtxTableSampleInput, type KtxTableListEntry, type KtxTableSampleResult } from '../../context/scan/types.js';
2
+ import { type KtxConnectorTestResult, type KtxColumnSampleInput, type KtxColumnSampleResult, type KtxColumnStatsInput, type KtxColumnStatsResult, type KtxQueryResult, type KtxReadOnlyQueryInput, type KtxScanConnector, type KtxScanContext, type KtxScanInput, type KtxSchemaSnapshot, type KtxTableRef, type KtxTableSampleInput, type KtxTableListEntry, type KtxTableSampleResult } from '../../context/scan/types.js';
3
3
  export interface KtxClickHouseConnectionConfig {
4
4
  driver?: string;
5
5
  host?: string;
@@ -94,10 +94,7 @@ export declare class KtxClickHouseScanConnector implements KtxScanConnector {
94
94
  private client;
95
95
  private resolvedEndpoint;
96
96
  constructor(options: KtxClickHouseScanConnectorOptions);
97
- testConnection(): Promise<{
98
- success: boolean;
99
- error?: string;
100
- }>;
97
+ testConnection(): Promise<KtxConnectorTestResult>;
101
98
  introspect(input: KtxScanInput, _ctx: KtxScanContext): Promise<KtxSchemaSnapshot>;
102
99
  private emptySnapshot;
103
100
  sampleTable(input: KtxTableSampleInput, _ctx: KtxScanContext): Promise<KtxTableSampleResult>;
@@ -1,7 +1,7 @@
1
1
  import { createClient } from '@clickhouse/client';
2
2
  import { getDialectForDriver } from '../../context/connections/dialects.js';
3
3
  import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js';
4
- import { createKtxConnectorCapabilities } from '../../context/scan/types.js';
4
+ import { connectorTestFailure, createKtxConnectorCapabilities } from '../../context/scan/types.js';
5
5
  import { scopedTableNames } from '../../context/scan/table-ref.js';
6
6
  import { readFileSync } from 'node:fs';
7
7
  import { Agent as HttpsAgent } from 'node:https';
@@ -162,7 +162,7 @@ export class KtxClickHouseScanConnector {
162
162
  return { success: true };
163
163
  }
164
164
  catch (error) {
165
- return { success: false, error: error instanceof Error ? error.message : String(error) };
165
+ return connectorTestFailure(error);
166
166
  }
167
167
  }
168
168
  async introspect(input, _ctx) {
@@ -1,5 +1,5 @@
1
1
  import { type FieldPacket, type RowDataPacket } from 'mysql2/promise';
2
- import { type KtxColumnSampleInput, type KtxColumnSampleResult, type KtxColumnStatsInput, type KtxColumnStatsResult, type KtxQueryResult, type KtxReadOnlyQueryInput, type KtxScanConnector, type KtxScanContext, type KtxScanInput, type KtxSchemaSnapshot, type KtxTableListEntry, type KtxTableRef, type KtxTableSampleInput, type KtxTableSampleResult } from '../../context/scan/types.js';
2
+ import { type KtxConnectorTestResult, type KtxColumnSampleInput, type KtxColumnSampleResult, type KtxColumnStatsInput, type KtxColumnStatsResult, type KtxQueryResult, type KtxReadOnlyQueryInput, type KtxScanConnector, type KtxScanContext, type KtxScanInput, type KtxSchemaSnapshot, type KtxTableListEntry, type KtxTableRef, type KtxTableSampleInput, type KtxTableSampleResult } from '../../context/scan/types.js';
3
3
  export interface KtxMysqlConnectionConfig {
4
4
  driver?: string;
5
5
  host?: string;
@@ -71,6 +71,9 @@ export interface KtxMysqlColumnDistinctValuesResult {
71
71
  values: string[] | null;
72
72
  cardinality: number;
73
73
  }
74
+ export interface KtxMysqlColumnStatisticsResult {
75
+ cardinalityByColumn: Map<string, number>;
76
+ }
74
77
  /** @internal */
75
78
  export declare function prepareMysqlReadOnlyQuery(sql: string, params?: Record<string, unknown>): {
76
79
  sql: string;
@@ -97,15 +100,13 @@ export declare class KtxMysqlScanConnector implements KtxScanConnector {
97
100
  private pool;
98
101
  private resolvedEndpoint;
99
102
  constructor(options: KtxMysqlScanConnectorOptions);
100
- testConnection(): Promise<{
101
- success: boolean;
102
- error?: string;
103
- }>;
103
+ testConnection(): Promise<KtxConnectorTestResult>;
104
104
  introspect(input: KtxScanInput, _ctx: KtxScanContext): Promise<KtxSchemaSnapshot>;
105
105
  private emptySnapshot;
106
106
  sampleTable(input: KtxTableSampleInput, _ctx: KtxScanContext): Promise<KtxTableSampleResult>;
107
107
  sampleColumn(input: KtxColumnSampleInput, _ctx: KtxScanContext): Promise<KtxColumnSampleResult>;
108
- columnStats(_input: KtxColumnStatsInput, _ctx: KtxScanContext): Promise<KtxColumnStatsResult | null>;
108
+ columnStats(input: KtxColumnStatsInput, _ctx: KtxScanContext): Promise<KtxColumnStatsResult | null>;
109
+ getColumnStatistics(table: KtxTableRef): Promise<KtxMysqlColumnStatisticsResult | null>;
109
110
  executeReadOnly(input: KtxMysqlReadOnlyQueryInput, _ctx: KtxScanContext): Promise<KtxQueryResult>;
110
111
  getColumnDistinctValues(table: KtxTableRef, columnName: string, options: KtxMysqlColumnDistinctValuesOptions): Promise<KtxMysqlColumnDistinctValuesResult | null>;
111
112
  getTableRowCount(tableName: string): Promise<number>;