@mnemom/mnemom 0.17.4-next.3 → 0.17.4-next.5
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/base-url-proxy.mjs +154 -0
- package/dist/commands/agent-first-run.d.ts +35 -0
- package/dist/commands/agent-first-run.js +78 -0
- package/dist/commands/agent.d.ts +24 -0
- package/dist/commands/agent.js +130 -14
- package/dist/index.js +10 -0
- package/dist/lib/agent.d.ts +112 -0
- package/dist/lib/agent.js +184 -0
- package/dist/lib/first-run/backtest.d.ts +92 -0
- package/dist/lib/first-run/backtest.js +196 -0
- package/dist/lib/first-run/governed.d.ts +65 -0
- package/dist/lib/first-run/governed.js +253 -0
- package/dist/lib/first-run/index.d.ts +76 -0
- package/dist/lib/first-run/index.js +243 -0
- package/dist/lib/first-run/lines.d.ts +16 -0
- package/dist/lib/first-run/lines.js +80 -0
- package/dist/lib/first-run/scope.d.ts +108 -0
- package/dist/lib/first-run/scope.js +308 -0
- package/dist/lib/first-run/screen.d.ts +93 -0
- package/dist/lib/first-run/screen.js +210 -0
- package/dist/lib/first-run/seat.d.ts +75 -0
- package/dist/lib/first-run/seat.js +250 -0
- package/dist/rc-proxy.mjs +16 -1
- package/dist/turn-origin.mjs +330 -0
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// mnemom-agent-base-url-proxy — the local proxy behind a TERMINAL `mnemom agent --implicit` launch.
|
|
3
|
+
//
|
|
4
|
+
// Claude Code's ANTHROPIC_BASE_URL points here (http://127.0.0.1:<port>); every request is
|
|
5
|
+
// forwarded unchanged to the Mnemom gateway door, with ONE addition: on a `POST /v1/messages`
|
|
6
|
+
// that Claude Code marks `x-claude-code-request-class: main`, the `x-mnemom-turn` header built
|
|
7
|
+
// from Claude Code's own transcript (turn-origin.mjs). That lets the gateway tell the user's typed
|
|
8
|
+
// words from cron ticks, skill bodies, hook text, channel relays and peer messages.
|
|
9
|
+
//
|
|
10
|
+
// AUTH IS PASSED THROUGH, NEVER HELD. An API-key launch carries its key in ANTHROPIC_CUSTOM_HEADERS
|
|
11
|
+
// (x-api-key) and a subscription launch carries Claude Code's own claude.ai bearer; both arrive as
|
|
12
|
+
// request headers and leave the same way. This proxy reads no secret, stores none, logs none.
|
|
13
|
+
//
|
|
14
|
+
// FAIL-OPEN. The turn header is best effort: no prompt id, no transcript, a late transcript entry
|
|
15
|
+
// (> WAIT_MS) → the request is forwarded without it (the gateway then behaves as before).
|
|
16
|
+
//
|
|
17
|
+
// CONFIG (env, none secret)
|
|
18
|
+
// MNEMOM_AGENT_BU_DOOR gateway door URL (required, https only), e.g. https://gateway.mnemom.ai/anthropic
|
|
19
|
+
// MNEMOM_AGENT_BU_CWD the session cwd (Claude Code's transcript lives under its slug)
|
|
20
|
+
// MNEMOM_AGENT_BU_LOG append log lines here instead of stderr (optional)
|
|
21
|
+
// MNEMOM_AGENT_BU_PARENT_PID exit when this pid is gone
|
|
22
|
+
// MNEMOM_AGENT_BU_PORT listen port (default 0 = ephemeral)
|
|
23
|
+
// MNEMOM_AGENT_TURN_HEADER 0/off disables the turn header (the proxy still forwards)
|
|
24
|
+
// CLAUDE_CONFIG_DIR Claude Code's home (default ~/.claude)
|
|
25
|
+
//
|
|
26
|
+
// stdout: exactly one line, `READY <port>`, once listening. Node >= 18, no deps.
|
|
27
|
+
import http from "node:http";
|
|
28
|
+
import https from "node:https";
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import { TURN_HEADER, TurnOrigins, turnHeaderEnabled, turnHeaderFor } from "./turn-origin.mjs";
|
|
31
|
+
|
|
32
|
+
const env = process.env;
|
|
33
|
+
function die(msg) {
|
|
34
|
+
process.stderr.write(`mnemom-agent-base-url-proxy: ${msg}\n`);
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let DOOR;
|
|
39
|
+
try {
|
|
40
|
+
DOOR = new URL(env.MNEMOM_AGENT_BU_DOOR || "");
|
|
41
|
+
} catch {
|
|
42
|
+
die("MNEMOM_AGENT_BU_DOOR is required and must be a URL");
|
|
43
|
+
}
|
|
44
|
+
if (DOOR.protocol !== "https:") die("MNEMOM_AGENT_BU_DOOR must be https");
|
|
45
|
+
const LOGFILE = env.MNEMOM_AGENT_BU_LOG || "";
|
|
46
|
+
const PARENT = Number(env.MNEMOM_AGENT_BU_PARENT_PID || 0);
|
|
47
|
+
const PORT = Number(env.MNEMOM_AGENT_BU_PORT || 0);
|
|
48
|
+
const TURNS = turnHeaderEnabled(env)
|
|
49
|
+
? new TurnOrigins({ cwd: env.MNEMOM_AGENT_BU_CWD || process.cwd() })
|
|
50
|
+
: null;
|
|
51
|
+
|
|
52
|
+
const log = (line) => {
|
|
53
|
+
const s = `${new Date().toISOString()} ${String(line).replace(/[\r\n]+/g, " ")}\n`;
|
|
54
|
+
if (LOGFILE) {
|
|
55
|
+
try {
|
|
56
|
+
fs.appendFileSync(LOGFILE, s);
|
|
57
|
+
return;
|
|
58
|
+
} catch {
|
|
59
|
+
/* fall through */
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
process.stderr.write(s);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const HOP = new Set([
|
|
66
|
+
"connection",
|
|
67
|
+
"proxy-connection",
|
|
68
|
+
"keep-alive",
|
|
69
|
+
"upgrade",
|
|
70
|
+
"host",
|
|
71
|
+
"te",
|
|
72
|
+
"trailer",
|
|
73
|
+
"transfer-encoding",
|
|
74
|
+
]);
|
|
75
|
+
const agent = new https.Agent({ keepAlive: true, maxSockets: 32 });
|
|
76
|
+
const doorPath = DOOR.pathname.replace(/\/$/, "");
|
|
77
|
+
const isMessages = (u) => u === "/v1/messages" || u.startsWith("/v1/messages?");
|
|
78
|
+
|
|
79
|
+
function handle(req, res) {
|
|
80
|
+
const started = Date.now();
|
|
81
|
+
const headers = {};
|
|
82
|
+
for (const [k, v] of Object.entries(req.headers)) if (!HOP.has(k)) headers[k] = v;
|
|
83
|
+
// A client-supplied x-mnemom-turn never passes: only this proxy states turn origins.
|
|
84
|
+
delete headers["x-mnemom-turn"];
|
|
85
|
+
|
|
86
|
+
// Origin-form only: the path is appended to the door's origin, so nothing in it can name a host.
|
|
87
|
+
if (!req.url.startsWith("/")) {
|
|
88
|
+
log(`${req.method} (not origin-form) -> local 400`);
|
|
89
|
+
res.writeHead(400, { "content-type": "text/plain" });
|
|
90
|
+
res.end("mnemom local proxy: origin-form request path required\n");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const tag = `${req.method} ${req.url.split("?")[0]}`;
|
|
94
|
+
|
|
95
|
+
const forward = (note) => {
|
|
96
|
+
const target = new URL(DOOR.origin + doorPath + req.url);
|
|
97
|
+
const up = https.request(target, { method: req.method, headers, agent }, (ur) => {
|
|
98
|
+
res.writeHead(ur.statusCode, ur.headers);
|
|
99
|
+
ur.pipe(res);
|
|
100
|
+
ur.on("end", () => log(`${tag} -> ${ur.statusCode} ${Date.now() - started}ms${note}`));
|
|
101
|
+
// The door dropped the connection mid-response (headers already sent): cut the client off too,
|
|
102
|
+
// so Claude Code sees a broken stream and retries. Unhandled, this error would kill the proxy.
|
|
103
|
+
ur.on("error", (e) => {
|
|
104
|
+
log(`${tag} -> ${ur.statusCode} ABORTED ${e.message}`);
|
|
105
|
+
res.destroy();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
up.on("error", (e) => {
|
|
109
|
+
log(`${tag} -> ERROR ${e.message}`);
|
|
110
|
+
res.writeHead(502, { "content-type": "application/json" });
|
|
111
|
+
res.end(
|
|
112
|
+
JSON.stringify({
|
|
113
|
+
type: "error",
|
|
114
|
+
error: { type: "api_error", message: `mnemom local proxy: ${e.message}` },
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
// The client went away mid-response: stop the upstream call too.
|
|
119
|
+
res.on("close", () => {
|
|
120
|
+
if (!res.writableFinished) up.destroy();
|
|
121
|
+
});
|
|
122
|
+
req.pipe(up);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (TURNS && req.method === "POST" && isMessages(req.url)) {
|
|
126
|
+
// Never rejects (turnHeaderFor): a transcript problem only means no header.
|
|
127
|
+
turnHeaderFor(TURNS, headers).then((t) => {
|
|
128
|
+
if (t.value) headers[TURN_HEADER] = t.value;
|
|
129
|
+
forward(` turn=${t.note}`);
|
|
130
|
+
});
|
|
131
|
+
} else {
|
|
132
|
+
forward("");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const server = http.createServer(handle);
|
|
137
|
+
server.listen(PORT, "127.0.0.1", () => {
|
|
138
|
+
const { port } = server.address();
|
|
139
|
+
log(`listening 127.0.0.1:${port} door=${DOOR.href} turn_header=${TURNS ? "on" : "off"}`);
|
|
140
|
+
process.stdout.write(`READY ${port}\n`);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// SIGINT is ignored: the proxy shares the terminal's process group with Claude Code, and a Ctrl-C
|
|
144
|
+
// meant for Claude Code must not take the session's route down. It exits with the launcher.
|
|
145
|
+
process.on("SIGINT", () => {});
|
|
146
|
+
for (const sig of ["SIGTERM", "SIGHUP"]) process.on(sig, () => process.exit(0));
|
|
147
|
+
if (PARENT)
|
|
148
|
+
setInterval(() => {
|
|
149
|
+
try {
|
|
150
|
+
process.kill(PARENT, 0);
|
|
151
|
+
} catch {
|
|
152
|
+
process.exit(0);
|
|
153
|
+
}
|
|
154
|
+
}, 2000).unref();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wiring for the `mnemom agent` first-run look (lib/first-run): the opt-in offer on the
|
|
3
|
+
* first interactive launch, and `mnemom agent --first-run` to show it again.
|
|
4
|
+
*
|
|
5
|
+
* The look is local and read-only: no network call, transcripts never leave the machine.
|
|
6
|
+
* It never blocks a launch — a failure prints one note and the launch goes on.
|
|
7
|
+
*/
|
|
8
|
+
import { computeFirstRun, type FirstRunDeps } from "../lib/first-run/index.js";
|
|
9
|
+
/** Set to 1 to never be offered the first-run look (scripts, shared machines). */
|
|
10
|
+
export declare const NO_FIRST_RUN_ENV = "MNEMOM_AGENT_NO_FIRST_RUN";
|
|
11
|
+
/**
|
|
12
|
+
* Ask one question; resolve the line, or null on EOF (Ctrl-D closes the stream without
|
|
13
|
+
* ever calling readline's question callback, which would otherwise hang the launch).
|
|
14
|
+
*/
|
|
15
|
+
export declare function askLine(question: string, input?: NodeJS.ReadableStream, output?: NodeJS.WritableStream): Promise<string | null>;
|
|
16
|
+
/** The real I/O behind defaultFirstRunDeps; tests swap pieces. */
|
|
17
|
+
export interface FirstRunIo {
|
|
18
|
+
tty: boolean;
|
|
19
|
+
writeErr: (s: string) => void;
|
|
20
|
+
compute: typeof computeFirstRun;
|
|
21
|
+
/** The prompt's streams (default: the terminal). */
|
|
22
|
+
input: NodeJS.ReadableStream;
|
|
23
|
+
output: NodeJS.WritableStream;
|
|
24
|
+
}
|
|
25
|
+
export declare function defaultFirstRunDeps(env?: NodeJS.ProcessEnv, io?: Partial<FirstRunIo>): FirstRunDeps;
|
|
26
|
+
/** Is the first-launch offer allowed at all for this launch? */
|
|
27
|
+
export declare function firstRunOfferAllowed(opts: {
|
|
28
|
+
dryRun?: boolean;
|
|
29
|
+
}, env: NodeJS.ProcessEnv): boolean;
|
|
30
|
+
/** Called before a real launch: offers once, records the answer, shows on yes. */
|
|
31
|
+
export declare function offerFirstRunOnLaunch(opts: {
|
|
32
|
+
dryRun?: boolean;
|
|
33
|
+
}, env?: NodeJS.ProcessEnv, deps?: FirstRunDeps): Promise<string>;
|
|
34
|
+
/** `mnemom agent --first-run`: show the look again, no prompt, no launch. */
|
|
35
|
+
export declare function agentFirstRunCommand(deps?: FirstRunDeps): void;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wiring for the `mnemom agent` first-run look (lib/first-run): the opt-in offer on the
|
|
3
|
+
* first interactive launch, and `mnemom agent --first-run` to show it again.
|
|
4
|
+
*
|
|
5
|
+
* The look is local and read-only: no network call, transcripts never leave the machine.
|
|
6
|
+
* It never blocks a launch — a failure prints one note and the launch goes on.
|
|
7
|
+
*/
|
|
8
|
+
import { MNEMOM_DIR } from "../lib/config.js";
|
|
9
|
+
import { resolveRetentionDays } from "../lib/agent-ledger.js";
|
|
10
|
+
import { loadCliConfig } from "../lib/cli-config.js";
|
|
11
|
+
import { launchPreflighted } from "../lib/agent.js";
|
|
12
|
+
import { createInterface } from "node:readline";
|
|
13
|
+
import { isInteractive } from "../lib/prompt.js";
|
|
14
|
+
import { computeFirstRun, maybeOfferFirstRun, showFirstRun, } from "../lib/first-run/index.js";
|
|
15
|
+
/** Set to 1 to never be offered the first-run look (scripts, shared machines). */
|
|
16
|
+
export const NO_FIRST_RUN_ENV = "MNEMOM_AGENT_NO_FIRST_RUN";
|
|
17
|
+
/**
|
|
18
|
+
* Ask one question; resolve the line, or null on EOF (Ctrl-D closes the stream without
|
|
19
|
+
* ever calling readline's question callback, which would otherwise hang the launch).
|
|
20
|
+
*/
|
|
21
|
+
export function askLine(question, input = process.stdin, output = process.stdout) {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const rl = createInterface({ input, output });
|
|
24
|
+
let done = false;
|
|
25
|
+
const finish = (v) => {
|
|
26
|
+
if (done)
|
|
27
|
+
return;
|
|
28
|
+
done = true;
|
|
29
|
+
rl.close();
|
|
30
|
+
resolve(v);
|
|
31
|
+
};
|
|
32
|
+
rl.on("close", () => finish(null));
|
|
33
|
+
rl.question(`${question} `, (ans) => finish(ans));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function defaultFirstRunDeps(env = process.env, io = {}) {
|
|
37
|
+
const tty = io.tty ?? process.stderr.isTTY === true;
|
|
38
|
+
const writeErr = io.writeErr ?? ((s) => void process.stderr.write(s));
|
|
39
|
+
const compute = io.compute ?? computeFirstRun;
|
|
40
|
+
return {
|
|
41
|
+
mnemomDir: MNEMOM_DIR,
|
|
42
|
+
interactive: isInteractive(),
|
|
43
|
+
ask: (q) => askLine(q, io.input, io.output),
|
|
44
|
+
print: (line) => process.stdout.write(`${line}\n`),
|
|
45
|
+
note: (line) => console.error(line),
|
|
46
|
+
now: () => new Date(),
|
|
47
|
+
compute: () => {
|
|
48
|
+
const res = compute({
|
|
49
|
+
retentionDays: resolveRetentionDays(loadCliConfig().sessions?.retentionDays, env),
|
|
50
|
+
progress: (m) => {
|
|
51
|
+
if (tty)
|
|
52
|
+
writeErr(`\r mnemom: ${m}`.padEnd(78).slice(0, 78));
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
if (tty)
|
|
56
|
+
writeErr(`\r${" ".repeat(78)}\r`);
|
|
57
|
+
return res;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Is the first-launch offer allowed at all for this launch? */
|
|
62
|
+
export function firstRunOfferAllowed(opts, env) {
|
|
63
|
+
if (opts.dryRun)
|
|
64
|
+
return false;
|
|
65
|
+
if (launchPreflighted(env))
|
|
66
|
+
return false; // an orchestrator's launch, not a person's
|
|
67
|
+
return env[NO_FIRST_RUN_ENV] !== "1";
|
|
68
|
+
}
|
|
69
|
+
/** Called before a real launch: offers once, records the answer, shows on yes. */
|
|
70
|
+
export async function offerFirstRunOnLaunch(opts, env = process.env, deps = defaultFirstRunDeps(env)) {
|
|
71
|
+
if (!firstRunOfferAllowed(opts, env))
|
|
72
|
+
return "skipped";
|
|
73
|
+
return maybeOfferFirstRun(deps);
|
|
74
|
+
}
|
|
75
|
+
/** `mnemom agent --first-run`: show the look again, no prompt, no launch. */
|
|
76
|
+
export function agentFirstRunCommand(deps = defaultFirstRunDeps()) {
|
|
77
|
+
showFirstRun(deps);
|
|
78
|
+
}
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -212,6 +212,30 @@ export interface RcCapability {
|
|
|
212
212
|
* and by the auto-default selection (a bare launch prefers RC when capable).
|
|
213
213
|
*/
|
|
214
214
|
export declare function checkRcCapability(cli: ResolvedCli): RcCapability;
|
|
215
|
+
/**
|
|
216
|
+
* Add 127.0.0.1 and localhost to BOTH NO_PROXY and no_proxy (proxy clients disagree on which one
|
|
217
|
+
* wins, and some read only the lowercase one), keeping whatever each already listed.
|
|
218
|
+
*/
|
|
219
|
+
export declare function exemptLoopbackFromProxy(env: NodeJS.ProcessEnv): void;
|
|
220
|
+
/** Whether a terminal implicit launch runs the turn-origin proxy (MNEMOM_AGENT_TURN_HEADER=0 opts out). */
|
|
221
|
+
export declare function turnProxyWanted(env: NodeJS.ProcessEnv): boolean;
|
|
222
|
+
/** Test seams for startBaseUrlProxy (every real caller omits them). */
|
|
223
|
+
export interface BaseUrlProxyOptions {
|
|
224
|
+
/** The proxy script (default: the bundled base-url-proxy.mjs beside rc-proxy.mjs). */
|
|
225
|
+
script?: string;
|
|
226
|
+
/** The node binary (default: this process's own). */
|
|
227
|
+
node?: string;
|
|
228
|
+
/** How long to wait for READY before launching straight to the door (default 5 s). */
|
|
229
|
+
readyTimeoutMs?: number;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Start base-url-proxy.mjs in front of `door`; calls `done` exactly once with its
|
|
233
|
+
* `http://127.0.0.1:<port>` base URL, or with null (after a one-line warning) when it does not
|
|
234
|
+
* report READY in time or exits first — the launch then goes straight to the door. A missing
|
|
235
|
+
* script is the same case (node exits 1 before READY). The proxy holds no secret (auth headers
|
|
236
|
+
* pass through it), exits with this process, and is killed on exit.
|
|
237
|
+
*/
|
|
238
|
+
export declare function startBaseUrlProxy(door: string, done: (baseUrl: string | null) => void, opts?: BaseUrlProxyOptions): void;
|
|
215
239
|
/**
|
|
216
240
|
* Locates the `handoff` MCP server (src/mcp/handoff-server.mjs in dev, dist/mcp/handoff-server.mjs
|
|
217
241
|
* once built — a raw, uncompiled `.mjs` file that `copy-assets.mjs` copies verbatim, exactly like
|
package/dist/commands/agent.js
CHANGED
|
@@ -43,7 +43,7 @@ import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
|
43
43
|
import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, } from "../lib/login-detect.js";
|
|
44
44
|
import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
|
|
45
45
|
import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
|
|
46
|
-
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, GATEWAY_HINT_HEADERS_ENV, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
|
|
46
|
+
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, GATEWAY_HINT_HEADERS_ENV, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
|
|
47
47
|
import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
|
|
48
48
|
import { slackChannelArgs } from "../lib/slack-launch.js";
|
|
49
49
|
/** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
|
|
@@ -345,6 +345,25 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
345
345
|
// hosts many sessions in one process, so it records none and injects nothing.
|
|
346
346
|
const cc = shape === "server" ? { id: null, inject: false } : pickCcSessionId(options.passthrough ?? []);
|
|
347
347
|
const ccSessionId = cc.inject && cc.id ? cc.id : undefined;
|
|
348
|
+
// ── 1M context window by default ── the model this launch runs on gets `[1m]` when it has no
|
|
349
|
+
// window suffix and supports 1M (resolveWindowModel); an explicit choice — the
|
|
350
|
+
// CLAUDE_CODE_DISABLE_1M_CONTEXT opt-out, a passthrough `-- --model`, a suffixed id — is kept.
|
|
351
|
+
// Claude Code only: it is the adapter that reads the window off the model id.
|
|
352
|
+
const win = cli.name === "claude"
|
|
353
|
+
? resolveWindowModel({
|
|
354
|
+
explicit: options.model,
|
|
355
|
+
passthrough: options.passthrough ?? [],
|
|
356
|
+
env: process.env,
|
|
357
|
+
settings: () => readCcSettings({
|
|
358
|
+
cwd: process.cwd(),
|
|
359
|
+
configDir: (process.env.CLAUDE_CONFIG_DIR ?? "").trim() || join(homedir(), ".claude"),
|
|
360
|
+
}),
|
|
361
|
+
})
|
|
362
|
+
: null;
|
|
363
|
+
const launchOptions = win?.reason === "one-m" ? { ...options, model: win.model } : options;
|
|
364
|
+
if (win?.reason === "one-m") {
|
|
365
|
+
say(`mnemom agent: model ${win.from} → ${win.model} (1M context window; set ${DISABLE_1M_ENV}=1, or pass the model as \`-- --model <id>\`, to keep the standard window).`);
|
|
366
|
+
}
|
|
348
367
|
await recordSessionLaunch({
|
|
349
368
|
session: scenario,
|
|
350
369
|
conversationId,
|
|
@@ -352,7 +371,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
352
371
|
account,
|
|
353
372
|
gatewayHost,
|
|
354
373
|
shape,
|
|
355
|
-
options,
|
|
374
|
+
options: launchOptions,
|
|
356
375
|
cli,
|
|
357
376
|
envelope,
|
|
358
377
|
goalMode,
|
|
@@ -412,7 +431,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
412
431
|
envelope,
|
|
413
432
|
goalMode,
|
|
414
433
|
sessionConfigB64,
|
|
415
|
-
options,
|
|
434
|
+
options: launchOptions,
|
|
416
435
|
yolo,
|
|
417
436
|
contextHint,
|
|
418
437
|
ccSessionId,
|
|
@@ -433,7 +452,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
433
452
|
goalMode,
|
|
434
453
|
sessionConfigB64,
|
|
435
454
|
handoffMcpConfigPath,
|
|
436
|
-
options,
|
|
455
|
+
options: launchOptions,
|
|
437
456
|
yolo,
|
|
438
457
|
ccSessionId,
|
|
439
458
|
});
|
|
@@ -520,7 +539,13 @@ export async function recordSessionLaunch(ctx) {
|
|
|
520
539
|
org_label: ctx.account.orgLabel,
|
|
521
540
|
gateway_host: ctx.gatewayHost,
|
|
522
541
|
launch_shape: ctx.shape,
|
|
523
|
-
model
|
|
542
|
+
// What the launch runs on — `--model`, else a passthrough `-- --model`, else the inherited
|
|
543
|
+
// ANTHROPIC_MODEL — verbatim, so a `[1m]` (1M-window) launch reads as one in the ledger.
|
|
544
|
+
model: resolveLaunchModel({
|
|
545
|
+
explicit: ctx.options.model,
|
|
546
|
+
passthrough: ctx.options.passthrough ?? [],
|
|
547
|
+
env: process.env,
|
|
548
|
+
}).model,
|
|
524
549
|
effort: ctx.options.effort?.trim() || null,
|
|
525
550
|
cli: ctx.cli.name,
|
|
526
551
|
contract: toLedgerContract(ctx.envelope?.contract ?? null),
|
|
@@ -825,6 +850,9 @@ function launchTerminal(ctx) {
|
|
|
825
850
|
else {
|
|
826
851
|
childEnv = subscriptionChildEnv(process.env, ctx.door, headers);
|
|
827
852
|
}
|
|
853
|
+
// Both auth arms point ANTHROPIC_BASE_URL at the gateway door, which makes Claude Code turn
|
|
854
|
+
// tool search off and inline every tool schema on every request — unless told otherwise.
|
|
855
|
+
applyToolSearchDefault(childEnv);
|
|
828
856
|
const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
|
|
829
857
|
childEnv[SESSION_NAME_ENV] = sessionName;
|
|
830
858
|
childEnv[GATEWAY_HINT_HEADERS_ENV] = "1";
|
|
@@ -835,17 +863,101 @@ function launchTerminal(ctx) {
|
|
|
835
863
|
childEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
|
|
836
864
|
childEnv.CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF = "1";
|
|
837
865
|
}
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
866
|
+
const run = () => {
|
|
867
|
+
const child = spawn(ctx.cli.bin, args, { stdio: "inherit", env: childEnv });
|
|
868
|
+
child.on("error", (err) => {
|
|
869
|
+
say(`mnemom agent: failed to launch ${ctx.cli.name}: ${err.message}`);
|
|
870
|
+
process.exit(1);
|
|
871
|
+
});
|
|
872
|
+
child.on("exit", (code, signal) => {
|
|
873
|
+
if (signal)
|
|
874
|
+
process.kill(process.pid, signal);
|
|
875
|
+
else
|
|
876
|
+
process.exit(code ?? 0);
|
|
877
|
+
});
|
|
878
|
+
};
|
|
879
|
+
// Implicit goal mode: put the local base-URL proxy in front of the door so each main request
|
|
880
|
+
// carries x-mnemom-turn (where each recent user turn came from, read from Claude Code's own
|
|
881
|
+
// transcript — base-url-proxy.mjs / turn-origin.mjs). The prompt id it joins on comes from the
|
|
882
|
+
// gateway hint headers set above. Fail-open: no proxy → the door directly, as before.
|
|
883
|
+
if (!ctx.goalMode || !turnProxyWanted(process.env)) {
|
|
884
|
+
run();
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
startBaseUrlProxy(ctx.door, (baseUrl) => {
|
|
888
|
+
if (baseUrl) {
|
|
889
|
+
childEnv.ANTHROPIC_BASE_URL = baseUrl;
|
|
890
|
+
// An inherited HTTPS_PROXY must not carry the loopback hop.
|
|
891
|
+
exemptLoopbackFromProxy(childEnv);
|
|
892
|
+
}
|
|
893
|
+
run();
|
|
842
894
|
});
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Add 127.0.0.1 and localhost to BOTH NO_PROXY and no_proxy (proxy clients disagree on which one
|
|
898
|
+
* wins, and some read only the lowercase one), keeping whatever each already listed.
|
|
899
|
+
*/
|
|
900
|
+
export function exemptLoopbackFromProxy(env) {
|
|
901
|
+
const upper = env.NO_PROXY ?? env.no_proxy ?? "";
|
|
902
|
+
const lower = env.no_proxy ?? env.NO_PROXY ?? "";
|
|
903
|
+
env.NO_PROXY = [upper, "127.0.0.1,localhost"].filter(Boolean).join(",");
|
|
904
|
+
env.no_proxy = [lower, "127.0.0.1,localhost"].filter(Boolean).join(",");
|
|
905
|
+
}
|
|
906
|
+
/** Whether a terminal implicit launch runs the turn-origin proxy (MNEMOM_AGENT_TURN_HEADER=0 opts out). */
|
|
907
|
+
export function turnProxyWanted(env) {
|
|
908
|
+
const raw = (env.MNEMOM_AGENT_TURN_HEADER ?? "").trim().toLowerCase();
|
|
909
|
+
return !["0", "off", "false", "no"].includes(raw);
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* Start base-url-proxy.mjs in front of `door`; calls `done` exactly once with its
|
|
913
|
+
* `http://127.0.0.1:<port>` base URL, or with null (after a one-line warning) when it does not
|
|
914
|
+
* report READY in time or exits first — the launch then goes straight to the door. A missing
|
|
915
|
+
* script is the same case (node exits 1 before READY). The proxy holds no secret (auth headers
|
|
916
|
+
* pass through it), exits with this process, and is killed on exit.
|
|
917
|
+
*/
|
|
918
|
+
export function startBaseUrlProxy(door, done, opts = {}) {
|
|
919
|
+
const script = opts.script ?? join(dirname(fileURLToPath(import.meta.url)), "..", "base-url-proxy.mjs");
|
|
920
|
+
// The log carries method / path / status / latency / turn note only (never a header value or body).
|
|
921
|
+
const logDir = mkdtempSync(join(tmpdir(), "mnemom-turn-proxy."));
|
|
922
|
+
const proxy = spawn(opts.node ?? process.execPath, [script], {
|
|
923
|
+
env: {
|
|
924
|
+
...process.env,
|
|
925
|
+
MNEMOM_AGENT_BU_DOOR: door,
|
|
926
|
+
MNEMOM_AGENT_BU_CWD: process.cwd(),
|
|
927
|
+
MNEMOM_AGENT_BU_LOG: process.env.MNEMOM_AGENT_TURN_PROXY_LOG || join(logDir, "proxy.log"),
|
|
928
|
+
MNEMOM_AGENT_BU_PARENT_PID: String(process.pid),
|
|
929
|
+
},
|
|
930
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
931
|
+
});
|
|
932
|
+
process.on("exit", () => {
|
|
933
|
+
proxy.kill("SIGTERM"); // a no-op returning false when it is already gone
|
|
934
|
+
rmSync(logDir, { recursive: true, force: true });
|
|
935
|
+
});
|
|
936
|
+
// The first of READY / timeout / exit decides; a later event (the proxy exiting at the end of the
|
|
937
|
+
// session, after READY) must not call `done` again.
|
|
938
|
+
let finished = false;
|
|
939
|
+
const finish = (url, why) => {
|
|
940
|
+
if (finished)
|
|
941
|
+
return;
|
|
942
|
+
finished = true;
|
|
943
|
+
clearTimeout(timer);
|
|
944
|
+
if (!url) {
|
|
945
|
+
say(fmt.warn(`mnemom agent: turn-origin proxy unavailable (${why}) — launching straight to the gateway; the gateway reads turn origins from the wire only.`));
|
|
946
|
+
}
|
|
947
|
+
done(url);
|
|
948
|
+
};
|
|
949
|
+
const timeoutMs = opts.readyTimeoutMs ?? 5000;
|
|
950
|
+
const timer = setTimeout(() => finish(null, `no READY within ${timeoutMs}ms`), timeoutMs);
|
|
951
|
+
let buf = "";
|
|
952
|
+
proxy.stdout?.on("data", (c) => {
|
|
953
|
+
buf += c.toString("utf8");
|
|
954
|
+
const m = buf.match(/^READY (\d+)$/m);
|
|
955
|
+
if (m)
|
|
956
|
+
finish(`http://127.0.0.1:${m[1]}`, "ready");
|
|
848
957
|
});
|
|
958
|
+
proxy.on("exit", (code) => finish(null, `exited ${code ?? "by signal"} before READY`));
|
|
959
|
+
// Unhandled, a spawn failure (EAGAIN, a missing binary) would throw out of the launcher.
|
|
960
|
+
proxy.on("error", (err) => finish(null, err.message));
|
|
849
961
|
}
|
|
850
962
|
// ───────────────────────────────────────────────────────────────────────────
|
|
851
963
|
// Remote Control launch (terminal RC + headless server) — behind the bundled proxy
|
|
@@ -987,6 +1099,10 @@ function spawnRcCli(ctx) {
|
|
|
987
1099
|
childEnv.https_proxy = `http://127.0.0.1:${ctx.port}`;
|
|
988
1100
|
childEnv.NODE_EXTRA_CA_CERTS = ctx.ca;
|
|
989
1101
|
childEnv[SESSION_NAME_ENV] = rcName;
|
|
1102
|
+
// Under RC Claude Code sees a first-party host (the proxy, not ANTHROPIC_BASE_URL, routes to the
|
|
1103
|
+
// gateway), so tool search is already on by default; setting it anyway keeps every launch shape
|
|
1104
|
+
// on the same switch. A user value is kept.
|
|
1105
|
+
applyToolSearchDefault(childEnv);
|
|
990
1106
|
// Context handoff (MNE-7564), RC shapes only: the strip list above deliberately removes every
|
|
991
1107
|
// ANTHROPIC_* var, so the handoff MCP server has nothing to inherit for the key unless we give
|
|
992
1108
|
// it something — a dedicated, non-ANTHROPIC-named var, added AFTER the strip loop so it can
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import { agentSlackCommand } from "./commands/agent-slack.js";
|
|
|
28
28
|
import { agentDoctorCommand } from "./commands/agent-doctor.js";
|
|
29
29
|
import { agentSetupCommand } from "./commands/agent-setup.js";
|
|
30
30
|
import { agentInviteCommand, runAgentInvocation } from "./commands/agent-invite.js";
|
|
31
|
+
import { agentFirstRunCommand, offerFirstRunOnLaunch } from "./commands/agent-first-run.js";
|
|
31
32
|
import { applyConfigToEnv, launcherEntries, loadLauncherConfig, withoutDisabledExperimental, } from "./lib/launcher-config.js";
|
|
32
33
|
import { loadExperimentalState } from "./lib/cli-config.js";
|
|
33
34
|
import { assertEnabled, EXPERIMENTAL_CONTRACT, gateCheck, ignoredSettingLines, reportCliError, } from "./lib/experimental.js";
|
|
@@ -173,6 +174,7 @@ const agentCmd = program
|
|
|
173
174
|
.addOption(new Option("--context-handoff", "EXPERIMENTAL (`mnemom experimental enable context-handoff`): register the `handoff(note)` MCP tool and tell the gateway to turn on the context-handoff capability for this session").hideHelp(hiddenUnless("context-handoff")))
|
|
174
175
|
.option("--no-context-handoff", "Keep context handoff off for this launch even when launcher.toml (context_handoff = true) or MNEMOM_AGENT_CONTEXT_HANDOFF turns it on")
|
|
175
176
|
.option("--dry-run", "Print the resolved launch plan and exit — no key read, no spawn")
|
|
177
|
+
.option("--first-run", "Show the first-run look again: what Mnemom would have saved on your last 30 days of Claude Code (runs on this machine, read-only, nothing uploaded) — then exit")
|
|
176
178
|
.action(async (scenario, passthrough, opts) => {
|
|
177
179
|
try {
|
|
178
180
|
// Invitation redemption — `mnemom agent invite <token>` (sub-verb) or
|
|
@@ -235,6 +237,11 @@ const agentCmd = program
|
|
|
235
237
|
applyConfigToEnv(cfg, process.env);
|
|
236
238
|
// Sub-verb: `mnemom agent doctor` runs a read-only preflight and never
|
|
237
239
|
// launches. Placed after config load so it reports the effective settings.
|
|
240
|
+
// `mnemom agent --first-run`: show the first-run look again and never launch.
|
|
241
|
+
if (opts.firstRun) {
|
|
242
|
+
agentFirstRunCommand();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
238
245
|
if (scenario === "doctor") {
|
|
239
246
|
await agentDoctorCommand({ cli: opts.cli });
|
|
240
247
|
return;
|
|
@@ -265,6 +272,9 @@ const agentCmd = program
|
|
|
265
272
|
// shadows a subcommand `--agent` under commander@12 — the MNE-238 class),
|
|
266
273
|
// exactly as status/logs/onboard read it.
|
|
267
274
|
const parentOpts = program.opts();
|
|
275
|
+
// First interactive launch only: offer (opt-in) the local look at what Mnemom
|
|
276
|
+
// would have saved. Tracked in ~/.mnemom/first-run.json; never blocks the launch.
|
|
277
|
+
await offerFirstRunOnLaunch({ dryRun: opts.dryRun });
|
|
268
278
|
await agentCommand(scenario, {
|
|
269
279
|
goal: opts.goal,
|
|
270
280
|
requirement: opts.requirement,
|