@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 +26 -0
- package/dist/commands/agent-export.d.ts +53 -0
- package/dist/commands/agent-export.js +127 -0
- package/dist/commands/agent-sessions.d.ts +15 -5
- package/dist/commands/agent-sessions.js +29 -5
- package/dist/commands/agent.d.ts +1 -1
- package/dist/commands/agent.js +21 -3
- package/dist/commands/validate.d.ts +1 -1
- package/dist/commands/validate.js +7 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +20 -2
- package/dist/lib/agent-export.d.ts +133 -0
- package/dist/lib/agent-export.js +293 -0
- package/dist/lib/api.d.ts +53 -0
- package/dist/lib/api.js +17 -0
- package/dist/lib/claude-plugin.d.ts +7 -8
- package/dist/lib/claude-plugin.js +8 -9
- package/dist/lib/config.d.ts +13 -2
- package/dist/lib/config.js +22 -6
- package/dist/lib/first-run/governed.js +7 -4
- package/dist/lib/retired-env-guard.d.ts +7 -0
- package/dist/lib/retired-env-guard.js +46 -0
- package/dist/rc-proxy.mjs +83 -13
- package/dist/rc-state.mjs +257 -0
- package/package.json +5 -1
- package/plugins/.claude-plugin/marketplace.json +20 -0
- package/plugins/mnemom/.claude-plugin/plugin.json +47 -0
- package/plugins/mnemom/.mcp.json +12 -0
- package/plugins/mnemom/hooks/hooks.json +3 -0
- package/plugins/mnemom/hooks/register.tsx +1476 -0
- package/plugins/mnemom/skills/about-mnemom/SKILL.md +88 -0
- package/plugins/mnemom/types/index.d.ts +142 -0
|
@@ -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
|
|
@@ -1305,6 +1317,29 @@ export interface AgentSessionVerdictTrailEntry {
|
|
|
1305
1317
|
fence_breach: boolean | null;
|
|
1306
1318
|
at: string;
|
|
1307
1319
|
}
|
|
1320
|
+
/**
|
|
1321
|
+
* The seal's lifecycle (MNE-8149 R2, MNE-8199 D9/R9, MNE-8234/mnemom-api#3541). `live`: a sealed
|
|
1322
|
+
* contract currently governs — `mode` says whether a crossed guardrail only nudges or enforces
|
|
1323
|
+
* (refuses the next request). `expired`: the prior seal lapsed (24h idle) and the next engaged
|
|
1324
|
+
* request will seal afresh; no `mode` because nothing is being enforced.
|
|
1325
|
+
*/
|
|
1326
|
+
export interface AgentSessionSealLive {
|
|
1327
|
+
state: "live";
|
|
1328
|
+
version: number;
|
|
1329
|
+
hash: string;
|
|
1330
|
+
sealed_at: string;
|
|
1331
|
+
reason: "first" | "after_expiry" | null;
|
|
1332
|
+
expired_hash: string | null;
|
|
1333
|
+
updated_from_hash: string | null;
|
|
1334
|
+
mode: "nudge" | "enforce";
|
|
1335
|
+
}
|
|
1336
|
+
export interface AgentSessionSealExpired {
|
|
1337
|
+
state: "expired";
|
|
1338
|
+
version: number;
|
|
1339
|
+
hash: string;
|
|
1340
|
+
sealed_at: string;
|
|
1341
|
+
}
|
|
1342
|
+
export type AgentSessionSeal = AgentSessionSealLive | AgentSessionSealExpired;
|
|
1308
1343
|
/** `GET /v1/agent/sessions/{conversationId}` 200 body. Nulls mean "nothing stored" — absence is data. */
|
|
1309
1344
|
export interface AgentSessionResponse {
|
|
1310
1345
|
conversation_id: string;
|
|
@@ -1319,6 +1354,24 @@ export interface AgentSessionResponse {
|
|
|
1319
1354
|
guardrails: AgentSessionGuardrails | null;
|
|
1320
1355
|
/** Absent on an older server (back-compat) — never shown then, never an empty-array assumption. */
|
|
1321
1356
|
verdict_trail?: AgentSessionVerdictTrailEntry[];
|
|
1357
|
+
/**
|
|
1358
|
+
* MNE-8234: the seal's lifecycle, incl. `expired`. Absent on an older server (back-compat —
|
|
1359
|
+
* never shown then). `null` means the API itself reports no seal: either none was ever made for
|
|
1360
|
+
* this conversation, or the gateway sent a seal payload out of contract and the API dropped it
|
|
1361
|
+
* rather than relay it half-trusted — the two are indistinguishable from this field alone.
|
|
1362
|
+
*/
|
|
1363
|
+
seal?: AgentSessionSeal | null;
|
|
1364
|
+
/**
|
|
1365
|
+
* MNE-8166 R4 / MNE-8234: assistant turns under the current seal with no verdict (a cut-off/
|
|
1366
|
+
* aborted response, a judge that failed or never ran, a non-streamed final turn). Absent on an
|
|
1367
|
+
* older server (back-compat). `null` when no judge ledger counts under the current seal.
|
|
1368
|
+
*/
|
|
1369
|
+
unjudged_turns?: number | null;
|
|
1370
|
+
/**
|
|
1371
|
+
* MNE-8166 R4 / MNE-8234: turns the judge memo graded (aligned by inheritance, no verdict
|
|
1372
|
+
* stored). Absent on an older server (back-compat). `null` alongside `unjudged_turns`.
|
|
1373
|
+
*/
|
|
1374
|
+
memoised_turns?: number | null;
|
|
1322
1375
|
}
|
|
1323
1376
|
/**
|
|
1324
1377
|
* The live goal state of one `mnemom agent` session, read through mnemom-api
|
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
|
|
@@ -14,13 +14,12 @@
|
|
|
14
14
|
* Only Claude Code proper (`cli.name === "claude"`) understands `--plugin-dir`; every other
|
|
15
15
|
* adapter launches without the plugin.
|
|
16
16
|
*
|
|
17
|
-
* OFF BY DEFAULT
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* back into `files`, and `pluginEnabled`'s default to true. It never blocks the launch.
|
|
17
|
+
* OFF BY DEFAULT. The folder ships in the npm tarball (`package.json` `files`, minus its tests
|
|
18
|
+
* and checkout-only files) but no launch loads it unless asked: `--plugin` or
|
|
19
|
+
* MNEMOM_AGENT_PLUGIN=1|true|yes|on opts in (`--no-plugin` or 0|false|off|no wins over the env),
|
|
20
|
+
* and the flags stay out of `--help`. An install missing the folder says so and launches without
|
|
21
|
+
* it. Turning it on for everyone is one edit: `pluginEnabled`'s default to true. It never blocks
|
|
22
|
+
* the launch.
|
|
24
23
|
*/
|
|
25
24
|
/** The plugin's `name` in its plugin.json, and its folder under `cli/plugins/`. */
|
|
26
25
|
export declare const CLAUDE_PLUGIN_NAME = "mnemom";
|
|
@@ -68,7 +67,7 @@ export declare const PLUGIN_ENV: {
|
|
|
68
67
|
export type PluginEnvName = (typeof PLUGIN_ENV)[keyof typeof PLUGIN_ENV];
|
|
69
68
|
/**
|
|
70
69
|
* Whether this launch loads the plugin: `--plugin` / `--no-plugin` (an explicit boolean) wins,
|
|
71
|
-
* then MNEMOM_AGENT_PLUGIN (1/true/yes/on ⇒ on), else OFF: the plugin
|
|
70
|
+
* then MNEMOM_AGENT_PLUGIN (1/true/yes/on ⇒ on), else OFF: the plugin lands dark.
|
|
72
71
|
*/
|
|
73
72
|
export declare function pluginEnabled(optionPlugin: boolean | undefined, env: NodeJS.ProcessEnv): boolean;
|
|
74
73
|
/**
|
|
@@ -14,13 +14,12 @@
|
|
|
14
14
|
* Only Claude Code proper (`cli.name === "claude"`) understands `--plugin-dir`; every other
|
|
15
15
|
* adapter launches without the plugin.
|
|
16
16
|
*
|
|
17
|
-
* OFF BY DEFAULT
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* back into `files`, and `pluginEnabled`'s default to true. It never blocks the launch.
|
|
17
|
+
* OFF BY DEFAULT. The folder ships in the npm tarball (`package.json` `files`, minus its tests
|
|
18
|
+
* and checkout-only files) but no launch loads it unless asked: `--plugin` or
|
|
19
|
+
* MNEMOM_AGENT_PLUGIN=1|true|yes|on opts in (`--no-plugin` or 0|false|off|no wins over the env),
|
|
20
|
+
* and the flags stay out of `--help`. An install missing the folder says so and launches without
|
|
21
|
+
* it. Turning it on for everyone is one edit: `pluginEnabled`'s default to true. It never blocks
|
|
22
|
+
* the launch.
|
|
24
23
|
*/
|
|
25
24
|
import { existsSync } from "node:fs";
|
|
26
25
|
import { dirname, join, delimiter } from "node:path";
|
|
@@ -71,7 +70,7 @@ export const PLUGIN_ENV = {
|
|
|
71
70
|
const TRUTHY = new Set(["1", "true", "yes", "on"]);
|
|
72
71
|
/**
|
|
73
72
|
* Whether this launch loads the plugin: `--plugin` / `--no-plugin` (an explicit boolean) wins,
|
|
74
|
-
* then MNEMOM_AGENT_PLUGIN (1/true/yes/on ⇒ on), else OFF: the plugin
|
|
73
|
+
* then MNEMOM_AGENT_PLUGIN (1/true/yes/on ⇒ on), else OFF: the plugin lands dark.
|
|
75
74
|
*/
|
|
76
75
|
export function pluginEnabled(optionPlugin, env) {
|
|
77
76
|
if (optionPlugin !== undefined)
|
|
@@ -144,6 +143,6 @@ export function pluginPlanLine(pluginDir, enabled) {
|
|
|
144
143
|
if (!enabled)
|
|
145
144
|
return "off";
|
|
146
145
|
if (!pluginDir)
|
|
147
|
-
return "off (the plugin folder is not in this install
|
|
146
|
+
return "off (the plugin folder is not in this install)";
|
|
148
147
|
return `${CLAUDE_PLUGIN_NAME} (${pluginDir})`;
|
|
149
148
|
}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -7,10 +7,21 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/** Base directory for mnemom CLI state (auth tokens, caches). */
|
|
9
9
|
export declare const MNEMOM_DIR: string;
|
|
10
|
-
export type Environment = "production" | "
|
|
10
|
+
export type Environment = "production" | "local";
|
|
11
|
+
/**
|
|
12
|
+
* Thrown for `MNEMOM_ENV=staging`: the value used to be accepted, and the ring
|
|
13
|
+
* it named is gone (MNE-7968). Failing loud is deliberate — the two silent
|
|
14
|
+
* alternatives are both wrong: resolving to the dead staging hostnames makes
|
|
15
|
+
* every call fail with a DNS/connection error that does not say why, and
|
|
16
|
+
* falling through to production would send a caller who asked for a non-prod
|
|
17
|
+
* ring at real customer data.
|
|
18
|
+
*/
|
|
19
|
+
export declare class RetiredEnvironmentError extends Error {
|
|
20
|
+
constructor();
|
|
21
|
+
}
|
|
11
22
|
/**
|
|
12
23
|
* Resolve the active environment from MNEMOM_ENV.
|
|
13
|
-
* Defaults to production.
|
|
24
|
+
* Defaults to production. `staging` throws (see RetiredEnvironmentError).
|
|
14
25
|
*/
|
|
15
26
|
export declare function getEnvironment(): Environment;
|
|
16
27
|
export type CellRing = "us-1";
|
package/dist/lib/config.js
CHANGED
|
@@ -11,27 +11,43 @@ import * as os from "node:os";
|
|
|
11
11
|
export const MNEMOM_DIR = path.join(os.homedir(), ".mnemom");
|
|
12
12
|
const API_URLS = {
|
|
13
13
|
production: "https://api.mnemom.ai",
|
|
14
|
-
staging: "https://api-staging.mnemom.ai",
|
|
15
14
|
local: "http://localhost:8787",
|
|
16
15
|
};
|
|
17
16
|
const GATEWAY_URLS = {
|
|
18
17
|
production: "https://gateway.mnemom.ai",
|
|
19
|
-
staging: "https://gateway-staging.mnemom.ai",
|
|
20
18
|
local: "http://localhost:8787",
|
|
21
19
|
};
|
|
22
20
|
const WEBSITE_URLS = {
|
|
23
21
|
production: "https://www.mnemom.ai",
|
|
24
|
-
staging: "https://staging.mnemom.ai",
|
|
25
22
|
local: "http://localhost:5173",
|
|
26
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Thrown for `MNEMOM_ENV=staging`: the value used to be accepted, and the ring
|
|
26
|
+
* it named is gone (MNE-7968). Failing loud is deliberate — the two silent
|
|
27
|
+
* alternatives are both wrong: resolving to the dead staging hostnames makes
|
|
28
|
+
* every call fail with a DNS/connection error that does not say why, and
|
|
29
|
+
* falling through to production would send a caller who asked for a non-prod
|
|
30
|
+
* ring at real customer data.
|
|
31
|
+
*/
|
|
32
|
+
export class RetiredEnvironmentError extends Error {
|
|
33
|
+
constructor() {
|
|
34
|
+
super("MNEMOM_ENV=staging is no longer supported: the staging ring was retired (MNE-7968) " +
|
|
35
|
+
"and nothing serves api-staging / gateway-staging.mnemom.ai. " +
|
|
36
|
+
"Use MNEMOM_RING=us-1 for the preview cell, MNEMOM_ENV=local for a local stack, " +
|
|
37
|
+
"or set MNEMOM_API_URL / MNEMOM_GATEWAY_URL / MNEMOM_WEBSITE_URL to target another cell.");
|
|
38
|
+
this.name = "RetiredEnvironmentError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
27
41
|
/**
|
|
28
42
|
* Resolve the active environment from MNEMOM_ENV.
|
|
29
|
-
* Defaults to production.
|
|
43
|
+
* Defaults to production. `staging` throws (see RetiredEnvironmentError).
|
|
30
44
|
*/
|
|
31
45
|
export function getEnvironment() {
|
|
32
46
|
const env = process.env.MNEMOM_ENV;
|
|
33
|
-
if (env === "
|
|
47
|
+
if (env === "local")
|
|
34
48
|
return env;
|
|
49
|
+
if (env === "staging")
|
|
50
|
+
throw new RetiredEnvironmentError();
|
|
35
51
|
return "production";
|
|
36
52
|
}
|
|
37
53
|
const RING_URLS = {
|
|
@@ -61,7 +77,7 @@ export function getRing() {
|
|
|
61
77
|
const env = process.env.MNEMOM_RING?.trim();
|
|
62
78
|
return env && isCellRing(env) ? env : undefined;
|
|
63
79
|
}
|
|
64
|
-
// Process-level base overrides. MNEMOM_ENV only selects prod/
|
|
80
|
+
// Process-level base overrides. MNEMOM_ENV only selects prod/local;
|
|
65
81
|
// to target an arbitrary ring (preview/us-1, test) a command can set an
|
|
66
82
|
// explicit override (e.g. `try-me --api <ring>`) or the operator can export
|
|
67
83
|
// MNEMOM_API_URL / MNEMOM_GATEWAY_URL / MNEMOM_WEBSITE_URL. Crucially these
|
|
@@ -123,11 +123,14 @@ export class Governance {
|
|
|
123
123
|
if (!r.cc_session_id && t !== null)
|
|
124
124
|
this.windows.push(["registry_window", t]);
|
|
125
125
|
}
|
|
126
|
-
// Remote Control proxy logs:
|
|
126
|
+
// Remote Control proxy logs: ~/.mnemom/rc/mnemom-code-rc.*/proxy.log (the launcher's dir),
|
|
127
|
+
// $TMPDIR/mnemom-code-rc.*/proxy.log (where older CLIs put it), ~/.mnemom/*rc-proxy*.log
|
|
127
128
|
const logs = [];
|
|
128
|
-
for (const
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
for (const base of [join(mnemomDir, "rc"), tmp]) {
|
|
130
|
+
for (const d of safeList(base)) {
|
|
131
|
+
if (d.startsWith("mnemom-code-rc."))
|
|
132
|
+
logs.push(join(base, d, "proxy.log"));
|
|
133
|
+
}
|
|
131
134
|
}
|
|
132
135
|
for (const f of safeList(mnemomDir)) {
|
|
133
136
|
if (f.includes("rc-proxy") && f.endsWith(".log"))
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const RETIRED_ENV_EXIT_CODE = 2;
|
|
2
|
+
/**
|
|
3
|
+
* The startup error line when any CLI base URL would resolve via the retired
|
|
4
|
+
* `MNEMOM_ENV=staging`, else undefined. Exported for the unit test; the module
|
|
5
|
+
* body below is what the CLI relies on.
|
|
6
|
+
*/
|
|
7
|
+
export declare function retiredEnvMessage(): string | undefined;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup guard for `MNEMOM_ENV=staging` (MNE-7968).
|
|
3
|
+
*
|
|
4
|
+
* `getEnvironment()` throws `RetiredEnvironmentError` for that value, and three
|
|
5
|
+
* modules resolve the API base at MODULE-EVAL time (`lib/api.ts`,
|
|
6
|
+
* `lib/webhooks-api.ts`, `commands/listen.ts` — see the note at the top of
|
|
7
|
+
* `__tests__/cli-api-integration.test.ts`). Those modules are statically
|
|
8
|
+
* imported by `index.ts`, so without this guard the throw surfaces during ESM
|
|
9
|
+
* linking — before `runEntrypoint()`, before commander, before any try/catch —
|
|
10
|
+
* and every invocation, `mnemom --help` included, dies with a raw Node stack
|
|
11
|
+
* trace. This module is the FIRST import of `index.ts`, so it evaluates before
|
|
12
|
+
* any of them: it prints the same message on one line and exits 2 (the CLI's
|
|
13
|
+
* usage-error code).
|
|
14
|
+
*
|
|
15
|
+
* The guard decides by running config.ts's own resolvers rather than reading
|
|
16
|
+
* MNEMOM_ENV, so it follows their precedence exactly (raw MNEMOM_*_URL > ring >
|
|
17
|
+
* MNEMOM_ENV): when a valid MNEMOM_RING, or all three MNEMOM_*_URL overrides,
|
|
18
|
+
* answer every base URL, MNEMOM_ENV is never consulted and a stale
|
|
19
|
+
* `MNEMOM_ENV=staging` beside them is harmless. Importing this module has no
|
|
20
|
+
* effect unless some base URL would resolve via the retired environment.
|
|
21
|
+
*/
|
|
22
|
+
import { getApiUrl, getGatewayUrl, getWebsiteUrl, RetiredEnvironmentError } from "./config.js";
|
|
23
|
+
export const RETIRED_ENV_EXIT_CODE = 2;
|
|
24
|
+
/**
|
|
25
|
+
* The startup error line when any CLI base URL would resolve via the retired
|
|
26
|
+
* `MNEMOM_ENV=staging`, else undefined. Exported for the unit test; the module
|
|
27
|
+
* body below is what the CLI relies on.
|
|
28
|
+
*/
|
|
29
|
+
export function retiredEnvMessage() {
|
|
30
|
+
try {
|
|
31
|
+
getApiUrl();
|
|
32
|
+
getGatewayUrl();
|
|
33
|
+
getWebsiteUrl();
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
if (err instanceof RetiredEnvironmentError)
|
|
38
|
+
return `error: ${err.message}`;
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const message = retiredEnvMessage();
|
|
43
|
+
if (message !== undefined) {
|
|
44
|
+
console.error(message);
|
|
45
|
+
process.exit(RETIRED_ENV_EXIT_CODE);
|
|
46
|
+
}
|