@mnemom/mnemom 0.17.2-next.3 → 0.17.2-next.4
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 +20 -0
- package/dist/commands/agent-config.js +3 -0
- package/dist/commands/agent-goal.d.ts +79 -0
- package/dist/commands/agent-goal.js +276 -0
- package/dist/commands/agent-sessions.d.ts +13 -3
- package/dist/commands/agent-sessions.js +46 -4
- package/dist/commands/agent.d.ts +41 -2
- package/dist/commands/agent.js +190 -8
- package/dist/index.js +67 -0
- package/dist/lib/agent-ledger.d.ts +8 -0
- package/dist/lib/agent.d.ts +107 -4
- package/dist/lib/agent.js +87 -6
- package/dist/lib/goal-api.d.ts +133 -0
- package/dist/lib/goal-api.js +113 -0
- package/dist/lib/launcher-config.d.ts +6 -0
- package/dist/lib/launcher-config.js +15 -0
- package/dist/mcp/handoff-server.mjs +331 -0
- package/dist/rc-proxy.mjs +113 -37
- package/package.json +1 -1
package/dist/commands/agent.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { spawn, spawnSync } from "node:child_process";
|
|
29
29
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, renameSync, } from "node:fs";
|
|
30
|
+
import { randomUUID } from "node:crypto";
|
|
30
31
|
import { tmpdir, homedir } from "node:os";
|
|
31
32
|
import { basename, join, dirname } from "node:path";
|
|
32
33
|
import { fileURLToPath } from "node:url";
|
|
@@ -38,7 +39,7 @@ import { fmt } from "../lib/format.js";
|
|
|
38
39
|
import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
39
40
|
import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
|
|
40
41
|
import { defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
|
|
41
|
-
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildSessionConfigB64, resolveContextFold, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, resolveRemoteControlShape, sanitizeAgentName, } from "../lib/agent.js";
|
|
42
|
+
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfigB64, resolveContextFold, resolveContextHandoff, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, resolveRemoteControlShape, sanitizeAgentName, } from "../lib/agent.js";
|
|
42
43
|
/** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
|
|
43
44
|
const CODE_STORE = join(MNEMOM_DIR, "launcher.json");
|
|
44
45
|
const say = (line = "") => console.error(line);
|
|
@@ -99,7 +100,8 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
99
100
|
// launcher.toml → MNEMOM_AGENT_CONTEXT) turns it off for this session via the
|
|
100
101
|
// per-session config header. `undefined` sessionConfigB64 ⇒ nothing sent.
|
|
101
102
|
const contextFold = resolveContextFold({ context: options.context, env });
|
|
102
|
-
const
|
|
103
|
+
const contextHandoff = resolveContextHandoff({ contextHandoff: options.contextHandoff, env });
|
|
104
|
+
const sessionConfigB64 = buildSessionConfigB64({ fold: contextFold, contextHandoff });
|
|
103
105
|
// ── which CLI executable (name or full path) ── resolve GRACEFULLY: if it's not
|
|
104
106
|
// found we don't crash with a technical error — we ask for the path at launch.
|
|
105
107
|
let cli;
|
|
@@ -145,6 +147,9 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
145
147
|
if (!contextFold) {
|
|
146
148
|
say("mnemom agent: context folding OFF for this session (--no-context or context = false).");
|
|
147
149
|
}
|
|
150
|
+
if (contextHandoff) {
|
|
151
|
+
say("mnemom agent: context handoff ON (experimental) — the `handoff` MCP tool would be registered.");
|
|
152
|
+
}
|
|
148
153
|
return;
|
|
149
154
|
}
|
|
150
155
|
// Real launch: make sure we actually have a CLI to run. If we couldn't find it,
|
|
@@ -237,6 +242,10 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
237
242
|
// launch so `mnemom agent sessions` / `show` can find this conversation later.
|
|
238
243
|
// Written after provisioning so a freshly claimed agent's id resolves. Holds
|
|
239
244
|
// NO secret (never the key, the headers, or the child env); best-effort.
|
|
245
|
+
// The Claude Code session id this launch pins (terminal + interactive RC). The server shape
|
|
246
|
+
// hosts many sessions in one process, so it records none and injects nothing.
|
|
247
|
+
const cc = shape === "server" ? { id: null, inject: false } : pickCcSessionId(options.passthrough ?? []);
|
|
248
|
+
const ccSessionId = cc.inject && cc.id ? cc.id : undefined;
|
|
240
249
|
await recordSessionLaunch({
|
|
241
250
|
session: scenario,
|
|
242
251
|
conversationId,
|
|
@@ -248,12 +257,49 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
248
257
|
cli,
|
|
249
258
|
envelope,
|
|
250
259
|
goalMode,
|
|
260
|
+
ccSessionId: cc.id,
|
|
251
261
|
});
|
|
252
262
|
logLaunch({ shape, door, agent, conversationId, cellNote, envelope, goalMode, cli });
|
|
253
263
|
say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
|
|
254
264
|
if (!contextFold) {
|
|
255
265
|
say("mnemom agent: context folding OFF for this session (--no-context or context = false).");
|
|
256
266
|
}
|
|
267
|
+
// ── handoff MCP tool (MNE-7564) ── OFF by default: absent/false ⇒ handoffMcpConfigPath stays
|
|
268
|
+
// undefined and buildCliArgs/spawnRcCli are byte-identical to a build without this flag. ON:
|
|
269
|
+
// the config is written to a 0600 temp file and referenced by PATH only — never inlined into
|
|
270
|
+
// argv, where `ps` could see it, and — for every shape — it carries no key material at all
|
|
271
|
+
// (see `HandoffAnthropicKeySource`). A failure here degrades to "no handoff tool" with a
|
|
272
|
+
// warning; it never blocks the launch.
|
|
273
|
+
let handoffMcpConfigPath;
|
|
274
|
+
if (contextHandoff) {
|
|
275
|
+
try {
|
|
276
|
+
// Terminal shape: the real key never needs to touch this file at all — it already rides
|
|
277
|
+
// in the launched process's OWN `ANTHROPIC_CUSTOM_HEADERS` env var (buildCustomHeaders,
|
|
278
|
+
// below), which a spawned stdio MCP server inherits in full, so the server can read it
|
|
279
|
+
// back out at request time. Remote Control (both shapes) strips every ANTHROPIC_* auth
|
|
280
|
+
// var from the child's env, so `spawnRcCli` below instead sets a dedicated, non-ANTHROPIC
|
|
281
|
+
// var (HANDOFF_RC_KEY_ENV_NAME) on that same child env — still nothing in this file.
|
|
282
|
+
const anthropicKeySource = shape === "terminal"
|
|
283
|
+
? { envName: "ANTHROPIC_CUSTOM_HEADERS" }
|
|
284
|
+
: { envName: HANDOFF_RC_KEY_ENV_NAME };
|
|
285
|
+
handoffMcpConfigPath = writeHandoffMcpConfigFile(buildHandoffMcpConfigJson({
|
|
286
|
+
// The SAME origin the session's own model traffic goes to — not resolveGatewayHost,
|
|
287
|
+
// which would ignore a MNEMOM_AGENT_ANTHROPIC_GATEWAY override (a test-cell launch
|
|
288
|
+
// must never send its handoff calls to prod, or vice versa).
|
|
289
|
+
gatewayHost: resolveHandoffGatewayHost(door),
|
|
290
|
+
anthropicKeySource,
|
|
291
|
+
agent,
|
|
292
|
+
conversationId,
|
|
293
|
+
sessionConfigB64,
|
|
294
|
+
serverScript: resolveHandoffServerScript(),
|
|
295
|
+
nodeBin: process.execPath,
|
|
296
|
+
}));
|
|
297
|
+
say("mnemom agent: context handoff ON (experimental) — the `handoff` tool is registered; call it at a natural stopping point or on the gateway's warning line.");
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
say(fmt.warn(`mnemom agent: context handoff could not be registered (${err instanceof Error ? err.message : String(err)}) — launching without the handoff tool.`));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
257
303
|
if (shape === "terminal") {
|
|
258
304
|
launchTerminal({
|
|
259
305
|
cli,
|
|
@@ -267,6 +313,8 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
267
313
|
options,
|
|
268
314
|
yolo,
|
|
269
315
|
contextHint,
|
|
316
|
+
ccSessionId,
|
|
317
|
+
handoffMcpConfigPath,
|
|
270
318
|
});
|
|
271
319
|
return; // launchTerminal exits the process
|
|
272
320
|
}
|
|
@@ -281,8 +329,10 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
281
329
|
envelope,
|
|
282
330
|
goalMode,
|
|
283
331
|
sessionConfigB64,
|
|
332
|
+
handoffMcpConfigPath,
|
|
284
333
|
options,
|
|
285
334
|
yolo,
|
|
335
|
+
ccSessionId,
|
|
286
336
|
});
|
|
287
337
|
}
|
|
288
338
|
// ───────────────────────────────────────────────────────────────────────────
|
|
@@ -315,6 +365,7 @@ export async function recordSessionLaunch(ctx) {
|
|
|
315
365
|
cli: ctx.cli.name,
|
|
316
366
|
contract: toLedgerContract(ctx.envelope?.contract ?? null),
|
|
317
367
|
goal_mode: goalAlignmentMode(ctx.envelope, ctx.goalMode),
|
|
368
|
+
cc_session_id: ctx.ccSessionId ?? null,
|
|
318
369
|
}, { dir: MNEMOM_DIR, now: () => new Date(), pid: process.pid });
|
|
319
370
|
if (!result.ok) {
|
|
320
371
|
say(fmt.warn(`mnemom agent: could not record this session in ~/.mnemom/${LEDGER_FILE} (${result.error}) — launching anyway.`));
|
|
@@ -567,7 +618,12 @@ function launchTerminal(ctx) {
|
|
|
567
618
|
goalMode: ctx.goalMode ?? undefined,
|
|
568
619
|
sessionConfigB64: ctx.sessionConfigB64,
|
|
569
620
|
});
|
|
570
|
-
const args = buildCliArgs(ctx.options, {
|
|
621
|
+
const args = buildCliArgs(ctx.options, {
|
|
622
|
+
yolo: ctx.yolo,
|
|
623
|
+
allowCliFlags: true,
|
|
624
|
+
ccSessionId: ctx.ccSessionId,
|
|
625
|
+
handoffMcpConfigPath: ctx.handoffMcpConfigPath,
|
|
626
|
+
});
|
|
571
627
|
// Claude Code logged in via claude.ai (OAuth) PREEMPTS ANTHROPIC_API_KEY, so
|
|
572
628
|
// it is force-unset; the real key rides in ANTHROPIC_CUSTOM_HEADERS and
|
|
573
629
|
// ANTHROPIC_AUTH_TOKEN is a fixed non-secret placeholder.
|
|
@@ -643,6 +699,16 @@ function launchRemoteControl(ctx) {
|
|
|
643
699
|
process.on("exit", onExit);
|
|
644
700
|
// Secrets on stdin — three lines (anthropic, mnemom, openai). Only the
|
|
645
701
|
// Anthropic key is used on this Anthropic-only door; the other two are empty.
|
|
702
|
+
// A proxy that exits before reading them turns this write into an EPIPE; that is
|
|
703
|
+
// not a failure of its own — the exited-before-READY check below refuses the
|
|
704
|
+
// launch. Any other stdin error fails closed like a spawn error.
|
|
705
|
+
proxy.stdin?.on("error", (err) => {
|
|
706
|
+
if (err.code === "EPIPE")
|
|
707
|
+
return;
|
|
708
|
+
say(`mnemom agent: could not hand the secrets to the Remote Control proxy: ${err.message}`);
|
|
709
|
+
onExit();
|
|
710
|
+
process.exit(1);
|
|
711
|
+
});
|
|
646
712
|
proxy.stdin?.end(`${ctx.anthropicKey}\n\n\n`);
|
|
647
713
|
let stdoutBuf = "";
|
|
648
714
|
const start = Date.now();
|
|
@@ -720,6 +786,15 @@ function spawnRcCli(ctx) {
|
|
|
720
786
|
childEnv.HTTPS_PROXY = `http://127.0.0.1:${ctx.port}`;
|
|
721
787
|
childEnv.https_proxy = `http://127.0.0.1:${ctx.port}`;
|
|
722
788
|
childEnv.NODE_EXTRA_CA_CERTS = ctx.ca;
|
|
789
|
+
// Context handoff (MNE-7564), RC shapes only: the strip list above deliberately removes every
|
|
790
|
+
// ANTHROPIC_* var, so the handoff MCP server has nothing to inherit for the key unless we give
|
|
791
|
+
// it something — a dedicated, non-ANTHROPIC-named var, added AFTER the strip loop so it can
|
|
792
|
+
// never collide with anything on that list. The RC gate above only inspects the recognized
|
|
793
|
+
// ANTHROPIC_* base-url/key/socket family (same reason HTTPS_PROXY/NODE_EXTRA_CA_CERTS already
|
|
794
|
+
// ride through here ungated), so this rides through untouched. Only set when the tool is
|
|
795
|
+
// actually registered — never on a plain RC launch.
|
|
796
|
+
if (ctx.handoffMcpConfigPath)
|
|
797
|
+
childEnv[HANDOFF_RC_KEY_ENV_NAME] = ctx.anthropicKey;
|
|
723
798
|
let args;
|
|
724
799
|
if (ctx.shape === "server") {
|
|
725
800
|
// Headless dispatcher: no --model/--effort/--trim-mcp/yolo flags; the model
|
|
@@ -728,13 +803,23 @@ function spawnRcCli(ctx) {
|
|
|
728
803
|
if (ctx.options.model)
|
|
729
804
|
childEnv.ANTHROPIC_MODEL = ctx.options.model;
|
|
730
805
|
args = ["remote-control", "--name", rcName, ...passthrough];
|
|
806
|
+
// Additive, never `--strict-mcp-config`: --trim-mcp is not applied under --server (every
|
|
807
|
+
// dispatched session keeps its full MCP set, unrestricted — see warnServerDrops below), so
|
|
808
|
+
// the handoff tool rides in on TOP of that set rather than replacing it.
|
|
809
|
+
if (ctx.handoffMcpConfigPath)
|
|
810
|
+
args.push("--mcp-config", ctx.handoffMcpConfigPath);
|
|
731
811
|
say(`mnemom agent: remote-control: headless server (session name '${rcName}'); drive the sessions it spawns from claude.ai/code or the Claude app. Stop this command to stop the server.`);
|
|
732
812
|
}
|
|
733
813
|
else {
|
|
734
814
|
// Interactive RC in THIS terminal: the CLI's own `--remote-control <name>`
|
|
735
815
|
// startup flag; --model/--effort/--trim-mcp/yolo apply as in a normal launch.
|
|
736
816
|
// buildCliArgs already includes the (name-stripped) passthrough.
|
|
737
|
-
const flagArgs = buildCliArgs(scopedOptions, {
|
|
817
|
+
const flagArgs = buildCliArgs(scopedOptions, {
|
|
818
|
+
yolo: ctx.yolo,
|
|
819
|
+
allowCliFlags: true,
|
|
820
|
+
ccSessionId: ctx.ccSessionId,
|
|
821
|
+
handoffMcpConfigPath: ctx.handoffMcpConfigPath,
|
|
822
|
+
});
|
|
738
823
|
args = ["--remote-control", rcName, ...flagArgs];
|
|
739
824
|
say(`mnemom agent: remote-control: interactive session in THIS terminal with Remote Control on (session name '${rcName}'); it also appears under Remote Control at claude.ai/code and in the Claude app. Exit the CLI to stop everything.`);
|
|
740
825
|
}
|
|
@@ -771,6 +856,46 @@ function resolveProxyScript() {
|
|
|
771
856
|
}
|
|
772
857
|
return cand;
|
|
773
858
|
}
|
|
859
|
+
/**
|
|
860
|
+
* Locates the `handoff` MCP server (src/mcp/handoff-server.mjs in dev, dist/mcp/handoff-server.mjs
|
|
861
|
+
* once built — a raw, uncompiled `.mjs` file that `copy-assets.mjs` copies verbatim, exactly like
|
|
862
|
+
* `resolveProxyScript`'s `rc-proxy.mjs`, so the SAME relative path resolves in both trees).
|
|
863
|
+
*
|
|
864
|
+
* `baseDirOverride` exists ONLY so a test can exercise the missing-file throw deterministically
|
|
865
|
+
* (point it at a directory with no `../mcp/handoff-server.mjs` beside it) without touching the
|
|
866
|
+
* real installed tree; every real caller omits it and gets `import.meta.url`'s own directory.
|
|
867
|
+
*/
|
|
868
|
+
export function resolveHandoffServerScript(baseDirOverride) {
|
|
869
|
+
const here = baseDirOverride ?? dirname(fileURLToPath(import.meta.url));
|
|
870
|
+
const cand = join(here, "..", "mcp", "handoff-server.mjs");
|
|
871
|
+
if (!existsSync(cand)) {
|
|
872
|
+
throw new Error(`the handoff MCP server is missing (expected ${cand}). Reinstall the CLI.`);
|
|
873
|
+
}
|
|
874
|
+
return cand;
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Writes the `--mcp-config` JSON (MNE-7564) to a fresh 0600 file in its own temp dir, referenced
|
|
878
|
+
* from argv only by PATH — never inlined, where `ps` could see it. For every launch shape this
|
|
879
|
+
* file carries NO key material at all — just the NAME of an env var the handoff server already
|
|
880
|
+
* inherits from its own parent (`HandoffAnthropicKeySource`): `ANTHROPIC_CUSTOM_HEADERS` for a
|
|
881
|
+
* terminal launch, or the dedicated `HANDOFF_RC_KEY_ENV_NAME` `spawnRcCli` sets for Remote
|
|
882
|
+
* Control. Still written 0600, since even the var name is worth protecting from a stray read.
|
|
883
|
+
* The dir is removed on process exit, same pattern as the Remote Control proxy's `rcDir` above.
|
|
884
|
+
*/
|
|
885
|
+
function writeHandoffMcpConfigFile(json) {
|
|
886
|
+
const dir = mkdtempSync(join(tmpdir(), "mnemom-code-handoff."));
|
|
887
|
+
const path = join(dir, "mcp-config.json");
|
|
888
|
+
writeFileSync(path, json, { mode: 0o600 });
|
|
889
|
+
process.on("exit", () => {
|
|
890
|
+
try {
|
|
891
|
+
rmSync(dir, { recursive: true, force: true });
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
/* best effort */
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
return path;
|
|
898
|
+
}
|
|
774
899
|
function redactedProxyLog(logPath) {
|
|
775
900
|
try {
|
|
776
901
|
const text = readFileSync(logPath, "utf8").replace(/sk-ant-[A-Za-z0-9_-]+/g, "sk-ant-REDACTED");
|
|
@@ -787,9 +912,15 @@ function redactedProxyLog(logPath) {
|
|
|
787
912
|
/**
|
|
788
913
|
* Build the coding-agent CLI args from the model/effort/trim-mcp/yolo knobs plus
|
|
789
914
|
* the caller's passthrough. `--model`/`--effort` are injected only when given (no
|
|
790
|
-
* silent default). yolo appends `--dangerously-skip-permissions`.
|
|
915
|
+
* silent default). yolo appends `--dangerously-skip-permissions`. A `ccSessionId`
|
|
916
|
+
* (pickCcSessionId, inject arm) appends `--session-id <id>`. `--trim-mcp`
|
|
791
917
|
* appends `--strict-mcp-config --mcp-config {}` LAST so it wins over any
|
|
792
|
-
* caller-supplied --mcp-config
|
|
918
|
+
* caller-supplied --mcp-config — UNLESS `handoffMcpConfigPath` is set (context
|
|
919
|
+
* handoff ON, MNE-7564), in which case that file (which already declares only the
|
|
920
|
+
* `mnemom-handoff` server) is what `--strict-mcp-config` points at, so trimming
|
|
921
|
+
* strips every OTHER server but the handoff tool survives. Without `--trim-mcp`,
|
|
922
|
+
* the same path is passed WITHOUT `--strict-mcp-config` so it only ADDS the tool
|
|
923
|
+
* on top of the caller's/CLI's own MCP config rather than replacing it.
|
|
793
924
|
*/
|
|
794
925
|
export function buildCliArgs(options, ctx) {
|
|
795
926
|
const args = [];
|
|
@@ -800,14 +931,65 @@ export function buildCliArgs(options, ctx) {
|
|
|
800
931
|
args.push("--effort", options.effort);
|
|
801
932
|
if (ctx.yolo)
|
|
802
933
|
args.push("--dangerously-skip-permissions");
|
|
934
|
+
if (ctx.ccSessionId)
|
|
935
|
+
args.push("--session-id", ctx.ccSessionId);
|
|
803
936
|
}
|
|
804
937
|
if (options.passthrough?.length)
|
|
805
938
|
args.push(...options.passthrough);
|
|
806
|
-
if (ctx.allowCliFlags
|
|
807
|
-
|
|
939
|
+
if (ctx.allowCliFlags) {
|
|
940
|
+
if (ctx.handoffMcpConfigPath) {
|
|
941
|
+
if (options.trimMcp)
|
|
942
|
+
args.push("--strict-mcp-config", "--mcp-config", ctx.handoffMcpConfigPath);
|
|
943
|
+
else
|
|
944
|
+
args.push("--mcp-config", ctx.handoffMcpConfigPath);
|
|
945
|
+
}
|
|
946
|
+
else if (options.trimMcp) {
|
|
947
|
+
args.push("--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}');
|
|
948
|
+
}
|
|
808
949
|
}
|
|
809
950
|
return args;
|
|
810
951
|
}
|
|
952
|
+
/**
|
|
953
|
+
* The Claude Code session id a launch runs under, from the caller's passthrough (implicit-goal
|
|
954
|
+
* CLI, cli-telemetry §1.5):
|
|
955
|
+
* - `--session-id <uuid>` (or `=<uuid>`) → that id; the caller's own flag, nothing injected;
|
|
956
|
+
* - `--fork-session` → null (Claude Code mints the fork's id);
|
|
957
|
+
* - `--resume <id>` / `-r <id>` (or `--resume=<id>`) → that id, nothing injected;
|
|
958
|
+
* - a bare `--resume` / `-r`, `--continue` / `-c` → null (Claude Code picks the session);
|
|
959
|
+
* - otherwise → a fresh UUID the launcher injects as `--session-id`.
|
|
960
|
+
*/
|
|
961
|
+
export function pickCcSessionId(passthrough, newId = randomUUID) {
|
|
962
|
+
const valueOf = (flags) => {
|
|
963
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
964
|
+
const a = passthrough[i];
|
|
965
|
+
for (const f of flags) {
|
|
966
|
+
if (a === f) {
|
|
967
|
+
const next = passthrough[i + 1];
|
|
968
|
+
return next !== undefined && !next.startsWith("-") && next.trim() !== ""
|
|
969
|
+
? next.trim()
|
|
970
|
+
: null;
|
|
971
|
+
}
|
|
972
|
+
if (a.startsWith(`${f}=`))
|
|
973
|
+
return a.slice(f.length + 1).trim() || null;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return undefined; // flag absent
|
|
977
|
+
};
|
|
978
|
+
const explicit = valueOf(["--session-id"]);
|
|
979
|
+
if (explicit)
|
|
980
|
+
return { id: explicit, inject: false };
|
|
981
|
+
if (explicit === null)
|
|
982
|
+
return { id: null, inject: false }; // `--session-id` with no value: CC errors, add nothing
|
|
983
|
+
if (passthrough.includes("--fork-session"))
|
|
984
|
+
return { id: null, inject: false };
|
|
985
|
+
const resumed = valueOf(["--resume", "-r"]);
|
|
986
|
+
if (resumed)
|
|
987
|
+
return { id: resumed, inject: false };
|
|
988
|
+
if (resumed === null || passthrough.includes("--continue") || passthrough.includes("-c")) {
|
|
989
|
+
return { id: null, inject: false };
|
|
990
|
+
}
|
|
991
|
+
return { id: newId(), inject: true };
|
|
992
|
+
}
|
|
811
993
|
// ───────────────────────────────────────────────────────────────────────────
|
|
812
994
|
// Context-hint seed (opt-out) — best-effort, never throws
|
|
813
995
|
// ───────────────────────────────────────────────────────────────────────────
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { policyInitCommand, policyValidateCommand, policyPublishCommand, policyL
|
|
|
11
11
|
import { protectionShowCommand, protectionPublishCommand, protectionValidateCommand, protectionEditCommand, protectionDriftCommand, } from "./commands/protection.js";
|
|
12
12
|
import { agentsListCommand, agentsClaimCommand, agentsMoveCommand } from "./commands/agents.js";
|
|
13
13
|
import { agentSessionsCommand, agentShowCommand, liveSessionDeps, } from "./commands/agent-sessions.js";
|
|
14
|
+
import { agentGoalPinCommand, agentGoalResetCommand, agentGoalShowCommand, liveGoalDeps, } from "./commands/agent-goal.js";
|
|
14
15
|
import { orgListCommand, orgShowCommand, orgUseCommand } from "./commands/org.js";
|
|
15
16
|
import { teamListCommand, teamShowCommand, teamTemplateCommand, teamPreviewComposeCommand, teamAdminGrantCommand, teamAdminRevokeCommand, teamAdminListCommand, teamCoverageCommand, } from "./commands/team.js";
|
|
16
17
|
import { advisoriesListCommand, advisoriesShowCommand } from "./commands/advisories.js";
|
|
@@ -136,6 +137,8 @@ const agentCmd = program
|
|
|
136
137
|
.option("--no-context-hint", "Do NOT seed Claude Code's context-hint feature flag before launch")
|
|
137
138
|
.option("--context", "Force context folding ON for this launch even when launcher.toml (context = false) or MNEMOM_AGENT_CONTEXT turns it off")
|
|
138
139
|
.option("--no-context", "Disable context folding for this launch (folding is on by default; tells the gateway to skip it for this session)")
|
|
140
|
+
.option("--context-handoff", "EXPERIMENTAL, off by default: register the `handoff(note)` MCP tool and tell the gateway to turn on the context-handoff capability for this session")
|
|
141
|
+
.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")
|
|
139
142
|
.option("--dry-run", "Print the resolved launch plan and exit — no key read, no spawn")
|
|
140
143
|
.action(async (scenario, passthrough, opts) => {
|
|
141
144
|
try {
|
|
@@ -218,6 +221,7 @@ const agentCmd = program
|
|
|
218
221
|
yolo: opts.yolo,
|
|
219
222
|
contextHint: opts.contextHint,
|
|
220
223
|
context: opts.context,
|
|
224
|
+
contextHandoff: opts.contextHandoff,
|
|
221
225
|
setup: opts.setup,
|
|
222
226
|
keySource: cfg.keySource,
|
|
223
227
|
passthrough,
|
|
@@ -730,6 +734,69 @@ agentCmd
|
|
|
730
734
|
process.exit(1);
|
|
731
735
|
}
|
|
732
736
|
});
|
|
737
|
+
// `mnemom agent goal show|pin|reset` — the implicit goal the gateway inferred for a session
|
|
738
|
+
// (implicit-goal CLI). The handlers set process.exitCode themselves (1 / 2); the catch is for
|
|
739
|
+
// the unexpected.
|
|
740
|
+
const agentGoalCmd = agentCmd
|
|
741
|
+
.command("goal")
|
|
742
|
+
.description("Read and steer the goal the gateway inferred for an implicit-mode session (show / pin / reset). " +
|
|
743
|
+
"Authenticates with the same key and agent the session launched with");
|
|
744
|
+
const collectRepeat = (value, prev = []) => [...prev, value];
|
|
745
|
+
agentGoalCmd
|
|
746
|
+
.command("show [ref]")
|
|
747
|
+
.description("Show the session's live inferred goal: revision, pin, statement, requirements, fences and the latest verdict. " +
|
|
748
|
+
"`ref` is a session name (newest match wins) or a unique conversation-id prefix; omitted = the newest session")
|
|
749
|
+
.option("--thread <id>", "Goal thread: main (default) or a thread id such as a.<agent-id>")
|
|
750
|
+
.option("--all-threads", "List every goal thread of the conversation (main + subagents)")
|
|
751
|
+
.option("--history", "Add the revision history table")
|
|
752
|
+
.option("--json", "Output the gateway body (source gateway) plus the ledger row (source ledger)")
|
|
753
|
+
.action(async (ref, opts) => {
|
|
754
|
+
try {
|
|
755
|
+
await agentGoalShowCommand(ref, opts, liveGoalDeps);
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
759
|
+
process.exit(1);
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
agentGoalCmd
|
|
763
|
+
.command("pin [ref]")
|
|
764
|
+
.description("Pin the current inferred goal so new messages no longer revise it (--off unpins)")
|
|
765
|
+
.option("--thread <id>", "Goal thread: main (default) or a thread id such as a.<agent-id>")
|
|
766
|
+
.option("--off", "Unpin: new directions revise the goal again")
|
|
767
|
+
.option("--json", "Output the gateway body as JSON")
|
|
768
|
+
.action(async (ref, opts) => {
|
|
769
|
+
try {
|
|
770
|
+
await agentGoalPinCommand(ref, opts, liveGoalDeps);
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
agentGoalCmd
|
|
778
|
+
.command("reset [ref]")
|
|
779
|
+
.description("Clear the inferred goal (the next message re-infers it), or replace it with --statement. " +
|
|
780
|
+
"Revisions keep counting up; spend and turn ceilings are never refunded")
|
|
781
|
+
.option("--thread <id>", "Goal thread: main (default) or a thread id such as a.<agent-id>")
|
|
782
|
+
.option("--statement <s>", "Replace the goal with this statement instead of clearing it")
|
|
783
|
+
.option("--requirement <r>", "A requirement for --statement (repeatable)", collectRepeat)
|
|
784
|
+
.option("--yes", "Skip the confirmation (required without a TTY)")
|
|
785
|
+
.option("--json", "Output the gateway body as JSON")
|
|
786
|
+
.action(async (ref, opts, cmd) => {
|
|
787
|
+
try {
|
|
788
|
+
// `mnemom agent` itself declares `--requirement` (a contract flag), and commander hands a
|
|
789
|
+
// parent's option to the parent wherever it appears, so the values land on `agent`.
|
|
790
|
+
// Read them from there when this command saw none.
|
|
791
|
+
const inherited = cmd.optsWithGlobals().requirement;
|
|
792
|
+
const requirement = opts.requirement ?? inherited;
|
|
793
|
+
await agentGoalResetCommand(ref, { ...opts, requirement }, liveGoalDeps);
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
797
|
+
process.exit(1);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
733
800
|
// ============================================================================
|
|
734
801
|
// Organization management (ADR-044, Piece 1 of T1-3.1)
|
|
735
802
|
// ============================================================================
|
|
@@ -55,6 +55,14 @@ export interface SessionRecord {
|
|
|
55
55
|
* captured).
|
|
56
56
|
*/
|
|
57
57
|
goal_mode?: GoalMode;
|
|
58
|
+
/**
|
|
59
|
+
* The Claude Code session id the launch pinned with `--session-id` (or the one the caller
|
|
60
|
+
* passed via `--session-id` / `--resume <id>`), so the ledger joins conversation_id ↔ the
|
|
61
|
+
* `x-claude-code-session-id` the gateway logs. `null` when Claude Code picks it (`--continue`,
|
|
62
|
+
* a bare `--resume`, `--fork-session`) or the shape hosts many sessions (`--server`). Optional:
|
|
63
|
+
* records written before this field existed omit it (still `v: 1`).
|
|
64
|
+
*/
|
|
65
|
+
cc_session_id?: string | null;
|
|
58
66
|
}
|
|
59
67
|
/** Goal-alignment mode as recorded at launch. */
|
|
60
68
|
export type GoalMode = "off" | "on" | "implicit";
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -76,6 +76,16 @@ export declare function resolveGatewayHost(env?: NodeJS.ProcessEnv): string;
|
|
|
76
76
|
export declare function resolveAnthropicDoor(env?: NodeJS.ProcessEnv): string;
|
|
77
77
|
/** True when the resolved gateway host is the us-2/prod cell (used for the launch banner). */
|
|
78
78
|
export declare function isProdGateway(env?: NodeJS.ProcessEnv): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* The gateway origin the `handoff` MCP tool's own `POST /mnemom/context/handoff` must hit —
|
|
81
|
+
* always the SAME cell the launched session's own `/v1/messages` traffic goes to. Derived from
|
|
82
|
+
* the already-resolved Anthropic `door` (scheme+host only, via `new URL(...).origin`) rather
|
|
83
|
+
* than `resolveGatewayHost` directly, because `MNEMOM_AGENT_ANTHROPIC_GATEWAY` can override the
|
|
84
|
+
* door to a DIFFERENT host than `MNEMOM_AGENT_GATEWAY`/the default (e.g. a test cell) — reading
|
|
85
|
+
* `resolveGatewayHost` here would silently send handoff calls to prod (or vice versa) while the
|
|
86
|
+
* session's model traffic went elsewhere (MNE-7564 review).
|
|
87
|
+
*/
|
|
88
|
+
export declare function resolveHandoffGatewayHost(door: string): string;
|
|
79
89
|
/**
|
|
80
90
|
* Sanitise a header value into a safe wire token: lowercase, alphanumerics and
|
|
81
91
|
* single hyphens only, no leading/trailing hyphen. Returns "" when nothing
|
|
@@ -135,6 +145,15 @@ export declare const GOAL_MODE_HEADER = "x-mnemom-goal-mode";
|
|
|
135
145
|
export declare const SESSION_CONFIG_HEADER = "x-mnemom-session-config";
|
|
136
146
|
/** The gateway env key that gates context folding (allow-listed for per-session overlay). */
|
|
137
147
|
export declare const CONTEXT_RUNTIME_KEY = "CONTEXT_RUNTIME_ENABLED";
|
|
148
|
+
/**
|
|
149
|
+
* The gateway env key that gates the whole context-handoff capability (level lines +
|
|
150
|
+
* `/mnemom/context/handoff` route + the digest note section) for a single session
|
|
151
|
+
* (MNE-7561..7565). Off by default on the gateway; turned on ONLY when this launch's
|
|
152
|
+
* `contextHandoff` setting is true (see `resolveContextHandoff`), which also registers the
|
|
153
|
+
* `handoff` MCP tool (MNE-7564) — a session never gets the tool without the header, or the
|
|
154
|
+
* header without the tool.
|
|
155
|
+
*/
|
|
156
|
+
export declare const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
|
|
138
157
|
/**
|
|
139
158
|
* The `x-mnemom-goal-mode` header value for `--implicit`, or null when the flag
|
|
140
159
|
* is absent: `implicit[; max_turns=N][; budget_usd=X][; stall_turns=N]` — the
|
|
@@ -238,11 +257,95 @@ export declare function resolveContextFold(opts: {
|
|
|
238
257
|
env?: NodeJS.ProcessEnv;
|
|
239
258
|
}): boolean;
|
|
240
259
|
/**
|
|
241
|
-
* The `x-mnemom-session-config` value
|
|
242
|
-
*
|
|
243
|
-
*
|
|
260
|
+
* The `x-mnemom-session-config` value for this launch, or `undefined` when there is
|
|
261
|
+
* nothing to override (the gateway's own defaults stand — the common case). Two
|
|
262
|
+
* independent overlays, both OFF by default (folding is ON by default so only its OFF
|
|
263
|
+
* state is ever sent; handoff is OFF by default so only its ON state is ever sent):
|
|
264
|
+
* - `fold: false` → `CONTEXT_RUNTIME_ENABLED: "false"` (`--no-context`).
|
|
265
|
+
* - `contextHandoff: true` → `CONTEXT_HANDOFF_ENABLED: "true"` (the `context_handoff`
|
|
266
|
+
* setting) — the SAME header the `handoff` MCP tool's own call to
|
|
267
|
+
* `/mnemom/context/handoff` must also send, since the gateway resolves the flag
|
|
268
|
+
* per request (see `commands/agent.ts` handoff MCP wiring).
|
|
269
|
+
* base64url so it survives the gateway's inbound strip.
|
|
270
|
+
*/
|
|
271
|
+
export declare function buildSessionConfigB64(opts: {
|
|
272
|
+
fold: boolean;
|
|
273
|
+
contextHandoff: boolean;
|
|
274
|
+
}): string | undefined;
|
|
275
|
+
/**
|
|
276
|
+
* Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
|
|
277
|
+
* this launch? OFF by default (operator directive, curation 2026-09-25): an absent flag
|
|
278
|
+
* and an absent `context_handoff` setting are both "off", matching `resolveContextFold`'s
|
|
279
|
+
* shape but the opposite default. Precedence: an explicit `--context-handoff`/
|
|
280
|
+
* `--no-context-handoff` flag wins, else `MNEMOM_AGENT_CONTEXT_HANDOFF` (which
|
|
281
|
+
* `applyConfigToEnv` sets from the `context_handoff` config key), else OFF.
|
|
282
|
+
*/
|
|
283
|
+
export declare function resolveContextHandoff(opts: {
|
|
284
|
+
contextHandoff?: boolean;
|
|
285
|
+
env?: NodeJS.ProcessEnv;
|
|
286
|
+
}): boolean;
|
|
287
|
+
/** The name the `handoff` MCP tool server is registered under in `--mcp-config`. */
|
|
288
|
+
export declare const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
|
|
289
|
+
/**
|
|
290
|
+
* The name of the non-`ANTHROPIC_*` env var `spawnRcCli` (commands/agent.ts) adds to Claude
|
|
291
|
+
* Code's Remote Control child env, carrying the real Anthropic key, ONLY when context handoff is
|
|
292
|
+
* on. `spawnRcCli` strips every `ANTHROPIC_*` auth var (including `ANTHROPIC_CUSTOM_HEADERS`) so
|
|
293
|
+
* the RC gate sees a clean first-party-OAuth setup, but that gate only inspects the recognized
|
|
294
|
+
* `ANTHROPIC_*` base-url/key/socket family — a custom-named var rides through ungated, exactly
|
|
295
|
+
* like `HTTPS_PROXY`/`NODE_EXTRA_CA_CERTS` already do for the proxy. The handoff server inherits
|
|
296
|
+
* it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
|
|
244
297
|
*/
|
|
245
|
-
export declare
|
|
298
|
+
export declare const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
|
|
299
|
+
/**
|
|
300
|
+
* How the `handoff` MCP server (cli/src/mcp/handoff-server.mjs) recovers the Anthropic key
|
|
301
|
+
* WITHOUT it ever being written into the `--mcp-config` file on disk: `envName` is the NAME of a
|
|
302
|
+
* var already present in Claude Code's OWN process env, which every stdio MCP server inherits in
|
|
303
|
+
* full (verified experimentally for MNE-7564: a probe server registered via `--mcp-config`
|
|
304
|
+
* received vars set only in the parent `claude` process's env, unfiltered).
|
|
305
|
+
* - Terminal shape: `{ envName: "ANTHROPIC_CUSTOM_HEADERS" }` — that var carries the real key
|
|
306
|
+
* as its `x-api-key:` line (`buildCustomHeaders` above), so the server parses that line out
|
|
307
|
+
* of its own inherited env at request time.
|
|
308
|
+
* - Remote Control (both `--remote-control` and `--server`): `{ envName:
|
|
309
|
+
* HANDOFF_RC_KEY_ENV_NAME }` — `spawnRcCli` sets that var directly (see above) since RC's
|
|
310
|
+
* child env carries the key nowhere else (it rides the local proxy's stdin instead, deleted
|
|
311
|
+
* from Claude Code's own env by the strip list).
|
|
312
|
+
* There is deliberately no embedded-value ("rawKey") variant: every launch shape has an
|
|
313
|
+
* inherited-env channel, so the config file never needs to carry the key itself.
|
|
314
|
+
*/
|
|
315
|
+
export interface HandoffAnthropicKeySource {
|
|
316
|
+
envName: string;
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* The env vars the launcher hands the `handoff` MCP server so it can call
|
|
320
|
+
* `POST /mnemom/context/handoff` with the SAME identity + session config the launched
|
|
321
|
+
* coding-agent session's own `/v1/messages` requests carry. No secret rides in this object or
|
|
322
|
+
* the config file it produces, but the file is still written to a 0600 path and passed to
|
|
323
|
+
* `--mcp-config` as a PATH, never inlined into argv (see `commands/agent.ts`
|
|
324
|
+
* `writeHandoffMcpConfigFile`), since even an env-var NAME is worth protecting from a stray read.
|
|
325
|
+
*/
|
|
326
|
+
export interface HandoffMcpParams {
|
|
327
|
+
/** The gateway ORIGIN (e.g. https://gateway.mnemom.ai) — see `resolveHandoffGatewayHost`. */
|
|
328
|
+
gatewayHost: string;
|
|
329
|
+
anthropicKeySource: HandoffAnthropicKeySource;
|
|
330
|
+
agent: string;
|
|
331
|
+
conversationId: string;
|
|
332
|
+
/** `buildSessionConfigB64` output — forwarded so the route resolves the SAME per-session flag. */
|
|
333
|
+
sessionConfigB64?: string;
|
|
334
|
+
/** Absolute path to the compiled server entry point (`resolveHandoffServerScript`). */
|
|
335
|
+
serverScript: string;
|
|
336
|
+
/** The node executable to run it with (the launcher's own `process.execPath`). */
|
|
337
|
+
nodeBin: string;
|
|
338
|
+
}
|
|
339
|
+
/** The `--mcp-config` document registering exactly the `handoff` tool. Pure — no I/O. */
|
|
340
|
+
export declare function buildHandoffMcpConfig(p: HandoffMcpParams): {
|
|
341
|
+
mcpServers: Record<string, {
|
|
342
|
+
command: string;
|
|
343
|
+
args: string[];
|
|
344
|
+
env: Record<string, string>;
|
|
345
|
+
}>;
|
|
346
|
+
};
|
|
347
|
+
/** `JSON.stringify(buildHandoffMcpConfig(p))` — the exact bytes written to the config file. */
|
|
348
|
+
export declare function buildHandoffMcpConfigJson(p: HandoffMcpParams): string;
|
|
246
349
|
/**
|
|
247
350
|
* The persistable goal-alignment default (`launcher.toml` `goal_mode`). Only the
|
|
248
351
|
* two statement-free modes can be a standing default: a SEALED contract needs a
|