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