@mnemom/mnemom 0.17.4-next.6 → 0.17.4-next.8

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.
package/README.md CHANGED
@@ -129,6 +129,25 @@ mnemom agent config set context_handoff off # back to the default
129
129
  This is experimental: it only ever ADDS the one tool (no gateway behavior changes for a
130
130
  session that leaves the setting off, and no change to Claude Code's own `/compact`).
131
131
 
132
+ #### Compaction signal and intercept
133
+
134
+ Every launch shape (terminal, `-- -p …` headless, `--remote-control`, `--server`) starts Claude
135
+ Code with `CLAUDE_CODE_GATEWAY_HINT_HEADERS=1`, so its requests carry the
136
+ `x-claude-code-request-class` / `x-claude-code-compaction` hint headers, which mark exactly
137
+ which request is a compaction (a gateway that does not read them ignores them). A value you set yourself (even `0`) wins. The launcher
138
+ never sets or changes your own auto-compaction settings (`CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`,
139
+ `CLAUDE_CODE_AUTO_COMPACT_WINDOW`, or `autoCompactWindow` in settings.json).
140
+
141
+ **Experimental, off by default.** `--compaction-intercept` (after `mnemom experimental enable
142
+ compaction-intercept`; standing default `mnemom agent config set compaction_intercept on`, or
143
+ `MNEMOM_AGENT_COMPACTION_INTERCEPT=1`) sends `GATEWAY_USAGE_REAL_SIZE=on` and
144
+ `GATEWAY_COMPACTION_INTERCEPT=on` in the per-session config header, asking the gateway to report
145
+ the real context size (so Claude Code compacts at its own threshold) and to answer that
146
+ compaction at once from its fold instead of a blocking model call. This needs the companion
147
+ gateway support: a gateway without it drops both keys (they are echoed back in
148
+ `x-mnemom-session-config-dropped`) and the session behaves exactly as with the setting off. `--no-compaction-intercept` turns it off for
149
+ one launch.
150
+
132
151
  ## How It Works
133
152
 
134
153
  ```
@@ -193,6 +193,9 @@ function deleteKey(cfg, key) {
193
193
  case "context_handoff":
194
194
  delete cfg.contextHandoff;
195
195
  break;
196
+ case "compaction_intercept":
197
+ delete cfg.compactionIntercept;
198
+ break;
196
199
  case "context_profile":
197
200
  delete cfg.contextProfile;
198
201
  break;
@@ -13,7 +13,8 @@
13
13
  */
14
14
  import { spawnSync } from "node:child_process";
15
15
  import { existsSync } from "node:fs";
16
- import { anthropicKeySource, checkRcCapability, resolveCliBin } from "./agent.js";
16
+ import { anthropicKeySource, checkRcCapability, defaultConsoleKeyDeps, resolveCliBin, } from "./agent.js";
17
+ import { readConsoleKey } from "../lib/console-key.js";
17
18
  import { resolveAgentAuth } from "../lib/subscription-login.js";
18
19
  import { isProdGateway, resolveAnthropicDoor, resolveGatewayHost, singleCellError, } from "../lib/agent.js";
19
20
  import { getApiUrl, getGatewayUrl } from "../lib/config.js";
@@ -52,11 +53,17 @@ export async function agentDoctorCommand(options = {}) {
52
53
  auth = resolveAgentAuth(undefined, env);
53
54
  }
54
55
  catch (err) {
55
- add("miss", `auth: ${err instanceof Error ? err.message : String(err)}`, "`mnemom agent config set auth api-key` (or subscription)");
56
+ add("miss", `auth: ${err instanceof Error ? err.message : String(err)}`, "`mnemom agent config set auth api-key` (or subscription, console)");
56
57
  }
57
58
  if (auth === "subscription") {
58
59
  add("ok", "auth: subscription — Claude Code uses your claude.ai login (Team/Enterprise plans only); no Anthropic key needed");
59
60
  }
61
+ else if (auth === "console") {
62
+ if (readConsoleKey(defaultConsoleKeyDeps(env)))
63
+ add("ok", "auth: console — using the API key your Claude Code Anthropic Console login created");
64
+ else
65
+ add("miss", "auth: console — Claude Code has no Anthropic Console login on this machine", 'run `claude`, then /login → "Anthropic Console account" — or `mnemom agent config set auth api-key`');
66
+ }
60
67
  else {
61
68
  const keySrc = anthropicKeySource(env);
62
69
  if (keySrc === "env")
@@ -28,6 +28,7 @@
28
28
  import { type MnemomKeyDeps } from "../lib/subscription-login.js";
29
29
  import { type SessionRecord } from "../lib/agent-ledger.js";
30
30
  import { type ExperimentalId } from "../lib/experimental.js";
31
+ import { type ConsoleKeyDeps } from "../lib/console-key.js";
31
32
  import { type ContextProfile, type LaunchShape, type SealedContractEnvelope } from "../lib/agent.js";
32
33
  export interface AgentOptions {
33
34
  goal?: string;
@@ -79,6 +80,13 @@ export interface AgentOptions {
79
80
  * ⇒ no header key, no MCP server — the launch is byte-identical to a build without this flag.
80
81
  */
81
82
  contextHandoff?: boolean;
83
+ /**
84
+ * EXPERIMENTAL, off by default (payload-bound fix): tells the gateway
85
+ * `GATEWAY_USAGE_REAL_SIZE=on` + `GATEWAY_COMPACTION_INTERCEPT=on` for this session only (real
86
+ * context size in usage, so Claude Code compacts at its OWN threshold; the compaction request
87
+ * answered at once from the gateway's fold). Absent/false ⇒ no header key.
88
+ */
89
+ compactionIntercept?: boolean;
82
90
  /**
83
91
  * Governed-agent provisioning: undefined = AUTO (provision-with-confirm when
84
92
  * interactive, warn-and-continue otherwise), true = force (`--setup`), false =
@@ -116,11 +124,30 @@ export declare function passthroughResumeId(passthrough: readonly string[]): str
116
124
  * - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
117
125
  * (else the newest matching row), reused without asking;
118
126
  * - no `--resume` → "Resume previous session <name>? [Y/n]", default Y; Y reuses that row's
119
- * conversation, n mints a new one. `--resume` is NOT added to Claude Code (the user often
120
- * resumes inside it with /resume). No TTY: Y.
127
+ * conversation, n mints a new one. No TTY: Y.
121
128
  * The ledger read is best-effort: a failure means a new id.
122
129
  */
123
- export declare function resolveLaunchConversationId(input: {
130
+ export declare function resolveLaunchConversationId(input: LaunchContinuationInput): Promise<string>;
131
+ /**
132
+ * Whether Claude Code still has the transcript of session `id`
133
+ * (`${CLAUDE_CONFIG_DIR:-~/.claude}/projects/<any project>/<id>.jsonl`; `--resume` finds it in any
134
+ * project). Claude Code exits on `--resume` of an id it has no transcript for, so a relaunch only
135
+ * resumes one that is there. Best-effort: an unreadable directory or a non-UUID id → false.
136
+ */
137
+ export declare function ccSessionTranscriptExists(id: string, env: NodeJS.ProcessEnv): boolean;
138
+ /**
139
+ * resolveLaunchConversationId, plus the Claude Code session to resume. A Y (or no-TTY) relaunch
140
+ * of an earlier implicit launch also resumes the CC session that launch pinned (`--resume <id>`),
141
+ * when the caller passed no session flag of their own: the gateway gives the conversation's main
142
+ * goal to ONE CC session id, and a fresh `--session-id` cannot reuse it (CC refuses an id already
143
+ * in use), so without the resume the relaunch would run on a new, empty thread. An n, a new
144
+ * conversation, or the caller's own `--resume` / `--session-id` / `--continue` resume nothing.
145
+ */
146
+ export declare function resolveLaunchContinuation(input: LaunchContinuationInput): Promise<{
147
+ conversationId: string;
148
+ resumeCcSessionId: string | null;
149
+ }>;
150
+ interface LaunchContinuationInput {
124
151
  session: string;
125
152
  orgId: string;
126
153
  agent: string;
@@ -131,7 +158,9 @@ export declare function resolveLaunchConversationId(input: {
131
158
  interactive: boolean;
132
159
  records: () => SessionRecord[];
133
160
  askYesNo: (question: string, defaultYes: boolean) => Promise<boolean>;
134
- }): Promise<string>;
161
+ /** Whether Claude Code still has that session's transcript (ccSessionTranscriptExists). */
162
+ ccSessionExists: (ccSessionId: string) => boolean;
163
+ }
135
164
  /**
136
165
  * Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
137
166
  * resolved best-effort (a lookup failure or an unknown slug records null); a
@@ -163,6 +192,8 @@ export declare function recordSessionLaunch(ctx: {
163
192
  }): Promise<void>;
164
193
  /** The real I/O behind a subscription launch's Mnemom key (lib/subscription-login.ts). */
165
194
  export declare function defaultMnemomKeyDeps(storePath?: string): MnemomKeyDeps;
195
+ /** The real machine behind lib/console-key.ts: the macOS keychain via `security`, no shell. */
196
+ export declare function defaultConsoleKeyDeps(env: NodeJS.ProcessEnv): ConsoleKeyDeps;
166
197
  /**
167
198
  * Resolve the Anthropic key with NO 1Password dependency (built for external
168
199
  * customer adoption), in order:
@@ -318,3 +349,4 @@ export declare function printPlan(ctx: {
318
349
  /** The experimental features on for this launch (the header's `experimental` array). */
319
350
  experimental?: readonly string[];
320
351
  }): void;
352
+ export {};
@@ -28,7 +28,7 @@
28
28
  import { spawn, spawnSync } from "node:child_process";
29
29
  import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, renameSync, } from "node:fs";
30
30
  import { randomUUID } from "node:crypto";
31
- import { tmpdir, homedir, hostname } from "node:os";
31
+ import { tmpdir, homedir, hostname, userInfo } from "node:os";
32
32
  import { basename, join, dirname } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
34
  import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
@@ -41,14 +41,17 @@ import { experimentalNotice } from "../lib/experimental.js";
41
41
  import { fmt } from "../lib/format.js";
42
42
  import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
43
43
  import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, } from "../lib/login-detect.js";
44
+ import { requireConsoleKey, readConsoleKey } from "../lib/console-key.js";
44
45
  import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
45
46
  import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
46
- import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, 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, GATEWAY_HINT_HEADERS_ENV, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
47
+ import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, 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";
47
48
  import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
48
49
  import { slackChannelArgs } from "../lib/slack-launch.js";
49
50
  /** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
50
51
  const CODE_STORE = LAUNCHER_STORE_PATH;
51
52
  const say = (line = "") => console.error(line);
53
+ /** The one-line launch note when the compaction intercept is ON. */
54
+ const COMPACTION_INTERCEPT_NOTE = "mnemom agent: compaction intercept ON (experimental) — requested from the gateway (GATEWAY_USAGE_REAL_SIZE, GATEWAY_COMPACTION_INTERCEPT); a gateway without the companion support drops both keys and the session runs as before.";
52
55
  /**
53
56
  * The one-line launch note for a non-default context profile (`balanced`, the default, is
54
57
  * silent — nothing is sent). `off` keeps the wording `--no-context` has always printed.
@@ -85,8 +88,10 @@ export async function agentCommand(scenarioArg, options = {}) {
85
88
  // Nothing configured it yet: detect Claude Code's own login and, when there is a real
86
89
  // choice, ask once and save the answer (lib/login-detect.ts). Never on --dry-run.
87
90
  if (!options.dryRun && !authConfigured(options.auth, env)) {
91
+ const loginState = readClaudeLoginState(env, homedir(), (path) => readFileSync(path, "utf8"));
88
92
  auth = await chooseAuth({
89
- login: detectClaudeLogin(readClaudeLoginState(env, homedir(), (path) => readFileSync(path, "utf8"))),
93
+ login: detectClaudeLogin(loginState),
94
+ consoleKeyAvailable: () => readConsoleKey(defaultConsoleKeyDeps(env)) !== undefined,
90
95
  interactive: isInteractive(),
91
96
  select: askSelect,
92
97
  persist: (choice) => {
@@ -149,11 +154,20 @@ export async function agentCommand(scenarioArg, options = {}) {
149
154
  say("mnemom agent: context handoff is not available with subscription login yet — launching without it.");
150
155
  contextHandoff = false;
151
156
  }
157
+ const compactionIntercept = resolveCompactionIntercept({
158
+ compactionIntercept: options.compactionIntercept,
159
+ env,
160
+ });
152
161
  const experimental = options.experimental ?? [];
153
- const sessionConfig = buildSessionConfig({ profile: contextProfile, contextHandoff });
162
+ const sessionConfig = buildSessionConfig({
163
+ profile: contextProfile,
164
+ contextHandoff,
165
+ compactionIntercept,
166
+ });
154
167
  const sessionConfigB64 = buildSessionConfigB64({
155
168
  profile: contextProfile,
156
169
  contextHandoff,
170
+ compactionIntercept,
157
171
  experimental,
158
172
  });
159
173
  const expNotice = experimentalNotice(experimental);
@@ -203,6 +217,9 @@ export async function agentCommand(scenarioArg, options = {}) {
203
217
  if (auth === "subscription") {
204
218
  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.");
205
219
  }
220
+ if (auth === "console") {
221
+ say("mnemom agent: auth = console — the launch would use the API key your Claude Code Anthropic Console login created.");
222
+ }
206
223
  if (expNotice)
207
224
  say(expNotice);
208
225
  if (previewShape === "remote-control") {
@@ -212,6 +229,8 @@ export async function agentCommand(scenarioArg, options = {}) {
212
229
  if (contextHandoff) {
213
230
  say("mnemom agent: context handoff ON (experimental) — the `handoff` MCP tool would be registered.");
214
231
  }
232
+ if (compactionIntercept)
233
+ say(COMPACTION_INTERCEPT_NOTE);
215
234
  return;
216
235
  }
217
236
  // Real launch: make sure we actually have a CLI to run. If we couldn't find it,
@@ -222,9 +241,12 @@ export async function agentCommand(scenarioArg, options = {}) {
222
241
  }
223
242
  // ── resolve the Anthropic key (zero 1Password; never logged) ──
224
243
  // Not on a subscription launch: Claude Code then sends its own claude.ai login instead.
225
- const anthropicKey = auth === "api-key"
226
- ? await resolveAnthropicKey(env, { allowPrompt: true, keySource: options.keySource })
227
- : undefined;
244
+ // A Console launch uses the key Claude Code's Console login created, then runs as api-key.
245
+ const anthropicKey = auth === "subscription"
246
+ ? undefined
247
+ : auth === "console"
248
+ ? requireConsoleKey(defaultConsoleKeyDeps(env))
249
+ : await resolveAnthropicKey(env, { allowPrompt: true, keySource: options.keySource });
228
250
  // ── account + µ-balance gate (no free tier) ─────────────────────────────────
229
251
  // mnemom agent runs governed and on µ, so before we seal a contract or launch we
230
252
  // (1) ensure a Mnemom session — offering the try-me-style sign-in / sign-up when
@@ -330,7 +352,7 @@ export async function agentCommand(scenarioArg, options = {}) {
330
352
  // so the goal the gateway inferred there is still the one it steers by. Resolved here, after
331
353
  // sign-in and the agent refresh, because a resume needs the org, agent and gateway host to
332
354
  // match the earlier launch.
333
- const conversationId = await resolveLaunchConversationId({
355
+ const continuation = await resolveLaunchContinuation({
334
356
  session: scenario,
335
357
  orgId: account.orgId,
336
358
  agent,
@@ -341,7 +363,19 @@ export async function agentCommand(scenarioArg, options = {}) {
341
363
  interactive: isInteractive(),
342
364
  records: () => readSessionRecords({ dir: MNEMOM_DIR }),
343
365
  askYesNo: (q, d) => askYesNo(q, d),
366
+ ccSessionExists: (id) => ccSessionTranscriptExists(id, process.env),
344
367
  });
368
+ const conversationId = continuation.conversationId;
369
+ // A relaunch that continues an earlier conversation resumes that Claude Code session, so the
370
+ // same CC session id keeps owning the conversation's main goal on the gateway (a fresh
371
+ // --session-id would land on a new, empty m.<sha16> thread). The server shape hosts many
372
+ // sessions and pins none.
373
+ if (continuation.resumeCcSessionId && shape !== "server") {
374
+ options = {
375
+ ...options,
376
+ passthrough: [...(options.passthrough ?? []), "--resume", continuation.resumeCcSessionId],
377
+ };
378
+ }
345
379
  // ── local session ledger (~/.mnemom/sessions.jsonl) ── one line per real
346
380
  // launch so `mnemom agent sessions` / `show` can find this conversation later.
347
381
  // Written after provisioning so a freshly claimed agent's id resolves. Holds
@@ -389,6 +423,8 @@ export async function agentCommand(scenarioArg, options = {}) {
389
423
  say(expNotice);
390
424
  say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
391
425
  sayContextProfile(contextProfile);
426
+ if (compactionIntercept)
427
+ say(COMPACTION_INTERCEPT_NOTE);
392
428
  // ── handoff MCP tool (MNE-7564) ── OFF by default: absent/false ⇒ handoffMcpConfigPath stays
393
429
  // undefined and buildCliArgs/spawnRcCli are byte-identical to a build without this flag. ON:
394
430
  // the config is written to a 0600 temp file and referenced by PATH only — never inlined into
@@ -489,21 +525,62 @@ export function passthroughResumeId(passthrough) {
489
525
  * - `-- --resume <cc-session-id>` → the matching row that pinned that Claude Code session
490
526
  * (else the newest matching row), reused without asking;
491
527
  * - no `--resume` → "Resume previous session <name>? [Y/n]", default Y; Y reuses that row's
492
- * conversation, n mints a new one. `--resume` is NOT added to Claude Code (the user often
493
- * resumes inside it with /resume). No TTY: Y.
528
+ * conversation, n mints a new one. No TTY: Y.
494
529
  * The ledger read is best-effort: a failure means a new id.
495
530
  */
496
531
  export async function resolveLaunchConversationId(input) {
497
- if ((input.explicit ?? "").trim())
498
- return resolveConversationId(input.explicit);
532
+ return (await resolveLaunchContinuation(input)).conversationId;
533
+ }
534
+ /**
535
+ * Whether the caller's passthrough already chooses the Claude Code session (`--session-id`,
536
+ * `--resume` / `-r`, `--continue` / `-c`, `--fork-session`): exactly when pickCcSessionId would
537
+ * NOT mint one, so the two never disagree about which flags count.
538
+ */
539
+ function choosesCcSession(passthrough) {
540
+ return !pickCcSessionId([...passthrough], () => "").inject;
541
+ }
542
+ 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;
543
+ /**
544
+ * Whether Claude Code still has the transcript of session `id`
545
+ * (`${CLAUDE_CONFIG_DIR:-~/.claude}/projects/<any project>/<id>.jsonl`; `--resume` finds it in any
546
+ * project). Claude Code exits on `--resume` of an id it has no transcript for, so a relaunch only
547
+ * resumes one that is there. Best-effort: an unreadable directory or a non-UUID id → false.
548
+ */
549
+ export function ccSessionTranscriptExists(id, env) {
550
+ if (!CC_SESSION_ID_RE.test(id))
551
+ return false;
552
+ const projects = join((env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"), "projects");
553
+ try {
554
+ return readdirSync(projects).some((d) => existsSync(join(projects, d, `${id}.jsonl`)));
555
+ }
556
+ catch {
557
+ return false;
558
+ }
559
+ }
560
+ /**
561
+ * resolveLaunchConversationId, plus the Claude Code session to resume. A Y (or no-TTY) relaunch
562
+ * of an earlier implicit launch also resumes the CC session that launch pinned (`--resume <id>`),
563
+ * when the caller passed no session flag of their own: the gateway gives the conversation's main
564
+ * goal to ONE CC session id, and a fresh `--session-id` cannot reuse it (CC refuses an id already
565
+ * in use), so without the resume the relaunch would run on a new, empty thread. An n, a new
566
+ * conversation, or the caller's own `--resume` / `--session-id` / `--continue` resume nothing.
567
+ */
568
+ export async function resolveLaunchContinuation(input) {
569
+ const fresh = () => ({
570
+ conversationId: resolveConversationId(undefined),
571
+ resumeCcSessionId: null,
572
+ });
573
+ if ((input.explicit ?? "").trim()) {
574
+ return { conversationId: resolveConversationId(input.explicit), resumeCcSessionId: null };
575
+ }
499
576
  if (!input.implicit)
500
- return resolveConversationId(undefined);
577
+ return fresh();
501
578
  let records;
502
579
  try {
503
580
  records = input.records();
504
581
  }
505
582
  catch {
506
- return resolveConversationId(undefined);
583
+ return fresh();
507
584
  }
508
585
  const found = findResumeCandidate(records, {
509
586
  session: input.session,
@@ -513,15 +590,24 @@ export async function resolveLaunchConversationId(input) {
513
590
  resumeId: passthroughResumeId(input.passthrough),
514
591
  });
515
592
  if (!found)
516
- return resolveConversationId(undefined);
593
+ return fresh();
517
594
  const conv = found.record.conversation_id;
518
595
  if (found.source === "session_name" && input.interactive) {
519
596
  const yes = await input.askYesNo(`Resume previous session ${input.session}?`, true);
520
597
  if (!yes)
521
- return resolveConversationId(undefined);
598
+ return fresh();
522
599
  }
523
600
  say(`mnemom agent: continuing conversation ${conv} from the earlier '${found.record.session}' launch (${found.record.started_at}), so its goal carries over.`);
524
- return resolveConversationId(conv);
601
+ const cc = found.record.cc_session_id?.trim() ?? "";
602
+ let resumeCcSessionId = null;
603
+ if (found.source === "session_name" && cc && !choosesCcSession(input.passthrough)) {
604
+ if (input.ccSessionExists(cc))
605
+ resumeCcSessionId = cc;
606
+ else {
607
+ say(`mnemom agent: Claude Code no longer has session ${cc}, so this launch starts a new one (its goal starts empty on the gateway).`);
608
+ }
609
+ }
610
+ return { conversationId: resolveConversationId(conv), resumeCcSessionId };
525
611
  }
526
612
  /**
527
613
  * Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
@@ -598,6 +684,23 @@ export function defaultMnemomKeyDeps(storePath = CODE_STORE) {
598
684
  say,
599
685
  };
600
686
  }
687
+ /** The real machine behind lib/console-key.ts: the macOS keychain via `security`, no shell. */
688
+ export function defaultConsoleKeyDeps(env) {
689
+ return {
690
+ platform: process.platform,
691
+ env,
692
+ username: () => userInfo().username,
693
+ readKeychain: (service, account) => {
694
+ const r = spawnSync("security", ["find-generic-password", "-a", account, "-w", "-s", service], {
695
+ encoding: "utf8",
696
+ timeout: 5_000,
697
+ });
698
+ return r.status === 0 ? r.stdout : undefined;
699
+ },
700
+ claudeJson: readClaudeLoginState(env, homedir(), (path) => readFileSync(path, "utf8"))
701
+ .claudeJson,
702
+ };
703
+ }
601
704
  /**
602
705
  * Resolve the Anthropic key with NO 1Password dependency (built for external
603
706
  * customer adoption), in order:
@@ -868,7 +971,11 @@ function launchTerminal(ctx) {
868
971
  applyToolSearchDefault(childEnv);
869
972
  const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
870
973
  childEnv[SESSION_NAME_ENV] = sessionName;
871
- childEnv[GATEWAY_HINT_HEADERS_ENV] = "1";
974
+ // Exact compaction signal for the gateway (x-claude-code-request-class: compaction), on every
975
+ // terminal launch — interactive and `-p` passthrough alike; a user's own value wins. The
976
+ // user's CLAUDE_AUTOCOMPACT_PCT_OVERRIDE / CLAUDE_CODE_AUTO_COMPACT_WINDOW ride through
977
+ // untouched in the copied env (USER_AUTOCOMPACT_ENV).
978
+ applyGatewayHintHeaders(childEnv);
872
979
  if (ctx.options.slack)
873
980
  args.push(...slackChannelArgs(sessionName));
874
981
  if (ctx.contextHint) {
@@ -1112,6 +1219,11 @@ function spawnRcCli(ctx) {
1112
1219
  childEnv.https_proxy = `http://127.0.0.1:${ctx.port}`;
1113
1220
  childEnv.NODE_EXTRA_CA_CERTS = ctx.ca;
1114
1221
  childEnv[SESSION_NAME_ENV] = rcName;
1222
+ // Same exact compaction signal as the terminal shape. Not an ANTHROPIC_* var, so the RC gate
1223
+ // ignores it (like HTTPS_PROXY above); the proxy forwards every non-hop request header, so the
1224
+ // x-claude-code-* hints reach the gateway. The user's own autocompact env is not on the strip
1225
+ // list and rides through untouched.
1226
+ applyGatewayHintHeaders(childEnv);
1115
1227
  // Under RC Claude Code sees a first-party host (the proxy, not ANTHROPIC_BASE_URL, routes to the
1116
1228
  // gateway), so tool search is already on by default; setting it anyway keeps every launch shape
1117
1229
  // on the same switch. A user value is kept.
package/dist/index.js CHANGED
@@ -143,7 +143,7 @@ const agentCmd = program
143
143
  .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)")
144
144
  .option("--remote-control", "Interactive Remote Control session in THIS terminal (behind a local proxy)")
145
145
  .option("--server", "Headless `claude remote-control` dispatcher (behind a local proxy)")
146
- .option("--auth <mode>", "How Claude Code authenticates: api-key (default) | subscription (your Claude Team/Enterprise login)")
146
+ .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)")
147
147
  .option("--model <alias|id>", "Model to launch (CLI alias or concrete id)")
148
148
  .addOption(new Option("--effort <level>", "Reasoning effort").choices([
149
149
  "low",
@@ -173,6 +173,8 @@ const agentCmd = program
173
173
  }))
174
174
  .addOption(new Option("--context-handoff", "EXPERIMENTAL (`mnemom experimental enable context-handoff`): register the `handoff(note)` MCP tool and tell the gateway to turn on the context-handoff capability for this session").hideHelp(hiddenUnless("context-handoff")))
175
175
  .option("--no-context-handoff", "Keep context handoff off for this launch even when launcher.toml (context_handoff = true) or MNEMOM_AGENT_CONTEXT_HANDOFF turns it on")
176
+ .addOption(new Option("--compaction-intercept", "EXPERIMENTAL (`mnemom experimental enable compaction-intercept`): tell the gateway to report the real context size (Claude Code compacts at its own threshold) and answer the compaction at once from its fold, for this session").hideHelp(hiddenUnless("compaction-intercept")))
177
+ .option("--no-compaction-intercept", "Keep the compaction intercept off for this launch even when launcher.toml (compaction_intercept = true) or MNEMOM_AGENT_COMPACTION_INTERCEPT turns it on")
176
178
  .option("--dry-run", "Print the resolved launch plan and exit — no key read, no spawn")
177
179
  .option("--first-run", "Show the first-run look again: what Mnemom would have saved on your last 30 days of Claude Code (runs on this machine, read-only, nothing uploaded) — then exit")
178
180
  .action(async (scenario, passthrough, opts) => {
@@ -216,6 +218,11 @@ const agentCmd = program
216
218
  used: opts.contextHandoff === true,
217
219
  },
218
220
  { what: "--implicit", id: "implicit", used: opts.implicit === true },
221
+ {
222
+ what: "--compaction-intercept",
223
+ id: "compaction-intercept",
224
+ used: opts.compactionIntercept === true,
225
+ },
219
226
  ], experimental);
220
227
  if (opts.gateway)
221
228
  process.env.MNEMOM_AGENT_GATEWAY = opts.gateway;
@@ -301,6 +308,10 @@ const agentCmd = program
301
308
  contextProfile: opts.contextProfile,
302
309
  // Off ⇒ an explicit false, so MNEMOM_AGENT_CONTEXT_HANDOFF cannot turn it on.
303
310
  contextHandoff: experimental.includes("context-handoff") ? opts.contextHandoff : false,
311
+ // Same gate: off ⇒ an explicit false, so MNEMOM_AGENT_COMPACTION_INTERCEPT cannot turn it on.
312
+ compactionIntercept: experimental.includes("compaction-intercept")
313
+ ? opts.compactionIntercept
314
+ : false,
304
315
  experimental,
305
316
  setup: opts.setup,
306
317
  keySource: cfg.keySource,
@@ -228,15 +228,6 @@ export declare function resolveWindowModel(input: {
228
228
  env: Record<string, string | undefined>;
229
229
  settings: () => CcSettingsView[];
230
230
  }): WindowModel;
231
- /**
232
- * Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class`,
233
- * `x-claude-code-agent-type`, `x-claude-code-prompt-id`). CC sends them to api.anthropic.com on
234
- * its own, so a Remote Control launch (rc-proxy in front of api.anthropic.com) already carries
235
- * them; with `ANTHROPIC_BASE_URL` pointing at the gateway door (the terminal shape) CC sends them
236
- * only when this is "1". The gateway reads them to keep away summaries, titles, compaction and
237
- * teammates off the main implicit goal.
238
- */
239
- export declare const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
240
231
  /** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
241
232
  export declare function sessionDisplayName(agent: string, scenario: string): string;
242
233
  /** A caller's own `--name <x>` in the passthrough args, if any (last one wins). */
@@ -322,6 +313,42 @@ export type ContextProfile = (typeof CONTEXT_PROFILES)[number];
322
313
  * header without the tool.
323
314
  */
324
315
  export declare const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
316
+ /**
317
+ * The two gateway env keys the `compaction_intercept` setting turns on for a single session
318
+ * (payload-bound fix; both default OFF on the gateway, both read from the per-session overlay):
319
+ * - `GATEWAY_USAGE_REAL_SIZE` — report the size of the body the client actually sent (the
320
+ * REAL, unfolded context) in the last `usage.iterations` entry, so Claude Code's own
321
+ * auto-compact threshold (its 1M default, or the user's `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` /
322
+ * `CLAUDE_CODE_AUTO_COMPACT_WINDOW` / `autoCompactWindow`) fires as it would without the fold;
323
+ * - `GATEWAY_COMPACTION_INTERCEPT` — answer the client's compaction request at once from the
324
+ * gateway's own fold digest (no model call), so the compaction never blocks the user.
325
+ * Sent together, value `"on"`, ONLY when the setting is on (see `resolveCompactionIntercept`).
326
+ */
327
+ export declare const USAGE_REAL_SIZE_KEY = "GATEWAY_USAGE_REAL_SIZE";
328
+ export declare const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
329
+ /**
330
+ * Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class: main |
331
+ * compaction`, `x-claude-code-compaction: auto | manual | reactive`,
332
+ * `x-claude-code-context-compacted` on the next turn, `x-claude-code-prompt-id`). Off by default
333
+ * in the client; the launcher turns it on for EVERY launch shape (terminal, `-p` passthrough,
334
+ * Remote Control interactive and server) so the gateway gets an exact compaction signal instead
335
+ * of relying on the body rule alone. Headers only — no behaviour change in the client. The
336
+ * gateway also reads them to keep summaries, titles, compaction and teammates off the main
337
+ * implicit goal.
338
+ */
339
+ export declare const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
340
+ /**
341
+ * Claude Code's own auto-compaction knobs (env). The launcher NEVER sets, changes or strips
342
+ * them: whatever the user configured is what the client honours (the `autoCompactWindow`
343
+ * settings.json key is likewise never touched — the launcher writes no settings file).
344
+ */
345
+ export declare const USER_AUTOCOMPACT_ENV: readonly ["CLAUDE_AUTOCOMPACT_PCT_OVERRIDE", "CLAUDE_CODE_AUTO_COMPACT_WINDOW"];
346
+ /**
347
+ * Turn Claude Code's gateway hint headers on in a child env, unless the user already set
348
+ * `CLAUDE_CODE_GATEWAY_HINT_HEADERS` themselves (any non-blank value, `0` included, wins).
349
+ * Mutates and returns `childEnv`. Touches no other key.
350
+ */
351
+ export declare function applyGatewayHintHeaders(childEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
325
352
  /**
326
353
  * The `x-mnemom-goal-mode` header value for `--implicit`, or null when the flag
327
354
  * is absent: `implicit[; max_turns=N][; budget_usd=X][; stall_turns=N]` — the
@@ -456,13 +483,15 @@ export declare function resolveContextProfile(opts: {
456
483
  * The decoded per-session env overlay for this launch — the `config` object of the
457
484
  * `x-mnemom-session-config` payload, in wire order: the profile's keys first
458
485
  * (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`; nothing
459
- * for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON. Empty
460
- * for `balanced` with handoff off — the common case, where no header is sent at all.
486
+ * for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
487
+ * `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when the compaction
488
+ * intercept is ON. Empty for `balanced` with both off — the common case, where no header is sent at all.
461
489
  * Exported so `--dry-run` can list exactly what the header carries.
462
490
  */
463
491
  export declare function buildSessionConfig(opts: {
464
492
  profile: ContextProfile;
465
493
  contextHandoff: boolean;
494
+ compactionIntercept?: boolean;
466
495
  }): Record<string, string>;
467
496
  /**
468
497
  * The `x-mnemom-session-config` payload: `{ v: 1, config, experimental? }`. The active
@@ -472,6 +501,7 @@ export declare function buildSessionConfig(opts: {
472
501
  export declare function buildSessionConfigPayload(opts: {
473
502
  profile: ContextProfile;
474
503
  contextHandoff: boolean;
504
+ compactionIntercept?: boolean;
475
505
  experimental?: readonly string[];
476
506
  }): {
477
507
  v: 1;
@@ -484,6 +514,8 @@ export declare function buildSessionConfigPayload(opts: {
484
514
  * byte-identical to a launch before profiles existed). Two independent overlays:
485
515
  * - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
486
516
  * `aggressive` → the three fold-tuning keys, `balanced` → nothing;
517
+ * - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
518
+ * `"on"` (the `compaction_intercept` setting);
487
519
  * - `contextHandoff: true` → `CONTEXT_HANDOFF_ENABLED: "true"` (the `context_handoff`
488
520
  * setting) — the SAME header the `handoff` MCP tool's own call to
489
521
  * `/mnemom/context/handoff` must also send, since the gateway resolves the flag
@@ -493,6 +525,7 @@ export declare function buildSessionConfigPayload(opts: {
493
525
  export declare function buildSessionConfigB64(opts: {
494
526
  profile: ContextProfile;
495
527
  contextHandoff: boolean;
528
+ compactionIntercept?: boolean;
496
529
  experimental?: readonly string[];
497
530
  }): string | undefined;
498
531
  /**
@@ -507,6 +540,17 @@ export declare function resolveContextHandoff(opts: {
507
540
  contextHandoff?: boolean;
508
541
  env?: NodeJS.ProcessEnv;
509
542
  }): boolean;
543
+ /**
544
+ * Is the compaction intercept (`GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT` in the
545
+ * session-config header) ON for this launch? OFF by default, same shape as
546
+ * `resolveContextHandoff`: an explicit `--compaction-intercept` / `--no-compaction-intercept`
547
+ * wins, else `MNEMOM_AGENT_COMPACTION_INTERCEPT` (which `applyConfigToEnv` sets from the
548
+ * `compaction_intercept` config key), else OFF.
549
+ */
550
+ export declare function resolveCompactionIntercept(opts: {
551
+ compactionIntercept?: boolean;
552
+ env?: NodeJS.ProcessEnv;
553
+ }): boolean;
510
554
  /** The name the `handoff` MCP tool server is registered under in `--mcp-config`. */
511
555
  export declare const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
512
556
  /**
package/dist/lib/agent.js CHANGED
@@ -301,15 +301,6 @@ 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
- /**
305
- * Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class`,
306
- * `x-claude-code-agent-type`, `x-claude-code-prompt-id`). CC sends them to api.anthropic.com on
307
- * its own, so a Remote Control launch (rc-proxy in front of api.anthropic.com) already carries
308
- * them; with `ANTHROPIC_BASE_URL` pointing at the gateway door (the terminal shape) CC sends them
309
- * only when this is "1". The gateway reads them to keep away summaries, titles, compaction and
310
- * teammates off the main implicit goal.
311
- */
312
- export const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
313
304
  /** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
314
305
  export function sessionDisplayName(agent, scenario) {
315
306
  return sanitizeAgentName(`${agent}-${scenario}`) || agent;
@@ -489,6 +480,50 @@ const CONTEXT_PROFILE_CONFIG = {
489
480
  * header without the tool.
490
481
  */
491
482
  export const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
483
+ /**
484
+ * The two gateway env keys the `compaction_intercept` setting turns on for a single session
485
+ * (payload-bound fix; both default OFF on the gateway, both read from the per-session overlay):
486
+ * - `GATEWAY_USAGE_REAL_SIZE` — report the size of the body the client actually sent (the
487
+ * REAL, unfolded context) in the last `usage.iterations` entry, so Claude Code's own
488
+ * auto-compact threshold (its 1M default, or the user's `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` /
489
+ * `CLAUDE_CODE_AUTO_COMPACT_WINDOW` / `autoCompactWindow`) fires as it would without the fold;
490
+ * - `GATEWAY_COMPACTION_INTERCEPT` — answer the client's compaction request at once from the
491
+ * gateway's own fold digest (no model call), so the compaction never blocks the user.
492
+ * Sent together, value `"on"`, ONLY when the setting is on (see `resolveCompactionIntercept`).
493
+ */
494
+ export const USAGE_REAL_SIZE_KEY = "GATEWAY_USAGE_REAL_SIZE";
495
+ export const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
496
+ /**
497
+ * Claude Code's opt-in for its gateway hint headers (`x-claude-code-request-class: main |
498
+ * compaction`, `x-claude-code-compaction: auto | manual | reactive`,
499
+ * `x-claude-code-context-compacted` on the next turn, `x-claude-code-prompt-id`). Off by default
500
+ * in the client; the launcher turns it on for EVERY launch shape (terminal, `-p` passthrough,
501
+ * Remote Control interactive and server) so the gateway gets an exact compaction signal instead
502
+ * of relying on the body rule alone. Headers only — no behaviour change in the client. The
503
+ * gateway also reads them to keep summaries, titles, compaction and teammates off the main
504
+ * implicit goal.
505
+ */
506
+ export const GATEWAY_HINT_HEADERS_ENV = "CLAUDE_CODE_GATEWAY_HINT_HEADERS";
507
+ /**
508
+ * Claude Code's own auto-compaction knobs (env). The launcher NEVER sets, changes or strips
509
+ * them: whatever the user configured is what the client honours (the `autoCompactWindow`
510
+ * settings.json key is likewise never touched — the launcher writes no settings file).
511
+ */
512
+ export const USER_AUTOCOMPACT_ENV = [
513
+ "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE",
514
+ "CLAUDE_CODE_AUTO_COMPACT_WINDOW",
515
+ ];
516
+ /**
517
+ * Turn Claude Code's gateway hint headers on in a child env, unless the user already set
518
+ * `CLAUDE_CODE_GATEWAY_HINT_HEADERS` themselves (any non-blank value, `0` included, wins).
519
+ * Mutates and returns `childEnv`. Touches no other key.
520
+ */
521
+ export function applyGatewayHintHeaders(childEnv) {
522
+ if ((childEnv[GATEWAY_HINT_HEADERS_ENV] ?? "").trim() === "") {
523
+ childEnv[GATEWAY_HINT_HEADERS_ENV] = "1";
524
+ }
525
+ return childEnv;
526
+ }
492
527
  const SEALED_ONLY_FLAGS = "--goal/--requirement/--allow/--forbid/--goal-id";
493
528
  /**
494
529
  * `--implicit` and a sealed contract are mutually exclusive: on the gateway a
@@ -732,8 +767,9 @@ export function resolveContextProfile(opts) {
732
767
  * The decoded per-session env overlay for this launch — the `config` object of the
733
768
  * `x-mnemom-session-config` payload, in wire order: the profile's keys first
734
769
  * (`CONTEXT_RUNTIME_ENABLED` for `off`; the three fold-tuning keys for `aggressive`; nothing
735
- * for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON. Empty
736
- * for `balanced` with handoff off — the common case, where no header is sent at all.
770
+ * for `balanced`), then `CONTEXT_HANDOFF_ENABLED: "true"` when context handoff is ON, then
771
+ * `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` when the compaction
772
+ * intercept is ON. Empty for `balanced` with both off — the common case, where no header is sent at all.
737
773
  * Exported so `--dry-run` can list exactly what the header carries.
738
774
  */
739
775
  export function buildSessionConfig(opts) {
@@ -742,6 +778,10 @@ export function buildSessionConfig(opts) {
742
778
  config[k] = v;
743
779
  if (opts.contextHandoff)
744
780
  config[CONTEXT_HANDOFF_KEY] = "true";
781
+ if (opts.compactionIntercept) {
782
+ config[USAGE_REAL_SIZE_KEY] = "on";
783
+ config[COMPACTION_INTERCEPT_KEY] = "on";
784
+ }
745
785
  return config;
746
786
  }
747
787
  /**
@@ -764,6 +804,8 @@ export function buildSessionConfigPayload(opts) {
764
804
  * byte-identical to a launch before profiles existed). Two independent overlays:
765
805
  * - the context profile (`buildSessionConfig`): `off` → `CONTEXT_RUNTIME_ENABLED: "false"`,
766
806
  * `aggressive` → the three fold-tuning keys, `balanced` → nothing;
807
+ * - `compactionIntercept: true` → `GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT`
808
+ * `"on"` (the `compaction_intercept` setting);
767
809
  * - `contextHandoff: true` → `CONTEXT_HANDOFF_ENABLED: "true"` (the `context_handoff`
768
810
  * setting) — the SAME header the `handoff` MCP tool's own call to
769
811
  * `/mnemom/context/handoff` must also send, since the gateway resolves the flag
@@ -792,6 +834,21 @@ export function resolveContextHandoff(opts) {
792
834
  return false;
793
835
  return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
794
836
  }
837
+ /**
838
+ * Is the compaction intercept (`GATEWAY_USAGE_REAL_SIZE` + `GATEWAY_COMPACTION_INTERCEPT` in the
839
+ * session-config header) ON for this launch? OFF by default, same shape as
840
+ * `resolveContextHandoff`: an explicit `--compaction-intercept` / `--no-compaction-intercept`
841
+ * wins, else `MNEMOM_AGENT_COMPACTION_INTERCEPT` (which `applyConfigToEnv` sets from the
842
+ * `compaction_intercept` config key), else OFF.
843
+ */
844
+ export function resolveCompactionIntercept(opts) {
845
+ if (opts.compactionIntercept !== undefined)
846
+ return opts.compactionIntercept;
847
+ const raw = (opts.env?.MNEMOM_AGENT_COMPACTION_INTERCEPT ?? "").trim().toLowerCase();
848
+ if (raw === "")
849
+ return false;
850
+ return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
851
+ }
795
852
  /** The name the `handoff` MCP tool server is registered under in `--mcp-config`. */
796
853
  export const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
797
854
  /**
@@ -0,0 +1,40 @@
1
+ /**
2
+ * `mnemom agent` with an Anthropic Console login (`auth = console`).
3
+ *
4
+ * Claude Code's `/login` → "Anthropic Console account" uses OAuth only to create a real
5
+ * Anthropic API key, which Claude Code stores and then sends as `x-api-key` ("/login
6
+ * managed key"). This module reads that key from where Claude Code keeps it, so the launch
7
+ * runs exactly like `auth = api-key` — same headers, same birth and claim, same identity —
8
+ * with no key for the user to copy. The key is read per launch and never stored by us.
9
+ *
10
+ * Where Claude Code keeps it (Claude Code 2.1.285, confirmed against the binary and a live
11
+ * probe on 2026-10-02):
12
+ * - macOS: the login keychain, service `Claude Code`, account $USER. When
13
+ * CLAUDE_CONFIG_DIR is set the service gains `-<first 8 hex of sha256(the raw
14
+ * CLAUDE_CONFIG_DIR value, NFC)>`; CLAUDE_SECURESTORAGE_CONFIG_DIR, when set, replaces
15
+ * that value (empty = no suffix).
16
+ * - everywhere (and the macOS fallback): `primaryApiKey` in Claude Code's .claude.json.
17
+ *
18
+ * A `claude auth login --console` *profile* (keyless OAuth, ~/.config/anthropic) creates no
19
+ * such key, so it is not read here.
20
+ *
21
+ * Pure apart from the injected deps, so every arm is unit-tested.
22
+ */
23
+ export interface ConsoleKeyDeps {
24
+ platform: NodeJS.Platform;
25
+ env: Record<string, string | undefined>;
26
+ /** os.userInfo().username, for when $USER is unset. */
27
+ username(): string;
28
+ /** The keychain password for (service, account), or undefined when absent. */
29
+ readKeychain(service: string, account: string): string | undefined;
30
+ /** Parsed .claude.json (see readClaudeLoginState), or undefined. */
31
+ claudeJson: unknown;
32
+ }
33
+ /** The keychain service name Claude Code stores the managed key under. */
34
+ export declare function claudeKeychainService(env: Record<string, string | undefined>): string;
35
+ /** The keychain account Claude Code uses: $USER, else the OS username, else a fixed name. */
36
+ export declare function claudeKeychainAccount(env: Record<string, string | undefined>, username: () => string): string;
37
+ /** Claude Code's Console-login API key on this machine, or undefined. Never throws. */
38
+ export declare function readConsoleKey(deps: ConsoleKeyDeps): string | undefined;
39
+ /** The key for an `auth = console` launch; throws with what to do when there is none. */
40
+ export declare function requireConsoleKey(deps: ConsoleKeyDeps): string;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * `mnemom agent` with an Anthropic Console login (`auth = console`).
3
+ *
4
+ * Claude Code's `/login` → "Anthropic Console account" uses OAuth only to create a real
5
+ * Anthropic API key, which Claude Code stores and then sends as `x-api-key` ("/login
6
+ * managed key"). This module reads that key from where Claude Code keeps it, so the launch
7
+ * runs exactly like `auth = api-key` — same headers, same birth and claim, same identity —
8
+ * with no key for the user to copy. The key is read per launch and never stored by us.
9
+ *
10
+ * Where Claude Code keeps it (Claude Code 2.1.285, confirmed against the binary and a live
11
+ * probe on 2026-10-02):
12
+ * - macOS: the login keychain, service `Claude Code`, account $USER. When
13
+ * CLAUDE_CONFIG_DIR is set the service gains `-<first 8 hex of sha256(the raw
14
+ * CLAUDE_CONFIG_DIR value, NFC)>`; CLAUDE_SECURESTORAGE_CONFIG_DIR, when set, replaces
15
+ * that value (empty = no suffix).
16
+ * - everywhere (and the macOS fallback): `primaryApiKey` in Claude Code's .claude.json.
17
+ *
18
+ * A `claude auth login --console` *profile* (keyless OAuth, ~/.config/anthropic) creates no
19
+ * such key, so it is not read here.
20
+ *
21
+ * Pure apart from the injected deps, so every arm is unit-tested.
22
+ */
23
+ import { createHash } from "node:crypto";
24
+ const ACCOUNT_RE = /^[a-zA-Z0-9._-]+$/;
25
+ /** The keychain service name Claude Code stores the managed key under. */
26
+ export function claudeKeychainService(env) {
27
+ const secure = env.CLAUDE_SECURESTORAGE_CONFIG_DIR;
28
+ const dir = secure !== undefined ? secure : (env.CLAUDE_CONFIG_DIR ?? "");
29
+ if (!dir)
30
+ return "Claude Code";
31
+ const hash = createHash("sha256").update(dir.normalize("NFC")).digest("hex").slice(0, 8);
32
+ return `Claude Code-${hash}`;
33
+ }
34
+ /** The keychain account Claude Code uses: $USER, else the OS username, else a fixed name. */
35
+ export function claudeKeychainAccount(env, username) {
36
+ let name;
37
+ try {
38
+ name = env.USER || username();
39
+ }
40
+ catch {
41
+ name = "";
42
+ }
43
+ return ACCOUNT_RE.test(name) ? name : "claude-code-user";
44
+ }
45
+ /** The key is spliced into a newline-joined header block, so CR/LF must never survive. */
46
+ function clean(raw) {
47
+ const v = (raw ?? "").replace(/[\r\n]+/g, "").trim();
48
+ return v || undefined;
49
+ }
50
+ /** Claude Code's Console-login API key on this machine, or undefined. Never throws. */
51
+ export function readConsoleKey(deps) {
52
+ if (deps.platform === "darwin") {
53
+ let fromKeychain;
54
+ try {
55
+ fromKeychain = clean(deps.readKeychain(claudeKeychainService(deps.env), claudeKeychainAccount(deps.env, deps.username)));
56
+ }
57
+ catch {
58
+ fromKeychain = undefined;
59
+ }
60
+ if (fromKeychain)
61
+ return fromKeychain;
62
+ }
63
+ const json = deps.claudeJson;
64
+ const stored = json && typeof json === "object"
65
+ ? json.primaryApiKey
66
+ : undefined;
67
+ return typeof stored === "string" ? clean(stored) : undefined;
68
+ }
69
+ /** The key for an `auth = console` launch; throws with what to do when there is none. */
70
+ export function requireConsoleKey(deps) {
71
+ const key = readConsoleKey(deps);
72
+ if (key)
73
+ return key;
74
+ throw new Error("mnemom agent: auth = console, but Claude Code has no Anthropic Console login on this machine.\n" +
75
+ ' Sign in: run `claude`, then /login → "Anthropic Console account".\n' +
76
+ " Or use a key instead: `mnemom agent config set auth api-key`.");
77
+ }
@@ -12,7 +12,7 @@
12
12
  * Everything here is pure or dependency-injected; index.ts and the command
13
13
  * modules are the thin drivers.
14
14
  */
15
- export type ExperimentalId = "context-handoff" | "slack" | "implicit";
15
+ export type ExperimentalId = "context-handoff" | "slack" | "implicit" | "compaction-intercept";
16
16
  export interface ExperimentalFeature {
17
17
  id: ExperimentalId;
18
18
  summary: string;
@@ -73,7 +73,8 @@ export declare function assertEnabled(uses: ReadonlyArray<{
73
73
  export declare function experimentalIdForConfigKey(key: string, value: string): ExperimentalId | undefined;
74
74
  /**
75
75
  * The stderr lines for experimental settings a launch ignores because their feature
76
- * is off: launcher.toml keys (`entries` as `[key, value]`) and MNEMOM_AGENT_CONTEXT_HANDOFF.
76
+ * is off: launcher.toml keys (`entries` as `[key, value]`), MNEMOM_AGENT_CONTEXT_HANDOFF and
77
+ * MNEMOM_AGENT_COMPACTION_INTERCEPT.
77
78
  */
78
79
  export declare function ignoredSettingLines(opts: {
79
80
  entries: ReadonlyArray<[string, string]>;
@@ -46,6 +46,17 @@ export const EXPERIMENTAL_FEATURES = [
46
46
  env: [],
47
47
  },
48
48
  },
49
+ {
50
+ id: "compaction-intercept",
51
+ summary: "Have the gateway report the real context size (so Claude Code compacts at its own threshold) and answer the compaction at once from its fold",
52
+ since: "0.17.4",
53
+ gates: {
54
+ flags: ["--compaction-intercept"],
55
+ configKeys: ["compaction_intercept"],
56
+ commands: [],
57
+ env: ["MNEMOM_AGENT_COMPACTION_INTERCEPT"],
58
+ },
59
+ },
49
60
  ];
50
61
  export const EXPERIMENTAL_IDS = EXPERIMENTAL_FEATURES.map((f) => f.id);
51
62
  export const EXPERIMENTAL_ENV = "MNEMOM_EXPERIMENTAL";
@@ -127,19 +138,22 @@ export function experimentalIdForConfigKey(key, value) {
127
138
  return value === "true" ? "slack" : undefined;
128
139
  case "context_handoff":
129
140
  return value === "true" ? "context-handoff" : undefined;
141
+ case "compaction_intercept":
142
+ return value === "true" ? "compaction-intercept" : undefined;
130
143
  case "goal_mode":
131
144
  return value === "implicit" ? "implicit" : undefined;
132
145
  default:
133
146
  return undefined;
134
147
  }
135
148
  }
136
- /** Is an env value one of the truthy spellings resolveContextHandoff accepts? */
149
+ /** Is an env value one of the truthy spellings resolveContextHandoff / resolveCompactionIntercept accept? */
137
150
  function truthy(raw) {
138
151
  return ["1", "true", "on", "yes"].includes((raw ?? "").trim().toLowerCase());
139
152
  }
140
153
  /**
141
154
  * The stderr lines for experimental settings a launch ignores because their feature
142
- * is off: launcher.toml keys (`entries` as `[key, value]`) and MNEMOM_AGENT_CONTEXT_HANDOFF.
155
+ * is off: launcher.toml keys (`entries` as `[key, value]`), MNEMOM_AGENT_CONTEXT_HANDOFF and
156
+ * MNEMOM_AGENT_COMPACTION_INTERCEPT.
143
157
  */
144
158
  export function ignoredSettingLines(opts) {
145
159
  const lines = [];
@@ -152,6 +166,10 @@ export function ignoredSettingLines(opts) {
152
166
  if (truthy(opts.env.MNEMOM_AGENT_CONTEXT_HANDOFF) && !opts.enabled.includes("context-handoff")) {
153
167
  lines.push(`mnemom agent: ignoring ${gatedMessage("MNEMOM_AGENT_CONTEXT_HANDOFF", "context-handoff")}`);
154
168
  }
169
+ if (truthy(opts.env.MNEMOM_AGENT_COMPACTION_INTERCEPT) &&
170
+ !opts.enabled.includes("compaction-intercept")) {
171
+ lines.push(`mnemom agent: ignoring ${gatedMessage("MNEMOM_AGENT_COMPACTION_INTERCEPT", "compaction-intercept")}`);
172
+ }
155
173
  return lines;
156
174
  }
157
175
  /** The features an existing launcher.toml already uses (the one-time migration). */
@@ -199,6 +217,8 @@ export function configKeyFeature(key) {
199
217
  return "slack";
200
218
  if (key === "context_handoff")
201
219
  return "context-handoff";
220
+ if (key === "compaction_intercept")
221
+ return "compaction-intercept";
202
222
  if (key === "goal_mode")
203
223
  return "implicit";
204
224
  return undefined;
@@ -49,6 +49,12 @@ export interface LauncherConfig {
49
49
  * every launch. Off means byte-identical to a launch with no MNE-7564 change at all.
50
50
  */
51
51
  contextHandoff?: boolean;
52
+ /**
53
+ * Compaction-intercept default (EXPERIMENTAL, off when unset): `true` sends
54
+ * `GATEWAY_USAGE_REAL_SIZE: "on"` + `GATEWAY_COMPACTION_INTERCEPT: "on"` in the per-session
55
+ * config header for every launch. Off means no header key at all.
56
+ */
57
+ compactionIntercept?: boolean;
52
58
  /**
53
59
  * How the gateway folds each session's context (`balanced` when unset): `off` leaves
54
60
  * compaction to the coding agent, `balanced` keeps more earlier work verbatim, `aggressive`
@@ -107,6 +113,6 @@ export declare function launcherEntries(cfg: LauncherConfig): Array<[string, str
107
113
  /**
108
114
  * `cfg` without the settings of experimental features that are off (a copy; `cfg` is
109
115
  * untouched): `slack_*` while `slack` is off, `context_handoff` while `context-handoff` is
110
- * off, and `goal_mode = implicit` while `implicit` is off (`goal_mode = off` is kept).
116
+ * off, `compaction_intercept` while `compaction-intercept` is off, and `goal_mode = implicit` while `implicit` is off (`goal_mode = off` is kept).
111
117
  */
112
118
  export declare function withoutDisabledExperimental(cfg: LauncherConfig, enabled: readonly ExperimentalId[]): LauncherConfig;
@@ -135,7 +135,7 @@ export const CONFIG_KEYS = [
135
135
  },
136
136
  {
137
137
  key: "auth",
138
- describe: `How Claude Code authenticates (${AGENT_AUTHS.join("|")}); subscription = your Claude Team/Enterprise login`,
138
+ describe: `How Claude Code authenticates (${AGENT_AUTHS.join("|")}); subscription = your Claude Team/Enterprise login, console = your Claude Code Anthropic Console login's key`,
139
139
  get: (c) => c.auth,
140
140
  set: (c, v) => {
141
141
  c.auth = coerceAgentAuth(v);
@@ -197,6 +197,14 @@ export const CONFIG_KEYS = [
197
197
  c.contextHandoff = coerceBool("context_handoff", v);
198
198
  },
199
199
  },
200
+ {
201
+ key: "compaction_intercept",
202
+ describe: "EXPERIMENTAL: tell the gateway GATEWAY_USAGE_REAL_SIZE=on + GATEWAY_COMPACTION_INTERCEPT=on (off by default)",
203
+ get: (c) => c.compactionIntercept,
204
+ set: (c, v) => {
205
+ c.compactionIntercept = coerceBool("compaction_intercept", v);
206
+ },
207
+ },
200
208
  {
201
209
  key: "context_profile",
202
210
  describe: `How the gateway folds each session's context (${CONTEXT_PROFILES.join("|")}; balanced by default)`,
@@ -285,6 +293,8 @@ export function parseLauncherConfig(text) {
285
293
  cfg.slackChannel = coerceNonEmpty("slack_channel", str(raw.slack_channel) ?? "");
286
294
  if (raw.context_handoff !== undefined)
287
295
  cfg.contextHandoff = coerceBool("context_handoff", str(raw.context_handoff) ?? "");
296
+ if (raw.compaction_intercept !== undefined)
297
+ cfg.compactionIntercept = coerceBool("compaction_intercept", str(raw.compaction_intercept) ?? "");
288
298
  if (raw.context_profile !== undefined)
289
299
  cfg.contextProfile = parseContextProfile(str(raw.context_profile) ?? "", "context_profile");
290
300
  const g = raw.guardrails;
@@ -357,6 +367,8 @@ export function serializeLauncherConfig(cfg) {
357
367
  doc.slack_allow = cfg.slackAllow;
358
368
  if (cfg.contextHandoff !== undefined)
359
369
  doc.context_handoff = cfg.contextHandoff;
370
+ if (cfg.compactionIntercept !== undefined)
371
+ doc.compaction_intercept = cfg.compactionIntercept;
360
372
  if (cfg.contextProfile !== undefined)
361
373
  doc.context_profile = cfg.contextProfile;
362
374
  const g = cfg.guardrails;
@@ -414,6 +426,10 @@ export function applyConfigToEnv(cfg, env = process.env) {
414
426
  if (cfg.contextHandoff !== undefined && !(env.MNEMOM_AGENT_CONTEXT_HANDOFF ?? "").trim()) {
415
427
  env.MNEMOM_AGENT_CONTEXT_HANDOFF = cfg.contextHandoff ? "1" : "0";
416
428
  }
429
+ if (cfg.compactionIntercept !== undefined &&
430
+ !(env.MNEMOM_AGENT_COMPACTION_INTERCEPT ?? "").trim()) {
431
+ env.MNEMOM_AGENT_COMPACTION_INTERCEPT = cfg.compactionIntercept ? "1" : "0";
432
+ }
417
433
  if (cfg.contextProfile !== undefined && !(env.MNEMOM_AGENT_CONTEXT_PROFILE ?? "").trim()) {
418
434
  env.MNEMOM_AGENT_CONTEXT_PROFILE = cfg.contextProfile;
419
435
  }
@@ -428,7 +444,7 @@ export function launcherEntries(cfg) {
428
444
  /**
429
445
  * `cfg` without the settings of experimental features that are off (a copy; `cfg` is
430
446
  * untouched): `slack_*` while `slack` is off, `context_handoff` while `context-handoff` is
431
- * off, and `goal_mode = implicit` while `implicit` is off (`goal_mode = off` is kept).
447
+ * off, `compaction_intercept` while `compaction-intercept` is off, and `goal_mode = implicit` while `implicit` is off (`goal_mode = off` is kept).
432
448
  */
433
449
  export function withoutDisabledExperimental(cfg, enabled) {
434
450
  const out = { ...cfg };
@@ -439,6 +455,8 @@ export function withoutDisabledExperimental(cfg, enabled) {
439
455
  }
440
456
  if (!enabled.includes("context-handoff"))
441
457
  delete out.contextHandoff;
458
+ if (!enabled.includes("compaction-intercept"))
459
+ delete out.compactionIntercept;
442
460
  if (!enabled.includes("implicit") && out.goalMode === "implicit")
443
461
  delete out.goalMode;
444
462
  return out;
@@ -6,7 +6,9 @@
6
6
  * Four ways Claude Code can be signed in, and what the gateway does with each today:
7
7
  * - Claude subscription, Team or Enterprise → `auth = subscription` (supported).
8
8
  * - Claude subscription, Pro / Max (individual) → not supported (Team/Enterprise only).
9
- * - Anthropic Console login → not supported yet.
9
+ * - Anthropic Console login (`/login` → Console account) → `auth = console` (supported): the
10
+ * API key Claude Code created is read from its keychain (lib/console-key.ts). A
11
+ * `claude auth login --console` profile creates no key → not supported yet.
10
12
  * - Third-party provider (Bedrock / Vertex / Foundry) → not supported yet (no gateway door).
11
13
  * An Anthropic API key always works (`auth = api-key`, the default).
12
14
  *
@@ -16,7 +18,7 @@
16
18
  *
17
19
  * Pure apart from the injected deps, so every arm is unit-tested.
18
20
  */
19
- import type { AgentAuth } from "./subscription-login.js";
21
+ import { type AgentAuth } from "./subscription-login.js";
20
22
  export type ThirdPartyProvider = "bedrock" | "vertex" | "foundry";
21
23
  export type ClaudeLogin = {
22
24
  kind: "subscription";
@@ -46,10 +48,13 @@ export type UnsupportedLogin = Extract<ClaudeLogin, {
46
48
  /** Why a detected login can't be used through the gateway. */
47
49
  export declare function unsupportedReason(login: UnsupportedLogin): string;
48
50
  export declare const MENU_SUBSCRIPTION = "Claude subscription \u2014 Team or Enterprise (your claude.ai login)";
51
+ export declare const MENU_CONSOLE = "Anthropic Console \u2014 the API key your Claude Code /login created (API billing)";
49
52
  export declare const MENU_API_KEY = "Anthropic API key";
50
53
  export interface ChooseAuthDeps {
51
54
  login: ClaudeLogin;
52
55
  interactive: boolean;
56
+ /** Whether Claude Code's Console-login key is readable (lib/console-key.ts); asked only for a Console login. */
57
+ consoleKeyAvailable(): boolean;
53
58
  select(question: string, options: string[]): Promise<string | null>;
54
59
  persist(auth: AgentAuth): void;
55
60
  say(line: string): void;
@@ -58,8 +63,9 @@ export interface ChooseAuthDeps {
58
63
  * Pick `auth` when nothing configured it (no --auth, MNEMOM_AGENT_AUTH or launcher.toml).
59
64
  * - No Claude login detected → api-key, silently, nothing saved (today's behaviour).
60
65
  * - A login we can't use → api-key, say why once, save it so the note isn't repeated.
61
- * - Team/Enterprise and non-interactive → api-key, nothing saved (never switch a script's
62
- * auth without asking); interactive → a two-option menu (subscription or API key), answer saved.
66
+ * - Team/Enterprise, or a Console login whose key is readable, and non-interactive →
67
+ * api-key, nothing saved (never switch a script's auth without asking); interactive → a
68
+ * two-option menu (that login or an API key), answer saved.
63
69
  */
64
70
  export declare function chooseAuth(deps: ChooseAuthDeps): Promise<AgentAuth>;
65
71
  /** True when `auth` is already decided: `--auth`, or MNEMOM_AGENT_AUTH (launcher.toml folds in). */
@@ -6,7 +6,9 @@
6
6
  * Four ways Claude Code can be signed in, and what the gateway does with each today:
7
7
  * - Claude subscription, Team or Enterprise → `auth = subscription` (supported).
8
8
  * - Claude subscription, Pro / Max (individual) → not supported (Team/Enterprise only).
9
- * - Anthropic Console login → not supported yet.
9
+ * - Anthropic Console login (`/login` → Console account) → `auth = console` (supported): the
10
+ * API key Claude Code created is read from its keychain (lib/console-key.ts). A
11
+ * `claude auth login --console` profile creates no key → not supported yet.
10
12
  * - Third-party provider (Bedrock / Vertex / Foundry) → not supported yet (no gateway door).
11
13
  * An Anthropic API key always works (`auth = api-key`, the default).
12
14
  *
@@ -16,6 +18,7 @@
16
18
  *
17
19
  * Pure apart from the injected deps, so every arm is unit-tested.
18
20
  */
21
+ import { AGENT_AUTHS } from "./subscription-login.js";
19
22
  const PROVIDER_SWITCHES = [
20
23
  ["CLAUDE_CODE_USE_BEDROCK", "bedrock"],
21
24
  ["CLAUDE_CODE_USE_VERTEX", "vertex"],
@@ -60,14 +63,15 @@ export function unsupportedReason(login) {
60
63
  case "individual":
61
64
  return `your Claude ${login.plan} plan is a personal plan — Mnemom supports subscription login on Team and Enterprise plans only`;
62
65
  case "console":
63
- return "Anthropic Console login is not supported yet";
66
+ return "your Anthropic Console login has no API key Claude Code created with /login (a `claude auth login --console` profile is not supported yet)";
64
67
  case "third-party":
65
68
  return `${PROVIDER_NAMES[login.provider]} is not supported yet`;
66
69
  }
67
70
  }
68
71
  export const MENU_SUBSCRIPTION = "Claude subscription — Team or Enterprise (your claude.ai login)";
72
+ export const MENU_CONSOLE = "Anthropic Console — the API key your Claude Code /login created (API billing)";
69
73
  export const MENU_API_KEY = "Anthropic API key";
70
- const SAVED_HINT = "change it any time with `mnemom agent config set auth api-key|subscription`";
74
+ const SAVED_HINT = `change it any time with \`mnemom agent config set auth ${AGENT_AUTHS.join("|")}\``;
71
75
  function save(deps, auth, line) {
72
76
  try {
73
77
  deps.persist(auth);
@@ -82,13 +86,27 @@ function save(deps, auth, line) {
82
86
  * Pick `auth` when nothing configured it (no --auth, MNEMOM_AGENT_AUTH or launcher.toml).
83
87
  * - No Claude login detected → api-key, silently, nothing saved (today's behaviour).
84
88
  * - A login we can't use → api-key, say why once, save it so the note isn't repeated.
85
- * - Team/Enterprise and non-interactive → api-key, nothing saved (never switch a script's
86
- * auth without asking); interactive → a two-option menu (subscription or API key), answer saved.
89
+ * - Team/Enterprise, or a Console login whose key is readable, and non-interactive →
90
+ * api-key, nothing saved (never switch a script's auth without asking); interactive → a
91
+ * two-option menu (that login or an API key), answer saved.
87
92
  */
88
93
  export async function chooseAuth(deps) {
89
94
  const { login } = deps;
90
95
  if (login.kind === "none")
91
96
  return "api-key";
97
+ if (login.kind === "console" && deps.consoleKeyAvailable()) {
98
+ if (!deps.interactive)
99
+ return "api-key";
100
+ const picked = await deps.select("mnemom agent: Claude Code is signed in with an Anthropic Console account. How should it sign in through Mnemom?", [MENU_CONSOLE, MENU_API_KEY]);
101
+ if (picked === MENU_CONSOLE) {
102
+ return save(deps, "console", "mnemom agent: using your Anthropic Console login's API key.");
103
+ }
104
+ if (picked === MENU_API_KEY) {
105
+ return save(deps, "api-key", "mnemom agent: using an Anthropic API key.");
106
+ }
107
+ deps.say("mnemom agent: no choice made — using an Anthropic API key for this launch.");
108
+ return "api-key";
109
+ }
92
110
  if (login.kind !== "subscription") {
93
111
  return save(deps, "api-key", `mnemom agent: ${unsupportedReason(login)}, so this launch uses an Anthropic API key.`);
94
112
  }
@@ -17,7 +17,8 @@
17
17
  * Pure apart from the injected deps, so every arm is unit-tested.
18
18
  */
19
19
  import type { LaunchShape } from "./agent.js";
20
- export type AgentAuth = "api-key" | "subscription";
20
+ /** `console` = the API key Claude Code's Anthropic Console login created (lib/console-key.ts). */
21
+ export type AgentAuth = "api-key" | "subscription" | "console";
21
22
  export declare const AGENT_AUTHS: readonly AgentAuth[];
22
23
  /** Env override for the setting (the launcher.toml `auth` key folds into it). */
23
24
  export declare const AGENT_AUTH_ENV = "MNEMOM_AGENT_AUTH";
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * Pure apart from the injected deps, so every arm is unit-tested.
18
18
  */
19
- export const AGENT_AUTHS = ["api-key", "subscription"];
19
+ export const AGENT_AUTHS = ["api-key", "subscription", "console"];
20
20
  /** Env override for the setting (the launcher.toml `auth` key folds into it). */
21
21
  export const AGENT_AUTH_ENV = "MNEMOM_AGENT_AUTH";
22
22
  /** Env override for the Mnemom key a subscription launch sends. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.4-next.6",
3
+ "version": "0.17.4-next.8",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {