@mnemom/mnemom 0.17.11 → 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.
@@ -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
@@ -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";
@@ -46,7 +46,7 @@ import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, }
46
46
  import { requireConsoleKey, readConsoleKey } from "../lib/console-key.js";
47
47
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
48
48
  import { defaultAgentSlug, ensureClaimedServedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.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, 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";
50
50
  import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
51
51
  import { slackChannelArgs } from "../lib/slack-launch.js";
52
52
  /** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
@@ -441,10 +441,16 @@ export async function agentCommand(scenarioArg, options = {}) {
441
441
  passthrough: [...(options.passthrough ?? []), "--resume", continuation.resumeCcSessionId],
442
442
  };
443
443
  }
444
- // A launch that resumes a Claude Code session (the continuation above, or the caller's own
445
- // `-- --resume <id>`) keeps the profile that session ran under, unless this launch chose one
446
- // with a flag. Read from the ledger only — nothing here writes a default anywhere.
447
- 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);
448
454
  if (resumeId && options.contextProfile === undefined && options.context !== false) {
449
455
  let resumed;
450
456
  try {
@@ -502,7 +508,8 @@ export async function agentCommand(scenarioArg, options = {}) {
502
508
  cli,
503
509
  envelope,
504
510
  goalMode,
505
- ccSessionId: cc.id,
511
+ // A `--continue` records the session it continues, so a later resume finds its profile.
512
+ ccSessionId: cc.id ?? continuedCcSessionId,
506
513
  contextProfile,
507
514
  compactAt: contextProfile === "off" ? null : compactAt,
508
515
  });
@@ -565,6 +572,12 @@ export async function agentCommand(scenarioArg, options = {}) {
565
572
  contextHint,
566
573
  ccSessionId,
567
574
  handoffMcpConfigPath,
575
+ resumeHint: (id) => resumeHintLine({
576
+ scenario,
577
+ agent: options.agent?.trim() ? agent : undefined,
578
+ ccSessionId: id,
579
+ }),
580
+ knownCcSessionId: cc.id ?? continuedCcSessionId,
568
581
  });
569
582
  return; // launchTerminal exits the process
570
583
  }
@@ -627,6 +640,52 @@ function choosesCcSession(passthrough) {
627
640
  return !pickCcSessionId([...passthrough], () => "").inject;
628
641
  }
629
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
+ }
630
689
  /**
631
690
  * Whether Claude Code still has the transcript of session `id`
632
691
  * (`${CLAUDE_CONFIG_DIR:-~/.claude}/projects/<any project>/<id>.jsonl`; `--resume` finds it in any
@@ -636,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
636
695
  export function ccSessionTranscriptExists(id, env) {
637
696
  if (!CC_SESSION_ID_RE.test(id))
638
697
  return false;
639
- const projects = join((env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"), "projects");
698
+ const projects = ccProjectsDir(env);
640
699
  try {
641
700
  return readdirSync(projects).some((d) => existsSync(join(projects, d, `${id}.jsonl`)));
642
701
  }
@@ -1086,8 +1145,15 @@ function launchTerminal(ctx) {
1086
1145
  child.on("exit", (code, signal) => {
1087
1146
  if (signal)
1088
1147
  process.kill(process.pid, signal);
1089
- 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
+ }
1090
1155
  process.exit(code ?? 0);
1156
+ }
1091
1157
  });
1092
1158
  };
1093
1159
  // Implicit goal mode: put the local base-URL proxy in front of the door so each main request
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, [])
@@ -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;
@@ -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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.11",
3
+ "version": "0.17.12",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {