@kaelio/ktx 0.11.0 → 0.13.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 (212) hide show
  1. package/assets/python/kaelio_ktx-0.13.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 +19 -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 +15 -3
  19. package/dist/connectors/bigquery/connector.js +1 -14
  20. package/dist/connectors/clickhouse/connector.js +2 -16
  21. package/dist/connectors/duckdb/federated-attach.d.ts +7 -0
  22. package/dist/connectors/duckdb/federated-attach.js +86 -0
  23. package/dist/connectors/duckdb/federated-executor.d.ts +5 -0
  24. package/dist/connectors/duckdb/federated-executor.js +59 -0
  25. package/dist/connectors/mysql/connector.js +2 -16
  26. package/dist/connectors/postgres/connector.js +1 -14
  27. package/dist/connectors/shared/string-reference.d.ts +6 -0
  28. package/dist/connectors/shared/string-reference.js +19 -0
  29. package/dist/connectors/snowflake/connector.d.ts +1 -1
  30. package/dist/connectors/snowflake/connector.js +1 -14
  31. package/dist/connectors/sqlite/connector.js +2 -25
  32. package/dist/connectors/sqlserver/connector.js +4 -17
  33. package/dist/context/connections/connection-type.d.ts +1 -1
  34. package/dist/context/connections/federation.d.ts +33 -0
  35. package/dist/context/connections/federation.js +51 -0
  36. package/dist/context/connections/local-warehouse-descriptor.d.ts +2 -0
  37. package/dist/context/connections/project-sql-executor.d.ts +18 -0
  38. package/dist/context/connections/project-sql-executor.js +39 -0
  39. package/dist/context/connections/query-executor.d.ts +2 -2
  40. package/dist/context/connections/read-only-sql.d.ts +1 -0
  41. package/dist/context/connections/read-only-sql.js +119 -4
  42. package/dist/context/connections/resolve-connection.d.ts +12 -0
  43. package/dist/context/connections/resolve-connection.js +37 -0
  44. package/dist/context/core/git-env.d.ts +4 -0
  45. package/dist/context/core/git-env.js +5 -1
  46. package/dist/context/core/git.service.d.ts +23 -0
  47. package/dist/context/core/git.service.js +71 -8
  48. package/dist/context/ingest/adapters/historic-sql/projection.js +2 -1
  49. package/dist/context/ingest/adapters/live-database/manifest.d.ts +3 -0
  50. package/dist/context/ingest/adapters/live-database/manifest.js +19 -11
  51. package/dist/context/ingest/adapters/looker/client.js +7 -2
  52. package/dist/context/ingest/adapters/looker/factory.d.ts +8 -1
  53. package/dist/context/ingest/adapters/looker/factory.js +9 -0
  54. package/dist/context/ingest/adapters/looker/mapping.js +1 -1
  55. package/dist/context/ingest/adapters/looker/types.d.ts +1 -1
  56. package/dist/context/ingest/adapters/metabase/client.d.ts +1 -1
  57. package/dist/context/ingest/adapters/metabase/client.js +1 -1
  58. package/dist/context/ingest/adapters/metabase/local-metabase.adapter.js +1 -1
  59. package/dist/context/ingest/adapters/metabase/mapping.js +6 -6
  60. package/dist/context/ingest/artifact-gates.d.ts +2 -6
  61. package/dist/context/ingest/artifact-gates.js +5 -47
  62. package/dist/context/ingest/constrained-repair.d.ts +55 -0
  63. package/dist/context/ingest/constrained-repair.js +167 -0
  64. package/dist/context/ingest/final-gate-repair.d.ts +9 -11
  65. package/dist/context/ingest/final-gate-repair.js +40 -128
  66. package/dist/context/ingest/finalization-scope.d.ts +1 -1
  67. package/dist/context/ingest/finalization-scope.js +15 -15
  68. package/dist/context/ingest/ingest-bundle.runner.d.ts +1 -0
  69. package/dist/context/ingest/ingest-bundle.runner.js +101 -67
  70. package/dist/context/ingest/isolated-diff/patch-integrator.d.ts +6 -13
  71. package/dist/context/ingest/isolated-diff/patch-integrator.js +32 -109
  72. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +8 -9
  73. package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +63 -141
  74. package/dist/context/ingest/local-bundle-runtime.d.ts +2 -0
  75. package/dist/context/ingest/local-bundle-runtime.js +9 -10
  76. package/dist/context/ingest/local-ingest.d.ts +2 -0
  77. package/dist/context/ingest/local-ingest.js +2 -0
  78. package/dist/context/ingest/memory-flow/view-model.js +1 -1
  79. package/dist/context/ingest/stages/stage-3-work-units.d.ts +2 -6
  80. package/dist/context/ingest/stages/stage-3-work-units.js +2 -1
  81. package/dist/context/ingest/stages/validate-wu-sources.d.ts +7 -1
  82. package/dist/context/ingest/stages/validate-wu-sources.js +109 -4
  83. package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.d.ts +2 -0
  84. package/dist/context/ingest/tools/warehouse-verification/create-warehouse-verification-tools.js +1 -1
  85. package/dist/context/ingest/tools/warehouse-verification/discover-data.tool.js +3 -3
  86. package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.d.ts +3 -1
  87. package/dist/context/ingest/tools/warehouse-verification/sql-execution.tool.js +15 -1
  88. package/dist/context/llm/ai-sdk-runtime.js +2 -2
  89. package/dist/context/llm/claude-code-runtime.js +19 -3
  90. package/dist/context/llm/local-config.js +1 -1
  91. package/dist/context/llm/runtime-tools.js +2 -2
  92. package/dist/context/mcp/context-tools.js +33 -8
  93. package/dist/context/mcp/local-project-ports.js +63 -89
  94. package/dist/context/mcp/types.d.ts +2 -0
  95. package/dist/context/memory/local-memory.js +4 -1
  96. package/dist/context/memory/memory-agent.service.js +1 -1
  97. package/dist/context/project/config.d.ts +11 -4
  98. package/dist/context/project/config.js +85 -30
  99. package/dist/context/project/driver-schemas.js +1 -1
  100. package/dist/context/project/mappings-yaml-schema.js +2 -2
  101. package/dist/context/project/project.js +12 -4
  102. package/dist/context/scan/description-generation.js +4 -4
  103. package/dist/context/scan/local-enrichment-artifacts.js +33 -4
  104. package/dist/context/scan/local-scan.js +2 -2
  105. package/dist/context/scan/local-structural-artifacts.js +5 -5
  106. package/dist/context/scan/relationship-benchmark-report.js +1 -1
  107. package/dist/context/scan/relationship-discovery.js +3 -3
  108. package/dist/context/scan/relationship-llm-proposal.js +3 -3
  109. package/dist/context/sl/local-query.js +31 -44
  110. package/dist/context/sl/local-sl.d.ts +0 -8
  111. package/dist/context/sl/local-sl.js +71 -70
  112. package/dist/context/sl/semantic-layer.service.d.ts +25 -8
  113. package/dist/context/sl/semantic-layer.service.js +109 -56
  114. package/dist/context/sl/source-files.d.ts +48 -0
  115. package/dist/context/sl/source-files.js +138 -0
  116. package/dist/context/sl/tools/base-semantic-layer.tool.d.ts +2 -2
  117. package/dist/context/sl/tools/base-semantic-layer.tool.js +2 -7
  118. package/dist/context/sl/tools/sl-edit-source.tool.js +10 -8
  119. package/dist/context/sl/tools/sl-warehouse-validation.js +55 -27
  120. package/dist/context/sl/tools/sl-write-source.tool.js +12 -9
  121. package/dist/context/sql-analysis/dialect.d.ts +2 -0
  122. package/dist/context/sql-analysis/dialect.js +20 -0
  123. package/dist/context/tools/base-tool.d.ts +6 -19
  124. package/dist/context/tools/base-tool.js +0 -14
  125. package/dist/context-build-view.js +5 -5
  126. package/dist/database-tree-picker.js +18 -3
  127. package/dist/demo-assets.js +0 -1
  128. package/dist/doctor.d.ts +1 -1
  129. package/dist/doctor.js +31 -23
  130. package/dist/errors.d.ts +31 -0
  131. package/dist/errors.js +44 -0
  132. package/dist/ingest-query-executor.d.ts +2 -0
  133. package/dist/ingest-query-executor.js +8 -22
  134. package/dist/ingest.d.ts +1 -1
  135. package/dist/ingest.js +8 -2
  136. package/dist/io/symbols.d.ts +2 -0
  137. package/dist/io/symbols.js +2 -0
  138. package/dist/io/tty.d.ts +8 -0
  139. package/dist/io/tty.js +16 -0
  140. package/dist/llm/embedding-health.js +1 -1
  141. package/dist/llm/embedding-provider.js +3 -3
  142. package/dist/llm/model-provider.js +1 -1
  143. package/dist/local-adapters.d.ts +1 -0
  144. package/dist/local-adapters.js +2 -2
  145. package/dist/local-scan-connectors.js +1 -1
  146. package/dist/managed-local-embeddings.js +17 -8
  147. package/dist/managed-mcp-daemon.js +3 -3
  148. package/dist/managed-python-command.d.ts +7 -0
  149. package/dist/managed-python-command.js +34 -8
  150. package/dist/managed-python-daemon.js +2 -2
  151. package/dist/managed-python-http.js +3 -3
  152. package/dist/managed-python-runtime.d.ts +30 -1
  153. package/dist/managed-python-runtime.js +134 -18
  154. package/dist/managed-uv-release.d.ts +7 -0
  155. package/dist/managed-uv-release.js +11 -0
  156. package/dist/mcp-http-server.js +4 -4
  157. package/dist/mcp-server-factory.js +3 -3
  158. package/dist/mcp-stdio-server.js +1 -1
  159. package/dist/memory-flow-hud.js +2 -2
  160. package/dist/next-steps.js +2 -2
  161. package/dist/prompt-navigation.d.ts +17 -0
  162. package/dist/prompt-navigation.js +49 -3
  163. package/dist/prompts/memory_agent_bundle_ingest_work_unit.md +2 -2
  164. package/dist/prompts/memory_agent_external_ingest.md +2 -2
  165. package/dist/public-ingest-copy.js +1 -1
  166. package/dist/public-ingest.js +3 -3
  167. package/dist/release-version.js +1 -1
  168. package/dist/runtime-requirements.js +1 -1
  169. package/dist/runtime.js +9 -9
  170. package/dist/scan.js +1 -1
  171. package/dist/setup-agents.d.ts +21 -15
  172. package/dist/setup-agents.js +143 -66
  173. package/dist/setup-banner.d.ts +20 -0
  174. package/dist/setup-banner.js +39 -0
  175. package/dist/setup-context.js +24 -15
  176. package/dist/setup-databases.d.ts +3 -0
  177. package/dist/setup-databases.js +47 -59
  178. package/dist/setup-demo-tour.js +12 -8
  179. package/dist/setup-embeddings.js +9 -9
  180. package/dist/setup-interrupt.js +1 -1
  181. package/dist/setup-models.d.ts +4 -1
  182. package/dist/setup-models.js +54 -28
  183. package/dist/setup-project.js +29 -5
  184. package/dist/setup-prompts.js +16 -1
  185. package/dist/setup-ready-menu.js +1 -1
  186. package/dist/setup-sources.js +28 -12
  187. package/dist/setup.d.ts +1 -0
  188. package/dist/setup.js +14 -13
  189. package/dist/skills/analytics/SKILL.md +3 -3
  190. package/dist/skills/dbt_ingest/SKILL.md +3 -3
  191. package/dist/skills/looker_ingest/SKILL.md +3 -3
  192. package/dist/skills/lookml_ingest/SKILL.md +7 -7
  193. package/dist/skills/metabase_ingest/SKILL.md +4 -4
  194. package/dist/skills/metricflow_ingest/SKILL.md +15 -15
  195. package/dist/skills/notion_synthesize/SKILL.md +1 -1
  196. package/dist/skills/sl/SKILL.md +3 -3
  197. package/dist/skills/sl_capture/SKILL.md +1 -1
  198. package/dist/skills/wiki_capture/SKILL.md +1 -1
  199. package/dist/source-mapping.js +1 -1
  200. package/dist/sql.d.ts +2 -0
  201. package/dist/sql.js +35 -53
  202. package/dist/startup-profile.js +1 -1
  203. package/dist/status-project.d.ts +0 -2
  204. package/dist/status-project.js +4 -6
  205. package/dist/telemetry/events.d.ts +3 -2
  206. package/dist/telemetry/events.js +11 -1
  207. package/dist/telemetry/exception.js +14 -0
  208. package/dist/text-ingest.js +1 -1
  209. package/dist/tree-picker-tui.d.ts +0 -1
  210. package/dist/tree-picker-tui.js +2 -3
  211. package/package.json +2 -1
  212. package/assets/python/kaelio_ktx-0.11.0-py3-none-any.whl +0 -0
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}'`);
@@ -83,6 +84,7 @@ function shouldShowSetupEntryMenu(options, command) {
83
84
  'target',
84
85
  'global',
85
86
  'local',
87
+ 'installDir',
86
88
  'skipAgents',
87
89
  'yes',
88
90
  'input',
@@ -125,8 +127,8 @@ function shouldShowSetupEntryMenu(options, command) {
125
127
  export function registerSetupCommands(program, context) {
126
128
  const setup = program
127
129
  .command('setup')
128
- .description('Set up or resume a local KTX project')
129
- .addOption(new Option('--project-dir <path>', 'KTX project directory').hideHelp())
130
+ .description('Set up or resume a local ktx project')
131
+ .addOption(new Option('--project-dir <path>', 'ktx project directory').hideHelp())
130
132
  .option('--agents', 'Install agent integration only', false)
131
133
  .addOption(new Option('--target <target>', 'Agent target').choices([
132
134
  'claude-code',
@@ -138,6 +140,7 @@ export function registerSetupCommands(program, context) {
138
140
  ]))
139
141
  .option('--global', 'Install agent integration into the global target scope', false)
140
142
  .option('--local', 'Install Claude Code MCP config into the private per-project ~/.claude.json scope', false)
143
+ .option('--install-dir <path>', 'Directory to install project-scoped agent config into (defaults to the ktx project directory)')
141
144
  .addOption(new Option('--skip-agents', 'Leave agent integration incomplete for now').hideHelp().default(false))
142
145
  .option('--yes', 'Accept project creation and runtime install defaults where setup confirms', false)
143
146
  .option('--no-input', 'Disable interactive terminal input')
@@ -184,7 +187,7 @@ export function registerSetupCommands(program, context) {
184
187
  .argParser((value, previous) => [...previous, value])
185
188
  .default([])
186
189
  .hideHelp())
187
- .addOption(new Option('--skip-databases', 'Leave database setup incomplete; KTX cannot work until a database is added')
190
+ .addOption(new Option('--skip-databases', 'Leave database setup incomplete; ktx cannot work until a database is added')
188
191
  .hideHelp()
189
192
  .default(false))
190
193
  .addOption(new Option('--source <type>', 'Source connector type').argParser(sourceType).hideHelp())
@@ -263,6 +266,16 @@ export function registerSetupCommands(program, context) {
263
266
  context.setExitCode(1);
264
267
  return;
265
268
  }
269
+ if (options.installDir && (options.global || options.local)) {
270
+ context.io.stderr.write('Choose either --install-dir or a scope flag (--global / --local), not both.\n');
271
+ context.setExitCode(1);
272
+ return;
273
+ }
274
+ if (options.installDir && options.target === 'claude-desktop') {
275
+ context.io.stderr.write('--install-dir does not apply to --target claude-desktop, which is always global.\n');
276
+ context.setExitCode(1);
277
+ return;
278
+ }
266
279
  const creatingDatabaseConnection = options.database.length > 0 || options.databaseUrl !== undefined;
267
280
  if (creatingDatabaseConnection && options.databaseConnectionId.length > 1) {
268
281
  context.io.stderr.write('Choose only one new database connection id when configuring a database.\n');
@@ -278,6 +291,7 @@ export function registerSetupCommands(program, context) {
278
291
  agents: options.agents === true,
279
292
  ...(options.target ? { target: options.target } : {}),
280
293
  agentScope: resolvedAgentScope,
294
+ ...(options.installDir ? { installRoot: options.installDir } : {}),
281
295
  skipAgents: options.skipAgents === true,
282
296
  inputMode: options.input === false ? 'disabled' : 'auto',
283
297
  ...(debugEnabled ? { debug: true } : {}),
@@ -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)
@@ -4,6 +4,7 @@ import { NotionClient } from './context/ingest/adapters/notion/notion-client.js'
4
4
  import { createLocalLookerCredentialResolver } from './context/ingest/adapters/looker/local-looker.adapter.js';
5
5
  import { metabaseRuntimeConfigFromLocalConnection } from './context/ingest/adapters/metabase/local-metabase.adapter.js';
6
6
  import { testRepoConnection } from './context/ingest/repo-fetch.js';
7
+ import { federatedConnectionListing } from './context/connections/federation.js';
7
8
  import { getDriverRegistration } from './context/connections/drivers.js';
8
9
  import { parseNotionConnectionConfig, resolveNotionConnectionAuthToken } from './context/connections/notion-config.js';
9
10
  import { resolveKtxConfigReference } from './context/core/config-reference.js';
@@ -86,7 +87,7 @@ async function testMetabaseConnection(project, connectionId, createClient) {
86
87
  }
87
88
  async function createDefaultLookerClient(project, connectionId) {
88
89
  const factory = new DefaultLookerConnectionClientFactory(createLocalLookerCredentialResolver(project));
89
- return (await factory.createClient(connectionId));
90
+ return factory.createLookerClient(connectionId);
90
91
  }
91
92
  async function testLookerConnection(project, connectionId, createClient) {
92
93
  const client = await createClient(project, connectionId);
@@ -292,12 +293,23 @@ export async function runKtxConnection(args, io = process, deps = {}) {
292
293
  io.stdout.write('No connections configured. Run `ktx setup` to add one.\n');
293
294
  return 0;
294
295
  }
295
- const idWidth = Math.max('ID'.length, ...entries.map(([id]) => id.length));
296
- const driverWidth = Math.max('DRIVER'.length, ...entries.map(([, c]) => (c.driver ?? 'unknown').length));
296
+ const federated = federatedConnectionListing(project.config.connections, args.projectDir);
297
+ const idCandidates = [...entries.map(([id]) => id), ...(federated ? [federated.id] : [])];
298
+ const driverLengths = [
299
+ ...entries.map(([, c]) => (c.driver ?? 'unknown').length),
300
+ ...(federated ? [federated.driver.length] : []),
301
+ ];
302
+ const idWidth = Math.max('ID'.length, ...idCandidates.map((id) => id.length));
303
+ const driverWidth = Math.max('DRIVER'.length, ...driverLengths);
297
304
  io.stdout.write(`${'ID'.padEnd(idWidth)} ${'DRIVER'.padEnd(driverWidth)}\n`);
298
305
  for (const [id, connection] of entries) {
299
306
  io.stdout.write(`${id.padEnd(idWidth)} ${(connection.driver ?? 'unknown').padEnd(driverWidth)}\n`);
300
307
  }
308
+ if (federated) {
309
+ io.stdout.write(`${federated.id.padEnd(idWidth)} ${federated.driver.padEnd(driverWidth)}\n`);
310
+ io.stdout.write(` federates: ${federated.members.join(', ')}\n`);
311
+ io.stdout.write(` ${federated.hint}\n`);
312
+ }
301
313
  return 0;
302
314
  }
303
315
  if (args.command === 'test-all') {
@@ -5,9 +5,7 @@ import { assertReadOnlySql, limitSqlForExecution } from '../../context/connectio
5
5
  import { tryConstraintQuery } from '../../context/scan/constraint-discovery.js';
6
6
  import { scopedTableNames } from '../../context/scan/table-ref.js';
7
7
  import { connectorTestFailure, createKtxConnectorCapabilities, } from '../../context/scan/types.js';
8
- import { readFileSync } from 'node:fs';
9
- import { homedir } from 'node:os';
10
- import { resolve } from 'node:path';
8
+ import { resolveStringReference } from '../shared/string-reference.js';
11
9
  class DefaultBigQueryClientFactory {
12
10
  createClient(input) {
13
11
  const client = new BigQuery(input);
@@ -24,17 +22,6 @@ class DefaultBigQueryClientFactory {
24
22
  };
25
23
  }
26
24
  }
27
- function resolveStringReference(value, env) {
28
- if (value.startsWith('env:')) {
29
- return env[value.slice('env:'.length)] ?? '';
30
- }
31
- if (value.startsWith('file:')) {
32
- const rawPath = value.slice('file:'.length);
33
- const path = rawPath.startsWith('~') ? resolve(homedir(), rawPath.slice(1)) : rawPath;
34
- return readFileSync(path, 'utf-8').trim();
35
- }
36
- return value;
37
- }
38
25
  function stringConfigValue(connection, key, env) {
39
26
  const value = connection?.[key];
40
27
  return typeof value === 'string' && value.trim().length > 0 ? resolveStringReference(value.trim(), env) : undefined;
@@ -3,10 +3,8 @@ import { getDialectForDriver } from '../../context/connections/dialects.js';
3
3
  import { assertReadOnlySql, limitSqlForExecution } from '../../context/connections/read-only-sql.js';
4
4
  import { connectorTestFailure, createKtxConnectorCapabilities } from '../../context/scan/types.js';
5
5
  import { scopedTableNames } from '../../context/scan/table-ref.js';
6
- import { readFileSync } from 'node:fs';
6
+ import { resolveStringReference } from '../shared/string-reference.js';
7
7
  import { Agent as HttpsAgent } from 'node:https';
8
- import { homedir } from 'node:os';
9
- import { resolve } from 'node:path';
10
8
  class DefaultClickHouseClientFactory {
11
9
  createClient(config) {
12
10
  return createClient(config);
@@ -16,18 +14,6 @@ function stringConfigValue(connection, key, env) {
16
14
  const value = connection?.[key];
17
15
  return typeof value === 'string' && value.trim().length > 0 ? resolveStringReference(value.trim(), env) : undefined;
18
16
  }
19
- function resolveStringReference(value, env) {
20
- if (value.startsWith('env:')) {
21
- const envName = value.slice('env:'.length);
22
- return env[envName] ?? '';
23
- }
24
- if (value.startsWith('file:')) {
25
- const rawPath = value.slice('file:'.length);
26
- const path = rawPath.startsWith('~') ? resolve(homedir(), rawPath.slice(1)) : rawPath;
27
- return readFileSync(path, 'utf-8').trim();
28
- }
29
- return value;
30
- }
31
17
  function maybeNumber(value) {
32
18
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
33
19
  }
@@ -432,7 +418,7 @@ export class KtxClickHouseScanConnector {
432
418
  }
433
419
  assertConnection(connectionId) {
434
420
  if (connectionId !== this.connectionId) {
435
- throw new Error(`KTX ClickHouse connector ${this.id} cannot serve connection ${connectionId}`);
421
+ throw new Error(`ktx ClickHouse connector ${this.id} cannot serve connection ${connectionId}`);
436
422
  }
437
423
  }
438
424
  }
@@ -0,0 +1,7 @@
1
+ import type { FederatedMember } from '../../context/connections/federation.js';
2
+ /**
3
+ * Resolves a federated member's ktx.yaml config into the connection target
4
+ * DuckDB's ATTACH wants for that driver, reusing each connector's canonical
5
+ * resolver so federation and standalone scans agree on config interpretation.
6
+ */
7
+ export declare function federatedAttachTarget(member: FederatedMember, env: NodeJS.ProcessEnv): string;
@@ -0,0 +1,86 @@
1
+ import { sqliteDatabasePathFromConfig } from '../sqlite/connector.js';
2
+ import { postgresPoolConfigFromConfig } from '../postgres/connector.js';
3
+ import { mysqlConnectionPoolConfigFromConfig, } from '../mysql/connector.js';
4
+ function kvKeyword(value) {
5
+ // libpq/DuckDB key-value values quote with single quotes and backslash-escape.
6
+ return /[\s'\\]/.test(value) ? `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'` : value;
7
+ }
8
+ function withRequiredSslMode(connectionString) {
9
+ // DuckDB passes this libpq URL straight to the server, so an ssl:true member
10
+ // must carry sslmode in the URL itself; keep a stronger mode the URL already pins.
11
+ const url = new URL(connectionString);
12
+ if (url.searchParams.has('sslmode')) {
13
+ return connectionString;
14
+ }
15
+ url.searchParams.set('sslmode', 'require');
16
+ return url.toString();
17
+ }
18
+ function postgresAttachString(member, env) {
19
+ const cfg = postgresPoolConfigFromConfig({
20
+ connectionId: member.connectionId,
21
+ connection: member.connection,
22
+ env,
23
+ });
24
+ if (cfg.connectionString) {
25
+ return cfg.ssl ? withRequiredSslMode(cfg.connectionString) : cfg.connectionString;
26
+ }
27
+ const parts = [];
28
+ if (cfg.host)
29
+ parts.push(`host=${kvKeyword(cfg.host)}`);
30
+ if (cfg.port)
31
+ parts.push(`port=${cfg.port}`);
32
+ if (cfg.database)
33
+ parts.push(`dbname=${kvKeyword(cfg.database)}`);
34
+ if (cfg.user)
35
+ parts.push(`user=${kvKeyword(cfg.user)}`);
36
+ if (cfg.password)
37
+ parts.push(`password=${kvKeyword(cfg.password)}`);
38
+ if (cfg.ssl) {
39
+ parts.push('sslmode=require');
40
+ }
41
+ if (cfg.options) {
42
+ parts.push(`options=${kvKeyword(cfg.options)}`);
43
+ }
44
+ return parts.join(' ');
45
+ }
46
+ function mysqlAttachString(member, env) {
47
+ const cfg = mysqlConnectionPoolConfigFromConfig({
48
+ connectionId: member.connectionId,
49
+ connection: member.connection,
50
+ env,
51
+ });
52
+ const parts = [
53
+ `host=${kvKeyword(cfg.host)}`,
54
+ `port=${cfg.port}`,
55
+ `database=${kvKeyword(cfg.database)}`,
56
+ `user=${kvKeyword(cfg.user)}`,
57
+ ];
58
+ if (cfg.password) {
59
+ parts.push(`password=${kvKeyword(cfg.password)}`);
60
+ }
61
+ if (cfg.ssl) {
62
+ parts.push('ssl_mode=REQUIRED');
63
+ }
64
+ return parts.join(' ');
65
+ }
66
+ /**
67
+ * Resolves a federated member's ktx.yaml config into the connection target
68
+ * DuckDB's ATTACH wants for that driver, reusing each connector's canonical
69
+ * resolver so federation and standalone scans agree on config interpretation.
70
+ */
71
+ export function federatedAttachTarget(member, env) {
72
+ switch (member.driver.toLowerCase()) {
73
+ case 'sqlite':
74
+ return sqliteDatabasePathFromConfig({
75
+ connectionId: member.connectionId,
76
+ projectDir: member.projectDir,
77
+ connection: member.connection,
78
+ });
79
+ case 'postgres':
80
+ return postgresAttachString(member, env);
81
+ case 'mysql':
82
+ return mysqlAttachString(member, env);
83
+ default:
84
+ throw new Error(`Driver "${member.driver}" cannot be attached by DuckDB federation.`);
85
+ }
86
+ }
@@ -0,0 +1,5 @@
1
+ import type { KtxSqlQueryExecutionInput, KtxSqlQueryExecutionResult } from '../../context/connections/query-executor.js';
2
+ import { type FederatedMember } from '../../context/connections/federation.js';
3
+ /** @internal */
4
+ export declare function buildAttachStatements(members: FederatedMember[], env: NodeJS.ProcessEnv): string[];
5
+ export declare function executeFederatedQuery(members: FederatedMember[], input: KtxSqlQueryExecutionInput, env?: NodeJS.ProcessEnv): Promise<KtxSqlQueryExecutionResult>;