@kaelio/ktx 0.12.0 → 0.13.1

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 (54) hide show
  1. package/assets/python/{kaelio_ktx-0.12.0-py3-none-any.whl → kaelio_ktx-0.13.1-py3-none-any.whl} +0 -0
  2. package/assets/python/manifest.json +4 -4
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/commands/setup-commands.js +13 -0
  5. package/dist/connection.js +14 -2
  6. package/dist/connectors/bigquery/connector.js +1 -14
  7. package/dist/connectors/clickhouse/connector.js +1 -15
  8. package/dist/connectors/duckdb/federated-attach.d.ts +7 -0
  9. package/dist/connectors/duckdb/federated-attach.js +86 -0
  10. package/dist/connectors/duckdb/federated-executor.d.ts +5 -0
  11. package/dist/connectors/duckdb/federated-executor.js +59 -0
  12. package/dist/connectors/mysql/connector.js +1 -15
  13. package/dist/connectors/postgres/connector.js +1 -14
  14. package/dist/connectors/shared/string-reference.d.ts +6 -0
  15. package/dist/connectors/shared/string-reference.js +19 -0
  16. package/dist/connectors/snowflake/connector.js +1 -14
  17. package/dist/connectors/sqlserver/connector.js +4 -16
  18. package/dist/context/connections/federation.d.ts +33 -0
  19. package/dist/context/connections/federation.js +51 -0
  20. package/dist/context/connections/local-warehouse-descriptor.d.ts +2 -0
  21. package/dist/context/connections/project-sql-executor.d.ts +18 -0
  22. package/dist/context/connections/project-sql-executor.js +39 -0
  23. package/dist/context/connections/query-executor.d.ts +2 -2
  24. package/dist/context/connections/read-only-sql.d.ts +5 -0
  25. package/dist/context/connections/read-only-sql.js +143 -4
  26. package/dist/context/connections/resolve-connection.d.ts +12 -0
  27. package/dist/context/connections/resolve-connection.js +37 -0
  28. package/dist/context/core/git-env.d.ts +4 -0
  29. package/dist/context/core/git-env.js +5 -1
  30. package/dist/context/ingest/adapters/live-database/manifest.d.ts +3 -0
  31. package/dist/context/ingest/adapters/live-database/manifest.js +19 -11
  32. package/dist/context/llm/claude-code-runtime.js +18 -2
  33. package/dist/context/mcp/context-tools.js +27 -2
  34. package/dist/context/mcp/local-project-ports.js +55 -50
  35. package/dist/context/mcp/types.d.ts +2 -0
  36. package/dist/context/scan/local-enrichment-artifacts.js +31 -3
  37. package/dist/context/sl/local-query.js +29 -12
  38. package/dist/context/sl/local-sl.js +27 -1
  39. package/dist/context/sl/source-files.d.ts +2 -0
  40. package/dist/context/sl/source-files.js +7 -0
  41. package/dist/ingest-query-executor.d.ts +2 -0
  42. package/dist/ingest-query-executor.js +8 -22
  43. package/dist/setup-agents.d.ts +21 -15
  44. package/dist/setup-agents.js +128 -42
  45. package/dist/setup-databases.d.ts +3 -0
  46. package/dist/setup-databases.js +16 -0
  47. package/dist/setup-sources.js +1 -5
  48. package/dist/setup.d.ts +1 -0
  49. package/dist/setup.js +1 -0
  50. package/dist/sql.d.ts +2 -0
  51. package/dist/sql.js +35 -53
  52. package/dist/telemetry/events.d.ts +2 -1
  53. package/dist/telemetry/events.js +11 -1
  54. package/package.json +2 -1
@@ -3,6 +3,7 @@ import { readFile, writeFile } from 'node:fs/promises';
3
3
  import { delimiter, dirname, join } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { promisify } from 'node:util';
6
+ import { deriveFederatedConnection, FEDERATED_CONNECTION_ID } from './context/connections/federation.js';
6
7
  import { getDriverRegistration } from './context/connections/drivers.js';
7
8
  import { createLocalKtxLlmRuntimeFromConfig } from './context/llm/local-config.js';
8
9
  import { queryHistoryDialectForConnection } from './context/ingest/adapters/historic-sql/connection-dialect.js';
@@ -845,6 +846,21 @@ async function writeConnectionConfig(input) {
845
846
  if (queryHistory?.enabled === true) {
846
847
  await ensureHistoricSqlIngestDefaults(input.projectDir);
847
848
  }
849
+ if (input.io) {
850
+ const federationNotice = federationNoticeFor(config.connections, input.projectDir);
851
+ if (federationNotice) {
852
+ writeSetupSection(input.io, 'Federated connection available', [federationNotice]);
853
+ }
854
+ }
855
+ }
856
+ /** @internal */
857
+ export function federationNoticeFor(connections, projectDir) {
858
+ const descriptor = deriveFederatedConnection(connections, projectDir);
859
+ if (!descriptor) {
860
+ return null;
861
+ }
862
+ const names = descriptor.members.map((m) => m.connectionId).join(', ');
863
+ return `Detected ${descriptor.members.length} attach-compatible databases (${names}). Run a cross-database join as read-only SQL against \`${FEDERATED_CONNECTION_ID}\` (ktx sql -c ${FEDERATED_CONNECTION_ID} "SELECT ..."), using catalog-qualified table names.`;
848
864
  }
849
865
  async function disableConnectionQueryHistory(projectDir, connectionId) {
850
866
  const project = await loadKtxProject({ projectDir });
@@ -19,6 +19,7 @@ import { markKtxSetupStateStepComplete } from './context/project/setup-config.js
19
19
  import { createCliSpinner, errorMessage, writePrefixedLines } from './clack.js';
20
20
  import { pickNotionRootPages } from './notion-page-picker.js';
21
21
  import { runKtxSourceMapping } from './source-mapping.js';
22
+ import { assertSafeConnectionId } from './context/sl/source-files.js';
22
23
  import { runConnectionSetupWithRecovery, } from './connection-recovery.js';
23
24
  import { withMultiselectNavigation, withTextInputNavigation } from './prompt-navigation.js';
24
25
  import { runKtxPublicIngest } from './public-ingest.js';
@@ -100,11 +101,6 @@ async function findDbtProjectSubpaths(rootDir) {
100
101
  async function promptText(prompts, options) {
101
102
  return await prompts.text({ ...options, message: withTextInputNavigation(options.message) });
102
103
  }
103
- function assertSafeConnectionId(connectionId) {
104
- if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(connectionId)) {
105
- throw new Error(`Unsafe connection id: ${connectionId}`);
106
- }
107
- }
108
104
  function credentialRef(value, label) {
109
105
  const ref = value?.trim();
110
106
  if (!ref) {
package/dist/setup.d.ts CHANGED
@@ -57,6 +57,7 @@ export type KtxSetupArgs = {
57
57
  agents: boolean;
58
58
  target?: KtxAgentTarget;
59
59
  agentScope?: KtxAgentScope;
60
+ installRoot?: string;
60
61
  skipAgents?: boolean;
61
62
  inputMode: 'auto' | 'disabled';
62
63
  debug?: boolean;
package/dist/setup.js CHANGED
@@ -624,6 +624,7 @@ async function runKtxSetupInner(args, io, deps = {}) {
624
624
  agents: true,
625
625
  ...(args.target ? { target: args.target } : {}),
626
626
  scope: args.agentScope ?? 'project',
627
+ ...(args.installRoot ? { installRoot: args.installRoot } : {}),
627
628
  mode: 'mcp',
628
629
  skipAgents: false,
629
630
  showNextActions: agentsRequested,
package/dist/sql.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { executeFederatedQuery } from './connectors/duckdb/federated-executor.js';
1
2
  import { loadKtxProject } from './context/project/project.js';
2
3
  import type { SqlAnalysisPort } from './context/sql-analysis/ports.js';
3
4
  import type { KtxCliIo } from './cli-runtime.js';
@@ -18,6 +19,7 @@ export interface KtxSqlDeps {
18
19
  loadProject?: typeof loadKtxProject;
19
20
  createSqlAnalysis?: () => SqlAnalysisPort;
20
21
  createScanConnector?: typeof createKtxCliScanConnector;
22
+ executeFederated?: typeof executeFederatedQuery;
21
23
  }
22
24
  export declare function runKtxSql(args: KtxSqlArgs, io?: KtxCliIo, deps?: KtxSqlDeps): Promise<number>;
23
25
  export {};
package/dist/sql.js CHANGED
@@ -1,4 +1,8 @@
1
+ import { FEDERATED_CONNECTION_ID } from './context/connections/federation.js';
2
+ import { executeProjectReadOnlySql } from './context/connections/project-sql-executor.js';
3
+ import { resolveConfiguredConnection } from './context/connections/resolve-connection.js';
1
4
  import { loadKtxProject } from './context/project/project.js';
5
+ import { sqlAnalysisDialectForDriver } from './context/sql-analysis/dialect.js';
2
6
  import { resolveOutputMode } from './io/mode.js';
3
7
  import { createKtxCliScanConnector } from './local-scan-connectors.js';
4
8
  import { createManagedDaemonSqlAnalysisPort } from './managed-python-http.js';
@@ -8,19 +12,6 @@ import { emitTelemetryEvent, reportException } from './telemetry/index.js';
8
12
  import { collectTelemetryRedactionSecrets } from './telemetry/redaction-secrets.js';
9
13
  import { scrubErrorClass } from './telemetry/scrubber.js';
10
14
  profileMark('module:sql');
11
- function sqlAnalysisDialectForDriver(driver) {
12
- const normalized = String(driver ?? '').trim().toLowerCase();
13
- const map = {
14
- postgres: 'postgres',
15
- bigquery: 'bigquery',
16
- snowflake: 'snowflake',
17
- mysql: 'mysql',
18
- sqlserver: 'tsql',
19
- sqlite: 'sqlite',
20
- clickhouse: 'clickhouse',
21
- };
22
- return map[normalized] ?? 'postgres';
23
- }
24
15
  function queryVerb(sql) {
25
16
  const first = sql.trim().split(/\s+/, 1)[0]?.toLowerCase();
26
17
  if (first === 'select' || first === 'explain' || first === 'show' || first === 'with') {
@@ -79,11 +70,6 @@ function printSqlResult(output, mode, io) {
79
70
  }
80
71
  printPretty(output, io);
81
72
  }
82
- async function cleanupConnector(connector) {
83
- if (connector?.cleanup) {
84
- await connector.cleanup();
85
- }
86
- }
87
73
  function resultOutput(connectionId, result) {
88
74
  return {
89
75
  connectionId,
@@ -100,12 +86,10 @@ export async function runKtxSql(args, io = process, deps = {}) {
100
86
  let project;
101
87
  try {
102
88
  project = await (deps.loadProject ?? loadKtxProject)({ projectDir: args.projectDir });
103
- const connection = project.config.connections[args.connectionId];
104
- if (!connection) {
105
- throw new Error(`Connection "${args.connectionId}" is not configured in ktx.yaml`);
106
- }
107
- driver = String(connection.driver ?? 'unknown').toLowerCase();
108
- demoConnection = isDemoConnection(args.connectionId, connection);
89
+ const isFederated = args.connectionId === FEDERATED_CONNECTION_ID;
90
+ const connection = isFederated ? undefined : resolveConfiguredConnection(project.config, args.connectionId);
91
+ driver = isFederated ? 'duckdb' : String(connection?.driver ?? 'unknown').toLowerCase();
92
+ demoConnection = isFederated ? false : isDemoConnection(args.connectionId, connection);
109
93
  const createSqlAnalysis = deps.createSqlAnalysis ??
110
94
  (() => createManagedDaemonSqlAnalysisPort({
111
95
  cliVersion: args.cliVersion,
@@ -114,44 +98,42 @@ export async function runKtxSql(args, io = process, deps = {}) {
114
98
  io,
115
99
  }));
116
100
  const analysisPort = createSqlAnalysis();
117
- const dialect = sqlAnalysisDialectForDriver(connection.driver);
101
+ const dialect = isFederated ? 'duckdb' : sqlAnalysisDialectForDriver(connection?.driver);
118
102
  const validation = await analysisPort.validateReadOnly(args.sql, dialect);
119
103
  if (!validation.ok) {
120
104
  throw new Error(validation.error ?? 'SQL is not read-only.');
121
105
  }
122
106
  const referencedTableCount = await safeReferencedTableCount(analysisPort, args.sql, dialect);
123
107
  const createScanConnector = deps.createScanConnector ?? createKtxCliScanConnector;
124
- let connector = null;
125
- try {
126
- connector = await createScanConnector(project, args.connectionId);
127
- if (!connector.capabilities.readOnlySql || !connector.executeReadOnly) {
128
- throw new Error(`Connection "${args.connectionId}" does not support read-only SQL execution.`);
129
- }
130
- const result = await connector.executeReadOnly({
108
+ const result = await executeProjectReadOnlySql({
109
+ project,
110
+ input: {
131
111
  connectionId: args.connectionId,
112
+ projectDir: args.projectDir,
113
+ connection,
132
114
  sql: args.sql,
133
115
  maxRows: args.maxRows,
134
- }, { runId: 'cli-sql' });
135
- const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
136
- printSqlResult(resultOutput(args.connectionId, result), mode, io);
137
- await emitTelemetryEvent({
138
- name: 'sql_completed',
139
- projectDir: args.projectDir,
140
- io,
141
- fields: {
142
- driver,
143
- isDemoConnection: demoConnection,
144
- queryVerb: queryVerb(args.sql),
145
- referencedTableCount,
146
- durationMs: Math.max(0, performance.now() - startedAt),
147
- outcome: 'ok',
148
- },
149
- });
150
- return 0;
151
- }
152
- finally {
153
- await cleanupConnector(connector);
154
- }
116
+ },
117
+ createConnector: (connectionId) => createScanConnector(project, connectionId),
118
+ executeFederated: deps.executeFederated,
119
+ runId: 'cli-sql',
120
+ });
121
+ const mode = resolveOutputMode({ explicit: args.output, json: args.json, io });
122
+ printSqlResult(resultOutput(args.connectionId, result), mode, io);
123
+ await emitTelemetryEvent({
124
+ name: 'sql_completed',
125
+ projectDir: args.projectDir,
126
+ io,
127
+ fields: {
128
+ driver,
129
+ isDemoConnection: demoConnection,
130
+ queryVerb: queryVerb(args.sql),
131
+ referencedTableCount,
132
+ durationMs: Math.max(0, performance.now() - startedAt),
133
+ outcome: 'ok',
134
+ },
135
+ });
136
+ return 0;
155
137
  }
156
138
  catch (error) {
157
139
  const errorClass = scrubErrorClass(error);
@@ -303,6 +303,7 @@ export declare const telemetryEventSchemas: {
303
303
  }>;
304
304
  durationMs: z.ZodNumber;
305
305
  errorClass: z.ZodOptional<z.ZodString>;
306
+ errorDetail: z.ZodOptional<z.ZodString>;
306
307
  sampleRate: z.ZodLiteral<1>;
307
308
  mcpClientName: z.ZodOptional<z.ZodString>;
308
309
  mcpClientVersion: z.ZodOptional<z.ZodString>;
@@ -459,7 +460,7 @@ export declare const telemetryEventCatalog: readonly [{
459
460
  }, {
460
461
  readonly name: "mcp_request_completed";
461
462
  readonly description: "Emitted for sampled MCP tool requests.";
462
- readonly fields: readonly ["toolName", "outcome", "durationMs", "errorClass", "sampleRate", "mcpClientName", "mcpClientVersion"];
463
+ readonly fields: readonly ["toolName", "outcome", "durationMs", "errorClass", "errorDetail", "sampleRate", "mcpClientName", "mcpClientVersion"];
463
464
  }, {
464
465
  readonly name: "daemon_started";
465
466
  readonly description: "Emitted when the long-lived ktx-daemon HTTP server starts.";
@@ -145,6 +145,7 @@ const mcpRequestCompletedSchema = telemetryCommonEnvelopeSchema
145
145
  outcome: outcomeSchema,
146
146
  durationMs: z.number().nonnegative(),
147
147
  errorClass: z.string().optional(),
148
+ errorDetail: z.string().max(1000).optional(),
148
149
  sampleRate: z.literal(1),
149
150
  // Raw, client-tool-controlled identity from the MCP initialize handshake
150
151
  // (clientInfo.name/version). Optional: clients may omit clientInfo. Stored
@@ -326,7 +327,16 @@ export const telemetryEventCatalog = [
326
327
  {
327
328
  name: 'mcp_request_completed',
328
329
  description: 'Emitted for sampled MCP tool requests.',
329
- fields: ['toolName', 'outcome', 'durationMs', 'errorClass', 'sampleRate', 'mcpClientName', 'mcpClientVersion'],
330
+ fields: [
331
+ 'toolName',
332
+ 'outcome',
333
+ 'durationMs',
334
+ 'errorClass',
335
+ 'errorDetail',
336
+ 'sampleRate',
337
+ 'mcpClientName',
338
+ 'mcpClientVersion',
339
+ ],
330
340
  },
331
341
  {
332
342
  name: 'daemon_started',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaelio/ktx",
3
- "version": "0.12.0",
3
+ "version": "0.13.1",
4
4
  "description": "Standalone ktx context layer for data agents",
5
5
  "author": {
6
6
  "name": "Kaelio",
@@ -40,6 +40,7 @@
40
40
  "@clack/prompts": "1.4.0",
41
41
  "@clickhouse/client": "^1.18.5",
42
42
  "@commander-js/extra-typings": "14.0.0",
43
+ "@duckdb/node-api": "1.5.3-r.3",
43
44
  "@google-cloud/bigquery": "^8.3.1",
44
45
  "@looker/sdk": "^26.8.0",
45
46
  "@looker/sdk-node": "^26.8.0",