@mnemom/mnemom 0.17.11-next.1 → 0.17.12

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.
@@ -24,6 +24,8 @@ export interface CodeAccessProbe {
24
24
  mark: Mark;
25
25
  text: string;
26
26
  hint?: string;
27
+ /** May this account use Remote Control? Only a determined `remote_control: true` says yes. */
28
+ remoteControl: boolean;
27
29
  }
28
30
  /**
29
31
  * The invited-cohort gate, as the doctor reports it. Signed out → info (we can't
@@ -23,7 +23,7 @@ import { LAUNCHER_CONFIG_PATH } from "../lib/launcher-config.js";
23
23
  import { getAgentByName, getCardSources, getCodeEntitlement, MnemomApiError } from "../lib/api.js";
24
24
  import { isLoggedIn } from "../lib/auth.js";
25
25
  import { getActiveOrg } from "../lib/cli-config.js";
26
- import { isCodeNotEnabled, sourceLabel } from "./agent-invite.js";
26
+ import { isCodeNotEnabled, REQUEST_ACCESS_URL, sanitizeTerminalText, sourceLabel, withRequestAccess, } from "./agent-invite.js";
27
27
  import { probeUpdateStatus } from "../lib/update/doctor.js";
28
28
  import { describeModes, GOVERNANCE_OPT_IN_HINT, resolveDefaultSlug } from "./agent-setup.js";
29
29
  const MARK = { ok: "[ ok ]", warn: "[warn]", miss: "[MISS]", info: "[info]" };
@@ -96,26 +96,31 @@ export async function agentDoctorCommand(options = {}) {
96
96
  else {
97
97
  add("warn", `gateway: ${host} [${cell}] unreachable (${health.detail})`, "check the host (MNEMOM_AGENT_GATEWAY / `mnemom agent config set gateway <url>`) and your network");
98
98
  }
99
- // ── Remote Control capability ──
100
- if (cli) {
99
+ // ── Mnemom Agent access (invited cohort gate) ── probed here, before the Remote Control
100
+ // lines, because Remote Control is internal-only: an account the api does not entitle to it
101
+ // (every customer) never sees those lines at all.
102
+ const gate = await probeCodeAccess();
103
+ // ── Remote Control capability (entitled accounts only) ──
104
+ if (cli && gate.remoteControl) {
101
105
  const rc = checkRcCapability(cli);
102
106
  if (rc.ok)
103
107
  add("ok", "Remote Control: available (node + openssl + --remote-control)");
104
108
  else
105
109
  add("warn", `Remote Control: unavailable — ${rc.missing.join("; ")}`, "launches fall back to terminal-only until these are in place");
106
- // ── claude.ai login (informational — RC needs it) ──
107
- if (cli.name === "claude") {
108
- const login = claudeLoggedIn(cli.bin);
109
- if (login === true)
110
- add("ok", "claude.ai login: logged in");
111
- else if (login === false)
112
- add("info", "claude.ai login: not logged in", "Remote Control needs a claude.ai login; the terminal-only shape does not");
113
- else
114
- add("info", "claude.ai login: could not determine");
115
- }
116
110
  }
117
- // ── Mnemom Agent access (invited cohort gate) ──
118
- const gate = await probeCodeAccess();
111
+ // ── claude.ai login (informational) ── Remote Control needs it, and so does auth =
112
+ // subscription; a terminal launch on an Anthropic key does not, so nobody else sees it.
113
+ if (cli && cli.name === "claude" && (gate.remoteControl || auth === "subscription")) {
114
+ const login = claudeLoggedIn(cli.bin);
115
+ if (login === true)
116
+ add("ok", "claude.ai login: logged in");
117
+ else if (login === false)
118
+ add("info", "claude.ai login: not logged in", auth === "subscription"
119
+ ? "auth = subscription launches Claude Code on your claude.ai login — run `claude` and /login"
120
+ : "Remote Control needs a claude.ai login; the terminal-only shape does not");
121
+ else
122
+ add("info", "claude.ai login: could not determine");
123
+ }
119
124
  add(gate.mark, gate.text, gate.hint);
120
125
  // ── governance (off by default for new agents) ──
121
126
  const gov = await probeGovernance(options.agent);
@@ -192,7 +197,8 @@ export async function probeCodeAccess() {
192
197
  return {
193
198
  mark: "info",
194
199
  text: "Mnemom Agent access: sign in to check",
195
- hint: "run `mnemom login`; @mnemom.ai accounts are enabled by default, others need an invitation (mnemom agent --invite <token>)",
200
+ hint: `run \`mnemom login\`; @mnemom.ai accounts are enabled by default, others need an invitation (mnemom agent --invite <token>) — request one at ${REQUEST_ACCESS_URL}`,
201
+ remoteControl: false,
196
202
  };
197
203
  }
198
204
  const org = getActiveOrg();
@@ -202,9 +208,7 @@ export async function probeCodeAccess() {
202
208
  return {
203
209
  mark: "ok",
204
210
  text: `Mnemom Agent access: enabled for ${orgLabel} (${sourceLabel(ent.source)})`,
205
- hint: ent.remote_control
206
- ? undefined
207
- : "Remote Control (steer from claude.ai / the Claude app) is an internal feature — your sessions run terminal-only",
211
+ remoteControl: ent.remote_control === true,
208
212
  };
209
213
  }
210
214
  catch (err) {
@@ -213,13 +217,18 @@ export async function probeCodeAccess() {
213
217
  return {
214
218
  mark: "miss",
215
219
  text: `Mnemom Agent access: not enabled for ${orgLabel}`,
216
- hint: typeof hint === "string" && hint
217
- ? hint
218
- : "redeem an invitation: mnemom agent --invite <token>",
220
+ hint: withRequestAccess(typeof hint === "string" && hint
221
+ ? sanitizeTerminalText(hint)
222
+ : "redeem an invitation: mnemom agent --invite <token>"),
223
+ remoteControl: false,
219
224
  };
220
225
  }
221
226
  const detail = err instanceof MnemomApiError || err instanceof Error ? err.message : String(err);
222
- return { mark: "info", text: `Mnemom Agent access: could not determine (${detail})` };
227
+ return {
228
+ mark: "info",
229
+ text: `Mnemom Agent access: could not determine (${detail})`,
230
+ remoteControl: false,
231
+ };
223
232
  }
224
233
  }
225
234
  const OPT_IN_HINT = GOVERNANCE_OPT_IN_HINT.trim();
@@ -29,6 +29,13 @@ export declare class CodeNotEnabledError extends Error {
29
29
  /** `pinv_` + base64url — the product-invite token shape the api mints. */
30
30
  export declare const INVITE_TOKEN_RE: RegExp;
31
31
  export declare function looksLikeInviteToken(value: unknown): value is string;
32
+ /**
33
+ * Where someone without an invitation (or a Mnemom contact) asks for access. Every
34
+ * locked-out message ends with it, so an uninvited customer is never at a dead end.
35
+ */
36
+ export declare const REQUEST_ACCESS_URL = "https://www.mnemom.ai/contact";
37
+ /** `hint`, plus the request-access pointer unless the hint already carries the URL. */
38
+ export declare function withRequestAccess(hint: string): string;
32
39
  /** Drop control characters (ANSI escapes included) from an api-supplied string before
33
40
  * it reaches the terminal — defense in depth; the value is first-party but printed raw. */
34
41
  export declare function sanitizeTerminalText(s: string): string;
@@ -39,6 +39,25 @@ export function looksLikeInviteToken(value) {
39
39
  return typeof value === "string" && INVITE_TOKEN_RE.test(value.trim());
40
40
  }
41
41
  const FALLBACK_HINT = "Ask your Mnemom contact for a Mnemom Agent invitation, then run: mnemom agent --invite <token>";
42
+ /**
43
+ * Where someone without an invitation (or a Mnemom contact) asks for access. Every
44
+ * locked-out message ends with it, so an uninvited customer is never at a dead end.
45
+ */
46
+ export const REQUEST_ACCESS_URL = "https://www.mnemom.ai/contact";
47
+ /** Whether `hint` already names the request-access page, as a whole word (trailing
48
+ * punctuation ignored) — a token match, not a substring one. */
49
+ function mentionsRequestAccess(hint) {
50
+ return hint.split(/\s+/).some((w) => w.replace(/[.,;:!?)]+$/, "") === REQUEST_ACCESS_URL);
51
+ }
52
+ /** `hint`, plus the request-access pointer unless the hint already carries the URL. */
53
+ export function withRequestAccess(hint) {
54
+ if (mentionsRequestAccess(hint))
55
+ return hint;
56
+ let end = hint.length;
57
+ while (end > 0 && (hint[end - 1] === "." || hint[end - 1].trim() === ""))
58
+ end--;
59
+ return `${hint.slice(0, end)}. No invitation yet? Request access at ${REQUEST_ACCESS_URL}`;
60
+ }
42
61
  /** Drop control characters (ANSI escapes included) from an api-supplied string before
43
62
  * it reaches the terminal — defense in depth; the value is first-party but printed raw. */
44
63
  export function sanitizeTerminalText(s) {
@@ -76,7 +95,7 @@ function printNotEnabled(orgLabel, hint) {
76
95
  say();
77
96
  say(" mnemom agent --invite <token>");
78
97
  say();
79
- say(fmt.dim(` ${hint}`));
98
+ say(fmt.dim(` ${withRequestAccess(hint)}`));
80
99
  }
81
100
  /**
82
101
  * Enforce the Mnemom Agent gate for the org the launch targets. Blocks (prints
@@ -37,7 +37,7 @@ export function goalAlignmentLine(mode) {
37
37
  }
38
38
  return m === "on"
39
39
  ? "mnemom agent: goal alignment: on (the judge nudges the agent back when it drifts from your goal)"
40
- : "mnemom agent: goal alignment: off (--no-implicit or goal_mode = off — pass --goal or drop --no-implicit to turn it back on)";
40
+ : 'mnemom agent: goal alignment: off — add --goal "…" to seal a goal for this session';
41
41
  }
42
42
  const defaultSay = (line = "") => console.error(line);
43
43
  /**
@@ -62,7 +62,7 @@ export async function resolveSessionSetup(input, ask) {
62
62
  // ── 1. session name ──
63
63
  const fallback = defaultSessionName(input.cwd);
64
64
  say();
65
- say("Session name — labels this session in your traces and the claude.ai session list.");
65
+ say("Session name — labels this session in your traces and in `mnemom agent sessions`.");
66
66
  say(` (Next time you can pass it directly: mnemom agent <name>.)`);
67
67
  const typed = await ask.askInput(`Session name [${fallback}]:`);
68
68
  const scenario = sanitizeAgentName(typed.trim()) || fallback;
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { readSessionRecords } from "../lib/agent-ledger.js";
17
17
  import { getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
18
- import { sameOrigin } from "../lib/agent.js";
18
+ import { resumeCommand, sameOrigin } from "../lib/agent.js";
19
19
  import { getAgentSession, MnemomApiError } from "../lib/api.js";
20
20
  import { fmt, formatRelativeAge } from "../lib/format.js";
21
21
  import { goalControlFetch } from "../lib/goal-api.js";
@@ -186,6 +186,13 @@ export function buildShowView(local, remote, reason) {
186
186
  L.push(fmt.label("experimental: ", local.experimental.join(", ")));
187
187
  }
188
188
  L.push(fmt.label("conversation: ", local.conversation_id));
189
+ if (local.cc_session_id && local.launch_shape !== "server") {
190
+ L.push(fmt.label("resume: ", resumeCommand({
191
+ scenario: local.session,
192
+ agent: local.agent_slug,
193
+ ccSessionId: local.cc_session_id,
194
+ })));
195
+ }
189
196
  const goalMode = resolveGoalMode(local, remote);
190
197
  L.push(fmt.label("goal alignment:", goalBadge(goalMode)));
191
198
  if (local.contract) {
@@ -598,7 +598,7 @@ export async function ensureMnemomSession(opts = {}) {
598
598
  say(fmt.section("Sign in to Mnemom"));
599
599
  say(fmt.dim(" mnemom agent runs your coding session governed and under your Mnemom account,"));
600
600
  say(fmt.dim(" so it shows up in your traces and draws on your org's µ balance."));
601
- say(fmt.dim(` New to Mnemom? You can create a free account in the same flow (or at ${getWebsiteUrl()}/signup).`));
601
+ say(fmt.dim(` New to Mnemom? You can create your account in the same flow (or at ${getWebsiteUrl()}/signup).`));
602
602
  if (!(await askYesNo("Sign in or create your account now?", true)))
603
603
  return false;
604
604
  const device = headlessEnv();
@@ -688,7 +688,7 @@ export async function ensureSessionAndBalance(opts) {
688
688
  if (!haveSession) {
689
689
  throw new Error("mnemom agent needs a Mnemom account — it runs your coding session governed and on your org's µ balance.\n" +
690
690
  " Sign in: mnemom login (or set MNEMOM_API_KEY)\n" +
691
- ` New here? create a free account, then top up µ: ${getWebsiteUrl()}/settings/billing`);
691
+ ` New here? create your account, then top up µ: ${getWebsiteUrl()}/settings/billing`);
692
692
  }
693
693
  const active = getActiveOrg();
694
694
  const orgId = active?.org_id ?? (await getMyPersonalOrg()).org_id;
@@ -143,6 +143,24 @@ export declare function passthroughResumeId(passthrough: readonly string[]): str
143
143
  * The ledger read is best-effort: a failure means a new id.
144
144
  */
145
145
  export declare function resolveLaunchConversationId(input: LaunchContinuationInput): Promise<string>;
146
+ /** Whether the passthrough continues the directory's newest session (`--continue` / `-c`, unforked). */
147
+ export declare function passthroughContinues(passthrough: readonly string[]): boolean;
148
+ /**
149
+ * The Claude Code session `--continue` picks in `cwd`: the newest `<uuid>.jsonl` in that
150
+ * directory's project folder (Claude Code names it after the path, each non-alphanumeric
151
+ * character a `-`). Best-effort: null when there is none or it can't be read.
152
+ */
153
+ export declare function newestCcSessionIn(cwd: string, env: NodeJS.ProcessEnv): string | null;
154
+ /**
155
+ * The line `mnemom agent` prints after Claude Code exits: how to resume the session THROUGH
156
+ * Mnemom (Claude Code's own `claude --resume <id>` leaves the gateway). `--agent` is repeated
157
+ * only when this launch named one; an unknown session id falls back to `--continue`.
158
+ */
159
+ export declare function resumeHintLine(ctx: {
160
+ scenario: string;
161
+ agent?: string;
162
+ ccSessionId: string | null;
163
+ }): string;
146
164
  /**
147
165
  * Whether Claude Code still has the transcript of session `id`
148
166
  * (`${CLAUDE_CONFIG_DIR:-~/.claude}/projects/<any project>/<id>.jsonl`; `--resume` finds it in any
@@ -345,6 +363,11 @@ export declare function logLaunch(ctx: {
345
363
  goalMode: string | null;
346
364
  cli: ResolvedCli;
347
365
  }): void;
366
+ /**
367
+ * The agent a signed-out `--dry-run` shows when the caller named none: the real name is
368
+ * `agent-<handle>` from the Mnemom login, which a dry run (no sign-in) cannot know yet.
369
+ */
370
+ export declare const DRY_RUN_SIGNED_OUT_AGENT = "agent-<your Mnemom login handle>";
348
371
  export declare function printPlan(ctx: {
349
372
  scenario: string;
350
373
  shape: LaunchShape;
@@ -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, writeFileSync, renameSync, } from "node:fs";
29
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
30
30
  import { randomUUID } from "node:crypto";
31
31
  import { tmpdir, homedir, hostname, userInfo } from "node:os";
32
32
  import { basename, join, dirname } from "node:path";
@@ -36,6 +36,7 @@ import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
36
36
  import { createApiKey } from "../lib/api.js";
37
37
  import { MNEMOM_KEY_STORE_FIELD, resolveAgentAuth, resolveSubscriptionMnemomKey, subscriptionChildEnv, subscriptionShape, } from "../lib/subscription-login.js";
38
38
  import { LEDGER_FILE, appendSessionRecord, findResumeCandidate, readSessionRecords, resolveRetentionDays, resumedContextProfile, toLedgerContract, } from "../lib/agent-ledger.js";
39
+ import { isLoggedIn } from "../lib/auth.js";
39
40
  import { loadCliConfig } from "../lib/cli-config.js";
40
41
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
41
42
  import { experimentalNotice } from "../lib/experimental.js";
@@ -45,7 +46,7 @@ import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, }
45
46
  import { requireConsoleKey, readConsoleKey } from "../lib/console-key.js";
46
47
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
47
48
  import { defaultAgentSlug, ensureClaimedServedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
48
- import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, assertOffProfileCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
49
+ import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, assertOffProfileCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, 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
50
  import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
50
51
  import { slackChannelArgs } from "../lib/slack-launch.js";
51
52
  /** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
@@ -261,20 +262,26 @@ export async function agentCommand(scenarioArg, options = {}) {
261
262
  // which we only know AFTER sign-in. The entitlement-aware decision
262
263
  // (`resolveRemoteControlShape`) happens below, right after
263
264
  // `ensureSessionAndBalance`. --dry-run prints the plan BEFORE sign-in, so it
264
- // can't know the entitlement; it previews the shape the MACHINE is capable of
265
- // (as the pre-gate behaviour did), then notes that the real launch applies the
266
- // entitlement gate. `shape` itself stays the requested (ungated) shape.
265
+ // can't know the entitlement: it previews the REQUESTED shape (terminal unless
266
+ // --remote-control/--server or `launch` asked otherwise) and never the RC
267
+ // auto-upgrade, which only an entitled (internal) account gets. An explicit RC
268
+ // request gets the note that the real launch still applies the entitlement gate.
267
269
  if (options.dryRun) {
268
270
  const planCli = cli ?? {
269
271
  bin: "(not found — pass --cli <path> or `mnemom agent config set cli <path>`)",
270
272
  name: basename((options.cli || env.MNEMOM_AGENT_CLI || "claude").trim()),
271
273
  };
272
- const previewShape = !shapeExplicit && shape === "terminal" && cli && checkRcCapability(cli).ok
273
- ? "remote-control"
274
- : shape;
274
+ // A defaulted slug is `agent-<handle>` from the login (resolved for real after
275
+ // sign-in, below). Signed in: show that name. Signed out, or no usable handle:
276
+ // say where it comes from rather than print the "agent-user" fallback as if it were it.
277
+ if (!options.agent?.trim()) {
278
+ agent =
279
+ ((await isLoggedIn()) && sanitizeAgentName(await resolveDefaultSlug())) ||
280
+ DRY_RUN_SIGNED_OUT_AGENT;
281
+ }
275
282
  printPlan({
276
283
  scenario,
277
- shape: previewShape,
284
+ shape,
278
285
  agent,
279
286
  // The org is only known after sign-in, so a dry run never offers a resume (IG5 #1).
280
287
  conversationId: resolveConversationId(options.conversationId),
@@ -298,8 +305,8 @@ export async function agentCommand(scenarioArg, options = {}) {
298
305
  }
299
306
  if (expNotice)
300
307
  say(expNotice);
301
- if (previewShape === "remote-control") {
302
- say("mnemom agent: the plan shows Remote Control because this machine can run it; the real launch uses it only if your account is entitled (internal Mnemom accounts), else terminal-only.");
308
+ if (shape !== "terminal") {
309
+ say("mnemom agent: the real launch uses this shape only if your account is entitled to it, else terminal-only.");
303
310
  }
304
311
  sayContextProfile(contextProfile);
305
312
  sayCompaction(contextProfile, compactionIntercept, compactAt, launchWindow(windowModelFor(options, cli)));
@@ -314,14 +321,6 @@ export async function agentCommand(scenarioArg, options = {}) {
314
321
  if (!cli) {
315
322
  cli = await promptForCliPath(options.cli, env, cliError);
316
323
  }
317
- // ── resolve the Anthropic key (zero 1Password; never logged) ──
318
- // Not on a subscription launch: Claude Code then sends its own claude.ai login instead.
319
- // A Console launch uses the key Claude Code's Console login created, then runs as api-key.
320
- const anthropicKey = auth === "subscription"
321
- ? undefined
322
- : auth === "console"
323
- ? requireConsoleKey(defaultConsoleKeyDeps(env))
324
- : await resolveAnthropicKey(env, { allowPrompt: true, keySource: options.keySource });
325
324
  // ── account + µ-balance gate (no free tier) ─────────────────────────────────
326
325
  // mnemom agent runs governed and on µ, so before we seal a contract or launch we
327
326
  // (1) ensure a Mnemom session — offering the try-me-style sign-in / sign-up when
@@ -342,6 +341,16 @@ export async function agentCommand(scenarioArg, options = {}) {
342
341
  process.exit(1); // guidance already printed
343
342
  throw err;
344
343
  }
344
+ // ── resolve the Anthropic key (zero 1Password; never logged) ──
345
+ // AFTER the account gate above: an account Mnemom Agent is not enabled for learns that
346
+ // (and how to get access) before it is asked for, or refused for want of, a key.
347
+ // Not on a subscription launch: Claude Code then sends its own claude.ai login instead.
348
+ // A Console launch uses the key Claude Code's Console login created, then runs as api-key.
349
+ const anthropicKey = auth === "subscription"
350
+ ? undefined
351
+ : auth === "console"
352
+ ? requireConsoleKey(defaultConsoleKeyDeps(env))
353
+ : await resolveAnthropicKey(env, { allowPrompt: true, keySource: options.keySource });
345
354
  // ── Remote-Control entitlement gate ─────────────────────────────────────────
346
355
  // Remote Control (a session steerable from claude.ai / the Claude app) is
347
356
  // internal-only. Now that we know the org's entitlement, resolve the final
@@ -432,10 +441,16 @@ export async function agentCommand(scenarioArg, options = {}) {
432
441
  passthrough: [...(options.passthrough ?? []), "--resume", continuation.resumeCcSessionId],
433
442
  };
434
443
  }
435
- // A launch that resumes a Claude Code session (the continuation above, or the caller's own
436
- // `-- --resume <id>`) keeps the profile that session ran under, unless this launch chose one
437
- // with a flag. Read from the ledger only — nothing here writes a default anywhere.
438
- const resumeId = shape === "server" ? null : passthroughResumeId(options.passthrough ?? []);
444
+ // A launch that resumes a Claude Code session (the continuation above, the caller's own
445
+ // `-- --resume <id>`, or `-- --continue`, which picks this directory's newest session) keeps
446
+ // the profile that session ran under, unless this launch chose one with a flag. Read from the
447
+ // ledger only — nothing here writes a default anywhere.
448
+ const continuedCcSessionId = shape !== "server" && passthroughContinues(options.passthrough ?? [])
449
+ ? newestCcSessionIn(process.cwd(), process.env)
450
+ : null;
451
+ const resumeId = shape === "server"
452
+ ? null
453
+ : (passthroughResumeId(options.passthrough ?? []) ?? continuedCcSessionId);
439
454
  if (resumeId && options.contextProfile === undefined && options.context !== false) {
440
455
  let resumed;
441
456
  try {
@@ -493,7 +508,8 @@ export async function agentCommand(scenarioArg, options = {}) {
493
508
  cli,
494
509
  envelope,
495
510
  goalMode,
496
- ccSessionId: cc.id,
511
+ // A `--continue` records the session it continues, so a later resume finds its profile.
512
+ ccSessionId: cc.id ?? continuedCcSessionId,
497
513
  contextProfile,
498
514
  compactAt: contextProfile === "off" ? null : compactAt,
499
515
  });
@@ -556,6 +572,12 @@ export async function agentCommand(scenarioArg, options = {}) {
556
572
  contextHint,
557
573
  ccSessionId,
558
574
  handoffMcpConfigPath,
575
+ resumeHint: (id) => resumeHintLine({
576
+ scenario,
577
+ agent: options.agent?.trim() ? agent : undefined,
578
+ ccSessionId: id,
579
+ }),
580
+ knownCcSessionId: cc.id ?? continuedCcSessionId,
559
581
  });
560
582
  return; // launchTerminal exits the process
561
583
  }
@@ -618,6 +640,52 @@ function choosesCcSession(passthrough) {
618
640
  return !pickCcSessionId([...passthrough], () => "").inject;
619
641
  }
620
642
  const CC_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
643
+ /** `${CLAUDE_CONFIG_DIR:-~/.claude}/projects` — where Claude Code keeps session transcripts. */
644
+ function ccProjectsDir(env) {
645
+ return join((env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"), "projects");
646
+ }
647
+ /** Whether the passthrough continues the directory's newest session (`--continue` / `-c`, unforked). */
648
+ export function passthroughContinues(passthrough) {
649
+ return ((passthrough.includes("--continue") || passthrough.includes("-c")) &&
650
+ !passthrough.includes("--fork-session") &&
651
+ passthroughResumeId(passthrough) === null);
652
+ }
653
+ /**
654
+ * The Claude Code session `--continue` picks in `cwd`: the newest `<uuid>.jsonl` in that
655
+ * directory's project folder (Claude Code names it after the path, each non-alphanumeric
656
+ * character a `-`). Best-effort: null when there is none or it can't be read.
657
+ */
658
+ export function newestCcSessionIn(cwd, env) {
659
+ const dir = join(ccProjectsDir(env), cwd.replace(/[^a-zA-Z0-9]/g, "-"));
660
+ try {
661
+ let best = null;
662
+ for (const f of readdirSync(dir)) {
663
+ const id = f.endsWith(".jsonl") ? f.slice(0, -".jsonl".length) : "";
664
+ if (!CC_SESSION_ID_RE.test(id))
665
+ continue;
666
+ const mtime = statSync(join(dir, f)).mtimeMs;
667
+ if (!best || mtime > best.mtime)
668
+ best = { id, mtime };
669
+ }
670
+ return best?.id ?? null;
671
+ }
672
+ catch {
673
+ return null;
674
+ }
675
+ }
676
+ /**
677
+ * The line `mnemom agent` prints after Claude Code exits: how to resume the session THROUGH
678
+ * Mnemom (Claude Code's own `claude --resume <id>` leaves the gateway). `--agent` is repeated
679
+ * only when this launch named one; an unknown session id falls back to `--continue`.
680
+ */
681
+ export function resumeHintLine(ctx) {
682
+ return (`Resume with: ${resumeCommand(ctx)}` +
683
+ " (keeps it governed; `claude --resume` runs outside Mnemom)");
684
+ }
685
+ /** An interactive Claude Code session — not `-p` / `--print`, which has nothing to resume into. */
686
+ function wantsResumeHint(cli, args) {
687
+ return cli.name === "claude" && !args.includes("-p") && !args.includes("--print");
688
+ }
621
689
  /**
622
690
  * Whether Claude Code still has the transcript of session `id`
623
691
  * (`${CLAUDE_CONFIG_DIR:-~/.claude}/projects/<any project>/<id>.jsonl`; `--resume` finds it in any
@@ -627,7 +695,7 @@ const CC_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a
627
695
  export function ccSessionTranscriptExists(id, env) {
628
696
  if (!CC_SESSION_ID_RE.test(id))
629
697
  return false;
630
- const projects = join((env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"), "projects");
698
+ const projects = ccProjectsDir(env);
631
699
  try {
632
700
  return readdirSync(projects).some((d) => existsSync(join(projects, d, `${id}.jsonl`)));
633
701
  }
@@ -1077,8 +1145,15 @@ function launchTerminal(ctx) {
1077
1145
  child.on("exit", (code, signal) => {
1078
1146
  if (signal)
1079
1147
  process.kill(process.pid, signal);
1080
- else
1148
+ else {
1149
+ // Claude Code's own last line is "Resume this session with: claude --resume <id>",
1150
+ // which reopens it OUTSIDE Mnemom (ungoverned, unbilled). Ours goes after it, last.
1151
+ if (code === 0 && ctx.resumeHint && wantsResumeHint(ctx.cli, args)) {
1152
+ const id = ctx.knownCcSessionId ?? newestCcSessionIn(process.cwd(), process.env);
1153
+ say(ctx.resumeHint(id));
1154
+ }
1081
1155
  process.exit(code ?? 0);
1156
+ }
1082
1157
  });
1083
1158
  };
1084
1159
  // Implicit goal mode: put the local base-URL proxy in front of the door so each main request
@@ -1585,6 +1660,11 @@ export function logLaunch(ctx) {
1585
1660
  say(`mnemom agent: goal mode: ${ctx.goalMode} (inferred from your messages; watch x-mnemom-goal-revision / x-mnemom-goal-steer on each response)`);
1586
1661
  }
1587
1662
  }
1663
+ /**
1664
+ * The agent a signed-out `--dry-run` shows when the caller named none: the real name is
1665
+ * `agent-<handle>` from the Mnemom login, which a dry run (no sign-in) cannot know yet.
1666
+ */
1667
+ export const DRY_RUN_SIGNED_OUT_AGENT = "agent-<your Mnemom login handle>";
1588
1668
  export function printPlan(ctx) {
1589
1669
  const L = [];
1590
1670
  L.push("mnemom agent — launch plan (dry run)");
@@ -16,3 +16,5 @@ export declare function experimentalEnableCommand(id: string | undefined, deps?:
16
16
  export declare function experimentalDisableCommand(id: string | undefined, deps?: ExperimentalDeps): void;
17
17
  /** The one-time migration (lib/experimental.ts migrateExperimental) against the real files. */
18
18
  export declare function runExperimentalMigration(): void;
19
+ /** Does `dir` exist with anything in it? Unreadable counts as yes (an upgrader keeps the notice). */
20
+ export declare function hasPriorState(dir: string): boolean;
@@ -2,8 +2,10 @@
2
2
  * `mnemom experimental list [--json] | enable <id> | disable <id>` — turn experimental
3
3
  * features on and off (registry: lib/experimental.ts; stored in ~/.mnemom/config.json).
4
4
  */
5
+ import { existsSync, readdirSync } from "node:fs";
5
6
  import { EXPERIMENTAL_CONTRACT, EXPERIMENTAL_ENV, EXPERIMENTAL_FEATURES, EXPERIMENTAL_IDS, isExperimentalId, migrateExperimental, resolveExperimental, } from "../lib/experimental.js";
6
7
  import { loadCliConfig, setExperimentalEnabled } from "../lib/cli-config.js";
8
+ import { MNEMOM_DIR } from "../lib/config.js";
7
9
  import { launcherEntries, loadLauncherConfig } from "../lib/launcher-config.js";
8
10
  function defaultDeps() {
9
11
  return {
@@ -74,5 +76,15 @@ export function runExperimentalMigration() {
74
76
  launcherEntries: () => launcherEntries(loadLauncherConfig()),
75
77
  persist: (ids) => setExperimentalEnabled(ids),
76
78
  writeErr: (line) => void process.stderr.write(line),
79
+ priorInstall: () => hasPriorState(MNEMOM_DIR),
77
80
  });
78
81
  }
82
+ /** Does `dir` exist with anything in it? Unreadable counts as yes (an upgrader keeps the notice). */
83
+ export function hasPriorState(dir) {
84
+ try {
85
+ return existsSync(dir) && readdirSync(dir).length > 0;
86
+ }
87
+ catch {
88
+ return true;
89
+ }
90
+ }
package/dist/index.js CHANGED
@@ -140,7 +140,7 @@ function printAgentHelp(verb) {
140
140
  }
141
141
  const agentCmd = program
142
142
  .command("agent [session] [passthrough...]")
143
- .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 and an optional goal. " +
143
+ .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
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.")
145
145
  .option("--goal <statement>", "Contract goal statement (required if any contract flag is given)")
146
146
  .option("--requirement <text>", "Contract requirement (repeatable)", collectRepeatable, [])
@@ -155,8 +155,11 @@ const agentCmd = program
155
155
  .option("--conversation-id <id>", "x-mnemom-conversation-id (default: auto-generated)")
156
156
  .option("--cli <name|path>", "Coding-agent CLI to launch: a name or full path (default: claude)")
157
157
  .option("--gateway <url>", "Gateway host override — must be the same cell as the rest of the CLI (default: that cell's gateway; or MNEMOM_AGENT_GATEWAY)")
158
- .option("--remote-control", "Interactive Remote Control session in THIS terminal (behind a local proxy)")
159
- .option("--server", "Headless `claude remote-control` dispatcher (behind a local proxy)")
158
+ // Remote Control is internal-only (the launch gates it on the org's entitlement and runs a
159
+ // customer terminal-only), so its two shapes stay out of --help for everyone. The help is
160
+ // built at load, before any sign-in, so it cannot ask the entitlement; the flags still parse.
161
+ .addOption(new Option("--remote-control", "Interactive Remote Control session in THIS terminal (behind a local proxy)").hideHelp())
162
+ .addOption(new Option("--server", "Headless `claude remote-control` dispatcher (behind a local proxy)").hideHelp())
160
163
  .option("--auth <mode>", "How Claude Code authenticates: api-key (default) | subscription (your Claude Team/Enterprise login) | console (the API key your Claude Code Anthropic Console login created)")
161
164
  .option("--model <alias|id>", "Model to launch (CLI alias or concrete id)")
162
165
  .addOption(new Option("--effort <level>", "Reasoning effort").choices([
@@ -228,6 +228,16 @@ export declare function resolveWindowModel(input: {
228
228
  env: Record<string, string | undefined>;
229
229
  settings: () => CcSettingsView[];
230
230
  }): WindowModel;
231
+ /**
232
+ * The command that resumes a Claude Code session THROUGH Mnemom (governed, billed) — never
233
+ * Claude Code's own `claude --resume <id>`, which leaves the gateway. An unknown session id
234
+ * falls back to `--continue` (Claude Code's newest session in this directory).
235
+ */
236
+ export declare function resumeCommand(ctx: {
237
+ scenario: string;
238
+ agent?: string;
239
+ ccSessionId: string | null;
240
+ }): string;
231
241
  /** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
232
242
  export declare function sessionDisplayName(agent: string, scenario: string): string;
233
243
  /** A caller's own `--name <x>` in the passthrough args, if any (last one wins). */
package/dist/lib/agent.js CHANGED
@@ -301,6 +301,20 @@ export function resolveWindowModel(input) {
301
301
  return { model: explicit, reason: "unsupported" };
302
302
  return { model: `${base}${ONE_M_SUFFIX}`, reason: "one-m", from: base };
303
303
  }
304
+ /** Single-quote `s` for a POSIX shell unless it is plainly safe as-is. */
305
+ function shellArg(s) {
306
+ return /^[A-Za-z0-9._@%+=:,/-]+$/.test(s) ? s : `'${s.replace(/'/g, "'\\''")}'`;
307
+ }
308
+ /**
309
+ * The command that resumes a Claude Code session THROUGH Mnemom (governed, billed) — never
310
+ * Claude Code's own `claude --resume <id>`, which leaves the gateway. An unknown session id
311
+ * falls back to `--continue` (Claude Code's newest session in this directory).
312
+ */
313
+ export function resumeCommand(ctx) {
314
+ const agentFlag = ctx.agent ? ` --agent ${shellArg(ctx.agent)}` : "";
315
+ const resume = ctx.ccSessionId ? `--resume ${shellArg(ctx.ccSessionId)}` : "--continue";
316
+ return `mnemom agent ${shellArg(ctx.scenario)}${agentFlag} -- ${resume}`;
317
+ }
304
318
  /** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
305
319
  export function sessionDisplayName(agent, scenario) {
306
320
  return sanitizeAgentName(`${agent}-${scenario}`) || agent;
@@ -98,6 +98,11 @@ export interface MigrationDeps {
98
98
  launcherEntries(): Array<[string, string]>;
99
99
  persist(ids: ExperimentalId[]): void;
100
100
  writeErr(line: string): void;
101
+ /**
102
+ * Did this machine run the CLI before this invocation (~/.mnemom already held something)?
103
+ * A brand-new install never had implicit on, so it gets no "it used to be on" notice.
104
+ */
105
+ priorInstall(): boolean;
101
106
  }
102
107
  /** One-time notice for users who had implicit goal alignment by default (no goal_mode set). */
103
108
  export declare const IMPLICIT_NOW_OFF_NOTICE: string;
@@ -213,6 +213,7 @@ export function migrateExperimental(deps) {
213
213
  try {
214
214
  if (deps.stored() !== undefined)
215
215
  return null;
216
+ const upgrading = deps.priorInstall();
216
217
  const entries = deps.launcherEntries();
217
218
  const ids = featuresInUse(entries);
218
219
  deps.persist(ids);
@@ -220,9 +221,10 @@ export function migrateExperimental(deps) {
220
221
  deps.writeErr(`mnemom: ${ids.join(", ")} ${ids.length === 1 ? "is" : "are"} now experimental and kept on because your launcher.toml uses ${ids.length === 1 ? "it" : "them"} — see \`mnemom experimental list\`.\n`);
221
222
  }
222
223
  // Implicit was the unconfigured DEFAULT, so most users relied on it without a
223
- // goal_mode entry. Don't let it switch off silently for them: say so once.
224
+ // goal_mode entry. Don't let it switch off silently for them: say so once. A fresh
225
+ // install had no default to lose, so it is told nothing.
224
226
  const optedOut = entries.some(([k, v]) => k === "goal_mode" && v === "off");
225
- if (!ids.includes("implicit") && !optedOut)
227
+ if (upgrading && !ids.includes("implicit") && !optedOut)
226
228
  deps.writeErr(IMPLICIT_NOW_OFF_NOTICE);
227
229
  return ids;
228
230
  }
@@ -135,7 +135,7 @@ export const CONFIG_KEYS = [
135
135
  },
136
136
  {
137
137
  key: "launch",
138
- describe: `Launch shape (${LAUNCH_SHAPES.join("|")})`,
138
+ describe: "Launch shape (terminal)",
139
139
  get: (c) => c.launch,
140
140
  set: (c, v) => {
141
141
  c.launch = coerceLaunch(v);
@@ -27,6 +27,11 @@ export function classifyInstall(entryRealpath, npmGlobalRoot) {
27
27
  if (npmGlobalRoot && p.startsWith(`${norm(npmGlobalRoot).replace(/\/$/, "")}/@mnemom/mnemom/`)) {
28
28
  return "npm-global";
29
29
  }
30
+ // `<prefix>/lib/node_modules/…` is npm's GLOBAL layout under a prefix `npm root -g` doesn't
31
+ // report (a custom --prefix, a switched HOME/.npmrc): not a project dependency, and not ours
32
+ // to self-update either — "unknown" gets the plain `npm install -g` hint.
33
+ if (p.includes("/lib/node_modules/@mnemom/mnemom/"))
34
+ return "unknown";
30
35
  if (p.includes("/node_modules/@mnemom/mnemom/"))
31
36
  return "project";
32
37
  return "unknown";
@@ -74,8 +74,7 @@ export function enforceMinCliVersion(response, exit = process.exit, write = (chu
74
74
  write(`\n✓ Updated the Mnemom CLI (was v${CLI_VERSION}; server minimum v${floor}). Re-run your command.\n\n`);
75
75
  exit(1);
76
76
  }
77
- write(`\n✖ Your Mnemom CLI (v${CLI_VERSION}) is too old for this server (minimum v${floor}).\n` +
78
- ` Old CLIs can silently drop newer request fields — e.g. \`--org\` on \`mnemom agent claim\`.\n\n` +
77
+ write(`\n✖ Your Mnemom CLI (v${CLI_VERSION}) is too old for this server (minimum v${floor}).\n\n` +
79
78
  ` Upgrade: npm i -g @mnemom/mnemom@latest\n\n`);
80
79
  exit(1);
81
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.11-next.1",
3
+ "version": "0.17.12",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {