@mnemom/mnemom 0.17.19 → 0.17.20

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.
@@ -52,6 +52,9 @@ export declare function assertCodeEnabled(orgId: string, orgLabel: string): Prom
52
52
  /** Ensures a Mnemom session exists (injected so tests never touch the real login flow). */
53
53
  export type EnsureSession = (opts: {
54
54
  interactive?: boolean;
55
+ /** The invitation being redeemed — carried into the browser sign-in so a new
56
+ * invitee's sign-up skips the confirmation email. */
57
+ inviteToken?: string;
55
58
  }) => Promise<boolean>;
56
59
  /** The accept route's 403 codes (mnemom-api `product-invite-accept.ts`). */
57
60
  export declare const INVITE_ACCOUNT_MISMATCH = "invite_account_mismatch";
@@ -210,7 +210,7 @@ export async function agentInviteCommand(token, opts = {}) {
210
210
  }
211
211
  const interactive = opts.interactive ?? isInteractive();
212
212
  const ensureSession = opts.ensureSession ?? defaultEnsureSession;
213
- const haveSession = await ensureSession({ interactive });
213
+ const haveSession = await ensureSession({ interactive, inviteToken: trimmed });
214
214
  if (!haveSession) {
215
215
  throw new Error("mnemom agent --invite: redeeming needs a Mnemom account.\n" +
216
216
  " Sign in: mnemom login\n" +
@@ -218,6 +218,7 @@ export declare function isQuietPreambleExit(err: unknown): boolean;
218
218
  */
219
219
  export declare function ensureMnemomSession(opts?: {
220
220
  interactive?: boolean;
221
+ inviteToken?: string;
221
222
  }): Promise<boolean>;
222
223
  /**
223
224
  * Enforce a positive Mnemom Units balance on the org the agent lives in.
@@ -621,16 +621,26 @@ export async function ensureMnemomSession(opts = {}) {
621
621
  if (!(await askYesNo("Sign in or create your account now?", true)))
622
622
  return false;
623
623
  const device = headlessEnv();
624
+ // The device-code page has no way to carry the invitation, so a NEW invitee
625
+ // signing up there gets the confirmation email — say so instead of dropping
626
+ // the invitation silently. (The browser path carries it; see loginWithLoopback.)
627
+ const deviceSignIn = async () => {
628
+ if (opts.inviteToken) {
629
+ say(fmt.dim(" New to Mnemom? Signing up from a device code asks you to confirm your email first."));
630
+ say(fmt.dim(" To skip that, open the link in your invitation email on a machine with a browser."));
631
+ }
632
+ await loginWithDeviceFlow();
633
+ };
624
634
  try {
625
635
  if (device)
626
- await loginWithDeviceFlow();
636
+ await deviceSignIn();
627
637
  else
628
- await loginWithBrowser();
638
+ await loginWithBrowser({ inviteToken: opts.inviteToken });
629
639
  }
630
640
  catch (err) {
631
641
  if (!device) {
632
642
  say(fmt.dim(" Browser sign-in didn't work here — falling back to a device code…"));
633
- await loginWithDeviceFlow();
643
+ await deviceSignIn();
634
644
  }
635
645
  else {
636
646
  throw err;
@@ -124,6 +124,13 @@ export interface AgentOptions {
124
124
  * session-config header's `experimental` array. Absent/empty ⇒ none of the three.
125
125
  */
126
126
  experimental?: readonly ExperimentalId[];
127
+ /**
128
+ * Load the "Mnemom for Claude Code" plugin (`plugins/mnemom`: the Mnemom band,
129
+ * `/mnemom`, the gateway guard, Mnemom attribution) into the launched Claude Code.
130
+ * Default OFF (not published yet); `--plugin` ⇒ true; `--no-plugin` ⇒ false; undefined ⇒
131
+ * MNEMOM_AGENT_PLUGIN decides (lib/claude-plugin).
132
+ */
133
+ plugin?: boolean;
127
134
  passthrough?: string[];
128
135
  /** Print the resolved launch plan and exit — no key read, no spawn. */
129
136
  dryRun?: boolean;
@@ -335,6 +342,8 @@ export declare function buildCliArgs(options: AgentOptions, ctx: {
335
342
  allowCliFlags: boolean;
336
343
  ccSessionId?: string;
337
344
  handoffMcpConfigPath?: string;
345
+ /** The bundled Mnemom plugin folder → `--plugin-dir <dir>` (lib/claude-plugin). */
346
+ pluginDir?: string;
338
347
  }): string[];
339
348
  /**
340
349
  * The Claude Code session id a launch runs under, from the caller's passthrough (implicit-goal
@@ -365,6 +374,8 @@ export declare function logLaunch(ctx: {
365
374
  envelope: SealedContractEnvelope | null;
366
375
  goalMode: string | null;
367
376
  cli: ResolvedCli;
377
+ /** The plugin line (`pluginPlanLine`): `off`, `off (<why>)` or `<name> (<dir>)`. */
378
+ plugin: string;
368
379
  }): void;
369
380
  /** The launch line for the yolo default: permission prompts are skipped, and how to keep them. */
370
381
  export declare function permissionPromptsLine(cli: ResolvedCli): string;
@@ -392,5 +403,7 @@ export declare function printPlan(ctx: {
392
403
  sessionConfig: Record<string, string>;
393
404
  /** The experimental features on for this launch (the header's `experimental` array). */
394
405
  experimental?: readonly string[];
406
+ /** The bundled Claude Code plugin line (`pluginPlanLine`); always printed, like the launch log. */
407
+ plugin: string;
395
408
  }): void;
396
409
  export {};
@@ -48,6 +48,8 @@ import { defaultAgentSlug, ensureClaimedServedAgent, ensureSessionAndBalance, is
48
48
  import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, assertOffProfileCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, resolveUsageSavings, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, resumeCommand, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
49
49
  import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
50
50
  import { slackChannelArgs } from "../lib/slack-launch.js";
51
+ import { appendPluginDirsEnv, applyPluginEnv, pluginEnabled, pluginPlanLine, resolvePluginDir, } from "../lib/claude-plugin.js";
52
+ import { CLI_VERSION } from "../version.js";
51
53
  /** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
52
54
  const CODE_STORE = LAUNCHER_STORE_PATH;
53
55
  const say = (line = "") => console.error(line);
@@ -194,6 +196,13 @@ export async function agentCommand(scenarioArg, options = {}) {
194
196
  if (cellError)
195
197
  throw new Error(cellError);
196
198
  const cellNote = isProdGateway(env) ? "us-2/prod" : `custom (${gatewayHost})`;
199
+ // ── the bundled Claude Code plugin (lib/claude-plugin) ── resolved up front so the dry-run plan
200
+ // and the launch log print the same line. Only Claude Code proper takes `--plugin-dir`.
201
+ const pluginWanted = pluginEnabled(options.plugin, env);
202
+ const bundledPluginDir = pluginWanted ? resolvePluginDir() : null;
203
+ const pluginFor = (cliName) => cliName !== "claude" && pluginWanted
204
+ ? { dir: null, line: `off (${cliName} is not Claude Code)` }
205
+ : { dir: bundledPluginDir, line: pluginPlanLine(bundledPluginDir, pluginWanted) };
197
206
  const yolo = options.yolo !== false; // default ON
198
207
  const contextHint = options.contextHint !== false; // default ON
199
208
  // Context profile: none (null — the cell's own default, no fold key sent) unless
@@ -303,6 +312,7 @@ export async function agentCommand(scenarioArg, options = {}) {
303
312
  compactAt,
304
313
  sessionConfig,
305
314
  experimental,
315
+ plugin: pluginFor(planCli.name).line,
306
316
  });
307
317
  if (auth === "subscription") {
308
318
  say("mnemom agent: auth = subscription — Claude Code would use your claude.ai login (Team/Enterprise plans only) with a Mnemom API key; no Anthropic key.");
@@ -516,7 +526,46 @@ export async function agentCommand(scenarioArg, options = {}) {
516
526
  contextProfile,
517
527
  compactAt: contextProfile === "off" ? null : compactAt,
518
528
  });
519
- logLaunch({ shape, door, agent, conversationId, cellNote, envelope, goalMode, cli });
529
+ const launchPlugin = pluginFor(cli.name);
530
+ logLaunch({
531
+ shape,
532
+ door,
533
+ agent,
534
+ conversationId,
535
+ cellNote,
536
+ envelope,
537
+ goalMode,
538
+ cli,
539
+ plugin: launchPlugin.line,
540
+ });
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."));
543
+ }
544
+ // Non-secret launch facts for the plugin's band and `/mnemom status`. Built here, applied to the
545
+ // child env by each launch shape AFTER it has built that env (lib/claude-plugin, PLUGIN_ENV).
546
+ const plugin = launchPlugin.dir
547
+ ? {
548
+ dir: launchPlugin.dir,
549
+ launch: {
550
+ gatewayHost,
551
+ apiUrl: getApiUrl(),
552
+ agent,
553
+ conversationId,
554
+ cellNote,
555
+ shape,
556
+ contractSha: envelope?.sha,
557
+ contract: envelope?.canonical,
558
+ goalMode,
559
+ scenario,
560
+ launcherVersion: CLI_VERSION,
561
+ // The mod re-runs this CLI (`agent show --json`, `agent goal show --json`) for the live
562
+ // goal state; execArgv keeps a `tsx`/`--import` dev launch runnable the same way.
563
+ launcherExec: process.argv[1]
564
+ ? [process.execPath, ...process.execArgv, process.argv[1]]
565
+ : [],
566
+ },
567
+ }
568
+ : undefined;
520
569
  if (expNotice)
521
570
  say(expNotice);
522
571
  say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
@@ -586,6 +635,7 @@ export async function agentCommand(scenarioArg, options = {}) {
586
635
  ccSessionId: id,
587
636
  }),
588
637
  knownCcSessionId: cc.id ?? continuedCcSessionId,
638
+ plugin,
589
639
  });
590
640
  return; // launchTerminal exits the process
591
641
  }
@@ -602,6 +652,7 @@ export async function agentCommand(scenarioArg, options = {}) {
602
652
  goalMode,
603
653
  sessionConfigB64,
604
654
  handoffMcpConfigPath,
655
+ plugin,
605
656
  options: launchOptions,
606
657
  yolo,
607
658
  ccSessionId,
@@ -1081,6 +1132,7 @@ function launchTerminal(ctx) {
1081
1132
  allowCliFlags: true,
1082
1133
  ccSessionId: ctx.ccSessionId,
1083
1134
  handoffMcpConfigPath: ctx.handoffMcpConfigPath,
1135
+ pluginDir: ctx.plugin?.dir,
1084
1136
  });
1085
1137
  // Claude Code logged in via claude.ai (OAuth) PREEMPTS ANTHROPIC_API_KEY, so
1086
1138
  // it is force-unset; the real key rides in ANTHROPIC_CUSTOM_HEADERS and
@@ -1102,6 +1154,9 @@ function launchTerminal(ctx) {
1102
1154
  // Both auth arms point ANTHROPIC_BASE_URL at the gateway door, which makes Claude Code turn
1103
1155
  // tool search off and inline every tool schema on every request — unless told otherwise.
1104
1156
  applyToolSearchDefault(childEnv);
1157
+ // The plugin's non-secret launch facts (MNEMOM_CC_*): never a key, never a header value.
1158
+ if (ctx.plugin)
1159
+ applyPluginEnv(childEnv, ctx.plugin.launch);
1105
1160
  const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
1106
1161
  childEnv[SESSION_NAME_ENV] = sessionName;
1107
1162
  // Exact compaction signal for the gateway (x-claude-code-request-class: compaction), on every
@@ -1397,6 +1452,10 @@ function spawnRcCli(ctx) {
1397
1452
  if (ctx.handoffMcpConfigPath && ctx.anthropicKey) {
1398
1453
  childEnv[HANDOFF_RC_KEY_ENV_NAME] = ctx.anthropicKey;
1399
1454
  }
1455
+ // The plugin's non-secret launch facts (MNEMOM_CC_*), also after the strip loop: not an
1456
+ // ANTHROPIC_* var, so the RC gate ignores them, and nothing here is a key or a header value.
1457
+ if (ctx.plugin)
1458
+ applyPluginEnv(childEnv, ctx.plugin.launch);
1400
1459
  let args;
1401
1460
  if (ctx.shape === "server") {
1402
1461
  // Headless dispatcher: no --model/--effort/--trim-mcp/yolo flags; the model
@@ -1408,6 +1467,10 @@ function spawnRcCli(ctx) {
1408
1467
  say("mnemom agent: --slack is not applied to a headless server (a channel server needs an interactive confirmation); use the terminal or remote-control shape.");
1409
1468
  }
1410
1469
  args = ["remote-control", "--name", rcName, ...passthrough];
1470
+ // No `--plugin-dir` on the headless server: the env path list loads the plugin into every
1471
+ // session it spawns instead (a user's own list is kept).
1472
+ if (ctx.plugin)
1473
+ appendPluginDirsEnv(childEnv, ctx.plugin.dir);
1411
1474
  // Additive, never `--strict-mcp-config`: --trim-mcp is not applied under --server (every
1412
1475
  // dispatched session keeps its full MCP set, unrestricted — see warnServerDrops below), so
1413
1476
  // the handoff tool rides in on TOP of that set rather than replacing it.
@@ -1424,6 +1487,7 @@ function spawnRcCli(ctx) {
1424
1487
  allowCliFlags: true,
1425
1488
  ccSessionId: ctx.ccSessionId,
1426
1489
  handoffMcpConfigPath: ctx.handoffMcpConfigPath,
1490
+ pluginDir: ctx.plugin?.dir,
1427
1491
  });
1428
1492
  args = ["--remote-control", rcName, ...flagArgs];
1429
1493
  if (ctx.options.slack)
@@ -1540,6 +1604,8 @@ export function buildCliArgs(options, ctx) {
1540
1604
  args.push("--dangerously-skip-permissions");
1541
1605
  if (ctx.ccSessionId)
1542
1606
  args.push("--session-id", ctx.ccSessionId);
1607
+ if (ctx.pluginDir)
1608
+ args.push("--plugin-dir", ctx.pluginDir);
1543
1609
  }
1544
1610
  if (options.passthrough?.length)
1545
1611
  args.push(...options.passthrough);
@@ -1650,6 +1716,7 @@ export function goalAlignmentMode(envelope, goalMode) {
1650
1716
  }
1651
1717
  export function logLaunch(ctx) {
1652
1718
  say(`mnemom agent: routing ${ctx.cli.name} through ${ctx.door} [${ctx.cellNote}] as '${ctx.agent}' (conversation ${ctx.conversationId}).`);
1719
+ say(`mnemom agent: plugin: ${ctx.plugin}`);
1653
1720
  if (ctx.envelope) {
1654
1721
  say(`mnemom agent: contract statement: ${ctx.envelope.contract.statement}`);
1655
1722
  say(`mnemom agent: contract sha256[0:16]: ${ctx.envelope.sha}`);
@@ -1673,6 +1740,7 @@ export function printPlan(ctx) {
1673
1740
  L.push("mnemom agent — launch plan (dry run)");
1674
1741
  L.push(` scenario: ${ctx.scenario}`);
1675
1742
  L.push(` cli: ${ctx.cli.name} (${ctx.cli.bin})`);
1743
+ L.push(` plugin: ${ctx.plugin}`);
1676
1744
  L.push(` shape: ${ctx.shape}`);
1677
1745
  L.push(` door: ${ctx.door} [${ctx.cellNote}]`);
1678
1746
  L.push(` agent: ${ctx.agent}`);
package/dist/index.js CHANGED
@@ -182,6 +182,11 @@ const agentCmd = program
182
182
  .option("--no-governance", "Turn governance OFF for the agent's cards, autonomy policy checks included (by default new agents run autonomy observe, rules only; a card refresh otherwise keeps an existing agent's modes)")
183
183
  .option("--no-yolo", "Do NOT pass --dangerously-skip-permissions (yolo is on by default)")
184
184
  .option("--no-context-hint", "Do NOT seed Claude Code's context-hint feature flag before launch")
185
+ // The Mnemom plugin lands dark: off by default and not in the published package, so its two
186
+ // flags stay out of --help until the shipping PR turns it on (lib/claude-plugin.ts). They still
187
+ // parse, for a source checkout.
188
+ .addOption(new Option("--plugin", "Load the Mnemom plugin (the Mnemom band, /mnemom, the gateway guard) into Claude Code — off by default and not in the published package yet (needs a source checkout; MNEMOM_AGENT_PLUGIN=1 does the same)").hideHelp())
189
+ .addOption(new Option("--no-plugin", "Do NOT load the Mnemom plugin even when MNEMOM_AGENT_PLUGIN=1").hideHelp())
185
190
  .addOption(new Option("--slack", "EXPERIMENTAL (`mnemom experimental enable slack`): attach the Slack channel — messages addressed to this session arrive in it, and it can reply (see `mnemom agent slack`)").hideHelp(hiddenUnless("slack")))
186
191
  .option("--context", "Force context folding ON for this launch even when launcher.toml (context = false) or MNEMOM_AGENT_CONTEXT turns it off")
187
192
  .option("--no-context", "Disable context folding for this launch (folding is on by default; tells the gateway to skip it for this session)")
@@ -366,6 +371,7 @@ const agentCmd = program
366
371
  trimMcp: opts.trimMcp,
367
372
  yolo: opts.yolo,
368
373
  contextHint: opts.contextHint,
374
+ plugin: opts.plugin,
369
375
  slack: opts.slack ?? cfg.slackListen,
370
376
  context: opts.context,
371
377
  contextProfile: opts.contextProfile,
@@ -99,7 +99,9 @@ export declare function isLoggedIn(): Promise<boolean>;
99
99
  * loopback redirect (the `wrangler login` pattern). Persists the resulting
100
100
  * SCOPED tokens (plus the client_id needed to refresh) to ~/.mnemom/auth.json.
101
101
  */
102
- export declare function loginWithBrowser(): Promise<AuthTokens>;
102
+ export declare function loginWithBrowser(opts?: {
103
+ inviteToken?: string;
104
+ }): Promise<AuthTokens>;
103
105
  /**
104
106
  * Headless login: RFC 8628 device authorization grant. Shows a user_code +
105
107
  * verification_uri for the user to approve in any browser (possibly on another
package/dist/lib/auth.js CHANGED
@@ -212,8 +212,8 @@ export async function isLoggedIn() {
212
212
  * loopback redirect (the `wrangler login` pattern). Persists the resulting
213
213
  * SCOPED tokens (plus the client_id needed to refresh) to ~/.mnemom/auth.json.
214
214
  */
215
- export async function loginWithBrowser() {
216
- const { tokens, clientId } = await loginWithLoopback();
215
+ export async function loginWithBrowser(opts = {}) {
216
+ const { tokens, clientId } = await loginWithLoopback(undefined, opts);
217
217
  const stored = fromOAuthTokens(tokens, clientId);
218
218
  saveAuthTokens(stored);
219
219
  return stored;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The bundled "Mnemom for Claude Code" plugin (`cli/plugins/mnemom`): a Claude Code
3
+ * mod + skill + MCP config that `mnemom agent` loads into the Claude Code it launches, so the
4
+ * session SHOWS it is governed — the Mnemom band above the prompt, `/mnemom` commands, the ◆ sign
5
+ * on the spinner, Mnemom trailers in commit attribution, and a guard that keeps the routing in
6
+ * place.
7
+ *
8
+ * The launcher tells the mod about the launch through NON-SECRET env vars. They carry a distinct
9
+ * `MNEMOM_CC_*` prefix so they can never be confused with the launcher's own `MNEMOM_AGENT_*`
10
+ * inputs, and none of them is a key, a header value or anything derived from one: the mod reads
11
+ * them by literal name (`claude plugin validate` lists every name), and a stray `env` dump of the
12
+ * child shows nothing it should not.
13
+ *
14
+ * Only Claude Code proper (`cli.name === "claude"`) understands `--plugin-dir`; every other
15
+ * adapter launches without the plugin.
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.
24
+ */
25
+ /** The plugin's `name` in its plugin.json, and its folder under `cli/plugins/`. */
26
+ export declare const CLAUDE_PLUGIN_NAME = "mnemom";
27
+ /** The env Claude Code reads a path list of extra plugin folders from (where no flag can be given). */
28
+ export declare const CLAUDE_PLUGIN_DIRS_ENV = "CLAUDE_CODE_PLUGIN_DIRS";
29
+ /** The env vars the launcher exports for the mod. Every value is non-secret. */
30
+ export declare const PLUGIN_ENV: {
31
+ /** `https://gateway.mnemom.ai` (no path): presence = the session is governed. */
32
+ readonly gatewayHost: "MNEMOM_CC_GATEWAY_HOST";
33
+ /** The public API origin the Trust Rating is read from (`https://api.mnemom.ai`). */
34
+ readonly apiUrl: "MNEMOM_CC_API_URL";
35
+ /** The governed agent's name (the `x-mnemom-agent` header's value). */
36
+ readonly agent: "MNEMOM_CC_AGENT";
37
+ readonly conversationId: "MNEMOM_CC_CONVERSATION_ID";
38
+ /** The cell note the launch log prints (`us-2/prod`, or `custom (<host>)`). */
39
+ readonly cell: "MNEMOM_CC_CELL";
40
+ /** `terminal` | `remote-control` | `server`. */
41
+ readonly shape: "MNEMOM_CC_SHAPE";
42
+ /** `sealed` (a contract rides in the header) | `implicit` | `off`. */
43
+ readonly goalMode: "MNEMOM_CC_GOAL_MODE";
44
+ /** The sealed contract's sha256[0:16]; empty unless sealed. */
45
+ readonly contractSha: "MNEMOM_CC_CONTRACT_SHA";
46
+ /**
47
+ * The sealed contract itself — the canonical JSON the gateway hashes (statement, requirements,
48
+ * allowed/forbidden paths, ceilings). Empty unless sealed. It is what `--goal`/`--requirement`/…
49
+ * said on the command line: the person's own words, no secret in it.
50
+ */
51
+ readonly contract: "MNEMOM_CC_CONTRACT";
52
+ /**
53
+ * The implicit mode's guardrail ceilings, `max_turns=N; budget_usd=X; stall_turns=N` (the
54
+ * `x-mnemom-goal-mode` header minus its leading `implicit`). Empty unless implicit with ceilings.
55
+ */
56
+ readonly guardrails: "MNEMOM_CC_GUARDRAILS";
57
+ /**
58
+ * How to run this very `mnemom` again, as a JSON argv (`[node, ...execArgv, script]`): the mod
59
+ * runs `mnemom agent show <conversation> --json` / `mnemom agent goal show …` through it to read
60
+ * the live goal state (the sealed verdict, the inferred goal, guardrail usage). The CLI
61
+ * authenticates with the launch key from its own ledger; the mod never holds the key.
62
+ */
63
+ readonly launcherExec: "MNEMOM_CC_LAUNCHER_EXEC";
64
+ /** The `mnemom agent <session>` scenario name. */
65
+ readonly session: "MNEMOM_CC_SESSION";
66
+ readonly launcherVersion: "MNEMOM_CC_LAUNCHER_VERSION";
67
+ };
68
+ export type PluginEnvName = (typeof PLUGIN_ENV)[keyof typeof PLUGIN_ENV];
69
+ /**
70
+ * 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.
72
+ */
73
+ export declare function pluginEnabled(optionPlugin: boolean | undefined, env: NodeJS.ProcessEnv): boolean;
74
+ /**
75
+ * The bundled plugin's folder (the one holding `.claude-plugin/plugin.json`), or null when it is
76
+ * missing. Lives at `<package>/plugins/mnemom`, next to `dist/` and `src/`, so it is
77
+ * found from a built `dist/lib/` and from `src/lib/` under tsx alike.
78
+ */
79
+ export declare function resolvePluginDir(baseDirOverride?: string): string | null;
80
+ /** What the launcher knows about a launch that the mod shows. */
81
+ export interface PluginLaunch {
82
+ gatewayHost: string;
83
+ apiUrl: string;
84
+ agent: string;
85
+ conversationId: string;
86
+ cellNote: string;
87
+ shape: "terminal" | "remote-control" | "server";
88
+ /** The sealed contract's sha (envelope.sha) when one rides along. */
89
+ contractSha?: string;
90
+ /** The sealed contract's canonical JSON (envelope.canonical) when one rides along. */
91
+ contract?: string;
92
+ /**
93
+ * The launcher's goal-mode header when no contract is sealed (`implicit[; max_turns=N; …]`),
94
+ * or null.
95
+ */
96
+ goalMode?: string | null;
97
+ /** The argv that runs this `mnemom` again (`[process.execPath, ...process.execArgv, process.argv[1]]`). */
98
+ launcherExec?: readonly string[];
99
+ scenario: string;
100
+ launcherVersion: string;
101
+ }
102
+ /**
103
+ * The `MNEMOM_CC_*` block for the child env. Pure: no key, no header, nothing the gateway
104
+ * authenticates with — just what the launch log already prints to stderr.
105
+ */
106
+ export declare function buildPluginEnv(launch: PluginLaunch): Record<PluginEnvName, string>;
107
+ /** Writes the block onto a child env in place (the launch shapes all build their env first). */
108
+ export declare function applyPluginEnv(childEnv: NodeJS.ProcessEnv, launch: PluginLaunch): void;
109
+ /**
110
+ * Appends the plugin folder to CLAUDE_CODE_PLUGIN_DIRS (for the headless `remote-control` server,
111
+ * which takes no `--plugin-dir`): a user's own list is kept, the folder is added once.
112
+ */
113
+ export declare function appendPluginDirsEnv(childEnv: NodeJS.ProcessEnv, pluginDir: string): void;
114
+ /** The line the launch log and the dry-run plan print for the plugin. */
115
+ export declare function pluginPlanLine(pluginDir: string | null | undefined, enabled: boolean): string;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The bundled "Mnemom for Claude Code" plugin (`cli/plugins/mnemom`): a Claude Code
3
+ * mod + skill + MCP config that `mnemom agent` loads into the Claude Code it launches, so the
4
+ * session SHOWS it is governed — the Mnemom band above the prompt, `/mnemom` commands, the ◆ sign
5
+ * on the spinner, Mnemom trailers in commit attribution, and a guard that keeps the routing in
6
+ * place.
7
+ *
8
+ * The launcher tells the mod about the launch through NON-SECRET env vars. They carry a distinct
9
+ * `MNEMOM_CC_*` prefix so they can never be confused with the launcher's own `MNEMOM_AGENT_*`
10
+ * inputs, and none of them is a key, a header value or anything derived from one: the mod reads
11
+ * them by literal name (`claude plugin validate` lists every name), and a stray `env` dump of the
12
+ * child shows nothing it should not.
13
+ *
14
+ * Only Claude Code proper (`cli.name === "claude"`) understands `--plugin-dir`; every other
15
+ * adapter launches without the plugin.
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.
24
+ */
25
+ import { existsSync } from "node:fs";
26
+ import { dirname, join, delimiter } from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+ /** The plugin's `name` in its plugin.json, and its folder under `cli/plugins/`. */
29
+ export const CLAUDE_PLUGIN_NAME = "mnemom";
30
+ /** The env Claude Code reads a path list of extra plugin folders from (where no flag can be given). */
31
+ export const CLAUDE_PLUGIN_DIRS_ENV = "CLAUDE_CODE_PLUGIN_DIRS";
32
+ /** The env vars the launcher exports for the mod. Every value is non-secret. */
33
+ export const PLUGIN_ENV = {
34
+ /** `https://gateway.mnemom.ai` (no path): presence = the session is governed. */
35
+ gatewayHost: "MNEMOM_CC_GATEWAY_HOST",
36
+ /** The public API origin the Trust Rating is read from (`https://api.mnemom.ai`). */
37
+ apiUrl: "MNEMOM_CC_API_URL",
38
+ /** The governed agent's name (the `x-mnemom-agent` header's value). */
39
+ agent: "MNEMOM_CC_AGENT",
40
+ conversationId: "MNEMOM_CC_CONVERSATION_ID",
41
+ /** The cell note the launch log prints (`us-2/prod`, or `custom (<host>)`). */
42
+ cell: "MNEMOM_CC_CELL",
43
+ /** `terminal` | `remote-control` | `server`. */
44
+ shape: "MNEMOM_CC_SHAPE",
45
+ /** `sealed` (a contract rides in the header) | `implicit` | `off`. */
46
+ goalMode: "MNEMOM_CC_GOAL_MODE",
47
+ /** The sealed contract's sha256[0:16]; empty unless sealed. */
48
+ contractSha: "MNEMOM_CC_CONTRACT_SHA",
49
+ /**
50
+ * The sealed contract itself — the canonical JSON the gateway hashes (statement, requirements,
51
+ * allowed/forbidden paths, ceilings). Empty unless sealed. It is what `--goal`/`--requirement`/…
52
+ * said on the command line: the person's own words, no secret in it.
53
+ */
54
+ contract: "MNEMOM_CC_CONTRACT",
55
+ /**
56
+ * The implicit mode's guardrail ceilings, `max_turns=N; budget_usd=X; stall_turns=N` (the
57
+ * `x-mnemom-goal-mode` header minus its leading `implicit`). Empty unless implicit with ceilings.
58
+ */
59
+ guardrails: "MNEMOM_CC_GUARDRAILS",
60
+ /**
61
+ * How to run this very `mnemom` again, as a JSON argv (`[node, ...execArgv, script]`): the mod
62
+ * runs `mnemom agent show <conversation> --json` / `mnemom agent goal show …` through it to read
63
+ * the live goal state (the sealed verdict, the inferred goal, guardrail usage). The CLI
64
+ * authenticates with the launch key from its own ledger; the mod never holds the key.
65
+ */
66
+ launcherExec: "MNEMOM_CC_LAUNCHER_EXEC",
67
+ /** The `mnemom agent <session>` scenario name. */
68
+ session: "MNEMOM_CC_SESSION",
69
+ launcherVersion: "MNEMOM_CC_LAUNCHER_VERSION",
70
+ };
71
+ const TRUTHY = new Set(["1", "true", "yes", "on"]);
72
+ /**
73
+ * 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.
75
+ */
76
+ export function pluginEnabled(optionPlugin, env) {
77
+ if (optionPlugin !== undefined)
78
+ return optionPlugin;
79
+ const raw = (env.MNEMOM_AGENT_PLUGIN ?? "").trim().toLowerCase();
80
+ return TRUTHY.has(raw);
81
+ }
82
+ /**
83
+ * The bundled plugin's folder (the one holding `.claude-plugin/plugin.json`), or null when it is
84
+ * missing. Lives at `<package>/plugins/mnemom`, next to `dist/` and `src/`, so it is
85
+ * found from a built `dist/lib/` and from `src/lib/` under tsx alike.
86
+ */
87
+ export function resolvePluginDir(baseDirOverride) {
88
+ const here = baseDirOverride ?? dirname(fileURLToPath(import.meta.url));
89
+ const dir = join(here, "..", "..", "plugins", CLAUDE_PLUGIN_NAME);
90
+ return existsSync(join(dir, ".claude-plugin", "plugin.json")) ? dir : null;
91
+ }
92
+ /** Header-free, newline-free: a value that ends up in a child env and, later, on a band. */
93
+ function clean(v) {
94
+ return v.replace(/[\r\n]+/g, " ").trim();
95
+ }
96
+ /**
97
+ * The `MNEMOM_CC_*` block for the child env. Pure: no key, no header, nothing the gateway
98
+ * authenticates with — just what the launch log already prints to stderr.
99
+ */
100
+ export function buildPluginEnv(launch) {
101
+ const sealed = Boolean(launch.contractSha);
102
+ const implicit = !sealed && /^implicit(\s*;|$)/.test((launch.goalMode ?? "").trim());
103
+ const goalMode = sealed ? "sealed" : implicit ? "implicit" : "off";
104
+ // `implicit; max_turns=40; budget_usd=5` → `max_turns=40; budget_usd=5`.
105
+ const guardrails = implicit
106
+ ? clean(launch.goalMode ?? "")
107
+ .replace(/^implicit\s*;?\s*/, "")
108
+ .trim()
109
+ : "";
110
+ const exec = (launch.launcherExec ?? []).map(clean).filter(Boolean);
111
+ return {
112
+ [PLUGIN_ENV.gatewayHost]: clean(launch.gatewayHost).replace(/\/+$/, ""),
113
+ [PLUGIN_ENV.apiUrl]: clean(launch.apiUrl).replace(/\/+$/, ""),
114
+ [PLUGIN_ENV.agent]: clean(launch.agent),
115
+ [PLUGIN_ENV.conversationId]: clean(launch.conversationId),
116
+ [PLUGIN_ENV.cell]: clean(launch.cellNote),
117
+ [PLUGIN_ENV.shape]: launch.shape,
118
+ [PLUGIN_ENV.goalMode]: goalMode,
119
+ [PLUGIN_ENV.contractSha]: clean(launch.contractSha ?? ""),
120
+ [PLUGIN_ENV.contract]: sealed ? clean(launch.contract ?? "") : "",
121
+ [PLUGIN_ENV.guardrails]: guardrails,
122
+ [PLUGIN_ENV.launcherExec]: exec.length > 0 ? JSON.stringify(exec) : "",
123
+ [PLUGIN_ENV.session]: clean(launch.scenario),
124
+ [PLUGIN_ENV.launcherVersion]: clean(launch.launcherVersion),
125
+ };
126
+ }
127
+ /** Writes the block onto a child env in place (the launch shapes all build their env first). */
128
+ export function applyPluginEnv(childEnv, launch) {
129
+ for (const [k, v] of Object.entries(buildPluginEnv(launch)))
130
+ childEnv[k] = v;
131
+ }
132
+ /**
133
+ * Appends the plugin folder to CLAUDE_CODE_PLUGIN_DIRS (for the headless `remote-control` server,
134
+ * which takes no `--plugin-dir`): a user's own list is kept, the folder is added once.
135
+ */
136
+ export function appendPluginDirsEnv(childEnv, pluginDir) {
137
+ const have = (childEnv[CLAUDE_PLUGIN_DIRS_ENV] ?? "").split(delimiter).filter(Boolean);
138
+ if (have.includes(pluginDir))
139
+ return;
140
+ childEnv[CLAUDE_PLUGIN_DIRS_ENV] = [...have, pluginDir].join(delimiter);
141
+ }
142
+ /** The line the launch log and the dry-run plan print for the plugin. */
143
+ export function pluginPlanLine(pluginDir, enabled) {
144
+ if (!enabled)
145
+ return "off";
146
+ if (!pluginDir)
147
+ return "off (the plugin folder is not in this install — run from a source checkout)";
148
+ return `${CLAUDE_PLUGIN_NAME} (${pluginDir})`;
149
+ }
@@ -83,7 +83,9 @@ export interface LoopbackResult {
83
83
  * for refresh). `openUrl` is injectable so tests can drive the flow without
84
84
  * spawning a real browser.
85
85
  */
86
- export declare function loginWithLoopback(openUrl?: (url: string) => void): Promise<LoopbackResult>;
86
+ export declare function loginWithLoopback(openUrl?: (url: string) => void, opts?: {
87
+ inviteToken?: string;
88
+ }): Promise<LoopbackResult>;
87
89
  export interface DeviceResult {
88
90
  tokens: OAuthTokens;
89
91
  clientId: string;
package/dist/lib/oauth.js CHANGED
@@ -146,7 +146,7 @@ function tokensFromResponse(data) {
146
146
  * for refresh). `openUrl` is injectable so tests can drive the flow without
147
147
  * spawning a real browser.
148
148
  */
149
- export async function loginWithLoopback(openUrl = openBrowser) {
149
+ export async function loginWithLoopback(openUrl = openBrowser, opts = {}) {
150
150
  const meta = await discover();
151
151
  const pkce = generatePkce();
152
152
  const state = crypto.randomBytes(16).toString("hex");
@@ -164,6 +164,12 @@ export async function loginWithLoopback(openUrl = openBrowser) {
164
164
  authUrl.searchParams.set("state", state);
165
165
  authUrl.searchParams.set("code_challenge", pkce.challenge);
166
166
  authUrl.searchParams.set("code_challenge_method", pkce.method);
167
+ // A Mnemom Agent invitation (`mnemom agent --invite`) rides along so a NEW
168
+ // invitee who signs up in this browser round-trip skips the confirmation
169
+ // email: the AS ignores the param but bounces a signed-out browser to /login
170
+ // with this exact URL as return_to, and sign-up forwards the token to the api.
171
+ if (opts.inviteToken)
172
+ authUrl.searchParams.set("invite_token", opts.inviteToken);
167
173
  console.log("Opening browser to authenticate...");
168
174
  console.log(`If the browser doesn't open, visit:\n ${authUrl.toString()}\n`);
169
175
  openUrl(authUrl.toString());
@@ -234,6 +240,12 @@ function startLoopbackServer(expectedState) {
234
240
  close: () => {
235
241
  clearTimeout(timeout);
236
242
  server.close();
243
+ // The browser keeps idle keep-alive sockets (incl. Chrome's spare
244
+ // preconnect) open while the tab stays up; server.close() alone waits
245
+ // on them, so `mnemom login` printed success and then sat there until
246
+ // the tab was closed. Drop them — the callback response is already sent.
247
+ // Optional call: closeAllConnections() landed in Node 18.2 (engines allow 18.0).
248
+ server.closeAllConnections?.();
237
249
  },
238
250
  });
239
251
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.19",
3
+ "version": "0.17.20",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {