@mnemom/mnemom 0.17.21 → 0.17.23
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 +51 -15
- package/dist/commands/agent-export.d.ts +53 -0
- package/dist/commands/agent-export.js +127 -0
- package/dist/commands/agent.d.ts +13 -4
- package/dist/commands/agent.js +23 -10
- package/dist/commands/experimental.js +8 -1
- package/dist/index.js +24 -25
- package/dist/lib/agent-export.d.ts +133 -0
- package/dist/lib/agent-export.js +293 -0
- package/dist/lib/agent.d.ts +30 -17
- package/dist/lib/agent.js +42 -30
- package/dist/lib/api.d.ts +12 -0
- package/dist/lib/api.js +17 -0
- package/dist/lib/experimental.d.ts +13 -10
- package/dist/lib/experimental.js +19 -38
- package/dist/lib/launcher-config.d.ts +5 -6
- package/dist/lib/launcher-config.js +4 -11
- package/package.json +1 -1
|
@@ -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/agent.d.ts
CHANGED
|
@@ -342,16 +342,24 @@ export declare const CONTEXT_HEAD_RECLAIM_KEY = "CONTEXT_RUNTIME_HEAD_BREAKPOINT
|
|
|
342
342
|
export declare const CONTEXT_SIZE_POLICY_KEY = "CONTEXT_RUNTIME_SIZE_POLICY";
|
|
343
343
|
/**
|
|
344
344
|
* The two gateway env keys the `compaction_intercept` setting turns on for a single session
|
|
345
|
-
* (payload-bound fix; both
|
|
345
|
+
* (payload-bound fix; both read from the per-session overlay). The us-2 prod cell runs both on in
|
|
346
|
+
* its own cell env; the test and us-1 preview cells leave them at the gateway default (off). Since
|
|
347
|
+
* 0.17.23 the CLI asks for them on every launch — a deliberate, authorized rollout decision
|
|
348
|
+
* (MNE-8532, CTO 2026-10-12): every cell a `mnemom agent` session runs on then behaves like
|
|
349
|
+
* prod, rather than the CLI deferring to a non-prod cell's off default. The keys are TUNE-tier
|
|
350
|
+
* (gateway session-config allowlist, honored for any authenticated agent), so a cell without
|
|
351
|
+
* the companion support drops them and the session behaves as before.
|
|
346
352
|
* - `GATEWAY_USAGE_REAL_SIZE` — report the size of the body the client actually sent (the
|
|
347
353
|
* REAL, unfolded context) in the last `usage.iterations` entry, so Claude Code's own
|
|
348
354
|
* auto-compact threshold (its 1M default, or the user's `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` /
|
|
349
355
|
* `CLAUDE_CODE_AUTO_COMPACT_WINDOW` / `autoCompactWindow`) fires as it would without the fold;
|
|
350
356
|
* - `GATEWAY_COMPACTION_INTERCEPT` — answer the client's compaction request at once from the
|
|
351
357
|
* gateway's own fold digest (no model call), so the compaction never blocks the user.
|
|
352
|
-
* Sent together, value `"on"`, when the `compaction_intercept` setting is on
|
|
353
|
-
* (`resolveCompactionIntercept`) OR when `--compact-at` arms a target below 1M (below)
|
|
354
|
-
* target cannot be honored without both.
|
|
358
|
+
* Sent together, value `"on"`, when the `compaction_intercept` setting is on — the DEFAULT since
|
|
359
|
+
* 0.17.23 (`resolveCompactionIntercept`) — OR when `--compact-at` arms a target below 1M (below):
|
|
360
|
+
* a target cannot be honored without both. An opt-out (`--no-compaction-intercept`,
|
|
361
|
+
* `compaction_intercept = false`, `MNEMOM_AGENT_COMPACTION_INTERCEPT=0`) and the `off` context
|
|
362
|
+
* profile send both as `"off"`, so the opt-out holds on a cell whose own default is on.
|
|
355
363
|
*/
|
|
356
364
|
export declare const USAGE_REAL_SIZE_KEY = "GATEWAY_USAGE_REAL_SIZE";
|
|
357
365
|
export declare const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
|
|
@@ -461,8 +469,7 @@ export declare function resolveCompactAt(opts: {
|
|
|
461
469
|
* (either silently re-arming the intercept the user just turned off, or silently sending an
|
|
462
470
|
* inert upload-target the gateway can never act on). `compactionIntercept` here is the raw,
|
|
463
471
|
* pre-`resolveCompactionIntercept` flag value: `false` only when `--no-compaction-intercept`
|
|
464
|
-
* was explicitly passed
|
|
465
|
-
* past either — see index.ts's `assertEnabled`), never the mere absence of `--compaction-intercept`.
|
|
472
|
+
* was explicitly passed, never the mere absence of `--compaction-intercept`.
|
|
466
473
|
*/
|
|
467
474
|
export declare function assertCompactAtCompatible(opts: {
|
|
468
475
|
compactionIntercept?: boolean;
|
|
@@ -648,18 +655,23 @@ export declare function resolveContextProfile(opts: {
|
|
|
648
655
|
* no profile was chosen), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
|
|
649
656
|
* `GATEWAY_USAGE_SAVINGS: "on"` when measured savings is ON (under every profile), then
|
|
650
657
|
* `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when EITHER the
|
|
651
|
-
* `compaction_intercept` setting is ON OR `--compact-at` arms a target below 1M (the
|
|
652
|
-
* cannot be honored without the intercept)
|
|
653
|
-
*
|
|
658
|
+
* `compaction_intercept` setting is ON (the default) OR `--compact-at` arms a target below 1M (the
|
|
659
|
+
* target cannot be honored without the intercept) — or both as `"off"` when the setting is
|
|
660
|
+
* explicitly `false` (an opt-out) and no target is armed, so the opt-out holds on a cell whose own
|
|
661
|
+
* env turns them on — then `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"` when a target is set. The `off` profile wins over the intercept and the target: a session with
|
|
654
662
|
* no fold never has its compaction intercepted (`assertOffProfileCompatible` refuses the explicit
|
|
655
663
|
* flag pair; a standing intercept or target from the environment or launcher.toml is dropped).
|
|
656
|
-
* Empty with no profile chosen
|
|
657
|
-
* no header is sent at all. Exported so `--dry-run` can list exactly what the header carries.
|
|
664
|
+
* Empty with no profile chosen, everything else off and `compactionIntercept` absent (callers
|
|
665
|
+
* that do not resolve it) — then no header is sent at all. Exported so `--dry-run` can list exactly what the header carries.
|
|
658
666
|
*/
|
|
659
667
|
export declare function buildSessionConfig(opts: {
|
|
660
668
|
/** `resolveContextProfile` output: `null` ⇒ no profile chosen, no fold key sent. */
|
|
661
669
|
profile: ContextProfile | null;
|
|
662
670
|
contextHandoff: boolean;
|
|
671
|
+
/**
|
|
672
|
+
* `resolveCompactionIntercept` output: `true` (the default) ⇒ both keys `"on"`; `false` (an
|
|
673
|
+
* opt-out) ⇒ both `"off"`; absent ⇒ neither key is sent (the cell's own default stands).
|
|
674
|
+
*/
|
|
663
675
|
compactionIntercept?: boolean;
|
|
664
676
|
/** `resolveCompactAt` output: `null` (or absent) ⇒ no target, send nothing new. */
|
|
665
677
|
compactAt?: number | null;
|
|
@@ -691,8 +703,8 @@ export declare function buildSessionConfigPayload(opts: {
|
|
|
691
703
|
* `aggressive` → the three fold-tuning keys + `CONTEXT_RUNTIME_SIZE_POLICY: "off"`,
|
|
692
704
|
* `balanced` → `CONTEXT_RUNTIME_SIZE_POLICY: "off"`, `adaptive` →
|
|
693
705
|
* `CONTEXT_RUNTIME_SIZE_POLICY: "banded"`, none chosen → nothing;
|
|
694
|
-
* - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` +
|
|
695
|
-
* `"on"` (
|
|
706
|
+
* - `compactionIntercept: true` (the default) → `GATEWAY_USAGE_REAL_SIZE` +
|
|
707
|
+
* `GATEWAY_COMPACTION_INTERCEPT` `"on"`; `false` (an opt-out) → both `"off"`;
|
|
696
708
|
* - `compactAt: <N>` (`--compact-at`, below 1M) → the SAME two intercept keys plus
|
|
697
709
|
* `GATEWAY_COMPACTION_UPLOAD_TARGET_TOKENS: "<N>"`; unset, `1m` or `off` → nothing beyond
|
|
698
710
|
* what `compactionIntercept` already sent (Claude Code's own 1M window, untouched);
|
|
@@ -725,10 +737,11 @@ export declare function resolveContextHandoff(opts: {
|
|
|
725
737
|
}): boolean;
|
|
726
738
|
/**
|
|
727
739
|
* Is the compaction intercept (`GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT` in the
|
|
728
|
-
* session-config header) ON for this launch?
|
|
729
|
-
* `
|
|
730
|
-
* wins, else `MNEMOM_AGENT_COMPACTION_INTERCEPT` (which
|
|
731
|
-
* `compaction_intercept` config key
|
|
740
|
+
* session-config header) ON for this launch? ON by default since 0.17.23 (it graduated from the
|
|
741
|
+
* `compaction-intercept` experimental feature). An explicit `--compaction-intercept` /
|
|
742
|
+
* `--no-compaction-intercept` wins, else `MNEMOM_AGENT_COMPACTION_INTERCEPT` (which
|
|
743
|
+
* `applyConfigToEnv` sets from the `compaction_intercept` config key: `0`/`false`/`off`/`no`
|
|
744
|
+
* opt out), else ON.
|
|
732
745
|
*/
|
|
733
746
|
export declare function resolveCompactionIntercept(opts: {
|
|
734
747
|
compactionIntercept?: boolean;
|