worktrust 0.4.1 → 0.6.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WorkTrust
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -4,16 +4,16 @@ 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 connect
7
+ npx worktrust
8
8
  ```
9
9
 
10
10
  **The only official package** is `worktrust`, published by the npm account **worktrustio** with
11
11
  provenance from this repository (github.com/fhomey/worktrust-cli). Check it on npmjs.com before you run
12
- it; a package of another name or from another publisher is not ours. Until the first version is on npm,
13
- use the same script from WorkTrust's own site:
12
+ it; a package of another name or from another publisher is not ours. Without npm, the same script runs
13
+ from WorkTrust's own site:
14
14
 
15
15
  ```
16
- curl -fsSO https://app.worktrust.io/counter/worktrust.mjs && node worktrust.mjs connect
16
+ curl -fsSO https://app.worktrust.io/counter/worktrust.mjs && node worktrust.mjs
17
17
  ```
18
18
 
19
19
  ## What happens
@@ -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
  ```
@@ -54,7 +58,7 @@ which would delete every password on the computer, and many people rightly keep
54
58
  signature. A key that leaks without the file, from a log, a proxy or a config, is worth nothing.
55
59
  - **The key renews itself every week.** A new secret is made here, only its hash is sent, signed,
56
60
  and last week's key stops working.
57
- - `npx worktrust connect --direct` writes a plain key into the apps' settings instead, for an app
61
+ - `npx worktrust --direct` writes a plain key into the apps' settings instead, for an app
58
62
  that cannot run a local command. Such a key is not bound to the computer and does not renew.
59
63
 
60
64
  ## What it reads, writes and sends
@@ -93,3 +97,8 @@ Then revoke the key in WorkTrust: Sources → Devices → this computer → Revo
93
97
  Node 18 or later. macOS, Linux or Windows.
94
98
 
95
99
  Security reports: see [SECURITY.md](SECURITY.md).
100
+
101
+ ## Licence
102
+
103
+ MIT, for this command-line tool only (see LICENSE). The WorkTrust service it couples to is not
104
+ open source.
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
  }
@@ -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.4.1",
3
+ "version": "0.6.0",
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": {
@@ -12,12 +12,13 @@
12
12
  "log-session.mjs",
13
13
  "count-behaviour.mjs",
14
14
  "README.md",
15
- "SECURITY.md"
15
+ "SECURITY.md",
16
+ "LICENSE"
16
17
  ],
17
18
  "engines": {
18
19
  "node": ">=18"
19
20
  },
20
- "license": "UNLICENSED",
21
+ "license": "MIT",
21
22
  "homepage": "https://worktrust.io",
22
23
  "keywords": [
23
24
  "worktrust",
package/worktrust.mjs CHANGED
@@ -2,10 +2,13 @@
2
2
  /**
3
3
  * WorkTrust on a computer, in one command — like `gh auth login` (2026-10-03).
4
4
  *
5
- * npx worktrust connect show the plan, ask, approve a code in the app, couple
6
- * npx worktrust connect --dry-run show the plan only; nothing is written, nothing is sent
7
- * npx worktrust connect --yes no question (for scripts); the computer is still approved in the app
8
- * npx worktrust connect --device a computer without a browser (SSH): type a code on another device
5
+ * npx worktrust show the plan, ask, approve this computer in the app, couple;
6
+ * on a computer already coupled: what is coupled here
7
+ * npx worktrust --dry-run show the plan only; nothing is written, nothing is sent
8
+ * npx worktrust --yes no question (for scripts); the computer is still approved in the app
9
+ * npx worktrust --device a computer without a browser (SSH): type a code on another device
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)
9
12
  * npx worktrust disconnect show what comes out, ask, take it out again
10
13
  * npx worktrust status what is coupled here
11
14
  *
@@ -56,7 +59,7 @@ import { fileURLToPath } from "node:url";
56
59
 
57
60
  const args = process.argv.slice(2);
58
61
  const VALUED = ["--name", "--origin", "--url"];
59
- const command = args.find((arg, at) => !arg.startsWith("--") && !(at > 0 && VALUED.includes(args[at - 1]))) ?? "help";
62
+ const command = args.find((arg, at) => !arg.startsWith("--") && !(at > 0 && VALUED.includes(args[at - 1]))) ?? (args.includes("--help") ? "help" : "default");
60
63
  const flag = (name) => { const at = args.indexOf(`--${name}`); return at >= 0 ? args[at + 1] : undefined; };
61
64
  const has = (name) => args.includes(`--${name}`);
62
65
  const ORIGIN = (flag("origin") ?? process.env.WORKTRUST_ORIGIN ?? "https://app.worktrust.io").replace(/\/$/, "");
@@ -93,9 +96,9 @@ async function scripts() {
93
96
  }
94
97
 
95
98
  /** Run a script; its output is shown, or collected for the plan. The key travels in the environment only. */
96
- function run(path, scriptArgs, token, collect = false) {
99
+ function run(path, scriptArgs, token, collect = false, extraEnv = {}) {
97
100
  return new Promise((resolve) => {
98
- 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 } });
99
102
  let out = "";
100
103
  child.stdout?.on("data", (chunk) => { out += chunk; });
101
104
  child.stderr?.on("data", (chunk) => { out += chunk; });
@@ -103,13 +106,14 @@ function run(path, scriptArgs, token, collect = false) {
103
106
  });
104
107
  }
105
108
 
106
- async function ask(question) {
107
- 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;
108
112
  if (!process.stdin.isTTY) fail("not a terminal: nothing was changed. Run it in a terminal, or add --yes to agree in advance.");
109
113
  const prompt = createInterface({ input: process.stdin, output: process.stdout });
110
- const answer = (await prompt.question(` ${question} [Y/n] `)).trim().toLowerCase();
114
+ const answer = (await prompt.question(` ${question} ${yes ? "[Y/n]" : "[y/N]"} `)).trim().toLowerCase();
111
115
  prompt.close();
112
- return answer === "" || answer === "y" || answer === "yes";
116
+ return answer === "" ? yes : answer === "y" || answer === "yes";
113
117
  }
114
118
 
115
119
  function openBrowser(url) {
@@ -396,11 +400,38 @@ async function connect() {
396
400
  say();
397
401
  const hookArgs = ["--install", ...(BUNDLED ? ["--counter", paths["count-behaviour.mjs"]] : []), ...(direct ? [] : ["--via", STABLE])];
398
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);
399
404
  say();
400
405
  say(" Done. Quit your AI apps completely and open them again. This computer appears in WorkTrust");
401
406
  say(" under Sources → Devices once one of them has spoken.");
402
407
  }
403
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
+
404
435
  async function disconnect() {
405
436
  say();
406
437
  say(` WorkTrust · disconnect ${hostname().replace(/\.local$/i, "")}`);
@@ -432,7 +463,7 @@ function status() {
432
463
  for (const project of Object.values(config.projects ?? {})) Object.assign(servers, project.mcpServers ?? {});
433
464
  } catch { /* no Claude Code here */ }
434
465
  const door = Object.entries(servers).find(([name, server]) => /worktrust/i.test(`${name} ${server?.url ?? ""} ${(server?.args ?? []).join(" ")}`));
435
- say(door ? ` Claude Code: coupled (${door[1].url ?? "through the local bridge"})` : " Claude Code: not coupled. Run `worktrust connect`.");
466
+ say(door ? ` Claude Code: coupled (${door[1].url ?? "through the local bridge"})` : " Claude Code: not coupled. Run `npx worktrust connect`.");
436
467
  const key = keyStore.load();
437
468
  say(key ? ` Key: in ${KEY_PLACE}, ${key.device ? "bound to this computer, " : ""}last renewed ${String(key.renewedAt ?? "").slice(0, 10) || "never"}` : " Key: not on this computer (or written into the apps' settings with --direct)");
438
469
  let hook = false;
@@ -444,19 +475,27 @@ function status() {
444
475
  function help() {
445
476
  say("WorkTrust: couple this computer to your record. Metadata only.");
446
477
  say();
447
- say(" worktrust connect show the plan, ask, approve a code in your browser, couple");
478
+ say(" worktrust couple this computer: show the plan, ask, approve in your browser");
479
+ say(" (already coupled: shows what is coupled here)");
448
480
  say(" --dry-run the plan only: nothing is written, nothing is sent");
449
481
  say(" --yes no question (the computer is still approved in the browser)");
450
482
  say(" --device no browser here (SSH): type a code on another device");
451
483
  say(" --name <name> the name this computer gets (default: its host name)");
452
484
  say(" --direct put the key in each app's settings instead of the key file");
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)");
453
487
  say(" worktrust disconnect take WorkTrust out of every AI app here (asks first)");
454
488
  say(" worktrust status what is coupled here");
455
489
  }
456
490
 
457
- if (command === "mcp") await bridge();
491
+ // THE BARE COMMAND COUPLES (owner, 2026-10-03: "zo kort mogelijk"). `npx worktrust` on a computer
492
+ // without a key here shows the plan and asks, exactly as `connect` does; with one it says what is
493
+ // coupled, so running it twice never re-pairs by surprise.
494
+ if (command === "default") { if (keyStore.load()) { status(); say(); say(" This computer is coupled. `npx worktrust connect` couples it again; `npx worktrust disconnect` takes it out."); } else await connect(); }
495
+ else if (command === "mcp") await bridge();
458
496
  else if (command === "hook") await hook();
459
497
  else if (command === "connect") await connect();
498
+ else if (command === "history") await history();
460
499
  else if (command === "disconnect") await disconnect();
461
500
  else if (command === "status") status();
462
501
  else help();