@mnemom/mnemom 0.17.33 → 0.17.34-next.2

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 (236) hide show
  1. package/dist/base-url-proxy.mjs +18 -4
  2. package/dist/commands/agent-config.js +5 -1
  3. package/dist/commands/agent-doctor.d.ts +9 -0
  4. package/dist/commands/agent-doctor.js +4 -0
  5. package/dist/commands/agent-session.d.ts +1 -2
  6. package/dist/commands/agent-session.js +1 -2
  7. package/dist/commands/agent-setup.d.ts +6 -0
  8. package/dist/commands/agent-setup.js +12 -1
  9. package/dist/commands/agent-share-render.d.ts +1 -1
  10. package/dist/commands/agent-share-render.js +1 -1
  11. package/dist/commands/agent.d.ts +26 -0
  12. package/dist/commands/agent.js +228 -53
  13. package/dist/commands/durable-doctor.d.ts +50 -0
  14. package/dist/commands/durable-doctor.js +186 -0
  15. package/dist/commands/durable-lock.d.ts +254 -0
  16. package/dist/commands/durable-lock.js +595 -0
  17. package/dist/commands/durable-sessions.d.ts +87 -0
  18. package/dist/commands/durable-sessions.js +303 -0
  19. package/dist/commands/durable-verbs.d.ts +116 -0
  20. package/dist/commands/durable-verbs.js +393 -0
  21. package/dist/commands/protection.js +1 -1
  22. package/dist/commands/scan.d.ts +97 -0
  23. package/dist/commands/scan.js +238 -0
  24. package/dist/durable/flock.d.ts +26 -0
  25. package/dist/durable/flock.js +80 -0
  26. package/dist/durable/launch-spec.d.ts +7 -1
  27. package/dist/durable/launch-spec.js +10 -3
  28. package/dist/durable/seat-proxy-entry.d.ts +11 -0
  29. package/dist/durable/seat-proxy-entry.js +25 -0
  30. package/dist/durable/seat-proxy.d.ts +107 -0
  31. package/dist/durable/seat-proxy.js +599 -0
  32. package/dist/durable/seat-socket.d.ts +46 -0
  33. package/dist/durable/seat-socket.js +179 -0
  34. package/dist/durable/seat-spec-validator.d.ts +28 -0
  35. package/dist/durable/seat-spec-validator.js +59 -0
  36. package/dist/durable/test-mode.mjs +81 -0
  37. package/dist/durable/turn-chain.d.ts +55 -0
  38. package/dist/durable/turn-chain.js +178 -0
  39. package/dist/host/auto-continue.d.ts +67 -0
  40. package/dist/host/auto-continue.js +118 -0
  41. package/dist/host/bootid.d.ts +13 -0
  42. package/dist/host/bootid.js +41 -0
  43. package/dist/host/client.d.ts +123 -0
  44. package/dist/host/client.js +171 -0
  45. package/dist/host/commands.d.ts +2 -0
  46. package/dist/host/commands.js +122 -0
  47. package/dist/host/control.d.ts +33 -0
  48. package/dist/host/control.js +144 -0
  49. package/dist/host/daemon.d.ts +217 -0
  50. package/dist/host/daemon.js +606 -0
  51. package/dist/host/gate.d.ts +46 -0
  52. package/dist/host/gate.js +82 -0
  53. package/dist/host/grant.d.ts +24 -0
  54. package/dist/host/grant.js +24 -0
  55. package/dist/host/instance.d.ts +35 -0
  56. package/dist/host/instance.js +62 -0
  57. package/dist/host/integrity.d.ts +35 -0
  58. package/dist/host/integrity.js +115 -0
  59. package/dist/host/launchd.d.ts +16 -0
  60. package/dist/host/launchd.js +45 -0
  61. package/dist/host/lock.d.ts +20 -0
  62. package/dist/host/lock.js +63 -0
  63. package/dist/host/owner.d.ts +55 -0
  64. package/dist/host/owner.js +121 -0
  65. package/dist/host/plist.d.ts +16 -0
  66. package/dist/host/plist.js +85 -0
  67. package/dist/host/procs.d.ts +27 -0
  68. package/dist/host/procs.js +105 -0
  69. package/dist/host/run.d.ts +66 -0
  70. package/dist/host/run.js +314 -0
  71. package/dist/host/runtime.d.ts +8 -0
  72. package/dist/host/runtime.js +22 -0
  73. package/dist/host/s1-names.d.ts +22 -0
  74. package/dist/host/s1-names.js +21 -0
  75. package/dist/host/seat-credential.d.ts +27 -0
  76. package/dist/host/seat-credential.js +41 -0
  77. package/dist/host/seat-launch-main.d.ts +1 -0
  78. package/dist/host/seat-launch-main.js +59 -0
  79. package/dist/host/seat-launch.d.ts +116 -0
  80. package/dist/host/seat-launch.js +302 -0
  81. package/dist/host/seat.d.ts +48 -0
  82. package/dist/host/seat.js +149 -0
  83. package/dist/host/state.d.ts +33 -0
  84. package/dist/host/state.js +117 -0
  85. package/dist/host/status.d.ts +101 -0
  86. package/dist/host/status.js +307 -0
  87. package/dist/host/tmux.d.ts +56 -0
  88. package/dist/host/tmux.js +182 -0
  89. package/dist/host/transcript.d.ts +30 -0
  90. package/dist/host/transcript.js +130 -0
  91. package/dist/host/verbs.d.ts +50 -0
  92. package/dist/host/verbs.js +271 -0
  93. package/dist/index.d.ts +8 -6
  94. package/dist/index.js +232 -35
  95. package/dist/lib/agent.d.ts +8 -26
  96. package/dist/lib/agent.js +11 -26
  97. package/dist/lib/api.d.ts +13 -0
  98. package/dist/lib/api.js +8 -0
  99. package/dist/lib/claude-plugin.d.ts +11 -2
  100. package/dist/lib/claude-plugin.js +10 -1
  101. package/dist/lib/durable/api.d.ts +136 -0
  102. package/dist/lib/durable/api.js +220 -0
  103. package/dist/lib/durable/codes.d.ts +8 -0
  104. package/dist/lib/durable/codes.js +2 -0
  105. package/dist/lib/durable/first-turn.d.ts +50 -0
  106. package/dist/lib/durable/first-turn.js +102 -0
  107. package/dist/lib/durable/gate.d.ts +48 -0
  108. package/dist/lib/durable/gate.js +122 -0
  109. package/dist/lib/durable/handover.d.ts +101 -0
  110. package/dist/lib/durable/handover.js +175 -0
  111. package/dist/lib/durable/launch.d.ts +182 -0
  112. package/dist/lib/durable/launch.js +443 -0
  113. package/dist/lib/durable/launching.d.ts +15 -0
  114. package/dist/lib/durable/launching.js +70 -0
  115. package/dist/lib/durable/live.d.ts +194 -0
  116. package/dist/lib/durable/live.js +635 -0
  117. package/dist/lib/durable/refusal.d.ts +57 -0
  118. package/dist/lib/durable/refusal.js +78 -0
  119. package/dist/lib/durable/view.d.ts +83 -0
  120. package/dist/lib/durable/view.js +244 -0
  121. package/dist/lib/experimental.d.ts +0 -9
  122. package/dist/lib/experimental.js +18 -17
  123. package/dist/lib/launcher-config.d.ts +7 -0
  124. package/dist/lib/launcher-config.js +21 -0
  125. package/dist/lib/scan/caps.d.ts +35 -0
  126. package/dist/lib/scan/caps.js +67 -0
  127. package/dist/lib/scan/credential.d.ts +37 -0
  128. package/dist/lib/scan/credential.js +104 -0
  129. package/dist/lib/scan/failure.d.ts +10 -0
  130. package/dist/lib/scan/failure.js +43 -0
  131. package/dist/lib/scan/pr-source.d.ts +57 -0
  132. package/dist/lib/scan/pr-source.js +130 -0
  133. package/dist/lib/scan/record.d.ts +49 -0
  134. package/dist/lib/scan/record.js +85 -0
  135. package/dist/lib/scan/report-email.d.ts +92 -0
  136. package/dist/lib/scan/report-email.js +144 -0
  137. package/dist/lib/scan/report.d.ts +13 -0
  138. package/dist/lib/scan/report.js +30 -0
  139. package/dist/lib/scan/scan.d.ts +64 -0
  140. package/dist/lib/scan/scan.js +182 -0
  141. package/dist/lib/scan/share.d.ts +38 -0
  142. package/dist/lib/scan/share.js +47 -0
  143. package/dist/lib/scan/spend/ai-tool-detect.d.ts +6 -0
  144. package/dist/lib/scan/spend/ai-tool-detect.js +18 -0
  145. package/dist/lib/scan/spend/anthropic-usage.d.ts +57 -0
  146. package/dist/lib/scan/spend/anthropic-usage.js +244 -0
  147. package/dist/lib/scan/spend/cost-corpus.v1.d.ts +56 -0
  148. package/dist/lib/scan/spend/cost-corpus.v1.js +64 -0
  149. package/dist/lib/scan/spend/prices.d.ts +33 -0
  150. package/dist/lib/scan/spend/prices.js +57 -0
  151. package/dist/lib/scan/spend/spend-estimate.d.ts +30 -0
  152. package/dist/lib/scan/spend/spend-estimate.js +70 -0
  153. package/dist/lib/scan/spend/spend-tiers.d.ts +25 -0
  154. package/dist/lib/scan/spend/spend-tiers.js +42 -0
  155. package/dist/lib/scan/spend/token-rate.d.ts +30 -0
  156. package/dist/lib/scan/spend/token-rate.js +54 -0
  157. package/dist/lib/session-key.d.ts +41 -0
  158. package/dist/lib/session-key.js +124 -0
  159. package/dist/lib/subscription-login.d.ts +15 -0
  160. package/dist/lib/subscription-login.js +28 -1
  161. package/dist/rc-key-401.mjs +75 -0
  162. package/dist/rc-proxy.mjs +221 -25
  163. package/dist/vendor/durable-contract/index.js +240 -18
  164. package/dist/vendor/shared/durable-contract/admission.d.ts +240 -0
  165. package/dist/vendor/shared/durable-contract/admission.js +533 -0
  166. package/dist/vendor/shared/durable-contract/arm.d.ts +51 -0
  167. package/dist/vendor/shared/durable-contract/arm.js +133 -0
  168. package/dist/vendor/shared/durable-contract/canonical.d.ts +29 -0
  169. package/dist/vendor/shared/durable-contract/canonical.js +98 -0
  170. package/dist/vendor/shared/durable-contract/cell-id.d.ts +22 -0
  171. package/dist/vendor/shared/durable-contract/cell-id.js +38 -0
  172. package/dist/vendor/shared/durable-contract/cli-output.d.ts +342 -0
  173. package/dist/vendor/shared/durable-contract/cli-output.js +201 -0
  174. package/dist/vendor/shared/durable-contract/cli-surface.d.ts +119 -0
  175. package/dist/vendor/shared/durable-contract/cli-surface.js +270 -0
  176. package/dist/vendor/shared/durable-contract/codes.d.ts +537 -0
  177. package/dist/vendor/shared/durable-contract/codes.js +475 -0
  178. package/dist/vendor/shared/durable-contract/constants.d.ts +153 -0
  179. package/dist/vendor/shared/durable-contract/constants.js +179 -0
  180. package/dist/vendor/shared/durable-contract/doors.d.ts +119 -0
  181. package/dist/vendor/shared/durable-contract/doors.js +249 -0
  182. package/dist/vendor/shared/durable-contract/envelope.d.ts +103 -0
  183. package/dist/vendor/shared/durable-contract/envelope.js +317 -0
  184. package/dist/vendor/shared/durable-contract/events.d.ts +147 -0
  185. package/dist/vendor/shared/durable-contract/events.js +630 -0
  186. package/dist/vendor/shared/durable-contract/executor-state.d.ts +41 -0
  187. package/dist/vendor/shared/durable-contract/executor-state.js +32 -0
  188. package/dist/vendor/shared/durable-contract/fixlines.d.ts +318 -0
  189. package/dist/vendor/shared/durable-contract/fixlines.js +483 -0
  190. package/dist/vendor/shared/durable-contract/freeze-record.d.ts +787 -0
  191. package/dist/vendor/shared/durable-contract/freeze-record.js +361 -0
  192. package/dist/vendor/shared/durable-contract/index.d.ts +36 -0
  193. package/dist/vendor/shared/durable-contract/index.js +37 -0
  194. package/dist/vendor/shared/durable-contract/journal-approvals.d.ts +29 -0
  195. package/dist/vendor/shared/durable-contract/journal-approvals.js +60 -0
  196. package/dist/vendor/shared/durable-contract/keys.d.ts +24 -0
  197. package/dist/vendor/shared/durable-contract/keys.js +66 -0
  198. package/dist/vendor/shared/durable-contract/lock.d.ts +128 -0
  199. package/dist/vendor/shared/durable-contract/lock.js +151 -0
  200. package/dist/vendor/shared/durable-contract/names.d.ts +25 -0
  201. package/dist/vendor/shared/durable-contract/names.js +78 -0
  202. package/dist/vendor/shared/durable-contract/owner-verbs.d.ts +175 -0
  203. package/dist/vendor/shared/durable-contract/owner-verbs.js +403 -0
  204. package/dist/vendor/shared/durable-contract/paths.d.ts +23 -0
  205. package/dist/vendor/shared/durable-contract/paths.js +35 -0
  206. package/dist/vendor/shared/durable-contract/pure-fn.d.ts +48 -0
  207. package/dist/vendor/shared/durable-contract/pure-fn.js +89 -0
  208. package/dist/vendor/shared/durable-contract/routes.d.ts +205 -0
  209. package/dist/vendor/shared/durable-contract/routes.js +315 -0
  210. package/dist/vendor/shared/durable-contract/schema.d.ts +126 -0
  211. package/dist/vendor/shared/durable-contract/schema.js +441 -0
  212. package/dist/vendor/shared/durable-contract/seal.d.ts +54 -0
  213. package/dist/vendor/shared/durable-contract/seal.js +99 -0
  214. package/dist/vendor/shared/durable-contract/secret-scope.d.ts +28 -0
  215. package/dist/vendor/shared/durable-contract/secret-scope.js +76 -0
  216. package/dist/vendor/shared/durable-contract/session-id.d.ts +21 -0
  217. package/dist/vendor/shared/durable-contract/session-id.js +58 -0
  218. package/dist/vendor/shared/durable-contract/spec.d.ts +162 -0
  219. package/dist/vendor/shared/durable-contract/spec.js +286 -0
  220. package/dist/vendor/shared/durable-contract/states.d.ts +96 -0
  221. package/dist/vendor/shared/durable-contract/states.js +218 -0
  222. package/dist/vendor/shared/durable-contract/telemetry.d.ts +94 -0
  223. package/dist/vendor/shared/durable-contract/telemetry.js +180 -0
  224. package/dist/vendor/shared/durable-contract/test-mode.d.ts +62 -0
  225. package/dist/vendor/shared/durable-contract/test-mode.js +140 -0
  226. package/dist/vendor/shared/durable-contract/turn.d.ts +57 -0
  227. package/dist/vendor/shared/durable-contract/turn.js +81 -0
  228. package/dist/vendor/shared/durable-contract/vectors.d.ts +191 -0
  229. package/dist/vendor/shared/durable-contract/vectors.js +208 -0
  230. package/package.json +4 -3
  231. package/plugins/integrity-manifest.json +35 -0
  232. package/plugins/mnemom/hooks/hooks.json +15 -1
  233. package/plugins/mnemom/hooks/register.tsx +60 -2
  234. package/plugins/mnemom/hooks/seat-hello.mjs +116 -0
  235. package/dist/commands/agent-implicit-notice.d.ts +0 -31
  236. package/dist/commands/agent-implicit-notice.js +0 -73
@@ -0,0 +1,101 @@
1
+ import { type ClaimUnavailableCause } from "../vendor/shared/durable-contract/codes.js";
2
+ import type { HostStatusJson } from "../vendor/shared/durable-contract/cli-output.js";
3
+ import { type LauncherKey } from "../vendor/shared/durable-contract/fixlines.js";
4
+ import { type Launchctl } from "./launchd.js";
5
+ import { type SeatPane } from "./tmux.js";
6
+ import { type PsRow } from "./procs.js";
7
+ import { type ExecutorRow } from "./owner.js";
8
+ import { type GrantView } from "./grant.js";
9
+ export declare const fingerprint: (secret: string) => string;
10
+ export interface StatusDeps {
11
+ readonly env: Readonly<Record<string, string | undefined>>;
12
+ readonly home: string;
13
+ readonly uid: number;
14
+ readonly launchctl: Launchctl;
15
+ readonly panes: () => Promise<SeatPane[]>;
16
+ readonly ps: () => PsRow[];
17
+ readonly lsofPids: (file: string) => Promise<number[]>;
18
+ readonly lsofListenPort: (pid: number) => Promise<number | null>;
19
+ /** Absolute unix-socket names the process has open (its own listening seat socket among them). */
20
+ readonly lsofUnixPaths: (pid: number) => Promise<string[]>;
21
+ readonly alive: (pid: number) => boolean;
22
+ /** The executor row from exec_list (null when unknown or offline). */
23
+ readonly executorRow: (id: string) => Promise<ExecutorRow | null>;
24
+ readonly homeTag: string;
25
+ readonly cliVersion: string;
26
+ /** Wall clock for the grant window (defaults to Date.now). */
27
+ readonly now?: () => number;
28
+ }
29
+ export interface SeatRow {
30
+ session_id: string;
31
+ name: string | null;
32
+ epoch: number | null;
33
+ spawn_id: string;
34
+ pid: number | null;
35
+ state: string | null;
36
+ proxy_pid: number | null;
37
+ claude_pid: number | null;
38
+ proxy_port: number | null;
39
+ socket_path: string | null;
40
+ spec_path: string | null;
41
+ spec_sha256: string | null;
42
+ tmux_session: string;
43
+ }
44
+ export interface HostStatus {
45
+ instance: string;
46
+ installed: boolean;
47
+ loaded: boolean;
48
+ daemon: "running" | "idle" | "stopped";
49
+ daemon_pid: number | null;
50
+ lock_holder_pid: number | null;
51
+ lock_path: string;
52
+ enrolled: boolean;
53
+ executor_id: string | null;
54
+ executor_key_sha256: string | null;
55
+ nonce_sha256: string | null;
56
+ boot_id: string | null;
57
+ transcript_guard: "link" | "copy";
58
+ integrity: {
59
+ ok: boolean;
60
+ cause?: string;
61
+ };
62
+ risk_line_version: {
63
+ enrolled: number | null;
64
+ current: number;
65
+ };
66
+ /** The executor grant (C04-R18); null when the executor row is unknown (offline, not enrolled). */
67
+ grant: GrantView | null;
68
+ seats: SeatRow[];
69
+ sessions: {
70
+ id: string;
71
+ name: string | null;
72
+ state: string | null;
73
+ state_reason: string | null;
74
+ claim_unavailable: {
75
+ cause: ClaimUnavailableCause | null;
76
+ count: number;
77
+ } | null;
78
+ }[];
79
+ fixes: string[];
80
+ notes: string[];
81
+ }
82
+ /** The launcher's durable key (launcher.toml `durable = …`); default `ask`. */
83
+ export declare function launcherDurableKey(home: string): LauncherKey;
84
+ export declare function collectStatus(d: StatusDeps): Promise<HostStatus>;
85
+ /**
86
+ * `host status --json`: exactly the S1 HOST_STATUS_JSON document (ruling 26), closed at every level.
87
+ * A seat row the schema cannot hold yet (no session state known, no launch spec on disk, no pane
88
+ * pid) is left out rather than printed with a guessed value; the human view still shows it.
89
+ */
90
+ export declare function toHostStatusJson(s: HostStatus): HostStatusJson;
91
+ export declare function renderStatusHuman(s: HostStatus): string[];
92
+ /**
93
+ * Print a status. --json: the S1 document alone on stdout (one parseable object), and the notes
94
+ * and fix lines on stderr so a script still sees them. Human: everything on stdout.
95
+ */
96
+ export declare function printStatus(s: HostStatus, json: boolean, out: (l: string) => void, err: (l: string) => void): void;
97
+ export declare function lsofPids(file: string): Promise<number[]>;
98
+ /** `lsof -F n` output to the absolute names it lists (unnamed peers such as `->0x…` are skipped). */
99
+ export declare function parseLsofUnixPaths(out: string): string[];
100
+ export declare function lsofUnixPaths(pid: number): Promise<string[]>;
101
+ export declare function lsofListenPort(pid: number): Promise<number | null>;
@@ -0,0 +1,307 @@
1
+ /**
2
+ * `mnemom agent host status [--json]` (C12-R23, R24, R31, R36, R39; C04). Every per-seat field
3
+ * is read at call time from its own probe: tmux (seat rows and tags), ps (claude and proxy
4
+ * pids), lsof (proxy port, lock holder), stat (socket), sha256 (launch spec). The daemon's
5
+ * runtime file adds only what the OS cannot tell (unavailable cause and count, integrity, idle).
6
+ * Only fingerprints of the nonce and key are printed.
7
+ */
8
+ import { SESSION_STATES } from "../vendor/shared/durable-contract/states.js";
9
+ import { CLAIM_UNAVAILABLE_CAUSES, } from "../vendor/shared/durable-contract/codes.js";
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import { createHash } from "node:crypto";
13
+ import { execFile } from "node:child_process";
14
+ import { renderFixLine, RISK_LINE_TEXT, FIX_LINES, } from "../vendor/shared/durable-contract/fixlines.js";
15
+ import { CLAIM_UNAVAILABLE_SURFACE_N, RISK_LINE_VERSION, } from "../vendor/shared/durable-contract/constants.js";
16
+ import { resolveInstance, hostPaths } from "./instance.js";
17
+ import { durableOnInConfig, IDLE_TEXT, IDLE_FIX } from "./gate.js";
18
+ import { readExecutorState } from "./state.js";
19
+ import { readRuntime } from "./runtime.js";
20
+ import { printJob } from "./launchd.js";
21
+ import { seatPids } from "./procs.js";
22
+ import { grantStatus, GRANT_EXPIRED_LINE } from "./grant.js";
23
+ export const fingerprint = (secret) => createHash("sha256").update(secret).digest("hex").slice(0, 12);
24
+ const sha256File = (p) => {
25
+ try {
26
+ return createHash("sha256").update(fs.readFileSync(p)).digest("hex");
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ };
32
+ /** The launcher's durable key (launcher.toml `durable = …`); default `ask`. */
33
+ export function launcherDurableKey(home) {
34
+ try {
35
+ const t = fs.readFileSync(path.join(home, ".mnemom", "launcher.toml"), "utf8");
36
+ const m = /^\s*durable\s*=\s*"?(off|ask|always)"?\s*$/m.exec(t);
37
+ return m?.[1] ?? "ask";
38
+ }
39
+ catch {
40
+ return "ask";
41
+ }
42
+ }
43
+ export async function collectStatus(d) {
44
+ const instance = resolveInstance(d.env);
45
+ const paths = hostPaths(d.home, instance);
46
+ const installed = fs.existsSync(paths.plistFile);
47
+ const job = await printJob(d.launchctl, d.uid, instance.label);
48
+ const rt = readRuntime(paths.runtimeFile);
49
+ const holders = fs.existsSync(paths.lockFile) ? await d.lsofPids(paths.lockFile) : [];
50
+ const lockHolder = holders.length ? holders[0] : null;
51
+ const daemonPid = job.pid ?? (rt && d.alive(rt.pid) && lockHolder === rt.pid ? rt.pid : null);
52
+ const gateOn = durableOnInConfig(paths.configFile);
53
+ const st = readExecutorState(paths.stateFile);
54
+ const state = st.kind === "ok" ? st.state : null;
55
+ const fixes = [];
56
+ const notes = [];
57
+ let daemon = daemonPid ? "running" : "stopped";
58
+ if (daemonPid && !gateOn)
59
+ daemon = "idle";
60
+ if (daemon === "idle")
61
+ fixes.push(IDLE_FIX);
62
+ if (!installed)
63
+ fixes.push(FIX_LINES.daemon_not_running.body);
64
+ if (rt && rt.integrity && rt.integrity.ok === false && daemonPid)
65
+ fixes.push(FIX_LINES.integrity.body.replace("<cli_version>", d.cliVersion));
66
+ let enrolledRl = null;
67
+ let grant = null;
68
+ if (state) {
69
+ const row = await d.executorRow(state.executor_id).catch(() => null);
70
+ if (row) {
71
+ const g = grantStatus(row.grantExpiresAt, (d.now ?? Date.now)());
72
+ if (g.state === "warning") {
73
+ grant = { state: "warning", days: g.days };
74
+ fixes.push(g.line);
75
+ }
76
+ else if (g.state === "expired") {
77
+ grant = { state: "expired" };
78
+ fixes.push(GRANT_EXPIRED_LINE);
79
+ }
80
+ else
81
+ grant = { state: "ok" };
82
+ enrolledRl = row.riskLineVersion;
83
+ if (row.riskLineVersion !== null && row.riskLineVersion < RISK_LINE_VERSION) {
84
+ notes.push(RISK_LINE_TEXT);
85
+ notes.push(`risk line updated since enrollment (enrolled under v${row.riskLineVersion}, current v${RISK_LINE_VERSION})`);
86
+ }
87
+ }
88
+ }
89
+ const byId = new Map((rt?.sessions ?? []).map((s) => [s.id, s]));
90
+ const rtSeats = new Map((rt?.seats ?? []).map((s) => [s.spawn_id, s]));
91
+ const rows = d.ps();
92
+ const seats = [];
93
+ for (const p of await d.panes()) {
94
+ if (p.home !== d.homeTag || !p.sid)
95
+ continue;
96
+ const pids = p.panePid && !p.dead ? seatPids(rows, p.panePid) : { claude: null, proxy: null };
97
+ // The seat socket is C3's to name; status reads it off the proxy (lsof) and confirms it by stat.
98
+ const socketPath = pids.proxy ? firstSocket(await d.lsofUnixPaths(pids.proxy)) : null;
99
+ // seat.ts writes the launch spec per session (specs/<session id>.json); an adopted seat has no
100
+ // spec_path in the runtime file, so fall back to that same name.
101
+ const spec = rtSeats.get(p.spawn)?.spec_path ?? path.join(paths.specDir, `${p.sid}.json`);
102
+ const specSha = sha256File(spec);
103
+ seats.push({
104
+ session_id: p.sid,
105
+ name: byId.get(p.sid)?.name ?? null,
106
+ epoch: p.epoch,
107
+ spawn_id: p.spawn,
108
+ pid: p.panePid,
109
+ state: byId.get(p.sid)?.state ?? null,
110
+ proxy_pid: pids.proxy,
111
+ claude_pid: pids.claude,
112
+ proxy_port: pids.proxy ? await d.lsofListenPort(pids.proxy) : null,
113
+ socket_path: socketPath,
114
+ spec_path: specSha ? spec : null,
115
+ spec_sha256: specSha,
116
+ tmux_session: p.session,
117
+ });
118
+ }
119
+ const launcherKey = launcherDurableKey(d.home);
120
+ const sessions = (rt?.sessions ?? []).map((s) => {
121
+ const u = rt?.unavailable?.[s.id] ?? null;
122
+ if (u && u.cause === "guard" && u.count > CLAIM_UNAVAILABLE_SURFACE_N && s.name) {
123
+ fixes.push(renderFixLine("guard", { launcherKey, nameHeld: true, params: { name: s.name } }));
124
+ }
125
+ return {
126
+ id: s.id,
127
+ name: s.name,
128
+ state: s.state,
129
+ state_reason: s.state_reason,
130
+ claim_unavailable: u ? { cause: u.cause, count: u.count } : null,
131
+ };
132
+ });
133
+ return {
134
+ instance: instance.label,
135
+ installed,
136
+ loaded: job.loaded,
137
+ daemon,
138
+ daemon_pid: daemonPid,
139
+ lock_holder_pid: lockHolder,
140
+ lock_path: paths.lockFile,
141
+ enrolled: state !== null,
142
+ executor_id: state?.executor_id ?? null,
143
+ executor_key_sha256: state ? fingerprint(state.executor_key) : null,
144
+ nonce_sha256: state && state.nonce ? fingerprint(state.nonce) : null,
145
+ boot_id: state?.boot_id ?? null,
146
+ transcript_guard: rt?.transcript_guard ?? "link",
147
+ integrity: rt?.integrity
148
+ ? rt.integrity.ok
149
+ ? { ok: true }
150
+ : { ok: false, cause: rt.integrity.cause }
151
+ : { ok: true },
152
+ risk_line_version: { enrolled: enrolledRl, current: RISK_LINE_VERSION },
153
+ grant,
154
+ seats,
155
+ sessions,
156
+ fixes,
157
+ notes,
158
+ };
159
+ }
160
+ const isUuid = (v) => typeof v === "string" &&
161
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);
162
+ /**
163
+ * `host status --json`: exactly the S1 HOST_STATUS_JSON document (ruling 26), closed at every level.
164
+ * A seat row the schema cannot hold yet (no session state known, no launch spec on disk, no pane
165
+ * pid) is left out rather than printed with a guessed value; the human view still shows it.
166
+ */
167
+ export function toHostStatusJson(s) {
168
+ const seats = [];
169
+ for (const r of s.seats) {
170
+ if (!isUuid(r.session_id) || !isUuid(r.spawn_id) || r.epoch === null || r.pid === null)
171
+ continue;
172
+ if (r.state === null || !SESSION_STATES.includes(r.state))
173
+ continue;
174
+ if (r.spec_path === null || r.spec_sha256 === null)
175
+ continue;
176
+ seats.push({
177
+ session_id: r.session_id,
178
+ epoch: r.epoch,
179
+ spawn_id: r.spawn_id,
180
+ state: r.state,
181
+ pid: r.pid,
182
+ proxy_pid: r.proxy_pid,
183
+ claude_pid: r.claude_pid,
184
+ // ruling 26.1: a port is reported only with the proxy process it belongs to
185
+ proxy_port: r.proxy_pid === null ? null : r.proxy_port,
186
+ socket_path: r.socket_path,
187
+ spec_path: r.spec_path,
188
+ spec_sha256: r.spec_sha256,
189
+ });
190
+ }
191
+ const sessions = [];
192
+ for (const x of s.sessions) {
193
+ if (!isUuid(x.id))
194
+ continue;
195
+ const u = x.claim_unavailable;
196
+ const known = u !== null &&
197
+ u.cause !== null &&
198
+ CLAIM_UNAVAILABLE_CAUSES.includes(u.cause) &&
199
+ u.count >= 1;
200
+ sessions.push({
201
+ session_id: x.id,
202
+ claim_unavailable: known
203
+ ? { cause: u.cause, consecutive: u.count }
204
+ : null,
205
+ });
206
+ }
207
+ return {
208
+ installed: s.installed,
209
+ loaded: s.loaded,
210
+ instance: s.instance,
211
+ daemon_pid: s.daemon_pid,
212
+ lock_holder_pid: s.lock_holder_pid,
213
+ lock_path: s.lock_path,
214
+ transcript_guard: s.transcript_guard,
215
+ nonce_fingerprint: s.nonce_sha256,
216
+ key_fingerprint: s.executor_key_sha256,
217
+ seats,
218
+ sessions,
219
+ };
220
+ }
221
+ export function renderStatusHuman(s) {
222
+ const out = [];
223
+ out.push(`instance: ${s.instance}`);
224
+ out.push(`installed: ${s.installed ? "yes" : "no"}${s.loaded ? " (loaded)" : ""}`);
225
+ out.push(s.daemon === "idle"
226
+ ? IDLE_TEXT
227
+ : `daemon: ${s.daemon}${s.daemon_pid ? ` (pid ${s.daemon_pid})` : ""}`);
228
+ out.push(`lock: ${s.lock_path}${s.lock_holder_pid ? ` (held by ${s.lock_holder_pid})` : ""}`);
229
+ out.push(s.enrolled
230
+ ? `executor: ${s.executor_id} (key ${s.executor_key_sha256}, nonce ${s.nonce_sha256})`
231
+ : "executor: not enrolled");
232
+ out.push(`transcript guard: ${s.transcript_guard}`);
233
+ for (const r of s.seats) {
234
+ out.push(`seat ${r.name ?? r.session_id} epoch ${r.epoch ?? "?"} spawn ${r.spawn_id.slice(0, 8)} pane ${r.pid ?? "-"} claude ${r.claude_pid ?? "-"} proxy ${r.proxy_pid ?? "-"}${r.proxy_port ? `:${r.proxy_port}` : ""} ${r.state ?? ""}`.trimEnd());
235
+ }
236
+ for (const x of s.sessions) {
237
+ if (x.claim_unavailable)
238
+ out.push(`session ${x.name ?? x.id}: claim unavailable (${x.claim_unavailable.cause ?? "cause not given"}) x${x.claim_unavailable.count}`);
239
+ }
240
+ for (const n of s.notes)
241
+ out.push(n);
242
+ for (const f of s.fixes)
243
+ out.push(f);
244
+ return out;
245
+ }
246
+ /**
247
+ * Print a status. --json: the S1 document alone on stdout (one parseable object), and the notes
248
+ * and fix lines on stderr so a script still sees them. Human: everything on stdout.
249
+ */
250
+ export function printStatus(s, json, out, err) {
251
+ if (!json) {
252
+ for (const l of renderStatusHuman(s))
253
+ out(l);
254
+ return;
255
+ }
256
+ out(JSON.stringify(toHostStatusJson(s), null, 2));
257
+ for (const n of s.notes)
258
+ err(n);
259
+ for (const f of s.fixes)
260
+ err(f);
261
+ }
262
+ // ---- real probes ------------------------------------------------------------------------------
263
+ const run = (file, args) => new Promise((resolve) => {
264
+ execFile(file, args, { env: { PATH: "/usr/bin:/bin:/usr/sbin:/sbin" }, timeout: 15000 }, (_e, stdout) => resolve(String(stdout ?? "")));
265
+ });
266
+ export async function lsofPids(file) {
267
+ const out = await run("/usr/sbin/lsof", ["-F", "p", file]);
268
+ return out
269
+ .split("\n")
270
+ .filter((l) => /^p\d+$/.test(l))
271
+ .map((l) => Number(l.slice(1)));
272
+ }
273
+ function firstSocket(paths) {
274
+ for (const p of paths) {
275
+ try {
276
+ if (fs.lstatSync(p).isSocket())
277
+ return p;
278
+ }
279
+ catch {
280
+ /* gone or not a path */
281
+ }
282
+ }
283
+ return null;
284
+ }
285
+ /** `lsof -F n` output to the absolute names it lists (unnamed peers such as `->0x…` are skipped). */
286
+ export function parseLsofUnixPaths(out) {
287
+ return out
288
+ .split("\n")
289
+ .filter((l) => l.startsWith("n/"))
290
+ .map((l) => l.slice(1));
291
+ }
292
+ export async function lsofUnixPaths(pid) {
293
+ return parseLsofUnixPaths(await run("/usr/sbin/lsof", ["-nP", "-a", "-p", String(pid), "-U", "-Fn"]));
294
+ }
295
+ export async function lsofListenPort(pid) {
296
+ const out = await run("/usr/sbin/lsof", [
297
+ "-nP",
298
+ "-a",
299
+ "-p",
300
+ String(pid),
301
+ "-iTCP",
302
+ "-sTCP:LISTEN",
303
+ "-Fn",
304
+ ]);
305
+ const m = /^n[^\n]*:(\d+)$/m.exec(out);
306
+ return m ? Number(m[1]) : null;
307
+ }
@@ -0,0 +1,56 @@
1
+ export declare const TAG_SID = "@mnemom_sid";
2
+ export declare const TAG_EPOCH = "@mnemom_epoch";
3
+ export declare const TAG_SPAWN = "@mnemom_spawn";
4
+ export declare const TAG_HOME = "@mnemom_home";
5
+ /** A seat's tmux session name: the session id and its spawn's first 8 characters. */
6
+ export declare function seatTmuxSession(sessionId: string, spawnId: string): string;
7
+ export interface TmuxResult {
8
+ readonly code: number;
9
+ readonly stdout: string;
10
+ readonly stderr: string;
11
+ }
12
+ export type TmuxRunner = (args: readonly string[]) => Promise<TmuxResult>;
13
+ /** tmux's own environment: only what it needs to find its socket; no credential can ride along. */
14
+ export declare function tmuxEnv(env: Readonly<Record<string, string | undefined>>): Record<string, string>;
15
+ export declare function tmuxRunner(socket: string, env: Readonly<Record<string, string | undefined>>): TmuxRunner;
16
+ export interface SeatPane {
17
+ readonly session: string;
18
+ readonly paneId: string;
19
+ readonly panePid: number | null;
20
+ readonly dead: boolean;
21
+ readonly deadStatus: number | null;
22
+ readonly sid: string;
23
+ readonly epoch: number | null;
24
+ readonly spawn: string;
25
+ readonly home: string;
26
+ }
27
+ export declare function parsePanes(stdout: string): SeatPane[];
28
+ export declare class Tmux {
29
+ private readonly run;
30
+ constructor(run: TmuxRunner);
31
+ /** Start (or reuse) the server with the host's global options. */
32
+ ensureServer(): Promise<void>;
33
+ /** Every pane on this socket with its tags ([] when there is no server). */
34
+ panes(): Promise<SeatPane[]>;
35
+ /** A detached session running `command`, tagged in the same tmux command. Returns the pane pid. */
36
+ newSeat(opts: {
37
+ name: string;
38
+ cwd: string;
39
+ command: string;
40
+ env: Readonly<Record<string, string>>;
41
+ tags: {
42
+ sid: string;
43
+ epoch: number;
44
+ spawn: string;
45
+ home: string;
46
+ };
47
+ }): Promise<{
48
+ panePid: number;
49
+ paneId: string;
50
+ }>;
51
+ /** The visible text of a session's pane, or null when the session is gone. */
52
+ capturePane(name: string): Promise<string | null>;
53
+ /** Type `text` literally into a session's pane, then press Enter. */
54
+ sendLine(name: string, text: string): Promise<void>;
55
+ killSession(name: string): Promise<void>;
56
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The host's own tmux server (C12-R04, R10, R12, R22, R35). Every call names the instance socket
3
+ * (-L); the daemon never touches the default server or any other socket. The server is started
4
+ * from a scrubbed environment with update-environment empty, so neither the daemon's own env nor
5
+ * an owner's later attach leaks into seats. Seats are tagged at session scope with
6
+ * @mnemom_sid / @mnemom_epoch / @mnemom_spawn / @mnemom_home in the same tmux command that creates
7
+ * them; remain-on-exit keeps a dead pane so its exit status can be read (liveness itself is judged
8
+ * by pid, never by pane existence, R19 M23).
9
+ */
10
+ import { execFile } from "node:child_process";
11
+ export const TAG_SID = "@mnemom_sid";
12
+ export const TAG_EPOCH = "@mnemom_epoch";
13
+ export const TAG_SPAWN = "@mnemom_spawn";
14
+ export const TAG_HOME = "@mnemom_home";
15
+ /** A seat's tmux session name: the session id and its spawn's first 8 characters. */
16
+ export function seatTmuxSession(sessionId, spawnId) {
17
+ return `mnm-${sessionId}-${spawnId.slice(0, 8)}`;
18
+ }
19
+ /** tmux's own environment: only what it needs to find its socket; no credential can ride along. */
20
+ export function tmuxEnv(env) {
21
+ const out = {
22
+ PATH: env.PATH || "/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/usr/local/bin",
23
+ };
24
+ for (const k of ["HOME", "TMUX_TMPDIR", "LANG", "USER", "LOGNAME"]) {
25
+ const v = env[k];
26
+ if (v)
27
+ out[k] = v;
28
+ }
29
+ return out;
30
+ }
31
+ export function tmuxRunner(socket, env) {
32
+ const e = tmuxEnv(env);
33
+ return (args) => new Promise((resolve) => {
34
+ execFile("tmux", ["-L", socket, ...args], { env: e, timeout: 15000, maxBuffer: 8 * 1024 * 1024 }, (err, stdout, stderr) => {
35
+ const code = err
36
+ ? typeof err.code === "number"
37
+ ? err.code
38
+ : 1
39
+ : 0;
40
+ resolve({ code, stdout: String(stdout), stderr: String(stderr) });
41
+ });
42
+ });
43
+ }
44
+ const FMT = [
45
+ "#{session_name}",
46
+ "#{pane_id}",
47
+ "#{pane_pid}",
48
+ "#{pane_dead}",
49
+ "#{pane_dead_status}",
50
+ `#{${TAG_SID}}`,
51
+ `#{${TAG_EPOCH}}`,
52
+ `#{${TAG_SPAWN}}`,
53
+ `#{${TAG_HOME}}`,
54
+ ].join("\t");
55
+ export function parsePanes(stdout) {
56
+ const out = [];
57
+ for (const line of stdout.split("\n")) {
58
+ if (!line.trim())
59
+ continue;
60
+ const [session = "", paneId = "", pid = "", dead = "", status = "", sid = "", epoch = "", spawn = "", home = "",] = line.split("\t");
61
+ out.push({
62
+ session,
63
+ paneId,
64
+ panePid: /^\d+$/.test(pid) ? Number(pid) : null,
65
+ dead: dead === "1",
66
+ deadStatus: /^\d+$/.test(status) ? Number(status) : null,
67
+ sid,
68
+ epoch: /^\d+$/.test(epoch) ? Number(epoch) : null,
69
+ spawn,
70
+ home,
71
+ });
72
+ }
73
+ return out;
74
+ }
75
+ export class Tmux {
76
+ run;
77
+ constructor(run) {
78
+ this.run = run;
79
+ }
80
+ /** Start (or reuse) the server with the host's global options. */
81
+ async ensureServer() {
82
+ const r = await this.run([
83
+ "start-server",
84
+ ";",
85
+ "set-option",
86
+ "-g",
87
+ "exit-empty",
88
+ "off",
89
+ ";",
90
+ "set-option",
91
+ "-g",
92
+ "update-environment",
93
+ "",
94
+ ";",
95
+ "set-option",
96
+ "-g",
97
+ "remain-on-exit",
98
+ "on",
99
+ ";",
100
+ "set-option",
101
+ "-g",
102
+ "default-shell",
103
+ "/bin/sh",
104
+ ]);
105
+ if (r.code !== 0)
106
+ throw new Error(`tmux start-server failed: ${r.stderr.trim()}`);
107
+ }
108
+ /** Every pane on this socket with its tags ([] when there is no server). */
109
+ async panes() {
110
+ const r = await this.run(["list-panes", "-a", "-F", FMT]);
111
+ if (r.code !== 0)
112
+ return [];
113
+ return parsePanes(r.stdout);
114
+ }
115
+ /** A detached session running `command`, tagged in the same tmux command. Returns the pane pid. */
116
+ async newSeat(opts) {
117
+ const envArgs = Object.entries(opts.env).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
118
+ const t = opts.name;
119
+ const r = await this.run([
120
+ "new-session",
121
+ "-d",
122
+ "-s",
123
+ t,
124
+ "-c",
125
+ opts.cwd,
126
+ "-x",
127
+ "200",
128
+ "-y",
129
+ "50",
130
+ ...envArgs,
131
+ "-P",
132
+ "-F",
133
+ "#{pane_pid}\t#{pane_id}",
134
+ opts.command,
135
+ ";",
136
+ "set-option",
137
+ "-t",
138
+ t,
139
+ TAG_SID,
140
+ opts.tags.sid,
141
+ ";",
142
+ "set-option",
143
+ "-t",
144
+ t,
145
+ TAG_EPOCH,
146
+ String(opts.tags.epoch),
147
+ ";",
148
+ "set-option",
149
+ "-t",
150
+ t,
151
+ TAG_SPAWN,
152
+ opts.tags.spawn,
153
+ ";",
154
+ "set-option",
155
+ "-t",
156
+ t,
157
+ TAG_HOME,
158
+ opts.tags.home,
159
+ ]);
160
+ const m = /^(\d+)\t(%\d+)/m.exec(r.stdout);
161
+ if (r.code !== 0 || !m)
162
+ throw new Error(`tmux new-session failed: ${r.stderr.trim() || r.stdout.trim()}`);
163
+ return { panePid: Number(m[1]), paneId: m[2] };
164
+ }
165
+ /** The visible text of a session's pane, or null when the session is gone. */
166
+ async capturePane(name) {
167
+ const r = await this.run(["capture-pane", "-p", "-t", `=${name}:`]);
168
+ return r.code === 0 ? r.stdout : null;
169
+ }
170
+ /** Type `text` literally into a session's pane, then press Enter. */
171
+ async sendLine(name, text) {
172
+ const r = await this.run(["send-keys", "-t", `=${name}:`, "-l", text]);
173
+ if (r.code !== 0)
174
+ throw new Error(`tmux send-keys failed: ${r.stderr.trim()}`);
175
+ const e = await this.run(["send-keys", "-t", `=${name}:`, "Enter"]);
176
+ if (e.code !== 0)
177
+ throw new Error(`tmux send-keys failed: ${e.stderr.trim()}`);
178
+ }
179
+ async killSession(name) {
180
+ await this.run(["kill-session", "-t", `=${name}`]);
181
+ }
182
+ }
@@ -0,0 +1,30 @@
1
+ export type GuardMode = "link" | "copy";
2
+ export declare function ccProjectsDir(env: Readonly<Record<string, string | undefined>>, home?: string): string;
3
+ /** Claude Code's project folder name for a cwd: every non-alphanumeric character becomes '-'. */
4
+ export declare const projectSlug: (cwd: string) => string;
5
+ export declare function expectedTranscriptPath(projectsDir: string, cwd: string, ccSessionId: string): string;
6
+ /** The transcript as it is on disk now: the cwd's project folder first, then any project folder. */
7
+ export declare function findTranscript(projectsDir: string, cwd: string | null, ccSessionId: string): string | null;
8
+ export interface GuardResult {
9
+ readonly mode: GuardMode | null;
10
+ readonly guardPath: string;
11
+ }
12
+ /** Create or refresh the guard for one session. Returns the mode now held (null when there is nothing to guard). */
13
+ export declare function refreshGuard(home: string, sessionId: string, original: string | null): GuardResult;
14
+ export type TranscriptCheck = {
15
+ kind: "present";
16
+ path: string;
17
+ } | {
18
+ kind: "restored";
19
+ path: string;
20
+ } | {
21
+ kind: "lost";
22
+ };
23
+ /**
24
+ * Before a `--resume`: the original exists, or it is restored from the guard (a new hard link,
25
+ * or a copy across volumes), or both are missing (transcript_lost).
26
+ */
27
+ export declare function ensureTranscript(home: string, sessionId: string, projectsDir: string, cwd: string | null, ccSessionId: string, lastKnown: string | null): TranscriptCheck;
28
+ export declare function removeGuard(home: string, sessionId: string): void;
29
+ /** Session ids that have a guard file. */
30
+ export declare function guardedSessionIds(home: string): string[];