@kaelio/ktx 0.7.0 → 0.9.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.
- package/assets/python/{kaelio_ktx-0.7.0-py3-none-any.whl → kaelio_ktx-0.9.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli-program.js +7 -0
- package/dist/cli-runtime.js +50 -3
- package/dist/command-schemas.d.ts +1 -1
- package/dist/command-tree.js +5 -1
- package/dist/commands/completion-commands.d.ts +3 -0
- package/dist/commands/completion-commands.js +38 -0
- package/dist/commands/ingest-commands.js +0 -4
- package/dist/commands/knowledge-commands.js +15 -2
- package/dist/commands/setup-commands.js +3 -3
- package/dist/commands/sl-commands.js +19 -7
- package/dist/completion/complete-engine.d.ts +19 -0
- package/dist/completion/complete-engine.js +128 -0
- package/dist/completion/completion-scripts.d.ts +1 -0
- package/dist/completion/completion-scripts.js +36 -0
- package/dist/completion/dynamic-candidates.d.ts +6 -0
- package/dist/completion/dynamic-candidates.js +98 -0
- package/dist/connection-drivers.d.ts +3 -0
- package/dist/connection-drivers.js +17 -0
- package/dist/connection-recovery.d.ts +34 -0
- package/dist/connection-recovery.js +82 -0
- package/dist/connection.js +3 -1
- package/dist/context/ingest/adapters/historic-sql/bigquery-query-history-reader.js +71 -20
- package/dist/context/ingest/adapters/historic-sql/chunk-unified.js +2 -1
- package/dist/context/ingest/adapters/historic-sql/connection-dialect.d.ts +9 -0
- package/dist/context/ingest/adapters/historic-sql/connection-dialect.js +15 -4
- package/dist/context/ingest/adapters/historic-sql/pattern-inputs.js +8 -2
- package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.d.ts +29 -0
- package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +190 -0
- package/dist/context/ingest/adapters/historic-sql/scope-floor.d.ts +18 -0
- package/dist/context/ingest/adapters/historic-sql/scope-floor.js +229 -0
- package/dist/context/ingest/adapters/historic-sql/scope-membership.d.ts +8 -0
- package/dist/context/ingest/adapters/historic-sql/scope-membership.js +29 -0
- package/dist/context/ingest/adapters/historic-sql/snowflake-query-history-reader.js +68 -19
- package/dist/context/ingest/adapters/historic-sql/stage-unified.js +57 -50
- package/dist/context/ingest/adapters/historic-sql/types.d.ts +36 -3
- package/dist/context/ingest/adapters/historic-sql/types.js +14 -2
- package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
- package/dist/context/ingest/ingest-bundle.runner.d.ts +8 -0
- package/dist/context/ingest/ingest-bundle.runner.js +72 -15
- package/dist/context/ingest/ingest-profile.d.ts +102 -0
- package/dist/context/ingest/ingest-profile.js +306 -0
- package/dist/context/ingest/isolated-diff/patch-integrator.js +75 -5
- package/dist/context/ingest/isolated-diff/work-unit-executor.js +25 -2
- package/dist/context/ingest/local-adapters.js +21 -4
- package/dist/context/ingest/local-bundle-runtime.js +4 -2
- package/dist/context/ingest/local-ingest.d.ts +1 -1
- package/dist/context/ingest/local-ingest.js +6 -4
- package/dist/context/ingest/memory-flow/events.js +2 -1
- package/dist/context/ingest/ports.d.ts +2 -0
- package/dist/context/ingest/reports.d.ts +3 -0
- package/dist/context/ingest/reports.js +10 -0
- package/dist/context/ingest/stages/stage-3-work-units.d.ts +3 -1
- package/dist/context/ingest/stages/stage-3-work-units.js +2 -0
- package/dist/context/ingest/stages/stage-4-reconciliation.d.ts +2 -1
- package/dist/context/ingest/stages/stage-4-reconciliation.js +1 -1
- package/dist/context/ingest/tools/tool-call-logger.d.ts +6 -0
- package/dist/context/ingest/tools/tool-call-logger.js +36 -1
- package/dist/context/llm/ai-sdk-runtime.js +32 -3
- package/dist/context/llm/claude-code-runtime.js +35 -2
- package/dist/context/llm/codex-exec-events.d.ts +20 -0
- package/dist/context/llm/codex-exec-events.js +155 -0
- package/dist/context/llm/codex-isolation.d.ts +3 -0
- package/dist/context/llm/codex-isolation.js +5 -0
- package/dist/context/llm/codex-mcp-runtime-server.d.ts +24 -0
- package/dist/context/llm/codex-mcp-runtime-server.js +51 -0
- package/dist/context/llm/codex-models.d.ts +2 -0
- package/dist/context/llm/codex-models.js +17 -0
- package/dist/context/llm/codex-runtime-config.d.ts +16 -0
- package/dist/context/llm/codex-runtime-config.js +19 -0
- package/dist/context/llm/codex-runtime.d.ts +37 -0
- package/dist/context/llm/codex-runtime.js +304 -0
- package/dist/context/llm/codex-sdk-runner.d.ts +21 -0
- package/dist/context/llm/codex-sdk-runner.js +63 -0
- package/dist/context/llm/local-config.d.ts +2 -0
- package/dist/context/llm/local-config.js +12 -1
- package/dist/context/llm/runtime-port.d.ts +25 -0
- package/dist/context/mcp/context-tools.d.ts +2 -1
- package/dist/context/mcp/context-tools.js +82 -15
- package/dist/context/mcp/server.js +4 -0
- package/dist/context/mcp/types.d.ts +15 -1
- package/dist/context/project/config.d.ts +3 -0
- package/dist/context/project/config.js +6 -2
- package/dist/context/project/driver-schemas.js +1 -1
- package/dist/context/search/discover.js +4 -3
- package/dist/context/sl/local-sl.d.ts +15 -0
- package/dist/context/sl/local-sl.js +30 -0
- package/dist/context/sql-analysis/http-sql-analysis-port.js +32 -2
- package/dist/context/sql-analysis/ports.d.ts +12 -2
- package/dist/context/tools/context-candidate-mark.tool.d.ts +2 -2
- package/dist/context/wiki/local-knowledge.d.ts +10 -0
- package/dist/context/wiki/local-knowledge.js +22 -0
- package/dist/context-build-view.d.ts +0 -3
- package/dist/context-build-view.js +5 -39
- package/dist/ingest.js +7 -10
- package/dist/io/buffered-command-io.d.ts +11 -0
- package/dist/io/buffered-command-io.js +28 -0
- package/dist/knowledge.d.ts +5 -0
- package/dist/knowledge.js +10 -1
- package/dist/llm/types.d.ts +1 -1
- package/dist/local-adapters.d.ts +10 -2
- package/dist/local-adapters.js +19 -3
- package/dist/next-steps.js +1 -2
- package/dist/progress-port-adapter.d.ts +6 -0
- package/dist/progress-port-adapter.js +18 -0
- package/dist/public-ingest-copy.js +1 -1
- package/dist/public-ingest.d.ts +20 -8
- package/dist/public-ingest.js +198 -61
- package/dist/scan.js +3 -1
- package/dist/setup-context.d.ts +2 -0
- package/dist/setup-context.js +138 -64
- package/dist/setup-databases.d.ts +17 -1
- package/dist/setup-databases.js +366 -326
- package/dist/setup-models.d.ts +10 -1
- package/dist/setup-models.js +90 -2
- package/dist/setup-ready-menu.d.ts +16 -2
- package/dist/setup-ready-menu.js +37 -5
- package/dist/setup-sources.js +141 -33
- package/dist/setup.js +24 -12
- package/dist/skills/analytics/SKILL.md +6 -1
- package/dist/sl.d.ts +6 -1
- package/dist/sl.js +32 -8
- package/dist/status-project.d.ts +11 -0
- package/dist/status-project.js +50 -1
- package/dist/telemetry/command-hook.d.ts +1 -0
- package/dist/telemetry/command-hook.js +3 -1
- package/dist/telemetry/emitter.js +1 -1
- package/dist/telemetry/events.d.ts +15 -9
- package/dist/telemetry/events.js +17 -5
- package/dist/telemetry/identity.d.ts +1 -2
- package/dist/telemetry/identity.js +13 -10
- package/dist/telemetry/index.d.ts +13 -1
- package/dist/telemetry/index.js +18 -3
- package/dist/telemetry/scrubber.d.ts +10 -0
- package/dist/telemetry/scrubber.js +20 -0
- package/package.json +20 -19
- package/dist/ingest-depth.d.ts +0 -8
- package/dist/ingest-depth.js +0 -56
- package/dist/setup-database-context-depth.d.ts +0 -23
- package/dist/setup-database-context-depth.js +0 -84
package/dist/sl.d.ts
CHANGED
|
@@ -23,10 +23,15 @@ export type KtxSlArgs = {
|
|
|
23
23
|
output?: string;
|
|
24
24
|
json?: boolean;
|
|
25
25
|
cliVersion: string;
|
|
26
|
+
} | {
|
|
27
|
+
command: 'read';
|
|
28
|
+
projectDir: string;
|
|
29
|
+
connectionId?: string;
|
|
30
|
+
sourceName: string;
|
|
26
31
|
} | {
|
|
27
32
|
command: 'validate';
|
|
28
33
|
projectDir: string;
|
|
29
|
-
connectionId
|
|
34
|
+
connectionId?: string;
|
|
30
35
|
sourceName: string;
|
|
31
36
|
} | {
|
|
32
37
|
command: 'query';
|
package/dist/sl.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createDefaultLocalQueryExecutor } from './context/connections/local-que
|
|
|
3
3
|
import { KtxIngestEmbeddingPortAdapter } from './context/llm/embedding-port.js';
|
|
4
4
|
import { loadKtxProject } from './context/project/project.js';
|
|
5
5
|
import { compileLocalSlQuery } from './context/sl/local-query.js';
|
|
6
|
-
import { listLocalSlSources,
|
|
6
|
+
import { listLocalSlSources, resolveLocalSlSource, searchLocalSlSources as defaultSearchLocalSlSources, validateLocalSlSource, } from './context/sl/local-sl.js';
|
|
7
7
|
import { resolveProjectEmbeddingProvider, } from './embedding-resolution.js';
|
|
8
8
|
import { createManagedPythonSemanticLayerComputePort, } from './managed-python-command.js';
|
|
9
9
|
import { profileMark } from './startup-profile.js';
|
|
@@ -85,6 +85,9 @@ async function readSlQueryFile(path) {
|
|
|
85
85
|
}
|
|
86
86
|
return parsed;
|
|
87
87
|
}
|
|
88
|
+
function ambiguousSourceMessage(sourceName, connectionIds) {
|
|
89
|
+
return `Source '${sourceName}' exists in multiple connections: ${connectionIds.join(', ')}. Re-run with --connection-id <id>.`;
|
|
90
|
+
}
|
|
88
91
|
export async function runKtxSl(args, io = process, deps = {}) {
|
|
89
92
|
const startedAt = performance.now();
|
|
90
93
|
let queryForTelemetry;
|
|
@@ -132,17 +135,38 @@ export async function runKtxSl(args, io = process, deps = {}) {
|
|
|
132
135
|
});
|
|
133
136
|
return 0;
|
|
134
137
|
}
|
|
138
|
+
if (args.command === 'read') {
|
|
139
|
+
const resolved = await resolveLocalSlSource(project, {
|
|
140
|
+
connectionId: args.connectionId,
|
|
141
|
+
sourceName: args.sourceName,
|
|
142
|
+
});
|
|
143
|
+
if (resolved.kind === 'not-found') {
|
|
144
|
+
throw new Error(args.connectionId !== undefined
|
|
145
|
+
? `No semantic-layer source '${args.sourceName}' for connection '${args.connectionId}'`
|
|
146
|
+
: `No semantic-layer source '${args.sourceName}'`);
|
|
147
|
+
}
|
|
148
|
+
if (resolved.kind === 'ambiguous') {
|
|
149
|
+
throw new Error(ambiguousSourceMessage(args.sourceName, resolved.connectionIds));
|
|
150
|
+
}
|
|
151
|
+
io.stdout.write(resolved.source.yaml);
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
135
154
|
if (args.command === 'validate') {
|
|
136
|
-
const
|
|
155
|
+
const resolved = await resolveLocalSlSource(project, {
|
|
137
156
|
connectionId: args.connectionId,
|
|
138
157
|
sourceName: args.sourceName,
|
|
139
158
|
});
|
|
140
|
-
if (
|
|
141
|
-
throw new Error(
|
|
159
|
+
if (resolved.kind === 'not-found') {
|
|
160
|
+
throw new Error(args.connectionId !== undefined
|
|
161
|
+
? `Semantic-layer source "${args.connectionId}/${args.sourceName}" was not found`
|
|
162
|
+
: `Semantic-layer source "${args.sourceName}" was not found`);
|
|
142
163
|
}
|
|
143
|
-
|
|
164
|
+
if (resolved.kind === 'ambiguous') {
|
|
165
|
+
throw new Error(ambiguousSourceMessage(args.sourceName, resolved.connectionIds));
|
|
166
|
+
}
|
|
167
|
+
const result = await validateLocalSlSource(resolved.source.yaml, {
|
|
144
168
|
project,
|
|
145
|
-
connectionId:
|
|
169
|
+
connectionId: resolved.source.connectionId,
|
|
146
170
|
sourceName: args.sourceName,
|
|
147
171
|
});
|
|
148
172
|
await emitTelemetryEvent({
|
|
@@ -150,7 +174,7 @@ export async function runKtxSl(args, io = process, deps = {}) {
|
|
|
150
174
|
projectDir: args.projectDir,
|
|
151
175
|
io,
|
|
152
176
|
fields: {
|
|
153
|
-
sourceCount:
|
|
177
|
+
sourceCount: 1,
|
|
154
178
|
modelCount: 0,
|
|
155
179
|
validationErrorCount: result.valid ? 0 : result.errors.length,
|
|
156
180
|
outcome: result.valid ? 'ok' : 'error',
|
|
@@ -163,7 +187,7 @@ export async function runKtxSl(args, io = process, deps = {}) {
|
|
|
163
187
|
}
|
|
164
188
|
return 1;
|
|
165
189
|
}
|
|
166
|
-
io.stdout.write(`Valid semantic-layer source: ${
|
|
190
|
+
io.stdout.write(`Valid semantic-layer source: ${resolved.source.connectionId}/${args.sourceName}\n`);
|
|
167
191
|
return 0;
|
|
168
192
|
}
|
|
169
193
|
if (args.command === 'query') {
|
package/dist/status-project.d.ts
CHANGED
|
@@ -62,6 +62,16 @@ type ClaudeCodeAuthProbe = (input: {
|
|
|
62
62
|
ok: false;
|
|
63
63
|
message: string;
|
|
64
64
|
}>;
|
|
65
|
+
type CodexAuthProbe = (input: {
|
|
66
|
+
projectDir: string;
|
|
67
|
+
model: string;
|
|
68
|
+
}) => Promise<{
|
|
69
|
+
ok: true;
|
|
70
|
+
} | {
|
|
71
|
+
ok: false;
|
|
72
|
+
message: string;
|
|
73
|
+
fix: string;
|
|
74
|
+
}>;
|
|
65
75
|
interface LocalStatsIngestPerConnection {
|
|
66
76
|
connectionId: string;
|
|
67
77
|
adapter: string;
|
|
@@ -135,6 +145,7 @@ export interface BuildProjectStatusOptions {
|
|
|
135
145
|
env?: NodeJS.ProcessEnv;
|
|
136
146
|
queryHistoryReadinessProbe?: HistoricSqlReadinessProbe;
|
|
137
147
|
claudeCodeAuthProbe?: ClaudeCodeAuthProbe;
|
|
148
|
+
codexAuthProbe?: CodexAuthProbe;
|
|
138
149
|
configIssues?: KtxConfigIssue[];
|
|
139
150
|
fast?: boolean;
|
|
140
151
|
useSpinner?: boolean;
|
package/dist/status-project.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { stat as statAsync, readdir as readdirAsync } from 'node:fs/promises';
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
3
|
import { runClaudeCodeAuthProbe } from './context/llm/claude-code-runtime.js';
|
|
4
|
+
import { CODEX_ISOLATION_WARNING, CODEX_ISOLATION_WARNING_FIX, } from './context/llm/codex-isolation.js';
|
|
5
|
+
import { runCodexAuthProbe } from './context/llm/codex-runtime.js';
|
|
4
6
|
import { ktxLocalStateDbPath } from './context/project/local-state-db.js';
|
|
5
7
|
import { isQueryHistoryEnabled, queryHistoryDialectForConnection, } from './context/ingest/adapters/historic-sql/connection-dialect.js';
|
|
6
8
|
import { historicSqlProbeCatalogName, runHistoricSqlReadinessProbe, } from './context/ingest/historic-sql-probes.js';
|
|
@@ -49,6 +51,18 @@ async function buildLlmStatus(config, options) {
|
|
|
49
51
|
fix: 'Run: ktx setup (choose an LLM provider)',
|
|
50
52
|
};
|
|
51
53
|
}
|
|
54
|
+
// The runtime (resolveModelSlots) hard-requires llm.models.default for every
|
|
55
|
+
// non-none backend; without it ingest/scan/memory throw. Report that here so
|
|
56
|
+
// status never marks a project ready that the runtime would refuse to run.
|
|
57
|
+
if (!model || model.trim().length === 0) {
|
|
58
|
+
return {
|
|
59
|
+
backend,
|
|
60
|
+
model,
|
|
61
|
+
status: 'fail',
|
|
62
|
+
detail: `llm.models.default is required for backend "${backend}"`,
|
|
63
|
+
fix: 'Set llm.models.default in ktx.yaml, then rerun `ktx status` (or rerun `ktx setup`).',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
52
66
|
if (backend === 'anthropic') {
|
|
53
67
|
const ref = config.provider.anthropic?.api_key;
|
|
54
68
|
const resolved = resolveRef(ref, env);
|
|
@@ -90,7 +104,7 @@ async function buildLlmStatus(config, options) {
|
|
|
90
104
|
};
|
|
91
105
|
}
|
|
92
106
|
if (backend === 'claude-code') {
|
|
93
|
-
const modelName = model
|
|
107
|
+
const modelName = model;
|
|
94
108
|
if (options.fast === true) {
|
|
95
109
|
return {
|
|
96
110
|
backend,
|
|
@@ -117,6 +131,34 @@ async function buildLlmStatus(config, options) {
|
|
|
117
131
|
fix: 'Authenticate Claude Code locally with the Claude Code CLI, then rerun `ktx status`.',
|
|
118
132
|
};
|
|
119
133
|
}
|
|
134
|
+
if (backend === 'codex') {
|
|
135
|
+
const modelName = model;
|
|
136
|
+
if (options.fast === true) {
|
|
137
|
+
return {
|
|
138
|
+
backend,
|
|
139
|
+
model: modelName,
|
|
140
|
+
status: 'skipped',
|
|
141
|
+
detail: 'auth probe skipped (--fast)',
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const probe = options.codexAuthProbe ?? runCodexAuthProbe;
|
|
145
|
+
const auth = await withSpinner(options.useSpinner === true, 'Probing Codex authentication', () => probe({ projectDir: options.projectDir, model: modelName }));
|
|
146
|
+
if (auth.ok) {
|
|
147
|
+
return {
|
|
148
|
+
backend,
|
|
149
|
+
model: modelName,
|
|
150
|
+
status: 'ok',
|
|
151
|
+
detail: 'local Codex session authenticated',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
backend,
|
|
156
|
+
model: modelName,
|
|
157
|
+
status: 'fail',
|
|
158
|
+
detail: auth.message,
|
|
159
|
+
fix: auth.fix,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
120
162
|
return { backend, model, status: 'warn', detail: 'unknown LLM backend' };
|
|
121
163
|
}
|
|
122
164
|
function buildEmbeddingsStatus(config, env) {
|
|
@@ -378,6 +420,12 @@ function buildWarnings(config, connections, llm, embeddings) {
|
|
|
378
420
|
fix: formatClaudeCodePromptCachingFix(),
|
|
379
421
|
});
|
|
380
422
|
}
|
|
423
|
+
if (llm.backend === 'codex') {
|
|
424
|
+
warnings.push({
|
|
425
|
+
message: CODEX_ISOLATION_WARNING,
|
|
426
|
+
fix: CODEX_ISOLATION_WARNING_FIX,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
381
429
|
return warnings;
|
|
382
430
|
}
|
|
383
431
|
function buildVerdict(llm, embeddings, connections, queryHistory, warnings) {
|
|
@@ -625,6 +673,7 @@ export async function buildProjectStatus(project, options = {}) {
|
|
|
625
673
|
projectDir: project.projectDir,
|
|
626
674
|
env,
|
|
627
675
|
claudeCodeAuthProbe: options.claudeCodeAuthProbe,
|
|
676
|
+
codexAuthProbe: options.codexAuthProbe,
|
|
628
677
|
fast: options.fast,
|
|
629
678
|
useSpinner: options.useSpinner,
|
|
630
679
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { scrubErrorClass } from './scrubber.js';
|
|
1
|
+
import { formatErrorDetail, scrubErrorClass } from './scrubber.js';
|
|
2
2
|
let activeCommandSpan;
|
|
3
3
|
export function beginCommandSpan(input) {
|
|
4
4
|
activeCommandSpan = input;
|
|
@@ -10,11 +10,13 @@ export function completeCommandSpan(input) {
|
|
|
10
10
|
return undefined;
|
|
11
11
|
}
|
|
12
12
|
const errorClass = input.error ? scrubErrorClass(input.error) : undefined;
|
|
13
|
+
const errorDetail = input.error ? formatErrorDetail(input.error) : undefined;
|
|
13
14
|
return {
|
|
14
15
|
commandPath: span.commandPath,
|
|
15
16
|
durationMs: Math.max(0, input.completedAt - span.startedAt),
|
|
16
17
|
outcome: input.outcome,
|
|
17
18
|
...(errorClass ? { errorClass } : {}),
|
|
19
|
+
...(errorDetail ? { errorDetail } : {}),
|
|
18
20
|
flagsPresent: span.flagsPresent,
|
|
19
21
|
hasProject: span.hasProject,
|
|
20
22
|
projectDir: span.projectDir,
|
|
@@ -17,7 +17,7 @@ async function getPostHogClient(projectApiKey, host) {
|
|
|
17
17
|
return null;
|
|
18
18
|
}
|
|
19
19
|
clientPromise ??= import('posthog-node')
|
|
20
|
-
.then(({ PostHog }) => new PostHog(projectApiKey, { host, flushAt: 1, flushInterval: 0 }))
|
|
20
|
+
.then(({ PostHog }) => new PostHog(projectApiKey, { host, flushAt: 1, flushInterval: 0, disableGeoip: false }))
|
|
21
21
|
.catch(() => null);
|
|
22
22
|
return await clientPromise;
|
|
23
23
|
}
|
|
@@ -44,6 +44,7 @@ export declare const telemetryEventSchemas: {
|
|
|
44
44
|
aborted: "aborted";
|
|
45
45
|
}>;
|
|
46
46
|
errorClass: z.ZodOptional<z.ZodString>;
|
|
47
|
+
errorDetail: z.ZodOptional<z.ZodString>;
|
|
47
48
|
flagsPresent: z.ZodRecord<z.ZodString, z.ZodBoolean>;
|
|
48
49
|
hasProject: z.ZodBoolean;
|
|
49
50
|
projectGroupAttached: z.ZodBoolean;
|
|
@@ -67,9 +68,8 @@ export declare const telemetryEventSchemas: {
|
|
|
67
68
|
databases: "databases";
|
|
68
69
|
context: "context";
|
|
69
70
|
runtime: "runtime";
|
|
70
|
-
agents: "agents";
|
|
71
71
|
secrets: "secrets";
|
|
72
|
-
|
|
72
|
+
agents: "agents";
|
|
73
73
|
"demo-tour": "demo-tour";
|
|
74
74
|
}>;
|
|
75
75
|
outcome: z.ZodEnum<{
|
|
@@ -78,6 +78,7 @@ export declare const telemetryEventSchemas: {
|
|
|
78
78
|
abandoned: "abandoned";
|
|
79
79
|
}>;
|
|
80
80
|
durationMs: z.ZodNumber;
|
|
81
|
+
errorDetail: z.ZodOptional<z.ZodString>;
|
|
81
82
|
}, z.core.$strict>;
|
|
82
83
|
readonly connection_added: z.ZodObject<{
|
|
83
84
|
cliVersion: z.ZodString;
|
|
@@ -111,6 +112,7 @@ export declare const telemetryEventSchemas: {
|
|
|
111
112
|
error: "error";
|
|
112
113
|
}>;
|
|
113
114
|
errorClass: z.ZodOptional<z.ZodString>;
|
|
115
|
+
errorDetail: z.ZodOptional<z.ZodString>;
|
|
114
116
|
durationMs: z.ZodNumber;
|
|
115
117
|
serverVersion: z.ZodOptional<z.ZodString>;
|
|
116
118
|
}, z.core.$strict>;
|
|
@@ -164,6 +166,7 @@ export declare const telemetryEventSchemas: {
|
|
|
164
166
|
error: "error";
|
|
165
167
|
}>;
|
|
166
168
|
errorClass: z.ZodOptional<z.ZodString>;
|
|
169
|
+
errorDetail: z.ZodOptional<z.ZodString>;
|
|
167
170
|
}, z.core.$strict>;
|
|
168
171
|
readonly scan_completed: z.ZodObject<{
|
|
169
172
|
cliVersion: z.ZodString;
|
|
@@ -187,6 +190,7 @@ export declare const telemetryEventSchemas: {
|
|
|
187
190
|
error: "error";
|
|
188
191
|
}>;
|
|
189
192
|
errorClass: z.ZodOptional<z.ZodString>;
|
|
193
|
+
errorDetail: z.ZodOptional<z.ZodString>;
|
|
190
194
|
}, z.core.$strict>;
|
|
191
195
|
readonly sl_validate_completed: z.ZodObject<{
|
|
192
196
|
cliVersion: z.ZodString;
|
|
@@ -299,7 +303,9 @@ export declare const telemetryEventSchemas: {
|
|
|
299
303
|
}>;
|
|
300
304
|
durationMs: z.ZodNumber;
|
|
301
305
|
errorClass: z.ZodOptional<z.ZodString>;
|
|
302
|
-
sampleRate: z.ZodLiteral<
|
|
306
|
+
sampleRate: z.ZodLiteral<1>;
|
|
307
|
+
mcpClientName: z.ZodOptional<z.ZodString>;
|
|
308
|
+
mcpClientVersion: z.ZodOptional<z.ZodString>;
|
|
303
309
|
}, z.core.$strict>;
|
|
304
310
|
readonly daemon_started: z.ZodObject<{
|
|
305
311
|
cliVersion: z.ZodString;
|
|
@@ -389,11 +395,11 @@ export declare const telemetryEventCatalog: readonly [{
|
|
|
389
395
|
}, {
|
|
390
396
|
readonly name: "command";
|
|
391
397
|
readonly description: "Emitted once for each Commander action that reaches preAction.";
|
|
392
|
-
readonly fields: readonly ["commandPath", "durationMs", "outcome", "errorClass", "flagsPresent", "hasProject", "projectGroupAttached"];
|
|
398
|
+
readonly fields: readonly ["commandPath", "durationMs", "outcome", "errorClass", "errorDetail", "flagsPresent", "hasProject", "projectGroupAttached"];
|
|
393
399
|
}, {
|
|
394
400
|
readonly name: "setup_step";
|
|
395
401
|
readonly description: "Emitted after an interactive setup step completes, skips, or aborts.";
|
|
396
|
-
readonly fields: readonly ["step", "outcome", "durationMs"];
|
|
402
|
+
readonly fields: readonly ["step", "outcome", "durationMs", "errorDetail"];
|
|
397
403
|
}, {
|
|
398
404
|
readonly name: "connection_added";
|
|
399
405
|
readonly description: "Emitted when setup writes a database, source, or demo connection.";
|
|
@@ -401,7 +407,7 @@ export declare const telemetryEventCatalog: readonly [{
|
|
|
401
407
|
}, {
|
|
402
408
|
readonly name: "connection_test";
|
|
403
409
|
readonly description: "Emitted after ktx connection test completes.";
|
|
404
|
-
readonly fields: readonly ["driver", "isDemoConnection", "outcome", "errorClass", "durationMs", "serverVersion"];
|
|
410
|
+
readonly fields: readonly ["driver", "isDemoConnection", "outcome", "errorClass", "errorDetail", "durationMs", "serverVersion"];
|
|
405
411
|
}, {
|
|
406
412
|
readonly name: "project_stack_snapshot";
|
|
407
413
|
readonly description: "Emitted after commands that can summarize the local project stack.";
|
|
@@ -409,11 +415,11 @@ export declare const telemetryEventCatalog: readonly [{
|
|
|
409
415
|
}, {
|
|
410
416
|
readonly name: "ingest_completed";
|
|
411
417
|
readonly description: "Emitted after a public ingest target completes.";
|
|
412
|
-
readonly fields: readonly ["driver", "isDemoConnection", "schemaCount", "tableCount", "columnCount", "rowsBucket", "durationMs", "outcome", "errorClass"];
|
|
418
|
+
readonly fields: readonly ["driver", "isDemoConnection", "schemaCount", "tableCount", "columnCount", "rowsBucket", "durationMs", "outcome", "errorClass", "errorDetail"];
|
|
413
419
|
}, {
|
|
414
420
|
readonly name: "scan_completed";
|
|
415
421
|
readonly description: "Emitted after schema scan or relationship inference completes.";
|
|
416
|
-
readonly fields: readonly ["driver", "tableCount", "columnCount", "inferredFkCount", "declaredFkCount", "durationMs", "outcome", "errorClass"];
|
|
422
|
+
readonly fields: readonly ["driver", "tableCount", "columnCount", "inferredFkCount", "declaredFkCount", "durationMs", "outcome", "errorClass", "errorDetail"];
|
|
417
423
|
}, {
|
|
418
424
|
readonly name: "sl_validate_completed";
|
|
419
425
|
readonly description: "Emitted after ktx sl validate completes.";
|
|
@@ -433,7 +439,7 @@ export declare const telemetryEventCatalog: readonly [{
|
|
|
433
439
|
}, {
|
|
434
440
|
readonly name: "mcp_request_completed";
|
|
435
441
|
readonly description: "Emitted for sampled MCP tool requests.";
|
|
436
|
-
readonly fields: readonly ["toolName", "outcome", "durationMs", "errorClass", "sampleRate"];
|
|
442
|
+
readonly fields: readonly ["toolName", "outcome", "durationMs", "errorClass", "sampleRate", "mcpClientName", "mcpClientVersion"];
|
|
437
443
|
}, {
|
|
438
444
|
readonly name: "daemon_started";
|
|
439
445
|
readonly description: "Emitted when the long-lived ktx-daemon HTTP server starts.";
|
package/dist/telemetry/events.js
CHANGED
|
@@ -18,6 +18,7 @@ const commandSchema = telemetryCommonEnvelopeSchema
|
|
|
18
18
|
durationMs: z.number().nonnegative(),
|
|
19
19
|
outcome: z.enum(['ok', 'error', 'aborted']),
|
|
20
20
|
errorClass: z.string().optional(),
|
|
21
|
+
errorDetail: z.string().max(1000).optional(),
|
|
21
22
|
flagsPresent: z.record(z.string(), z.boolean()),
|
|
22
23
|
hasProject: z.boolean(),
|
|
23
24
|
projectGroupAttached: z.boolean(),
|
|
@@ -33,7 +34,6 @@ const setupStepSchema = telemetryCommonEnvelopeSchema
|
|
|
33
34
|
'embeddings',
|
|
34
35
|
'secrets',
|
|
35
36
|
'databases',
|
|
36
|
-
'database-context-depth',
|
|
37
37
|
'sources',
|
|
38
38
|
'context',
|
|
39
39
|
'agents',
|
|
@@ -41,6 +41,7 @@ const setupStepSchema = telemetryCommonEnvelopeSchema
|
|
|
41
41
|
]),
|
|
42
42
|
outcome: z.enum(['completed', 'skipped', 'abandoned']),
|
|
43
43
|
durationMs: z.number().nonnegative(),
|
|
44
|
+
errorDetail: z.string().max(1000).optional(),
|
|
44
45
|
})
|
|
45
46
|
.strict();
|
|
46
47
|
const connectionAddedSchema = telemetryCommonEnvelopeSchema
|
|
@@ -55,6 +56,7 @@ const connectionTestSchema = telemetryCommonEnvelopeSchema
|
|
|
55
56
|
isDemoConnection: z.boolean(),
|
|
56
57
|
outcome: outcomeSchema,
|
|
57
58
|
errorClass: z.string().optional(),
|
|
59
|
+
errorDetail: z.string().max(1000).optional(),
|
|
58
60
|
durationMs: z.number().nonnegative(),
|
|
59
61
|
serverVersion: z.string().optional(),
|
|
60
62
|
})
|
|
@@ -81,6 +83,7 @@ const ingestCompletedSchema = telemetryCommonEnvelopeSchema
|
|
|
81
83
|
durationMs: z.number().nonnegative(),
|
|
82
84
|
outcome: outcomeSchema,
|
|
83
85
|
errorClass: z.string().optional(),
|
|
86
|
+
errorDetail: z.string().max(1000).optional(),
|
|
84
87
|
})
|
|
85
88
|
.strict();
|
|
86
89
|
const scanCompletedSchema = telemetryCommonEnvelopeSchema
|
|
@@ -93,6 +96,7 @@ const scanCompletedSchema = telemetryCommonEnvelopeSchema
|
|
|
93
96
|
durationMs: z.number().nonnegative(),
|
|
94
97
|
outcome: outcomeSchema,
|
|
95
98
|
errorClass: z.string().optional(),
|
|
99
|
+
errorDetail: z.string().max(1000).optional(),
|
|
96
100
|
})
|
|
97
101
|
.strict();
|
|
98
102
|
const slValidateCompletedSchema = telemetryCommonEnvelopeSchema
|
|
@@ -141,7 +145,12 @@ const mcpRequestCompletedSchema = telemetryCommonEnvelopeSchema
|
|
|
141
145
|
outcome: outcomeSchema,
|
|
142
146
|
durationMs: z.number().nonnegative(),
|
|
143
147
|
errorClass: z.string().optional(),
|
|
144
|
-
sampleRate: z.literal(
|
|
148
|
+
sampleRate: z.literal(1),
|
|
149
|
+
// Raw, client-tool-controlled identity from the MCP initialize handshake
|
|
150
|
+
// (clientInfo.name/version). Optional: clients may omit clientInfo. Stored
|
|
151
|
+
// verbatim — normalize the free-form names at query time, not at write time.
|
|
152
|
+
mcpClientName: z.string().optional(),
|
|
153
|
+
mcpClientVersion: z.string().optional(),
|
|
145
154
|
})
|
|
146
155
|
.strict();
|
|
147
156
|
const daemonStartedSchema = telemetryCommonEnvelopeSchema
|
|
@@ -211,6 +220,7 @@ export const telemetryEventCatalog = [
|
|
|
211
220
|
'durationMs',
|
|
212
221
|
'outcome',
|
|
213
222
|
'errorClass',
|
|
223
|
+
'errorDetail',
|
|
214
224
|
'flagsPresent',
|
|
215
225
|
'hasProject',
|
|
216
226
|
'projectGroupAttached',
|
|
@@ -219,7 +229,7 @@ export const telemetryEventCatalog = [
|
|
|
219
229
|
{
|
|
220
230
|
name: 'setup_step',
|
|
221
231
|
description: 'Emitted after an interactive setup step completes, skips, or aborts.',
|
|
222
|
-
fields: ['step', 'outcome', 'durationMs'],
|
|
232
|
+
fields: ['step', 'outcome', 'durationMs', 'errorDetail'],
|
|
223
233
|
},
|
|
224
234
|
{
|
|
225
235
|
name: 'connection_added',
|
|
@@ -229,7 +239,7 @@ export const telemetryEventCatalog = [
|
|
|
229
239
|
{
|
|
230
240
|
name: 'connection_test',
|
|
231
241
|
description: 'Emitted after ktx connection test completes.',
|
|
232
|
-
fields: ['driver', 'isDemoConnection', 'outcome', 'errorClass', 'durationMs', 'serverVersion'],
|
|
242
|
+
fields: ['driver', 'isDemoConnection', 'outcome', 'errorClass', 'errorDetail', 'durationMs', 'serverVersion'],
|
|
233
243
|
},
|
|
234
244
|
{
|
|
235
245
|
name: 'project_stack_snapshot',
|
|
@@ -249,6 +259,7 @@ export const telemetryEventCatalog = [
|
|
|
249
259
|
'durationMs',
|
|
250
260
|
'outcome',
|
|
251
261
|
'errorClass',
|
|
262
|
+
'errorDetail',
|
|
252
263
|
],
|
|
253
264
|
},
|
|
254
265
|
{
|
|
@@ -263,6 +274,7 @@ export const telemetryEventCatalog = [
|
|
|
263
274
|
'durationMs',
|
|
264
275
|
'outcome',
|
|
265
276
|
'errorClass',
|
|
277
|
+
'errorDetail',
|
|
266
278
|
],
|
|
267
279
|
},
|
|
268
280
|
{
|
|
@@ -304,7 +316,7 @@ export const telemetryEventCatalog = [
|
|
|
304
316
|
{
|
|
305
317
|
name: 'mcp_request_completed',
|
|
306
318
|
description: 'Emitted for sampled MCP tool requests.',
|
|
307
|
-
fields: ['toolName', 'outcome', 'durationMs', 'errorClass', 'sampleRate'],
|
|
319
|
+
fields: ['toolName', 'outcome', 'durationMs', 'errorClass', 'sampleRate', 'mcpClientName', 'mcpClientVersion'],
|
|
308
320
|
},
|
|
309
321
|
{
|
|
310
322
|
name: 'daemon_started',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @internal */
|
|
2
|
-
export declare const TELEMETRY_NOTICE = "ktx collects
|
|
2
|
+
export declare const TELEMETRY_NOTICE = "ktx collects usage data to improve the product. Opt out: set KTX_TELEMETRY_DISABLED=1.";
|
|
3
3
|
/** @internal */
|
|
4
4
|
export interface TelemetryIdentityEnv {
|
|
5
5
|
KTX_TELEMETRY_DISABLED?: string;
|
|
@@ -11,7 +11,6 @@ export interface TelemetryIdentityEnv {
|
|
|
11
11
|
export interface LoadTelemetryIdentityOptions {
|
|
12
12
|
homeDir?: string;
|
|
13
13
|
env?: TelemetryIdentityEnv;
|
|
14
|
-
stdoutIsTTY: boolean;
|
|
15
14
|
stderr: {
|
|
16
15
|
write(chunk: string): void;
|
|
17
16
|
};
|
|
@@ -4,7 +4,7 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
/** @internal */
|
|
7
|
-
export const TELEMETRY_NOTICE = 'ktx collects
|
|
7
|
+
export const TELEMETRY_NOTICE = 'ktx collects usage data to improve the product. Opt out: set KTX_TELEMETRY_DISABLED=1.';
|
|
8
8
|
const NOTICE_VERSION = 1;
|
|
9
9
|
const telemetryFileSchema = z
|
|
10
10
|
.object({
|
|
@@ -41,16 +41,13 @@ async function writeTelemetryFile(path, value) {
|
|
|
41
41
|
export async function loadTelemetryIdentity(options) {
|
|
42
42
|
const env = options.env ?? process.env;
|
|
43
43
|
const path = telemetryPath(options.homeDir ?? homedir());
|
|
44
|
-
if (envDisablesTelemetry(env)
|
|
45
|
-
|
|
46
|
-
return {
|
|
47
|
-
installId: existing?.installId,
|
|
48
|
-
enabled: false,
|
|
49
|
-
createdFile: false,
|
|
50
|
-
noticeShown: false,
|
|
51
|
-
path,
|
|
52
|
-
};
|
|
44
|
+
if (envDisablesTelemetry(env)) {
|
|
45
|
+
return { enabled: false, createdFile: false, noticeShown: false, path };
|
|
53
46
|
}
|
|
47
|
+
// Honor an already-consented identity regardless of the current surface.
|
|
48
|
+
// Telemetry enablement follows the persisted decision and opt-out env vars,
|
|
49
|
+
// not whether this invocation happens to own a TTY — MCP servers always run
|
|
50
|
+
// headless (stdio stubs stdout; the HTTP server runs detached).
|
|
54
51
|
const existing = await readTelemetryFile(path);
|
|
55
52
|
if (existing) {
|
|
56
53
|
return {
|
|
@@ -61,6 +58,12 @@ export async function loadTelemetryIdentity(options) {
|
|
|
61
58
|
path,
|
|
62
59
|
};
|
|
63
60
|
}
|
|
61
|
+
// No identity yet → mint one regardless of surface. Telemetry is opt-out, so
|
|
62
|
+
// a fresh install is counted even when its first run is headless (an
|
|
63
|
+
// IDE-launched `ktx mcp stdio`, a scripted invocation); otherwise those
|
|
64
|
+
// installs would be permanently invisible. Opt-out env vars are honored
|
|
65
|
+
// above. The one-time notice is written to stderr — safe even under MCP
|
|
66
|
+
// stdio, which reserves stdout for its JSON-RPC protocol.
|
|
64
67
|
const timestamp = (options.now ?? (() => new Date()))().toISOString();
|
|
65
68
|
const next = {
|
|
66
69
|
installId: randomUUID(),
|
|
@@ -7,7 +7,7 @@ export type { CommandOutcome, CompletedCommandSpan };
|
|
|
7
7
|
export declare function showTelemetryNoticeIfNeeded(io: KtxCliIo, packageInfo: KtxCliPackageInfo): Promise<void>;
|
|
8
8
|
type TelemetryEventFields<Name extends TelemetryEventName> = Omit<TelemetryEventProperties<Name>, keyof TelemetryCommonEnvelope>;
|
|
9
9
|
export declare function shouldEmitMcpTelemetry(): boolean;
|
|
10
|
-
export declare function mcpTelemetrySampleRate():
|
|
10
|
+
export declare function mcpTelemetrySampleRate(): 1;
|
|
11
11
|
export declare function emitTelemetryEvent<Name extends TelemetryEventName>(input: {
|
|
12
12
|
name: Name;
|
|
13
13
|
fields: TelemetryEventFields<Name>;
|
|
@@ -25,3 +25,15 @@ export declare function emitCompletedCommand(input: {
|
|
|
25
25
|
packageInfo: KtxCliPackageInfo;
|
|
26
26
|
io: KtxCliIo;
|
|
27
27
|
}): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Flush telemetry when the process is interrupted (Ctrl-C / kill). The normal
|
|
30
|
+
* `command` emit + flush lives in a `finally` that a signal skips, so without
|
|
31
|
+
* this an interrupted long-running command (ingest, `mcp stdio`) loses its
|
|
32
|
+
* `command` event and any queued events. Marks the active command span as
|
|
33
|
+
* `aborted`, emits it, and drains the emitter. Best-effort and idempotent: if
|
|
34
|
+
* the span was already completed (normal exit racing a signal) the emit no-ops.
|
|
35
|
+
*/
|
|
36
|
+
export declare function emitAbortedCommandAndShutdown(input: {
|
|
37
|
+
packageInfo: KtxCliPackageInfo;
|
|
38
|
+
io: KtxCliIo;
|
|
39
|
+
}): Promise<void>;
|
package/dist/telemetry/index.js
CHANGED
|
@@ -8,7 +8,6 @@ import { buildProjectStackSnapshotFields } from './project-snapshot.js';
|
|
|
8
8
|
export { beginCommandSpan, completeCommandSpan, shutdownTelemetryEmitter };
|
|
9
9
|
export async function showTelemetryNoticeIfNeeded(io, packageInfo) {
|
|
10
10
|
const identity = await loadTelemetryIdentity({
|
|
11
|
-
stdoutIsTTY: io.stdout.isTTY === true,
|
|
12
11
|
stderr: io.stderr,
|
|
13
12
|
env: process.env,
|
|
14
13
|
});
|
|
@@ -26,7 +25,11 @@ export async function showTelemetryNoticeIfNeeded(io, packageInfo) {
|
|
|
26
25
|
});
|
|
27
26
|
}
|
|
28
27
|
const emittedProjectSnapshots = new Set();
|
|
29
|
-
|
|
28
|
+
// MCP tool calls are captured at full rate while ktx is early-stage: at current
|
|
29
|
+
// install counts any sampling below 1.0 yields too few events to be useful, and
|
|
30
|
+
// the recorded sampleRate lets us dial this down (and reweight history) once
|
|
31
|
+
// per-session call volume justifies it.
|
|
32
|
+
const MCP_SAMPLE_RATE = 1;
|
|
30
33
|
let mcpSampled;
|
|
31
34
|
function telemetryDebugEnabled() {
|
|
32
35
|
return process.env.KTX_TELEMETRY_DEBUG === '1';
|
|
@@ -41,7 +44,6 @@ export function mcpTelemetrySampleRate() {
|
|
|
41
44
|
export async function emitTelemetryEvent(input) {
|
|
42
45
|
const debug = telemetryDebugEnabled();
|
|
43
46
|
const identity = await loadTelemetryIdentity({
|
|
44
|
-
stdoutIsTTY: input.io.stdout.isTTY === true,
|
|
45
47
|
stderr: input.io.stderr,
|
|
46
48
|
env: process.env,
|
|
47
49
|
});
|
|
@@ -96,3 +98,16 @@ export async function emitCompletedCommand(input) {
|
|
|
96
98
|
packageInfo: input.packageInfo,
|
|
97
99
|
});
|
|
98
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Flush telemetry when the process is interrupted (Ctrl-C / kill). The normal
|
|
103
|
+
* `command` emit + flush lives in a `finally` that a signal skips, so without
|
|
104
|
+
* this an interrupted long-running command (ingest, `mcp stdio`) loses its
|
|
105
|
+
* `command` event and any queued events. Marks the active command span as
|
|
106
|
+
* `aborted`, emits it, and drains the emitter. Best-effort and idempotent: if
|
|
107
|
+
* the span was already completed (normal exit racing a signal) the emit no-ops.
|
|
108
|
+
*/
|
|
109
|
+
export async function emitAbortedCommandAndShutdown(input) {
|
|
110
|
+
const completed = completeCommandSpan({ completedAt: performance.now(), outcome: 'aborted' });
|
|
111
|
+
await emitCompletedCommand({ completed, packageInfo: input.packageInfo, io: input.io });
|
|
112
|
+
await shutdownTelemetryEmitter();
|
|
113
|
+
}
|
|
@@ -1 +1,11 @@
|
|
|
1
1
|
export declare function scrubErrorClass(error: unknown): string | undefined;
|
|
2
|
+
/**
|
|
3
|
+
* Human-readable failure detail for telemetry: the error's `.code` (when
|
|
4
|
+
* present) prefixed onto its `message`, collapsed to a single line and
|
|
5
|
+
* length-capped. Captures the message only — never the stack.
|
|
6
|
+
*
|
|
7
|
+
* This intentionally forwards raw error text, which can include identifiers from
|
|
8
|
+
* the user's environment (table/column names, hostnames, usernames), so that
|
|
9
|
+
* funnel failures are diagnosable. Callers must gate it to the failure path.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatErrorDetail(error: unknown): string | undefined;
|
|
@@ -20,3 +20,23 @@ export function scrubErrorClass(error) {
|
|
|
20
20
|
}
|
|
21
21
|
return constructorName;
|
|
22
22
|
}
|
|
23
|
+
const MAX_ERROR_DETAIL_LENGTH = 1000;
|
|
24
|
+
/**
|
|
25
|
+
* Human-readable failure detail for telemetry: the error's `.code` (when
|
|
26
|
+
* present) prefixed onto its `message`, collapsed to a single line and
|
|
27
|
+
* length-capped. Captures the message only — never the stack.
|
|
28
|
+
*
|
|
29
|
+
* This intentionally forwards raw error text, which can include identifiers from
|
|
30
|
+
* the user's environment (table/column names, hostnames, usernames), so that
|
|
31
|
+
* funnel failures are diagnosable. Callers must gate it to the failure path.
|
|
32
|
+
*/
|
|
33
|
+
export function formatErrorDetail(error) {
|
|
34
|
+
if (error === undefined || error === null) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const code = error.code;
|
|
38
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
39
|
+
const prefix = typeof code === 'string' || typeof code === 'number' ? `${code}: ` : '';
|
|
40
|
+
const detail = `${prefix}${message}`.replace(/\s+/g, ' ').trim();
|
|
41
|
+
return detail.length > 0 ? detail.slice(0, MAX_ERROR_DETAIL_LENGTH) : undefined;
|
|
42
|
+
}
|