@mnemom/mnemom 0.17.20 → 0.17.21

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.
@@ -110,11 +110,13 @@ export declare function formatTrailEntry(e: AgentSessionVerdictTrailEntry): stri
110
110
  */
111
111
  export declare function verdictTrailKey(e: Pick<AgentSessionVerdictTrailEntry, "request_id" | "turn">): string;
112
112
  /**
113
- * Best-effort "has this session ended" check for `--follow`'s exit condition. No field is locked
114
- * in on the wire yet: MNE-8149 adds an `expired` seal state on the gateway's row, and a later
115
- * mnemom-api card is expected to carry it through to this response — so this reads defensively,
116
- * off either the top-level response or the `sealed` block, and returns null (never throws) when
117
- * neither carries a recognized value, which is every server today.
113
+ * Best-effort "has this session ended" check for `--follow`'s exit condition. MNE-8234 wired the
114
+ * seal lifecycle through on the wire (`remote.seal.state`), so that is read first and is
115
+ * authoritative: `"expired"` there means the prior seal lapsed and ends the watch. The older
116
+ * top-level `state` and `remote.sealed.state` fallbacks predate that field actually landing on
117
+ * the response — neither has ever been observed non-null on any real server — and are kept
118
+ * defensively in case an even older/non-conformant server ever carries one of them; this never
119
+ * throws, returning null when nothing recognized is present.
118
120
  */
119
121
  export declare function sessionEndedState(remote: AgentSessionResponse): "expired" | "ended" | null;
120
122
  /** Default and floor for `--follow`'s poll interval. */
@@ -152,6 +154,14 @@ export interface ShowJson {
152
154
  guardrails: AgentSessionResponse["guardrails"];
153
155
  /** R10 (MNE-8200): oldest first (the gateway's own wire order), unlike the human view. */
154
156
  verdict_trail: AgentSessionResponse["verdict_trail"];
157
+ /**
158
+ * MNE-8234: the seal's lifecycle (state incl. `expired`, version, mode, hashes). Absent on
159
+ * an older server; `null` when the API reports no seal.
160
+ */
161
+ seal: AgentSessionResponse["seal"];
162
+ /** MNE-8234 (MNE-8166 R4): judging counts under the current seal. */
163
+ unjudged_turns: AgentSessionResponse["unjudged_turns"];
164
+ memoised_turns: AgentSessionResponse["memoised_turns"];
155
165
  } | {
156
166
  source: "gateway";
157
167
  available: false;
@@ -190,14 +190,21 @@ export function verdictTrailKey(e) {
190
190
  return `${e.request_id ?? ""}#${e.turn}`;
191
191
  }
192
192
  /**
193
- * Best-effort "has this session ended" check for `--follow`'s exit condition. No field is locked
194
- * in on the wire yet: MNE-8149 adds an `expired` seal state on the gateway's row, and a later
195
- * mnemom-api card is expected to carry it through to this response — so this reads defensively,
196
- * off either the top-level response or the `sealed` block, and returns null (never throws) when
197
- * neither carries a recognized value, which is every server today.
193
+ * Best-effort "has this session ended" check for `--follow`'s exit condition. MNE-8234 wired the
194
+ * seal lifecycle through on the wire (`remote.seal.state`), so that is read first and is
195
+ * authoritative: `"expired"` there means the prior seal lapsed and ends the watch. The older
196
+ * top-level `state` and `remote.sealed.state` fallbacks predate that field actually landing on
197
+ * the response — neither has ever been observed non-null on any real server — and are kept
198
+ * defensively in case an even older/non-conformant server ever carries one of them; this never
199
+ * throws, returning null when nothing recognized is present.
198
200
  */
199
201
  export function sessionEndedState(remote) {
200
202
  const as = (v) => v === "expired" || v === "ended" ? v : null;
203
+ if (remote.seal) {
204
+ const sealState = as(remote.seal.state);
205
+ if (sealState)
206
+ return sealState;
207
+ }
201
208
  const top = as(remote.state);
202
209
  if (top)
203
210
  return top;
@@ -349,6 +356,12 @@ export function buildShowView(local, remote, reason) {
349
356
  else {
350
357
  L.push(fmt.dim(" no sealed contract on the gateway for this conversation"));
351
358
  }
359
+ // MNE-8234: the seal's lifecycle — independent of `sealed` above (which carries the contract
360
+ // body, not whether the seal itself is still live or has lapsed). Absent on an older server.
361
+ if (remote.seal) {
362
+ const modeBit = remote.seal.state === "live" ? ` · mode ${remote.seal.mode}` : "";
363
+ L.push(fmt.label("seal: ", `${remote.seal.state.toUpperCase()} (v${remote.seal.version})${modeBit}`));
364
+ }
352
365
  if (remote.verdict) {
353
366
  const v = remote.verdict;
354
367
  L.push(fmt.label("verdict: ", `${v.verdict.toUpperCase()} at turn ${v.turn} — ${v.reason}`));
@@ -385,6 +398,14 @@ export function buildShowView(local, remote, reason) {
385
398
  if (remote.turn !== null) {
386
399
  L.push(fmt.label("turn: ", String(remote.turn)));
387
400
  }
401
+ // MNE-8234 (MNE-8166 R4): judging counts under the current seal. `null`/absent ⇒ no judge
402
+ // ledger counts (older server, or nothing judged under this seal yet) — no line then.
403
+ if (remote.unjudged_turns !== null && remote.unjudged_turns !== undefined) {
404
+ const memoisedBit = remote.memoised_turns !== null && remote.memoised_turns !== undefined
405
+ ? ` · memoised ${remote.memoised_turns}`
406
+ : "";
407
+ L.push(fmt.label("judging: ", `unjudged ${remote.unjudged_turns}${memoisedBit}`));
408
+ }
388
409
  return L;
389
410
  }
390
411
  export function buildShowJson(local, remote, reason) {
@@ -400,6 +421,9 @@ export function buildShowJson(local, remote, reason) {
400
421
  turn: remote.turn,
401
422
  guardrails: remote.guardrails,
402
423
  verdict_trail: remote.verdict_trail,
424
+ seal: remote.seal,
425
+ unjudged_turns: remote.unjudged_turns,
426
+ memoised_turns: remote.memoised_turns,
403
427
  }
404
428
  : { source: "gateway", available: false, reason: reason ?? "unknown" },
405
429
  };
@@ -127,7 +127,7 @@ export interface AgentOptions {
127
127
  /**
128
128
  * Load the "Mnemom for Claude Code" plugin (`plugins/mnemom`: the Mnemom band,
129
129
  * `/mnemom`, the gateway guard, Mnemom attribution) into the launched Claude Code.
130
- * Default OFF (not published yet); `--plugin` ⇒ true; `--no-plugin` ⇒ false; undefined ⇒
130
+ * Default OFF (the plugin lands dark); `--plugin` ⇒ true; `--no-plugin` ⇒ false; undefined ⇒
131
131
  * MNEMOM_AGENT_PLUGIN decides (lib/claude-plugin).
132
132
  */
133
133
  plugin?: boolean;
@@ -26,7 +26,7 @@
26
26
  * an argv value.
27
27
  */
28
28
  import { spawn, spawnSync } from "node:child_process";
29
- import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
29
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
30
30
  import { randomUUID } from "node:crypto";
31
31
  import { tmpdir, homedir, hostname } from "node:os";
32
32
  import { basename, join, dirname } from "node:path";
@@ -539,7 +539,7 @@ export async function agentCommand(scenarioArg, options = {}) {
539
539
  plugin: launchPlugin.line,
540
540
  });
541
541
  if (pluginWanted && cli.name === "claude" && !bundledPluginDir) {
542
- say(fmt.warn("mnemom agent: the Mnemom plugin folder is not in this install (it is not published yet; run from a source checkout) — launching without the Mnemom band and /mnemom."));
542
+ say(fmt.warn("mnemom agent: the Mnemom plugin folder is not in this install — launching without the Mnemom band and /mnemom."));
543
543
  }
544
544
  // Non-secret launch facts for the plugin's band and `/mnemom status`. Built here, applied to the
545
545
  // child env by each launch shape AFTER it has built that env (lib/claude-plugin, PLUGIN_ENV).
@@ -1303,7 +1303,8 @@ function launchRemoteControl(ctx) {
1303
1303
  }
1304
1304
  }
1305
1305
  const proxyScript = resolveProxyScript();
1306
- const rcDir = mkdtempSync(join(tmpdir(), "mnemom-code-rc."));
1306
+ const rcDir = mkdtempSync(join(rcStateRoot(), "mnemom-code-rc."));
1307
+ writeFileSync(join(rcDir, "owner.pid"), `${process.pid}\n`, { mode: 0o600 }); // rc-state.mjs sweep
1307
1308
  const proxyLog = process.env.MNEMOM_AGENT_RC_PROXY_LOG || join(rcDir, "proxy.log");
1308
1309
  const proxy = spawn("node", [proxyScript], {
1309
1310
  env: {
@@ -1519,6 +1520,23 @@ function warnServerDrops(options, yolo) {
1519
1520
  say("mnemom agent: yolo (--dangerously-skip-permissions) is not applied under --server; set permissions from the driven sessions.");
1520
1521
  }
1521
1522
  }
1523
+ /**
1524
+ * ~/.mnemom/rc, created 0700 — the same root src/rc-state.mjs gives the proxy's CA dir (tsc emits
1525
+ * no .mjs, so this mirrors its rcStateRoot() rather than importing it). Never the OS temp dir,
1526
+ * whose cleaners delete files behind a long session; that is only the fallback when home is not
1527
+ * writable. Read at call time, not from MNEMOM_DIR: HOME can move after import (tests do).
1528
+ */
1529
+ function rcStateRoot() {
1530
+ const root = join(homedir(), ".mnemom", "rc");
1531
+ try {
1532
+ mkdirSync(root, { recursive: true, mode: 0o700 });
1533
+ chmodSync(root, 0o700);
1534
+ return root;
1535
+ }
1536
+ catch {
1537
+ return tmpdir();
1538
+ }
1539
+ }
1522
1540
  function resolveProxyScript() {
1523
1541
  const here = dirname(fileURLToPath(import.meta.url));
1524
1542
  const cand = join(here, "..", "rc-proxy.mjs");
@@ -3,7 +3,7 @@
3
3
  * integration). Surface for engineers to check whether staging is safe
4
4
  * to promote to production based on the most recent harness run state.
5
5
  *
6
- * mnemom validate safe-house [--against=staging] [--max-age=24h]
6
+ * mnemom validate safe-house [--against=test] [--max-age=24h]
7
7
  * [--lane=full|fast] [--strict] [--json]
8
8
  *
9
9
  * Wraps `GET /v1/admin/safe-house/harness-state` (mnemom-staff only).
@@ -3,7 +3,7 @@
3
3
  * integration). Surface for engineers to check whether staging is safe
4
4
  * to promote to production based on the most recent harness run state.
5
5
  *
6
- * mnemom validate safe-house [--against=staging] [--max-age=24h]
6
+ * mnemom validate safe-house [--against=test] [--max-age=24h]
7
7
  * [--lane=full|fast] [--strict] [--json]
8
8
  *
9
9
  * Wraps `GET /v1/admin/safe-house/harness-state` (mnemom-staff only).
@@ -76,10 +76,12 @@ export async function validateSafeHouseCommand(opts) {
76
76
  await requireAuth();
77
77
  // `--against` is informational only today (validation is always against
78
78
  // the api root the user is authenticated to). Keep the flag for forward-
79
- // compat: production gains a separate harness corpus per T1.
80
- const against = opts.against ?? "staging";
81
- if (against !== "staging" && against !== "production") {
82
- console.log(fmt.error(`--against must be 'staging' or 'production' (got '${against}')`) + "\n");
79
+ // compat: production gains a separate harness corpus per T1. The non-prod
80
+ // value is `test` (the Azure test cell): `staging` named the retired ring
81
+ // (MNE-7968) and is rejected so a stale script fails loud, not quietly.
82
+ const against = opts.against ?? "test";
83
+ if (against !== "test" && against !== "production") {
84
+ console.log(fmt.error(`--against must be 'test' or 'production' (got '${against}')`) + "\n");
83
85
  process.exit(1);
84
86
  return;
85
87
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./lib/retired-env-guard.js";
2
3
  import { program } from "commander";
3
4
  export { program };
4
5
  /**
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ // MUST stay the first import: it exits cleanly on MNEMOM_ENV=staging before any
3
+ // module below resolves the (retired) staging URLs at import time (MNE-7968).
4
+ import "./lib/retired-env-guard.js";
2
5
  import { program, InvalidArgumentError, Option } from "commander";
3
6
  import { isEntrypoint } from "./lib/entrypoint.js";
4
7
  import { CLI_VERSION } from "./version.js";
@@ -141,7 +144,7 @@ function printAgentHelp(verb) {
141
144
  const agentCmd = program
142
145
  .command("agent [session] [passthrough...]")
143
146
  .description("Launch your governed agent (Claude Code in v1) through the Mnemom gateway — governed identity + optional sealed goal contract. Plain `mnemom agent` asks for a session name. " +
144
- "Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `invite <token>` redeems a Mnemom Agent invitation; `list` / `claim` / `move` manage the agents in your orgs; `sessions` lists your past launches and `show [ref]` merges one with its live goal state; `help [verb]` prints help and never launches.")
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.")
145
148
  .option("--goal <statement>", "Contract goal statement (required if any contract flag is given)")
146
149
  .option("--requirement <text>", "Contract requirement (repeatable)", collectRepeatable, [])
147
150
  .option("--allow <glob>", "Allowed path/glob (repeatable)", collectRepeatable, [])
@@ -1656,7 +1659,7 @@ const validateCmd = program
1656
1659
  validateCmd
1657
1660
  .command("safe-house")
1658
1661
  .description("Render Safe House harness state per lane (full/fast) — promotion-readiness signal")
1659
- .option("--against <env>", "Target environment ('staging' or 'production'; informational)", "staging")
1662
+ .option("--against <env>", "Target environment ('test' or 'production'; informational)", "test")
1660
1663
  .option("--max-age <duration>", "Reject runs older than this (e.g. 30m, 2h, 24h, 1d)", "24h")
1661
1664
  .option("--lane <lane>", "Show only one lane ('full', 'fast', or 'both')", "both")
1662
1665
  .option("--strict", "Exit non-zero if any selected lane is stale or red")
package/dist/lib/api.d.ts CHANGED
@@ -1305,6 +1305,29 @@ export interface AgentSessionVerdictTrailEntry {
1305
1305
  fence_breach: boolean | null;
1306
1306
  at: string;
1307
1307
  }
1308
+ /**
1309
+ * The seal's lifecycle (MNE-8149 R2, MNE-8199 D9/R9, MNE-8234/mnemom-api#3541). `live`: a sealed
1310
+ * contract currently governs — `mode` says whether a crossed guardrail only nudges or enforces
1311
+ * (refuses the next request). `expired`: the prior seal lapsed (24h idle) and the next engaged
1312
+ * request will seal afresh; no `mode` because nothing is being enforced.
1313
+ */
1314
+ export interface AgentSessionSealLive {
1315
+ state: "live";
1316
+ version: number;
1317
+ hash: string;
1318
+ sealed_at: string;
1319
+ reason: "first" | "after_expiry" | null;
1320
+ expired_hash: string | null;
1321
+ updated_from_hash: string | null;
1322
+ mode: "nudge" | "enforce";
1323
+ }
1324
+ export interface AgentSessionSealExpired {
1325
+ state: "expired";
1326
+ version: number;
1327
+ hash: string;
1328
+ sealed_at: string;
1329
+ }
1330
+ export type AgentSessionSeal = AgentSessionSealLive | AgentSessionSealExpired;
1308
1331
  /** `GET /v1/agent/sessions/{conversationId}` 200 body. Nulls mean "nothing stored" — absence is data. */
1309
1332
  export interface AgentSessionResponse {
1310
1333
  conversation_id: string;
@@ -1319,6 +1342,24 @@ export interface AgentSessionResponse {
1319
1342
  guardrails: AgentSessionGuardrails | null;
1320
1343
  /** Absent on an older server (back-compat) — never shown then, never an empty-array assumption. */
1321
1344
  verdict_trail?: AgentSessionVerdictTrailEntry[];
1345
+ /**
1346
+ * MNE-8234: the seal's lifecycle, incl. `expired`. Absent on an older server (back-compat —
1347
+ * never shown then). `null` means the API itself reports no seal: either none was ever made for
1348
+ * this conversation, or the gateway sent a seal payload out of contract and the API dropped it
1349
+ * rather than relay it half-trusted — the two are indistinguishable from this field alone.
1350
+ */
1351
+ seal?: AgentSessionSeal | null;
1352
+ /**
1353
+ * MNE-8166 R4 / MNE-8234: assistant turns under the current seal with no verdict (a cut-off/
1354
+ * aborted response, a judge that failed or never ran, a non-streamed final turn). Absent on an
1355
+ * older server (back-compat). `null` when no judge ledger counts under the current seal.
1356
+ */
1357
+ unjudged_turns?: number | null;
1358
+ /**
1359
+ * MNE-8166 R4 / MNE-8234: turns the judge memo graded (aligned by inheritance, no verdict
1360
+ * stored). Absent on an older server (back-compat). `null` alongside `unjudged_turns`.
1361
+ */
1362
+ memoised_turns?: number | null;
1322
1363
  }
1323
1364
  /**
1324
1365
  * The live goal state of one `mnemom agent` session, read through mnemom-api
@@ -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, AND NOT IN THE PUBLISHED PACKAGE YET. The plugin is committed under
18
- * `cli/plugins/` but `package.json` `files` leaves it out of the npm tarball, so an installed
19
- * `mnemom` has no folder to load and never looks for one. `--plugin` or
20
- * MNEMOM_AGENT_PLUGIN=1|true|yes|on opts a launch in (`--no-plugin` or 0|false|off|no wins over
21
- * the env); from a source checkout that loads the folder, from an install it says the folder is
22
- * not there and launches without it. Turning it on for everyone later is two edits: `plugins`
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 is not published yet.
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, AND NOT IN THE PUBLISHED PACKAGE YET. The plugin is committed under
18
- * `cli/plugins/` but `package.json` `files` leaves it out of the npm tarball, so an installed
19
- * `mnemom` has no folder to load and never looks for one. `--plugin` or
20
- * MNEMOM_AGENT_PLUGIN=1|true|yes|on opts a launch in (`--no-plugin` or 0|false|off|no wins over
21
- * the env); from a source checkout that loads the folder, from an install it says the folder is
22
- * not there and launches without it. Turning it on for everyone later is two edits: `plugins`
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 is not published yet.
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 — run from a source checkout)";
146
+ return "off (the plugin folder is not in this install)";
148
147
  return `${CLAUDE_PLUGIN_NAME} (${pluginDir})`;
149
148
  }
@@ -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" | "staging" | "local";
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";
@@ -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 === "staging" || env === "local")
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/staging/local;
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: $TMPDIR/mnemom-code-rc.*/proxy.log, ~/.mnemom/*rc-proxy*.log
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 d of safeList(tmp)) {
129
- if (d.startsWith("mnemom-code-rc."))
130
- logs.push(join(tmp, d, "proxy.log"));
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
+ }