@mnemom/mnemom 0.17.20 → 0.17.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -45,6 +45,7 @@ That's it. `mnemom init` detects your configured AI provider API keys (Anthropic
45
45
  | `mnemom card validate <file>` | Validate card JSON locally |
46
46
  | `mnemom usage --org <id>` | Show per-person token and request consumption |
47
47
  | `mnemom agent [name]` | Launch a governed coding agent through the gateway |
48
+ | `mnemom agent export [agent]` | Save an agent's full record to a file (JSON or CSV) |
48
49
 
49
50
  ### `mnemom agent` — goal alignment
50
51
 
@@ -244,6 +245,31 @@ export MNEMOM_COMPACT_AT=200k # for every launch in this shell
244
245
  Code default (1M)`) and, when a target is set, the decoded session-config keys; the session
245
246
  ledger records the effective value.
246
247
 
248
+ ### `mnemom agent export` — an agent's full record
249
+
250
+ `mnemom agent export [id-or-name]` saves the agent's full record — integrity checkpoints, score
251
+ history, reclassifications, card amendments, disagreement reviews and policy evaluations with
252
+ gaps — to `./mnemom-record-<agent>-<date>.json` (`--format csv` for CSV). Without an agent it
253
+ uses `MNEMOM_AGENT`, else the agent saved in `~/.mnemom/launcher.toml` (`mnemom agent config
254
+ set agent …`), else the agent of your latest `mnemom agent` launch. You need to be a member of
255
+ the org that owns the agent.
256
+
257
+ ```bash
258
+ mnemom agent export my-agent # everything, as JSON
259
+ mnemom agent export my-agent --since 30d # the last 30 days
260
+ mnemom agent export my-agent --from 2026-09-01 --to 2026-09-30 --format csv
261
+ mnemom agent export my-agent -o - | jq '.checkpoints | length' # stdout; summary on stderr
262
+ ```
263
+
264
+ `--from`/`--to` take an ISO-8601 date or datetime (a bare `--to` date includes that whole UTC
265
+ day). The command refuses to overwrite an existing file unless you pass `--force`, and prints a
266
+ summary: the period, rows per section, a SHA-256 checksum of the file (so a recipient can
267
+ confirm it was not altered in transit) and the agent's public verification links. The file
268
+ holds full checkpoint reasoning that outsiders normally see redacted, so it is written
269
+ readable by you only (mode 0600); to prove the agent's record to someone without sharing the
270
+ contents, send the public verification link instead. If the period is too large to export in
271
+ one go, narrow it with `--since`, `--from` or `--to`.
272
+
247
273
  ## How It Works
248
274
 
249
275
  ```
@@ -0,0 +1,53 @@
1
+ /**
2
+ * `mnemom agent export [id-or-name]` — download an agent's full record
3
+ * (`GET /v1/agents/{id}/compliance-export`) to a file, or to stdout with `-o -`.
4
+ *
5
+ * Without `[id-or-name]` the agent is, in order: MNEMOM_AGENT, the saved
6
+ * `agent` in ~/.mnemom/launcher.toml, then the agent of the newest launch in the
7
+ * session ledger (lib/agent-export.ts pickAgentRef).
8
+ *
9
+ * Pure apart from the injected deps (`resolveAgentId`, `agentSources`,
10
+ * `fetchExport`, `writeFile`, `writeStdout`, `out`, `err`, `now`), so every arm is
11
+ * unit-tested; `liveExportDeps` is the thin driver index.ts wires in. The
12
+ * parsing, counts, summary and error wording live in lib/agent-export.ts.
13
+ *
14
+ * Returns the process exit code (0 ok, 1 on any error) instead of exiting, so
15
+ * index.ts sets `process.exitCode` from it.
16
+ */
17
+ import { type DefaultAgentSources, type ExportFlags } from "../lib/agent-export.js";
18
+ export interface AgentExportDeps {
19
+ /** id-or-name → agent id. Exits itself when unresolvable. */
20
+ resolveAgentId: (ref: string) => Promise<string>;
21
+ /** Where the agent comes from when no `[id-or-name]` is given. */
22
+ agentSources: DefaultAgentSources;
23
+ fetchExport: (agentId: string, query: string) => Promise<{
24
+ body: Uint8Array;
25
+ headers: Headers;
26
+ }>;
27
+ /** Write owner-only (0600). Without `force`, throws EEXIST when the file exists. */
28
+ writeFile: (path: string, bytes: Uint8Array, force: boolean) => void;
29
+ writeStdout: (bytes: Uint8Array) => void;
30
+ out: (line: string) => void;
31
+ err: (line: string) => void;
32
+ now: () => Date;
33
+ apiBase: () => string;
34
+ websiteBase: () => string;
35
+ }
36
+ export interface PrivateFileOps {
37
+ chmodSync: (path: string, mode: number) => void;
38
+ writeFileSync: (path: string, bytes: Uint8Array, opts: {
39
+ mode: number;
40
+ flag: string;
41
+ }) => void;
42
+ }
43
+ /**
44
+ * Write `bytes` to `path` readable by the owner only. Without `force`, `wx`
45
+ * makes the no-overwrite check and the create one step. With `force`, an
46
+ * existing file is narrowed to 0600 BEFORE the write (so the new contents are
47
+ * never readable under its old mode) — chmod-and-catch-ENOENT rather than
48
+ * exists-then-chmod — and chmod'ed again after, since `writeFileSync` only
49
+ * applies `mode` to a file it creates.
50
+ */
51
+ export declare function writePrivateFile(path: string, bytes: Uint8Array, force: boolean, ops?: PrivateFileOps): void;
52
+ export declare const liveExportDeps: AgentExportDeps;
53
+ export declare function agentExportCommand(ref: string | undefined, flags: ExportFlags, deps: AgentExportDeps): Promise<number>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * `mnemom agent export [id-or-name]` — download an agent's full record
3
+ * (`GET /v1/agents/{id}/compliance-export`) to a file, or to stdout with `-o -`.
4
+ *
5
+ * Without `[id-or-name]` the agent is, in order: MNEMOM_AGENT, the saved
6
+ * `agent` in ~/.mnemom/launcher.toml, then the agent of the newest launch in the
7
+ * session ledger (lib/agent-export.ts pickAgentRef).
8
+ *
9
+ * Pure apart from the injected deps (`resolveAgentId`, `agentSources`,
10
+ * `fetchExport`, `writeFile`, `writeStdout`, `out`, `err`, `now`), so every arm is
11
+ * unit-tested; `liveExportDeps` is the thin driver index.ts wires in. The
12
+ * parsing, counts, summary and error wording live in lib/agent-export.ts.
13
+ *
14
+ * Returns the process exit code (0 ok, 1 on any error) instead of exiting, so
15
+ * index.ts sets `process.exitCode` from it.
16
+ */
17
+ import { chmodSync, writeFileSync } from "node:fs";
18
+ import { readSessionRecords } from "../lib/agent-ledger.js";
19
+ import { getComplianceExport, resolveAgentId } from "../lib/api.js";
20
+ import { getApiUrl, getWebsiteUrl, MNEMOM_DIR } from "../lib/config.js";
21
+ import { loadLauncherConfig } from "../lib/launcher-config.js";
22
+ import { buildExportQuery, buildSummary, defaultOutputPath, exportErrorMessage, extractCounts, filenameLabel, formatSummaryLines, NO_AGENT_MESSAGE, parseExportFlags, pickAgentRef, sensitivityNote, writeErrorMessage, } from "../lib/agent-export.js";
23
+ import { fmt } from "../lib/format.js";
24
+ /**
25
+ * Write `bytes` to `path` readable by the owner only. Without `force`, `wx`
26
+ * makes the no-overwrite check and the create one step. With `force`, an
27
+ * existing file is narrowed to 0600 BEFORE the write (so the new contents are
28
+ * never readable under its old mode) — chmod-and-catch-ENOENT rather than
29
+ * exists-then-chmod — and chmod'ed again after, since `writeFileSync` only
30
+ * applies `mode` to a file it creates.
31
+ */
32
+ export function writePrivateFile(path, bytes, force, ops = { chmodSync, writeFileSync }) {
33
+ if (force) {
34
+ try {
35
+ ops.chmodSync(path, 0o600);
36
+ }
37
+ catch (error) {
38
+ if (error.code !== "ENOENT")
39
+ throw error;
40
+ }
41
+ }
42
+ ops.writeFileSync(path, bytes, { mode: 0o600, flag: force ? "w" : "wx" });
43
+ ops.chmodSync(path, 0o600);
44
+ }
45
+ export const liveExportDeps = {
46
+ resolveAgentId: (ref) => resolveAgentId(ref),
47
+ agentSources: {
48
+ env: () => process.env.MNEMOM_AGENT,
49
+ configAgent: () => loadLauncherConfig().agent,
50
+ // The ledger is oldest-first on disk.
51
+ ledger: () => readSessionRecords({ dir: MNEMOM_DIR }).reverse(),
52
+ },
53
+ fetchExport: (agentId, query) => getComplianceExport(agentId, query),
54
+ writeFile: (path, bytes, force) => writePrivateFile(path, bytes, force),
55
+ writeStdout: (bytes) => {
56
+ process.stdout.write(bytes);
57
+ },
58
+ out: (line) => console.log(line),
59
+ err: (line) => console.error(line),
60
+ now: () => new Date(),
61
+ apiBase: getApiUrl,
62
+ websiteBase: getWebsiteUrl,
63
+ };
64
+ export async function agentExportCommand(ref, flags, deps) {
65
+ const now = deps.now();
66
+ let parsed;
67
+ try {
68
+ parsed = parseExportFlags(flags, now);
69
+ }
70
+ catch (error) {
71
+ deps.err(fmt.error(error.message));
72
+ return 1;
73
+ }
74
+ let target;
75
+ try {
76
+ target = pickAgentRef(ref, deps.agentSources);
77
+ }
78
+ catch (error) {
79
+ // A malformed ~/.mnemom/launcher.toml — its message names the file to fix.
80
+ deps.err(fmt.error(error.message));
81
+ return 1;
82
+ }
83
+ if (target === null) {
84
+ deps.err(fmt.error(NO_AGENT_MESSAGE));
85
+ return 1;
86
+ }
87
+ const agentId = await deps.resolveAgentId(target.ref);
88
+ const output = parsed.output ?? defaultOutputPath(filenameLabel(target.label, agentId), parsed.format, now);
89
+ const toStdout = output === "-";
90
+ let response;
91
+ try {
92
+ response = await deps.fetchExport(agentId, buildExportQuery(parsed.format, parsed.period));
93
+ }
94
+ catch (error) {
95
+ deps.err(fmt.error(exportErrorMessage(error, agentId)));
96
+ return 1;
97
+ }
98
+ try {
99
+ if (toStdout)
100
+ deps.writeStdout(response.body);
101
+ else
102
+ deps.writeFile(output, response.body, parsed.force);
103
+ }
104
+ catch (error) {
105
+ deps.err(fmt.error(writeErrorMessage(error, output)));
106
+ return 1;
107
+ }
108
+ const summary = buildSummary({
109
+ agentId,
110
+ output,
111
+ format: parsed.format,
112
+ period: parsed.period,
113
+ body: response.body,
114
+ counts: extractCounts(parsed.format, response.body, response.headers),
115
+ websiteBase: deps.websiteBase(),
116
+ apiBase: deps.apiBase(),
117
+ });
118
+ // With `-o -` stdout carries the record itself, so the summary goes to stderr.
119
+ const say = toStdout ? deps.err : deps.out;
120
+ if (parsed.json)
121
+ say(JSON.stringify(summary, null, 2));
122
+ else
123
+ for (const line of formatSummaryLines(summary))
124
+ say(line);
125
+ deps.err(fmt.dim(sensitivityNote(toStdout)));
126
+ return 0;
127
+ }
@@ -110,11 +110,13 @@ export declare function formatTrailEntry(e: AgentSessionVerdictTrailEntry): stri
110
110
  */
111
111
  export declare function verdictTrailKey(e: Pick<AgentSessionVerdictTrailEntry, "request_id" | "turn">): string;
112
112
  /**
113
- * Best-effort "has this session ended" check for `--follow`'s exit condition. No field is locked
114
- * in on the wire yet: MNE-8149 adds an `expired` seal state on the gateway's row, and a later
115
- * mnemom-api card is expected to carry it through to this response — so this reads defensively,
116
- * off either the top-level response or the `sealed` block, and returns null (never throws) when
117
- * neither carries a recognized value, which is every server today.
113
+ * Best-effort "has this session ended" check for `--follow`'s exit condition. MNE-8234 wired the
114
+ * seal lifecycle through on the wire (`remote.seal.state`), so that is read first and is
115
+ * authoritative: `"expired"` there means the prior seal lapsed and ends the watch. The older
116
+ * top-level `state` and `remote.sealed.state` fallbacks predate that field actually landing on
117
+ * the response — neither has ever been observed non-null on any real server — and are kept
118
+ * defensively in case an even older/non-conformant server ever carries one of them; this never
119
+ * throws, returning null when nothing recognized is present.
118
120
  */
119
121
  export declare function sessionEndedState(remote: AgentSessionResponse): "expired" | "ended" | null;
120
122
  /** Default and floor for `--follow`'s poll interval. */
@@ -152,6 +154,14 @@ export interface ShowJson {
152
154
  guardrails: AgentSessionResponse["guardrails"];
153
155
  /** R10 (MNE-8200): oldest first (the gateway's own wire order), unlike the human view. */
154
156
  verdict_trail: AgentSessionResponse["verdict_trail"];
157
+ /**
158
+ * MNE-8234: the seal's lifecycle (state incl. `expired`, version, mode, hashes). Absent on
159
+ * an older server; `null` when the API reports no seal.
160
+ */
161
+ seal: AgentSessionResponse["seal"];
162
+ /** MNE-8234 (MNE-8166 R4): judging counts under the current seal. */
163
+ unjudged_turns: AgentSessionResponse["unjudged_turns"];
164
+ memoised_turns: AgentSessionResponse["memoised_turns"];
155
165
  } | {
156
166
  source: "gateway";
157
167
  available: false;
@@ -190,14 +190,21 @@ export function verdictTrailKey(e) {
190
190
  return `${e.request_id ?? ""}#${e.turn}`;
191
191
  }
192
192
  /**
193
- * Best-effort "has this session ended" check for `--follow`'s exit condition. No field is locked
194
- * in on the wire yet: MNE-8149 adds an `expired` seal state on the gateway's row, and a later
195
- * mnemom-api card is expected to carry it through to this response — so this reads defensively,
196
- * off either the top-level response or the `sealed` block, and returns null (never throws) when
197
- * neither carries a recognized value, which is every server today.
193
+ * Best-effort "has this session ended" check for `--follow`'s exit condition. MNE-8234 wired the
194
+ * seal lifecycle through on the wire (`remote.seal.state`), so that is read first and is
195
+ * authoritative: `"expired"` there means the prior seal lapsed and ends the watch. The older
196
+ * top-level `state` and `remote.sealed.state` fallbacks predate that field actually landing on
197
+ * the response — neither has ever been observed non-null on any real server — and are kept
198
+ * defensively in case an even older/non-conformant server ever carries one of them; this never
199
+ * throws, returning null when nothing recognized is present.
198
200
  */
199
201
  export function sessionEndedState(remote) {
200
202
  const as = (v) => v === "expired" || v === "ended" ? v : null;
203
+ if (remote.seal) {
204
+ const sealState = as(remote.seal.state);
205
+ if (sealState)
206
+ return sealState;
207
+ }
201
208
  const top = as(remote.state);
202
209
  if (top)
203
210
  return top;
@@ -349,6 +356,12 @@ export function buildShowView(local, remote, reason) {
349
356
  else {
350
357
  L.push(fmt.dim(" no sealed contract on the gateway for this conversation"));
351
358
  }
359
+ // MNE-8234: the seal's lifecycle — independent of `sealed` above (which carries the contract
360
+ // body, not whether the seal itself is still live or has lapsed). Absent on an older server.
361
+ if (remote.seal) {
362
+ const modeBit = remote.seal.state === "live" ? ` · mode ${remote.seal.mode}` : "";
363
+ L.push(fmt.label("seal: ", `${remote.seal.state.toUpperCase()} (v${remote.seal.version})${modeBit}`));
364
+ }
352
365
  if (remote.verdict) {
353
366
  const v = remote.verdict;
354
367
  L.push(fmt.label("verdict: ", `${v.verdict.toUpperCase()} at turn ${v.turn} — ${v.reason}`));
@@ -385,6 +398,14 @@ export function buildShowView(local, remote, reason) {
385
398
  if (remote.turn !== null) {
386
399
  L.push(fmt.label("turn: ", String(remote.turn)));
387
400
  }
401
+ // MNE-8234 (MNE-8166 R4): judging counts under the current seal. `null`/absent ⇒ no judge
402
+ // ledger counts (older server, or nothing judged under this seal yet) — no line then.
403
+ if (remote.unjudged_turns !== null && remote.unjudged_turns !== undefined) {
404
+ const memoisedBit = remote.memoised_turns !== null && remote.memoised_turns !== undefined
405
+ ? ` · memoised ${remote.memoised_turns}`
406
+ : "";
407
+ L.push(fmt.label("judging: ", `unjudged ${remote.unjudged_turns}${memoisedBit}`));
408
+ }
388
409
  return L;
389
410
  }
390
411
  export function buildShowJson(local, remote, reason) {
@@ -400,6 +421,9 @@ export function buildShowJson(local, remote, reason) {
400
421
  turn: remote.turn,
401
422
  guardrails: remote.guardrails,
402
423
  verdict_trail: remote.verdict_trail,
424
+ seal: remote.seal,
425
+ unjudged_turns: remote.unjudged_turns,
426
+ memoised_turns: remote.memoised_turns,
403
427
  }
404
428
  : { source: "gateway", available: false, reason: reason ?? "unknown" },
405
429
  };
@@ -127,7 +127,7 @@ export interface AgentOptions {
127
127
  /**
128
128
  * Load the "Mnemom for Claude Code" plugin (`plugins/mnemom`: the Mnemom band,
129
129
  * `/mnemom`, the gateway guard, Mnemom attribution) into the launched Claude Code.
130
- * Default OFF (not published yet); `--plugin` ⇒ true; `--no-plugin` ⇒ false; undefined ⇒
130
+ * Default OFF (the plugin lands dark); `--plugin` ⇒ true; `--no-plugin` ⇒ false; undefined ⇒
131
131
  * MNEMOM_AGENT_PLUGIN decides (lib/claude-plugin).
132
132
  */
133
133
  plugin?: boolean;
@@ -26,7 +26,7 @@
26
26
  * an argv value.
27
27
  */
28
28
  import { spawn, spawnSync } from "node:child_process";
29
- import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
29
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
30
30
  import { randomUUID } from "node:crypto";
31
31
  import { tmpdir, homedir, hostname } from "node:os";
32
32
  import { basename, join, dirname } from "node:path";
@@ -539,7 +539,7 @@ export async function agentCommand(scenarioArg, options = {}) {
539
539
  plugin: launchPlugin.line,
540
540
  });
541
541
  if (pluginWanted && cli.name === "claude" && !bundledPluginDir) {
542
- say(fmt.warn("mnemom agent: the Mnemom plugin folder is not in this install (it is not published yet; run from a source checkout) — launching without the Mnemom band and /mnemom."));
542
+ say(fmt.warn("mnemom agent: the Mnemom plugin folder is not in this install — launching without the Mnemom band and /mnemom."));
543
543
  }
544
544
  // Non-secret launch facts for the plugin's band and `/mnemom status`. Built here, applied to the
545
545
  // child env by each launch shape AFTER it has built that env (lib/claude-plugin, PLUGIN_ENV).
@@ -1303,7 +1303,8 @@ function launchRemoteControl(ctx) {
1303
1303
  }
1304
1304
  }
1305
1305
  const proxyScript = resolveProxyScript();
1306
- const rcDir = mkdtempSync(join(tmpdir(), "mnemom-code-rc."));
1306
+ const rcDir = mkdtempSync(join(rcStateRoot(), "mnemom-code-rc."));
1307
+ writeFileSync(join(rcDir, "owner.pid"), `${process.pid}\n`, { mode: 0o600 }); // rc-state.mjs sweep
1307
1308
  const proxyLog = process.env.MNEMOM_AGENT_RC_PROXY_LOG || join(rcDir, "proxy.log");
1308
1309
  const proxy = spawn("node", [proxyScript], {
1309
1310
  env: {
@@ -1519,6 +1520,23 @@ function warnServerDrops(options, yolo) {
1519
1520
  say("mnemom agent: yolo (--dangerously-skip-permissions) is not applied under --server; set permissions from the driven sessions.");
1520
1521
  }
1521
1522
  }
1523
+ /**
1524
+ * ~/.mnemom/rc, created 0700 — the same root src/rc-state.mjs gives the proxy's CA dir (tsc emits
1525
+ * no .mjs, so this mirrors its rcStateRoot() rather than importing it). Never the OS temp dir,
1526
+ * whose cleaners delete files behind a long session; that is only the fallback when home is not
1527
+ * writable. Read at call time, not from MNEMOM_DIR: HOME can move after import (tests do).
1528
+ */
1529
+ function rcStateRoot() {
1530
+ const root = join(homedir(), ".mnemom", "rc");
1531
+ try {
1532
+ mkdirSync(root, { recursive: true, mode: 0o700 });
1533
+ chmodSync(root, 0o700);
1534
+ return root;
1535
+ }
1536
+ catch {
1537
+ return tmpdir();
1538
+ }
1539
+ }
1522
1540
  function resolveProxyScript() {
1523
1541
  const here = dirname(fileURLToPath(import.meta.url));
1524
1542
  const cand = join(here, "..", "rc-proxy.mjs");
@@ -3,7 +3,7 @@
3
3
  * integration). Surface for engineers to check whether staging is safe
4
4
  * to promote to production based on the most recent harness run state.
5
5
  *
6
- * mnemom validate safe-house [--against=staging] [--max-age=24h]
6
+ * mnemom validate safe-house [--against=test] [--max-age=24h]
7
7
  * [--lane=full|fast] [--strict] [--json]
8
8
  *
9
9
  * Wraps `GET /v1/admin/safe-house/harness-state` (mnemom-staff only).
@@ -3,7 +3,7 @@
3
3
  * integration). Surface for engineers to check whether staging is safe
4
4
  * to promote to production based on the most recent harness run state.
5
5
  *
6
- * mnemom validate safe-house [--against=staging] [--max-age=24h]
6
+ * mnemom validate safe-house [--against=test] [--max-age=24h]
7
7
  * [--lane=full|fast] [--strict] [--json]
8
8
  *
9
9
  * Wraps `GET /v1/admin/safe-house/harness-state` (mnemom-staff only).
@@ -76,10 +76,12 @@ export async function validateSafeHouseCommand(opts) {
76
76
  await requireAuth();
77
77
  // `--against` is informational only today (validation is always against
78
78
  // the api root the user is authenticated to). Keep the flag for forward-
79
- // compat: production gains a separate harness corpus per T1.
80
- const against = opts.against ?? "staging";
81
- if (against !== "staging" && against !== "production") {
82
- console.log(fmt.error(`--against must be 'staging' or 'production' (got '${against}')`) + "\n");
79
+ // compat: production gains a separate harness corpus per T1. The non-prod
80
+ // value is `test` (the Azure test cell): `staging` named the retired ring
81
+ // (MNE-7968) and is rejected so a stale script fails loud, not quietly.
82
+ const against = opts.against ?? "test";
83
+ if (against !== "test" && against !== "production") {
84
+ console.log(fmt.error(`--against must be 'test' or 'production' (got '${against}')`) + "\n");
83
85
  process.exit(1);
84
86
  return;
85
87
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./lib/retired-env-guard.js";
2
3
  import { program } from "commander";
3
4
  export { program };
4
5
  /**
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ // MUST stay the first import: it exits cleanly on MNEMOM_ENV=staging before any
3
+ // module below resolves the (retired) staging URLs at import time (MNE-7968).
4
+ import "./lib/retired-env-guard.js";
2
5
  import { program, InvalidArgumentError, Option } from "commander";
3
6
  import { isEntrypoint } from "./lib/entrypoint.js";
4
7
  import { CLI_VERSION } from "./version.js";
@@ -10,6 +13,7 @@ import { cardShowCommand, cardPublishCommand, cardValidateCommand, cardEditComma
10
13
  import { policyInitCommand, policyValidateCommand, policyPublishCommand, policyListCommand, policyTestCommand, policyEvaluateCommand, } from "./commands/policy.js";
11
14
  import { protectionShowCommand, protectionPublishCommand, protectionValidateCommand, protectionEditCommand, protectionDriftCommand, } from "./commands/protection.js";
12
15
  import { agentsListCommand, agentsClaimCommand, agentsMoveCommand } from "./commands/agents.js";
16
+ import { agentExportCommand, liveExportDeps } from "./commands/agent-export.js";
13
17
  import { agentSessionsCommand, agentShowCommand, liveSessionDeps, } from "./commands/agent-sessions.js";
14
18
  import { agentGoalPinCommand, agentGoalResetCommand, agentGoalShowCommand, liveGoalDeps, } from "./commands/agent-goal.js";
15
19
  import { orgListCommand, orgShowCommand, orgUseCommand } from "./commands/org.js";
@@ -141,7 +145,7 @@ function printAgentHelp(verb) {
141
145
  const agentCmd = program
142
146
  .command("agent [session] [passthrough...]")
143
147
  .description("Launch your governed agent (Claude Code in v1) through the Mnemom gateway — governed identity + optional sealed goal contract. Plain `mnemom agent` asks for a session name. " +
144
- "Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `invite <token>` redeems a Mnemom Agent invitation; `list` / `claim` / `move` manage the agents in your orgs; `sessions` lists your past launches and `show [ref]` merges one with its live goal state; `help [verb]` prints help and never launches.")
148
+ "Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `--invite <token>` redeems a Mnemom Agent invitation (the one-liner your invitation email shows; `invite <token>` is its alias); `list` / `claim` / `move` manage the agents in your orgs; `export` saves an agent's full record to a file; `sessions` lists your past launches and `show [ref]` merges one with its live goal state; `help [verb]` prints help and never launches.")
145
149
  .option("--goal <statement>", "Contract goal statement (required if any contract flag is given)")
146
150
  .option("--requirement <text>", "Contract requirement (repeatable)", collectRepeatable, [])
147
151
  .option("--allow <glob>", "Allowed path/glob (repeatable)", collectRepeatable, [])
@@ -872,6 +876,20 @@ agentCmd
872
876
  process.exit(1);
873
877
  }
874
878
  });
879
+ agentCmd
880
+ .command("export [id-or-name]")
881
+ .description("Save an agent's full record (checkpoints, score history, corrections) to a file. " +
882
+ "Without id-or-name: MNEMOM_AGENT, else the saved agent (mnemom agent config), else the agent of your latest launch")
883
+ .option("--format <json|csv>", "File format (default: json)")
884
+ .option("--since <Nd|Nh>", "Only the last N days or hours (instead of --from)")
885
+ .option("--from <date>", "Start of the period (ISO-8601 date or datetime)")
886
+ .option("--to <date>", "End of the period (ISO-8601 date or datetime; a date includes that day)")
887
+ .option("-o, --output <file|->", "Where to write it; - for stdout (default: ./mnemom-record-<agent>-<date>.<format>)")
888
+ .option("--force", "Overwrite the output file if it exists")
889
+ .option("--json", "Print the summary as JSON")
890
+ .action(async (idOrName, opts) => {
891
+ process.exitCode = await agentExportCommand(idOrName, opts, liveExportDeps);
892
+ });
875
893
  agentCmd
876
894
  .command("sessions")
877
895
  .description("List your Mnemom Agent sessions from the local ledger (~/.mnemom/sessions.jsonl), newest first — the last 20 unless --all")
@@ -1656,7 +1674,7 @@ const validateCmd = program
1656
1674
  validateCmd
1657
1675
  .command("safe-house")
1658
1676
  .description("Render Safe House harness state per lane (full/fast) — promotion-readiness signal")
1659
- .option("--against <env>", "Target environment ('staging' or 'production'; informational)", "staging")
1677
+ .option("--against <env>", "Target environment ('test' or 'production'; informational)", "test")
1660
1678
  .option("--max-age <duration>", "Reject runs older than this (e.g. 30m, 2h, 24h, 1d)", "24h")
1661
1679
  .option("--lane <lane>", "Show only one lane ('full', 'fast', or 'both')", "both")
1662
1680
  .option("--strict", "Exit non-zero if any selected lane is stale or red")
@@ -0,0 +1,133 @@
1
+ /**
2
+ * `mnemom agent export` — the pure half: option parsing (format, --since /
3
+ * --from / --to), the default output filename, the per-section counts (from the
4
+ * JSON body / the CSV `X-Mnemom-Export-Counts` header), the summary, and the
5
+ * error-message mapping. No I/O here; the command driver
6
+ * (commands/agent-export.ts) does the network and filesystem work, so every
7
+ * branch below is unit-tested directly.
8
+ *
9
+ * Server side: `GET /v1/agents/{id}/compliance-export?format=json|csv&from=&to=`
10
+ * (mnemom-api reclassification/handlers.ts handleComplianceExport). `from`/`to`
11
+ * are passed straight into PostgREST `gte.`/`lte.` filters, so they are
12
+ * normalised here to full UTC ISO timestamps.
13
+ */
14
+ import type { SessionRecord } from "./agent-ledger.js";
15
+ export type ExportFormat = "json" | "csv";
16
+ /** The record's sections, in the order the summary prints them. */
17
+ export declare const EXPORT_SECTIONS: readonly ["checkpoints", "score_history", "reclassifications", "card_amendments", "disagreement_reviews", "policy_evaluations_with_gaps"];
18
+ export type ExportSection = (typeof EXPORT_SECTIONS)[number];
19
+ /** Rows per section; null = the server didn't say (and it couldn't be derived). */
20
+ export type ExportCounts = Record<ExportSection, number | null>;
21
+ export declare const COUNTS_HEADER = "X-Mnemom-Export-Counts";
22
+ /** A bad flag value — reported as-is, before any network call. */
23
+ export declare class ExportUsageError extends Error {
24
+ constructor(message: string);
25
+ }
26
+ export interface ExportFlags {
27
+ format?: string;
28
+ since?: string;
29
+ from?: string;
30
+ to?: string;
31
+ output?: string;
32
+ force?: boolean;
33
+ json?: boolean;
34
+ }
35
+ export interface ExportPeriod {
36
+ from: string | null;
37
+ to: string | null;
38
+ }
39
+ export interface ParsedExportFlags {
40
+ format: ExportFormat;
41
+ period: ExportPeriod;
42
+ /** The -o value as given; undefined = use the default filename. "-" = stdout. */
43
+ output?: string;
44
+ force: boolean;
45
+ json: boolean;
46
+ }
47
+ export declare function parseFormat(raw: string | undefined): ExportFormat;
48
+ /** `--since 7d` / `--since 12h` → the UTC ISO timestamp that long before `now`. */
49
+ export declare function parseSince(raw: string, now: Date): string;
50
+ /**
51
+ * Validate an ISO-8601 date or datetime for `--from` / `--to` and normalise it to
52
+ * a UTC ISO timestamp. A bare date is a UTC day: `--from` starts at its first
53
+ * millisecond and `--to` includes the whole day (ends at its last millisecond).
54
+ * A datetime with no offset is read in local time, like `new Date()` does.
55
+ */
56
+ export declare function parseDateBound(raw: string, flag: "--from" | "--to"): string;
57
+ /** Resolve `--since` / `--from` / `--to` into the period sent to the server. */
58
+ export declare function resolvePeriod(flags: ExportFlags, now: Date): ExportPeriod;
59
+ /** All the flag validation, done locally before any network call. */
60
+ export declare function parseExportFlags(flags: ExportFlags, now: Date): ParsedExportFlags;
61
+ /** `?format=…&from=…&to=…` for the compliance-export route. */
62
+ export declare function buildExportQuery(format: ExportFormat, period: ExportPeriod): string;
63
+ export declare const NO_AGENT_MESSAGE = "No agent given. Pass one: mnemom agent export <id-or-name> (see mnemom agent list)";
64
+ /** What to resolve (`ref`, an id or name) and what to name the default file after (`label`). */
65
+ export interface AgentRef {
66
+ ref: string;
67
+ label: string;
68
+ }
69
+ /** Where the no-argument default comes from; each is read only if the ones before it are empty. */
70
+ export interface DefaultAgentSources {
71
+ /** MNEMOM_AGENT. */
72
+ env: () => string | undefined;
73
+ /** The saved `agent` in ~/.mnemom/launcher.toml (throws on a malformed file). */
74
+ configAgent: () => string | undefined;
75
+ /** The session ledger, newest launch first. */
76
+ ledger: () => SessionRecord[];
77
+ }
78
+ /**
79
+ * The agent to export: the positional `[id-or-name]` when given; else, in
80
+ * order, MNEMOM_AGENT, the saved launcher-config `agent`, and the agent of the
81
+ * newest launch in the session ledger that resolved one (resolved by id, named
82
+ * after its slug). null when none of them has an agent.
83
+ */
84
+ export declare function pickAgentRef(given: string | undefined, sources: DefaultAgentSources): AgentRef | null;
85
+ /** The filename-safe agent label: `label` sanitised; the resolved id when that leaves nothing. */
86
+ export declare function filenameLabel(label: string, agentId: string): string;
87
+ /** `./mnemom-record-<label>-<YYYY-MM-DD>.<json|csv>` (UTC date). */
88
+ export declare function defaultOutputPath(label: string, format: ExportFormat, now: Date): string;
89
+ /**
90
+ * Counts from a JSON export body: the server's `counts` object where it has a
91
+ * section, else the length of that section's array. null when the body is not
92
+ * a JSON object.
93
+ */
94
+ export declare function countsFromJsonBody(text: string): ExportCounts | null;
95
+ export declare function countsFromHeader(header: string | null): ExportCounts | null;
96
+ export declare function extractCounts(format: ExportFormat, body: Uint8Array, headers: Headers): ExportCounts | null;
97
+ export declare function sha256Hex(bytes: Uint8Array): string;
98
+ export interface VerificationLinks {
99
+ reputation_url: string;
100
+ verify_url: string;
101
+ }
102
+ export declare function verificationLinks(agentId: string, websiteBase: string, apiBase: string): VerificationLinks;
103
+ export interface ExportSummary {
104
+ agent_id: string;
105
+ /** The file written, or "-" for stdout. */
106
+ output: string;
107
+ format: ExportFormat;
108
+ period: ExportPeriod;
109
+ counts: ExportCounts | null;
110
+ bytes: number;
111
+ /** SHA-256 of the exact bytes written — a file checksum, not a signature. */
112
+ sha256: string;
113
+ verification: VerificationLinks;
114
+ }
115
+ export declare function buildSummary(input: {
116
+ agentId: string;
117
+ output: string;
118
+ format: ExportFormat;
119
+ period: ExportPeriod;
120
+ body: Uint8Array;
121
+ counts: ExportCounts | null;
122
+ websiteBase: string;
123
+ apiBase: string;
124
+ }): ExportSummary;
125
+ export declare function describePeriod(period: ExportPeriod): string;
126
+ /** The human summary, one line per entry (no colour; the driver adds none). */
127
+ export declare function formatSummaryLines(summary: ExportSummary): string[];
128
+ /** The one-line sensitivity note (the driver prints it dim, on stderr). */
129
+ export declare function sensitivityNote(toStdout: boolean): string;
130
+ /** The user-facing message for a failed export request. */
131
+ export declare function exportErrorMessage(error: unknown, agentId: string): string;
132
+ /** The user-facing message for a failed file write. */
133
+ export declare function writeErrorMessage(error: unknown, path: string): string;