worktrust 0.6.13 → 0.6.15

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/log-session.mjs CHANGED
@@ -62,6 +62,8 @@ import { createHash, randomBytes, sign as cryptoSign } from "node:crypto";
62
62
 
63
63
  const args = process.argv.slice(2);
64
64
  const flag = (name) => args.includes(`--${name}`);
65
+ // Exit once stdout is written: to a pipe it is asynchronous and process.exit drops what is queued (0.6.13 cut --archive-lines).
66
+ const exitFlushed = (code) => new Promise((resolve) => process.stdout.write("", resolve)).then(() => process.exit(code));
65
67
  const value = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : undefined; };
66
68
  const DRY = flag("dry-run");
67
69
 
@@ -748,7 +750,7 @@ if (flag("uninstall")) {
748
750
  if (flag("purge")) for (const path of [COUNTER, PINNED, join(homedir(), ".worktrust", "log-session.mjs"), STATE, ...READER_FILES.map((name) => join(homedir(), ".worktrust", name)), join(homedir(), ".worktrust", "antigravity.json")]) { try { rmSync(path); } catch { /* already gone */ } }
749
751
  console.log(`${removed ? "removed" : "nothing wired"}: ${file}${flag("purge") ? "\n purged ~/.worktrust" : ""}`);
750
752
  console.log("the coupling itself still exists: end it in the app (Sources → the computer's row → end this coupling)");
751
- process.exit(0);
753
+ await exitFlushed(0);
752
754
  }
753
755
 
754
756
  if (flag("install")) {
@@ -827,7 +829,7 @@ if (flag("install")) {
827
829
  } catch (error) { console.log(`counter: could not fetch it (${error.message}) — the hook works without it; fetch /counter/count-behaviour.mjs later`); }
828
830
  }
829
831
  }
830
- if (already && !repaired) { console.log(hookRefreshed ? `hook updated: ${home} now runs the version you downloaded; the wiring was already right (${command})` : `already installed — nothing changed (${command})`); process.exit(0); }
832
+ if (already && !repaired) { console.log(hookRefreshed ? `hook updated: ${home} now runs the version you downloaded; the wiring was already right (${command})` : `already installed — nothing changed (${command})`); await exitFlushed(0); }
831
833
  for (const event of added) settings.hooks[event].push({ hooks: [{ type: "command", command }] });
832
834
  mkdirSync(dirname(file), { recursive: true });
833
835
  writeFileSync(file, JSON.stringify(settings, null, 2));
@@ -838,7 +840,7 @@ if (flag("install")) {
838
840
  // never rewrites: `npx worktrust` asks and writes it (WORKTRUST_CODEX_OFFER, so no hint twice), and
839
841
  // without it Codex rollouts are still swept whenever the Claude Code hook runs.
840
842
  if (existsSync(join(homedir(), ".codex")) && !process.env.WORKTRUST_CODEX_OFFER) console.log(`codex: to have Codex wake this hook too, add to ~/.codex/config.toml:\n notify = ${via ? `["${process.env.WORKTRUST_NODE || process.execPath}", "${via}", "hook", "--codex-notify"]` : `["node", "${home}", "--codex-notify"]`}\n (its rollouts are swept on every run either way)`);
841
- process.exit(0);
843
+ await exitFlushed(0);
842
844
  }
843
845
 
844
846
  const door = coupling();
@@ -853,7 +855,7 @@ const doorFor = (entry) => (String(entry?.id ?? "").startsWith("codex:") ? codex
853
855
  const entrySource = new WeakMap(); // an entry → "codex" or "antigravity" by the client that wrote it; the sends route and name by it
854
856
  const entryFile = new WeakMap(); // a history entry → its transcript (`--summary` counts sessions), first moment and folder name (`--archive-lines`)
855
857
  // THE LOCAL ARCHIVE (`npx worktrust preserve --archive`, 2026-10-06): every measured day, live window and floors aside, as JSON lines to preserve.mjs on this computer, which keeps metadata only; nothing is sent.
856
- if (flag("archive-lines")) { for (const entry of historyEntries(null, true)) console.log(JSON.stringify({ ...entry, client: entrySource.get(entry) ?? "claude", started_at: new Date(entryFile.get(entry).from).toISOString(), folder: entryFile.get(entry).folder })); process.exit(0); }
858
+ if (flag("archive-lines")) { const lines = historyEntries(null, true).map((entry) => JSON.stringify({ ...entry, client: entrySource.get(entry) ?? "claude", started_at: new Date(entryFile.get(entry).from).toISOString(), folder: entryFile.get(entry).folder })); process.stdout.write(`${[...lines, JSON.stringify({ archive_end: lines.length })].join("\n")}\n`); await exitFlushed(0); } // one write and a count, so the reader tells a whole answer from a cut one
857
859
  if (!door && !codexDoor && !DRY) { process.exit(0); } // Not coupled on this machine: nothing to do, quietly.
858
860
 
859
861
  if (flag("history")) {
@@ -862,16 +864,16 @@ if (flag("history")) {
862
864
  const hours = entries.reduce((sum, entry) => sum + (entry.seconds ?? 0), 0) / 3600;
863
865
  const months = [...new Set(entries.map((entry) => entry.at.slice(0, 7)))].sort();
864
866
  // ONE LINE FOR `npx worktrust` TO ASK WITH (2026-10-03): sessions, hours, first and last month; nothing sent.
865
- if (flag("summary")) { console.log(JSON.stringify({ sessions: new Set(entries.map((entry) => entryFile.get(entry).file)).size, hours: Math.round(hours * 10) / 10, first: months[0] ?? null, last: months.at(-1) ?? null })); process.exit(0); }
867
+ if (flag("summary")) { console.log(JSON.stringify({ sessions: new Set(entries.map((entry) => entryFile.get(entry).file)).size, hours: Math.round(hours * 10) / 10, first: months[0] ?? null, last: months.at(-1) ?? null })); await exitFlushed(0); }
866
868
  console.log(`${entries.length} day-stretches · ${hours.toFixed(1)} measured hours · ${months.join(", ")}`);
867
869
  if (DRY) {
868
870
  // One example in full: the shape is the argument for trusting it, and a summary hides it.
869
871
  if (entries[0]) console.log(`\nthe oldest of them, in full:\n${JSON.stringify(entries[0], null, 1)}`);
870
872
  console.log("\ndry run — nothing sent. Add --history without --dry-run to import them.");
871
- process.exit(0);
873
+ await exitFlushed(0);
872
874
  }
873
875
  // A batch the door refused is a failure the caller must see, not a quiet zero.
874
- process.exit((await sendHistory(door, entries, true)) === entries.length ? 0 : 1);
876
+ await exitFlushed((await sendHistory(door, entries, true)) === entries.length ? 0 : 1);
875
877
  }
876
878
 
877
879
  const input = hookInput();
@@ -964,4 +966,4 @@ if (!DRY && existsSync(COUNTER)) {
964
966
  } catch { /* the counter is a bonus; a hook that fails on it would cost the stretch it just logged */ }
965
967
  }
966
968
  }
967
- process.exit(0);
969
+ await exitFlushed(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktrust",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "description": "Couple this computer to WorkTrust: approve a code in your browser, and every AI app here reports what you built. Metadata only, no dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * --summary: what a person or a team reads from the archive, counted locally after the chain checks out. Per month the
3
3
  * hours, the tokens per model and the share of input served from the cache (where the tokens go, and so where they
4
- * can be saved); and the PARALLEL WORK: per project and day the sessions' summed time against the wall-clock time they
5
- * covered together, and the most open at once. A stretch without a project or a measured token stays out of that
4
+ * can be saved); and the PARALLEL WORK: per project and day the sessions' open spans added up against the wall-clock
5
+ * time they covered together (spans on both sides, so the sum is never below the clock), and the most open at once. A stretch without a project or a measured token stays out of that
6
6
  * figure; nothing is estimated.
7
7
  *
8
8
  * Pure: the archive's verified lines in, the lines to print out. No file, no clock, no network.
@@ -31,7 +31,7 @@ export function summaryLines(lines) {
31
31
  const edges = spans.flatMap(([from, to]) => [[from, 1], [to, -1]]).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
32
32
  let open = 0, top = 0, covered = 0, since = 0;
33
33
  for (const [at, step] of edges) { if (open > 0) covered += at - since; open += step; since = at; top = Math.max(top, open); }
34
- if (top > 1) { parallelDays += 1; peak = Math.max(peak, top); summed += spans.reduce((sum, span) => sum + span[2], 0); wall += covered / 1000; }
34
+ if (top > 1) { parallelDays += 1; peak = Math.max(peak, top); summed += spans.reduce((sum, [from, to]) => sum + (to - from), 0) / 1000; wall += covered / 1000; }
35
35
  }
36
36
  if (parallelDays > 0) out.push(` parallel sessions in one project on ${parallelDays} day${parallelDays === 1 ? "" : "s"}: up to ${peak} at once, ${hours(summed)} h of session time in ${hours(wall)} h of wall-clock time`);
37
37
  }
package/preserve.mjs CHANGED
@@ -271,13 +271,15 @@ function archive(dir) {
271
271
  if (existing.broken) { say(` The archive in ${dir} does not verify, so nothing was added: ${existing.broken}`); process.exit(1); }
272
272
  const hook = spawnSync(process.execPath, [join(HERE, "log-session.mjs"), "--archive-lines"], { encoding: "utf8", maxBuffer: 1 << 29, env: { ...process.env, WORKTRUST_MCP_URL: "", WORKTRUST_MCP_TOKEN: "", WORKTRUST_HOOK_INPUT: "" } });
273
273
  if (hook.status !== 0) { say(` The session hook could not read this computer's sessions: ${(hook.stderr || "").trim().split("\n").pop()}`); process.exit(1); }
274
- const known = new Set(existing.lines.map((line) => line.stretch_ref));
275
- const fresh = [];
274
+ // A WHOLE ANSWER OR NOTHING: the hook ends with its own count; one line missing or cut is a read to refuse, never part of an archive.
275
+ const rows = hook.stdout.split("\n").filter(Boolean), end = (() => { try { return JSON.parse(rows.at(-1) ?? "").archive_end; } catch { return undefined; } })();
276
+ if (!Number.isInteger(end) || end !== rows.length - 1) { say(` The session hook's answer came back incomplete (${rows.length - 1} of ${end ?? "an unknown number of"} lines), so nothing was added. Run it again.`); process.exit(1); }
277
+ const known = new Set(existing.lines.map((line) => line.stretch_ref)), fresh = [], measured = new Set();
276
278
  let waiting = 0;
277
- for (const text of hook.stdout.split("\n")) {
279
+ for (const text of rows.slice(0, -1)) {
278
280
  let entry; try { entry = JSON.parse(text); } catch { continue; }
279
281
  const line = archiveLine(entry);
280
- if (!line || known.has(line.stretch_ref)) continue;
282
+ if (line && line.day < today()) measured.add(line.stretch_ref); if (!line || known.has(line.stretch_ref)) continue; // a file present twice is one stretch
281
283
  if (line.day >= today()) { waiting += 1; continue; }
282
284
  known.add(line.stretch_ref);
283
285
  fresh.push(line);
@@ -303,7 +305,7 @@ function archive(dir) {
303
305
  }
304
306
  if (fresh.length > 0 || !existing.manifest) writeFileSync(join(dir, "manifest.json"), `${JSON.stringify({ format: "worktrust-ai-evidence/1", collector_version: COLLECTOR, created: existing.manifest?.created ?? new Date().toISOString(), updated: new Date().toISOString(), clients: [...new Set(all.map((line) => line.client))].sort(), ...IDS, count: all.length, head: prev }, null, 1)}\n`);
305
307
  say();
306
- say(` ✓ ${fresh.length} new day-stretch${fresh.length === 1 ? "" : "es"} archived in ${dir} (${all.length} in all)${key ? ", each day's root signed with this computer's device key" : ", unsigned (no device key on this computer)"}.`);
308
+ say(` ✓ ${fresh.length} new day-stretch${fresh.length === 1 ? "" : "es"} archived in ${dir}: it holds ${all.length}, the session hook measures ${measured.size} finished ones here now${key ? ", each day's root signed with this computer's device key" : ", unsigned (no device key on this computer)"}.`);
307
309
  if (waiting > 0) say(` ${waiting} of today wait for tomorrow's run: a day is archived once it is over.`);
308
310
  say(" Metadata only: clocks, counts, model names, a hash of each project's name. Nothing was sent.");
309
311
  }
package/worktrust.mjs CHANGED
@@ -69,7 +69,7 @@ const command = args.find((arg, at) => !arg.startsWith("--") && !(at > 0 && VALU
69
69
  const flag = (name) => { const at = args.indexOf(`--${name}`); return at >= 0 ? args[at + 1] : undefined; };
70
70
  const has = (name) => args.includes(`--${name}`);
71
71
  /** This CLI's version, said to the door so the app can tell which computer runs an old one (check-cli-package holds it equal to package.json). */
72
- const CLI_VERSION = "0.6.13";
72
+ const CLI_VERSION = "0.6.15";
73
73
  const ORIGIN = (flag("origin") ?? process.env.WORKTRUST_ORIGIN ?? "https://app.worktrust.io").replace(/\/$/, "");
74
74
  const MCP = flag("url") ?? process.env.WORKTRUST_MCP_URL ?? `${ORIGIN}/api/mcp`;
75
75
  const HOME_DIR = join(homedir(), ".worktrust");