@kaelio/ktx 0.11.0 → 0.12.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 (181) hide show
  1. package/assets/python/{kaelio_ktx-0.11.0-py3-none-any.whl → kaelio_ktx-0.12.0-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/admin.js +1 -1
  5. package/dist/clack.d.ts +16 -0
  6. package/dist/clack.js +37 -6
  7. package/dist/claude-code-prompt-caching.js +1 -1
  8. package/dist/cli-program.js +3 -3
  9. package/dist/cli-runtime.js +2 -2
  10. package/dist/commands/connection-commands.js +1 -1
  11. package/dist/commands/ingest-commands.js +4 -4
  12. package/dist/commands/mcp-commands.js +12 -12
  13. package/dist/commands/runtime-commands.js +4 -4
  14. package/dist/commands/setup-commands.js +6 -5
  15. package/dist/commands/sl-commands.js +1 -1
  16. package/dist/commands/sql-commands.js +1 -1
  17. package/dist/commands/status-commands.js +1 -1
  18. package/dist/connection.js +1 -1
  19. package/dist/connectors/clickhouse/connector.js +1 -1
  20. package/dist/connectors/mysql/connector.js +1 -1
  21. package/dist/connectors/snowflake/connector.d.ts +1 -1
  22. package/dist/connectors/sqlite/connector.js +2 -25
  23. package/dist/connectors/sqlserver/connector.js +3 -3
  24. package/dist/context/connections/connection-type.d.ts +1 -1
  25. package/dist/context/connections/read-only-sql.d.ts +1 -0
  26. package/dist/context/connections/read-only-sql.js +116 -2
  27. package/dist/context/core/git.service.d.ts +23 -0
  28. package/dist/context/core/git.service.js +71 -8
  29. package/dist/context/ingest/adapters/historic-sql/projection.js +2 -1
  30. package/dist/context/ingest/adapters/looker/client.js +7 -2
  31. package/dist/context/ingest/adapters/looker/factory.d.ts +8 -1
  32. package/dist/context/ingest/adapters/looker/factory.js +9 -0
  33. package/dist/context/ingest/adapters/looker/mapping.js +1 -1
  34. package/dist/context/ingest/adapters/looker/types.d.ts +1 -1
  35. package/dist/context/ingest/adapters/metabase/client.d.ts +1 -1
  36. package/dist/context/ingest/adapters/metabase/client.js +1 -1
  37. package/dist/context/ingest/adapters/metabase/local-metabase.adapter.js +1 -1
  38. package/dist/context/ingest/adapters/metabase/mapping.js +6 -6
  39. package/dist/context/ingest/artifact-gates.d.ts +2 -6
  40. package/dist/context/ingest/artifact-gates.js +5 -47
  41. package/dist/context/ingest/constrained-repair.d.ts +55 -0
  42. package/dist/context/ingest/constrained-repair.js +167 -0
  43. package/dist/context/ingest/final-gate-repair.d.ts +9 -11
  44. package/dist/context/ingest/final-gate-repair.js +40 -128
  45. package/dist/context/ingest/finalization-scope.d.ts +1 -1
  46. package/dist/context/ingest/finalization-scope.js +15 -15
  47. package/dist/context/ingest/ingest-bundle.runner.d.ts +1 -0
  48. package/dist/context/ingest/ingest-bundle.runner.js +101 -67
  49. package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +6 -13
  50. package/dist/context/ingest/isolated-diff/patch-integrator.js +32 -109
  51. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +8 -9
  52. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +63 -141
  53. package/dist/context/ingest/local-bundle-runtime.d.ts +2 -0
  54. package/dist/context/ingest/local-bundle-runtime.js +9 -10
  55. package/dist/context/ingest/local-ingest.d.ts +2 -0
  56. package/dist/context/ingest/local-ingest.js +2 -0
  57. package/dist/context/ingest/memory-flow/view-model.js +1 -1
  58. package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -6
  59. package/dist/context/ingest/stages/stage-3-work-units.js +2 -1
  60. package/dist/context/ingest/stages/validate-wu-sources.d.ts +7 -1
  61. package/dist/context/ingest/stages/validate-wu-sources.js +109 -4
  62. package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.d.ts +2 -0
  63. package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.js +1 -1
  64. package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +3 -3
  65. package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.d.ts +3 -1
  66. package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +15 -1
  67. package/dist/context/llm/ai-sdk-runtime.js +2 -2
  68. package/dist/context/llm/claude-code-runtime.js +1 -1
  69. package/dist/context/llm/local-config.js +1 -1
  70. package/dist/context/llm/runtime-tools.js +2 -2
  71. package/dist/context/mcp/context-tools.js +7 -7
  72. package/dist/context/mcp/local-project-ports.js +23 -54
  73. package/dist/context/memory/local-memory.js +4 -1
  74. package/dist/context/memory/memory-agent.service.js +1 -1
  75. package/dist/context/project/config.d.ts +11 -4
  76. package/dist/context/project/config.js +85 -30
  77. package/dist/context/project/driver-schemas.js +1 -1
  78. package/dist/context/project/mappings-yaml-schema.js +2 -2
  79. package/dist/context/project/project.js +12 -4
  80. package/dist/context/scan/description-generation.js +4 -4
  81. package/dist/context/scan/local-enrichment-artifacts.js +2 -1
  82. package/dist/context/scan/local-scan.js +2 -2
  83. package/dist/context/scan/local-structural-artifacts.js +5 -5
  84. package/dist/context/scan/relationship-benchmark-report.js +1 -1
  85. package/dist/context/scan/relationship-discovery.js +3 -3
  86. package/dist/context/scan/relationship-llm-proposal.js +3 -3
  87. package/dist/context/sl/local-query.js +3 -33
  88. package/dist/context/sl/local-sl.d.ts +0 -8
  89. package/dist/context/sl/local-sl.js +44 -69
  90. package/dist/context/sl/semantic-layer.service.d.ts +25 -8
  91. package/dist/context/sl/semantic-layer.service.js +109 -56
  92. package/dist/context/sl/source-files.d.ts +46 -0
  93. package/dist/context/sl/source-files.js +131 -0
  94. package/dist/context/sl/tools/base-semantic-layer.tool.d.ts +2 -2
  95. package/dist/context/sl/tools/base-semantic-layer.tool.js +2 -7
  96. package/dist/context/sl/tools/sl-edit-source.tool.js +10 -8
  97. package/dist/context/sl/tools/sl-warehouse-validation.js +55 -27
  98. package/dist/context/sl/tools/sl-write-source.tool.js +12 -9
  99. package/dist/context/sql-analysis/dialect.d.ts +2 -0
  100. package/dist/context/sql-analysis/dialect.js +20 -0
  101. package/dist/context/tools/base-tool.d.ts +6 -19
  102. package/dist/context/tools/base-tool.js +0 -14
  103. package/dist/context-build-view.js +5 -5
  104. package/dist/database-tree-picker.js +18 -3
  105. package/dist/demo-assets.js +0 -1
  106. package/dist/doctor.d.ts +1 -1
  107. package/dist/doctor.js +31 -23
  108. package/dist/errors.d.ts +31 -0
  109. package/dist/errors.js +44 -0
  110. package/dist/ingest.d.ts +1 -1
  111. package/dist/ingest.js +8 -2
  112. package/dist/io/symbols.d.ts +2 -0
  113. package/dist/io/symbols.js +2 -0
  114. package/dist/io/tty.d.ts +8 -0
  115. package/dist/io/tty.js +16 -0
  116. package/dist/llm/embedding-health.js +1 -1
  117. package/dist/llm/embedding-provider.js +3 -3
  118. package/dist/llm/model-provider.js +1 -1
  119. package/dist/local-adapters.d.ts +1 -0
  120. package/dist/local-adapters.js +2 -2
  121. package/dist/local-scan-connectors.js +1 -1
  122. package/dist/managed-local-embeddings.js +17 -8
  123. package/dist/managed-mcp-daemon.js +3 -3
  124. package/dist/managed-python-command.d.ts +7 -0
  125. package/dist/managed-python-command.js +34 -8
  126. package/dist/managed-python-daemon.js +2 -2
  127. package/dist/managed-python-http.js +3 -3
  128. package/dist/managed-python-runtime.d.ts +30 -1
  129. package/dist/managed-python-runtime.js +134 -18
  130. package/dist/managed-uv-release.d.ts +7 -0
  131. package/dist/managed-uv-release.js +11 -0
  132. package/dist/mcp-http-server.js +4 -4
  133. package/dist/mcp-server-factory.js +3 -3
  134. package/dist/mcp-stdio-server.js +1 -1
  135. package/dist/memory-flow-hud.js +2 -2
  136. package/dist/next-steps.js +2 -2
  137. package/dist/prompt-navigation.d.ts +17 -0
  138. package/dist/prompt-navigation.js +49 -3
  139. package/dist/prompts/memory_agent_bundle_ingest_work_unit.md +2 -2
  140. package/dist/prompts/memory_agent_external_ingest.md +2 -2
  141. package/dist/public-ingest-copy.js +1 -1
  142. package/dist/public-ingest.js +3 -3
  143. package/dist/release-version.js +1 -1
  144. package/dist/runtime-requirements.js +1 -1
  145. package/dist/runtime.js +9 -9
  146. package/dist/scan.js +1 -1
  147. package/dist/setup-agents.js +21 -30
  148. package/dist/setup-banner.d.ts +20 -0
  149. package/dist/setup-banner.js +39 -0
  150. package/dist/setup-context.js +24 -15
  151. package/dist/setup-databases.js +31 -59
  152. package/dist/setup-demo-tour.js +12 -8
  153. package/dist/setup-embeddings.js +9 -9
  154. package/dist/setup-interrupt.js +1 -1
  155. package/dist/setup-models.d.ts +4 -1
  156. package/dist/setup-models.js +54 -28
  157. package/dist/setup-project.js +29 -5
  158. package/dist/setup-prompts.js +16 -1
  159. package/dist/setup-ready-menu.js +1 -1
  160. package/dist/setup-sources.js +27 -7
  161. package/dist/setup.js +13 -13
  162. package/dist/skills/analytics/SKILL.md +3 -3
  163. package/dist/skills/dbt_ingest/SKILL.md +3 -3
  164. package/dist/skills/looker_ingest/SKILL.md +3 -3
  165. package/dist/skills/lookml_ingest/SKILL.md +7 -7
  166. package/dist/skills/metabase_ingest/SKILL.md +4 -4
  167. package/dist/skills/metricflow_ingest/SKILL.md +15 -15
  168. package/dist/skills/notion_synthesize/SKILL.md +1 -1
  169. package/dist/skills/sl/SKILL.md +3 -3
  170. package/dist/skills/sl_capture/SKILL.md +1 -1
  171. package/dist/skills/wiki_capture/SKILL.md +1 -1
  172. package/dist/source-mapping.js +1 -1
  173. package/dist/startup-profile.js +1 -1
  174. package/dist/status-project.d.ts +0 -2
  175. package/dist/status-project.js +4 -6
  176. package/dist/telemetry/events.d.ts +1 -1
  177. package/dist/telemetry/exception.js +14 -0
  178. package/dist/text-ingest.js +1 -1
  179. package/dist/tree-picker-tui.d.ts +0 -1
  180. package/dist/tree-picker-tui.js +2 -3
  181. package/package.json +1 -1
package/dist/admin.js CHANGED
@@ -18,7 +18,7 @@ export function registerAdminCommands(program, context) {
18
18
  });
19
19
  admin
20
20
  .command('init')
21
- .description('Initialize a Git-backed KTX project directory for maintenance scripts')
21
+ .description('Initialize a Git-backed ktx project directory for maintenance scripts')
22
22
  .argument('[directory]', 'Project directory')
23
23
  .option('--force', 'Rewrite ktx.yaml and scaffold files in an existing project', false)
24
24
  .action(async (projectDir, commandOptions, command) => {
package/dist/clack.d.ts CHANGED
@@ -39,5 +39,21 @@ export interface KtxCliPromptAdapter {
39
39
  spinner(): KtxCliSpinner;
40
40
  }
41
41
  export declare function createClackSpinner(): KtxCliSpinner;
42
+ /**
43
+ * Stderr-only, non-animated spinner. Use this instead of {@link createCliSpinner}
44
+ * when the next step reads stdin in raw mode (an Ink TUI or a keypress wait):
45
+ * the animated clack spinner seizes stdin via `@clack/core`'s `block()` and
46
+ * leaves it dirty, which the following raw-mode reader misreads as a stray key.
47
+ */
42
48
  export declare function createStaticCliSpinner(io: KtxCliSpinnerIo): KtxCliSpinner;
49
+ /**
50
+ * Animated spinner in an interactive terminal, static `◐/◇/■` lines otherwise
51
+ * (scripts, CI, piped output) so logs stay clean and uncluttered by frames.
52
+ */
53
+ export declare function createCliSpinner(io: KtxCliIo): KtxCliSpinner;
54
+ export declare function runWithCliSpinner<T>(spinner: KtxCliSpinner, text: {
55
+ start: string;
56
+ success: string;
57
+ failure: string;
58
+ }, run: () => Promise<T>): Promise<T>;
43
59
  export declare function createClackPromptAdapter(): KtxCliPromptAdapter;
package/dist/clack.js CHANGED
@@ -36,30 +36,61 @@ class KtxCliPromptCancelledError extends Error {
36
36
  }
37
37
  }
38
38
  export function createClackSpinner() {
39
- return spinner();
39
+ // clack colors the animated spinner frame magenta by default; styleFrame
40
+ // (typed in SpinnerOptions, absent from the README) recolors it ktx orange.
41
+ return spinner({ styleFrame: orange });
40
42
  }
41
- function magenta(text) {
42
- return ansiColor(text, 35, 39);
43
+ // ktx mascot orange (#FF8A4C) via 24-bit truecolor.
44
+ function orange(text) {
45
+ if (!ansiEnabled()) {
46
+ return text;
47
+ }
48
+ return `${ESC}[38;2;255;138;76m${text}${ESC}[39m`;
43
49
  }
44
50
  function red(text) {
45
51
  return ansiColor(text, 31, 39);
46
52
  }
53
+ /**
54
+ * Stderr-only, non-animated spinner. Use this instead of {@link createCliSpinner}
55
+ * when the next step reads stdin in raw mode (an Ink TUI or a keypress wait):
56
+ * the animated clack spinner seizes stdin via `@clack/core`'s `block()` and
57
+ * leaves it dirty, which the following raw-mode reader misreads as a stray key.
58
+ */
47
59
  export function createStaticCliSpinner(io) {
48
60
  return {
49
61
  start(message) {
50
- io.stderr.write(`${magenta('◐')} ${message}\n`);
62
+ io.stderr.write(`${orange('◐')} ${message}\n`);
51
63
  },
52
64
  message(message) {
53
- io.stderr.write(`${magenta('│')} ${message}\n`);
65
+ io.stderr.write(`${orange('│')} ${message}\n`);
54
66
  },
55
67
  stop(message) {
56
- io.stderr.write(`${magenta('◇')} ${message}\n`);
68
+ io.stderr.write(`${orange('◇')} ${message}\n`);
57
69
  },
58
70
  error(message) {
59
71
  io.stderr.write(`${red('■')} ${message}\n`);
60
72
  },
61
73
  };
62
74
  }
75
+ /**
76
+ * Animated spinner in an interactive terminal, static `◐/◇/■` lines otherwise
77
+ * (scripts, CI, piped output) so logs stay clean and uncluttered by frames.
78
+ */
79
+ export function createCliSpinner(io) {
80
+ return io.stdout.isTTY === true ? createClackSpinner() : createStaticCliSpinner(io);
81
+ }
82
+ export async function runWithCliSpinner(spinner, text, run) {
83
+ spinner.start(text.start);
84
+ try {
85
+ const value = await run();
86
+ spinner.stop(text.success);
87
+ return value;
88
+ }
89
+ catch (error) {
90
+ spinner.error(text.failure);
91
+ throw error;
92
+ }
93
+ }
63
94
  export function createClackPromptAdapter() {
64
95
  return {
65
96
  async confirm(options) {
@@ -15,7 +15,7 @@ export function formatClaudeCodePromptCachingWarning(fields) {
15
15
  if (fields.length === 0) {
16
16
  return null;
17
17
  }
18
- return `claude-code ignores ${fields.join(', ')} because the Claude Agent SDK does not expose KTX prompt-cache TTL, tool, or history markers.`;
18
+ return `claude-code ignores ${fields.join(', ')} because the Claude Agent SDK does not expose ktx prompt-cache TTL, tool, or history markers.`;
19
19
  }
20
20
  export function formatClaudeCodePromptCachingFix() {
21
21
  return 'Remove those promptCaching fields or use anthropic, vertex, or gateway when those cache knobs are required.';
@@ -162,8 +162,8 @@ export function resolveCommandProjectDirOverride(command) {
162
162
  function createBaseProgram(info, io) {
163
163
  return new Command()
164
164
  .name('ktx')
165
- .description('KTX data agent context layer CLI')
166
- .option('--project-dir <path>', 'KTX project directory (default: KTX_PROJECT_DIR, nearest ktx.yaml, or cwd)')
165
+ .description('ktx data agent context layer CLI')
166
+ .option('--project-dir <path>', 'ktx project directory (default: KTX_PROJECT_DIR, nearest ktx.yaml, or cwd)')
167
167
  .option('--debug', 'Enable diagnostic logging to stderr')
168
168
  .version(`${info.name} ${info.version}`, '-v, --version', 'Show CLI version')
169
169
  .helpOption('-h, --help', 'Show this help text')
@@ -349,7 +349,7 @@ export function buildKtxProgram(options) {
349
349
  const attachProjectGroup = shouldAttachCommandProjectGroup(path, hasProject);
350
350
  telemetry.beginCommandSpan({
351
351
  commandPath: path,
352
- flagsPresent: collectCommandFlagsPresent(commandNode),
352
+ flagsPresent: collectCommandFlagsPresent(actionCommand),
353
353
  projectDir: attachProjectGroup ? projectDir : undefined,
354
354
  hasProject,
355
355
  attachProjectGroup,
@@ -14,7 +14,7 @@ export function packageInfoFromJson(packageJson) {
14
14
  !('version' in packageJson) ||
15
15
  typeof packageJson.name !== 'string' ||
16
16
  typeof packageJson.version !== 'string') {
17
- throw new Error('Invalid KTX CLI package metadata');
17
+ throw new Error('Invalid ktx CLI package metadata');
18
18
  }
19
19
  return {
20
20
  name: packageJson.name,
@@ -28,7 +28,7 @@ async function runInit(args, io) {
28
28
  projectDir: args.projectDir,
29
29
  force: args.force,
30
30
  });
31
- io.stdout.write(`Initialized KTX project at ${result.projectDir}\n`);
31
+ io.stdout.write(`Initialized ktx project at ${result.projectDir}\n`);
32
32
  io.stdout.write(`Config: ${result.configPath}\n`);
33
33
  io.stdout.write(`Commit: ${result.commitHash ?? 'none'}\n`);
34
34
  return 0;
@@ -27,7 +27,7 @@ export function registerConnectionCommands(program, context, commandName = 'conn
27
27
  connection
28
28
  .command('test')
29
29
  .description('Test one or all configured connections (default: all)')
30
- .argument('[connectionId]', 'KTX connection id to test (omit to test all)')
30
+ .argument('[connectionId]', 'ktx connection id to test (omit to test all)')
31
31
  .option('--all', 'Test every configured connection and print a summary list')
32
32
  .action(async (connectionId, options, command) => {
33
33
  if (options.all === true && connectionId !== undefined) {
@@ -7,16 +7,16 @@ profileMark('module:commands/ingest-commands');
7
7
  export function registerIngestCommands(program, context, commandOptions) {
8
8
  const ingest = program
9
9
  .command('ingest')
10
- .description('Build or inspect KTX context, or capture text into memory')
10
+ .description('Build or inspect ktx context, or capture text into memory')
11
11
  .usage('[options] [connectionId]')
12
12
  .argument('[connectionId]', 'Configured connection id to ingest (omit to ingest all)')
13
13
  .option('--all', 'Ingest all configured connections', false)
14
14
  .addOption(new Option('--query-history', 'Include database query-history usage patterns').conflicts('noQueryHistory'))
15
15
  .addOption(new Option('--no-query-history', 'Skip database query-history usage patterns'))
16
16
  .option('--query-history-window-days <days>', 'Query-history lookback window for this run', parsePositiveIntegerOption)
17
- .option('--text <content>', 'Capture inline text into KTX memory; repeatable', collectOption, [])
18
- .option('--file <path>', 'Capture a text file into KTX memory; use - for stdin; repeatable', collectOption, [])
19
- .option('--connection-id <connectionId>', 'KTX connection id to tag captured text/file notes')
17
+ .option('--text <content>', 'Capture inline text into ktx memory; repeatable', collectOption, [])
18
+ .option('--file <path>', 'Capture a text file into ktx memory; use - for stdin; repeatable', collectOption, [])
19
+ .option('--connection-id <connectionId>', 'ktx connection id to tag captured text/file notes')
20
20
  .option('--user-id <id>', 'Memory user id for text/file capture attribution', 'local-cli')
21
21
  .option('--fail-fast', 'Stop after the first failed text/file item', false)
22
22
  .addOption(new Option('--plain', 'Print plain text output').conflicts(['json']))
@@ -13,11 +13,11 @@ function binPath() {
13
13
  }
14
14
  function formatMcpStartResultMessage(input) {
15
15
  return [
16
- input.status === 'started' ? `KTX MCP daemon started: ${input.url}` : `KTX MCP daemon already running: ${input.url}`,
16
+ input.status === 'started' ? `ktx MCP daemon started: ${input.url}` : `ktx MCP daemon already running: ${input.url}`,
17
17
  '',
18
- 'KTX is ready for configured agents.',
19
- 'Open your agent for this KTX project and ask a data question, for example:',
20
- ' "Use KTX to show me the available tables and metrics."',
18
+ 'ktx is ready for configured agents.',
19
+ 'Open your agent for this ktx project and ask a data question, for example:',
20
+ ' "Use ktx to show me the available tables and metrics."',
21
21
  '',
22
22
  ].join('\n');
23
23
  }
@@ -34,13 +34,13 @@ async function printMcpStatus(context, projectDir) {
34
34
  export function registerMcpCommands(program, context) {
35
35
  const mcp = program
36
36
  .command('mcp')
37
- .description('Manage the KTX MCP HTTP server (bare command: show status)')
37
+ .description('Manage the ktx MCP HTTP server (bare command: show status)')
38
38
  .action(async (_options, command) => {
39
39
  await printMcpStatus(context, resolveCommandProjectDir(command));
40
40
  });
41
41
  mcp
42
42
  .command('stdio')
43
- .description('Run the KTX MCP server over stdio')
43
+ .description('Run the ktx MCP server over stdio')
44
44
  .action(async (_options, command) => {
45
45
  await (context.deps.mcp?.runStdioServer ?? runKtxMcpStdioServer)({
46
46
  projectDir: resolveCommandProjectDir(command),
@@ -50,7 +50,7 @@ export function registerMcpCommands(program, context) {
50
50
  });
51
51
  mcp
52
52
  .command('start')
53
- .description('Start the KTX MCP HTTP server')
53
+ .description('Start the ktx MCP HTTP server')
54
54
  .option('--host <host>', 'Host to bind', '127.0.0.1')
55
55
  .option('--port <n>', 'Port to bind', parsePositiveIntegerOption, 7878)
56
56
  .option('--token <token>', 'Bearer token required for non-loopback binding')
@@ -78,7 +78,7 @@ export function registerMcpCommands(program, context) {
78
78
  allowedOrigins: options.allowedOrigin,
79
79
  io: context.io,
80
80
  });
81
- context.io.stdout.write(`KTX MCP server listening at http://${options.host}:${options.port}/mcp\n`);
81
+ context.io.stdout.write(`ktx MCP server listening at http://${options.host}:${options.port}/mcp\n`);
82
82
  return;
83
83
  }
84
84
  const result = await (context.deps.mcp?.startDaemon ?? startKtxMcpDaemon)({
@@ -95,22 +95,22 @@ export function registerMcpCommands(program, context) {
95
95
  });
96
96
  mcp
97
97
  .command('stop')
98
- .description('Stop the KTX MCP daemon')
98
+ .description('Stop the ktx MCP daemon')
99
99
  .action(async (_options, command) => {
100
100
  const result = await (context.deps.mcp?.stopDaemon ?? stopKtxMcpDaemon)({
101
101
  projectDir: resolveCommandProjectDir(command),
102
102
  });
103
- context.io.stdout.write(result.status === 'stopped' ? 'KTX MCP daemon stopped.\n' : 'KTX MCP daemon is not running.\n');
103
+ context.io.stdout.write(result.status === 'stopped' ? 'ktx MCP daemon stopped.\n' : 'ktx MCP daemon is not running.\n');
104
104
  });
105
105
  mcp
106
106
  .command('status')
107
- .description('Show KTX MCP daemon status')
107
+ .description('Show ktx MCP daemon status')
108
108
  .action(async (_options, command) => {
109
109
  await printMcpStatus(context, resolveCommandProjectDir(command));
110
110
  });
111
111
  mcp
112
112
  .command('logs')
113
- .description('Print the KTX MCP daemon log')
113
+ .description('Print the ktx MCP daemon log')
114
114
  .option('--follow', 'Follow log output', false)
115
115
  .action(async (options, command) => {
116
116
  const logPath = mcpDaemonLayout(resolveCommandProjectDir(command)).logPath;
@@ -12,7 +12,7 @@ async function runRuntimeArgs(context, args) {
12
12
  export function registerRuntimeCommands(program, context) {
13
13
  const runtime = program
14
14
  .command('runtime')
15
- .description('Install, start, stop, and inspect the KTX-managed Python runtime')
15
+ .description('Install, start, stop, and inspect the ktx-managed Python runtime')
16
16
  .showHelpAfterError();
17
17
  runtime
18
18
  .command('install')
@@ -30,7 +30,7 @@ export function registerRuntimeCommands(program, context) {
30
30
  });
31
31
  runtime
32
32
  .command('start')
33
- .description('Start the KTX daemon')
33
+ .description('Start the ktx daemon')
34
34
  .addOption(createRuntimeFeatureOption())
35
35
  .option('--force', 'Restart even when a matching daemon is already running', false)
36
36
  .action(async (options, command) => {
@@ -44,8 +44,8 @@ export function registerRuntimeCommands(program, context) {
44
44
  });
45
45
  runtime
46
46
  .command('stop')
47
- .description('Stop the KTX daemon')
48
- .option('--all', 'Stop all KTX daemon processes recorded or discoverable on this machine', false)
47
+ .description('Stop the ktx daemon')
48
+ .option('--all', 'Stop all ktx daemon processes recorded or discoverable on this machine', false)
49
49
  .action(async (options, command) => {
50
50
  await runRuntimeArgs(context, {
51
51
  command: 'stop',
@@ -1,5 +1,6 @@
1
1
  import { InvalidArgumentError, Option } from '@commander-js/extra-typings';
2
2
  import { resolveCommandProjectDir } from '../cli-program.js';
3
+ import { isKtxSetupLlmBackend } from '../setup-models.js';
3
4
  async function runSetupArgs(context, args) {
4
5
  const runner = context.deps.setup ?? (await import('../setup.js')).runKtxSetup;
5
6
  context.setExitCode(await runner(args, context.io));
@@ -7,7 +8,7 @@ async function runSetupArgs(context, args) {
7
8
  function positiveInteger(value) {
8
9
  const parsed = Number.parseInt(value, 10);
9
10
  if (!Number.isInteger(parsed) || parsed <= 0) {
10
- throw new Error(`Expected a positive integer, received ${value}`);
11
+ throw new InvalidArgumentError(`Expected a positive integer, received ${value}`);
11
12
  }
12
13
  return parsed;
13
14
  }
@@ -18,7 +19,7 @@ function embeddingBackend(value) {
18
19
  throw new InvalidArgumentError(`invalid choice '${value}'`);
19
20
  }
20
21
  function llmBackend(value) {
21
- if (value === 'anthropic' || value === 'vertex' || value === 'claude-code' || value === 'codex') {
22
+ if (isKtxSetupLlmBackend(value)) {
22
23
  return value;
23
24
  }
24
25
  throw new InvalidArgumentError(`invalid choice '${value}'`);
@@ -125,8 +126,8 @@ function shouldShowSetupEntryMenu(options, command) {
125
126
  export function registerSetupCommands(program, context) {
126
127
  const setup = program
127
128
  .command('setup')
128
- .description('Set up or resume a local KTX project')
129
- .addOption(new Option('--project-dir <path>', 'KTX project directory').hideHelp())
129
+ .description('Set up or resume a local ktx project')
130
+ .addOption(new Option('--project-dir <path>', 'ktx project directory').hideHelp())
130
131
  .option('--agents', 'Install agent integration only', false)
131
132
  .addOption(new Option('--target <target>', 'Agent target').choices([
132
133
  'claude-code',
@@ -184,7 +185,7 @@ export function registerSetupCommands(program, context) {
184
185
  .argParser((value, previous) => [...previous, value])
185
186
  .default([])
186
187
  .hideHelp())
187
- .addOption(new Option('--skip-databases', 'Leave database setup incomplete; KTX cannot work until a database is added')
188
+ .addOption(new Option('--skip-databases', 'Leave database setup incomplete; ktx cannot work until a database is added')
188
189
  .hideHelp()
189
190
  .default(false))
190
191
  .addOption(new Option('--source <type>', 'Source connector type').argParser(sourceType).hideHelp())
@@ -30,7 +30,7 @@ export function registerSlCommands(program, context, commandName = 'sl') {
30
30
  .description('List, search, validate, or query local semantic-layer sources')
31
31
  .usage('[options] [query...]')
32
32
  .argument('[query...]', 'Search query; omit to list all sources')
33
- .option('--connection-id <id>', 'KTX connection id')
33
+ .option('--connection-id <id>', 'ktx connection id')
34
34
  .option('--limit <number>', 'Maximum search results (search mode only)', parsePositiveIntegerOption)
35
35
  .addOption(new Option('--output <mode>', 'Output mode: pretty (default in TTY), plain (TSV), or json').choices([
36
36
  'pretty',
@@ -20,7 +20,7 @@ export function registerSqlCommands(program, context) {
20
20
  .command('sql')
21
21
  .description('Execute parser-validated read-only SQL against a configured connection')
22
22
  .argument('<sql...>', 'SQL query to execute')
23
- .requiredOption('-c, --connection <id>', 'KTX connection id')
23
+ .requiredOption('-c, --connection <id>', 'ktx connection id')
24
24
  .option('--max-rows <n>', 'Maximum rows to return', parseSqlMaxRowsOption, DEFAULT_MAX_ROWS)
25
25
  .addOption(new Option('--output <mode>', 'Output mode: pretty (default), plain (TSV), or json').choices([
26
26
  'pretty',
@@ -10,7 +10,7 @@ function inputMode(options) {
10
10
  export function registerStatusCommands(program, context) {
11
11
  program
12
12
  .command('status')
13
- .description('Check current KTX setup and project readiness')
13
+ .description('Check current ktx setup and project readiness')
14
14
  .option('--json', 'Print JSON output', false)
15
15
  .option('-v, --verbose', 'Show every check, including passing ones', false)
16
16
  .option('--validate', 'Only validate the ktx.yaml schema; skip readiness checks', false)
@@ -86,7 +86,7 @@ async function testMetabaseConnection(project, connectionId, createClient) {
86
86
  }
87
87
  async function createDefaultLookerClient(project, connectionId) {
88
88
  const factory = new DefaultLookerConnectionClientFactory(createLocalLookerCredentialResolver(project));
89
- return (await factory.createClient(connectionId));
89
+ return factory.createLookerClient(connectionId);
90
90
  }
91
91
  async function testLookerConnection(project, connectionId, createClient) {
92
92
  const client = await createClient(project, connectionId);
@@ -432,7 +432,7 @@ export class KtxClickHouseScanConnector {
432
432
  }
433
433
  assertConnection(connectionId) {
434
434
  if (connectionId !== this.connectionId) {
435
- throw new Error(`KTX ClickHouse connector ${this.id} cannot serve connection ${connectionId}`);
435
+ throw new Error(`ktx ClickHouse connector ${this.id} cannot serve connection ${connectionId}`);
436
436
  }
437
437
  }
438
438
  }
@@ -508,7 +508,7 @@ export class KtxMysqlScanConnector {
508
508
  }
509
509
  assertConnection(connectionId) {
510
510
  if (connectionId !== this.connectionId) {
511
- throw new Error(`KTX MySQL connector ${this.id} cannot serve connection ${connectionId}`);
511
+ throw new Error(`ktx MySQL connector ${this.id} cannot serve connection ${connectionId}`);
512
512
  }
513
513
  }
514
514
  }
@@ -72,7 +72,7 @@ export interface KtxSnowflakeScanConnectorOptions {
72
72
  connectionId: string;
73
73
  connection: KtxSnowflakeConnectionConfig | undefined;
74
74
  /**
75
- * KTX project directory. When provided, snowflake-sdk's logger is redirected to
75
+ * ktx project directory. When provided, snowflake-sdk's logger is redirected to
76
76
  * `<projectDir>/.ktx/logs/snowflake.log` so its JSON output does not bleed into
77
77
  * the CLI's TTY. Tests that use a fake driverFactory can leave this undefined.
78
78
  */
@@ -35,29 +35,6 @@ function sqlitePathFromUrl(url) {
35
35
  }
36
36
  return url;
37
37
  }
38
- function stripLeadingSqlComments(sql) {
39
- let index = 0;
40
- while (index < sql.length) {
41
- while (/\s/.test(sql[index] ?? '')) {
42
- index += 1;
43
- }
44
- if (sql.startsWith('--', index)) {
45
- const end = sql.indexOf('\n', index + 2);
46
- index = end === -1 ? sql.length : end + 1;
47
- continue;
48
- }
49
- if (sql.startsWith('/*', index)) {
50
- const end = sql.indexOf('*/', index + 2);
51
- if (end === -1) {
52
- return sql.slice(index);
53
- }
54
- index = end + 2;
55
- continue;
56
- }
57
- break;
58
- }
59
- return sql.slice(index);
60
- }
61
38
  export function isKtxSqliteConnectionConfig(connection) {
62
39
  const driver = String(connection?.driver ?? '').toLowerCase();
63
40
  return driver === 'sqlite';
@@ -171,7 +148,7 @@ export class KtxSqliteScanConnector {
171
148
  }
172
149
  async executeReadOnly(input, _ctx) {
173
150
  this.assertConnection(input.connectionId);
174
- const result = this.query(limitSqlForExecution(stripLeadingSqlComments(input.sql), input.maxRows), input.params);
151
+ const result = this.query(limitSqlForExecution(input.sql, input.maxRows), input.params);
175
152
  return { ...result, rowCount: result.rows.length };
176
153
  }
177
154
  async getColumnDistinctValues(table, columnName, options) {
@@ -274,7 +251,7 @@ export class KtxSqliteScanConnector {
274
251
  }
275
252
  assertConnection(connectionId) {
276
253
  if (connectionId !== this.connectionId) {
277
- throw new Error(`KTX SQLite connector ${this.id} cannot serve connection ${connectionId}`);
254
+ throw new Error(`ktx SQLite connector ${this.id} cannot serve connection ${connectionId}`);
278
255
  }
279
256
  }
280
257
  }
@@ -1,4 +1,4 @@
1
- import { assertReadOnlySql } from '../../context/connections/read-only-sql.js';
1
+ import { assertReadOnlySql, stripTrailingSqlNoise } from '../../context/connections/read-only-sql.js';
2
2
  import { getDialectForDriver } from '../../context/connections/dialects.js';
3
3
  import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js';
4
4
  import { scopedTableNames } from '../../context/scan/table-ref.js';
@@ -142,7 +142,7 @@ function isDeniedError(error) {
142
142
  return number === 229 || number === 230 || number === 297;
143
143
  }
144
144
  function limitSqlForSqlServerExecution(sqlText, maxRows) {
145
- const trimmed = assertReadOnlySql(sqlText).replace(/;+\s*$/, '');
145
+ const trimmed = stripTrailingSqlNoise(assertReadOnlySql(sqlText));
146
146
  if (!maxRows) {
147
147
  return trimmed;
148
148
  }
@@ -575,7 +575,7 @@ export class KtxSqlServerScanConnector {
575
575
  }
576
576
  assertConnection(connectionId) {
577
577
  if (connectionId !== this.connectionId) {
578
- throw new Error(`KTX SQL Server connector ${this.id} cannot serve connection ${connectionId}`);
578
+ throw new Error(`ktx SQL Server connector ${this.id} cannot serve connection ${connectionId}`);
579
579
  }
580
580
  }
581
581
  }
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  export declare const connectionTypeSchema: z.ZodEnum<{
3
+ PLAIN: "PLAIN";
3
4
  BIGQUERY: "BIGQUERY";
4
5
  SNOWFLAKE: "SNOWFLAKE";
5
6
  MYSQL: "MYSQL";
@@ -19,7 +20,6 @@ export declare const connectionTypeSchema: z.ZodEnum<{
19
20
  METABASE: "METABASE";
20
21
  LOOKER: "LOOKER";
21
22
  NOTION: "NOTION";
22
- PLAIN: "PLAIN";
23
23
  BETTERSTACK: "BETTERSTACK";
24
24
  }>;
25
25
  export type ConnectionType = z.infer<typeof connectionTypeSchema>;
@@ -1,2 +1,3 @@
1
1
  export declare function assertReadOnlySql(sql: string): string;
2
+ export declare function stripTrailingSqlNoise(sql: string): string;
2
3
  export declare function limitSqlForExecution(sql: string, maxRows: number | undefined): string;
@@ -1,14 +1,128 @@
1
1
  const MUTATING_SQL = /^\s*(insert|update|delete|merge|alter|drop|create|truncate|grant|revoke|copy|call|do|vacuum|analyze|refresh)\b/i;
2
2
  const READ_SQL = /^\s*(select|with)\b/i;
3
+ // Agents (and the daemon's sqlglot validator, which ignores comments) routinely
4
+ // emit read-only queries prefixed with `-- ...` or `/* ... */`. Strip leading
5
+ // comments so the prefix check sees the real statement; otherwise valid SELECT/WITH
6
+ // SQL is rejected here while the parser-backed validator accepts it.
7
+ function stripLeadingSqlComments(sql) {
8
+ let index = 0;
9
+ while (index < sql.length) {
10
+ while (/\s/.test(sql[index] ?? '')) {
11
+ index += 1;
12
+ }
13
+ if (sql.startsWith('--', index)) {
14
+ const end = sql.indexOf('\n', index + 2);
15
+ index = end === -1 ? sql.length : end + 1;
16
+ continue;
17
+ }
18
+ if (sql.startsWith('/*', index)) {
19
+ const end = sql.indexOf('*/', index + 2);
20
+ if (end === -1) {
21
+ return sql.slice(index);
22
+ }
23
+ index = end + 2;
24
+ continue;
25
+ }
26
+ break;
27
+ }
28
+ return sql.slice(index);
29
+ }
30
+ // Lexes past one string literal, quoted identifier, or comment starting at
31
+ // `index`, using standard-SQL rules ('' and "" escapes; no dialect extensions
32
+ // such as backslash escapes or dollar quoting). Returns the index after the
33
+ // token, or `index` unchanged when no quoted/comment token starts there.
34
+ function skipQuotedOrComment(sql, index) {
35
+ const quote = sql[index];
36
+ if (quote === "'" || quote === '"') {
37
+ let i = index + 1;
38
+ while (i < sql.length) {
39
+ if (sql[i] === quote) {
40
+ if (sql[i + 1] === quote) {
41
+ i += 2;
42
+ continue;
43
+ }
44
+ return i + 1;
45
+ }
46
+ i += 1;
47
+ }
48
+ return sql.length;
49
+ }
50
+ if (sql.startsWith('--', index)) {
51
+ const end = sql.indexOf('\n', index + 2);
52
+ return end === -1 ? sql.length : end + 1;
53
+ }
54
+ if (sql.startsWith('/*', index)) {
55
+ const end = sql.indexOf('*/', index + 2);
56
+ return end === -1 ? sql.length : end + 2;
57
+ }
58
+ return index;
59
+ }
60
+ // Backstop against statement smuggling (`select 1; drop table x`): reject any
61
+ // semicolon that is followed by real content. Semicolons inside string
62
+ // literals, quoted identifiers, and comments are fine, as are trailing
63
+ // semicolons (optionally followed by whitespace and comments). This deliberately
64
+ // lexes standard SQL only, so dialect-specific escapes can cause a false
65
+ // reject — never a false accept; the canonical gate is the daemon's
66
+ // sqlglot-backed validateReadOnly.
67
+ function assertSingleSqlStatement(sql) {
68
+ let index = 0;
69
+ let sawSemicolon = false;
70
+ while (index < sql.length) {
71
+ const skipped = skipQuotedOrComment(sql, index);
72
+ if (skipped > index) {
73
+ index = skipped;
74
+ continue;
75
+ }
76
+ if (sql[index] === ';') {
77
+ sawSemicolon = true;
78
+ }
79
+ else if (sawSemicolon && !/\s/.test(sql[index])) {
80
+ throw new Error('Only one SQL statement can be executed.');
81
+ }
82
+ index += 1;
83
+ }
84
+ }
3
85
  export function assertReadOnlySql(sql) {
4
- const trimmed = sql.trim();
86
+ const trimmed = stripLeadingSqlComments(sql).trim();
5
87
  if (!READ_SQL.test(trimmed) || MUTATING_SQL.test(trimmed)) {
6
88
  throw new Error('Only read-only SELECT/WITH queries can be executed locally.');
7
89
  }
90
+ assertSingleSqlStatement(trimmed);
8
91
  return trimmed;
9
92
  }
93
+ // `assertReadOnlySql` deliberately keeps trailing semicolons, comments, and
94
+ // whitespace (e.g. `select 1; -- done`) — harmless for direct single-statement
95
+ // execution. A row-limit subquery wrapper needs a bare expression instead: a
96
+ // trailing `;` would sit illegally inside the subquery, and a trailing line
97
+ // comment would comment out the closing paren and limit clause. Lex forward with
98
+ // the same standard-SQL rules as the single-statement gate and truncate at the
99
+ // end of the last meaningful token, dropping trailing semicolons, comments, and
100
+ // whitespace. Characters inside string literals and quoted identifiers stay
101
+ // meaningful, so a `;` or `--` within a literal is never mistaken for a
102
+ // terminator (a plain regex cannot make that distinction).
103
+ export function stripTrailingSqlNoise(sql) {
104
+ let index = 0;
105
+ let meaningfulEnd = 0;
106
+ while (index < sql.length) {
107
+ if (sql.startsWith('--', index) || sql.startsWith('/*', index)) {
108
+ index = skipQuotedOrComment(sql, index);
109
+ continue;
110
+ }
111
+ const afterQuoted = skipQuotedOrComment(sql, index);
112
+ if (afterQuoted > index) {
113
+ meaningfulEnd = afterQuoted;
114
+ index = afterQuoted;
115
+ continue;
116
+ }
117
+ if (sql[index] !== ';' && !/\s/.test(sql[index] ?? '')) {
118
+ meaningfulEnd = index + 1;
119
+ }
120
+ index += 1;
121
+ }
122
+ return sql.slice(0, meaningfulEnd);
123
+ }
10
124
  export function limitSqlForExecution(sql, maxRows) {
11
- const trimmed = assertReadOnlySql(sql).replace(/;+\s*$/, '');
125
+ const trimmed = stripTrailingSqlNoise(assertReadOnlySql(sql));
12
126
  if (!maxRows) {
13
127
  return trimmed;
14
128
  }