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 +10 -8
- package/package.json +1 -1
- package/preserve-summary.mjs +3 -3
- package/preserve.mjs +7 -5
- package/worktrust.mjs +1 -1
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
|
-
|
|
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})`);
|
|
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
|
-
|
|
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")) {
|
|
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 }));
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
969
|
+
await exitFlushed(0);
|
package/package.json
CHANGED
package/preserve-summary.mjs
CHANGED
|
@@ -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'
|
|
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,
|
|
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
|
-
|
|
275
|
-
const
|
|
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
|
|
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}
|
|
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.
|
|
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");
|