worktrust 0.5.0 → 0.6.1

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 CHANGED
@@ -4,7 +4,7 @@ Couple a computer to [WorkTrust](https://worktrust.io) in one command. WorkTrust
4
4
  record of the work you do with AI without anyone reading that work: it receives metadata only.
5
5
 
6
6
  ```
7
- npx worktrust
7
+ npx worktrust@latest
8
8
  ```
9
9
 
10
10
  **The only official package** is `worktrust`, published by the npm account **worktrustio** with
@@ -32,8 +32,12 @@ curl -fsSO https://app.worktrust.io/counter/worktrust.mjs && node worktrust.mjs
32
32
  3. **The terminal finishes by itself.** It keeps the key in its private file and gives every
33
33
  AI app here the WorkTrust door, then installs the session hook. Quit your AI apps and open them
34
34
  again.
35
+ 4. **Earlier work only if you say yes.** It counts the Claude Code and Codex sessions already on
36
+ this computer and asks `Send these as history? [y/N]`. Enter sends nothing. What goes is hours
37
+ and tokens per day, never text, and WorkTrust shows it as earlier work, never as verified hours.
35
38
 
36
39
  ```
40
+ npx worktrust history send this computer's earlier sessions later (asks first)
37
41
  npx worktrust status what is coupled here
38
42
  npx worktrust disconnect take it out again (shows the plan, asks first)
39
43
  ```
package/log-session.mjs CHANGED
@@ -23,6 +23,7 @@
23
23
  * node log-session.mjs --uninstall [--purge] take it out again (--purge also deletes the copies)
24
24
  * node log-session.mjs --dry-run show what the current transcripts would send
25
25
  * node log-session.mjs --history [--dry-run] send the months already on this machine, as HISTORY
26
+ * node log-session.mjs --history --summary one JSON line: sessions, hours, first and last month (sends nothing)
26
27
  * (as a hook) reads the hook's JSON on stdin, logs, exits
27
28
  * (from Codex) notify = ["node", "~/.worktrust/log-session.mjs", "--codex-notify"]
28
29
  * in ~/.codex/config.toml: Codex calls it at every turn's end and
@@ -587,6 +588,7 @@ function historyEntries() {
587
588
  from: messages[0].at, to: last,
588
589
  });
589
590
  if (CODEX_ROLLOUT.test(file)) entrySource.set(entry, "codex");
591
+ entryFile.set(entry, file);
590
592
  entries.push(entry);
591
593
  void day;
592
594
  }
@@ -751,7 +753,7 @@ if (flag("install")) {
751
753
  // Codex has no settings.json to wire; its notify hook is one line in config.toml, added by hand so
752
754
  // this script never rewrites a TOML file it did not create. Without it, Codex rollouts are still
753
755
  // swept whenever the Claude Code hook runs.
754
- if (existsSync(join(homedir(), ".codex"))) console.log(`codex: to have Codex wake this hook too, add to ~/.codex/config.toml:\n notify = ${via ? `["${process.execPath}", "${via}", "hook", "--codex-notify"]` : `["node", "${home}", "--codex-notify"]`}\n (its rollouts are swept on every run either way)`);
756
+ if (existsSync(join(homedir(), ".codex"))) 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)`);
755
757
  process.exit(0);
756
758
  }
757
759
 
@@ -765,12 +767,15 @@ const door = coupling();
765
767
  const codexDoor = codexCoupling();
766
768
  const doorFor = (entry) => (String(entry?.id ?? "").startsWith("codex:") ? codexDoor ?? door : door);
767
769
  const entrySource = new WeakMap(); // a history entry → "codex" when its rollout was Codex's; sendHistory routes by it
770
+ const entryFile = new WeakMap(); // a history entry → its transcript, so `--summary` can count sessions
768
771
  if (!door && !codexDoor && !DRY) { process.exit(0); } // Not coupled on this machine: nothing to do, quietly.
769
772
 
770
773
  if (flag("history")) {
771
774
  const entries = historyEntries();
772
775
  const hours = entries.reduce((sum, entry) => sum + (entry.seconds ?? 0), 0) / 3600;
773
776
  const months = [...new Set(entries.map((entry) => entry.at.slice(0, 7)))].sort();
777
+ // ONE LINE FOR `npx worktrust` TO ASK WITH (2026-10-03): sessions, hours, first and last month; nothing sent.
778
+ if (flag("summary")) { console.log(JSON.stringify({ sessions: new Set(entries.map((entry) => entryFile.get(entry))).size, hours: Math.round(hours * 10) / 10, first: months[0] ?? null, last: months.at(-1) ?? null })); process.exit(0); }
774
779
  console.log(`${entries.length} day-stretches · ${hours.toFixed(1)} measured hours · ${months.join(", ")}`);
775
780
  if (DRY) {
776
781
  // One example in full: the shape is the argument for trusting it, and a summary hides it.
@@ -778,8 +783,8 @@ if (flag("history")) {
778
783
  console.log("\ndry run — nothing sent. Add --history without --dry-run to import them.");
779
784
  process.exit(0);
780
785
  }
781
- await sendHistory(door, entries, true);
782
- process.exit(0);
786
+ // A batch the door refused is a failure the caller must see, not a quiet zero.
787
+ process.exit((await sendHistory(door, entries, true)) === entries.length ? 0 : 1);
783
788
  }
784
789
 
785
790
  const input = hookInput();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktrust",
3
- "version": "0.5.0",
3
+ "version": "0.6.1",
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/worktrust.mjs CHANGED
@@ -8,6 +8,7 @@
8
8
  * npx worktrust --yes no question (for scripts); the computer is still approved in the app
9
9
  * npx worktrust --device a computer without a browser (SSH): type a code on another device
10
10
  * npx worktrust connect couple again, also on a computer already coupled
11
+ * npx worktrust history send this computer's earlier sessions as history (asks first)
11
12
  * npx worktrust disconnect show what comes out, ask, take it out again
12
13
  * npx worktrust status what is coupled here
13
14
  *
@@ -95,9 +96,9 @@ async function scripts() {
95
96
  }
96
97
 
97
98
  /** Run a script; its output is shown, or collected for the plan. The key travels in the environment only. */
98
- function run(path, scriptArgs, token, collect = false) {
99
+ function run(path, scriptArgs, token, collect = false, extraEnv = {}) {
99
100
  return new Promise((resolve) => {
100
- const child = spawn(process.execPath, [path, ...scriptArgs], { stdio: collect ? ["ignore", "pipe", "pipe"] : "inherit", env: { ...process.env, WORKTRUST_MCP_URL: MCP, WORKTRUST_MCP_TOKEN: token, WORKTRUST_NODE: NODE } });
101
+ const child = spawn(process.execPath, [path, ...scriptArgs], { stdio: collect ? ["ignore", "pipe", "pipe"] : "inherit", env: { ...process.env, WORKTRUST_MCP_URL: MCP, WORKTRUST_MCP_TOKEN: token, WORKTRUST_NODE: NODE, ...extraEnv } });
101
102
  let out = "";
102
103
  child.stdout?.on("data", (chunk) => { out += chunk; });
103
104
  child.stderr?.on("data", (chunk) => { out += chunk; });
@@ -105,13 +106,14 @@ function run(path, scriptArgs, token, collect = false) {
105
106
  });
106
107
  }
107
108
 
108
- async function ask(question) {
109
- if (has("yes")) return true;
109
+ /** `yes` is the answer Enter gives, and the one `--yes` gives: a question whose default is No stays No under --yes. */
110
+ async function ask(question, yes = true) {
111
+ if (has("yes")) return yes;
110
112
  if (!process.stdin.isTTY) fail("not a terminal: nothing was changed. Run it in a terminal, or add --yes to agree in advance.");
111
113
  const prompt = createInterface({ input: process.stdin, output: process.stdout });
112
- const answer = (await prompt.question(` ${question} [Y/n] `)).trim().toLowerCase();
114
+ const answer = (await prompt.question(` ${question} ${yes ? "[Y/n]" : "[y/N]"} `)).trim().toLowerCase();
113
115
  prompt.close();
114
- return answer === "" || answer === "y" || answer === "yes";
116
+ return answer === "" ? yes : answer === "y" || answer === "yes";
115
117
  }
116
118
 
117
119
  function openBrowser(url) {
@@ -398,11 +400,38 @@ async function connect() {
398
400
  say();
399
401
  const hookArgs = ["--install", ...(BUNDLED ? ["--counter", paths["count-behaviour.mjs"]] : []), ...(direct ? [] : ["--via", STABLE])];
400
402
  if ((await run(paths["log-session.mjs"], hookArgs, token)).code !== 0) fail("installing the session hook failed; the output above says where.");
403
+ await offerHistory(paths, token, device ? device.privateKey.export({ format: "pem", type: "pkcs8" }) : null);
401
404
  say();
402
405
  say(" Done. Quit your AI apps completely and open them again. This computer appears in WorkTrust");
403
406
  say(" under Sources → Devices once one of them has spoken.");
404
407
  }
405
408
 
409
+ /**
410
+ * EARLIER WORK IS SENT ONLY ON A YES (owner, 2026-10-03). After coupling, or as `npx worktrust history`:
411
+ * what this computer holds is counted here and shown, and nothing leaves before the person types y.
412
+ * Enter, --yes and a closed terminal all mean No; `--history` says yes in advance. What goes is hours
413
+ * and tokens per day, filed as history (never verified hours, never text).
414
+ */
415
+ async function offerHistory(paths, token, devicePem, url = MCP) {
416
+ const env = { WORKTRUST_MCP_URL: url, ...(devicePem ? { WORKTRUST_DEVICE_KEY: devicePem } : {}) };
417
+ const counted = await run(paths["log-session.mjs"], ["--history", "--dry-run", "--summary"], token, true, env);
418
+ let found = null;
419
+ try { found = JSON.parse(counted.out.trim().split("\n").pop()); } catch { /* no line: nothing to offer */ }
420
+ say();
421
+ if (!found?.sessions) { say(" No earlier Claude Code or Codex sessions on this computer to send."); return; }
422
+ say(` This computer holds ${found.sessions} earlier Claude Code / Codex sessions · ${found.hours} measured hours · ${found.first} … ${found.last}.`);
423
+ if (has("dry-run")) { say(" Dry run: nothing was sent."); return; }
424
+ if (!has("history") && !(await ask("Send these as history (hours and tokens per day; never text)?", false))) { say(" Not sent. Send them later with: npx worktrust history"); return; }
425
+ const sent = await run(paths["log-session.mjs"], ["--history"], token, true, env);
426
+ say(sent.code === 0 ? " ✓ Sent as history. It shows in WorkTrust as earlier work, never as verified hours." : " Not all of it arrived. Run npx worktrust history again: a line already received is kept once.");
427
+ }
428
+
429
+ async function history() {
430
+ const key = await freshKey();
431
+ if (!key) fail("this computer is not coupled through the key file. Run npx worktrust first.");
432
+ await offerHistory(await scripts(), key.token, key.device ?? null, key.url);
433
+ }
434
+
406
435
  async function disconnect() {
407
436
  say();
408
437
  say(` WorkTrust · disconnect ${hostname().replace(/\.local$/i, "")}`);
@@ -454,6 +483,7 @@ function help() {
454
483
  say(" --name <name> the name this computer gets (default: its host name)");
455
484
  say(" --direct put the key in each app's settings instead of the key file");
456
485
  say(" worktrust connect couple again, also when this computer is already coupled");
486
+ say(" worktrust history send this computer's earlier sessions as history (asks first)");
457
487
  say(" worktrust disconnect take WorkTrust out of every AI app here (asks first)");
458
488
  say(" worktrust status what is coupled here");
459
489
  }
@@ -465,6 +495,7 @@ if (command === "default") { if (keyStore.load()) { status(); say(); say(" This
465
495
  else if (command === "mcp") await bridge();
466
496
  else if (command === "hook") await hook();
467
497
  else if (command === "connect") await connect();
498
+ else if (command === "history") await history();
468
499
  else if (command === "disconnect") await disconnect();
469
500
  else if (command === "status") status();
470
501
  else help();