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 +21 -0
- package/README.md +14 -5
- package/log-session.mjs +7 -2
- package/package.json +4 -3
- package/worktrust.mjs +53 -14
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
|
|
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.
|
|
13
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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
|
|
6
|
-
*
|
|
7
|
-
* npx worktrust
|
|
8
|
-
* npx worktrust
|
|
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
|
-
|
|
107
|
-
|
|
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 === ""
|
|
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
|
|
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
|
-
|
|
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();
|