@mnemom/mnemom 0.17.2-next.7 → 0.17.2-next.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent-doctor.d.ts +1 -1
- package/dist/commands/agent-doctor.js +5 -1
- package/dist/commands/agent-setup.d.ts +34 -3
- package/dist/commands/agent-setup.js +86 -11
- package/dist/commands/agent.js +11 -3
- package/dist/commands/config.d.ts +36 -0
- package/dist/commands/config.js +82 -0
- package/dist/commands/update.d.ts +30 -0
- package/dist/commands/update.js +90 -0
- package/dist/index.js +41 -0
- package/dist/lib/agent.d.ts +13 -7
- package/dist/lib/agent.js +12 -12
- package/dist/lib/cli-config.d.ts +8 -0
- package/dist/lib/cli-config.js +13 -0
- package/dist/lib/update/action.d.ts +9 -0
- package/dist/lib/update/action.js +17 -0
- package/dist/lib/update/decide.d.ts +152 -0
- package/dist/lib/update/decide.js +195 -0
- package/dist/lib/update/doctor.d.ts +18 -0
- package/dist/lib/update/doctor.js +63 -0
- package/dist/lib/update/install-method.d.ts +12 -0
- package/dist/lib/update/install-method.js +33 -0
- package/dist/lib/update/min-version.d.ts +34 -0
- package/dist/lib/update/min-version.js +69 -0
- package/dist/lib/update/registry.d.ts +27 -0
- package/dist/lib/update/registry.js +50 -0
- package/dist/lib/update/run.d.ts +54 -0
- package/dist/lib/update/run.js +100 -0
- package/dist/lib/update/semver.d.ts +22 -0
- package/dist/lib/update/semver.js +65 -0
- package/dist/lib/update/startup.d.ts +27 -0
- package/dist/lib/update/startup.js +75 -0
- package/dist/lib/update/state.d.ts +23 -0
- package/dist/lib/update/state.js +83 -0
- package/dist/lib/update/system.d.ts +55 -0
- package/dist/lib/update/system.js +104 -0
- package/dist/lib/update/worker.d.ts +42 -0
- package/dist/lib/update/worker.js +104 -0
- package/dist/lib/version-gate.d.ts +2 -1
- package/dist/lib/version-gate.js +9 -1
- package/dist/update-worker.d.ts +6 -0
- package/dist/update-worker.js +8 -0
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Checks: settings file, coding-agent CLI, Anthropic key source, gateway health
|
|
10
10
|
* (200 + the x-mnemom-verdict header that proves the governance pipeline is
|
|
11
11
|
* live), Remote-Control capability (node + openssl + the --remote-control flag),
|
|
12
|
-
*
|
|
12
|
+
* claude.ai login (informational — Remote Control needs it), and CLI auto-update status.
|
|
13
13
|
*/
|
|
14
14
|
type Mark = "ok" | "warn" | "miss" | "info";
|
|
15
15
|
export interface DoctorOptions {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Checks: settings file, coding-agent CLI, Anthropic key source, gateway health
|
|
10
10
|
* (200 + the x-mnemom-verdict header that proves the governance pipeline is
|
|
11
11
|
* live), Remote-Control capability (node + openssl + the --remote-control flag),
|
|
12
|
-
*
|
|
12
|
+
* claude.ai login (informational — Remote Control needs it), and CLI auto-update status.
|
|
13
13
|
*/
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
15
|
import { existsSync } from "node:fs";
|
|
@@ -20,6 +20,7 @@ import { getCodeEntitlement, MnemomApiError } from "../lib/api.js";
|
|
|
20
20
|
import { isLoggedIn } from "../lib/auth.js";
|
|
21
21
|
import { getActiveOrg } from "../lib/cli-config.js";
|
|
22
22
|
import { isCodeNotEnabled, sourceLabel } from "./agent-invite.js";
|
|
23
|
+
import { probeUpdateStatus } from "../lib/update/doctor.js";
|
|
23
24
|
const MARK = { ok: "[ ok ]", warn: "[warn]", miss: "[MISS]", info: "[info]" };
|
|
24
25
|
const out = (line = "") => console.log(line);
|
|
25
26
|
/** Run the preflight and print the checklist. Sets process.exitCode on failure. */
|
|
@@ -85,6 +86,9 @@ export async function agentDoctorCommand(options = {}) {
|
|
|
85
86
|
// ── Mnemom Agent access (invited cohort gate) ──
|
|
86
87
|
const gate = await probeCodeAccess();
|
|
87
88
|
add(gate.mark, gate.text, gate.hint);
|
|
89
|
+
// ── CLI auto-update ──
|
|
90
|
+
const upd = await probeUpdateStatus();
|
|
91
|
+
add(upd.mark, upd.text, upd.hint);
|
|
88
92
|
// ── render ──
|
|
89
93
|
const door = resolveAnthropicDoor(env);
|
|
90
94
|
out("mnemom agent doctor");
|
|
@@ -34,10 +34,35 @@ export declare function defaultAgentSlug(): string;
|
|
|
34
34
|
* The default slug resolved from the authenticated IDENTITY: `agent-<handle>` from
|
|
35
35
|
* the login email fetched via `/v1/auth/me`. The CLI's stored OAuth token carries
|
|
36
36
|
* no email, so the sync `defaultAgentSlug()` alone falls back to "agent-user";
|
|
37
|
-
* this fetches the real email once a session exists.
|
|
38
|
-
*
|
|
37
|
+
* this fetches the real email once a session exists. Call it AFTER the session
|
|
38
|
+
* is ensured.
|
|
39
|
+
*
|
|
40
|
+
* Default (lenient): falls back to `defaultAgentSlug()` on any failure. With
|
|
41
|
+
* `strict` (an explicit `--setup` / `mnemom agent setup`) a handle that cannot
|
|
42
|
+
* be resolved THROWS instead: the gateway identity is hash(credential, name),
|
|
43
|
+
* so a silent fall-back to "agent-user" would provision and govern a different
|
|
44
|
+
* agent than the one a prior setup under the real handle created (MNE-7650).
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveDefaultSlug(opts?: {
|
|
47
|
+
strict?: boolean;
|
|
48
|
+
}): Promise<string>;
|
|
49
|
+
/**
|
|
50
|
+
* Thrown when a name-matched agent in the caller's org is NOT the agent the
|
|
51
|
+
* gateway serves for (credential, name). Both ids are in the message; the
|
|
52
|
+
* launcher treats it as fatal on an explicit `--setup` (MNE-7650).
|
|
39
53
|
*/
|
|
40
|
-
export declare
|
|
54
|
+
export declare class AgentIdentityMismatchError extends Error {
|
|
55
|
+
readonly slug: string;
|
|
56
|
+
/** The agent the org lookup found by name. */
|
|
57
|
+
readonly namedId: string;
|
|
58
|
+
/** The agent the gateway answered with on x-mnemom-agent. */
|
|
59
|
+
readonly servedId: string;
|
|
60
|
+
constructor(slug: string,
|
|
61
|
+
/** The agent the org lookup found by name. */
|
|
62
|
+
namedId: string,
|
|
63
|
+
/** The agent the gateway answered with on x-mnemom-agent. */
|
|
64
|
+
servedId: string);
|
|
65
|
+
}
|
|
41
66
|
export interface EnsureAgentOptions {
|
|
42
67
|
/** The already-resolved final agent slug (what rides as x-mnemom-agent). */
|
|
43
68
|
slug: string;
|
|
@@ -65,6 +90,12 @@ export interface EnsureAgentResult {
|
|
|
65
90
|
* setup`) or a warn-and-continue (a bare launch).
|
|
66
91
|
*/
|
|
67
92
|
export declare function ensureGovernedAgent(opts: EnsureAgentOptions): Promise<EnsureAgentResult>;
|
|
93
|
+
/**
|
|
94
|
+
* One line for an api failure that keeps the parts an operator can act on: the
|
|
95
|
+
* message, the error code, and the scope the endpoint said it needed
|
|
96
|
+
* (`details.required_scope`). Never the token, never the body.
|
|
97
|
+
*/
|
|
98
|
+
export declare function describeApiError(err: unknown): string;
|
|
68
99
|
/** `mnemom agent setup [--agent <slug>]` — the explicit provisioning entry point. */
|
|
69
100
|
export declare function agentSetupCommand(opts?: {
|
|
70
101
|
agent?: string;
|
|
@@ -51,21 +51,59 @@ export function defaultAgentSlug() {
|
|
|
51
51
|
* The default slug resolved from the authenticated IDENTITY: `agent-<handle>` from
|
|
52
52
|
* the login email fetched via `/v1/auth/me`. The CLI's stored OAuth token carries
|
|
53
53
|
* no email, so the sync `defaultAgentSlug()` alone falls back to "agent-user";
|
|
54
|
-
* this fetches the real email once a session exists.
|
|
55
|
-
*
|
|
54
|
+
* this fetches the real email once a session exists. Call it AFTER the session
|
|
55
|
+
* is ensured.
|
|
56
|
+
*
|
|
57
|
+
* Default (lenient): falls back to `defaultAgentSlug()` on any failure. With
|
|
58
|
+
* `strict` (an explicit `--setup` / `mnemom agent setup`) a handle that cannot
|
|
59
|
+
* be resolved THROWS instead: the gateway identity is hash(credential, name),
|
|
60
|
+
* so a silent fall-back to "agent-user" would provision and govern a different
|
|
61
|
+
* agent than the one a prior setup under the real handle created (MNE-7650).
|
|
56
62
|
*/
|
|
57
|
-
export async function resolveDefaultSlug() {
|
|
63
|
+
export async function resolveDefaultSlug(opts = {}) {
|
|
64
|
+
let reason;
|
|
58
65
|
try {
|
|
59
66
|
const email = (await getMe()).email ?? "";
|
|
60
67
|
const handle = sanitizeAgentName(email.split("@")[0] ?? "");
|
|
61
68
|
if (handle)
|
|
62
69
|
return `agent-${handle}`;
|
|
70
|
+
reason = email
|
|
71
|
+
? `login email '${email}' yields no usable handle`
|
|
72
|
+
: "no login email on this session";
|
|
63
73
|
}
|
|
64
|
-
catch {
|
|
65
|
-
|
|
74
|
+
catch (err) {
|
|
75
|
+
reason = `/v1/auth/me failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
76
|
+
}
|
|
77
|
+
if (opts.strict) {
|
|
78
|
+
throw new Error(`mnemom agent setup: couldn't derive your agent handle (${reason}) — pass --agent <slug>. ` +
|
|
79
|
+
"Setup won't silently fall back to 'agent-user': the gateway identity is hash(credential, name), so a different name is a different agent.");
|
|
66
80
|
}
|
|
67
81
|
return defaultAgentSlug();
|
|
68
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Thrown when a name-matched agent in the caller's org is NOT the agent the
|
|
85
|
+
* gateway serves for (credential, name). Both ids are in the message; the
|
|
86
|
+
* launcher treats it as fatal on an explicit `--setup` (MNE-7650).
|
|
87
|
+
*/
|
|
88
|
+
export class AgentIdentityMismatchError extends Error {
|
|
89
|
+
slug;
|
|
90
|
+
namedId;
|
|
91
|
+
servedId;
|
|
92
|
+
constructor(slug,
|
|
93
|
+
/** The agent the org lookup found by name. */
|
|
94
|
+
namedId,
|
|
95
|
+
/** The agent the gateway answered with on x-mnemom-agent. */
|
|
96
|
+
servedId) {
|
|
97
|
+
super(`mnemom agent setup: '${slug}' in your org is ${namedId}, but the gateway serves ${servedId} for this Anthropic key + name. ` +
|
|
98
|
+
"Refusing to govern the wrong agent. The gateway identity is hash(credential, name): a different key, " +
|
|
99
|
+
"or a prior setup under another slug, is a different agent. " +
|
|
100
|
+
`Either claim the served one (mnemom agent claim ${servedId} --key <the same key>) or launch with the --agent and key you set up.`);
|
|
101
|
+
this.slug = slug;
|
|
102
|
+
this.namedId = namedId;
|
|
103
|
+
this.servedId = servedId;
|
|
104
|
+
this.name = "AgentIdentityMismatchError";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
69
107
|
/**
|
|
70
108
|
* Ensure `slug` is a usable governed agent in the customer's org. Returns
|
|
71
109
|
* {provisioned:false} when it is already usable, when provisioning is not
|
|
@@ -87,7 +125,18 @@ export async function ensureGovernedAgent(opts) {
|
|
|
87
125
|
// already-claimed agent (e.g. a protection card that failed on a prior run
|
|
88
126
|
// before we minted its one-time grant). A bare launch leaves it alone.
|
|
89
127
|
if (opts.setup === true) {
|
|
90
|
-
|
|
128
|
+
// MNE-7650: the name is client-controlled and the API exposes no
|
|
129
|
+
// agent_hash, so a name match proves nothing about which agent the
|
|
130
|
+
// gateway will serve for (key, slug) — it looks agents up by
|
|
131
|
+
// hash(credential, name) only. Ask the gateway: the same 16-token
|
|
132
|
+
// "hello" the birth path uses answers with the served mnm- id on
|
|
133
|
+
// x-mnemom-agent. Govern that agent or none.
|
|
134
|
+
const gatewayUrl = opts.gatewayHost.replace(/\/+$/, "");
|
|
135
|
+
const served = await birthThroughGateway(gatewayUrl, "anthropic", slug, opts.anthropicKey);
|
|
136
|
+
if (served !== existing.id) {
|
|
137
|
+
throw new AgentIdentityMismatchError(slug, existing.id, served);
|
|
138
|
+
}
|
|
139
|
+
say(fmt.dim(`mnemom agent: '${slug}' is already claimed (${existing.id}, the agent the gateway serves) — refreshing its cards.`));
|
|
91
140
|
await publishDefaultCards(existing.id);
|
|
92
141
|
persistAgentSlug(slug);
|
|
93
142
|
return { slug, provisioned: true };
|
|
@@ -140,6 +189,25 @@ export async function ensureGovernedAgent(opts) {
|
|
|
140
189
|
return { slug, provisioned: true };
|
|
141
190
|
}
|
|
142
191
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
192
|
+
/**
|
|
193
|
+
* One line for an api failure that keeps the parts an operator can act on: the
|
|
194
|
+
* message, the error code, and the scope the endpoint said it needed
|
|
195
|
+
* (`details.required_scope`). Never the token, never the body.
|
|
196
|
+
*/
|
|
197
|
+
export function describeApiError(err) {
|
|
198
|
+
if (!(err instanceof MnemomApiError))
|
|
199
|
+
return err instanceof Error ? err.message : String(err);
|
|
200
|
+
const bits = [];
|
|
201
|
+
if (err.code)
|
|
202
|
+
bits.push(`code ${err.code}`);
|
|
203
|
+
const details = err.details;
|
|
204
|
+
const scope = details && typeof details === "object" && "required_scope" in details
|
|
205
|
+
? details.required_scope
|
|
206
|
+
: undefined;
|
|
207
|
+
if (typeof scope === "string" && scope)
|
|
208
|
+
bits.push(`requires ${scope}`);
|
|
209
|
+
return bits.length ? `${err.message} (${bits.join(", ")})` : err.message;
|
|
210
|
+
}
|
|
143
211
|
/**
|
|
144
212
|
* Write one card with a brief retry to absorb the org-membership propagation that
|
|
145
213
|
* can transiently 401/403/409 a write right after a claim (the same window
|
|
@@ -172,12 +240,18 @@ async function publishDefaultCards(agentId) {
|
|
|
172
240
|
// it with our human login JWT — the "Provision and claim?" confirm is the
|
|
173
241
|
// explicit human approval, and the mint endpoint accepts a human operator (only
|
|
174
242
|
// agent OAuth principals are rejected). The next write consumes the grant. A
|
|
175
|
-
// mint failure falls through to the write's retry + graceful degrade
|
|
243
|
+
// mint failure falls through to the write's retry + graceful degrade, but it
|
|
244
|
+
// is NAMED: the mint's error code (e.g. an ownership `agent_org_forbidden`)
|
|
245
|
+
// and `details.required_scope` are the actionable diagnosis — the downstream
|
|
246
|
+
// protection-write denial alone points at the wrong remedy (MNE-7650).
|
|
176
247
|
try {
|
|
177
248
|
await mintProtectionCapability(agentId);
|
|
178
249
|
}
|
|
179
|
-
catch {
|
|
180
|
-
|
|
250
|
+
catch (err) {
|
|
251
|
+
say(fmt.warn(`mnemom agent: couldn't mint the protection grant for ${agentId} — ${describeApiError(err)}`));
|
|
252
|
+
if (err instanceof MnemomApiError && err.code === "agent_org_forbidden") {
|
|
253
|
+
say(fmt.dim(" That agent is owned by an org other than your active one — `mnemom org use <org>` to switch, or set up under a different --agent."));
|
|
254
|
+
}
|
|
181
255
|
}
|
|
182
256
|
const prot = await tryPublishCard("protection", () => putProtectionCard(agentId, JSON.stringify(buildCodeProtectionCard(agentId)), "application/json"));
|
|
183
257
|
if (align.ok && prot.ok) {
|
|
@@ -209,10 +283,11 @@ export async function agentSetupCommand(opts = {}) {
|
|
|
209
283
|
throw err;
|
|
210
284
|
}
|
|
211
285
|
const anthropicKey = await resolveAnthropicKey(process.env, { allowPrompt: true });
|
|
212
|
-
// Session exists now, so derive the real handle from /v1/auth/me
|
|
286
|
+
// Session exists now, so derive the real handle from /v1/auth/me. Strict: an
|
|
287
|
+
// explicit setup never silently provisions "agent-user" (MNE-7650).
|
|
213
288
|
const slug = opts.agent?.trim()
|
|
214
289
|
? sanitizeAgentName(opts.agent.trim())
|
|
215
|
-
: await resolveDefaultSlug();
|
|
290
|
+
: await resolveDefaultSlug({ strict: true });
|
|
216
291
|
if (!slug)
|
|
217
292
|
throw new Error("mnemom agent setup: agent slug sanitised to empty — pass --agent <slug>.");
|
|
218
293
|
const result = await ensureGovernedAgent({ slug, anthropicKey, setup: true, gatewayHost });
|
package/dist/commands/agent.js
CHANGED
|
@@ -38,7 +38,7 @@ import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/l
|
|
|
38
38
|
import { fmt } from "../lib/format.js";
|
|
39
39
|
import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
40
40
|
import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
|
|
41
|
-
import { defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
|
|
41
|
+
import { AgentIdentityMismatchError, defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
|
|
42
42
|
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfigB64, resolveContextFold, resolveContextHandoff, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, } from "../lib/agent.js";
|
|
43
43
|
import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
|
|
44
44
|
import { slackChannelArgs } from "../lib/slack-launch.js";
|
|
@@ -215,9 +215,12 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
215
215
|
});
|
|
216
216
|
// Now that a session exists, a defaulted slug uses the real login handle from
|
|
217
217
|
// /v1/auth/me (the stored token has no email, so the pre-sign-in value was the
|
|
218
|
-
// "agent-user" fallback). Only when the caller didn't name an agent.
|
|
218
|
+
// "agent-user" fallback). Only when the caller didn't name an agent. Under an
|
|
219
|
+
// explicit --setup the lookup is strict and a missing handle aborts the launch:
|
|
220
|
+
// provisioning "agent-user" would govern a different gateway identity than the
|
|
221
|
+
// one a prior setup under the real handle created (MNE-7650).
|
|
219
222
|
if (!options.agent?.trim()) {
|
|
220
|
-
const refreshed = sanitizeAgentName(await resolveDefaultSlug());
|
|
223
|
+
const refreshed = sanitizeAgentName(await resolveDefaultSlug({ strict: options.setup === true }));
|
|
221
224
|
if (refreshed)
|
|
222
225
|
agent = refreshed;
|
|
223
226
|
}
|
|
@@ -236,6 +239,11 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
236
239
|
});
|
|
237
240
|
}
|
|
238
241
|
catch (err) {
|
|
242
|
+
// An identity mismatch is the one setup failure that must NOT degrade: the
|
|
243
|
+
// launch would run as an agent other than the one --setup just tried to
|
|
244
|
+
// govern. Refuse, with both ids in the message (MNE-7650).
|
|
245
|
+
if (err instanceof AgentIdentityMismatchError)
|
|
246
|
+
throw err;
|
|
239
247
|
const msg = err instanceof Error ? err.message : String(err);
|
|
240
248
|
say(fmt.warn(`mnemom agent: agent setup did not complete (${msg}) — launching with the unclaimed identity.`));
|
|
241
249
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom config list|get|set|unset` — CLI-wide preferences in
|
|
3
|
+
* ~/.mnemom/config.json. Today: the auto-update keys. (Launcher settings for
|
|
4
|
+
* `mnemom agent` stay in `mnemom agent config` / launcher.toml.)
|
|
5
|
+
*
|
|
6
|
+
* Keys are data (CONFIG_KEYS) so list/get/set/unset share one source of truth.
|
|
7
|
+
*/
|
|
8
|
+
import { type UpdatePrefs } from "../lib/update/decide.js";
|
|
9
|
+
import { setUpdatePref } from "../lib/cli-config.js";
|
|
10
|
+
export interface CliConfigKey {
|
|
11
|
+
key: string;
|
|
12
|
+
describe: string;
|
|
13
|
+
env: string;
|
|
14
|
+
/** Stored value (undefined when unset). */
|
|
15
|
+
stored(prefs: UpdatePrefs): string | undefined;
|
|
16
|
+
/** Value in effect after env + default. */
|
|
17
|
+
effective(prefs: UpdatePrefs, env: Record<string, string | undefined>): string;
|
|
18
|
+
/** Validate + persist; throws with a user-facing message on a bad value. */
|
|
19
|
+
set(raw: string, persist: typeof setUpdatePref): void;
|
|
20
|
+
unset(persist: typeof setUpdatePref): void;
|
|
21
|
+
}
|
|
22
|
+
export declare const CONFIG_KEYS: readonly CliConfigKey[];
|
|
23
|
+
export declare function findKey(key: string): CliConfigKey;
|
|
24
|
+
export interface ConfigDeps {
|
|
25
|
+
prefs(): UpdatePrefs;
|
|
26
|
+
env: Record<string, string | undefined>;
|
|
27
|
+
persist: typeof setUpdatePref;
|
|
28
|
+
log(line: string): void;
|
|
29
|
+
}
|
|
30
|
+
/** One line per key: `key = effective (source) — description`. */
|
|
31
|
+
export declare function configListCommand(opts?: {
|
|
32
|
+
json?: boolean;
|
|
33
|
+
}, deps?: ConfigDeps): void;
|
|
34
|
+
export declare function configGetCommand(key: string, deps?: ConfigDeps): void;
|
|
35
|
+
export declare function configSetCommand(key: string, value: string, deps?: ConfigDeps): void;
|
|
36
|
+
export declare function configUnsetCommand(key: string, deps?: ConfigDeps): void;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom config list|get|set|unset` — CLI-wide preferences in
|
|
3
|
+
* ~/.mnemom/config.json. Today: the auto-update keys. (Launcher settings for
|
|
4
|
+
* `mnemom agent` stay in `mnemom agent config` / launcher.toml.)
|
|
5
|
+
*
|
|
6
|
+
* Keys are data (CONFIG_KEYS) so list/get/set/unset share one source of truth.
|
|
7
|
+
*/
|
|
8
|
+
import { isUpdateChannel, isUpdateMode, resolveChannel, resolveMode, UPDATE_CHANNELS, UPDATE_MODES, } from "../lib/update/decide.js";
|
|
9
|
+
import { loadCliConfig, setUpdatePref } from "../lib/cli-config.js";
|
|
10
|
+
export const CONFIG_KEYS = [
|
|
11
|
+
{
|
|
12
|
+
key: "update.channel",
|
|
13
|
+
describe: `Release channel for auto-update: ${UPDATE_CHANNELS.join(" | ")} (next requires an invite)`,
|
|
14
|
+
env: "MNEMOM_UPDATE_CHANNEL",
|
|
15
|
+
stored: (p) => p.channel,
|
|
16
|
+
effective: (p, env) => resolveChannel(p, env),
|
|
17
|
+
set: (raw, persist) => {
|
|
18
|
+
const v = raw.trim();
|
|
19
|
+
if (!isUpdateChannel(v))
|
|
20
|
+
throw new Error(`update.channel must be one of ${UPDATE_CHANNELS.join(", ")} (got '${raw}').`);
|
|
21
|
+
persist("channel", v);
|
|
22
|
+
},
|
|
23
|
+
unset: (persist) => persist("channel", undefined),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: "update.mode",
|
|
27
|
+
describe: `Auto-update behaviour: ${UPDATE_MODES.join(" | ")}`,
|
|
28
|
+
env: "MNEMOM_UPDATE_MODE",
|
|
29
|
+
stored: (p) => p.mode,
|
|
30
|
+
effective: (p, env) => resolveMode(p, env),
|
|
31
|
+
set: (raw, persist) => {
|
|
32
|
+
const v = raw.trim();
|
|
33
|
+
if (!isUpdateMode(v))
|
|
34
|
+
throw new Error(`update.mode must be one of ${UPDATE_MODES.join(", ")} (got '${raw}').`);
|
|
35
|
+
persist("mode", v);
|
|
36
|
+
},
|
|
37
|
+
unset: (persist) => persist("mode", undefined),
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
export function findKey(key) {
|
|
41
|
+
const spec = CONFIG_KEYS.find((k) => k.key === key);
|
|
42
|
+
if (!spec)
|
|
43
|
+
throw new Error(`Unknown config key '${key}'. Known keys: ${CONFIG_KEYS.map((k) => k.key).join(", ")}`);
|
|
44
|
+
return spec;
|
|
45
|
+
}
|
|
46
|
+
function defaultDeps() {
|
|
47
|
+
return {
|
|
48
|
+
prefs: () => loadCliConfig().update ?? {},
|
|
49
|
+
env: process.env,
|
|
50
|
+
persist: setUpdatePref,
|
|
51
|
+
log: (line) => console.log(line),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** One line per key: `key = effective (source) — description`. */
|
|
55
|
+
export function configListCommand(opts = {}, deps = defaultDeps()) {
|
|
56
|
+
const prefs = deps.prefs();
|
|
57
|
+
const rows = CONFIG_KEYS.map((k) => {
|
|
58
|
+
const stored = k.stored(prefs);
|
|
59
|
+
const source = deps.env[k.env] ? `env ${k.env}` : stored !== undefined ? "config" : "default";
|
|
60
|
+
return { key: k.key, value: k.effective(prefs, deps.env), source, describe: k.describe };
|
|
61
|
+
});
|
|
62
|
+
if (opts.json) {
|
|
63
|
+
deps.log(JSON.stringify(rows, null, 2));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const r of rows)
|
|
67
|
+
deps.log(`${r.key} = ${r.value} (${r.source}) — ${r.describe}`);
|
|
68
|
+
}
|
|
69
|
+
export function configGetCommand(key, deps = defaultDeps()) {
|
|
70
|
+
deps.log(findKey(key).effective(deps.prefs(), deps.env));
|
|
71
|
+
}
|
|
72
|
+
export function configSetCommand(key, value, deps = defaultDeps()) {
|
|
73
|
+
const spec = findKey(key);
|
|
74
|
+
spec.set(value, deps.persist);
|
|
75
|
+
deps.log(`${key} = ${value.trim()}`);
|
|
76
|
+
if (deps.env[spec.env])
|
|
77
|
+
deps.log(`note: ${spec.env} is set in your environment and overrides this.`);
|
|
78
|
+
}
|
|
79
|
+
export function configUnsetCommand(key, deps = defaultDeps()) {
|
|
80
|
+
findKey(key).unset(deps.persist);
|
|
81
|
+
deps.log(`${key} reset to default`);
|
|
82
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom update [--check] [--channel latest|next] [--force] [--json]`
|
|
3
|
+
*
|
|
4
|
+
* The foreground twin of the background worker: same `runUpdate` pass, but it
|
|
5
|
+
* reports what happened and ignores the CI / non-TTY auto-off (an explicit
|
|
6
|
+
* command is consent). `--check` never installs; `--force` permits a downgrade
|
|
7
|
+
* to the channel's version (e.g. leaving next for latest).
|
|
8
|
+
*/
|
|
9
|
+
import { type ResolvedSettings } from "../lib/update/decide.js";
|
|
10
|
+
import { type RunDeps, type RunResult } from "../lib/update/run.js";
|
|
11
|
+
export declare const NEXT_NOT_ENTITLED_HINT: string;
|
|
12
|
+
export interface UpdateOptions {
|
|
13
|
+
check?: boolean;
|
|
14
|
+
channel?: string;
|
|
15
|
+
force?: boolean;
|
|
16
|
+
json?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Validate flags → the settings for this foreground pass, or an error message. */
|
|
19
|
+
export declare function foregroundSettings(opts: UpdateOptions, prefs?: import("../lib/update/decide.js").UpdatePrefs, env?: Record<string, string | undefined>): ResolvedSettings | string;
|
|
20
|
+
/** Human-readable report of one pass. Pure, for testing. */
|
|
21
|
+
export declare function formatUpdateResult(result: RunResult, current: string): string[];
|
|
22
|
+
/** Exit status: non-zero only when something actually failed. */
|
|
23
|
+
export declare function updateExitCode(result: RunResult): number;
|
|
24
|
+
export declare function updateCommand(opts: UpdateOptions, deps?: {
|
|
25
|
+
settings?: ResolvedSettings | string;
|
|
26
|
+
acquire?: () => (() => void) | null;
|
|
27
|
+
runDeps?: RunDeps;
|
|
28
|
+
log?: (line: string) => void;
|
|
29
|
+
setExit?: (code: number) => void;
|
|
30
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom update [--check] [--channel latest|next] [--force] [--json]`
|
|
3
|
+
*
|
|
4
|
+
* The foreground twin of the background worker: same `runUpdate` pass, but it
|
|
5
|
+
* reports what happened and ignores the CI / non-TTY auto-off (an explicit
|
|
6
|
+
* command is consent). `--check` never installs; `--force` permits a downgrade
|
|
7
|
+
* to the channel's version (e.g. leaving next for latest).
|
|
8
|
+
*/
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { CLI_VERSION } from "../version.js";
|
|
11
|
+
import { canSelfUpdate, isUpdateChannel, manualUpgradeCommand, resolveChannel, UPDATE_CHANNELS, } from "../lib/update/decide.js";
|
|
12
|
+
import { runUpdate } from "../lib/update/run.js";
|
|
13
|
+
import { acquireLock } from "../lib/update/state.js";
|
|
14
|
+
import { entryRealpath, foregroundCredential, loadUpdatePrefs } from "../lib/update/system.js";
|
|
15
|
+
import { realRunDeps } from "../lib/update/worker.js";
|
|
16
|
+
export const NEXT_NOT_ENTITLED_HINT = "The next channel is available to invited accounts. Ask your Mnemom contact for an invite " +
|
|
17
|
+
"(and make sure you're logged in: mnemom login). Using latest for now.";
|
|
18
|
+
/** Validate flags → the settings for this foreground pass, or an error message. */
|
|
19
|
+
export function foregroundSettings(opts, prefs = loadUpdatePrefs(), env = process.env) {
|
|
20
|
+
if (opts.channel !== undefined && !isUpdateChannel(opts.channel)) {
|
|
21
|
+
return `--channel must be one of ${UPDATE_CHANNELS.join(", ")} (got '${opts.channel}').`;
|
|
22
|
+
}
|
|
23
|
+
const channel = resolveChannel(prefs, env, opts.channel);
|
|
24
|
+
return { channel, mode: opts.check ? "notify" : "auto" };
|
|
25
|
+
}
|
|
26
|
+
/** Human-readable report of one pass. Pure, for testing. */
|
|
27
|
+
export function formatUpdateResult(result, current) {
|
|
28
|
+
const lines = [];
|
|
29
|
+
if (result.channelDowngraded)
|
|
30
|
+
lines.push(chalk.yellow(NEXT_NOT_ENTITLED_HINT));
|
|
31
|
+
const action = result.action;
|
|
32
|
+
if (!action) {
|
|
33
|
+
lines.push(chalk.red(`✖ Could not check for updates: ${result.error ?? "unknown error"}`));
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
36
|
+
const manual = (v) => manualUpgradeCommand(result.method ?? "unknown", v);
|
|
37
|
+
if (result.installed) {
|
|
38
|
+
lines.push(chalk.green(`✓ Updated mnemom ${current} → ${result.installed} (${result.channel})`));
|
|
39
|
+
}
|
|
40
|
+
else if (action.kind === "install") {
|
|
41
|
+
// An install action that didn't produce `installed` failed.
|
|
42
|
+
lines.push(chalk.red(`✖ Update to ${action.version} failed: ${result.error ?? "unknown error"}`));
|
|
43
|
+
lines.push(` Try manually: ${manual(action.version)}`);
|
|
44
|
+
}
|
|
45
|
+
else if (action.kind === "notify") {
|
|
46
|
+
lines.push(`mnemom ${action.version} is available on ${result.channel} (you have ${current}).`);
|
|
47
|
+
lines.push(` Run: ${canSelfUpdate(result.method ?? "unknown") ? "mnemom update" : manual(action.version)}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
lines.push(`mnemom ${current}: ${action.reason} (${result.channel}).`);
|
|
51
|
+
}
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
/** Exit status: non-zero only when something actually failed. */
|
|
55
|
+
export function updateExitCode(result) {
|
|
56
|
+
return result.error ? 1 : 0;
|
|
57
|
+
}
|
|
58
|
+
export async function updateCommand(opts, deps = {}) {
|
|
59
|
+
const log = deps.log ?? ((line) => console.log(line));
|
|
60
|
+
const setExit = deps.setExit ?? ((code) => void (process.exitCode = code));
|
|
61
|
+
const settings = deps.settings ?? foregroundSettings(opts);
|
|
62
|
+
if (typeof settings === "string") {
|
|
63
|
+
log(chalk.red(settings));
|
|
64
|
+
setExit(2);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const release = (deps.acquire ?? (() => acquireLock()))();
|
|
68
|
+
if (!release) {
|
|
69
|
+
log("Another mnemom update is already running — try again in a minute.");
|
|
70
|
+
setExit(1);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
if (!opts.check && !opts.json)
|
|
75
|
+
log(`Checking the ${settings.channel} channel…`);
|
|
76
|
+
const runDeps = deps.runDeps ?? realRunDeps(entryRealpath(process.argv[1]), () => foregroundCredential());
|
|
77
|
+
const result = await runUpdate(runDeps, settings, { force: opts.force, foreground: true });
|
|
78
|
+
if (opts.json) {
|
|
79
|
+
log(JSON.stringify({ current: CLI_VERSION, requested_channel: settings.channel, ...result }, null, 2));
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
for (const line of formatUpdateResult(result, runDeps.current))
|
|
83
|
+
log(line);
|
|
84
|
+
}
|
|
85
|
+
setExit(updateExitCode(result));
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
release();
|
|
89
|
+
}
|
|
90
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,10 @@ import { wrapCommand } from "./commands/wrap.js";
|
|
|
34
34
|
import { onboardCommand } from "./commands/onboard.js";
|
|
35
35
|
import { skillsListCommand, skillsDescribeCommand } from "./commands/skills.js";
|
|
36
36
|
import { usageCommand, parseNumericFlag, parseDaysFlag } from "./commands/usage.js";
|
|
37
|
+
import { updateCommand } from "./commands/update.js";
|
|
38
|
+
import { runAction } from "./lib/update/action.js";
|
|
39
|
+
import { configListCommand, configGetCommand, configSetCommand, configUnsetCommand, } from "./commands/config.js";
|
|
40
|
+
import { defaultStartupDeps, maybeRunUpdateHook } from "./lib/update/startup.js";
|
|
37
41
|
program
|
|
38
42
|
.name("mnemom")
|
|
39
43
|
.description("Transparent AI agent tracing")
|
|
@@ -1876,6 +1880,42 @@ program
|
|
|
1876
1880
|
process.exit(1);
|
|
1877
1881
|
}
|
|
1878
1882
|
});
|
|
1883
|
+
// ============================================================================
|
|
1884
|
+
// update + config — CLI auto-update (channel latest|next, mode auto|notify|off)
|
|
1885
|
+
//
|
|
1886
|
+
// A detached worker (lib/update/startup.ts → update-worker.ts) checks npm at
|
|
1887
|
+
// most daily (6h on next) and installs in the background; the new version takes
|
|
1888
|
+
// effect on the next run. `mnemom update` is the foreground twin. Preferences
|
|
1889
|
+
// live in ~/.mnemom/config.json via `mnemom config`.
|
|
1890
|
+
// ============================================================================
|
|
1891
|
+
program
|
|
1892
|
+
.command("update")
|
|
1893
|
+
.description("Check for and install a newer mnemom CLI (auto-update runs this in the background)")
|
|
1894
|
+
.option("--check", "Only report whether an update is available; never install")
|
|
1895
|
+
.option("--channel <channel>", "Channel for this run: latest | next (default: update.channel)")
|
|
1896
|
+
.option("--force", "Allow installing an OLDER version (e.g. moving from next back to latest)")
|
|
1897
|
+
.option("--json", "Output JSON")
|
|
1898
|
+
.action((options) => runAction(() => updateCommand(options)));
|
|
1899
|
+
const configCmd = program
|
|
1900
|
+
.command("config")
|
|
1901
|
+
.description("CLI-wide preferences in ~/.mnemom/config.json (auto-update channel and mode)");
|
|
1902
|
+
configCmd
|
|
1903
|
+
.command("list")
|
|
1904
|
+
.description("Show every setting, its effective value, and where it came from")
|
|
1905
|
+
.option("--json", "Output JSON")
|
|
1906
|
+
.action((options) => runAction(() => configListCommand(options)));
|
|
1907
|
+
configCmd
|
|
1908
|
+
.command("get <key>")
|
|
1909
|
+
.description("Print a setting's effective value (e.g. update.channel)")
|
|
1910
|
+
.action((key) => runAction(() => configGetCommand(key)));
|
|
1911
|
+
configCmd
|
|
1912
|
+
.command("set <key> <value>")
|
|
1913
|
+
.description("Set a preference, e.g. `mnemom config set update.channel next`")
|
|
1914
|
+
.action((key, value) => runAction(() => configSetCommand(key, value)));
|
|
1915
|
+
configCmd
|
|
1916
|
+
.command("unset <key>")
|
|
1917
|
+
.description("Reset a preference to its default")
|
|
1918
|
+
.action((key) => runAction(() => configUnsetCommand(key)));
|
|
1879
1919
|
// Export the fully-assembled commander program so tooling (e.g. the
|
|
1880
1920
|
// command-tree snapshot generator in scripts/gen-command-tree.mjs) can
|
|
1881
1921
|
// statically introspect the command surface WITHOUT executing the CLI.
|
|
@@ -1897,6 +1937,7 @@ export function runEntrypoint(argv = process.argv.slice(2)) {
|
|
|
1897
1937
|
});
|
|
1898
1938
|
}
|
|
1899
1939
|
else {
|
|
1940
|
+
maybeRunUpdateHook(defaultStartupDeps(argv));
|
|
1900
1941
|
program.parse();
|
|
1901
1942
|
}
|
|
1902
1943
|
}
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -24,15 +24,17 @@ export declare const PROD_GATEWAY_HOST = "https://gateway.mnemom.ai";
|
|
|
24
24
|
/**
|
|
25
25
|
* The sealed-contract shape sent as `x-mnemom-contract` and re-parsed by the
|
|
26
26
|
* gateway's goal-contract hook (gateway/src/goal-contract.ts `SealedContract`).
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* The three list fields are ALWAYS present (empty when no flag was given) —
|
|
28
|
+
* that is the shape the gateway's `parseContractHeader` materialises before it
|
|
29
|
+
* hashes, so the sha we print is the sha it seals (MNE-7649). An empty
|
|
30
|
+
* `goal_id` is omitted, and the guardrail ceilings are plain finite numbers.
|
|
29
31
|
*/
|
|
30
32
|
export interface SealedContract {
|
|
31
33
|
goal_id?: string;
|
|
32
34
|
statement: string;
|
|
33
|
-
requirements
|
|
34
|
-
allowed_paths
|
|
35
|
-
forbidden_paths
|
|
35
|
+
requirements: string[];
|
|
36
|
+
allowed_paths: string[];
|
|
37
|
+
forbidden_paths: string[];
|
|
36
38
|
/** Guardrail ceilings — positive finite numbers, sealed into the contract hash. */
|
|
37
39
|
max_turns?: number;
|
|
38
40
|
budget_usd?: number;
|
|
@@ -140,8 +142,12 @@ export interface SealedContractEnvelope {
|
|
|
140
142
|
* - a guardrail ceiling that is not strictly positive (`parseContractHeader`
|
|
141
143
|
* drops those, so a "guarded" launch would actually be unguarded).
|
|
142
144
|
*
|
|
143
|
-
* The canonical form
|
|
144
|
-
*
|
|
145
|
+
* The canonical form always carries `requirements`, `allowed_paths` and
|
|
146
|
+
* `forbidden_paths` (empty arrays when unset) and omits an empty goal_id —
|
|
147
|
+
* exactly what the gateway's `parseContractHeader` normalises to before
|
|
148
|
+
* `canonicalHash`, so the sha printed here is the `x-mnemom-contract-hash` it
|
|
149
|
+
* returns. (Omitting the empty arrays made every `--goal`-only launch print a
|
|
150
|
+
* sha the gateway never sealed: MNE-7649.)
|
|
145
151
|
*/
|
|
146
152
|
export declare function assembleContract(flags: ContractFlags): SealedContractEnvelope | null;
|
|
147
153
|
/** The request header that opts a conversation into implicit goal mode. */
|
package/dist/lib/agent.js
CHANGED
|
@@ -159,8 +159,12 @@ function hasText(v) {
|
|
|
159
159
|
* - a guardrail ceiling that is not strictly positive (`parseContractHeader`
|
|
160
160
|
* drops those, so a "guarded" launch would actually be unguarded).
|
|
161
161
|
*
|
|
162
|
-
* The canonical form
|
|
163
|
-
*
|
|
162
|
+
* The canonical form always carries `requirements`, `allowed_paths` and
|
|
163
|
+
* `forbidden_paths` (empty arrays when unset) and omits an empty goal_id —
|
|
164
|
+
* exactly what the gateway's `parseContractHeader` normalises to before
|
|
165
|
+
* `canonicalHash`, so the sha printed here is the `x-mnemom-contract-hash` it
|
|
166
|
+
* returns. (Omitting the empty arrays made every `--goal`-only launch print a
|
|
167
|
+
* sha the gateway never sealed: MNE-7649.)
|
|
164
168
|
*/
|
|
165
169
|
export function assembleContract(flags) {
|
|
166
170
|
// Implicit goal mode seals nothing: the ceilings ride on the mode header
|
|
@@ -176,16 +180,12 @@ export function assembleContract(flags) {
|
|
|
176
180
|
throw new Error("--goal is required when any contract flag " +
|
|
177
181
|
"(--requirement/--allow/--forbid/--goal-id/--budget/--max-turns/--stall) is given.");
|
|
178
182
|
}
|
|
179
|
-
const contract = {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
contract.allowed_paths = allowed;
|
|
186
|
-
const forbidden = cleanList(flags.forbid);
|
|
187
|
-
if (forbidden.length)
|
|
188
|
-
contract.forbidden_paths = forbidden;
|
|
183
|
+
const contract = {
|
|
184
|
+
statement,
|
|
185
|
+
requirements: cleanList(flags.requirement),
|
|
186
|
+
allowed_paths: cleanList(flags.allow),
|
|
187
|
+
forbidden_paths: cleanList(flags.forbid),
|
|
188
|
+
};
|
|
189
189
|
const goalId = (flags.goalId ?? "").trim();
|
|
190
190
|
if (goalId)
|
|
191
191
|
contract.goal_id = goalId;
|