@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,393 @@
1
+ /**
2
+ * The flat durable owner verbs (BACKBONE C1; S1 owner-verbs.ts, ROUTES.own_*; C11, C16):
3
+ *
4
+ * mnemom agent pause|resume|done|kill <name> [--json]
5
+ * mnemom agent amend <name> [--budget <n>] [--turns <n>] [--json]
6
+ * mnemom agent sessions unlock <name>
7
+ * mnemom agent containment pause|resume <agent>
8
+ *
9
+ * The API decides (S1 owner-verb table, first match wins); the CLI resolves the name through the
10
+ * registry, sends the verb, and renders the answer: a refusal with its S1 fix line and machine
11
+ * code, a 200 no-op with its state, a 202 device step (user_code + verification_uri_complete,
12
+ * polled at the server interval with the 202's device_code and client_id; never a token printed
13
+ * or kept), or the applied change. `done`/`kill` on a tracked session also stop the local tracked
14
+ * claude after the 200 (final F04; SIGTERM, SIGKILL after TRACKED_STOP_GRACE_S).
15
+ *
16
+ * Every dependency is injected (`DurableVerbDeps`), so each branch is unit-tested.
17
+ */
18
+ import { PER_SESSION_BUDGET_LIMIT_USD, TRACKED_STOP_GRACE_S, } from "../vendor/shared/durable-contract/constants.js";
19
+ import { renderFixLine, } from "../vendor/shared/durable-contract/fixlines.js";
20
+ import { errorCodeOf, errorDetailsOf, errorMessageOf, RegistryAnswerError, RegistryUnavailable, } from "../lib/durable/api.js";
21
+ import { DurableRefusal, printDurableRefusal } from "../lib/durable/refusal.js";
22
+ import { isLocked, liveRow, stateLabel } from "../lib/durable/view.js";
23
+ import { CODE } from "../lib/durable/codes.js";
24
+ const VERB_ROUTE = {
25
+ pause: "own_pause",
26
+ resume: "own_resume",
27
+ done: "own_complete",
28
+ kill: "own_kill",
29
+ amend: "own_amend",
30
+ unlock: "own_unlock",
31
+ };
32
+ const APPLIED_TEXT = {
33
+ pause: "paused",
34
+ resume: "resumed; the host daemon picks it up",
35
+ done: "done",
36
+ kill: "killed",
37
+ amend: "amended",
38
+ unlock: "unlocked",
39
+ };
40
+ /** The session a verb acts on: the non-terminal row holding the name, else the newest row. */
41
+ export async function resolveTarget(api, name) {
42
+ let rows;
43
+ try {
44
+ rows = await api.listSessions({ name });
45
+ }
46
+ catch (e) {
47
+ if (e instanceof RegistryAnswerError)
48
+ throw refusalFromAnswer(e.answer, name, "off");
49
+ throw e;
50
+ }
51
+ const row = liveRow(rows) ?? rows[0] ?? null;
52
+ if (row === null) {
53
+ throw new DurableRefusal({
54
+ code: "agent_not_found",
55
+ message: `no session named ${name}`,
56
+ fix: null,
57
+ });
58
+ }
59
+ return row;
60
+ }
61
+ /**
62
+ * A refusal from an API answer: the API's message and code, its rendered fix (details.fix, as the
63
+ * API already applied the fix-line rule), else the CLI's catalogue fallback for that code.
64
+ */
65
+ export function refusalFromAnswer(a, name, launcherKey, row) {
66
+ const code = errorCodeOf(a.body) ?? `http_${a.status}`;
67
+ const details = errorDetailsOf(a.body);
68
+ const apiFix = typeof details.fix === "string" && details.fix !== "" ? details.fix : null;
69
+ const fix = apiFix ?? fallbackFix(code, details, name, launcherKey, row);
70
+ const apiMessage = errorMessageOf(a.body);
71
+ // The refusal line always names the session the verb was for (C02-R05: the user sees what failed).
72
+ const message = apiMessage === null
73
+ ? `${name}: refused (${a.status})`
74
+ : apiMessage.includes(name)
75
+ ? apiMessage
76
+ : `${name}: ${apiMessage}`;
77
+ return new DurableRefusal({ code, message: sanitizeLine(message), fix, apiBody: a.body });
78
+ }
79
+ /** A one-line message: no control characters reach the terminal. */
80
+ function sanitizeLine(s) {
81
+ return (s
82
+ // eslint-disable-next-line no-control-regex
83
+ .replace(/[\u0000-\u001f\u007f]+/g, " ")
84
+ .replace(/ {2,}/g, " ")
85
+ .trim());
86
+ }
87
+ function render(key, params, launcherKey, held) {
88
+ return renderFixLine(key, { launcherKey, nameHeld: held, params });
89
+ }
90
+ /** The catalogue fix the CLI prints when the API body carries none. */
91
+ export function fallbackFix(code, details, name, launcherKey, row) {
92
+ const agent = row?.agent_name ?? row?.agent_id ?? "<agent>";
93
+ switch (code) {
94
+ case CODE.durable_session_ended:
95
+ return details.state_reason === "agent_killed"
96
+ ? render("agent_killed", { name }, launcherKey, false)
97
+ : render("session_ended", { name }, launcherKey, false);
98
+ case CODE.durable_agent_contained:
99
+ return render("agent_contained", { agent }, launcherKey, true);
100
+ case CODE.durable_not_owner:
101
+ return typeof details.owner === "string"
102
+ ? render("not_owner_info", { owner: details.owner }, launcherKey, true)
103
+ : null;
104
+ case CODE.durable_session_login_required:
105
+ return render("device_step_login", {}, launcherKey, true);
106
+ case CODE.durable_unsupported:
107
+ if (details.feature === "launch_spec_change") {
108
+ // The flags come from the answer; without them there is no line to print.
109
+ const flags = Array.isArray(details.flags)
110
+ ? details.flags.filter((f) => typeof f === "string").join(" ")
111
+ : typeof details.flags === "string"
112
+ ? details.flags
113
+ : "";
114
+ return flags.trim() === ""
115
+ ? null
116
+ : render("launch_spec_change", { name, flags: flags.trim() }, launcherKey, true);
117
+ }
118
+ if (details.feature === "above_limit_raise")
119
+ return render("above_limit_raise", { name, limit: String(PER_SESSION_BUDGET_LIMIT_USD) }, launcherKey, true);
120
+ return null;
121
+ default:
122
+ return null;
123
+ }
124
+ }
125
+ function isRecord(v) {
126
+ return v !== null && typeof v === "object" && !Array.isArray(v);
127
+ }
128
+ export function deviceStepOf(body) {
129
+ if (!isRecord(body))
130
+ return null;
131
+ const b = (isRecord(body.device_step) ? body.device_step : body);
132
+ const s = (k) => (typeof b[k] === "string" && b[k] !== "" ? b[k] : null);
133
+ const user = s("user_code");
134
+ const uri = s("verification_uri_complete");
135
+ const dc = s("device_code");
136
+ const cid = s("client_id");
137
+ if (!user || !uri || !dc || !cid)
138
+ return null;
139
+ const interval = typeof b.interval === "number" && b.interval > 0 ? b.interval : 5;
140
+ return {
141
+ user_code: user,
142
+ verification_uri_complete: uri,
143
+ device_code: dc,
144
+ interval,
145
+ client_id: cid,
146
+ ...(s("approval_id") ? { approval_id: s("approval_id") } : {}),
147
+ };
148
+ }
149
+ /** How long the CLI keeps polling a device step before giving up (the approval TTL). */
150
+ export const DEVICE_STEP_POLL_MAX_MS = 15 * 60 * 1000;
151
+ /**
152
+ * Print the device step and poll until it resolves. 0 when applied; throws a DurableRefusal when
153
+ * denied, expired or refused. Never prints the device_code or any token.
154
+ */
155
+ export async function runDeviceStep(step, note, deps) {
156
+ deps.err(`Approve this change: open ${step.verification_uri_complete}`);
157
+ deps.err(`and confirm the code ${step.user_code}.`);
158
+ if (note)
159
+ deps.err(note);
160
+ let interval = step.interval * 1000;
161
+ const deadline = deps.now() + DEVICE_STEP_POLL_MAX_MS;
162
+ for (;;) {
163
+ await deps.sleep(interval);
164
+ const a = await deps.api.devicePoll(step.device_code, step.client_id);
165
+ const code = errorCodeOf(a.body);
166
+ if (a.status < 300 && code === null) {
167
+ deps.err("approved and applied");
168
+ return 0;
169
+ }
170
+ if (code === "authorization_pending" || code === "slow_down") {
171
+ if (code === "slow_down")
172
+ interval += 5000;
173
+ if (deps.now() + interval > deadline) {
174
+ throw new DurableRefusal({
175
+ code: "expired_token",
176
+ message: "the approval expired",
177
+ fix: null,
178
+ });
179
+ }
180
+ continue;
181
+ }
182
+ const why = code === "access_denied"
183
+ ? "the approval was denied"
184
+ : code === "expired_token"
185
+ ? "the approval expired"
186
+ : (errorMessageOf(a.body) ?? "the approval was refused");
187
+ throw new DurableRefusal({ code: code ?? `http_${a.status}`, message: why, fix: null });
188
+ }
189
+ }
190
+ /** Run one verb end to end and return its exit status (refusals rendered here). */
191
+ export async function runVerb(verb, name, body, opts, deps) {
192
+ const launcherKey = deps.launcherKey();
193
+ try {
194
+ const row = await resolveTarget(deps.api, name);
195
+ const a = await deps.api.ownerVerb(VERB_ROUTE[verb], row.conversation_id, row.agent_id, body);
196
+ if (a.status === 202) {
197
+ const step = deviceStepOf(a.body);
198
+ if (step === null) {
199
+ throw new DurableRefusal({
200
+ code: "device_step_malformed",
201
+ message: "the API answered 202 without a usable device step",
202
+ fix: null,
203
+ });
204
+ }
205
+ const contained = row.state === "paused" && row.state_reason === "agent_contained";
206
+ const note = contained
207
+ ? "an org admin other than you must approve this unpause (a solo org approves it itself)"
208
+ : null;
209
+ const rc = await runDeviceStep(step, note, deps);
210
+ if (opts.json)
211
+ deps.out(JSON.stringify({ name, verb, applied: true }));
212
+ return rc;
213
+ }
214
+ if (a.status >= 200 && a.status < 300) {
215
+ if (verb === "done" || verb === "kill") {
216
+ // An ending verb fails closed: only an answer that says the session ended (or a re-read
217
+ // that shows it) counts, and only then is a tracked claude stopped. Never SIGKILL on an
218
+ // ambiguous 2xx (a null body, a proxy-swallowed body, schema drift).
219
+ if (!(isRecord(a.body) && a.body.changed === false)) {
220
+ await confirmEnded(verb, row, a, deps.api, name);
221
+ }
222
+ }
223
+ const changed = !(isRecord(a.body) && a.body.changed === false);
224
+ // --json: stdout carries the one JSON document; the human lines go to stderr.
225
+ const say = opts.json ? deps.err : deps.out;
226
+ if (!changed) {
227
+ say(`${name}: no change (${stateLabel(row)})`);
228
+ if (verb === "resume")
229
+ say(`fix: ${render("resume_live", { name }, launcherKey, true)}`);
230
+ }
231
+ else {
232
+ say(`${name}: ${APPLIED_TEXT[verb]}`);
233
+ if ((verb === "done" || verb === "kill") && !isLocked(row) && deps.stopTrackedClaude) {
234
+ await deps.stopTrackedClaude(row, TRACKED_STOP_GRACE_S);
235
+ }
236
+ }
237
+ if (opts.json)
238
+ deps.out(JSON.stringify(a.body ?? { changed }));
239
+ return 0;
240
+ }
241
+ throw refusalFromAnswer(a, name, launcherKey, row);
242
+ }
243
+ catch (e) {
244
+ return handleVerbError(e, opts.json === true, deps);
245
+ }
246
+ }
247
+ /** The state an applied done / kill writes (S1 owner-verbs: complete -> completed, kill -> killed). */
248
+ const ENDED_STATE = { done: "completed", kill: "killed" };
249
+ /**
250
+ * Confirm an applied done / kill: the answer's own `{changed: true, state}` when it names the ended
251
+ * state, else one re-read of the session by conversation id. Throws a refusal (nothing stopped
252
+ * locally) when neither shows the session ended.
253
+ */
254
+ export async function confirmEnded(verb, row, a, api, name) {
255
+ const want = ENDED_STATE[verb];
256
+ if (isRecord(a.body) && a.body.changed === true && a.body.state === want)
257
+ return;
258
+ const seen = await api.listSessions({ name }).then((rows) => rows.find((r) => r.conversation_id === row.conversation_id) ?? null, () => null);
259
+ if (seen !== null && seen.state === want)
260
+ return;
261
+ throw new DurableRefusal({
262
+ code: "verb_unconfirmed",
263
+ message: `${name}: the API answered ${a.status} but did not confirm the session ${want === "completed" ? "completed" : "was killed"}; nothing was stopped locally`,
264
+ fix: null,
265
+ });
266
+ }
267
+ export function handleVerbError(e, json, deps) {
268
+ if (e instanceof DurableRefusal)
269
+ return printDurableRefusal(e, json, deps);
270
+ if (e instanceof RegistryUnavailable) {
271
+ return printDurableRefusal(new DurableRefusal({
272
+ code: "session_registry_unavailable",
273
+ message: `the session registry is unreachable (${e.message}); nothing was changed`,
274
+ fix: null,
275
+ }), json, deps);
276
+ }
277
+ throw e;
278
+ }
279
+ /** Parse a positive amount (amend --budget); `integer` for a count (amend --turns). */
280
+ export function parsePositive(flag, raw, integer = false) {
281
+ if (raw === undefined)
282
+ return undefined;
283
+ const n = Number(raw);
284
+ if (!Number.isFinite(n) || n <= 0 || (integer && !Number.isInteger(n))) {
285
+ throw new DurableRefusal({
286
+ code: "invalid_argument",
287
+ message: `${flag} must be a positive ${integer ? "whole number" : "number"} (got '${raw}')`,
288
+ fix: null,
289
+ exit: 2,
290
+ });
291
+ }
292
+ return n;
293
+ }
294
+ export async function amendCommand(name, opts, deps) {
295
+ let budget;
296
+ let turns;
297
+ try {
298
+ budget = parsePositive("--budget", opts.budget);
299
+ turns = parsePositive("--turns", opts.turns, true);
300
+ }
301
+ catch (e) {
302
+ return handleVerbError(e, opts.json === true, deps);
303
+ }
304
+ if (budget === undefined && turns === undefined) {
305
+ deps.err("mnemom agent amend: give --budget <n> and/or --turns <n>");
306
+ return 2;
307
+ }
308
+ const body = {};
309
+ if (budget !== undefined)
310
+ body.budget_usd = budget;
311
+ if (turns !== undefined)
312
+ body.max_turns = turns;
313
+ return runVerb("amend", name, body, opts, deps);
314
+ }
315
+ export async function unlockCommand(name, deps) {
316
+ return runVerb("unlock", name, {}, {}, deps);
317
+ }
318
+ /**
319
+ * `mnemom agent containment pause|resume <agent>` (BACKBONE C1, amendment 35; C16-R30): a thin
320
+ * verb over POST /v1/orgs/{org}/agents/{id}/pause|resume. The org comes from the agent record,
321
+ * never the active org. It needs a session login: on an API-key-only credential, or the api's
322
+ * 403 org_scope_requires_session, it exits non-zero with the containment_login line and never
323
+ * sends the API key or prints the raw 403.
324
+ */
325
+ export async function containmentCommand(verb, agentRef, deps) {
326
+ const launcherKey = deps.launcherKey();
327
+ const loginRefusal = () => new DurableRefusal({
328
+ // Never the raw 403 code (C11-R26): the machine code is the S1 fix key.
329
+ code: "containment_login",
330
+ message: `mnemom agent containment ${verb}: org containment acts as a signed-in org admin`,
331
+ fix: render("containment_login", {}, launcherKey, false),
332
+ });
333
+ try {
334
+ if ((await deps.api.credentialType()) !== "jwt")
335
+ throw loginRefusal();
336
+ const { orgId, agentId } = await resolveContainmentAgent(agentRef, deps, launcherKey);
337
+ const a = await deps.api.containment(orgId, agentId, verb);
338
+ if (a.status === 403 && errorCodeOf(a.body) === "org_scope_requires_session")
339
+ throw loginRefusal();
340
+ if (a.status >= 200 && a.status < 300) {
341
+ deps.out(`agent ${agentRef}: ${verb === "pause" ? "paused (contained)" : "active again"}`);
342
+ return 0;
343
+ }
344
+ throw refusalFromAnswer(a, agentRef, launcherKey);
345
+ }
346
+ catch (e) {
347
+ return handleVerbError(e, false, deps);
348
+ }
349
+ }
350
+ /** An agent id as the CLI recognises one (lib/api.ts resolveAgentId). */
351
+ export const AGENT_ID_RE = /^(smolt-[0-9a-f]{8}|mnm-[0-9a-f-]{36})$/;
352
+ /**
353
+ * The containment target and its org, from the agent record (never the active org): an id is read
354
+ * with GET /v1/agents/{id}; a name is looked up in the caller's org fleets and must match exactly one.
355
+ */
356
+ export async function resolveContainmentAgent(agentRef, deps, launcherKey) {
357
+ if (AGENT_ID_RE.test(agentRef)) {
358
+ const ag = await deps.api.getAgent(agentRef);
359
+ if (ag.status !== 200 || !isRecord(ag.body))
360
+ throw refusalFromAnswer(ag, agentRef, launcherKey);
361
+ const rec = (isRecord(ag.body.agent) ? ag.body.agent : ag.body);
362
+ const orgId = typeof rec.org_id === "string" ? rec.org_id : null;
363
+ if (orgId === null) {
364
+ throw new DurableRefusal({
365
+ code: "agent_not_found",
366
+ message: `agent ${agentRef} has no org on record`,
367
+ fix: null,
368
+ });
369
+ }
370
+ return { orgId, agentId: typeof rec.id === "string" ? rec.id : agentRef };
371
+ }
372
+ const found = await deps.api.agentsByName(agentRef);
373
+ if (found.length === 0) {
374
+ throw new DurableRefusal({
375
+ code: "agent_not_found",
376
+ message: `no agent named ${agentRef} in your orgs`,
377
+ fix: null,
378
+ });
379
+ }
380
+ if (found.length > 1) {
381
+ throw new DurableRefusal({
382
+ code: "agent_ambiguous",
383
+ message: `${found.length} agents are named ${agentRef} in your orgs (${found.map((a) => a.id).join(", ")}); name one by its id`,
384
+ fix: null,
385
+ });
386
+ }
387
+ const [only] = found;
388
+ return { orgId: only.org_id, agentId: only.id };
389
+ }
390
+ /** The thin verbs (pause/resume/done/kill) with no body. */
391
+ export async function ownerVerbCommand(verb, name, opts, deps) {
392
+ return runVerb(verb, name, {}, opts, deps);
393
+ }
@@ -15,7 +15,7 @@ const OP_SEVERITIES = ["low", "medium", "high", "critical"];
15
15
  // Per ADR-037 Decision 4: deny public LLM endpoints + public DNS providers,
16
16
  // and the any-host CIDRs, at write time.
17
17
  // T8-4 (2026-05-19) extended the corpus per
18
- // `safe-house-hardening/audit/deny-list-completeness.md`: added emerging
18
+ // `safe-house-hardening/audits/deny-list-completeness.md`: added emerging
19
19
  // LLM providers (Replicate, Together, Anyscale, Fireworks, xAI,
20
20
  // Perplexity, DeepSeek) and three reserved IP ranges (link-local v4/v6
21
21
  // + multicast v4).
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `mnemom scan`: a read-only local scan of GitHub PR metadata. Runs entirely on this machine; nothing is
3
+ * uploaded in this build. The share-level prompt gates what a later upload step may send, and the chosen
4
+ * payload is printed so the user sees exactly what would leave. Pure logic is exported so each reject
5
+ * arm is unit-testable without Commander (same pattern as commands/usage.ts).
6
+ */
7
+ import { type GitHubCredential } from "../lib/scan/credential.js";
8
+ import { type ScanDeps } from "../lib/scan/scan.js";
9
+ import { type AnthropicUsageDeps } from "../lib/scan/spend/anthropic-usage.js";
10
+ import { type RecordDeps, type RecordOutcome } from "../lib/scan/record.js";
11
+ import { type ReportEmailDeps } from "../lib/scan/report-email.js";
12
+ import { type ReportRecipients } from "../lib/scan/share.js";
13
+ export interface ScanCommandOptions {
14
+ repos: string[];
15
+ period?: {
16
+ start: string;
17
+ end: string;
18
+ };
19
+ share?: string;
20
+ /** Report recipient. Required unless the runner is the CTO (asked when omitted). */
21
+ ctoEmail?: string;
22
+ /** Tier 2: the CTO's own monthly figure, as typed. Needs MNEMOM_ORG_SCAN_MODE=true. */
23
+ monthlySpend?: string;
24
+ /** Tier 2 without a figure on the command line: ask for one on the terminal. */
25
+ noAnthropicUsage?: boolean;
26
+ /** Tier 1: ask for the Anthropic admin key on the terminal (masked). Otherwise ANTHROPIC_ADMIN_KEY is used if set. */
27
+ anthropicAdminKey?: boolean;
28
+ /** Tier 1 result, when the caller already has it (test seam; set from the Anthropic reads otherwise). */
29
+ anthropicUsageUsd?: number;
30
+ anthropicOver200kShare?: number | null;
31
+ /** Test seam: the fetch behind the two Anthropic reads. */
32
+ anthropicDeps?: AnthropicUsageDeps;
33
+ /**
34
+ * The org the report is emailed for; inferred from the signed-in user's single non-personal org when
35
+ * omitted (see resolveScanOrg). The Record totals preview (MNEMOM_ORG_SCAN_MODE=true) reads ONLY an
36
+ * org passed explicitly here, never an inferred one: a guess could read the wrong org.
37
+ */
38
+ org?: string;
39
+ /** Test seam: the Record read. */
40
+ recordDeps?: RecordDeps;
41
+ /** Test seam: the report-email POST. */
42
+ emailDeps?: ReportEmailDeps;
43
+ /** Test seam: the signed-in user's orgs, used to infer --org when it is omitted. */
44
+ listOrgs?: () => Promise<Array<{
45
+ org_id: string;
46
+ name?: string;
47
+ is_personal?: boolean;
48
+ }>>;
49
+ /** Test seams. */
50
+ isTty?: boolean;
51
+ env?: NodeJS.ProcessEnv;
52
+ runnerEmail?: string;
53
+ credential?: GitHubCredential;
54
+ deps?: Partial<ScanDeps>;
55
+ now?: () => number;
56
+ }
57
+ /** Parses repeated/comma-separated `--repo owner/name`. Returns null on any invalid slug. */
58
+ export declare function parseRepoFlags(raw: string[] | undefined): string[] | null;
59
+ export declare const isEmail: (v: unknown) => v is string;
60
+ /** The runner's email (mnemom login, else asked) and the CTO's (flag, else "are you the CTO?"). */
61
+ export declare function resolveRecipients(o: {
62
+ runnerEmail?: string;
63
+ ctoEmail?: string;
64
+ }): Promise<ReportRecipients>;
65
+ /** The CTO's figure in USD, or null for none. Throws fixed text only; the input is never echoed. */
66
+ export declare function resolveSelfReportedSpend(o: ScanCommandOptions): Promise<number | null>;
67
+ export declare const NO_ADMIN_KEY_TERMINAL_MSG = "Set ANTHROPIC_ADMIN_KEY (there is no terminal to ask on).";
68
+ export declare const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tier.";
69
+ /**
70
+ * Tier 1: read measured usage with the CTO's Anthropic admin key, locally. The key comes from
71
+ * ANTHROPIC_ADMIN_KEY or a masked prompt (--anthropic-admin-key; never a value on argv), lives only in this
72
+ * call, and goes only to api.anthropic.com. Returns null for "no key" and for every failure, after printing a
73
+ * fixed sentence: the key, the thrown value and the response never reach the terminal. The caller then falls
74
+ * back to tier 2, then tier 3.
75
+ */
76
+ export declare function resolveMeasuredUsage(o: ScanCommandOptions, period: {
77
+ start: string;
78
+ end: string;
79
+ }): Promise<{
80
+ usd: number;
81
+ over200kShare: number | null;
82
+ } | null>;
83
+ /**
84
+ * --org is optional only because it can be inferred: the report is emailed on every scan, so the scan
85
+ * needs an org. Inference uses the signed-in user's own memberships (GET /v1/orgs, personal org
86
+ * excluded): exactly one org is used; none, several, or a failed read stops the scan loudly BEFORE any
87
+ * scan work, naming the fix. It never guesses between orgs and never skips the email.
88
+ */
89
+ export declare function resolveScanOrg(o: ScanCommandOptions): Promise<string>;
90
+ export declare const INVALID_ORG_MSG = "--org must be an org id like org-1a2b3c4d.";
91
+ /**
92
+ * The Record read for the preview: null when the flag gate is off (no read, no output, as before).
93
+ * With the gate on and no --org it is a note, not a guess. 404 becomes a note; 401/403 throw
94
+ * RecordAuthError (fail closed); every other failure becomes a fixed note. Nothing is stored.
95
+ */
96
+ export declare function resolveRecordOutcome(o: ScanCommandOptions): Promise<RecordOutcome | "no_org" | null>;
97
+ export declare function scanCommand(o: ScanCommandOptions): Promise<void>;