@mnemom/mnemom 0.17.0-next.7 → 0.17.0-next.9
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 +26 -0
- package/dist/commands/agent-config.d.ts +12 -0
- package/dist/commands/agent-config.js +22 -2
- package/dist/commands/agent-session.d.ts +26 -10
- package/dist/commands/agent-session.js +59 -23
- package/dist/commands/agent-sessions.d.ts +99 -0
- package/dist/commands/agent-sessions.js +271 -0
- package/dist/commands/agent.d.ts +55 -0
- package/dist/commands/agent.js +123 -13
- package/dist/commands/validate.js +1 -16
- package/dist/index.js +47 -5
- package/dist/lib/agent-ledger.d.ts +91 -0
- package/dist/lib/agent-ledger.js +126 -0
- package/dist/lib/agent.d.ts +43 -1
- package/dist/lib/agent.js +80 -1
- package/dist/lib/api.d.ts +62 -0
- package/dist/lib/api.js +11 -0
- package/dist/lib/auth.js +21 -1
- package/dist/lib/format.d.ts +6 -0
- package/dist/lib/format.js +20 -0
- package/dist/lib/launcher-config.d.ts +3 -1
- package/dist/lib/launcher-config.js +20 -1
- package/dist/lib/oauth.d.ts +26 -4
- package/dist/lib/oauth.js +98 -29
- package/dist/rc-proxy.mjs +14 -4
- package/package.json +7 -3
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent sessions [--all] [--json]` and `mnemom agent show [ref] [--json]`
|
|
3
|
+
* — the read side of the local session ledger (`~/.mnemom/sessions.jsonl`, one
|
|
4
|
+
* line per real launch) merged with the gateway's live goal state for one
|
|
5
|
+
* session (`GET /v1/agent/sessions/{conversationId}` via mnemom-api).
|
|
6
|
+
*
|
|
7
|
+
* Pure apart from the injected deps (`readRecords`, `fetchSession`, `out`,
|
|
8
|
+
* `err`), so every arm is unit-tested; `liveSessionDeps` is the thin driver
|
|
9
|
+
* index.ts wires in.
|
|
10
|
+
*
|
|
11
|
+
* Hash note: the CLI's launch-time envelope sha will NOT equal the gateway's
|
|
12
|
+
* sealed hash in general (the gateway normalises absent arrays to `[]`), so the
|
|
13
|
+
* local sha is never printed as "the sealed hash" and no mismatch is flagged —
|
|
14
|
+
* only the server's hash is shown, labelled "sealed hash".
|
|
15
|
+
*/
|
|
16
|
+
import { readSessionRecords } from "../lib/agent-ledger.js";
|
|
17
|
+
import { MNEMOM_DIR } from "../lib/config.js";
|
|
18
|
+
import { getAgentSession, MnemomApiError } from "../lib/api.js";
|
|
19
|
+
import { fmt, formatRelativeAge } from "../lib/format.js";
|
|
20
|
+
/** The production wiring (index.ts). Lazy so a test that mocks lib/api without
|
|
21
|
+
* this export can still import index.ts. */
|
|
22
|
+
export const liveSessionDeps = {
|
|
23
|
+
// The ledger is oldest-first on disk; every consumer here wants newest-first.
|
|
24
|
+
readRecords: () => readSessionRecords({ dir: MNEMOM_DIR }).reverse(),
|
|
25
|
+
fetchSession: (conversationId, agentId, orgId) => getAgentSession(conversationId, agentId, orgId),
|
|
26
|
+
out: (line) => console.log(line),
|
|
27
|
+
err: (line) => console.error(line),
|
|
28
|
+
};
|
|
29
|
+
export const NO_SESSIONS_MESSAGE = "No sessions yet — run `mnemom agent` to start one.";
|
|
30
|
+
export const DEFAULT_SESSIONS_LIMIT = 20;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve `[ref]` against the ledger (newest first):
|
|
33
|
+
* - no ref → the newest record;
|
|
34
|
+
* - an exact session name → the newest record with that name, older ones in `others`;
|
|
35
|
+
* - else a conversation-id prefix → the unique match, or `{record: null, candidates}`
|
|
36
|
+
* when several share the prefix;
|
|
37
|
+
* - nothing matches (or the ledger is empty) → null.
|
|
38
|
+
*/
|
|
39
|
+
export function resolveSessionRef(records, ref) {
|
|
40
|
+
const needle = ref?.trim() ?? "";
|
|
41
|
+
if (needle === "") {
|
|
42
|
+
const newest = records[0];
|
|
43
|
+
return newest ? { record: newest, others: [] } : null;
|
|
44
|
+
}
|
|
45
|
+
const newestByName = records.find((r) => r.session === needle);
|
|
46
|
+
if (newestByName) {
|
|
47
|
+
const others = records.filter((r) => r !== newestByName && r.session === needle);
|
|
48
|
+
return { record: newestByName, others };
|
|
49
|
+
}
|
|
50
|
+
const byConv = records.filter((r) => r.conversation_id.startsWith(needle));
|
|
51
|
+
if (byConv.length === 1)
|
|
52
|
+
return { record: byConv[0], others: [] };
|
|
53
|
+
if (byConv.length > 1)
|
|
54
|
+
return { record: null, candidates: byConv };
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// ─── sessions table ───────────────────────────────────────────────────────────
|
|
58
|
+
const COL = { session: 26, started: 16, agent: 22, goal: 6, conv: 8 };
|
|
59
|
+
function cell(text, width) {
|
|
60
|
+
const max = width - 2;
|
|
61
|
+
const shown = text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
62
|
+
return shown.padEnd(width);
|
|
63
|
+
}
|
|
64
|
+
/** Goal alignment as the ledger recorded it: a contract at launch ⇒ on. */
|
|
65
|
+
export function localGoalFlag(record) {
|
|
66
|
+
return record.contract ? "on" : "off";
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Render the ledger as a table, newest first — the newest
|
|
70
|
+
* `DEFAULT_SESSIONS_LIMIT` rows unless `all`, with a footer naming the rest.
|
|
71
|
+
*/
|
|
72
|
+
export function formatSessionsTable(rows, opts) {
|
|
73
|
+
const shown = opts.all ? rows : rows.slice(0, DEFAULT_SESSIONS_LIMIT);
|
|
74
|
+
const lines = [];
|
|
75
|
+
lines.push(" " +
|
|
76
|
+
cell("Session", COL.session) +
|
|
77
|
+
cell("Started", COL.started) +
|
|
78
|
+
cell("Agent", COL.agent) +
|
|
79
|
+
cell("Goal", COL.goal) +
|
|
80
|
+
"Conv");
|
|
81
|
+
lines.push(" " + "─".repeat(COL.session + COL.started + COL.agent + COL.goal + COL.conv));
|
|
82
|
+
for (const r of shown) {
|
|
83
|
+
lines.push(" " +
|
|
84
|
+
cell(r.session, COL.session) +
|
|
85
|
+
cell(formatRelativeAge(r.started_at), COL.started) +
|
|
86
|
+
cell(r.agent_slug, COL.agent) +
|
|
87
|
+
cell(localGoalFlag(r), COL.goal) +
|
|
88
|
+
r.conversation_id.slice(0, 8));
|
|
89
|
+
}
|
|
90
|
+
if (shown.length < rows.length) {
|
|
91
|
+
lines.push(fmt.dim(` showing ${shown.length} of ${rows.length} sessions — pass --all for every session`));
|
|
92
|
+
}
|
|
93
|
+
return lines;
|
|
94
|
+
}
|
|
95
|
+
// ─── show view ────────────────────────────────────────────────────────────────
|
|
96
|
+
function listOrDash(items) {
|
|
97
|
+
return items && items.length > 0 ? items.join(", ") : "—";
|
|
98
|
+
}
|
|
99
|
+
function numOrDash(n) {
|
|
100
|
+
return n === null || n === undefined ? "—" : String(n);
|
|
101
|
+
}
|
|
102
|
+
/** `turns 3/10 (crossed)` — value against its ceiling (none ⇒ just the value). */
|
|
103
|
+
export function gauge(label, value, ceiling, fired) {
|
|
104
|
+
const ratio = ceiling === null ? String(value) : `${value}/${ceiling}`;
|
|
105
|
+
return `${label} ${ratio}${fired ? " (crossed)" : ""}`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The human view for `mnemom agent show`: the local record, then the live
|
|
109
|
+
* gateway state (or a dim "live state unavailable (<reason>)" line when
|
|
110
|
+
* `remote` is null).
|
|
111
|
+
*/
|
|
112
|
+
export function buildShowView(local, remote, reason) {
|
|
113
|
+
const L = [];
|
|
114
|
+
L.push(fmt.header(`Session ${local.session}`));
|
|
115
|
+
L.push(fmt.label("started: ", `${local.started_at} (${formatRelativeAge(local.started_at)})`));
|
|
116
|
+
L.push(fmt.label("agent: ", `${local.agent_slug} ${local.agent_id ? `(${local.agent_id})` : fmt.dim("(id unresolved at launch)")}`));
|
|
117
|
+
L.push(fmt.label("org: ", `${local.org_label} (${local.org_id})`));
|
|
118
|
+
L.push(fmt.label("gateway: ", local.gateway_host));
|
|
119
|
+
const launchBits = [local.launch_shape, `cli ${local.cli}`];
|
|
120
|
+
if (local.model)
|
|
121
|
+
launchBits.push(`model ${local.model}`);
|
|
122
|
+
if (local.effort)
|
|
123
|
+
launchBits.push(`effort ${local.effort}`);
|
|
124
|
+
L.push(fmt.label("launch: ", launchBits.join(" · ")));
|
|
125
|
+
L.push(fmt.label("conversation: ", local.conversation_id));
|
|
126
|
+
const goalOn = remote ? remote.sealed !== null : local.contract !== null;
|
|
127
|
+
L.push(fmt.label("goal alignment:", goalOn ? fmt.badge("ON", "green") : fmt.badge("OFF", "yellow")));
|
|
128
|
+
if (local.contract) {
|
|
129
|
+
L.push(fmt.label("goal (at launch):", local.contract.statement));
|
|
130
|
+
}
|
|
131
|
+
L.push(fmt.section("Live state (gateway)"));
|
|
132
|
+
if (!remote) {
|
|
133
|
+
L.push(fmt.dim(` live state unavailable (${reason ?? "unknown"})`));
|
|
134
|
+
return L;
|
|
135
|
+
}
|
|
136
|
+
if (remote.sealed) {
|
|
137
|
+
const c = remote.sealed.contract;
|
|
138
|
+
L.push(fmt.label("statement: ", c.statement));
|
|
139
|
+
L.push(fmt.label("requirements: ", listOrDash(c.requirements)));
|
|
140
|
+
L.push(fmt.label("allowed paths: ", listOrDash(c.allowed_paths)));
|
|
141
|
+
L.push(fmt.label("forbidden paths:", listOrDash(c.forbidden_paths)));
|
|
142
|
+
L.push(fmt.label("ceilings: ", `max_turns ${numOrDash(c.max_turns)} · budget_usd ${numOrDash(c.budget_usd)} · stall_turns ${numOrDash(c.stall_turns)}`));
|
|
143
|
+
L.push(fmt.label("sealed hash: ", remote.sealed.hash));
|
|
144
|
+
L.push(fmt.label("sealed at: ", remote.sealed.sealed_at));
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
L.push(fmt.dim(" no sealed contract on the gateway for this conversation"));
|
|
148
|
+
}
|
|
149
|
+
if (remote.verdict) {
|
|
150
|
+
const v = remote.verdict;
|
|
151
|
+
L.push(fmt.label("verdict: ", `${v.verdict.toUpperCase()} at turn ${v.turn} — ${v.reason}`));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
L.push(fmt.label("verdict: ", fmt.dim("none yet")));
|
|
155
|
+
}
|
|
156
|
+
if (remote.guardrails) {
|
|
157
|
+
const { ceilings, state } = remote.guardrails;
|
|
158
|
+
if (state) {
|
|
159
|
+
const parts = [
|
|
160
|
+
gauge("turns", state.turns, ceilings.max_turns, state.fired.turns),
|
|
161
|
+
gauge("spend $", state.spend_usd, ceilings.budget_usd, state.fired.budget),
|
|
162
|
+
gauge("stall", state.stall_run, ceilings.stall_turns, state.fired.stall),
|
|
163
|
+
];
|
|
164
|
+
L.push(fmt.label("guardrails: ", parts.join(" · ")));
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
L.push(fmt.label("guardrails: ", `ceilings max_turns ${numOrDash(ceilings.max_turns)} · budget_usd ${numOrDash(ceilings.budget_usd)} · stall_turns ${numOrDash(ceilings.stall_turns)} ${fmt.dim("(no turns counted yet)")}`));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
L.push(fmt.label("guardrails: ", fmt.dim("none")));
|
|
172
|
+
}
|
|
173
|
+
if (remote.turn !== null) {
|
|
174
|
+
L.push(fmt.label("turn: ", String(remote.turn)));
|
|
175
|
+
}
|
|
176
|
+
return L;
|
|
177
|
+
}
|
|
178
|
+
export function buildShowJson(local, remote, reason) {
|
|
179
|
+
const goalOn = remote ? remote.sealed !== null : local.contract !== null;
|
|
180
|
+
return {
|
|
181
|
+
session: { ...local, source: "local" },
|
|
182
|
+
goal_alignment: goalOn ? "on" : "off",
|
|
183
|
+
live: remote
|
|
184
|
+
? {
|
|
185
|
+
source: "gateway",
|
|
186
|
+
available: true,
|
|
187
|
+
sealed: remote.sealed,
|
|
188
|
+
verdict: remote.verdict,
|
|
189
|
+
turn: remote.turn,
|
|
190
|
+
guardrails: remote.guardrails,
|
|
191
|
+
}
|
|
192
|
+
: { source: "gateway", available: false, reason: reason ?? "unknown" },
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/** `mnemom agent sessions [--all] [--json]` */
|
|
196
|
+
export async function agentSessionsCommand(opts, deps) {
|
|
197
|
+
const records = deps.readRecords();
|
|
198
|
+
const rows = opts.all ? records : records.slice(0, DEFAULT_SESSIONS_LIMIT);
|
|
199
|
+
if (opts.json) {
|
|
200
|
+
deps.out(JSON.stringify(rows, null, 2));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (records.length === 0) {
|
|
204
|
+
deps.out(NO_SESSIONS_MESSAGE);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
deps.out(fmt.header("Mnemom Agent sessions"));
|
|
208
|
+
deps.out("");
|
|
209
|
+
for (const line of formatSessionsTable(records, { all: opts.all }))
|
|
210
|
+
deps.out(line);
|
|
211
|
+
deps.out("");
|
|
212
|
+
}
|
|
213
|
+
/** Why the live read could not happen, from the thrown error. */
|
|
214
|
+
export function unavailableReason(err) {
|
|
215
|
+
if (err instanceof MnemomApiError && err.code)
|
|
216
|
+
return `${err.code}: ${err.message}`;
|
|
217
|
+
return err instanceof Error ? err.message : String(err);
|
|
218
|
+
}
|
|
219
|
+
/** `mnemom agent show [ref] [--json]` */
|
|
220
|
+
export async function agentShowCommand(ref, opts, deps) {
|
|
221
|
+
const records = deps.readRecords();
|
|
222
|
+
if (records.length === 0) {
|
|
223
|
+
if (opts.json)
|
|
224
|
+
deps.out(JSON.stringify(null));
|
|
225
|
+
else
|
|
226
|
+
deps.out(NO_SESSIONS_MESSAGE);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const resolved = resolveSessionRef(records, ref);
|
|
230
|
+
if (resolved === null) {
|
|
231
|
+
deps.err(fmt.error(`No session matches '${ref ?? ""}' — try \`mnemom agent sessions\`.`));
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (resolved.record === null) {
|
|
236
|
+
deps.err(fmt.error(`'${ref ?? ""}' matches ${resolved.candidates.length} sessions — give more of the conversation id:`));
|
|
237
|
+
for (const c of resolved.candidates) {
|
|
238
|
+
deps.err(` ${c.conversation_id} ${c.session} ${formatRelativeAge(c.started_at)}`);
|
|
239
|
+
}
|
|
240
|
+
process.exitCode = 1;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const { record, others } = resolved;
|
|
244
|
+
let remote = null;
|
|
245
|
+
let reason;
|
|
246
|
+
if (record.agent_id === null) {
|
|
247
|
+
reason = "agent id was not resolved at launch";
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
try {
|
|
251
|
+
remote = await deps.fetchSession(record.conversation_id, record.agent_id, record.org_id);
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
reason = unavailableReason(e);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (opts.json) {
|
|
258
|
+
deps.out(JSON.stringify(buildShowJson(record, remote, reason), null, 2));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
for (const line of buildShowView(record, remote, reason))
|
|
262
|
+
deps.out(line);
|
|
263
|
+
if (others.length > 0) {
|
|
264
|
+
deps.out("");
|
|
265
|
+
deps.out(fmt.dim(` ${others.length} older session(s) share this name — \`mnemom agent show <conv-id prefix>\` picks one:`));
|
|
266
|
+
for (const o of others) {
|
|
267
|
+
deps.out(fmt.dim(` ${o.conversation_id.slice(0, 8)} ${formatRelativeAge(o.started_at)}`));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
deps.out("");
|
|
271
|
+
}
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* line, or the proxy's stdin under RC). It is never logged, printed, or passed as
|
|
26
26
|
* an argv value.
|
|
27
27
|
*/
|
|
28
|
+
import { type LaunchShape, type SealedContractEnvelope } from "../lib/agent.js";
|
|
28
29
|
export interface AgentOptions {
|
|
29
30
|
goal?: string;
|
|
30
31
|
requirement?: string[];
|
|
@@ -34,6 +35,12 @@ export interface AgentOptions {
|
|
|
34
35
|
budget?: string;
|
|
35
36
|
maxTurns?: string;
|
|
36
37
|
stall?: string;
|
|
38
|
+
/**
|
|
39
|
+
* `--implicit`: goal alignment with no sealed contract — the gateway infers the
|
|
40
|
+
* goal from the user's messages and revises it as they steer; the ceilings above
|
|
41
|
+
* ride on the `x-mnemom-goal-mode` header. Exclusive with the contract flags.
|
|
42
|
+
*/
|
|
43
|
+
implicit?: boolean;
|
|
37
44
|
/** x-mnemom-agent value (defaults to "code-agent"). */
|
|
38
45
|
agent?: string;
|
|
39
46
|
conversationId?: string;
|
|
@@ -66,6 +73,25 @@ export interface AgentOptions {
|
|
|
66
73
|
}
|
|
67
74
|
/** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
|
|
68
75
|
export declare function agentCommand(scenarioArg: string | undefined, options?: AgentOptions): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
78
|
+
* resolved best-effort (a lookup failure or an unknown slug records null); a
|
|
79
|
+
* ledger write failure warns to stderr and the launch proceeds regardless.
|
|
80
|
+
*/
|
|
81
|
+
export declare function recordSessionLaunch(ctx: {
|
|
82
|
+
session: string;
|
|
83
|
+
conversationId: string;
|
|
84
|
+
agent: string;
|
|
85
|
+
account: {
|
|
86
|
+
orgId: string;
|
|
87
|
+
orgLabel: string;
|
|
88
|
+
};
|
|
89
|
+
gatewayHost: string;
|
|
90
|
+
shape: LaunchShape;
|
|
91
|
+
options: AgentOptions;
|
|
92
|
+
cli: ResolvedCli;
|
|
93
|
+
envelope: SealedContractEnvelope | null;
|
|
94
|
+
}): Promise<void>;
|
|
69
95
|
/**
|
|
70
96
|
* Resolve the Anthropic key with NO 1Password dependency (built for external
|
|
71
97
|
* customer adoption), in order:
|
|
@@ -131,3 +157,32 @@ export declare function buildCliArgs(options: AgentOptions, ctx: {
|
|
|
131
157
|
yolo: boolean;
|
|
132
158
|
allowCliFlags: boolean;
|
|
133
159
|
}): string[];
|
|
160
|
+
/**
|
|
161
|
+
* Which goal-alignment mode the launch status line names: a sealed contract →
|
|
162
|
+
* "on"; `--implicit` (mode header, no contract) → "implicit"; neither → "off".
|
|
163
|
+
* A sealed contract and a mode header never coexist (`assembleContract` refuses
|
|
164
|
+
* the combination), so the first arm winning is not a precedence choice.
|
|
165
|
+
*/
|
|
166
|
+
export declare function goalAlignmentMode(envelope: SealedContractEnvelope | null, goalMode: string | null): "on" | "implicit" | "off";
|
|
167
|
+
export declare function logLaunch(ctx: {
|
|
168
|
+
shape: LaunchShape;
|
|
169
|
+
door: string;
|
|
170
|
+
agent: string;
|
|
171
|
+
conversationId: string;
|
|
172
|
+
cellNote: string;
|
|
173
|
+
envelope: SealedContractEnvelope | null;
|
|
174
|
+
goalMode: string | null;
|
|
175
|
+
cli: ResolvedCli;
|
|
176
|
+
}): void;
|
|
177
|
+
export declare function printPlan(ctx: {
|
|
178
|
+
scenario: string;
|
|
179
|
+
shape: LaunchShape;
|
|
180
|
+
agent: string;
|
|
181
|
+
conversationId: string;
|
|
182
|
+
door: string;
|
|
183
|
+
cellNote: string;
|
|
184
|
+
envelope: SealedContractEnvelope | null;
|
|
185
|
+
goalMode: string | null;
|
|
186
|
+
yolo: boolean;
|
|
187
|
+
cli: ResolvedCli;
|
|
188
|
+
}): void;
|
package/dist/commands/agent.js
CHANGED
|
@@ -31,20 +31,32 @@ import { tmpdir, homedir } from "node:os";
|
|
|
31
31
|
import { basename, join, dirname } from "node:path";
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
33
|
import { MNEMOM_DIR } from "../lib/config.js";
|
|
34
|
+
import { getAgentByName } from "../lib/api.js";
|
|
35
|
+
import { LEDGER_FILE, appendSessionRecord, toLedgerContract } from "../lib/agent-ledger.js";
|
|
34
36
|
import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
|
|
35
37
|
import { fmt } from "../lib/format.js";
|
|
36
|
-
import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
38
|
+
import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
37
39
|
import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
|
|
38
40
|
import { defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, } from "./agent-setup.js";
|
|
39
|
-
import { assembleContract, buildCustomHeaders, isProdGateway, launchPreferenceExplicit, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, sanitizeAgentName, } from "../lib/agent.js";
|
|
41
|
+
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, isProdGateway, launchPreferenceExplicit, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, sanitizeAgentName, } from "../lib/agent.js";
|
|
40
42
|
/** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
|
|
41
43
|
const CODE_STORE = join(MNEMOM_DIR, "launcher.json");
|
|
42
44
|
const say = (line = "") => console.error(line);
|
|
43
45
|
/** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
|
|
44
46
|
export async function agentCommand(scenarioArg, options = {}) {
|
|
45
47
|
// ── session name + goal ── (a bare `mnemom agent` in a terminal asks for both,
|
|
46
|
-
// explaining goal alignment
|
|
47
|
-
|
|
48
|
+
// explaining goal alignment: infer it from your messages (implicit), type a
|
|
49
|
+
// one-line goal (sealed), or off — no contract header and no mode header)
|
|
50
|
+
const session = await resolveSessionSetup({
|
|
51
|
+
scenario: scenarioArg,
|
|
52
|
+
goal: options.goal,
|
|
53
|
+
implicit: options.implicit,
|
|
54
|
+
interactive: isInteractive(),
|
|
55
|
+
}, {
|
|
56
|
+
askInput: (q) => askInput(q),
|
|
57
|
+
askYesNo: (q, d) => askYesNo(q, d),
|
|
58
|
+
askSelect: (q, o) => askSelect(q, o),
|
|
59
|
+
});
|
|
48
60
|
const scenario = session.scenario;
|
|
49
61
|
const env = process.env;
|
|
50
62
|
let shape = resolveLaunchShape({ remoteControl: options.remoteControl, server: options.server }, env);
|
|
@@ -68,6 +80,15 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
68
80
|
budget: options.budget,
|
|
69
81
|
maxTurns: options.maxTurns,
|
|
70
82
|
stall: options.stall,
|
|
83
|
+
implicit: options.implicit,
|
|
84
|
+
});
|
|
85
|
+
// ── implicit goal mode (no contract; the gateway infers the goal from the user's
|
|
86
|
+
// messages) — the guardrail ceilings ride on the mode header instead ──
|
|
87
|
+
const goalMode = buildGoalModeHeader({
|
|
88
|
+
implicit: options.implicit,
|
|
89
|
+
budget: options.budget,
|
|
90
|
+
maxTurns: options.maxTurns,
|
|
91
|
+
stall: options.stall,
|
|
71
92
|
});
|
|
72
93
|
// ── door ──
|
|
73
94
|
const gatewayHost = resolveGatewayHost(env);
|
|
@@ -117,6 +138,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
117
138
|
door,
|
|
118
139
|
cellNote,
|
|
119
140
|
envelope,
|
|
141
|
+
goalMode,
|
|
120
142
|
yolo,
|
|
121
143
|
cli: planCli,
|
|
122
144
|
});
|
|
@@ -140,8 +162,9 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
140
162
|
// there isn't one — and (2) enforce a positive µ balance on the org the agent
|
|
141
163
|
// lives in (default: the user's personal org). No µ ⇒ blocked with a
|
|
142
164
|
// portal-style top-up message. Skipped only on --dry-run (handled above).
|
|
165
|
+
let account;
|
|
143
166
|
try {
|
|
144
|
-
await ensureSessionAndBalance({ interactive: isInteractive() });
|
|
167
|
+
account = await ensureSessionAndBalance({ interactive: isInteractive() });
|
|
145
168
|
}
|
|
146
169
|
catch (err) {
|
|
147
170
|
if (isQuietPreambleExit(err))
|
|
@@ -175,8 +198,23 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
175
198
|
say(fmt.warn(`mnemom agent: agent setup did not complete (${msg}) — launching with the unclaimed identity.`));
|
|
176
199
|
}
|
|
177
200
|
}
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
// ── local session ledger (~/.mnemom/sessions.jsonl) ── one line per real
|
|
202
|
+
// launch so `mnemom agent sessions` / `show` can find this conversation later.
|
|
203
|
+
// Written after provisioning so a freshly claimed agent's id resolves. Holds
|
|
204
|
+
// NO secret (never the key, the headers, or the child env); best-effort.
|
|
205
|
+
await recordSessionLaunch({
|
|
206
|
+
session: scenario,
|
|
207
|
+
conversationId,
|
|
208
|
+
agent,
|
|
209
|
+
account,
|
|
210
|
+
gatewayHost,
|
|
211
|
+
shape,
|
|
212
|
+
options,
|
|
213
|
+
cli,
|
|
214
|
+
envelope,
|
|
215
|
+
});
|
|
216
|
+
logLaunch({ shape, door, agent, conversationId, cellNote, envelope, goalMode, cli });
|
|
217
|
+
say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
|
|
180
218
|
if (shape === "terminal") {
|
|
181
219
|
launchTerminal({
|
|
182
220
|
cli,
|
|
@@ -185,6 +223,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
185
223
|
agent,
|
|
186
224
|
conversationId,
|
|
187
225
|
envelope,
|
|
226
|
+
goalMode,
|
|
188
227
|
options,
|
|
189
228
|
yolo,
|
|
190
229
|
contextHint,
|
|
@@ -200,11 +239,46 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
200
239
|
conversationId,
|
|
201
240
|
scenario,
|
|
202
241
|
envelope,
|
|
242
|
+
goalMode,
|
|
203
243
|
options,
|
|
204
244
|
yolo,
|
|
205
245
|
});
|
|
206
246
|
}
|
|
207
247
|
// ───────────────────────────────────────────────────────────────────────────
|
|
248
|
+
// Session ledger (thin driver over lib/agent-ledger.ts)
|
|
249
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
250
|
+
/**
|
|
251
|
+
* Record this launch in ~/.mnemom/sessions.jsonl. The agent record id is
|
|
252
|
+
* resolved best-effort (a lookup failure or an unknown slug records null); a
|
|
253
|
+
* ledger write failure warns to stderr and the launch proceeds regardless.
|
|
254
|
+
*/
|
|
255
|
+
export async function recordSessionLaunch(ctx) {
|
|
256
|
+
let agentId;
|
|
257
|
+
try {
|
|
258
|
+
agentId = (await getAgentByName(ctx.agent))?.id ?? null;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
agentId = null; // unresolvable right now — the ledger still records the launch
|
|
262
|
+
}
|
|
263
|
+
const result = appendSessionRecord({
|
|
264
|
+
session: ctx.session,
|
|
265
|
+
conversation_id: ctx.conversationId,
|
|
266
|
+
agent_slug: ctx.agent,
|
|
267
|
+
agent_id: agentId,
|
|
268
|
+
org_id: ctx.account.orgId,
|
|
269
|
+
org_label: ctx.account.orgLabel,
|
|
270
|
+
gateway_host: ctx.gatewayHost,
|
|
271
|
+
launch_shape: ctx.shape,
|
|
272
|
+
model: ctx.options.model?.trim() || null,
|
|
273
|
+
effort: ctx.options.effort?.trim() || null,
|
|
274
|
+
cli: ctx.cli.name,
|
|
275
|
+
contract: toLedgerContract(ctx.envelope?.contract ?? null),
|
|
276
|
+
}, { dir: MNEMOM_DIR, now: () => new Date(), pid: process.pid });
|
|
277
|
+
if (!result.ok) {
|
|
278
|
+
say(fmt.warn(`mnemom agent: could not record this session in ~/.mnemom/${LEDGER_FILE} (${result.error}) — launching anyway.`));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
208
282
|
// Credential resolution (zero 1Password)
|
|
209
283
|
// ───────────────────────────────────────────────────────────────────────────
|
|
210
284
|
/**
|
|
@@ -220,7 +294,7 @@ export async function resolveAnthropicKey(env, opts) {
|
|
|
220
294
|
const source = opts.keySource;
|
|
221
295
|
// "prompt" forces the interactive prompt (skips env + store).
|
|
222
296
|
if (source !== "prompt") {
|
|
223
|
-
const fromEnv = (env.MNEMOM_AGENT_ANTHROPIC_KEY || env.ANTHROPIC_API_KEY || "")
|
|
297
|
+
const fromEnv = stripCrlf(env.MNEMOM_AGENT_ANTHROPIC_KEY || env.ANTHROPIC_API_KEY || "");
|
|
224
298
|
if (fromEnv)
|
|
225
299
|
return fromEnv;
|
|
226
300
|
// "env" restricts to env only — no store, no prompt.
|
|
@@ -231,11 +305,17 @@ export async function resolveAnthropicKey(env, opts) {
|
|
|
231
305
|
const stored = readStoredKey();
|
|
232
306
|
if (stored)
|
|
233
307
|
return stored;
|
|
308
|
+
// "store" restricts to env → store — never the interactive prompt.
|
|
309
|
+
if (source === "store") {
|
|
310
|
+
throw new Error("mnemom agent: key_source is 'store' but no Anthropic key is set in env or ~/.mnemom/launcher.json.\n" +
|
|
311
|
+
" Set one: export MNEMOM_AGENT_ANTHROPIC_KEY=sk-ant-... (or ANTHROPIC_API_KEY)\n" +
|
|
312
|
+
" Or store it: `mnemom agent config set key_source prompt`, run `mnemom agent` once and accept the save.");
|
|
313
|
+
}
|
|
234
314
|
}
|
|
235
|
-
// (source can only be "
|
|
315
|
+
// (source can only be "prompt" or undefined here — "env" and "store" threw above.)
|
|
236
316
|
if (opts.allowPrompt && isInteractive()) {
|
|
237
317
|
say("mnemom agent: no Anthropic key in env (MNEMOM_AGENT_ANTHROPIC_KEY / ANTHROPIC_API_KEY) or ~/.mnemom/launcher.json.");
|
|
238
|
-
const entered = (await askInput("Enter your Anthropic API key (sk-ant-…):", true))
|
|
318
|
+
const entered = stripCrlf(await askInput("Enter your Anthropic API key (sk-ant-…):", true));
|
|
239
319
|
if (entered) {
|
|
240
320
|
if (await askYesNo("Save it to ~/.mnemom/launcher.json (0600) for next time?", true)) {
|
|
241
321
|
storeKey(entered);
|
|
@@ -265,7 +345,7 @@ export function anthropicKeySource(env = process.env) {
|
|
|
265
345
|
function readStoredKey() {
|
|
266
346
|
try {
|
|
267
347
|
const data = JSON.parse(readFileSync(CODE_STORE, "utf8"));
|
|
268
|
-
const key = typeof data.anthropic_key === "string" ? data.anthropic_key
|
|
348
|
+
const key = typeof data.anthropic_key === "string" ? stripCrlf(data.anthropic_key) : "";
|
|
269
349
|
return key || undefined;
|
|
270
350
|
}
|
|
271
351
|
catch {
|
|
@@ -442,6 +522,7 @@ function launchTerminal(ctx) {
|
|
|
442
522
|
agent: ctx.agent,
|
|
443
523
|
conversationId: ctx.conversationId,
|
|
444
524
|
contractB64: ctx.envelope?.base64,
|
|
525
|
+
goalMode: ctx.goalMode ?? undefined,
|
|
445
526
|
});
|
|
446
527
|
const args = buildCliArgs(ctx.options, { yolo: ctx.yolo, allowCliFlags: true });
|
|
447
528
|
// Claude Code logged in via claude.ai (OAuth) PREEMPTS ANTHROPIC_API_KEY, so
|
|
@@ -489,6 +570,7 @@ function launchRemoteControl(ctx) {
|
|
|
489
570
|
MNEMOM_AGENT_RC_AGENT: ctx.agent,
|
|
490
571
|
MNEMOM_AGENT_RC_CONVERSATION_ID: ctx.conversationId,
|
|
491
572
|
MNEMOM_AGENT_RC_CONTRACT_B64: ctx.envelope?.base64 ?? "",
|
|
573
|
+
MNEMOM_AGENT_RC_GOAL_MODE: ctx.goalMode ?? "",
|
|
492
574
|
MNEMOM_AGENT_RC_PROXY_LOG: proxyLog,
|
|
493
575
|
MNEMOM_AGENT_RC_PARENT_PID: String(process.pid),
|
|
494
576
|
},
|
|
@@ -720,14 +802,30 @@ function seedContextHintFlag(childEnv) {
|
|
|
720
802
|
// ───────────────────────────────────────────────────────────────────────────
|
|
721
803
|
// Human-facing logging (never prints the key)
|
|
722
804
|
// ───────────────────────────────────────────────────────────────────────────
|
|
723
|
-
|
|
805
|
+
/**
|
|
806
|
+
* Which goal-alignment mode the launch status line names: a sealed contract →
|
|
807
|
+
* "on"; `--implicit` (mode header, no contract) → "implicit"; neither → "off".
|
|
808
|
+
* A sealed contract and a mode header never coexist (`assembleContract` refuses
|
|
809
|
+
* the combination), so the first arm winning is not a precedence choice.
|
|
810
|
+
*/
|
|
811
|
+
export function goalAlignmentMode(envelope, goalMode) {
|
|
812
|
+
if (envelope)
|
|
813
|
+
return "on";
|
|
814
|
+
if (goalMode)
|
|
815
|
+
return "implicit";
|
|
816
|
+
return "off";
|
|
817
|
+
}
|
|
818
|
+
export function logLaunch(ctx) {
|
|
724
819
|
say(`mnemom agent: routing ${ctx.cli.name} through ${ctx.door} [${ctx.cellNote}] as '${ctx.agent}' (conversation ${ctx.conversationId}).`);
|
|
725
820
|
if (ctx.envelope) {
|
|
726
821
|
say(`mnemom agent: contract statement: ${ctx.envelope.contract.statement}`);
|
|
727
822
|
say(`mnemom agent: contract sha256[0:16]: ${ctx.envelope.sha}`);
|
|
728
823
|
}
|
|
824
|
+
else if (ctx.goalMode) {
|
|
825
|
+
say(`mnemom agent: goal mode: ${ctx.goalMode} (inferred from your messages; watch x-mnemom-goal-revision / x-mnemom-goal-steer on each response)`);
|
|
826
|
+
}
|
|
729
827
|
}
|
|
730
|
-
function printPlan(ctx) {
|
|
828
|
+
export function printPlan(ctx) {
|
|
731
829
|
const L = [];
|
|
732
830
|
L.push("mnemom agent — launch plan (dry run)");
|
|
733
831
|
L.push(` scenario: ${ctx.scenario}`);
|
|
@@ -741,6 +839,10 @@ function printPlan(ctx) {
|
|
|
741
839
|
L.push(` contract: ${ctx.envelope.canonical}`);
|
|
742
840
|
L.push(` contract sha: ${ctx.envelope.sha}`);
|
|
743
841
|
}
|
|
842
|
+
else if (ctx.goalMode) {
|
|
843
|
+
L.push(" contract: (none — implicit goal mode: the gateway infers the goal from your messages)");
|
|
844
|
+
L.push(` goal mode: ${ctx.goalMode}`);
|
|
845
|
+
}
|
|
744
846
|
else {
|
|
745
847
|
L.push(" contract: (none — no contract flag given)");
|
|
746
848
|
}
|
|
@@ -748,3 +850,11 @@ function printPlan(ctx) {
|
|
|
748
850
|
L.push(" Dry run — no key was read and nothing was launched.");
|
|
749
851
|
process.stdout.write(`${L.join("\n")}\n`);
|
|
750
852
|
}
|
|
853
|
+
/**
|
|
854
|
+
* Header-value hygiene for the Anthropic key: the key is spliced into the newline-joined
|
|
855
|
+
* ANTHROPIC_CUSTOM_HEADERS block (`x-api-key: <key>`), so an embedded CR/LF from a multi-line
|
|
856
|
+
* paste would inject extra header lines. Same rule resolveConversationId applies to its input.
|
|
857
|
+
*/
|
|
858
|
+
function stripCrlf(raw) {
|
|
859
|
+
return raw.replace(/[\r\n]+/g, "").trim();
|
|
860
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* green — useful for shell scripts and the `mnemom/deploy` gate-guard
|
|
13
13
|
* workflow which can call the CLI rather than parse the JSON itself.
|
|
14
14
|
*/
|
|
15
|
-
import { fmt } from "../lib/format.js";
|
|
15
|
+
import { fmt, formatRelativeAge } from "../lib/format.js";
|
|
16
16
|
import { requireAuth } from "../lib/auth.js";
|
|
17
17
|
import { getSafeHouseHarnessState } from "../lib/api.js";
|
|
18
18
|
/**
|
|
@@ -32,21 +32,6 @@ function parseMaxAge(input) {
|
|
|
32
32
|
const ms = n * { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[unit];
|
|
33
33
|
return ms;
|
|
34
34
|
}
|
|
35
|
-
function formatRelativeAge(startedAt) {
|
|
36
|
-
const ageMs = Date.now() - Date.parse(startedAt);
|
|
37
|
-
if (ageMs < 0)
|
|
38
|
-
return "in the future?";
|
|
39
|
-
const m = Math.floor(ageMs / 60_000);
|
|
40
|
-
if (m < 1)
|
|
41
|
-
return "<1m ago";
|
|
42
|
-
if (m < 60)
|
|
43
|
-
return `${m}m ago`;
|
|
44
|
-
const h = Math.floor(m / 60);
|
|
45
|
-
if (h < 24)
|
|
46
|
-
return `${h}h ${m % 60}m ago`;
|
|
47
|
-
const d = Math.floor(h / 24);
|
|
48
|
-
return `${d}d ${h % 24}h ago`;
|
|
49
|
-
}
|
|
50
35
|
function evaluateLane(lane, run, maxAgeMs) {
|
|
51
36
|
if (!run) {
|
|
52
37
|
return {
|