@mnemom/mnemom 0.16.1 → 0.17.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/agents.d.ts +14 -0
  3. package/dist/commands/agents.js +100 -2
  4. package/dist/commands/card.d.ts +43 -0
  5. package/dist/commands/card.js +153 -102
  6. package/dist/commands/code-config.d.ts +17 -0
  7. package/dist/commands/code-config.js +147 -0
  8. package/dist/commands/code-doctor.d.ts +18 -0
  9. package/dist/commands/code-doctor.js +138 -0
  10. package/dist/commands/code-setup.d.ts +97 -0
  11. package/dist/commands/code-setup.js +330 -0
  12. package/dist/commands/code.d.ts +133 -0
  13. package/dist/commands/code.js +661 -0
  14. package/dist/commands/logs.js +11 -1
  15. package/dist/commands/onboard.d.ts +59 -0
  16. package/dist/commands/onboard.js +395 -0
  17. package/dist/commands/org.d.ts +13 -0
  18. package/dist/commands/org.js +63 -2
  19. package/dist/commands/protection.d.ts +10 -0
  20. package/dist/commands/protection.js +109 -0
  21. package/dist/commands/status.js +5 -0
  22. package/dist/commands/try-me.js +16 -1
  23. package/dist/commands/usage.d.ts +35 -0
  24. package/dist/commands/usage.js +265 -0
  25. package/dist/commands/wrap.d.ts +28 -0
  26. package/dist/commands/wrap.js +331 -0
  27. package/dist/index.js +315 -7
  28. package/dist/lib/agent-config.d.ts +27 -0
  29. package/dist/lib/agent-config.js +86 -0
  30. package/dist/lib/api.d.ts +139 -1
  31. package/dist/lib/api.js +132 -183
  32. package/dist/lib/cli-config.d.ts +33 -0
  33. package/dist/lib/cli-config.js +70 -0
  34. package/dist/lib/code-config.d.ts +78 -0
  35. package/dist/lib/code-config.js +281 -0
  36. package/dist/lib/code.d.ts +154 -0
  37. package/dist/lib/code.js +252 -0
  38. package/dist/lib/config.d.ts +12 -0
  39. package/dist/lib/config.js +55 -3
  40. package/dist/lib/keyed-identity.d.ts +35 -0
  41. package/dist/lib/keyed-identity.js +363 -0
  42. package/dist/lib/protection-drift.d.ts +117 -0
  43. package/dist/lib/protection-drift.js +180 -0
  44. package/dist/lib/skills.js +25 -12
  45. package/dist/lib/version-gate.d.ts +37 -0
  46. package/dist/lib/version-gate.js +84 -0
  47. package/dist/rc-proxy.mjs +341 -0
  48. package/package.json +9 -7
@@ -0,0 +1,252 @@
1
+ /**
2
+ * lib/code.ts — the pure, IO-free core of `mnemom code`, the CLI's first-class
3
+ * governed-coding-agent launcher.
4
+ *
5
+ * `mnemom code` launches your coding-agent CLI (Claude Code in v1) with its
6
+ * dev-time model traffic routed through the Mnemom gateway for observability and
7
+ * governance, under a governed agent identity and (optionally) a governed
8
+ * per-conversation CONTRACT (goal / requirements / path allow-forbid lists +
9
+ * guardrail ceilings) that the gateway SEALS on the first turn of a conversation.
10
+ *
11
+ * Everything here is deterministic and side-effect-free so it can be unit-tested
12
+ * without spawning a process or touching the network: the contract builder + its
13
+ * canonical seal (byte-identical to the gateway's `parseContractHeader` +
14
+ * `canonicalHash`), the guardrail validators, agent-name sanitisation, and the
15
+ * gateway-door resolution. The command shell (commands/code.ts) does the IO —
16
+ * key resolution, env assembly, and spawning the coding-agent CLI.
17
+ *
18
+ * The us-2/PROD gateway is the DEFAULT door (https://gateway.mnemom.ai). An
19
+ * explicit MNEMOM_CODE_GATEWAY still wins (so any other cell is reachable when
20
+ * NAMED), but prod is never selected silently — see `resolveGatewayHost`.
21
+ */
22
+ import { createHash, randomUUID } from "node:crypto";
23
+ /** The us-2/prod gateway host. The default door — never any other cell silently. */
24
+ export const PROD_GATEWAY_HOST = "https://gateway.mnemom.ai";
25
+ /**
26
+ * Resolve the gateway HOST (scheme + host, no path). An explicit, non-empty
27
+ * `MNEMOM_CODE_GATEWAY` wins verbatim; otherwise the us-2/prod host. The default
28
+ * is PROD — an unset var resolves to prod, and any other cell is reached only by
29
+ * NAMING it. Trailing slash trimmed so door concatenation is clean.
30
+ */
31
+ export function resolveGatewayHost(env = process.env) {
32
+ const raw = (env.MNEMOM_CODE_GATEWAY ?? "").trim();
33
+ const host = raw.length > 0 ? raw : PROD_GATEWAY_HOST;
34
+ return host.replace(/\/+$/, "");
35
+ }
36
+ /**
37
+ * The Anthropic door the CLI points Claude Code at:
38
+ * `${MNEMOM_CODE_GATEWAY}/anthropic`, or the full-door override
39
+ * `MNEMOM_CODE_ANTHROPIC_GATEWAY` when set. v1 is Anthropic-only (no router door
40
+ * / Mnemom key), so the /anthropic door carries the whole contract: x-api-key +
41
+ * x-mnemom-{agent,conversation-id,contract}.
42
+ */
43
+ export function resolveAnthropicDoor(env = process.env) {
44
+ const override = (env.MNEMOM_CODE_ANTHROPIC_GATEWAY ?? "").trim();
45
+ if (override.length > 0)
46
+ return override.replace(/\/+$/, "");
47
+ return `${resolveGatewayHost(env)}/anthropic`;
48
+ }
49
+ /** True when the resolved gateway host is the us-2/prod cell (used for the launch banner). */
50
+ export function isProdGateway(env = process.env) {
51
+ return resolveGatewayHost(env) === PROD_GATEWAY_HOST;
52
+ }
53
+ /**
54
+ * Sanitise a header value into a safe wire token: lowercase, alphanumerics and
55
+ * single hyphens only, no leading/trailing hyphen. Returns "" when nothing
56
+ * survives (the caller rejects that).
57
+ */
58
+ export function sanitizeAgentName(raw) {
59
+ return raw
60
+ .toLowerCase()
61
+ .replace(/[^a-z0-9]+/g, "-")
62
+ .replace(/^-+/, "")
63
+ .replace(/-+$/, "");
64
+ }
65
+ /** A single-line, header-safe conversation id: honour a caller value, else a uuid. */
66
+ export function resolveConversationId(raw) {
67
+ const trimmed = (raw ?? "").replace(/[\r\n]+/g, "").trim();
68
+ if (trimmed.length > 0)
69
+ return trimmed;
70
+ return randomUUID().toLowerCase();
71
+ }
72
+ /** A strictly-positive integer (rejects 0, negatives, decimals, NaN, non-numeric strings). */
73
+ export function isPositiveInt(value) {
74
+ const n = typeof value === "number" ? value : Number(String(value).trim());
75
+ return Number.isInteger(n) && n > 0 && /^\d+$/.test(String(value).trim());
76
+ }
77
+ /** A strictly-positive finite number (rejects 0, negatives, NaN, Infinity, non-numeric). */
78
+ export function isPositiveNumber(value) {
79
+ const s = String(value).trim();
80
+ if (!/^([0-9]+(\.[0-9]*)?|\.[0-9]+)$/.test(s))
81
+ return false;
82
+ const n = Number(s);
83
+ return Number.isFinite(n) && n > 0;
84
+ }
85
+ /**
86
+ * Canonical JSON — object keys sorted recursively, arrays in order, no
87
+ * whitespace, `undefined` keys dropped. Byte-identical to the gateway's
88
+ * `canonicalJson` (gateway/src/goal-contract.ts), so the base64 we send and the
89
+ * sha we print match what the gateway seals.
90
+ */
91
+ export function canonicalJson(value) {
92
+ if (value === null || typeof value !== "object")
93
+ return JSON.stringify(value);
94
+ if (Array.isArray(value))
95
+ return `[${value.map(canonicalJson).join(",")}]`;
96
+ const obj = value;
97
+ const keys = Object.keys(obj)
98
+ .filter((k) => obj[k] !== undefined)
99
+ .sort();
100
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
101
+ }
102
+ /** sha256 hex (first 16 chars) of a string — the operator-eyeball contract hash. */
103
+ export function sha256Hex16(text) {
104
+ return createHash("sha256").update(text, "utf8").digest("hex").slice(0, 16);
105
+ }
106
+ /**
107
+ * Was ANY contract/guardrail flag supplied? A contract is built only then; if so
108
+ * `--goal` becomes required (see `assembleContract`). Purely presence-based.
109
+ */
110
+ export function contractFlagsPresent(flags) {
111
+ return (hasText(flags.goal) ||
112
+ (flags.requirement?.length ?? 0) > 0 ||
113
+ (flags.allow?.length ?? 0) > 0 ||
114
+ (flags.forbid?.length ?? 0) > 0 ||
115
+ hasText(flags.goalId) ||
116
+ flags.maxTurns !== undefined ||
117
+ flags.budget !== undefined ||
118
+ flags.stall !== undefined);
119
+ }
120
+ function hasText(v) {
121
+ return typeof v === "string" && v.trim().length > 0;
122
+ }
123
+ /**
124
+ * Build + seal the contract from the raw flags, or return null when no contract
125
+ * flag was given. Throws (with a human message) on the two invalid states the
126
+ * gateway would otherwise silently swallow:
127
+ * - a contract flag present without `--goal` (the statement is required);
128
+ * - a guardrail ceiling that is not strictly positive (`parseContractHeader`
129
+ * drops those, so a "guarded" launch would actually be unguarded).
130
+ *
131
+ * The canonical form OMITS empty arrays and an empty goal_id, so the sha printed
132
+ * here matches whatever the gateway seals for the same flags.
133
+ */
134
+ export function assembleContract(flags) {
135
+ if (!contractFlagsPresent(flags))
136
+ return null;
137
+ const statement = (flags.goal ?? "").trim();
138
+ if (!statement) {
139
+ throw new Error("--goal is required when any contract flag " +
140
+ "(--requirement/--allow/--forbid/--goal-id/--budget/--max-turns/--stall) is given.");
141
+ }
142
+ const contract = { statement };
143
+ const requirements = cleanList(flags.requirement);
144
+ if (requirements.length)
145
+ contract.requirements = requirements;
146
+ const allowed = cleanList(flags.allow);
147
+ if (allowed.length)
148
+ contract.allowed_paths = allowed;
149
+ const forbidden = cleanList(flags.forbid);
150
+ if (forbidden.length)
151
+ contract.forbidden_paths = forbidden;
152
+ const goalId = (flags.goalId ?? "").trim();
153
+ if (goalId)
154
+ contract.goal_id = goalId;
155
+ if (flags.maxTurns !== undefined) {
156
+ if (!isPositiveInt(flags.maxTurns)) {
157
+ throw new Error(`--max-turns must be a positive integer (got '${flags.maxTurns}').`);
158
+ }
159
+ contract.max_turns = Number(flags.maxTurns);
160
+ }
161
+ if (flags.budget !== undefined) {
162
+ if (!isPositiveNumber(flags.budget)) {
163
+ throw new Error(`--budget must be a positive USD amount (got '${flags.budget}').`);
164
+ }
165
+ contract.budget_usd = Number(flags.budget);
166
+ }
167
+ if (flags.stall !== undefined) {
168
+ if (!isPositiveInt(flags.stall)) {
169
+ throw new Error(`--stall must be a positive integer (got '${flags.stall}').`);
170
+ }
171
+ contract.stall_turns = Number(flags.stall);
172
+ }
173
+ const canonical = canonicalJson(contract);
174
+ return {
175
+ contract,
176
+ canonical,
177
+ base64: Buffer.from(canonical, "utf8").toString("base64"),
178
+ sha: sha256Hex16(canonical),
179
+ };
180
+ }
181
+ /** Trim, drop empties — matches the gateway's `stringArray` filtering. */
182
+ function cleanList(values) {
183
+ return (values ?? []).map((v) => v.trim()).filter((v) => v.length > 0);
184
+ }
185
+ /**
186
+ * Resolve the launch shape from the flags and the `MNEMOM_CODE_REMOTE_CONTROL`
187
+ * env var (the flags win). Default is `terminal` — a plain governed launch in
188
+ * THIS terminal. `--remote-control` → `remote-control` (interactive RC in this
189
+ * terminal); `--server` (or `MNEMOM_CODE_REMOTE_CONTROL=server`) → `server` (the
190
+ * headless `claude remote-control` dispatcher). Throws on an invalid env value.
191
+ */
192
+ export function resolveLaunchShape(opts, env = process.env) {
193
+ let rc = false;
194
+ let server = false;
195
+ const envRc = (env.MNEMOM_CODE_REMOTE_CONTROL ?? "").trim();
196
+ switch (envRc) {
197
+ case "":
198
+ case "0":
199
+ break;
200
+ case "1":
201
+ case "terminal":
202
+ rc = true;
203
+ break;
204
+ case "server":
205
+ rc = true;
206
+ server = true;
207
+ break;
208
+ default:
209
+ throw new Error(`MNEMOM_CODE_REMOTE_CONTROL must be 0, 1, terminal or server (got '${envRc}').`);
210
+ }
211
+ if (opts.remoteControl)
212
+ rc = true;
213
+ if (opts.server) {
214
+ rc = true;
215
+ server = true;
216
+ }
217
+ if (server)
218
+ return "server";
219
+ if (rc)
220
+ return "remote-control";
221
+ return "terminal";
222
+ }
223
+ /**
224
+ * Did anyone EXPLICITLY choose a launch shape — a `--remote-control`/`--server`
225
+ * flag, or a non-empty `MNEMOM_CODE_REMOTE_CONTROL` (which config.launch also
226
+ * sets)? When false, the command is free to AUTO-select the best shape: prefer
227
+ * Remote-Control-in-terminal when the machine can do it, else terminal-only. An
228
+ * explicit `terminal` choice (config `launch = "terminal"` → env "0") is
229
+ * explicit and must be respected — never auto-upgraded.
230
+ */
231
+ export function launchPreferenceExplicit(opts, env = process.env) {
232
+ if (opts.remoteControl || opts.server)
233
+ return true;
234
+ return (env.MNEMOM_CODE_REMOTE_CONTROL ?? "").trim() !== "";
235
+ }
236
+ /**
237
+ * Build the `ANTHROPIC_CUSTOM_HEADERS` value for the terminal-only launch: the
238
+ * real Anthropic key as `x-api-key`, plus the governed-identity headers and the
239
+ * sealed contract when present. Newline-separated, exactly the wire shape Claude
240
+ * Code sends to the /anthropic door. NB: the returned string contains the key —
241
+ * callers must place it only in the child's env, never in a log line.
242
+ */
243
+ export function buildCustomHeaders(opts) {
244
+ const lines = [
245
+ `x-api-key: ${opts.anthropicKey}`,
246
+ `x-mnemom-agent: ${opts.agent}`,
247
+ `x-mnemom-conversation-id: ${opts.conversationId}`,
248
+ ];
249
+ if (opts.contractB64)
250
+ lines.push(`x-mnemom-contract: ${opts.contractB64}`);
251
+ return lines.join("\n");
252
+ }
@@ -13,6 +13,18 @@ export type Environment = "production" | "staging" | "local";
13
13
  * Defaults to production.
14
14
  */
15
15
  export declare function getEnvironment(): Environment;
16
+ export type CellRing = "us-1";
17
+ /** Explicitly select a ring for this process (clears on undefined). Wins over MNEMOM_RING. */
18
+ export declare function setRing(ring: CellRing | undefined): void;
19
+ /**
20
+ * Resolve the active ring, if any: an explicit `setRing()` call wins over
21
+ * the `MNEMOM_RING` env var. Returns undefined for an unset or unrecognized
22
+ * value — an unknown ring name is deliberately NOT resolved to production;
23
+ * callers fall through to the existing Environment-based URLs instead.
24
+ */
25
+ export declare function getRing(): CellRing | undefined;
26
+ /** Override the active API base for this process (clears on undefined/empty). */
27
+ export declare function setApiUrlOverride(url: string | undefined): void;
16
28
  export declare function getApiUrl(): string;
17
29
  export declare function getGatewayUrl(): string;
18
30
  export declare function getWebsiteUrl(): string;
@@ -34,12 +34,64 @@ export function getEnvironment() {
34
34
  return env;
35
35
  return "production";
36
36
  }
37
+ const RING_URLS = {
38
+ "us-1": {
39
+ api: "https://api-us1.mnemom.ai",
40
+ gateway: "https://gateway-us1.mnemom.ai",
41
+ website: "https://preview.mnemom.ai",
42
+ },
43
+ };
44
+ function isCellRing(value) {
45
+ return Object.prototype.hasOwnProperty.call(RING_URLS, value);
46
+ }
47
+ let ringOverride;
48
+ /** Explicitly select a ring for this process (clears on undefined). Wins over MNEMOM_RING. */
49
+ export function setRing(ring) {
50
+ ringOverride = ring;
51
+ }
52
+ /**
53
+ * Resolve the active ring, if any: an explicit `setRing()` call wins over
54
+ * the `MNEMOM_RING` env var. Returns undefined for an unset or unrecognized
55
+ * value — an unknown ring name is deliberately NOT resolved to production;
56
+ * callers fall through to the existing Environment-based URLs instead.
57
+ */
58
+ export function getRing() {
59
+ if (ringOverride)
60
+ return ringOverride;
61
+ const env = process.env.MNEMOM_RING?.trim();
62
+ return env && isCellRing(env) ? env : undefined;
63
+ }
64
+ // Process-level base overrides. MNEMOM_ENV only selects prod/staging/local;
65
+ // to target an arbitrary ring (preview/us-1, test) a command can set an
66
+ // explicit override (e.g. `try-me --api <ring>`) or the operator can export
67
+ // MNEMOM_API_URL / MNEMOM_GATEWAY_URL / MNEMOM_WEBSITE_URL. Crucially these
68
+ // flow through EVERY surface — including OAuth discovery + the one-click claim
69
+ // login — so a non-prod ring's sign-in no longer falls back to the prod AS.
70
+ //
71
+ // Precedence (highest wins): apiUrlOverride > raw MNEMOM_*_URL env >
72
+ // ring (setRing()/MNEMOM_RING) > Environment table. A ring only fills the
73
+ // gap the raw overrides leave — it never has to be perfectly bypass-able,
74
+ // because the raw overrides above it in the chain always can.
75
+ let apiUrlOverride;
76
+ /** Override the active API base for this process (clears on undefined/empty). */
77
+ export function setApiUrlOverride(url) {
78
+ const trimmed = url?.trim().replace(/\/+$/, "");
79
+ apiUrlOverride = trimmed || undefined;
80
+ }
81
+ function envBase(name) {
82
+ const v = process.env[name]?.trim();
83
+ return v ? v.replace(/\/+$/, "") : undefined;
84
+ }
85
+ function ringUrls() {
86
+ const ring = getRing();
87
+ return ring ? RING_URLS[ring] : undefined;
88
+ }
37
89
  export function getApiUrl() {
38
- return API_URLS[getEnvironment()];
90
+ return (apiUrlOverride ?? envBase("MNEMOM_API_URL") ?? ringUrls()?.api ?? API_URLS[getEnvironment()]);
39
91
  }
40
92
  export function getGatewayUrl() {
41
- return GATEWAY_URLS[getEnvironment()];
93
+ return envBase("MNEMOM_GATEWAY_URL") ?? ringUrls()?.gateway ?? GATEWAY_URLS[getEnvironment()];
42
94
  }
43
95
  export function getWebsiteUrl() {
44
- return WEBSITE_URLS[getEnvironment()];
96
+ return envBase("MNEMOM_WEBSITE_URL") ?? ringUrls()?.website ?? WEBSITE_URLS[getEnvironment()];
45
97
  }
@@ -0,0 +1,35 @@
1
+ import { type ValidationCheck } from "../commands/protection.js";
2
+ export declare const KEYED_MODES: readonly ["observe", "nudge", "enforce"];
3
+ export type KeyedMode = (typeof KEYED_MODES)[number];
4
+ export declare const EVAL_BATTERY_MODEL: "claude-opus-5";
5
+ export interface KeyedModeEntry {
6
+ label?: unknown;
7
+ mode?: unknown;
8
+ model?: unknown;
9
+ agent_id?: unknown;
10
+ agent_hash?: unknown;
11
+ secret_ref?: unknown;
12
+ snapshot?: unknown;
13
+ }
14
+ export interface KeyedDirectLane {
15
+ label?: unknown;
16
+ mode?: unknown;
17
+ path?: unknown;
18
+ secret_ref?: unknown;
19
+ model?: unknown;
20
+ snapshot?: unknown;
21
+ agent_id?: unknown;
22
+ agent_hash?: unknown;
23
+ }
24
+ /**
25
+ * Validate the keyed-mode manifest against the three parsed snapshots.
26
+ *
27
+ * @param manifest Parsed `cards/keyed-modes.manifest.yaml` (`{ entries: [...] }`).
28
+ * @param snapshots Map of snapshot filename → parsed protection card, keyed by
29
+ * the `snapshot:` value each manifest entry declares.
30
+ * @returns Flat list of `{ name, passed, message }` checks (mirrors
31
+ * ValidationCheck) so callers can render/aggregate. A returned list
32
+ * with every `passed === true` means the record is internally
33
+ * consistent; any `passed === false` is a failing invariant.
34
+ */
35
+ export declare function validateKeyedModeManifest(manifest: Record<string, unknown>, snapshots: Record<string, Record<string, unknown>>): ValidationCheck[];