privateer-agent 0.15.4 → 0.15.6

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.
Files changed (229) hide show
  1. package/README.md +11 -0
  2. package/bin/boot-bundle.d.mts +2 -0
  3. package/bin/boot-bundle.mjs +44 -0
  4. package/bin/fresh-supervisor.d.mts +5 -1
  5. package/bin/fresh-supervisor.mjs +47 -10
  6. package/bin/privateer-launch.mjs +27 -3
  7. package/bin/privateer-subagent.mjs +5 -2
  8. package/bin/privateer-verify.mjs +12 -0
  9. package/bin/session-recovery.d.mts +53 -0
  10. package/bin/session-recovery.mjs +240 -0
  11. package/bin/session-seal-preload.mjs +17 -0
  12. package/bin/session-seal.d.mts +25 -0
  13. package/bin/session-seal.mjs +242 -0
  14. package/dist-boot/boot.mjs +3 -0
  15. package/dist-boot/c-27JKSMWL.mjs +1460 -0
  16. package/dist-boot/c-2BMRFV24.mjs +269 -0
  17. package/dist-boot/c-2CTF6JA3.mjs +166 -0
  18. package/dist-boot/c-2G3BBHRJ.mjs +84 -0
  19. package/dist-boot/c-2IDLKXX6.mjs +250 -0
  20. package/dist-boot/c-2NJD5VYM.mjs +8 -0
  21. package/dist-boot/c-2S5EEQPW.mjs +1602 -0
  22. package/dist-boot/c-2WRDCUVI.mjs +25 -0
  23. package/dist-boot/c-2YN3M256.mjs +14 -0
  24. package/dist-boot/c-37GM3GMU.mjs +187 -0
  25. package/dist-boot/c-3STKRSVO.mjs +2480 -0
  26. package/dist-boot/c-3TPV6TQQ.mjs +2320 -0
  27. package/dist-boot/c-3UVHLQDJ.mjs +225 -0
  28. package/dist-boot/c-42PUUZYC.mjs +697 -0
  29. package/dist-boot/c-4D5UMWVW.mjs +683 -0
  30. package/dist-boot/c-4ETZRMTA.mjs +46 -0
  31. package/dist-boot/c-4QT25BD5.mjs +1482 -0
  32. package/dist-boot/c-4WVR32AP.mjs +2579 -0
  33. package/dist-boot/c-5EMM567G.mjs +152 -0
  34. package/dist-boot/c-5FA4CV63.mjs +145 -0
  35. package/dist-boot/c-5GV3X3RB.mjs +14 -0
  36. package/dist-boot/c-5JCEPRIY.mjs +52 -0
  37. package/dist-boot/c-5NDKIIWT.mjs +16 -0
  38. package/dist-boot/c-5QL3AR26.mjs +4665 -0
  39. package/dist-boot/c-5XY6MFKN.mjs +5142 -0
  40. package/dist-boot/c-65UZIZC7.mjs +2897 -0
  41. package/dist-boot/c-6FUXKHOK.mjs +1969 -0
  42. package/dist-boot/c-6JHWQE4V.mjs +18242 -0
  43. package/dist-boot/c-6LOBR4I6.mjs +5280 -0
  44. package/dist-boot/c-6QHOZXKQ.mjs +14394 -0
  45. package/dist-boot/c-6ZO2SADQ.mjs +178 -0
  46. package/dist-boot/c-6ZORVVZP.mjs +1449 -0
  47. package/dist-boot/c-76QH4UCP.mjs +2441 -0
  48. package/dist-boot/c-77GO5L5Y.mjs +20982 -0
  49. package/dist-boot/c-7M23XFTN.mjs +5052 -0
  50. package/dist-boot/c-7TC367E2.mjs +32 -0
  51. package/dist-boot/c-7UOG7POX.mjs +1524 -0
  52. package/dist-boot/c-7VHP3FLN.mjs +376 -0
  53. package/dist-boot/c-A73JHZP2.mjs +13207 -0
  54. package/dist-boot/c-ABXULAFL.mjs +71 -0
  55. package/dist-boot/c-ANDD2AQO.mjs +74 -0
  56. package/dist-boot/c-AOY77AZL.mjs +1430 -0
  57. package/dist-boot/c-AXEPZ22I.mjs +108 -0
  58. package/dist-boot/c-B2Y7VNHU.mjs +427 -0
  59. package/dist-boot/c-B7VJUH73.mjs +77 -0
  60. package/dist-boot/c-BFW6CDVL.mjs +24 -0
  61. package/dist-boot/c-BHNIF2AR.mjs +188 -0
  62. package/dist-boot/c-BKF2O6BL.mjs +23 -0
  63. package/dist-boot/c-BOZXFTZN.mjs +8 -0
  64. package/dist-boot/c-BRNYJWZL.mjs +28 -0
  65. package/dist-boot/c-C3N4Z2HO.mjs +93 -0
  66. package/dist-boot/c-C4IV7ZFG.mjs +717 -0
  67. package/dist-boot/c-C5NK6UYV.mjs +793 -0
  68. package/dist-boot/c-C67644MB.mjs +1659 -0
  69. package/dist-boot/c-C6S2YB53.mjs +620 -0
  70. package/dist-boot/c-CDBEJSBF.mjs +37 -0
  71. package/dist-boot/c-CG47HBKU.mjs +435 -0
  72. package/dist-boot/c-CKCTWCT5.mjs +51 -0
  73. package/dist-boot/c-CN77DCGF.mjs +60 -0
  74. package/dist-boot/c-CRBXNKFO.mjs +1795 -0
  75. package/dist-boot/c-CRIHASQ6.mjs +973 -0
  76. package/dist-boot/c-CU4NGAUO.mjs +3786 -0
  77. package/dist-boot/c-CWRFLGQU.mjs +1883 -0
  78. package/dist-boot/c-CYQB2USO.mjs +32 -0
  79. package/dist-boot/c-DEE67DJO.mjs +379 -0
  80. package/dist-boot/c-DIFDBYOP.mjs +1258 -0
  81. package/dist-boot/c-DOSGVXDZ.mjs +34879 -0
  82. package/dist-boot/c-DR7XQI32.mjs +381 -0
  83. package/dist-boot/c-DTGPSB46.mjs +14666 -0
  84. package/dist-boot/c-DTHDNA4P.mjs +320 -0
  85. package/dist-boot/c-E4Z3Y5B5.mjs +3864 -0
  86. package/dist-boot/c-E66Z2TI7.mjs +203 -0
  87. package/dist-boot/c-EBU6RLRV.mjs +1527 -0
  88. package/dist-boot/c-EBZZNGSF.mjs +432 -0
  89. package/dist-boot/c-EC3LJ2KF.mjs +1032 -0
  90. package/dist-boot/c-EQBKXWNX.mjs +15829 -0
  91. package/dist-boot/c-EUEPIW4S.mjs +574 -0
  92. package/dist-boot/c-EX4YVWAT.mjs +378 -0
  93. package/dist-boot/c-EYZGSAJU.mjs +98 -0
  94. package/dist-boot/c-F75VRKY4.mjs +172 -0
  95. package/dist-boot/c-FH4NXKHV.mjs +177 -0
  96. package/dist-boot/c-FHNUCTEM.mjs +140 -0
  97. package/dist-boot/c-FJ5T42DS.mjs +5349 -0
  98. package/dist-boot/c-FPCNRL7G.mjs +1728 -0
  99. package/dist-boot/c-G4DLN7Q4.mjs +8 -0
  100. package/dist-boot/c-G56GYQFZ.mjs +2507 -0
  101. package/dist-boot/c-GLIOERDQ.mjs +1200 -0
  102. package/dist-boot/c-GMJ5HUBM.mjs +14 -0
  103. package/dist-boot/c-H3HSGMAQ.mjs +461 -0
  104. package/dist-boot/c-HIKTO6P7.mjs +275 -0
  105. package/dist-boot/c-I2MCJ34O.mjs +464 -0
  106. package/dist-boot/c-I3C6LBW4.mjs +765 -0
  107. package/dist-boot/c-I54OKQHW.mjs +387 -0
  108. package/dist-boot/c-IFSP3R4D.mjs +80 -0
  109. package/dist-boot/c-IZK77VB7.mjs +171 -0
  110. package/dist-boot/c-J3Y25UGE.mjs +54 -0
  111. package/dist-boot/c-J5IESS7W.mjs +27750 -0
  112. package/dist-boot/c-JJWZBY7L.mjs +254 -0
  113. package/dist-boot/c-JKTFXTGF.mjs +463 -0
  114. package/dist-boot/c-JN57AXQ4.mjs +77 -0
  115. package/dist-boot/c-KFIO6LUW.mjs +1096 -0
  116. package/dist-boot/c-KIEMEFWW.mjs +478 -0
  117. package/dist-boot/c-KIVXWBYD.mjs +28 -0
  118. package/dist-boot/c-KP6CRIP4.mjs +580 -0
  119. package/dist-boot/c-L26VOJX4.mjs +11558 -0
  120. package/dist-boot/c-L5GHWG5G.mjs +5988 -0
  121. package/dist-boot/c-M7NSQAKK.mjs +353 -0
  122. package/dist-boot/c-MCEWOPBW.mjs +56 -0
  123. package/dist-boot/c-MGPIEG7I.mjs +20 -0
  124. package/dist-boot/c-NREXJV3M.mjs +393 -0
  125. package/dist-boot/c-O5PP6CTX.mjs +162 -0
  126. package/dist-boot/c-OBQDHSXM.mjs +1774 -0
  127. package/dist-boot/c-OLQHHDD4.mjs +21 -0
  128. package/dist-boot/c-OXW4JQQH.mjs +319 -0
  129. package/dist-boot/c-P3D7ZUWN.mjs +1288 -0
  130. package/dist-boot/c-P4PQZVV5.mjs +948 -0
  131. package/dist-boot/c-P63T3XL2.mjs +26373 -0
  132. package/dist-boot/c-P6N5DQGZ.mjs +194 -0
  133. package/dist-boot/c-P7MLKQFC.mjs +58 -0
  134. package/dist-boot/c-PB3PZJL5.mjs +1276 -0
  135. package/dist-boot/c-PTN4S6G2.mjs +1259 -0
  136. package/dist-boot/c-PYCE5J7U.mjs +1083 -0
  137. package/dist-boot/c-Q4HYCRLB.mjs +19410 -0
  138. package/dist-boot/c-QUXMETGG.mjs +68 -0
  139. package/dist-boot/c-RDMJZCOX.mjs +356 -0
  140. package/dist-boot/c-ROGJK76M.mjs +17 -0
  141. package/dist-boot/c-RQJJ26T4.mjs +708 -0
  142. package/dist-boot/c-RSKCUPJF.mjs +7559 -0
  143. package/dist-boot/c-RVJU4O6P.mjs +37 -0
  144. package/dist-boot/c-S2RVHR3M.mjs +165 -0
  145. package/dist-boot/c-SKMK3PEE.mjs +91120 -0
  146. package/dist-boot/c-TI7SM4EG.mjs +20 -0
  147. package/dist-boot/c-TILJ6GXD.mjs +1235 -0
  148. package/dist-boot/c-TT6BCUJD.mjs +114 -0
  149. package/dist-boot/c-U4ITOXAN.mjs +2107 -0
  150. package/dist-boot/c-UGERXVO2.mjs +7120 -0
  151. package/dist-boot/c-UPBH5AXY.mjs +1363 -0
  152. package/dist-boot/c-US2PUNZL.mjs +597 -0
  153. package/dist-boot/c-UU4WZPPE.mjs +20 -0
  154. package/dist-boot/c-UWVIYPQS.mjs +39 -0
  155. package/dist-boot/c-V4RHQBPQ.mjs +17683 -0
  156. package/dist-boot/c-V5MRVK47.mjs +351 -0
  157. package/dist-boot/c-V6CL266R.mjs +1755 -0
  158. package/dist-boot/c-VJTMFKYE.mjs +14975 -0
  159. package/dist-boot/c-VJTRUKIB.mjs +773 -0
  160. package/dist-boot/c-W4IFSHPW.mjs +43 -0
  161. package/dist-boot/c-WETGB2CF.mjs +113 -0
  162. package/dist-boot/c-WO3QSZUU.mjs +1337 -0
  163. package/dist-boot/c-WQLH73TY.mjs +242 -0
  164. package/dist-boot/c-XL735JW5.mjs +500 -0
  165. package/dist-boot/c-Y7HADL5B.mjs +1107 -0
  166. package/dist-boot/c-YGQHSVSK.mjs +41 -0
  167. package/dist-boot/c-YMP2AMEB.mjs +10350 -0
  168. package/dist-boot/c-YRDO2WEF.mjs +12012 -0
  169. package/dist-boot/c-YUSTRATJ.mjs +8 -0
  170. package/dist-boot/c-Z2262R22.mjs +2697 -0
  171. package/dist-boot/c-Z4ALQMJC.mjs +709 -0
  172. package/dist-boot/c-Z5ZUQVUY.mjs +62 -0
  173. package/dist-boot/c-ZDZUKXF7.mjs +874 -0
  174. package/dist-boot/c-ZJYLGIC3.mjs +32 -0
  175. package/dist-boot/c-ZNA5CDMI.mjs +77 -0
  176. package/dist-boot/c-ZSO6DI3M.mjs +204 -0
  177. package/dist-boot/c-ZWUVHKMK.mjs +100 -0
  178. package/dist-boot/c-ZY4MESEW.mjs +832 -0
  179. package/dist-boot/c-ZZ5REI4L.mjs +329 -0
  180. package/dist-boot/cli.mjs +51 -0
  181. package/dist-boot/ext-anthropic-attribution.mjs +207 -0
  182. package/dist-boot/ext-pi-background-tasks.mjs +7234 -0
  183. package/dist-boot/ext-pi-subagents.mjs +37 -0
  184. package/dist-boot/ext-privateer-account.mjs +4426 -0
  185. package/dist-boot/ext-privateer-brand.mjs +5274 -0
  186. package/dist-boot/ext-privateer-codemode.mjs +37 -0
  187. package/dist-boot/ext-privateer-computer.mjs +626 -0
  188. package/dist-boot/ext-privateer-connect.mjs +1862 -0
  189. package/dist-boot/ext-privateer-context.mjs +323 -0
  190. package/dist-boot/ext-privateer-desktop.mjs +92 -0
  191. package/dist-boot/ext-privateer-final-audit.mjs +48 -0
  192. package/dist-boot/ext-privateer-fresh.mjs +81 -0
  193. package/dist-boot/ext-privateer-gate.mjs +5959 -0
  194. package/dist-boot/ext-privateer-hints.mjs +236 -0
  195. package/dist-boot/ext-privateer-media.mjs +2626 -0
  196. package/dist-boot/ext-privateer-models.mjs +3397 -0
  197. package/dist-boot/ext-privateer-posture.mjs +3006 -0
  198. package/dist-boot/ext-privateer-privacy.mjs +4578 -0
  199. package/dist-boot/ext-privateer-recovery.mjs +287 -0
  200. package/dist-boot/ext-privateer-speak.mjs +4754 -0
  201. package/dist-boot/ext-privateer-terminals.mjs +725 -0
  202. package/dist-boot/ext-privateer-tools.mjs +528 -0
  203. package/dist-boot/ext-privateer-truncation.mjs +74 -0
  204. package/dist-boot/ext-privateer-update.mjs +210 -0
  205. package/dist-boot/ext-privateer-vision.mjs +148 -0
  206. package/dist-boot/ext-privateer-web.mjs +543 -0
  207. package/dist-boot/ext-rpiv-ask-user-question.mjs +710 -0
  208. package/dist-boot/manifest.json +9 -0
  209. package/extensions/privateer-brand.ts +3 -1
  210. package/extensions/privateer-context.ts +4 -2
  211. package/extensions/privateer-final-audit.ts +17 -0
  212. package/extensions/privateer-gate.ts +55 -9
  213. package/extensions/privateer-recovery.ts +175 -0
  214. package/extensions/privateer-web.ts +4 -4
  215. package/package.json +3 -1
  216. package/patches/@earendil-works+pi-coding-agent+1.3.0.patch +199 -2
  217. package/patches/pi-background-tasks+2.6.10.patch +91 -7
  218. package/src/cli/chat.ts +7 -1
  219. package/src/config/moatManifest.json +2 -0
  220. package/src/config/whatsNew.json +4 -4
  221. package/src/context.ts +33 -5
  222. package/src/engine/finalAudit.ts +38 -0
  223. package/src/engine/stallWatchdog.ts +8 -1
  224. package/src/providers/account.ts +34 -3
  225. package/src/remote/attentionPush.ts +40 -0
  226. package/src/remote/liveTaskSession.ts +7 -1
  227. package/src/remote/relayClient.ts +38 -4
  228. package/src/remote/remoteBridge.ts +17 -0
  229. package/src/remote/transcriptSnapshot.ts +104 -0
@@ -0,0 +1,242 @@
1
+ // Session sealing — Pi's session transcripts, encrypted at rest.
2
+ //
3
+ // Pi writes every conversation to ~/.privateer/agent/sessions/<cwd>/<ts>_<id>.jsonl,
4
+ // one JSON entry per line, and until now in plaintext: every prompt, every reply,
5
+ // every file the agent read and every command's output, readable by anything that
6
+ // can read the home directory — backup and sync tools, indexers, a stray `grep -r`,
7
+ // any other program the user runs.
8
+ //
9
+ // Now each line except the header is sealed with AES-256-GCM:
10
+ // {"pvs":1,"iv":"<b64 12 bytes>","ct":"<b64 ciphertext‖tag>"}
11
+ // The HEADER (type "session": id, timestamp, cwd) stays readable on purpose. Pi
12
+ // finds and lists sessions by reading only the first line of each file, and the cwd
13
+ // it holds is already spelled out in the directory name, so sealing it would cost
14
+ // session discovery and hide nothing.
15
+ //
16
+ // The patch in patches/@earendil-works+pi-coding-agent+*.patch routes Pi's reads
17
+ // (parseSessionEntryLine / parseSessionEntries) and writes (every JSON.stringify of
18
+ // an entry) through the codec this module installs on globalThis. Without the codec
19
+ // — Pi run by hand, outside `privateer` — Pi writes plaintext as before, and sealed
20
+ // lines read as absent rather than as garbage. Readers accept both forms forever, so
21
+ // files written before this change, and files of mixed form, keep working.
22
+ //
23
+ // THE KEY, AND WHAT THIS DOES NOT DO. One random 32-byte key per machine, in
24
+ // $PRIVATEER_HOME/session-key.json (0600) — next to the data, like this tool's other
25
+ // secrets (terminal-key.json, credentials). So this protects transcripts from
26
+ // anything that reads the session FILES (indexers, a shared or synced sessions
27
+ // folder, an attachment pasted from the wrong directory, tools that scan for
28
+ // .jsonl), and it does NOT protect them from someone who can read the whole
29
+ // ~/.privateer as this user: that reader has the key too. Say exactly that, never
30
+ // more. An OS keychain would move the key out of the folder, at the price of
31
+ // sessions that can't open over ssh or with the keychain locked — a different
32
+ // trade, not a free upgrade.
33
+ //
34
+ // Never mint a key over one that exists: a second key orphans every session sealed
35
+ // under the first. A key file that is present but unreadable or malformed is an
36
+ // error (sealing is switched off and the user is told), never a reason to start over.
37
+
38
+ import crypto from "node:crypto";
39
+ import fs from "node:fs";
40
+ import path from "node:path";
41
+
42
+ export const CODEC_KEY = Symbol.for("privateer.sessionCodec");
43
+ const KEY_FILE = "session-key.json";
44
+
45
+ export function keyPath(home) {
46
+ return path.join(home, KEY_FILE);
47
+ }
48
+
49
+ function parseKeyFile(text) {
50
+ const parsed = JSON.parse(text);
51
+ const key = Buffer.from(String(parsed?.key ?? ""), "base64");
52
+ if (parsed?.v !== 1 || key.length !== 32) throw new Error("session key file is malformed");
53
+ return key;
54
+ }
55
+
56
+ /**
57
+ * Load this machine's session key, minting it only when no key file exists at all.
58
+ * Two terminals starting at once race on `wx`: the loser re-reads the winner's key.
59
+ */
60
+ export function loadOrCreateKey(home) {
61
+ const file = keyPath(home);
62
+ try {
63
+ return parseKeyFile(fs.readFileSync(file, "utf8"));
64
+ } catch (e) {
65
+ if (e?.code !== "ENOENT") throw e;
66
+ }
67
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
68
+ const key = crypto.randomBytes(32);
69
+ try {
70
+ fs.writeFileSync(file, JSON.stringify({ v: 1, key: key.toString("base64"), created: new Date().toISOString() }) + "\n", { mode: 0o600, flag: "wx" });
71
+ return key;
72
+ } catch (e) {
73
+ if (e?.code === "EEXIST") return parseKeyFile(fs.readFileSync(file, "utf8"));
74
+ throw e;
75
+ }
76
+ }
77
+
78
+ export function isSealed(obj) {
79
+ return !!obj && typeof obj === "object" && obj.pvs === 1 && typeof obj.iv === "string" && typeof obj.ct === "string";
80
+ }
81
+
82
+ /** One entry → one line (no newline). The session header is never sealed. */
83
+ export function encodeEntry(entry, key) {
84
+ if (entry?.type === "session") return JSON.stringify(entry);
85
+ const iv = crypto.randomBytes(12);
86
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
87
+ const ct = Buffer.concat([cipher.update(JSON.stringify(entry), "utf8"), cipher.final(), cipher.getAuthTag()]);
88
+ return JSON.stringify({ pvs: 1, iv: iv.toString("base64"), ct: ct.toString("base64") });
89
+ }
90
+
91
+ /** A parsed line → the entry. Plaintext passes through; a line that fails to open → null. */
92
+ export function decodeEntry(obj, key) {
93
+ if (!isSealed(obj)) return obj;
94
+ try {
95
+ const iv = Buffer.from(obj.iv, "base64");
96
+ const raw = Buffer.from(obj.ct, "base64");
97
+ if (iv.length !== 12 || raw.length < 17) return null;
98
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
99
+ decipher.setAuthTag(raw.subarray(raw.length - 16));
100
+ const text = Buffer.concat([decipher.update(raw.subarray(0, raw.length - 16)), decipher.final()]).toString("utf8");
101
+ return JSON.parse(text);
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Install the codec Pi's patched session manager calls. Returns { ok, error? }.
109
+ * `failures` counts lines that would not open (a different machine's key, a damaged
110
+ * file), so the agent can say so instead of showing a silently shorter session.
111
+ */
112
+ export function installCodec(home) {
113
+ let key;
114
+ try {
115
+ key = loadOrCreateKey(home);
116
+ } catch (e) {
117
+ delete globalThis[CODEC_KEY];
118
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
119
+ }
120
+ const codec = {
121
+ failures: 0,
122
+ encode: (entry) => encodeEntry(entry, key),
123
+ decode: (obj) => {
124
+ const out = decodeEntry(obj, key);
125
+ if (out === null) codec.failures++;
126
+ return out;
127
+ },
128
+ };
129
+ globalThis[CODEC_KEY] = codec;
130
+ return { ok: true };
131
+ }
132
+
133
+ /**
134
+ * Seal one existing file in place. Lines that are already sealed, the header, and
135
+ * lines that don't parse are carried over byte-for-byte (a malformed line is Pi's to
136
+ * skip, not ours to drop). Written to a sibling temp file, VERIFIED by reading it
137
+ * back, then renamed over the original only if the original hasn't changed since it
138
+ * was read — a session another terminal is appending to is left for a later pass.
139
+ * The file keeps its original atime/mtime (Pi sorts sessions by mtime).
140
+ * Returns "sealed" | "unchanged" | "busy" | "failed".
141
+ */
142
+ export function sealFile(file, key) {
143
+ let before, text;
144
+ try {
145
+ before = fs.statSync(file);
146
+ text = fs.readFileSync(file, "utf8");
147
+ } catch {
148
+ return "failed";
149
+ }
150
+ const lines = text.split("\n");
151
+ let changed = false;
152
+ const out = lines.map((line) => {
153
+ if (!line.trim()) return line;
154
+ let obj;
155
+ try {
156
+ obj = JSON.parse(line);
157
+ } catch {
158
+ return line;
159
+ }
160
+ if (isSealed(obj) || obj?.type === "session") return line;
161
+ changed = true;
162
+ return encodeEntry(obj, key);
163
+ });
164
+ if (!changed) return "unchanged";
165
+
166
+ // Verify: every original entry must come back identical from what we will write.
167
+ for (let i = 0; i < lines.length; i++) {
168
+ if (out[i] === lines[i]) continue;
169
+ const back = decodeEntry(JSON.parse(out[i]), key);
170
+ if (JSON.stringify(back) !== JSON.stringify(JSON.parse(lines[i]))) return "failed";
171
+ }
172
+
173
+ const tmp = `${file}.sealing-${process.pid}`;
174
+ try {
175
+ fs.writeFileSync(tmp, out.join("\n"), { mode: 0o600 });
176
+ const now = fs.statSync(file);
177
+ if (now.size !== before.size || now.mtimeMs !== before.mtimeMs) {
178
+ fs.rmSync(tmp, { force: true });
179
+ return "busy";
180
+ }
181
+ // Keep the original times: Pi orders sessions by mtime (`privateer -c` continues
182
+ // the most recent one), so a conversion must not make an old session look new.
183
+ fs.utimesSync(tmp, before.atime, before.mtime);
184
+ fs.renameSync(tmp, file);
185
+ return "sealed";
186
+ } catch {
187
+ try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort */ }
188
+ return "failed";
189
+ }
190
+ }
191
+
192
+ /** Every session file under Pi's sessions dir. */
193
+ export function listSessionFiles(sessionsDir) {
194
+ const out = [];
195
+ let dirs = [];
196
+ try {
197
+ dirs = fs.readdirSync(sessionsDir, { withFileTypes: true });
198
+ } catch {
199
+ return out;
200
+ }
201
+ for (const d of dirs) {
202
+ if (!d.isDirectory()) continue;
203
+ const dir = path.join(sessionsDir, d.name);
204
+ try {
205
+ for (const f of fs.readdirSync(dir)) if (f.endsWith(".jsonl")) out.push(path.join(dir, f));
206
+ } catch { /* unreadable folder: skip */ }
207
+ }
208
+ return out;
209
+ }
210
+
211
+ /**
212
+ * Seal every plaintext session on this machine — once, then never again. Files
213
+ * touched in the last QUIET_MS are skipped (another terminal may be writing them);
214
+ * the done-marker is only written when nothing was skipped or failed, so a later
215
+ * launch finishes the job. Async-yielding so the launcher's event loop stays live.
216
+ */
217
+ export const QUIET_MS = 10 * 60 * 1000;
218
+ export async function sweepSessions(home, sessionsDir, { now = Date.now(), quietMs = QUIET_MS } = {}) {
219
+ const done = path.join(home, "run", "sessions-sealed-v1");
220
+ if (fs.existsSync(done)) return { skipped: true };
221
+ let key;
222
+ try {
223
+ key = loadOrCreateKey(home);
224
+ } catch {
225
+ return { error: true };
226
+ }
227
+ const tally = { sealed: 0, unchanged: 0, busy: 0, failed: 0, recent: 0 };
228
+ for (const file of listSessionFiles(sessionsDir)) {
229
+ try {
230
+ if (now - fs.statSync(file).mtimeMs < quietMs) { tally.recent++; continue; }
231
+ } catch { continue; }
232
+ tally[sealFile(file, key)]++;
233
+ await new Promise((r) => setImmediate(r));
234
+ }
235
+ if (!tally.busy && !tally.failed && !tally.recent) {
236
+ try {
237
+ fs.mkdirSync(path.dirname(done), { recursive: true, mode: 0o700 });
238
+ fs.writeFileSync(done, new Date().toISOString() + "\n", { mode: 0o600 });
239
+ } catch { /* retried next launch */ }
240
+ }
241
+ return tally;
242
+ }
@@ -0,0 +1,3 @@
1
+ import { fileURLToPath } from "node:url";
2
+ process.argv[1] = fileURLToPath(new URL("../node_modules/@earendil-works/pi-coding-agent/dist/cli.js", import.meta.url));
3
+ await import("./cli.mjs");