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.
@@ -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, normalizeTerminalDimension, } from "./init-progress.js";
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
- process.stdin.setEncoding("utf8");
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 { normalizeTerminalDimension } from "./init-progress.js";
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
- process.stdin.setEncoding("utf8");
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);