@mnemom/mnemom 0.17.21 → 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 +26 -0
- package/dist/commands/agent-export.d.ts +53 -0
- package/dist/commands/agent-export.js +127 -0
- package/dist/index.js +16 -1
- package/dist/lib/agent-export.d.ts +133 -0
- package/dist/lib/agent-export.js +293 -0
- package/dist/lib/api.d.ts +12 -0
- package/dist/lib/api.js +17 -0
- package/package.json +1 -1
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
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { cardShowCommand, cardPublishCommand, cardValidateCommand, cardEditComma
|
|
|
13
13
|
import { policyInitCommand, policyValidateCommand, policyPublishCommand, policyListCommand, policyTestCommand, policyEvaluateCommand, } from "./commands/policy.js";
|
|
14
14
|
import { protectionShowCommand, protectionPublishCommand, protectionValidateCommand, protectionEditCommand, protectionDriftCommand, } from "./commands/protection.js";
|
|
15
15
|
import { agentsListCommand, agentsClaimCommand, agentsMoveCommand } from "./commands/agents.js";
|
|
16
|
+
import { agentExportCommand, liveExportDeps } from "./commands/agent-export.js";
|
|
16
17
|
import { agentSessionsCommand, agentShowCommand, liveSessionDeps, } from "./commands/agent-sessions.js";
|
|
17
18
|
import { agentGoalPinCommand, agentGoalResetCommand, agentGoalShowCommand, liveGoalDeps, } from "./commands/agent-goal.js";
|
|
18
19
|
import { orgListCommand, orgShowCommand, orgUseCommand } from "./commands/org.js";
|
|
@@ -144,7 +145,7 @@ function printAgentHelp(verb) {
|
|
|
144
145
|
const agentCmd = program
|
|
145
146
|
.command("agent [session] [passthrough...]")
|
|
146
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. " +
|
|
147
|
-
"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; `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.")
|
|
148
149
|
.option("--goal <statement>", "Contract goal statement (required if any contract flag is given)")
|
|
149
150
|
.option("--requirement <text>", "Contract requirement (repeatable)", collectRepeatable, [])
|
|
150
151
|
.option("--allow <glob>", "Allowed path/glob (repeatable)", collectRepeatable, [])
|
|
@@ -875,6 +876,20 @@ agentCmd
|
|
|
875
876
|
process.exit(1);
|
|
876
877
|
}
|
|
877
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
|
+
});
|
|
878
893
|
agentCmd
|
|
879
894
|
.command("sessions")
|
|
880
895
|
.description("List your Mnemom Agent sessions from the local ledger (~/.mnemom/sessions.jsonl), newest first — the last 20 unless --all")
|
|
@@ -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;
|
|
@@ -0,0 +1,293 @@
|
|
|
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 { createHash } from "node:crypto";
|
|
15
|
+
import { sanitizeAgentName } from "./agent.js";
|
|
16
|
+
import { MnemomApiError } from "./api.js";
|
|
17
|
+
/** The record's sections, in the order the summary prints them. */
|
|
18
|
+
export const EXPORT_SECTIONS = [
|
|
19
|
+
"checkpoints",
|
|
20
|
+
"score_history",
|
|
21
|
+
"reclassifications",
|
|
22
|
+
"card_amendments",
|
|
23
|
+
"disagreement_reviews",
|
|
24
|
+
"policy_evaluations_with_gaps",
|
|
25
|
+
];
|
|
26
|
+
export const COUNTS_HEADER = "X-Mnemom-Export-Counts";
|
|
27
|
+
/** A bad flag value — reported as-is, before any network call. */
|
|
28
|
+
export class ExportUsageError extends Error {
|
|
29
|
+
constructor(message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "ExportUsageError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// ─── option parsing ───────────────────────────────────────────────────────────
|
|
35
|
+
export function parseFormat(raw) {
|
|
36
|
+
const v = (raw ?? "json").trim().toLowerCase();
|
|
37
|
+
if (v === "json" || v === "csv")
|
|
38
|
+
return v;
|
|
39
|
+
throw new ExportUsageError(`--format must be json or csv (got "${raw}").`);
|
|
40
|
+
}
|
|
41
|
+
const SINCE_RE = /^(\d+)([dh])$/i;
|
|
42
|
+
const HOUR_MS = 3_600_000;
|
|
43
|
+
/** `--since 7d` / `--since 12h` → the UTC ISO timestamp that long before `now`. */
|
|
44
|
+
export function parseSince(raw, now) {
|
|
45
|
+
const m = SINCE_RE.exec(raw.trim());
|
|
46
|
+
const n = m ? Number(m[1]) : 0;
|
|
47
|
+
if (!m || n <= 0) {
|
|
48
|
+
throw new ExportUsageError(`--since must be a positive number of days or hours, like 7d or 12h (got "${raw}").`);
|
|
49
|
+
}
|
|
50
|
+
const unitMs = m[2].toLowerCase() === "d" ? 24 * HOUR_MS : HOUR_MS;
|
|
51
|
+
return new Date(now.getTime() - n * unitMs).toISOString();
|
|
52
|
+
}
|
|
53
|
+
const DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
54
|
+
const DATETIME_RE = /^(\d{4})-(\d{2})-(\d{2})T\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})?$/i;
|
|
55
|
+
/** True when y-m-d is a real calendar day (rejects 2026-02-30, month 13, …). */
|
|
56
|
+
function isCalendarDate(y, m, d) {
|
|
57
|
+
const date = new Date(Date.UTC(Number(y), Number(m) - 1, Number(d)));
|
|
58
|
+
return (date.getUTCFullYear() === Number(y) &&
|
|
59
|
+
date.getUTCMonth() === Number(m) - 1 &&
|
|
60
|
+
date.getUTCDate() === Number(d));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validate an ISO-8601 date or datetime for `--from` / `--to` and normalise it to
|
|
64
|
+
* a UTC ISO timestamp. A bare date is a UTC day: `--from` starts at its first
|
|
65
|
+
* millisecond and `--to` includes the whole day (ends at its last millisecond).
|
|
66
|
+
* A datetime with no offset is read in local time, like `new Date()` does.
|
|
67
|
+
*/
|
|
68
|
+
export function parseDateBound(raw, flag) {
|
|
69
|
+
const v = raw.trim();
|
|
70
|
+
const dateOnly = DATE_RE.exec(v);
|
|
71
|
+
const parts = dateOnly ?? DATETIME_RE.exec(v);
|
|
72
|
+
const ms = parts ? Date.parse(v) : NaN;
|
|
73
|
+
if (!parts || Number.isNaN(ms) || !isCalendarDate(parts[1], parts[2], parts[3])) {
|
|
74
|
+
throw new ExportUsageError(`${flag} must be an ISO-8601 date or datetime, like 2026-09-01 or 2026-09-01T09:30:00Z (got "${raw}").`);
|
|
75
|
+
}
|
|
76
|
+
if (dateOnly && flag === "--to")
|
|
77
|
+
return `${v}T23:59:59.999Z`;
|
|
78
|
+
return new Date(ms).toISOString();
|
|
79
|
+
}
|
|
80
|
+
/** Resolve `--since` / `--from` / `--to` into the period sent to the server. */
|
|
81
|
+
export function resolvePeriod(flags, now) {
|
|
82
|
+
if (flags.since !== undefined && flags.from !== undefined) {
|
|
83
|
+
throw new ExportUsageError("Use either --since or --from, not both.");
|
|
84
|
+
}
|
|
85
|
+
const from = flags.since !== undefined
|
|
86
|
+
? parseSince(flags.since, now)
|
|
87
|
+
: flags.from !== undefined
|
|
88
|
+
? parseDateBound(flags.from, "--from")
|
|
89
|
+
: null;
|
|
90
|
+
const to = flags.to !== undefined ? parseDateBound(flags.to, "--to") : null;
|
|
91
|
+
if (from !== null && to !== null && from > to) {
|
|
92
|
+
throw new ExportUsageError(`The start of the period (${from}) is after its end (${to}).`);
|
|
93
|
+
}
|
|
94
|
+
return { from, to };
|
|
95
|
+
}
|
|
96
|
+
/** All the flag validation, done locally before any network call. */
|
|
97
|
+
export function parseExportFlags(flags, now) {
|
|
98
|
+
const output = flags.output?.trim();
|
|
99
|
+
if (output === "")
|
|
100
|
+
throw new ExportUsageError("-o/--output needs a file path, or - for stdout.");
|
|
101
|
+
return {
|
|
102
|
+
format: parseFormat(flags.format),
|
|
103
|
+
period: resolvePeriod(flags, now),
|
|
104
|
+
output,
|
|
105
|
+
force: flags.force === true,
|
|
106
|
+
json: flags.json === true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/** `?format=…&from=…&to=…` for the compliance-export route. */
|
|
110
|
+
export function buildExportQuery(format, period) {
|
|
111
|
+
const params = new URLSearchParams({ format });
|
|
112
|
+
if (period.from !== null)
|
|
113
|
+
params.set("from", period.from);
|
|
114
|
+
if (period.to !== null)
|
|
115
|
+
params.set("to", period.to);
|
|
116
|
+
return `?${params.toString()}`;
|
|
117
|
+
}
|
|
118
|
+
// ─── agent ────────────────────────────────────────────────────────────────────
|
|
119
|
+
export const NO_AGENT_MESSAGE = "No agent given. Pass one: mnemom agent export <id-or-name> (see mnemom agent list)";
|
|
120
|
+
/**
|
|
121
|
+
* The agent to export: the positional `[id-or-name]` when given; else, in
|
|
122
|
+
* order, MNEMOM_AGENT, the saved launcher-config `agent`, and the agent of the
|
|
123
|
+
* newest launch in the session ledger that resolved one (resolved by id, named
|
|
124
|
+
* after its slug). null when none of them has an agent.
|
|
125
|
+
*/
|
|
126
|
+
export function pickAgentRef(given, sources) {
|
|
127
|
+
const named = given?.trim() || sources.env()?.trim() || sources.configAgent()?.trim();
|
|
128
|
+
if (named)
|
|
129
|
+
return { ref: named, label: named };
|
|
130
|
+
const launch = sources
|
|
131
|
+
.ledger()
|
|
132
|
+
.find((r) => Boolean(r.agent_id));
|
|
133
|
+
if (!launch)
|
|
134
|
+
return null;
|
|
135
|
+
return { ref: launch.agent_id, label: launch.agent_slug || launch.agent_id };
|
|
136
|
+
}
|
|
137
|
+
// ─── output path ──────────────────────────────────────────────────────────────
|
|
138
|
+
/** The filename-safe agent label: `label` sanitised; the resolved id when that leaves nothing. */
|
|
139
|
+
export function filenameLabel(label, agentId) {
|
|
140
|
+
return sanitizeAgentName(label) || sanitizeAgentName(agentId);
|
|
141
|
+
}
|
|
142
|
+
/** `./mnemom-record-<label>-<YYYY-MM-DD>.<json|csv>` (UTC date). */
|
|
143
|
+
export function defaultOutputPath(label, format, now) {
|
|
144
|
+
return `./mnemom-record-${label}-${now.toISOString().slice(0, 10)}.${format}`;
|
|
145
|
+
}
|
|
146
|
+
// ─── counts ───────────────────────────────────────────────────────────────────
|
|
147
|
+
function isCount(v) {
|
|
148
|
+
return typeof v === "number" && Number.isInteger(v) && v >= 0;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Counts from a JSON export body: the server's `counts` object where it has a
|
|
152
|
+
* section, else the length of that section's array. null when the body is not
|
|
153
|
+
* a JSON object.
|
|
154
|
+
*/
|
|
155
|
+
export function countsFromJsonBody(text) {
|
|
156
|
+
let body;
|
|
157
|
+
try {
|
|
158
|
+
body = JSON.parse(text);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (body === null || typeof body !== "object" || Array.isArray(body))
|
|
164
|
+
return null;
|
|
165
|
+
const record = body;
|
|
166
|
+
const declared = record.counts !== null && typeof record.counts === "object"
|
|
167
|
+
? record.counts
|
|
168
|
+
: {};
|
|
169
|
+
const counts = {};
|
|
170
|
+
for (const section of EXPORT_SECTIONS) {
|
|
171
|
+
const fromServer = declared[section];
|
|
172
|
+
const rows = record[section];
|
|
173
|
+
counts[section] = isCount(fromServer) ? fromServer : Array.isArray(rows) ? rows.length : null;
|
|
174
|
+
}
|
|
175
|
+
return counts;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Counts from the CSV response header
|
|
179
|
+
* `checkpoints=N;score_history=N;…`. null when the header is absent or carries
|
|
180
|
+
* no recognisable section (an older API) — the summary then says so.
|
|
181
|
+
*/
|
|
182
|
+
const COUNT_PAIR_RE = /^\s*([a-z_]+)\s*=\s*(\d+)\s*$/;
|
|
183
|
+
export function countsFromHeader(header) {
|
|
184
|
+
if (header === null)
|
|
185
|
+
return null;
|
|
186
|
+
const counts = Object.fromEntries(EXPORT_SECTIONS.map((s) => [s, null]));
|
|
187
|
+
let found = false;
|
|
188
|
+
for (const pair of header.split(";")) {
|
|
189
|
+
const m = COUNT_PAIR_RE.exec(pair);
|
|
190
|
+
if (m && EXPORT_SECTIONS.includes(m[1])) {
|
|
191
|
+
counts[m[1]] = Number(m[2]);
|
|
192
|
+
found = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return found ? counts : null;
|
|
196
|
+
}
|
|
197
|
+
export function extractCounts(format, body, headers) {
|
|
198
|
+
return format === "csv"
|
|
199
|
+
? countsFromHeader(headers.get(COUNTS_HEADER))
|
|
200
|
+
: countsFromJsonBody(new TextDecoder().decode(body));
|
|
201
|
+
}
|
|
202
|
+
// ─── summary ──────────────────────────────────────────────────────────────────
|
|
203
|
+
export function sha256Hex(bytes) {
|
|
204
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
205
|
+
}
|
|
206
|
+
export function verificationLinks(agentId, websiteBase, apiBase) {
|
|
207
|
+
const id = encodeURIComponent(agentId);
|
|
208
|
+
return {
|
|
209
|
+
reputation_url: `${websiteBase}/agents/${id}/reputation`,
|
|
210
|
+
verify_url: `${apiBase}/v1/reputation/${id}/verify`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export function buildSummary(input) {
|
|
214
|
+
return {
|
|
215
|
+
agent_id: input.agentId,
|
|
216
|
+
output: input.output,
|
|
217
|
+
format: input.format,
|
|
218
|
+
period: input.period,
|
|
219
|
+
counts: input.counts,
|
|
220
|
+
bytes: input.body.byteLength,
|
|
221
|
+
sha256: sha256Hex(input.body),
|
|
222
|
+
verification: verificationLinks(input.agentId, input.websiteBase, input.apiBase),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export function describePeriod(period) {
|
|
226
|
+
if (period.from !== null && period.to !== null)
|
|
227
|
+
return `${period.from} to ${period.to}`;
|
|
228
|
+
if (period.from !== null)
|
|
229
|
+
return `${period.from} to now`;
|
|
230
|
+
if (period.to !== null)
|
|
231
|
+
return `everything up to ${period.to}`;
|
|
232
|
+
return "all time";
|
|
233
|
+
}
|
|
234
|
+
/** The human summary, one line per entry (no colour; the driver adds none). */
|
|
235
|
+
export function formatSummaryLines(summary) {
|
|
236
|
+
const lines = [
|
|
237
|
+
`Exported the record of ${summary.agent_id}`,
|
|
238
|
+
` Output: ${summary.output === "-" ? "stdout" : summary.output} (${summary.format}, ${summary.bytes} bytes)`,
|
|
239
|
+
` Period: ${describePeriod(summary.period)}`,
|
|
240
|
+
];
|
|
241
|
+
if (summary.counts === null) {
|
|
242
|
+
lines.push(" Counts: unavailable (this server version does not report them)");
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
lines.push(" Counts:");
|
|
246
|
+
const width = Math.max(...EXPORT_SECTIONS.map((s) => s.length));
|
|
247
|
+
for (const section of EXPORT_SECTIONS) {
|
|
248
|
+
const n = summary.counts[section];
|
|
249
|
+
lines.push(` ${section.padEnd(width)} ${n === null ? "unavailable" : n}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
lines.push(` SHA-256: ${summary.sha256}`);
|
|
253
|
+
lines.push(" (file checksum — lets the recipient confirm the file was not altered in transit)");
|
|
254
|
+
lines.push(` Verify: ${summary.verification.reputation_url}`);
|
|
255
|
+
lines.push(` ${summary.verification.verify_url}`);
|
|
256
|
+
return lines;
|
|
257
|
+
}
|
|
258
|
+
/** The one-line sensitivity note (the driver prints it dim, on stderr). */
|
|
259
|
+
export function sensitivityNote(toStdout) {
|
|
260
|
+
const where = toStdout
|
|
261
|
+
? "handle it as confidential"
|
|
262
|
+
: "the file is readable by you only (mode 0600)";
|
|
263
|
+
return (`This export holds full checkpoint reasoning and concerns that outsiders normally see redacted — ${where}. ` +
|
|
264
|
+
"To share proof without the contents, send the public verification link instead.");
|
|
265
|
+
}
|
|
266
|
+
// ─── errors ───────────────────────────────────────────────────────────────────
|
|
267
|
+
/** The user-facing message for a failed export request. */
|
|
268
|
+
export function exportErrorMessage(error, agentId) {
|
|
269
|
+
if (error instanceof MnemomApiError) {
|
|
270
|
+
// effectiveStatus, not status: the enforce hook can rewrite an undocumented
|
|
271
|
+
// status to a synthetic 500 carrying spec_deviation.original_status.
|
|
272
|
+
const status = error.effectiveStatus;
|
|
273
|
+
if (status === 401)
|
|
274
|
+
return "Not signed in, or your session has expired. Run `mnemom login`.";
|
|
275
|
+
if (status === 403)
|
|
276
|
+
return `You are not a member of the org that owns agent ${agentId}.`;
|
|
277
|
+
if (status === 404)
|
|
278
|
+
return `Agent not found: ${agentId}.`;
|
|
279
|
+
if (status === 413 || error.code === "export_too_large") {
|
|
280
|
+
return "That period has more rows than the server exports in one go. Narrow it with --since, --from or --to.";
|
|
281
|
+
}
|
|
282
|
+
return `Export failed: ${error.message}`;
|
|
283
|
+
}
|
|
284
|
+
return `Export failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
285
|
+
}
|
|
286
|
+
/** The user-facing message for a failed file write. */
|
|
287
|
+
export function writeErrorMessage(error, path) {
|
|
288
|
+
const code = error?.code;
|
|
289
|
+
if (code === "EEXIST") {
|
|
290
|
+
return `${path} already exists. Pass --force to overwrite it, or pick another path with -o.`;
|
|
291
|
+
}
|
|
292
|
+
return `Could not write ${path}: ${error instanceof Error ? error.message : String(error)}`;
|
|
293
|
+
}
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -371,6 +371,18 @@ export interface MoveAgentResult {
|
|
|
371
371
|
export declare function moveAgent(agentId: string, destOrgId: string, opts?: {
|
|
372
372
|
idempotencyKey?: string;
|
|
373
373
|
}): Promise<MoveAgentResult>;
|
|
374
|
+
/**
|
|
375
|
+
* GET /v1/agents/:id/compliance-export — the agent's full record (JSON or CSV).
|
|
376
|
+
*
|
|
377
|
+
* Returns the raw body bytes (written to disk verbatim so the checksum the CLI
|
|
378
|
+
* prints matches what the server sent) plus the response headers (CSV carries
|
|
379
|
+
* its per-section counts in `X-Mnemom-Export-Counts`). `query` is the full
|
|
380
|
+
* `?format=…&from=…&to=…` string (see lib/agent-export.ts buildExportQuery).
|
|
381
|
+
*/
|
|
382
|
+
export declare function getComplianceExport(agentId: string, query: string): Promise<{
|
|
383
|
+
body: Uint8Array;
|
|
384
|
+
headers: Headers;
|
|
385
|
+
}>;
|
|
374
386
|
/**
|
|
375
387
|
* GET /v1/auth/me/personal-org — accessor for the user's personal org.
|
|
376
388
|
* Idempotent: lazily provisions for legacy accounts that pre-date the
|
package/dist/lib/api.js
CHANGED
|
@@ -367,6 +367,23 @@ export async function moveAgent(agentId, destOrgId, opts = {}) {
|
|
|
367
367
|
to_org_id: data.to_org_id ?? null,
|
|
368
368
|
};
|
|
369
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* GET /v1/agents/:id/compliance-export — the agent's full record (JSON or CSV).
|
|
372
|
+
*
|
|
373
|
+
* Returns the raw body bytes (written to disk verbatim so the checksum the CLI
|
|
374
|
+
* prints matches what the server sent) plus the response headers (CSV carries
|
|
375
|
+
* its per-section counts in `X-Mnemom-Export-Counts`). `query` is the full
|
|
376
|
+
* `?format=…&from=…&to=…` string (see lib/agent-export.ts buildExportQuery).
|
|
377
|
+
*/
|
|
378
|
+
export async function getComplianceExport(agentId, query) {
|
|
379
|
+
const url = validateUrl(`${API_BASE}/v1/agents/${encodeURIComponent(agentId)}/compliance-export${query}`);
|
|
380
|
+
const response = await fetchWithAuthRetry(url, async () => ({
|
|
381
|
+
headers: { ...(await authHeaders()), Accept: "application/json, text/csv" },
|
|
382
|
+
}));
|
|
383
|
+
if (!response.ok)
|
|
384
|
+
throw await readApiError(response, "Failed to export the agent record");
|
|
385
|
+
return { body: new Uint8Array(await response.arrayBuffer()), headers: response.headers };
|
|
386
|
+
}
|
|
370
387
|
/**
|
|
371
388
|
* GET /v1/auth/me/personal-org — accessor for the user's personal org.
|
|
372
389
|
* Idempotent: lazily provisions for legacy accounts that pre-date the
|