@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
@@ -189,7 +189,7 @@ export class AiSdkKtxLlmRuntime {
189
189
  const result = await this.generateTextWithRateLimitRetry(modelProviderName(model), input.abortSignal, () => generateText(request));
190
190
  input.onMetrics?.({ totalMs: Date.now() - startedAt, usage: toLlmTokenUsage(result.totalUsage ?? result.usage) });
191
191
  if (typeof result.text !== 'string') {
192
- throw new Error('KTX LLM text generation returned no text');
192
+ throw new Error('ktx LLM text generation returned no text');
193
193
  }
194
194
  return result.text;
195
195
  }
@@ -223,7 +223,7 @@ export class AiSdkKtxLlmRuntime {
223
223
  const result = await this.generateTextWithRateLimitRetry(modelProviderName(model), input.abortSignal, () => generateText(request));
224
224
  input.onMetrics?.({ totalMs: Date.now() - startedAt, usage: toLlmTokenUsage(result.totalUsage ?? result.usage) });
225
225
  if (result.output == null) {
226
- throw new Error('KTX LLM object generation returned no output');
226
+ throw new Error('ktx LLM object generation returned no output');
227
227
  }
228
228
  return result.output;
229
229
  }
@@ -89,7 +89,23 @@ function assertInitIsolation(message, allowedToolIds, expectedMcpServerNames) {
89
89
  function expectedMcpServerNames(tools) {
90
90
  return tools && Object.keys(tools).length > 0 ? new Set([KTX_MCP_SERVER_NAME]) : new Set();
91
91
  }
92
- const CLAUDE_RATE_LIMIT_ERROR_MARKERS = /\b429\b|rate limit|too many requests|quota exceeded|overloaded|max_retries/i;
92
+ // "session limit" is the Claude Code subscription cap ("You've hit your session
93
+ // limit · resets …"); the rest are transient 429-style throttling. All mean
94
+ // Claude Code authenticated successfully, so they must not be read as auth
95
+ // failures by the governor classifier or the auth probe.
96
+ const CLAUDE_RATE_LIMIT_ERROR_MARKERS = /\b429\b|rate limit|session limit|usage limit|too many requests|quota exceeded|overloaded|max_retries/i;
97
+ // The subscription cap is its own case: re-authenticating and retrying both fail
98
+ // until reset, so it gets a distinct message from transient rate limiting.
99
+ const CLAUDE_SESSION_LIMIT_MARKERS = /session limit|usage limit/i;
100
+ function describeClaudeProbeFailure(message) {
101
+ if (CLAUDE_SESSION_LIMIT_MARKERS.test(message)) {
102
+ return `Claude Code session limit reached. Wait for the reset shown, then rerun setup or the command. Details: ${message}`;
103
+ }
104
+ if (CLAUDE_RATE_LIMIT_ERROR_MARKERS.test(message)) {
105
+ return `Claude Code is rate limited. Retry shortly, then rerun setup or the command. Details: ${message}`;
106
+ }
107
+ return `Claude Code authentication is not usable. Authenticate Claude Code locally with the Claude Code CLI, then rerun setup or the command. ${message}`;
108
+ }
93
109
  function normalizeClaudeResetAtMs(value) {
94
110
  if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
95
111
  return Math.round(value < 10_000_000_000 ? value * 1_000 : value);
@@ -176,7 +192,7 @@ function baseOptions(input) {
176
192
  ? { behavior: 'allow', toolUseID: options.toolUseID }
177
193
  : {
178
194
  behavior: 'deny',
179
- message: `KTX claude-code runtime only permits current KTX MCP tools; denied ${toolName}.`,
195
+ message: `ktx claude-code runtime only permits current ktx MCP tools; denied ${toolName}.`,
180
196
  toolUseID: options.toolUseID,
181
197
  },
182
198
  permissionMode: 'dontAsk',
@@ -402,7 +418,7 @@ export async function runClaudeCodeAuthProbe(input) {
402
418
  const message = error instanceof Error ? error.message : String(error);
403
419
  return {
404
420
  ok: false,
405
- message: `Claude Code authentication is not usable. Authenticate Claude Code locally with the Claude Code CLI, then rerun setup or the command. ${message}`,
421
+ message: describeClaudeProbeFailure(message),
406
422
  };
407
423
  }
408
424
  }
@@ -143,7 +143,7 @@ export function resolveLocalKtxEmbeddingConfig(config, env) {
143
143
  batchSize: config.batchSize,
144
144
  };
145
145
  }
146
- throw new Error(`Unsupported KTX embedding backend: ${String(config.backend)}`);
146
+ throw new Error(`Unsupported ktx embedding backend: ${String(config.backend)}`);
147
147
  }
148
148
  /** @internal */
149
149
  export function createLocalKtxEmbeddingProviderFromConfig(config, deps = {}) {
@@ -21,7 +21,7 @@ export function normalizeKtxRuntimeToolOutput(value) {
21
21
  }
22
22
  function assertObjectSchema(name, schema) {
23
23
  if (!(schema instanceof z.ZodObject)) {
24
- throw new Error(`KTX runtime tool "${name}" must use z.object input schema for claude-code`);
24
+ throw new Error(`ktx runtime tool "${name}" must use z.object input schema for claude-code`);
25
25
  }
26
26
  }
27
27
  export function createAiSdkToolSet(tools = {}) {
@@ -57,7 +57,7 @@ export function createRuntimeToolDescriptorFromAiTool(name, aiSdkTool) {
57
57
  inputSchema: aiSdkTool.inputSchema,
58
58
  execute: async (input) => {
59
59
  if (typeof aiSdkTool.execute !== 'function') {
60
- throw new Error(`KTX runtime tool "${name}" has no execute function`);
60
+ throw new Error(`ktx runtime tool "${name}" has no execute function`);
61
61
  }
62
62
  return normalizeKtxRuntimeToolOutput(await aiSdkTool.execute(input, { toolCallId: `runtime-${name}` }));
63
63
  },
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
2
2
  import { z } from 'zod';
3
3
  import { emitTelemetryEvent, mcpTelemetrySampleRate, reportException, shouldEmitMcpTelemetry, } from '../../telemetry/index.js';
4
4
  import { collectTelemetryRedactionSecrets } from '../../telemetry/redaction-secrets.js';
5
- import { scrubErrorClass } from '../../telemetry/scrubber.js';
5
+ import { formatErrorDetail, scrubErrorClass } from '../../telemetry/scrubber.js';
6
6
  const connectionIdSchema = z.string().min(1);
7
7
  const unknownRecordSchema = z.record(z.string(), z.unknown());
8
8
  const tableRefSchema = z.object({
@@ -24,16 +24,16 @@ const toolAnnotations = {
24
24
  memory_ingest_status: { title: 'Memory Ingest Status', readOnlyHint: true, openWorldHint: false },
25
25
  };
26
26
  const toolDescriptions = {
27
- connection_list: 'List configured read-only data connections available to this KTX project. Use this before connection-scoped tools when the project may have multiple warehouses.',
28
- discover_data: 'Search across KTX wiki pages, semantic-layer sources, measures, dimensions, raw tables, and columns. Example: discover_data({ query: "monthly orders by customer", connectionId: "warehouse", kinds: ["sl_source", "table"] }).',
29
- wiki_search: 'Search KTX wiki pages for reusable business context. Example: wiki_search({ query: "revenue recognition", limit: 5 }).',
30
- wiki_read: 'Read a KTX wiki page by key returned from wiki_search. Example: wiki_read({ key: "global/revenue" }).',
27
+ connection_list: 'List configured read-only data connections available to this ktx project. Use this before connection-scoped tools when the project may have multiple warehouses. A "_ktx_federated" entry (when present) queries all its member databases together; use its id for cross-database joins.',
28
+ discover_data: 'Search across ktx wiki pages, semantic-layer sources, measures, dimensions, raw tables, and columns. Example: discover_data({ query: "monthly orders by customer", connectionId: "warehouse", kinds: ["sl_source", "table"] }).',
29
+ wiki_search: 'Search ktx wiki pages for reusable business context. Example: wiki_search({ query: "revenue recognition", limit: 5 }).',
30
+ wiki_read: 'Read a ktx wiki page by key returned from wiki_search. Example: wiki_read({ key: "global/revenue" }).',
31
31
  entity_details: 'Read table and column metadata from the latest live-database scan snapshot. Example: entity_details({ connectionId: "warehouse", entities: [{ table: { catalog: null, db: "public", name: "orders" }, columns: ["id"] }] }).',
32
32
  dictionary_search: 'Search profile-sampled warehouse values to locate likely source columns for business values. Example: dictionary_search({ values: ["Acme Corp"], connectionId: "warehouse" }).',
33
33
  sl_read_source: 'Read a semantic-layer YAML source by connection id and source name. Example: sl_read_source({ connectionId: "warehouse", sourceName: "orders" }).',
34
34
  sl_query: 'Execute a semantic-layer query and return headers, rows, and total row count, plus correctness notes (e.g. compile-only or fan-out) when relevant. The generated SQL and full query plan are omitted by default; request them with include: ["sql"] and/or include: ["plan"]. Example: sl_query({ connectionId: "warehouse", measures: ["orders.order_count"], dimensions: [{ field: "orders.created_at", granularity: "month" }], include: ["sql"] }).',
35
- sql_execution: 'Execute one parser-validated read-only SQL query against a configured KTX connection. Example: sql_execution({ connectionId: "warehouse", sql: "select count(*) from public.orders", maxRows: 100 }).',
36
- memory_ingest: 'Ingest free-form markdown knowledge into durable KTX memory. Use this for business rules, metric definitions, schema gotchas, recurring findings, or explicit user requests to remember something. Example: memory_ingest({ connectionId: "warehouse", content: "ARR is reported in cents in this warehouse." }).',
35
+ sql_execution: 'Execute one parser-validated read-only SQL query against a configured ktx connection. Example: sql_execution({ connectionId: "warehouse", sql: "select count(*) from public.orders", maxRows: 100 }).',
36
+ memory_ingest: 'Ingest free-form markdown knowledge into durable ktx memory. Use this for business rules, metric definitions, schema gotchas, recurring findings, or explicit user requests to remember something. Example: memory_ingest({ connectionId: "warehouse", content: "ARR is reported in cents in this warehouse." }).',
37
37
  memory_ingest_status: 'Read the current or final status for a memory ingest run. Example: memory_ingest_status({ runId: "memory-run-1" }).',
38
38
  };
39
39
  const connectionListSchema = z.object({});
@@ -160,6 +160,8 @@ const connectionListOutputSchema = z.object({
160
160
  id: z.string(),
161
161
  name: z.string(),
162
162
  connectionType: z.string(),
163
+ members: z.array(z.string()).optional(),
164
+ hint: z.string().optional(),
163
165
  })),
164
166
  });
165
167
  const wikiSearchOutputSchema = z.object({
@@ -442,6 +444,25 @@ function clientTelemetryFields(getClientInfo) {
442
444
  ...(client?.version ? { mcpClientVersion: client.version } : {}),
443
445
  };
444
446
  }
447
+ // Tools registered via registerParsedTool catch their own errors and return an
448
+ // isError result, so the telemetry layer never sees the thrown Error. Recover
449
+ // the failure message from the result's text content (the same string the agent
450
+ // reads) so the outcome event is self-diagnosing.
451
+ function mcpErrorResultDetail(result) {
452
+ if (typeof result !== 'object' || result === null || !('content' in result)) {
453
+ return undefined;
454
+ }
455
+ const content = result.content;
456
+ if (!Array.isArray(content)) {
457
+ return undefined;
458
+ }
459
+ const text = content
460
+ .map((block) => typeof block === 'object' && block !== null && typeof block.text === 'string'
461
+ ? block.text
462
+ : '')
463
+ .join('\n');
464
+ return formatErrorDetail(text);
465
+ }
445
466
  function instrumentMcpServer(server, telemetry) {
446
467
  return {
447
468
  registerTool(name, config, handler) {
@@ -451,6 +472,7 @@ function instrumentMcpServer(server, telemetry) {
451
472
  const result = await handler(input, context);
452
473
  if (telemetry.io && telemetry.projectDir && shouldEmitMcpTelemetry()) {
453
474
  const isError = typeof result === 'object' && result !== null && 'isError' in result && result.isError === true;
475
+ const errorDetail = isError ? mcpErrorResultDetail(result) : undefined;
454
476
  await emitTelemetryEvent({
455
477
  name: 'mcp_request_completed',
456
478
  projectDir: telemetry.projectDir,
@@ -460,6 +482,7 @@ function instrumentMcpServer(server, telemetry) {
460
482
  outcome: isError ? 'error' : 'ok',
461
483
  durationMs: Math.max(0, performance.now() - startedAt),
462
484
  sampleRate: mcpTelemetrySampleRate(),
485
+ ...(errorDetail ? { errorDetail } : {}),
463
486
  ...clientTelemetryFields(telemetry.getClientInfo),
464
487
  },
465
488
  });
@@ -483,6 +506,7 @@ function instrumentMcpServer(server, telemetry) {
483
506
  }
484
507
  if (telemetry.io && telemetry.projectDir && shouldEmitMcpTelemetry()) {
485
508
  const errorClass = scrubErrorClass(error);
509
+ const errorDetail = formatErrorDetail(error);
486
510
  await emitTelemetryEvent({
487
511
  name: 'mcp_request_completed',
488
512
  projectDir: telemetry.projectDir,
@@ -491,6 +515,7 @@ function instrumentMcpServer(server, telemetry) {
491
515
  toolName: name,
492
516
  outcome: 'error',
493
517
  ...(errorClass ? { errorClass } : {}),
518
+ ...(errorDetail ? { errorDetail } : {}),
494
519
  durationMs: Math.max(0, performance.now() - startedAt),
495
520
  sampleRate: mcpTelemetrySampleRate(),
496
521
  ...clientTelemetryFields(telemetry.getClientInfo),
@@ -641,7 +666,7 @@ export function registerKtxContextTools(deps) {
641
666
  const ingestInput = {
642
667
  userId: userContext.userId,
643
668
  chatId: `mcp-${randomUUID()}`,
644
- userMessage: 'Ingest external knowledge into KTX memory.',
669
+ userMessage: 'Ingest external knowledge into ktx memory.',
645
670
  assistantMessage: input.content,
646
671
  connectionId: input.connectionId,
647
672
  sourceType: 'external_ingest',
@@ -1,110 +1,87 @@
1
- import { localConnectionInfoFromConfig } from '../../context/connections/local-warehouse-descriptor.js';
1
+ import { KtxExpectedError, KtxQueryError, isNativeProgrammingFault } from '../../errors.js';
2
+ import { executeProjectReadOnlySql } from '../../context/connections/project-sql-executor.js';
3
+ import { FEDERATED_CONNECTION_ID, federatedConnectionListing } from '../../context/connections/federation.js';
4
+ import { resolveConfiguredConnection } from '../../context/connections/resolve-connection.js';
5
+ import { localConnectionInfoFromConfig, } from '../../context/connections/local-warehouse-descriptor.js';
2
6
  import { createKtxEntityDetailsService } from '../../context/scan/entity-details.js';
3
7
  import { createKtxDiscoverDataService } from '../../context/search/discover.js';
8
+ import { sqlAnalysisDialectForDriver } from '../../context/sql-analysis/dialect.js';
4
9
  import { compileLocalSlQuery } from '../../context/sl/local-query.js';
5
10
  import { createKtxDictionarySearchService } from '../../context/sl/dictionary-search.js';
11
+ import { readLocalSlSource } from '../../context/sl/local-sl.js';
12
+ import { assertSafeConnectionId } from '../../context/sl/source-files.js';
6
13
  import { readLocalKnowledgePage, searchLocalKnowledgePages } from '../wiki/local-knowledge.js';
7
- function dialectForDriver(driver) {
8
- const normalized = (driver ?? 'postgres').toUpperCase();
9
- const map = {
10
- POSTGRES: 'postgres',
11
- BIGQUERY: 'bigquery',
12
- SNOWFLAKE: 'snowflake',
13
- MYSQL: 'mysql',
14
- SQLSERVER: 'tsql',
15
- SQLITE: 'sqlite',
16
- DUCKDB: 'duckdb',
17
- CLICKHOUSE: 'clickhouse',
18
- DATABRICKS: 'databricks',
19
- };
20
- return map[normalized] ?? 'postgres';
21
- }
22
- function sqlAnalysisDialectForDriver(driver) {
23
- return dialectForDriver(driver);
24
- }
25
- function assertSafePathToken(kind, value) {
26
- if (value.trim().length === 0 ||
27
- value.includes('..') ||
28
- value.includes('\\') ||
29
- value.startsWith('/') ||
30
- value.startsWith('.') ||
31
- value.includes('//')) {
32
- throw new Error(`Unsafe ${kind}: ${value}`);
33
- }
34
- return value;
35
- }
36
- function assertSafeConnectionId(connectionId) {
37
- if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(connectionId)) {
38
- throw new Error(`Unsafe connection id: ${connectionId}`);
39
- }
40
- return assertSafePathToken('connection id', connectionId);
41
- }
42
- function assertSafeSourceName(sourceName) {
43
- if (!/^[a-z0-9][a-z0-9_]*$/.test(sourceName)) {
44
- throw new Error(`Unsafe semantic-layer source name: ${sourceName}`);
45
- }
46
- return assertSafePathToken('semantic-layer source name', sourceName);
47
- }
48
- async function cleanupConnector(connector) {
49
- if (connector?.cleanup) {
50
- await connector.cleanup();
51
- }
52
- }
53
- function slPath(connectionId, sourceName) {
54
- return `semantic-layer/${assertSafeConnectionId(connectionId)}/${assertSafeSourceName(sourceName)}.yaml`;
55
- }
56
14
  async function executeValidatedReadOnlySql(project, options, input, onProgress) {
57
15
  await onProgress?.({ progress: 0, message: 'Validating SQL' });
58
- const connectionId = assertSafeConnectionId(input.connectionId);
59
- const connection = project.config.connections[connectionId];
60
- if (!connection) {
61
- throw new Error(`Connection "${connectionId}" is not configured in ktx.yaml`);
62
- }
63
16
  if (!options.sqlAnalysis) {
64
17
  throw new Error('sql_execution requires parser-backed SQL validation.');
65
18
  }
66
- const validation = await options.sqlAnalysis.validateReadOnly(input.sql, sqlAnalysisDialectForDriver(connection.driver));
67
- if (!validation.ok) {
68
- throw new Error(validation.error ?? 'SQL is not read-only.');
69
- }
70
19
  const createConnector = options.localScan?.createConnector;
71
20
  if (!createConnector) {
72
21
  throw new Error('sql_execution requires a local scan connector factory.');
73
22
  }
74
- let connector = null;
75
- try {
76
- connector = await createConnector(connectionId);
77
- if (!connector.capabilities.readOnlySql || !connector.executeReadOnly) {
78
- throw new Error(`Connection "${connectionId}" does not support read-only SQL execution.`);
79
- }
80
- await onProgress?.({ progress: 0.3, message: 'Executing' });
81
- const result = await connector.executeReadOnly({
23
+ const isFederated = input.connectionId === FEDERATED_CONNECTION_ID;
24
+ const connectionId = isFederated ? input.connectionId : assertSafeConnectionId(input.connectionId);
25
+ const connection = isFederated ? undefined : resolveConfiguredConnection(project.config, connectionId);
26
+ const dialect = sqlAnalysisDialectForDriver(isFederated ? 'duckdb' : connection.driver);
27
+ const validation = await options.sqlAnalysis.validateReadOnly(input.sql, dialect);
28
+ if (!validation.ok) {
29
+ // A read-only guard rejecting the agent's SQL is an expected outcome, not a
30
+ // ktx fault: classify it so reportException keeps it out of Error Tracking.
31
+ throw new KtxQueryError(validation.error ?? 'SQL is not read-only.');
32
+ }
33
+ await onProgress?.({ progress: 0.3, message: 'Executing' });
34
+ const result = await executeProjectReadOnlySql({
35
+ project,
36
+ input: {
82
37
  connectionId,
38
+ projectDir: project.projectDir,
39
+ connection,
83
40
  sql: input.sql,
84
41
  maxRows: input.maxRows,
85
- }, { runId: 'mcp-sql-execution' });
86
- const response = {
87
- headers: result.headers,
88
- ...(result.headerTypes ? { headerTypes: result.headerTypes } : {}),
89
- rows: result.rows,
90
- rowCount: result.rowCount ?? result.rows.length,
91
- };
92
- await onProgress?.({ progress: 1, message: `Fetched ${response.rowCount} rows` });
93
- return response;
94
- }
95
- finally {
96
- await cleanupConnector(connector);
97
- }
42
+ },
43
+ createConnector,
44
+ runId: 'mcp-sql-execution',
45
+ }).catch((error) => {
46
+ // A warehouse/driver rejection (e.g. the agent's SQL failed to compile) is a
47
+ // surfaced operational outcome, not a ktx fault: mark it expected while
48
+ // preserving the warehouse's own diagnostics. A native JS error (TypeError,
49
+ // etc.) signals a bug in connector code — let it propagate unchanged so Error
50
+ // Tracking still sees it.
51
+ if (isNativeProgrammingFault(error) || error instanceof KtxExpectedError) {
52
+ throw error;
53
+ }
54
+ throw new KtxQueryError(error instanceof Error ? error.message : String(error), { cause: error });
55
+ });
56
+ const response = {
57
+ headers: result.headers,
58
+ ...(result.headerTypes ? { headerTypes: result.headerTypes } : {}),
59
+ rows: result.rows,
60
+ rowCount: result.rowCount ?? result.rows.length,
61
+ };
62
+ await onProgress?.({ progress: 1, message: `Fetched ${response.rowCount} rows` });
63
+ return response;
98
64
  }
99
65
  export function createLocalProjectMcpContextPorts(project, options) {
100
66
  const embeddingService = options.embeddingService;
101
67
  const ports = {
102
68
  connections: {
103
69
  async list() {
104
- return Object.entries(project.config.connections)
70
+ const configured = Object.entries(project.config.connections)
105
71
  .map(([id, config]) => localConnectionInfoFromConfig(id, config))
106
72
  .filter((connection) => connection !== null)
107
73
  .sort((a, b) => a.id.localeCompare(b.id));
74
+ const federated = federatedConnectionListing(project.config.connections, project.projectDir);
75
+ if (federated) {
76
+ configured.push({
77
+ id: federated.id,
78
+ name: federated.id,
79
+ connectionType: 'DUCKDB',
80
+ members: federated.members,
81
+ hint: federated.hint,
82
+ });
83
+ }
84
+ return configured;
108
85
  },
109
86
  },
110
87
  knowledge: {
@@ -148,14 +125,11 @@ export function createLocalProjectMcpContextPorts(project, options) {
148
125
  },
149
126
  semanticLayer: {
150
127
  async readSource(input) {
151
- const path = slPath(input.connectionId, input.sourceName);
152
- try {
153
- const result = await project.fileStore.readFile(path);
154
- return { sourceName: input.sourceName, yaml: result.content };
155
- }
156
- catch {
157
- return null;
158
- }
128
+ const source = await readLocalSlSource(project, {
129
+ connectionId: input.connectionId,
130
+ sourceName: input.sourceName,
131
+ });
132
+ return source ? { sourceName: source.name, yaml: source.yaml } : null;
159
133
  },
160
134
  async query(input, executionOptions) {
161
135
  if (!options.semanticLayerCompute) {
@@ -68,6 +68,8 @@ interface KtxConnectionSummary {
68
68
  id: string;
69
69
  name: string;
70
70
  connectionType: string;
71
+ members?: string[];
72
+ hint?: string;
71
73
  }
72
74
  interface KtxConnectionsMcpPort {
73
75
  list(): Promise<KtxConnectionSummary[]>;
@@ -31,7 +31,7 @@ import { MemoryAgentService } from './memory-agent.service.js';
31
31
  import { MemoryIngestService } from './memory-runs.js';
32
32
  const promptsDir = fileURLToPath(new URL('../../prompts', import.meta.url));
33
33
  const skillsDir = fileURLToPath(new URL('../../skills', import.meta.url));
34
- const LOCAL_AUTHOR = { name: 'KTX Local', email: 'local@ktx.local' };
34
+ const LOCAL_AUTHOR = { name: 'ktx Local', email: 'local@ktx.local' };
35
35
  const LOCAL_SHAPE_WARNING = 'Local memory ingest validates semantic-layer YAML shape only.';
36
36
  export function createLocalProjectMemoryIngest(project, options = {}) {
37
37
  const logger = options.logger ?? noopLogger;
@@ -307,6 +307,9 @@ class LocalShapeOnlySlValidator {
307
307
  async validateSingleSource(deps, connectionId, sourceName) {
308
308
  try {
309
309
  const file = await deps.semanticLayerService.readSourceFile(connectionId, sourceName);
310
+ if (!file) {
311
+ return { errors: [`${sourceName}: no standalone or overlay file found`], warnings: [] };
312
+ }
310
313
  const parsed = YAML.parse(file.content);
311
314
  const isOverlay = parsed.table == null && parsed.sql == null;
312
315
  const result = (isOverlay ? sourceOverlaySchema : sourceDefinitionSchema).safeParse(parsed);
@@ -391,7 +391,7 @@ export class MemoryAgentService {
391
391
  if (session.connectionId) {
392
392
  for (const { connectionId, sourceName } of listTouchedSlSources(session.touchedSlSources)) {
393
393
  try {
394
- const file = await this.deps.semanticLayerService.readSourceFile(connectionId, sourceName).catch(() => null);
394
+ const file = await this.deps.semanticLayerService.readSourceFile(connectionId, sourceName);
395
395
  if (file?.content) {
396
396
  const parsed = this.parseYamlOrNull(file.content);
397
397
  if (parsed) {
@@ -317,7 +317,6 @@ declare const ktxProjectConfigSchema: z.ZodObject<{
317
317
  "postgres-hybrid": "postgres-hybrid";
318
318
  }>>;
319
319
  git: z.ZodPrefault<z.ZodObject<{
320
- auto_commit: z.ZodDefault<z.ZodBoolean>;
321
320
  author: z.ZodDefault<z.ZodString>;
322
321
  }, z.core.$strict>>;
323
322
  }, z.core.$strict>>;
@@ -418,9 +417,6 @@ declare const ktxProjectConfigSchema: z.ZodObject<{
418
417
  default_toolset: z.ZodDefault<z.ZodArray<z.ZodString>>;
419
418
  }, z.core.$strict>>;
420
419
  }, z.core.$strict>>;
421
- memory: z.ZodPrefault<z.ZodObject<{
422
- auto_commit: z.ZodDefault<z.ZodBoolean>;
423
- }, z.core.$strict>>;
424
420
  scan: z.ZodPrefault<z.ZodObject<{
425
421
  enrichment: z.ZodPrefault<z.ZodObject<{
426
422
  mode: z.ZodDefault<z.ZodEnum<{
@@ -472,12 +468,23 @@ export interface KtxConfigIssue {
472
468
  path: string;
473
469
  message: string;
474
470
  fix?: string;
471
+ /**
472
+ * 'error' blocks the project (bad value on a recognized field); 'warning' is
473
+ * a condition the loader recovers from on its own (an ignored unknown key).
474
+ */
475
+ severity: 'error' | 'warning';
475
476
  }
476
477
  export interface KtxConfigValidation {
477
478
  ok: boolean;
478
479
  issues: KtxConfigIssue[];
479
480
  }
480
481
  export declare function buildDefaultKtxProjectConfig(): KtxProjectConfig;
482
+ /**
483
+ * Parse and validate a ktx.yaml document. Keys this ktx version does not
484
+ * recognize are stripped from the returned config — never from the file, which
485
+ * a load must not rewrite — so a config written by a different ktx version
486
+ * still loads. Malformed values on recognized fields still throw.
487
+ */
481
488
  export declare function parseKtxProjectConfig(raw: string): KtxProjectConfig;
482
489
  export declare function validateKtxProjectConfig(raw: string): KtxConfigValidation;
483
490
  export declare function generateKtxProjectConfigJsonSchema(): Record<string, unknown>;