knodin 0.8.2 → 0.8.4
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/README.md +36 -13
- package/dist/bin/cli.js +248 -62
- package/dist/src/agent-events.js +128 -0
- package/dist/src/agent-hooks.js +156 -0
- package/dist/src/cli-model.js +16 -1
- package/dist/src/docs-sections.js +1 -0
- package/dist/src/engine/index.js +911 -55
- package/dist/src/init-progress-worker.js +4 -40
- package/dist/src/output-telemetry.js +8 -3
- package/dist/src/progress-worker-runtime.js +46 -0
- package/dist/src/repair-progress-worker.js +3 -40
- package/dist/src/resource-reachability.js +456 -0
- package/dist/src/response-budget.js +69 -65
- package/dist/src/session-telemetry.js +163 -0
- package/dist/src/tools/knodin-tools.js +12 -7
- package/docs/BEHAVIORAL-CONTRACT.md +47 -5
- package/docs/CLI.md +27 -0
- package/docs/COMPARISON.md +10 -0
- package/docs/DEMO.md +49 -0
- package/docs/INSTALLATION.md +11 -0
- package/docs/MCP.md +5 -0
- package/docs/TELEMETRY.md +19 -1
- package/docs/releases/0.8.3.md +47 -0
- package/docs/releases/0.8.4.md +22 -0
- package/package.json +14 -1
- package/roadmap/competitive-roadmap.md +115 -0
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { clearLine as clearTerminalLine, moveCursor as moveTerminalCursor, cursorTo as moveTerminalCursorTo, } from "node:readline";
|
|
3
2
|
import ora from "ora";
|
|
4
|
-
import { formatProgress
|
|
3
|
+
import { formatProgress } from "./init-progress.js";
|
|
4
|
+
import { consumeProgressLines, createProgressStream } from "./progress-worker-runtime.js";
|
|
5
5
|
const terminalEnabled = process.stderr.isTTY === true || process.env.KNODIN_FORCE_PROGRESS_TTY === "1";
|
|
6
6
|
// Ora divides rendered text width by `stream.columns`. Some pseudo-terminals
|
|
7
7
|
// report zero columns, which turns its clear loop into an infinite loop. Keep
|
|
8
8
|
// the real terminal methods while guaranteeing finite viewport dimensions.
|
|
9
|
-
const progressStream =
|
|
10
|
-
isTTY: terminalEnabled,
|
|
11
|
-
columns: normalizeTerminalDimension(process.stderr.columns, 80),
|
|
12
|
-
rows: normalizeTerminalDimension(process.stderr.rows, 24),
|
|
13
|
-
write: process.stderr.write.bind(process.stderr),
|
|
14
|
-
cursorTo: (x, y, callback) => moveTerminalCursorTo(process.stderr, x, y, callback),
|
|
15
|
-
moveCursor: (dx, dy, callback) => moveTerminalCursor(process.stderr, dx, dy, callback),
|
|
16
|
-
clearLine: (direction, callback) => clearTerminalLine(process.stderr, direction, callback),
|
|
17
|
-
once: process.stderr.once.bind(process.stderr),
|
|
18
|
-
removeListener: process.stderr.removeListener.bind(process.stderr),
|
|
19
|
-
};
|
|
9
|
+
const progressStream = createProgressStream(terminalEnabled);
|
|
20
10
|
const spinner = ora({
|
|
21
11
|
text: "[init:starting] Opening local graph database (0s elapsed)",
|
|
22
12
|
stream: progressStream,
|
|
23
13
|
isEnabled: terminalEnabled,
|
|
24
14
|
discardStdin: false,
|
|
25
15
|
});
|
|
26
|
-
let buffer = "";
|
|
27
16
|
let stopped = false;
|
|
28
17
|
let exiting = false;
|
|
29
18
|
let startedAt = 0;
|
|
@@ -75,32 +64,7 @@ function handle(line) {
|
|
|
75
64
|
else
|
|
76
65
|
stop(true);
|
|
77
66
|
}
|
|
78
|
-
|
|
79
|
-
process.stdin.on("data", (chunk) => {
|
|
80
|
-
buffer += chunk;
|
|
81
|
-
for (;;) {
|
|
82
|
-
const newline = buffer.indexOf("\n");
|
|
83
|
-
if (newline < 0)
|
|
84
|
-
break;
|
|
85
|
-
const line = buffer.slice(0, newline);
|
|
86
|
-
buffer = buffer.slice(newline + 1);
|
|
87
|
-
try {
|
|
88
|
-
handle(line);
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// Progress transport is observational; malformed input cannot affect init.
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
process.stdin.on("end", () => {
|
|
96
|
-
if (buffer) {
|
|
97
|
-
try {
|
|
98
|
-
handle(buffer);
|
|
99
|
-
}
|
|
100
|
-
catch { }
|
|
101
|
-
}
|
|
102
|
-
stop(true);
|
|
103
|
-
});
|
|
67
|
+
consumeProgressLines(handle, () => stop(true));
|
|
104
68
|
process.on("SIGTERM", () => {
|
|
105
69
|
stop();
|
|
106
70
|
process.exit(0);
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { encode } from "gpt-tokenizer/encoding/o200k_base";
|
|
5
5
|
import { compareBytes } from "./compare.js";
|
|
6
6
|
import { resolveDbPath } from "./engine/state-paths.js";
|
|
7
|
+
import { summarizeAdoption } from "./session-telemetry.js";
|
|
7
8
|
export const TELEMETRY_TOKENIZER = "gpt-tokenizer@3.4.0:o200k_base";
|
|
8
9
|
export const countOutputTokens = (value) => encode(value).length;
|
|
9
10
|
export const DEFAULT_TELEMETRY_RETENTION_DAYS = 30;
|
|
@@ -182,7 +183,7 @@ function telemetrySummary(records) {
|
|
|
182
183
|
indexingMs: records.reduce((sum, record) => sum + (record.indexingMs ?? 0), 0),
|
|
183
184
|
};
|
|
184
185
|
}
|
|
185
|
-
export function writeTelemetryReport(repoPath, records, outputPath = ".knodin/telemetry-report.html") {
|
|
186
|
+
export function writeTelemetryReport(repoPath, records, outputPath = ".knodin/telemetry-report.html", sessionEvents = []) {
|
|
186
187
|
const { repo, target: output } = privatePath(repoPath, outputPath, "report");
|
|
187
188
|
const groups = new Map();
|
|
188
189
|
for (const record of records) {
|
|
@@ -201,6 +202,7 @@ export function writeTelemetryReport(repoPath, records, outputPath = ".knodin/te
|
|
|
201
202
|
})
|
|
202
203
|
.join("");
|
|
203
204
|
const summary = telemetrySummary(records);
|
|
205
|
+
const adoption = summarizeAdoption(sessionEvents);
|
|
204
206
|
const timeSeries = new Map();
|
|
205
207
|
for (const record of records) {
|
|
206
208
|
const day = typeof record.at === "string" ? record.at.slice(0, 10) : "unknown";
|
|
@@ -221,12 +223,13 @@ export function writeTelemetryReport(repoPath, records, outputPath = ".knodin/te
|
|
|
221
223
|
.sort(([left], [right]) => compareBytes(left, right))
|
|
222
224
|
.map(([id, calls]) => `<tr><td>${escapeHtml(id)}</td><td>${calls}</td></tr>`)
|
|
223
225
|
.join("");
|
|
224
|
-
const html = `<!doctype html><meta charset="utf-8"><meta name="color-scheme" content="light dark"><title>knodin telemetry</title><style>:root{color-scheme:light dark;--line:#d0d7de;--card:#f6f8fa}body{font:14px system-ui;margin:2rem;max-width:76rem}section{background:var(--card);padding:1rem;margin:1rem 0;border-radius:.5rem}table{border-collapse:collapse;width:100%}th,td{padding:.55rem;border-bottom:1px solid var(--line);text-align:left}@media(prefers-color-scheme:dark){:root{--line:#30363d;--card:#161b22}}</style><h1>knodin local telemetry</h1><p>Tokenizer: ${TELEMETRY_TOKENIZER}. Local, opt-in, metadata-only; source, raw output, command text, usernames, and paths are not persisted.</p><section><h2>Summary</h2><p>Total measured tokens avoided: ${summary.measuredTokensAvoided}</p><p>Calls: ${summary.calls}; successes: ${summary.successes}; errors: ${summary.errors}; measured baselines: ${summary.measuredBaselines}</p><p>Latency p50 / p95: ${summary.latencyP50Ms} / ${summary.latencyP95Ms} ms</p><p>Freshness failures: ${summary.freshnessFailures}; compression fidelity samples / p50: ${summary.compressionFidelitySamples} / ${summary.compressionFidelityP50}; indexing time: ${summary.indexingMs} ms; index storage: ${summary.indexStorageBytes} bytes</p></section><section><h2>Operation breakdown</h2><table><thead><tr><th>Operation</th><th>Calls</th><th>Errors</th><th>Measured baselines</th><th>Net tokens saved</th><th>p50 ms</th><th>p95 ms</th></tr></thead><tbody>${rows}</tbody></table></section><section><h2>Time series</h2><table><thead><tr><th>Day</th><th>Calls</th><th>Tokens avoided</th></tr></thead><tbody>${seriesRows}</tbody></table></section><section><h2>Repository breakdown</h2><table><thead><tr><th>Private repository id</th><th>Calls</th></tr></thead><tbody>${repositoryRows}</tbody></table></section><p>Use <code>knodin telemetry export</code> for the machine-readable evidence bundle and <code>knodin telemetry clear</code> to delete persisted records.</p>`;
|
|
226
|
+
const html = `<!doctype html><meta charset="utf-8"><meta name="color-scheme" content="light dark"><title>knodin telemetry</title><style>:root{color-scheme:light dark;--line:#d0d7de;--card:#f6f8fa}body{font:14px system-ui;margin:2rem;max-width:76rem}section{background:var(--card);padding:1rem;margin:1rem 0;border-radius:.5rem}table{border-collapse:collapse;width:100%}th,td{padding:.55rem;border-bottom:1px solid var(--line);text-align:left}@media(prefers-color-scheme:dark){:root{--line:#30363d;--card:#161b22}}</style><h1>knodin local telemetry</h1><p>Tokenizer: ${TELEMETRY_TOKENIZER}. Local, opt-in, metadata-only; source, raw output, command text, prompts, usernames, and paths are not persisted.</p><section><h2>Summary</h2><p>Total measured tokens avoided: ${summary.measuredTokensAvoided}</p><p>Calls: ${summary.calls}; successes: ${summary.successes}; errors: ${summary.errors}; measured baselines: ${summary.measuredBaselines}</p><p>Latency p50 / p95: ${summary.latencyP50Ms} / ${summary.latencyP95Ms} ms</p><p>Freshness failures: ${summary.freshnessFailures}; compression fidelity samples / p50: ${summary.compressionFidelitySamples} / ${summary.compressionFidelityP50}; indexing time: ${summary.indexingMs} ms; index storage: ${summary.indexStorageBytes} bytes</p></section><section><h2>Adoption evidence</h2><p>Sessions: ${adoption.sessions}; context delivered / degraded: ${adoption.contextDelivered} / ${adoption.contextDegraded}; sessions using knodin: ${adoption.knodinSessions}; observed turns: ${adoption.turns}</p><p>Fallback traversal after knodin: ${adoption.fallbackTraversalAfterKnodin}; edits preceded by knodin evidence in the same turn: ${adoption.evidenceBeforeEdit}</p><p>Corrective round trips: unavailable. Ordinary hook events do not prove why a follow-up occurred.</p></section><section><h2>Operation breakdown</h2><table><thead><tr><th>Operation</th><th>Calls</th><th>Errors</th><th>Measured baselines</th><th>Net tokens saved</th><th>p50 ms</th><th>p95 ms</th></tr></thead><tbody>${rows}</tbody></table></section><section><h2>Time series</h2><table><thead><tr><th>Day</th><th>Calls</th><th>Tokens avoided</th></tr></thead><tbody>${seriesRows}</tbody></table></section><section><h2>Repository breakdown</h2><table><thead><tr><th>Private repository id</th><th>Calls</th></tr></thead><tbody>${repositoryRows}</tbody></table></section><p>Use <code>knodin telemetry export</code> for the machine-readable evidence bundle and <code>knodin telemetry clear</code> to delete persisted records.</p>`;
|
|
225
227
|
atomicPrivateWrite(output, html);
|
|
226
228
|
return {
|
|
227
229
|
outputPath: path.relative(repo, output),
|
|
228
230
|
records: records.length,
|
|
229
231
|
operations: groups.size,
|
|
232
|
+
sessionEvents: sessionEvents.length,
|
|
230
233
|
};
|
|
231
234
|
}
|
|
232
235
|
function sanitizeTelemetryRecord(record) {
|
|
@@ -335,7 +338,7 @@ export function telemetryStatus(repoPath, inputPath = DEFAULT_INPUT, retentionDa
|
|
|
335
338
|
tokenizer: TELEMETRY_TOKENIZER,
|
|
336
339
|
};
|
|
337
340
|
}
|
|
338
|
-
export function exportTelemetry(repoPath, records, outputPath = ".knodin/telemetry-export.json") {
|
|
341
|
+
export function exportTelemetry(repoPath, records, outputPath = ".knodin/telemetry-export.json", sessionEvents = []) {
|
|
339
342
|
const { repo, target } = privatePath(repoPath, outputPath, "export");
|
|
340
343
|
const bundle = {
|
|
341
344
|
schemaVersion: 1,
|
|
@@ -349,6 +352,8 @@ export function exportTelemetry(repoPath, records, outputPath = ".knodin/telemet
|
|
|
349
352
|
},
|
|
350
353
|
summary: telemetrySummary(records),
|
|
351
354
|
records,
|
|
355
|
+
adoption: summarizeAdoption(sessionEvents),
|
|
356
|
+
sessionEvents,
|
|
352
357
|
};
|
|
353
358
|
atomicPrivateWrite(target, `${JSON.stringify(bundle, null, 2)}\n`);
|
|
354
359
|
return {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { clearLine as clearTerminalLine, moveCursor as moveTerminalCursor, cursorTo as moveTerminalCursorTo, } from "node:readline";
|
|
2
|
+
import { normalizeTerminalDimension } from "./init-progress.js";
|
|
3
|
+
export function createProgressStream(terminalEnabled) {
|
|
4
|
+
return {
|
|
5
|
+
isTTY: terminalEnabled,
|
|
6
|
+
columns: normalizeTerminalDimension(process.stderr.columns, 80),
|
|
7
|
+
rows: normalizeTerminalDimension(process.stderr.rows, 24),
|
|
8
|
+
write: process.stderr.write.bind(process.stderr),
|
|
9
|
+
cursorTo: (x, y, callback) => moveTerminalCursorTo(process.stderr, x, y, callback),
|
|
10
|
+
moveCursor: (dx, dy, callback) => moveTerminalCursor(process.stderr, dx, dy, callback),
|
|
11
|
+
clearLine: (direction, callback) => clearTerminalLine(process.stderr, direction, callback),
|
|
12
|
+
once: process.stderr.once.bind(process.stderr),
|
|
13
|
+
removeListener: process.stderr.removeListener.bind(process.stderr),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function consumeProgressLines(handle, finish) {
|
|
17
|
+
let buffer = "";
|
|
18
|
+
process.stdin.setEncoding("utf8");
|
|
19
|
+
process.stdin.on("data", (chunk) => {
|
|
20
|
+
buffer += chunk;
|
|
21
|
+
for (;;) {
|
|
22
|
+
const newline = buffer.indexOf("\n");
|
|
23
|
+
if (newline < 0)
|
|
24
|
+
break;
|
|
25
|
+
const line = buffer.slice(0, newline);
|
|
26
|
+
buffer = buffer.slice(newline + 1);
|
|
27
|
+
try {
|
|
28
|
+
handle(line);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Progress transport is observational and cannot affect lifecycle work.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
process.stdin.on("end", () => {
|
|
36
|
+
if (buffer) {
|
|
37
|
+
try {
|
|
38
|
+
handle(buffer);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Progress transport is observational and cannot affect lifecycle work.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
finish();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -1,27 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { clearLine as clearTerminalLine, moveCursor as moveTerminalCursor, cursorTo as moveTerminalCursorTo, } from "node:readline";
|
|
3
2
|
import ora from "ora";
|
|
4
|
-
import {
|
|
3
|
+
import { consumeProgressLines, createProgressStream } from "./progress-worker-runtime.js";
|
|
5
4
|
import { formatRepairProgress } from "./repair-progress.js";
|
|
6
5
|
const terminalEnabled = process.stderr.isTTY === true || process.env.KNODIN_FORCE_PROGRESS_TTY === "1";
|
|
7
|
-
const progressStream =
|
|
8
|
-
isTTY: terminalEnabled,
|
|
9
|
-
columns: normalizeTerminalDimension(process.stderr.columns, 80),
|
|
10
|
-
rows: normalizeTerminalDimension(process.stderr.rows, 24),
|
|
11
|
-
write: process.stderr.write.bind(process.stderr),
|
|
12
|
-
cursorTo: (x, y, callback) => moveTerminalCursorTo(process.stderr, x, y, callback),
|
|
13
|
-
moveCursor: (dx, dy, callback) => moveTerminalCursor(process.stderr, dx, dy, callback),
|
|
14
|
-
clearLine: (direction, callback) => clearTerminalLine(process.stderr, direction, callback),
|
|
15
|
-
once: process.stderr.once.bind(process.stderr),
|
|
16
|
-
removeListener: process.stderr.removeListener.bind(process.stderr),
|
|
17
|
-
};
|
|
6
|
+
const progressStream = createProgressStream(terminalEnabled);
|
|
18
7
|
const spinner = ora({
|
|
19
8
|
text: "[repair:starting] Opening local graph database (0s elapsed)",
|
|
20
9
|
stream: progressStream,
|
|
21
10
|
isEnabled: terminalEnabled,
|
|
22
11
|
discardStdin: false,
|
|
23
12
|
});
|
|
24
|
-
let buffer = "";
|
|
25
13
|
let stopped = false;
|
|
26
14
|
let exiting = false;
|
|
27
15
|
let startedAt = 0;
|
|
@@ -89,32 +77,7 @@ function handle(line) {
|
|
|
89
77
|
else
|
|
90
78
|
stop(true);
|
|
91
79
|
}
|
|
92
|
-
|
|
93
|
-
process.stdin.on("data", (chunk) => {
|
|
94
|
-
buffer += chunk;
|
|
95
|
-
for (;;) {
|
|
96
|
-
const newline = buffer.indexOf("\n");
|
|
97
|
-
if (newline < 0)
|
|
98
|
-
break;
|
|
99
|
-
const line = buffer.slice(0, newline);
|
|
100
|
-
buffer = buffer.slice(newline + 1);
|
|
101
|
-
try {
|
|
102
|
-
handle(line);
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
// Progress transport is observational; malformed input cannot affect repair.
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
process.stdin.on("end", () => {
|
|
110
|
-
if (buffer) {
|
|
111
|
-
try {
|
|
112
|
-
handle(buffer);
|
|
113
|
-
}
|
|
114
|
-
catch { }
|
|
115
|
-
}
|
|
116
|
-
stop(true);
|
|
117
|
-
});
|
|
80
|
+
consumeProgressLines(handle, () => stop(true));
|
|
118
81
|
process.on("SIGTERM", () => {
|
|
119
82
|
stop();
|
|
120
83
|
process.exit(0);
|