@mnemom/mnemom 0.17.20-next.0 → 0.17.21
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-sessions.d.ts +15 -5
- package/dist/commands/agent-sessions.js +29 -5
- 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 +89 -3
- package/dist/commands/validate.d.ts +1 -1
- package/dist/commands/validate.js +7 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +11 -2
- package/dist/lib/api.d.ts +41 -0
- package/dist/lib/auth.d.ts +3 -1
- package/dist/lib/auth.js +2 -2
- package/dist/lib/claude-plugin.d.ts +114 -0
- package/dist/lib/claude-plugin.js +148 -0
- package/dist/lib/config.d.ts +13 -2
- package/dist/lib/config.js +22 -6
- package/dist/lib/first-run/governed.js +7 -4
- package/dist/lib/oauth.d.ts +3 -1
- package/dist/lib/oauth.js +13 -1
- package/dist/lib/retired-env-guard.d.ts +7 -0
- package/dist/lib/retired-env-guard.js +46 -0
- package/dist/rc-proxy.mjs +83 -13
- package/dist/rc-state.mjs +257 -0
- package/package.json +5 -1
- package/plugins/.claude-plugin/marketplace.json +20 -0
- package/plugins/mnemom/.claude-plugin/plugin.json +47 -0
- package/plugins/mnemom/.mcp.json +12 -0
- package/plugins/mnemom/hooks/hooks.json +3 -0
- package/plugins/mnemom/hooks/register.tsx +1476 -0
- package/plugins/mnemom/skills/about-mnemom/SKILL.md +88 -0
- package/plugins/mnemom/types/index.d.ts +142 -0
|
@@ -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" +
|
|
@@ -110,11 +110,13 @@ export declare function formatTrailEntry(e: AgentSessionVerdictTrailEntry): stri
|
|
|
110
110
|
*/
|
|
111
111
|
export declare function verdictTrailKey(e: Pick<AgentSessionVerdictTrailEntry, "request_id" | "turn">): string;
|
|
112
112
|
/**
|
|
113
|
-
* Best-effort "has this session ended" check for `--follow`'s exit condition.
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* neither
|
|
113
|
+
* Best-effort "has this session ended" check for `--follow`'s exit condition. MNE-8234 wired the
|
|
114
|
+
* seal lifecycle through on the wire (`remote.seal.state`), so that is read first and is
|
|
115
|
+
* authoritative: `"expired"` there means the prior seal lapsed and ends the watch. The older
|
|
116
|
+
* top-level `state` and `remote.sealed.state` fallbacks predate that field actually landing on
|
|
117
|
+
* the response — neither has ever been observed non-null on any real server — and are kept
|
|
118
|
+
* defensively in case an even older/non-conformant server ever carries one of them; this never
|
|
119
|
+
* throws, returning null when nothing recognized is present.
|
|
118
120
|
*/
|
|
119
121
|
export declare function sessionEndedState(remote: AgentSessionResponse): "expired" | "ended" | null;
|
|
120
122
|
/** Default and floor for `--follow`'s poll interval. */
|
|
@@ -152,6 +154,14 @@ export interface ShowJson {
|
|
|
152
154
|
guardrails: AgentSessionResponse["guardrails"];
|
|
153
155
|
/** R10 (MNE-8200): oldest first (the gateway's own wire order), unlike the human view. */
|
|
154
156
|
verdict_trail: AgentSessionResponse["verdict_trail"];
|
|
157
|
+
/**
|
|
158
|
+
* MNE-8234: the seal's lifecycle (state incl. `expired`, version, mode, hashes). Absent on
|
|
159
|
+
* an older server; `null` when the API reports no seal.
|
|
160
|
+
*/
|
|
161
|
+
seal: AgentSessionResponse["seal"];
|
|
162
|
+
/** MNE-8234 (MNE-8166 R4): judging counts under the current seal. */
|
|
163
|
+
unjudged_turns: AgentSessionResponse["unjudged_turns"];
|
|
164
|
+
memoised_turns: AgentSessionResponse["memoised_turns"];
|
|
155
165
|
} | {
|
|
156
166
|
source: "gateway";
|
|
157
167
|
available: false;
|
|
@@ -190,14 +190,21 @@ export function verdictTrailKey(e) {
|
|
|
190
190
|
return `${e.request_id ?? ""}#${e.turn}`;
|
|
191
191
|
}
|
|
192
192
|
/**
|
|
193
|
-
* Best-effort "has this session ended" check for `--follow`'s exit condition.
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* neither
|
|
193
|
+
* Best-effort "has this session ended" check for `--follow`'s exit condition. MNE-8234 wired the
|
|
194
|
+
* seal lifecycle through on the wire (`remote.seal.state`), so that is read first and is
|
|
195
|
+
* authoritative: `"expired"` there means the prior seal lapsed and ends the watch. The older
|
|
196
|
+
* top-level `state` and `remote.sealed.state` fallbacks predate that field actually landing on
|
|
197
|
+
* the response — neither has ever been observed non-null on any real server — and are kept
|
|
198
|
+
* defensively in case an even older/non-conformant server ever carries one of them; this never
|
|
199
|
+
* throws, returning null when nothing recognized is present.
|
|
198
200
|
*/
|
|
199
201
|
export function sessionEndedState(remote) {
|
|
200
202
|
const as = (v) => v === "expired" || v === "ended" ? v : null;
|
|
203
|
+
if (remote.seal) {
|
|
204
|
+
const sealState = as(remote.seal.state);
|
|
205
|
+
if (sealState)
|
|
206
|
+
return sealState;
|
|
207
|
+
}
|
|
201
208
|
const top = as(remote.state);
|
|
202
209
|
if (top)
|
|
203
210
|
return top;
|
|
@@ -349,6 +356,12 @@ export function buildShowView(local, remote, reason) {
|
|
|
349
356
|
else {
|
|
350
357
|
L.push(fmt.dim(" no sealed contract on the gateway for this conversation"));
|
|
351
358
|
}
|
|
359
|
+
// MNE-8234: the seal's lifecycle — independent of `sealed` above (which carries the contract
|
|
360
|
+
// body, not whether the seal itself is still live or has lapsed). Absent on an older server.
|
|
361
|
+
if (remote.seal) {
|
|
362
|
+
const modeBit = remote.seal.state === "live" ? ` · mode ${remote.seal.mode}` : "";
|
|
363
|
+
L.push(fmt.label("seal: ", `${remote.seal.state.toUpperCase()} (v${remote.seal.version})${modeBit}`));
|
|
364
|
+
}
|
|
352
365
|
if (remote.verdict) {
|
|
353
366
|
const v = remote.verdict;
|
|
354
367
|
L.push(fmt.label("verdict: ", `${v.verdict.toUpperCase()} at turn ${v.turn} — ${v.reason}`));
|
|
@@ -385,6 +398,14 @@ export function buildShowView(local, remote, reason) {
|
|
|
385
398
|
if (remote.turn !== null) {
|
|
386
399
|
L.push(fmt.label("turn: ", String(remote.turn)));
|
|
387
400
|
}
|
|
401
|
+
// MNE-8234 (MNE-8166 R4): judging counts under the current seal. `null`/absent ⇒ no judge
|
|
402
|
+
// ledger counts (older server, or nothing judged under this seal yet) — no line then.
|
|
403
|
+
if (remote.unjudged_turns !== null && remote.unjudged_turns !== undefined) {
|
|
404
|
+
const memoisedBit = remote.memoised_turns !== null && remote.memoised_turns !== undefined
|
|
405
|
+
? ` · memoised ${remote.memoised_turns}`
|
|
406
|
+
: "";
|
|
407
|
+
L.push(fmt.label("judging: ", `unjudged ${remote.unjudged_turns}${memoisedBit}`));
|
|
408
|
+
}
|
|
388
409
|
return L;
|
|
389
410
|
}
|
|
390
411
|
export function buildShowJson(local, remote, reason) {
|
|
@@ -400,6 +421,9 @@ export function buildShowJson(local, remote, reason) {
|
|
|
400
421
|
turn: remote.turn,
|
|
401
422
|
guardrails: remote.guardrails,
|
|
402
423
|
verdict_trail: remote.verdict_trail,
|
|
424
|
+
seal: remote.seal,
|
|
425
|
+
unjudged_turns: remote.unjudged_turns,
|
|
426
|
+
memoised_turns: remote.memoised_turns,
|
|
403
427
|
}
|
|
404
428
|
: { source: "gateway", available: false, reason: reason ?? "unknown" },
|
|
405
429
|
};
|
|
@@ -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 (the plugin lands dark); `--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
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* an argv value.
|
|
27
27
|
*/
|
|
28
28
|
import { spawn, spawnSync } from "node:child_process";
|
|
29
|
-
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
|
|
29
|
+
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
|
|
30
30
|
import { randomUUID } from "node:crypto";
|
|
31
31
|
import { tmpdir, homedir, hostname } from "node:os";
|
|
32
32
|
import { basename, join, dirname } from "node:path";
|
|
@@ -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 — 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
|
|
@@ -1248,7 +1303,8 @@ function launchRemoteControl(ctx) {
|
|
|
1248
1303
|
}
|
|
1249
1304
|
}
|
|
1250
1305
|
const proxyScript = resolveProxyScript();
|
|
1251
|
-
const rcDir = mkdtempSync(join(
|
|
1306
|
+
const rcDir = mkdtempSync(join(rcStateRoot(), "mnemom-code-rc."));
|
|
1307
|
+
writeFileSync(join(rcDir, "owner.pid"), `${process.pid}\n`, { mode: 0o600 }); // rc-state.mjs sweep
|
|
1252
1308
|
const proxyLog = process.env.MNEMOM_AGENT_RC_PROXY_LOG || join(rcDir, "proxy.log");
|
|
1253
1309
|
const proxy = spawn("node", [proxyScript], {
|
|
1254
1310
|
env: {
|
|
@@ -1397,6 +1453,10 @@ function spawnRcCli(ctx) {
|
|
|
1397
1453
|
if (ctx.handoffMcpConfigPath && ctx.anthropicKey) {
|
|
1398
1454
|
childEnv[HANDOFF_RC_KEY_ENV_NAME] = ctx.anthropicKey;
|
|
1399
1455
|
}
|
|
1456
|
+
// The plugin's non-secret launch facts (MNEMOM_CC_*), also after the strip loop: not an
|
|
1457
|
+
// ANTHROPIC_* var, so the RC gate ignores them, and nothing here is a key or a header value.
|
|
1458
|
+
if (ctx.plugin)
|
|
1459
|
+
applyPluginEnv(childEnv, ctx.plugin.launch);
|
|
1400
1460
|
let args;
|
|
1401
1461
|
if (ctx.shape === "server") {
|
|
1402
1462
|
// Headless dispatcher: no --model/--effort/--trim-mcp/yolo flags; the model
|
|
@@ -1408,6 +1468,10 @@ function spawnRcCli(ctx) {
|
|
|
1408
1468
|
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
1469
|
}
|
|
1410
1470
|
args = ["remote-control", "--name", rcName, ...passthrough];
|
|
1471
|
+
// No `--plugin-dir` on the headless server: the env path list loads the plugin into every
|
|
1472
|
+
// session it spawns instead (a user's own list is kept).
|
|
1473
|
+
if (ctx.plugin)
|
|
1474
|
+
appendPluginDirsEnv(childEnv, ctx.plugin.dir);
|
|
1411
1475
|
// Additive, never `--strict-mcp-config`: --trim-mcp is not applied under --server (every
|
|
1412
1476
|
// dispatched session keeps its full MCP set, unrestricted — see warnServerDrops below), so
|
|
1413
1477
|
// the handoff tool rides in on TOP of that set rather than replacing it.
|
|
@@ -1424,6 +1488,7 @@ function spawnRcCli(ctx) {
|
|
|
1424
1488
|
allowCliFlags: true,
|
|
1425
1489
|
ccSessionId: ctx.ccSessionId,
|
|
1426
1490
|
handoffMcpConfigPath: ctx.handoffMcpConfigPath,
|
|
1491
|
+
pluginDir: ctx.plugin?.dir,
|
|
1427
1492
|
});
|
|
1428
1493
|
args = ["--remote-control", rcName, ...flagArgs];
|
|
1429
1494
|
if (ctx.options.slack)
|
|
@@ -1455,6 +1520,23 @@ function warnServerDrops(options, yolo) {
|
|
|
1455
1520
|
say("mnemom agent: yolo (--dangerously-skip-permissions) is not applied under --server; set permissions from the driven sessions.");
|
|
1456
1521
|
}
|
|
1457
1522
|
}
|
|
1523
|
+
/**
|
|
1524
|
+
* ~/.mnemom/rc, created 0700 — the same root src/rc-state.mjs gives the proxy's CA dir (tsc emits
|
|
1525
|
+
* no .mjs, so this mirrors its rcStateRoot() rather than importing it). Never the OS temp dir,
|
|
1526
|
+
* whose cleaners delete files behind a long session; that is only the fallback when home is not
|
|
1527
|
+
* writable. Read at call time, not from MNEMOM_DIR: HOME can move after import (tests do).
|
|
1528
|
+
*/
|
|
1529
|
+
function rcStateRoot() {
|
|
1530
|
+
const root = join(homedir(), ".mnemom", "rc");
|
|
1531
|
+
try {
|
|
1532
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
1533
|
+
chmodSync(root, 0o700);
|
|
1534
|
+
return root;
|
|
1535
|
+
}
|
|
1536
|
+
catch {
|
|
1537
|
+
return tmpdir();
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1458
1540
|
function resolveProxyScript() {
|
|
1459
1541
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
1460
1542
|
const cand = join(here, "..", "rc-proxy.mjs");
|
|
@@ -1540,6 +1622,8 @@ export function buildCliArgs(options, ctx) {
|
|
|
1540
1622
|
args.push("--dangerously-skip-permissions");
|
|
1541
1623
|
if (ctx.ccSessionId)
|
|
1542
1624
|
args.push("--session-id", ctx.ccSessionId);
|
|
1625
|
+
if (ctx.pluginDir)
|
|
1626
|
+
args.push("--plugin-dir", ctx.pluginDir);
|
|
1543
1627
|
}
|
|
1544
1628
|
if (options.passthrough?.length)
|
|
1545
1629
|
args.push(...options.passthrough);
|
|
@@ -1650,6 +1734,7 @@ export function goalAlignmentMode(envelope, goalMode) {
|
|
|
1650
1734
|
}
|
|
1651
1735
|
export function logLaunch(ctx) {
|
|
1652
1736
|
say(`mnemom agent: routing ${ctx.cli.name} through ${ctx.door} [${ctx.cellNote}] as '${ctx.agent}' (conversation ${ctx.conversationId}).`);
|
|
1737
|
+
say(`mnemom agent: plugin: ${ctx.plugin}`);
|
|
1653
1738
|
if (ctx.envelope) {
|
|
1654
1739
|
say(`mnemom agent: contract statement: ${ctx.envelope.contract.statement}`);
|
|
1655
1740
|
say(`mnemom agent: contract sha256[0:16]: ${ctx.envelope.sha}`);
|
|
@@ -1673,6 +1758,7 @@ export function printPlan(ctx) {
|
|
|
1673
1758
|
L.push("mnemom agent — launch plan (dry run)");
|
|
1674
1759
|
L.push(` scenario: ${ctx.scenario}`);
|
|
1675
1760
|
L.push(` cli: ${ctx.cli.name} (${ctx.cli.bin})`);
|
|
1761
|
+
L.push(` plugin: ${ctx.plugin}`);
|
|
1676
1762
|
L.push(` shape: ${ctx.shape}`);
|
|
1677
1763
|
L.push(` door: ${ctx.door} [${ctx.cellNote}]`);
|
|
1678
1764
|
L.push(` agent: ${ctx.agent}`);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* integration). Surface for engineers to check whether staging is safe
|
|
4
4
|
* to promote to production based on the most recent harness run state.
|
|
5
5
|
*
|
|
6
|
-
* mnemom validate safe-house [--against=
|
|
6
|
+
* mnemom validate safe-house [--against=test] [--max-age=24h]
|
|
7
7
|
* [--lane=full|fast] [--strict] [--json]
|
|
8
8
|
*
|
|
9
9
|
* Wraps `GET /v1/admin/safe-house/harness-state` (mnemom-staff only).
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* integration). Surface for engineers to check whether staging is safe
|
|
4
4
|
* to promote to production based on the most recent harness run state.
|
|
5
5
|
*
|
|
6
|
-
* mnemom validate safe-house [--against=
|
|
6
|
+
* mnemom validate safe-house [--against=test] [--max-age=24h]
|
|
7
7
|
* [--lane=full|fast] [--strict] [--json]
|
|
8
8
|
*
|
|
9
9
|
* Wraps `GET /v1/admin/safe-house/harness-state` (mnemom-staff only).
|
|
@@ -76,10 +76,12 @@ export async function validateSafeHouseCommand(opts) {
|
|
|
76
76
|
await requireAuth();
|
|
77
77
|
// `--against` is informational only today (validation is always against
|
|
78
78
|
// the api root the user is authenticated to). Keep the flag for forward-
|
|
79
|
-
// compat: production gains a separate harness corpus per T1.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
// compat: production gains a separate harness corpus per T1. The non-prod
|
|
80
|
+
// value is `test` (the Azure test cell): `staging` named the retired ring
|
|
81
|
+
// (MNE-7968) and is rejected so a stale script fails loud, not quietly.
|
|
82
|
+
const against = opts.against ?? "test";
|
|
83
|
+
if (against !== "test" && against !== "production") {
|
|
84
|
+
console.log(fmt.error(`--against must be 'test' or 'production' (got '${against}')`) + "\n");
|
|
83
85
|
process.exit(1);
|
|
84
86
|
return;
|
|
85
87
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// MUST stay the first import: it exits cleanly on MNEMOM_ENV=staging before any
|
|
3
|
+
// module below resolves the (retired) staging URLs at import time (MNE-7968).
|
|
4
|
+
import "./lib/retired-env-guard.js";
|
|
2
5
|
import { program, InvalidArgumentError, Option } from "commander";
|
|
3
6
|
import { isEntrypoint } from "./lib/entrypoint.js";
|
|
4
7
|
import { CLI_VERSION } from "./version.js";
|
|
@@ -141,7 +144,7 @@ function printAgentHelp(verb) {
|
|
|
141
144
|
const agentCmd = program
|
|
142
145
|
.command("agent [session] [passthrough...]")
|
|
143
146
|
.description("Launch your governed agent (Claude Code in v1) through the Mnemom gateway — governed identity + optional sealed goal contract. Plain `mnemom agent` asks for a session name. " +
|
|
144
|
-
"Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml);
|
|
147
|
+
"Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `--invite <token>` redeems a Mnemom Agent invitation (the one-liner your invitation email shows; `invite <token>` is its alias); `list` / `claim` / `move` manage the agents in your orgs; `sessions` lists your past launches and `show [ref]` merges one with its live goal state; `help [verb]` prints help and never launches.")
|
|
145
148
|
.option("--goal <statement>", "Contract goal statement (required if any contract flag is given)")
|
|
146
149
|
.option("--requirement <text>", "Contract requirement (repeatable)", collectRepeatable, [])
|
|
147
150
|
.option("--allow <glob>", "Allowed path/glob (repeatable)", collectRepeatable, [])
|
|
@@ -182,6 +185,11 @@ const agentCmd = program
|
|
|
182
185
|
.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
186
|
.option("--no-yolo", "Do NOT pass --dangerously-skip-permissions (yolo is on by default)")
|
|
184
187
|
.option("--no-context-hint", "Do NOT seed Claude Code's context-hint feature flag before launch")
|
|
188
|
+
// The Mnemom plugin lands dark: off by default and not in the published package, so its two
|
|
189
|
+
// flags stay out of --help until the shipping PR turns it on (lib/claude-plugin.ts). They still
|
|
190
|
+
// parse, for a source checkout.
|
|
191
|
+
.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())
|
|
192
|
+
.addOption(new Option("--no-plugin", "Do NOT load the Mnemom plugin even when MNEMOM_AGENT_PLUGIN=1").hideHelp())
|
|
185
193
|
.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
194
|
.option("--context", "Force context folding ON for this launch even when launcher.toml (context = false) or MNEMOM_AGENT_CONTEXT turns it off")
|
|
187
195
|
.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 +374,7 @@ const agentCmd = program
|
|
|
366
374
|
trimMcp: opts.trimMcp,
|
|
367
375
|
yolo: opts.yolo,
|
|
368
376
|
contextHint: opts.contextHint,
|
|
377
|
+
plugin: opts.plugin,
|
|
369
378
|
slack: opts.slack ?? cfg.slackListen,
|
|
370
379
|
context: opts.context,
|
|
371
380
|
contextProfile: opts.contextProfile,
|
|
@@ -1650,7 +1659,7 @@ const validateCmd = program
|
|
|
1650
1659
|
validateCmd
|
|
1651
1660
|
.command("safe-house")
|
|
1652
1661
|
.description("Render Safe House harness state per lane (full/fast) — promotion-readiness signal")
|
|
1653
|
-
.option("--against <env>", "Target environment ('
|
|
1662
|
+
.option("--against <env>", "Target environment ('test' or 'production'; informational)", "test")
|
|
1654
1663
|
.option("--max-age <duration>", "Reject runs older than this (e.g. 30m, 2h, 24h, 1d)", "24h")
|
|
1655
1664
|
.option("--lane <lane>", "Show only one lane ('full', 'fast', or 'both')", "both")
|
|
1656
1665
|
.option("--strict", "Exit non-zero if any selected lane is stale or red")
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -1305,6 +1305,29 @@ export interface AgentSessionVerdictTrailEntry {
|
|
|
1305
1305
|
fence_breach: boolean | null;
|
|
1306
1306
|
at: string;
|
|
1307
1307
|
}
|
|
1308
|
+
/**
|
|
1309
|
+
* The seal's lifecycle (MNE-8149 R2, MNE-8199 D9/R9, MNE-8234/mnemom-api#3541). `live`: a sealed
|
|
1310
|
+
* contract currently governs — `mode` says whether a crossed guardrail only nudges or enforces
|
|
1311
|
+
* (refuses the next request). `expired`: the prior seal lapsed (24h idle) and the next engaged
|
|
1312
|
+
* request will seal afresh; no `mode` because nothing is being enforced.
|
|
1313
|
+
*/
|
|
1314
|
+
export interface AgentSessionSealLive {
|
|
1315
|
+
state: "live";
|
|
1316
|
+
version: number;
|
|
1317
|
+
hash: string;
|
|
1318
|
+
sealed_at: string;
|
|
1319
|
+
reason: "first" | "after_expiry" | null;
|
|
1320
|
+
expired_hash: string | null;
|
|
1321
|
+
updated_from_hash: string | null;
|
|
1322
|
+
mode: "nudge" | "enforce";
|
|
1323
|
+
}
|
|
1324
|
+
export interface AgentSessionSealExpired {
|
|
1325
|
+
state: "expired";
|
|
1326
|
+
version: number;
|
|
1327
|
+
hash: string;
|
|
1328
|
+
sealed_at: string;
|
|
1329
|
+
}
|
|
1330
|
+
export type AgentSessionSeal = AgentSessionSealLive | AgentSessionSealExpired;
|
|
1308
1331
|
/** `GET /v1/agent/sessions/{conversationId}` 200 body. Nulls mean "nothing stored" — absence is data. */
|
|
1309
1332
|
export interface AgentSessionResponse {
|
|
1310
1333
|
conversation_id: string;
|
|
@@ -1319,6 +1342,24 @@ export interface AgentSessionResponse {
|
|
|
1319
1342
|
guardrails: AgentSessionGuardrails | null;
|
|
1320
1343
|
/** Absent on an older server (back-compat) — never shown then, never an empty-array assumption. */
|
|
1321
1344
|
verdict_trail?: AgentSessionVerdictTrailEntry[];
|
|
1345
|
+
/**
|
|
1346
|
+
* MNE-8234: the seal's lifecycle, incl. `expired`. Absent on an older server (back-compat —
|
|
1347
|
+
* never shown then). `null` means the API itself reports no seal: either none was ever made for
|
|
1348
|
+
* this conversation, or the gateway sent a seal payload out of contract and the API dropped it
|
|
1349
|
+
* rather than relay it half-trusted — the two are indistinguishable from this field alone.
|
|
1350
|
+
*/
|
|
1351
|
+
seal?: AgentSessionSeal | null;
|
|
1352
|
+
/**
|
|
1353
|
+
* MNE-8166 R4 / MNE-8234: assistant turns under the current seal with no verdict (a cut-off/
|
|
1354
|
+
* aborted response, a judge that failed or never ran, a non-streamed final turn). Absent on an
|
|
1355
|
+
* older server (back-compat). `null` when no judge ledger counts under the current seal.
|
|
1356
|
+
*/
|
|
1357
|
+
unjudged_turns?: number | null;
|
|
1358
|
+
/**
|
|
1359
|
+
* MNE-8166 R4 / MNE-8234: turns the judge memo graded (aligned by inheritance, no verdict
|
|
1360
|
+
* stored). Absent on an older server (back-compat). `null` alongside `unjudged_turns`.
|
|
1361
|
+
*/
|
|
1362
|
+
memoised_turns?: number | null;
|
|
1322
1363
|
}
|
|
1323
1364
|
/**
|
|
1324
1365
|
* The live goal state of one `mnemom agent` session, read through mnemom-api
|
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;
|