worktrust 0.6.13 → 0.6.14

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.14",
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": {
package/preserve.mjs CHANGED
@@ -271,10 +271,12 @@ 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 = [];
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
282
  if (!line || known.has(line.stretch_ref)) continue;
@@ -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 ${rows.length - 1 - waiting} 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.14";
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");