@kaelio/ktx 0.8.0 → 0.10.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.8.0-py3-none-any.whl → kaelio_ktx-0.10.0-py3-none-any.whl} +0 -0
- package/assets/python/manifest.json +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/clack.d.ts +6 -0
- package/dist/clack.js +17 -2
- package/dist/cli-program.d.ts +3 -0
- package/dist/cli-program.js +42 -2
- package/dist/cli-runtime.d.ts +3 -0
- package/dist/cli-runtime.js +94 -3
- package/dist/commands/setup-commands.js +3 -4
- package/dist/connection-recovery.d.ts +34 -0
- package/dist/connection-recovery.js +82 -0
- package/dist/connection.js +26 -2
- package/dist/connectors/bigquery/connector.d.ts +2 -5
- package/dist/connectors/bigquery/connector.js +2 -2
- package/dist/connectors/clickhouse/connector.d.ts +2 -5
- package/dist/connectors/clickhouse/connector.js +2 -2
- package/dist/connectors/mysql/connector.d.ts +7 -6
- package/dist/connectors/mysql/connector.js +25 -5
- package/dist/connectors/mysql/dialect.d.ts +1 -1
- package/dist/connectors/mysql/dialect.js +12 -2
- package/dist/connectors/postgres/connector.d.ts +2 -5
- package/dist/connectors/postgres/connector.js +2 -2
- package/dist/connectors/snowflake/connector.d.ts +2 -5
- package/dist/connectors/snowflake/connector.js +2 -2
- package/dist/connectors/sqlite/connector.d.ts +2 -5
- package/dist/connectors/sqlite/connector.js +2 -2
- package/dist/connectors/sqlserver/connector.d.ts +2 -5
- package/dist/connectors/sqlserver/connector.js +2 -2
- package/dist/context/connections/drivers.d.ts +0 -1
- package/dist/context/connections/drivers.js +0 -7
- package/dist/context/connections/query-executor.d.ts +2 -1
- package/dist/context/core/abort.d.ts +9 -0
- package/dist/context/core/abort.js +36 -0
- 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 +30 -0
- package/dist/context/ingest/adapters/historic-sql/query-history-filter-picker.js +194 -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-candidates/curator-pagination.service.d.ts +1 -5
- package/dist/context/ingest/context-candidates/curator-pagination.service.js +1 -3
- package/dist/context/ingest/context-evidence/sqlite-context-evidence-store.d.ts +1 -1
- package/dist/context/ingest/final-gate-repair.d.ts +1 -0
- package/dist/context/ingest/final-gate-repair.js +1 -0
- package/dist/context/ingest/ingest-bundle.runner.d.ts +3 -0
- package/dist/context/ingest/ingest-bundle.runner.js +127 -53
- package/dist/context/ingest/isolated-diff/patch-integrator.js +75 -5
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.d.ts +1 -0
- package/dist/context/ingest/isolated-diff/textual-conflict-resolver.js +1 -0
- package/dist/context/ingest/isolated-diff/work-unit-executor.d.ts +1 -0
- package/dist/context/ingest/local-adapters.js +21 -4
- package/dist/context/ingest/local-bundle-runtime.js +13 -5
- package/dist/context/ingest/local-ingest.d.ts +1 -0
- package/dist/context/ingest/local-ingest.js +13 -3
- package/dist/context/ingest/memory-flow/events.js +1 -1
- package/dist/context/ingest/memory-flow/schema.js +8 -3
- package/dist/context/ingest/memory-flow/types.d.ts +7 -3
- package/dist/context/ingest/ports.d.ts +3 -5
- package/dist/context/ingest/stages/stage-3-work-units.d.ts +1 -4
- package/dist/context/ingest/stages/stage-3-work-units.js +5 -1
- package/dist/context/ingest/stages/stage-4-reconciliation.d.ts +1 -4
- package/dist/context/ingest/stages/stage-4-reconciliation.js +1 -1
- package/dist/context/ingest/types.d.ts +1 -0
- package/dist/context/llm/ai-sdk-runtime.d.ts +3 -0
- package/dist/context/llm/ai-sdk-runtime.js +152 -16
- package/dist/context/llm/claude-code-runtime.d.ts +6 -4
- package/dist/context/llm/claude-code-runtime.js +127 -48
- 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 +347 -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 +16 -4
- package/dist/context/llm/local-config.js +18 -2
- package/dist/context/llm/rate-limit-governor.d.ts +103 -0
- package/dist/context/llm/rate-limit-governor.js +285 -0
- package/dist/context/llm/runtime-port.d.ts +3 -6
- package/dist/context/mcp/context-tools.js +43 -13
- package/dist/context/project/config.d.ts +14 -0
- package/dist/context/project/config.js +37 -2
- package/dist/context/scan/types.d.ts +15 -2
- package/dist/context/scan/types.js +12 -0
- package/dist/context/sl/description-normalization.js +4 -14
- 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-build-view.d.ts +13 -0
- package/dist/context-build-view.js +63 -32
- package/dist/demo-metrics.d.ts +0 -2
- package/dist/demo-metrics.js +1 -11
- package/dist/ingest.d.ts +1 -0
- package/dist/ingest.js +32 -3
- package/dist/io/buffered-command-io.d.ts +11 -0
- package/dist/io/buffered-command-io.js +28 -0
- package/dist/io/symbols.d.ts +2 -0
- package/dist/io/symbols.js +2 -0
- 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/memory-flow-hud.js +8 -16
- 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.d.ts +20 -1
- package/dist/public-ingest.js +228 -42
- package/dist/reveal-password-prompt.d.ts +24 -0
- package/dist/reveal-password-prompt.js +78 -0
- package/dist/scan.js +21 -3
- package/dist/setup-context.d.ts +2 -0
- package/dist/setup-context.js +133 -27
- package/dist/setup-databases.d.ts +18 -1
- package/dist/setup-databases.js +378 -249
- package/dist/setup-demo-tour.js +1 -0
- package/dist/setup-embeddings.js +1 -1
- package/dist/setup-models.d.ts +11 -15
- package/dist/setup-models.js +140 -276
- package/dist/setup-prompts.js +3 -2
- package/dist/setup-ready-menu.d.ts +16 -2
- package/dist/setup-ready-menu.js +37 -5
- package/dist/setup-sources.js +115 -35
- package/dist/setup.d.ts +1 -1
- package/dist/setup.js +23 -11
- package/dist/sl.d.ts +2 -2
- package/dist/sl.js +20 -4
- package/dist/sql.js +18 -2
- package/dist/star-prompt/cache.d.ts +16 -0
- package/dist/star-prompt/cache.js +45 -0
- package/dist/star-prompt/star-count.d.ts +7 -0
- package/dist/star-prompt/star-count.js +66 -0
- package/dist/star-prompt/star-line.d.ts +12 -0
- package/dist/star-prompt/star-line.js +26 -0
- 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.d.ts +10 -0
- package/dist/telemetry/emitter.js +31 -0
- package/dist/telemetry/events.d.ts +35 -6
- package/dist/telemetry/events.js +25 -2
- package/dist/telemetry/exception.d.ts +18 -0
- package/dist/telemetry/exception.js +162 -0
- package/dist/telemetry/identity.d.ts +0 -1
- package/dist/telemetry/identity.js +6 -6
- package/dist/telemetry/index.d.ts +15 -2
- package/dist/telemetry/index.js +15 -3
- package/dist/telemetry/redaction-secrets.d.ts +11 -0
- package/dist/telemetry/redaction-secrets.js +92 -0
- package/dist/telemetry/scrubber.d.ts +10 -0
- package/dist/telemetry/scrubber.js +20 -0
- package/dist/update-check/cache.d.ts +21 -0
- package/dist/update-check/cache.js +38 -0
- package/dist/update-check/channel.d.ts +15 -0
- package/dist/update-check/channel.js +30 -0
- package/dist/update-check/registry.d.ts +1 -0
- package/dist/update-check/registry.js +45 -0
- package/dist/update-check/update-check.d.ts +43 -0
- package/dist/update-check/update-check.js +116 -0
- package/package.json +12 -4
- package/dist/context/connections/local-query-executor.d.ts +0 -6
- package/dist/context/connections/local-query-executor.js +0 -39
- package/dist/context/connections/postgres-query-executor.d.ts +0 -25
- package/dist/context/connections/postgres-query-executor.js +0 -53
- package/dist/context/connections/sqlite-query-executor.d.ts +0 -4
- package/dist/context/connections/sqlite-query-executor.js +0 -74
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { inspect } from 'node:util';
|
|
2
|
+
import { getKtxCliPackageInfo } from '../cli-runtime.js';
|
|
3
|
+
import { buildCommonEnvelope } from './events.js';
|
|
4
|
+
import { trackTelemetryException } from './emitter.js';
|
|
5
|
+
import { computeTelemetryProjectId, loadTelemetryIdentity } from './identity.js';
|
|
6
|
+
const reportedObjects = new WeakSet();
|
|
7
|
+
const recentHandledPrimitives = [];
|
|
8
|
+
const RECENT_PRIMITIVE_LIMIT = 128;
|
|
9
|
+
function primitiveKey(value) {
|
|
10
|
+
return `${typeof value}:${String(value)}`;
|
|
11
|
+
}
|
|
12
|
+
function rememberHandledPrimitive(value) {
|
|
13
|
+
recentHandledPrimitives.push(primitiveKey(value));
|
|
14
|
+
if (recentHandledPrimitives.length > RECENT_PRIMITIVE_LIMIT) {
|
|
15
|
+
recentHandledPrimitives.splice(0, recentHandledPrimitives.length - RECENT_PRIMITIVE_LIMIT);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function consumeHandledPrimitive(value) {
|
|
19
|
+
const key = primitiveKey(value);
|
|
20
|
+
const index = recentHandledPrimitives.indexOf(key);
|
|
21
|
+
if (index < 0) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
recentHandledPrimitives.splice(index, 1);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function shouldSkipAsAlreadyReported(error, handled) {
|
|
28
|
+
if ((typeof error === 'object' || typeof error === 'function') && error !== null) {
|
|
29
|
+
if (reportedObjects.has(error)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
reportedObjects.add(error);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (handled) {
|
|
36
|
+
rememberHandledPrimitive(error);
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
return consumeHandledPrimitive(error);
|
|
40
|
+
}
|
|
41
|
+
function escapeRegExp(value) {
|
|
42
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
43
|
+
}
|
|
44
|
+
function redactStaticPatterns(value) {
|
|
45
|
+
return value
|
|
46
|
+
.replace(/([a-z][a-z0-9+.-]*:\/\/[^:\s/@]+:)([^@\s/]+)(@)/gi, '$1[redacted]$3')
|
|
47
|
+
.replace(/\b(password|pwd)=([^;&\s]+)/gi, '$1=[redacted]')
|
|
48
|
+
.replace(/\bAuthorization\s*:\s*[^\r\n,;]+/gi, 'Authorization: [redacted]')
|
|
49
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [redacted]')
|
|
50
|
+
.replace(/\b(api[_-]?key)\s*[:=]\s*([^\s,;]+)/gi, '$1=[redacted]')
|
|
51
|
+
.replace(/\b(KTX_[A-Z0-9_]*|[A-Z0-9_]*(?:TOKEN|SECRET))\s*[:=]\s*([^\s,;]+)/g, '$1=[redacted]')
|
|
52
|
+
.replace(/([?&](?:X-Amz-Signature|X-Goog-Signature|sig)=)[^&\s]+/gi, '$1[redacted]');
|
|
53
|
+
}
|
|
54
|
+
function redactText(value, secrets) {
|
|
55
|
+
let redacted = value;
|
|
56
|
+
for (const secret of secrets) {
|
|
57
|
+
if (secret) {
|
|
58
|
+
redacted = redacted.replace(new RegExp(escapeRegExp(secret), 'g'), '[redacted]');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return redactStaticPatterns(redacted);
|
|
62
|
+
}
|
|
63
|
+
const FORBIDDEN_EXTRA_PROPERTY_KEYS = new Set([
|
|
64
|
+
'argv',
|
|
65
|
+
'args',
|
|
66
|
+
'env',
|
|
67
|
+
'environment',
|
|
68
|
+
'sql',
|
|
69
|
+
'query',
|
|
70
|
+
'prompt',
|
|
71
|
+
'mcparguments',
|
|
72
|
+
'mcpargs',
|
|
73
|
+
'tablename',
|
|
74
|
+
'schemaname',
|
|
75
|
+
'columnname',
|
|
76
|
+
'databaseurl',
|
|
77
|
+
'connectionstring',
|
|
78
|
+
'url',
|
|
79
|
+
'password',
|
|
80
|
+
'token',
|
|
81
|
+
'apikey',
|
|
82
|
+
'api_key',
|
|
83
|
+
'authorization',
|
|
84
|
+
]);
|
|
85
|
+
function safeExtraProperties(extra) {
|
|
86
|
+
const safe = {};
|
|
87
|
+
for (const [key, value] of Object.entries(extra ?? {})) {
|
|
88
|
+
if (!FORBIDDEN_EXTRA_PROPERTY_KEYS.has(key.replace(/[^a-z0-9_]/gi, '').toLowerCase())) {
|
|
89
|
+
safe[key] = value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return safe;
|
|
93
|
+
}
|
|
94
|
+
function toMessage(error) {
|
|
95
|
+
if (error instanceof Error) {
|
|
96
|
+
return error.message;
|
|
97
|
+
}
|
|
98
|
+
if (typeof error === 'string') {
|
|
99
|
+
return error;
|
|
100
|
+
}
|
|
101
|
+
return inspect(error, { depth: 4, breakLength: 120 });
|
|
102
|
+
}
|
|
103
|
+
function sanitizedError(error, secrets) {
|
|
104
|
+
if (error instanceof Error) {
|
|
105
|
+
const cause = 'cause' in error ? error.cause : undefined;
|
|
106
|
+
const clone = new Error(redactText(error.message, secrets), {
|
|
107
|
+
...(cause !== undefined ? { cause: sanitizedError(cause, secrets) } : {}),
|
|
108
|
+
});
|
|
109
|
+
clone.name = error.name;
|
|
110
|
+
if (error.stack) {
|
|
111
|
+
clone.stack = redactText(error.stack, secrets);
|
|
112
|
+
}
|
|
113
|
+
return clone;
|
|
114
|
+
}
|
|
115
|
+
return new Error(redactText(toMessage(error), secrets));
|
|
116
|
+
}
|
|
117
|
+
export async function reportException(input) {
|
|
118
|
+
try {
|
|
119
|
+
if (shouldSkipAsAlreadyReported(input.error, input.context.handled)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const debug = process.env.KTX_TELEMETRY_DEBUG === '1';
|
|
123
|
+
const identity = await loadTelemetryIdentity({
|
|
124
|
+
stderr: input.io.stderr,
|
|
125
|
+
env: process.env,
|
|
126
|
+
});
|
|
127
|
+
if ((!identity.enabled || !identity.installId) && !debug) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const packageInfo = input.packageInfo ?? getKtxCliPackageInfo();
|
|
131
|
+
const installId = identity.installId ?? 'debug';
|
|
132
|
+
const projectId = input.projectDir ? computeTelemetryProjectId(installId, input.projectDir) : undefined;
|
|
133
|
+
const safeError = sanitizedError(input.error, input.redactionSecrets ?? []);
|
|
134
|
+
const properties = {
|
|
135
|
+
...buildCommonEnvelope({
|
|
136
|
+
cliVersion: packageInfo.version,
|
|
137
|
+
isCi: Boolean(process.env.CI),
|
|
138
|
+
}),
|
|
139
|
+
source: input.context.source,
|
|
140
|
+
handled: input.context.handled,
|
|
141
|
+
fatal: input.context.fatal,
|
|
142
|
+
...(projectId ? { projectId } : {}),
|
|
143
|
+
...safeExtraProperties(input.context.extra),
|
|
144
|
+
};
|
|
145
|
+
delete properties.$groups;
|
|
146
|
+
await trackTelemetryException({
|
|
147
|
+
error: safeError,
|
|
148
|
+
distinctId: installId,
|
|
149
|
+
properties,
|
|
150
|
+
env: process.env,
|
|
151
|
+
stderr: input.io.stderr,
|
|
152
|
+
immediate: input.immediate,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** @internal */
|
|
160
|
+
export function __resetTelemetryExceptionStateForTests() {
|
|
161
|
+
recentHandledPrimitives.length = 0;
|
|
162
|
+
}
|
|
@@ -58,12 +58,12 @@ export async function loadTelemetryIdentity(options) {
|
|
|
58
58
|
path,
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
-
// No identity yet
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
67
67
|
const timestamp = (options.now ?? (() => new Date()))().toISOString();
|
|
68
68
|
const next = {
|
|
69
69
|
installId: randomUUID(),
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type KtxCliIo, type KtxCliPackageInfo } from '../cli-runtime.js';
|
|
2
2
|
import { beginCommandSpan, completeCommandSpan, type CommandOutcome, type CompletedCommandSpan } from './command-hook.js';
|
|
3
3
|
import { shutdownTelemetryEmitter } from './emitter.js';
|
|
4
|
+
import { reportException, type ExceptionContext } from './exception.js';
|
|
4
5
|
import { type TelemetryCommonEnvelope, type TelemetryEventName, type TelemetryEventProperties } from './events.js';
|
|
5
|
-
export { beginCommandSpan, completeCommandSpan, shutdownTelemetryEmitter };
|
|
6
|
-
export type { CommandOutcome, CompletedCommandSpan };
|
|
6
|
+
export { beginCommandSpan, completeCommandSpan, reportException, shutdownTelemetryEmitter };
|
|
7
|
+
export type { CommandOutcome, CompletedCommandSpan, ExceptionContext };
|
|
7
8
|
export declare function showTelemetryNoticeIfNeeded(io: KtxCliIo, packageInfo: KtxCliPackageInfo): Promise<void>;
|
|
8
9
|
type TelemetryEventFields<Name extends TelemetryEventName> = Omit<TelemetryEventProperties<Name>, keyof TelemetryCommonEnvelope>;
|
|
9
10
|
export declare function shouldEmitMcpTelemetry(): boolean;
|
|
@@ -25,3 +26,15 @@ export declare function emitCompletedCommand(input: {
|
|
|
25
26
|
packageInfo: KtxCliPackageInfo;
|
|
26
27
|
io: KtxCliIo;
|
|
27
28
|
}): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Flush telemetry when the process is interrupted (Ctrl-C / kill). The normal
|
|
31
|
+
* `command` emit + flush lives in a `finally` that a signal skips, so without
|
|
32
|
+
* this an interrupted long-running command (ingest, `mcp stdio`) loses its
|
|
33
|
+
* `command` event and any queued events. Marks the active command span as
|
|
34
|
+
* `aborted`, emits it, and drains the emitter. Best-effort and idempotent: if
|
|
35
|
+
* the span was already completed (normal exit racing a signal) the emit no-ops.
|
|
36
|
+
*/
|
|
37
|
+
export declare function emitAbortedCommandAndShutdown(input: {
|
|
38
|
+
packageInfo: KtxCliPackageInfo;
|
|
39
|
+
io: KtxCliIo;
|
|
40
|
+
}): Promise<void>;
|
package/dist/telemetry/index.js
CHANGED
|
@@ -2,13 +2,13 @@ import { getKtxCliPackageInfo } from '../cli-runtime.js';
|
|
|
2
2
|
import { loadKtxProject } from '../context/project/project.js';
|
|
3
3
|
import { beginCommandSpan, completeCommandSpan, } from './command-hook.js';
|
|
4
4
|
import { shutdownTelemetryEmitter, trackTelemetryEvent } from './emitter.js';
|
|
5
|
+
import { reportException } from './exception.js';
|
|
5
6
|
import { buildCommonEnvelope, buildTelemetryEvent, } from './events.js';
|
|
6
7
|
import { computeTelemetryProjectId, loadTelemetryIdentity } from './identity.js';
|
|
7
8
|
import { buildProjectStackSnapshotFields } from './project-snapshot.js';
|
|
8
|
-
export { beginCommandSpan, completeCommandSpan, shutdownTelemetryEmitter };
|
|
9
|
+
export { beginCommandSpan, completeCommandSpan, reportException, shutdownTelemetryEmitter };
|
|
9
10
|
export async function showTelemetryNoticeIfNeeded(io, packageInfo) {
|
|
10
11
|
const identity = await loadTelemetryIdentity({
|
|
11
|
-
stdoutIsTTY: io.stdout.isTTY === true,
|
|
12
12
|
stderr: io.stderr,
|
|
13
13
|
env: process.env,
|
|
14
14
|
});
|
|
@@ -45,7 +45,6 @@ export function mcpTelemetrySampleRate() {
|
|
|
45
45
|
export async function emitTelemetryEvent(input) {
|
|
46
46
|
const debug = telemetryDebugEnabled();
|
|
47
47
|
const identity = await loadTelemetryIdentity({
|
|
48
|
-
stdoutIsTTY: input.io.stdout.isTTY === true,
|
|
49
48
|
stderr: input.io.stderr,
|
|
50
49
|
env: process.env,
|
|
51
50
|
});
|
|
@@ -100,3 +99,16 @@ export async function emitCompletedCommand(input) {
|
|
|
100
99
|
packageInfo: input.packageInfo,
|
|
101
100
|
});
|
|
102
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Flush telemetry when the process is interrupted (Ctrl-C / kill). The normal
|
|
104
|
+
* `command` emit + flush lives in a `finally` that a signal skips, so without
|
|
105
|
+
* this an interrupted long-running command (ingest, `mcp stdio`) loses its
|
|
106
|
+
* `command` event and any queued events. Marks the active command span as
|
|
107
|
+
* `aborted`, emits it, and drains the emitter. Best-effort and idempotent: if
|
|
108
|
+
* the span was already completed (normal exit racing a signal) the emit no-ops.
|
|
109
|
+
*/
|
|
110
|
+
export async function emitAbortedCommandAndShutdown(input) {
|
|
111
|
+
const completed = completeCommandSpan({ completedAt: performance.now(), outcome: 'aborted' });
|
|
112
|
+
await emitCompletedCommand({ completed, packageInfo: input.packageInfo, io: input.io });
|
|
113
|
+
await shutdownTelemetryEmitter();
|
|
114
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type KtxLocalProject } from '../context/project/project.js';
|
|
2
|
+
type TelemetryRedactionProject = Pick<KtxLocalProject, 'config' | 'projectDir'>;
|
|
3
|
+
export declare function collectTelemetryRedactionSecrets(input: {
|
|
4
|
+
project?: TelemetryRedactionProject;
|
|
5
|
+
projectDir?: string;
|
|
6
|
+
connectionId?: string;
|
|
7
|
+
includeLlm?: boolean;
|
|
8
|
+
includeEmbeddings?: boolean;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
}): Promise<string[]>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { resolveKtxConfigReference } from '../context/core/config-reference.js';
|
|
2
|
+
import { loadKtxProject } from '../context/project/project.js';
|
|
3
|
+
const SENSITIVE_KEY = /(password|secret|token|api[_-]?key|auth[_-]?token|auth_token_ref|private[_-]?key|passphrase|credential|authorization|url)$/i;
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function addSecret(values, value) {
|
|
8
|
+
const trimmed = value?.trim();
|
|
9
|
+
if (trimmed && !values.includes(trimmed)) {
|
|
10
|
+
values.push(trimmed);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function tryResolve(value, env) {
|
|
14
|
+
try {
|
|
15
|
+
return resolveKtxConfigReference(value, env);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function addUrlCredentials(values, value) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = new URL(value);
|
|
24
|
+
addSecret(values, parsed.password ? decodeURIComponent(parsed.password) : undefined);
|
|
25
|
+
addSecret(values, parsed.username ? decodeURIComponent(parsed.username) : undefined);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function collectFromRecord(input, env, values) {
|
|
32
|
+
if (Array.isArray(input)) {
|
|
33
|
+
for (const item of input) {
|
|
34
|
+
collectFromRecord(item, env, values);
|
|
35
|
+
}
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!isRecord(input)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
for (const [key, raw] of Object.entries(input)) {
|
|
42
|
+
if (isRecord(raw) || Array.isArray(raw)) {
|
|
43
|
+
collectFromRecord(raw, env, values);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (typeof raw !== 'string' || !SENSITIVE_KEY.test(key)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const resolved = tryResolve(raw, env);
|
|
50
|
+
addSecret(values, resolved);
|
|
51
|
+
if (resolved) {
|
|
52
|
+
addUrlCredentials(values, resolved);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function collectLlmSecrets(project, env, values) {
|
|
57
|
+
collectFromRecord(project.config.llm.provider, env, values);
|
|
58
|
+
}
|
|
59
|
+
function collectEmbeddingSecrets(project, env, values) {
|
|
60
|
+
collectFromRecord(project.config.ingest.embeddings, env, values);
|
|
61
|
+
collectFromRecord(project.config.scan.enrichment.embeddings, env, values);
|
|
62
|
+
}
|
|
63
|
+
function collectConnectionSecrets(project, connectionId, env, values) {
|
|
64
|
+
if (!connectionId) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
collectFromRecord(project.config.connections[connectionId], env, values);
|
|
68
|
+
}
|
|
69
|
+
export async function collectTelemetryRedactionSecrets(input) {
|
|
70
|
+
const env = input.env ?? process.env;
|
|
71
|
+
let project = input.project;
|
|
72
|
+
if (!project && input.projectDir) {
|
|
73
|
+
try {
|
|
74
|
+
project = await loadKtxProject({ projectDir: input.projectDir });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
project = undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!project) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
const values = [];
|
|
84
|
+
if (input.includeLlm) {
|
|
85
|
+
collectLlmSecrets(project, env, values);
|
|
86
|
+
}
|
|
87
|
+
if (input.includeEmbeddings) {
|
|
88
|
+
collectEmbeddingSecrets(project, env, values);
|
|
89
|
+
}
|
|
90
|
+
collectConnectionSecrets(project, input.connectionId, env, values);
|
|
91
|
+
return values;
|
|
92
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const updateCheckCacheSchema: z.ZodObject<{
|
|
3
|
+
checkedAt: z.ZodString;
|
|
4
|
+
channel: z.ZodEnum<{
|
|
5
|
+
next: "next";
|
|
6
|
+
latest: "latest";
|
|
7
|
+
}>;
|
|
8
|
+
installedVersion: z.ZodString;
|
|
9
|
+
latestForChannel: z.ZodString;
|
|
10
|
+
lastNoticeAt: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$strict>;
|
|
12
|
+
export type UpdateCheckCache = z.infer<typeof updateCheckCacheSchema>;
|
|
13
|
+
/** @internal */
|
|
14
|
+
export declare function updateCheckCachePath(homeDir?: string): string;
|
|
15
|
+
export declare function readUpdateCheckCache(options?: {
|
|
16
|
+
homeDir?: string;
|
|
17
|
+
}): Promise<UpdateCheckCache | null>;
|
|
18
|
+
export declare function writeUpdateCheckCache(value: UpdateCheckCache, options?: {
|
|
19
|
+
homeDir?: string;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
const updateCheckCacheSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
checkedAt: z.string(),
|
|
9
|
+
channel: z.enum(['latest', 'next']),
|
|
10
|
+
installedVersion: z.string(),
|
|
11
|
+
latestForChannel: z.string(),
|
|
12
|
+
lastNoticeAt: z.string().optional(),
|
|
13
|
+
})
|
|
14
|
+
.strict();
|
|
15
|
+
/** @internal */
|
|
16
|
+
export function updateCheckCachePath(homeDir = homedir()) {
|
|
17
|
+
return join(homeDir, '.ktx', 'update-check.json');
|
|
18
|
+
}
|
|
19
|
+
export async function readUpdateCheckCache(options = {}) {
|
|
20
|
+
try {
|
|
21
|
+
return updateCheckCacheSchema.parse(JSON.parse(await readFile(updateCheckCachePath(options.homeDir), 'utf-8')));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function writeUpdateCheckCache(value, options = {}) {
|
|
28
|
+
try {
|
|
29
|
+
const path = updateCheckCachePath(options.homeDir);
|
|
30
|
+
await mkdir(dirname(path), { recursive: true });
|
|
31
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
32
|
+
writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
33
|
+
renameSync(tempPath, path);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type UpdateChannel = 'latest' | 'next';
|
|
2
|
+
export type UpdateDecision = {
|
|
3
|
+
status: 'skip';
|
|
4
|
+
} | {
|
|
5
|
+
status: 'upToDate';
|
|
6
|
+
channel: UpdateChannel;
|
|
7
|
+
target: string;
|
|
8
|
+
} | {
|
|
9
|
+
status: 'available';
|
|
10
|
+
channel: UpdateChannel;
|
|
11
|
+
target: string;
|
|
12
|
+
};
|
|
13
|
+
/** @internal */
|
|
14
|
+
export declare function inferUpdateChannel(installed: string): UpdateChannel | null;
|
|
15
|
+
export declare function decideUpdate(installed: string, distTags: Record<string, string>): UpdateDecision;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
/** @internal */
|
|
3
|
+
export function inferUpdateChannel(installed) {
|
|
4
|
+
const parsed = semver.parse(installed);
|
|
5
|
+
if (!parsed || installed === '0.0.0') {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
const [prereleaseId] = parsed.prerelease;
|
|
9
|
+
if (prereleaseId === undefined) {
|
|
10
|
+
return 'latest';
|
|
11
|
+
}
|
|
12
|
+
if (prereleaseId === 'rc') {
|
|
13
|
+
return 'next';
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
export function decideUpdate(installed, distTags) {
|
|
18
|
+
const channel = inferUpdateChannel(installed);
|
|
19
|
+
if (!channel || !semver.valid(installed)) {
|
|
20
|
+
return { status: 'skip' };
|
|
21
|
+
}
|
|
22
|
+
const target = distTags[channel];
|
|
23
|
+
if (!target || !semver.valid(target)) {
|
|
24
|
+
return { status: 'skip' };
|
|
25
|
+
}
|
|
26
|
+
if (semver.gt(target, installed)) {
|
|
27
|
+
return { status: 'available', channel, target };
|
|
28
|
+
}
|
|
29
|
+
return { status: 'upToDate', channel, target };
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function fetchDistTags(): Promise<Record<string, string>>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { request as httpsRequest } from 'node:https';
|
|
2
|
+
import { URL } from 'node:url';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
const DIST_TAGS_URL = new URL('https://registry.npmjs.org/-/package/@kaelio/ktx/dist-tags');
|
|
5
|
+
const distTagsSchema = z.record(z.string(), z.string());
|
|
6
|
+
function parseDistTags(raw) {
|
|
7
|
+
return distTagsSchema.parse(JSON.parse(raw));
|
|
8
|
+
}
|
|
9
|
+
export function fetchDistTags() {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const request = httpsRequest(DIST_TAGS_URL, {
|
|
12
|
+
method: 'GET',
|
|
13
|
+
headers: {
|
|
14
|
+
accept: 'application/json',
|
|
15
|
+
},
|
|
16
|
+
}, (response) => {
|
|
17
|
+
const chunks = [];
|
|
18
|
+
response.on('data', (chunk) => {
|
|
19
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
20
|
+
});
|
|
21
|
+
response.on('end', () => {
|
|
22
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
23
|
+
const statusCode = response.statusCode ?? 0;
|
|
24
|
+
if (statusCode < 200 || statusCode >= 300) {
|
|
25
|
+
reject(new Error(`npm dist-tags request failed with ${statusCode}: ${text}`));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
resolve(parseDistTags(text));
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
reject(error);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
request.on('socket', (socket) => {
|
|
37
|
+
socket.unref();
|
|
38
|
+
});
|
|
39
|
+
request.on('error', reject);
|
|
40
|
+
request.setTimeout(5000, () => {
|
|
41
|
+
request.destroy(new Error('npm dist-tags request timed out'));
|
|
42
|
+
});
|
|
43
|
+
request.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { KtxCliIo } from '../cli-runtime.js';
|
|
2
|
+
import { type CliStyleEnv } from '../clack.js';
|
|
3
|
+
import { type UpdateChannel } from './channel.js';
|
|
4
|
+
/** @internal */
|
|
5
|
+
export interface UpdateCheckEnv extends NodeJS.ProcessEnv, CliStyleEnv {
|
|
6
|
+
CI?: string;
|
|
7
|
+
DO_NOT_TRACK?: string;
|
|
8
|
+
KTX_NO_UPDATE_CHECK?: string;
|
|
9
|
+
KTX_OUTPUT?: string;
|
|
10
|
+
NO_UPDATE_NOTIFIER?: string;
|
|
11
|
+
}
|
|
12
|
+
/** @internal */
|
|
13
|
+
export interface UpdateCheckCommandOptions {
|
|
14
|
+
format?: unknown;
|
|
15
|
+
json?: unknown;
|
|
16
|
+
output?: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface PrepareUpdateCheckNoticeOptions {
|
|
19
|
+
commandOptions?: UpdateCheckCommandOptions;
|
|
20
|
+
env?: UpdateCheckEnv;
|
|
21
|
+
fetchDistTags?: () => Promise<Record<string, string>>;
|
|
22
|
+
homeDir?: string;
|
|
23
|
+
installedVersion: string;
|
|
24
|
+
io: KtxCliIo;
|
|
25
|
+
now?: () => Date;
|
|
26
|
+
}
|
|
27
|
+
export interface PreparedUpdateCheckNotice {
|
|
28
|
+
notice: string | null;
|
|
29
|
+
}
|
|
30
|
+
/** @internal */
|
|
31
|
+
export declare function shouldSuppressUpdateCheck(args: {
|
|
32
|
+
commandOptions?: UpdateCheckCommandOptions;
|
|
33
|
+
env?: UpdateCheckEnv;
|
|
34
|
+
io: KtxCliIo;
|
|
35
|
+
}): boolean;
|
|
36
|
+
/** @internal */
|
|
37
|
+
export declare function renderUpdateNotice(args: {
|
|
38
|
+
channel: UpdateChannel;
|
|
39
|
+
env?: CliStyleEnv;
|
|
40
|
+
installedVersion: string;
|
|
41
|
+
targetVersion: string;
|
|
42
|
+
}): string;
|
|
43
|
+
export declare function prepareUpdateCheckNotice(options: PrepareUpdateCheckNoticeOptions): Promise<PreparedUpdateCheckNotice>;
|