@mnemom/mnemom 0.17.2-next.6 → 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.
Files changed (53) hide show
  1. package/dist/commands/agent-doctor.d.ts +1 -1
  2. package/dist/commands/agent-doctor.js +5 -1
  3. package/dist/commands/agent-setup.d.ts +34 -3
  4. package/dist/commands/agent-setup.js +86 -11
  5. package/dist/commands/agent-slack.d.ts +1 -0
  6. package/dist/commands/agent-slack.js +33 -4
  7. package/dist/commands/agent.js +11 -3
  8. package/dist/commands/config.d.ts +36 -0
  9. package/dist/commands/config.js +82 -0
  10. package/dist/commands/update.d.ts +30 -0
  11. package/dist/commands/update.js +90 -0
  12. package/dist/index.js +41 -0
  13. package/dist/lib/agent.d.ts +13 -7
  14. package/dist/lib/agent.js +12 -12
  15. package/dist/lib/cli-config.d.ts +8 -0
  16. package/dist/lib/cli-config.js +13 -0
  17. package/dist/lib/slack-channel.d.ts +3 -0
  18. package/dist/lib/slack-channel.js +78 -5
  19. package/dist/lib/slack-route.d.ts +5 -0
  20. package/dist/lib/slack-router.js +28 -2
  21. package/dist/lib/slack-state.d.ts +8 -0
  22. package/dist/lib/slack-state.js +11 -0
  23. package/dist/lib/slack.d.ts +38 -0
  24. package/dist/lib/slack.js +69 -0
  25. package/dist/lib/update/action.d.ts +9 -0
  26. package/dist/lib/update/action.js +17 -0
  27. package/dist/lib/update/decide.d.ts +152 -0
  28. package/dist/lib/update/decide.js +195 -0
  29. package/dist/lib/update/doctor.d.ts +18 -0
  30. package/dist/lib/update/doctor.js +63 -0
  31. package/dist/lib/update/install-method.d.ts +12 -0
  32. package/dist/lib/update/install-method.js +33 -0
  33. package/dist/lib/update/min-version.d.ts +34 -0
  34. package/dist/lib/update/min-version.js +69 -0
  35. package/dist/lib/update/registry.d.ts +27 -0
  36. package/dist/lib/update/registry.js +50 -0
  37. package/dist/lib/update/run.d.ts +54 -0
  38. package/dist/lib/update/run.js +100 -0
  39. package/dist/lib/update/semver.d.ts +22 -0
  40. package/dist/lib/update/semver.js +65 -0
  41. package/dist/lib/update/startup.d.ts +27 -0
  42. package/dist/lib/update/startup.js +75 -0
  43. package/dist/lib/update/state.d.ts +23 -0
  44. package/dist/lib/update/state.js +83 -0
  45. package/dist/lib/update/system.d.ts +55 -0
  46. package/dist/lib/update/system.js +104 -0
  47. package/dist/lib/update/worker.d.ts +42 -0
  48. package/dist/lib/update/worker.js +104 -0
  49. package/dist/lib/version-gate.d.ts +2 -1
  50. package/dist/lib/version-gate.js +9 -1
  51. package/dist/update-worker.d.ts +6 -0
  52. package/dist/update-worker.js +8 -0
  53. 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
- * and claude.ai login (informational — Remote Control needs it).
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
- * and claude.ai login (informational — Remote Control needs it).
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. Falls back to
38
- * `defaultAgentSlug()` on any failure. Call it AFTER the session is ensured.
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 function resolveDefaultSlug(): Promise<string>;
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. Falls back to
55
- * `defaultAgentSlug()` on any failure. Call it AFTER the session is ensured.
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
- // fall through to the token-based best-effort
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
- say(fmt.dim(`mnemom agent: '${slug}' is already claimed — refreshing its cards.`));
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
- // best effort — the protection write below surfaces the real, actionable error
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 (not "agent-user").
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 });
@@ -7,6 +7,7 @@
7
7
  * mnemom agent slack logout # remove them
8
8
  * mnemom agent slack status # tokens, bot identity, router, live sessions
9
9
  * mnemom agent slack post [--channel C] [--thread TS] [--icon-emoji E] <text…|->
10
+ * mnemom agent slack delete [--channel C] <ts> # delete a message THIS session posted
10
11
  * mnemom agent slack inbox [--peek] # unread messages addressed to THIS session
11
12
  * mnemom agent slack listen # this session's channel server (spawned by --slack)
12
13
  * mnemom agent slack router # the per-operator Socket Mode router (foreground)
@@ -7,6 +7,7 @@
7
7
  * mnemom agent slack logout # remove them
8
8
  * mnemom agent slack status # tokens, bot identity, router, live sessions
9
9
  * mnemom agent slack post [--channel C] [--thread TS] [--icon-emoji E] <text…|->
10
+ * mnemom agent slack delete [--channel C] <ts> # delete a message THIS session posted
10
11
  * mnemom agent slack inbox [--peek] # unread messages addressed to THIS session
11
12
  * mnemom agent slack listen # this session's channel server (spawned by --slack)
12
13
  * mnemom agent slack router # the per-operator Socket Mode router (foreground)
@@ -20,10 +21,10 @@ import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/l
20
21
  import { runChannelServer } from "../lib/slack-channel.js";
21
22
  import { mergeStore, readStore } from "../lib/launcher-store.js";
22
23
  import { askInput, isInteractive } from "../lib/prompt.js";
23
- import { explainSlackError, looksLikeAppToken, looksLikeBotToken, postMessage, resolveSlackToken, sessionPosterName, slackAppManifest, slackCall, SLACK_APP_TOKEN_STORE_KEY, SLACK_TOKEN_STORE_KEY, } from "../lib/slack.js";
24
+ import { explainSlackError, looksLikeAppToken, looksLikeBotToken, deleteMessage, postMessage, resolveSlackToken, sessionPosterName, slackAppManifest, slackCall, SLACK_APP_TOKEN_STORE_KEY, SLACK_TOKEN_STORE_KEY, } from "../lib/slack.js";
24
25
  import { routerStatus, runRouter } from "../lib/slack-router.js";
25
26
  import { normSession } from "../lib/slack-route.js";
26
- import { liveSessions, readInboxFrom, recordPost, slackDir } from "../lib/slack-state.js";
27
+ import { findOwnPost, liveSessions, readInboxFrom, recordPost, slackDir, } from "../lib/slack-state.js";
27
28
  const out = (line = "") => console.log(line);
28
29
  /** Entry point for `mnemom agent slack <verb> [args...]`. Throws on misuse. */
29
30
  export async function agentSlackCommand(argv) {
@@ -45,6 +46,8 @@ export async function agentSlackCommand(argv) {
45
46
  return status();
46
47
  case "post":
47
48
  return post(rest);
49
+ case "delete":
50
+ return del(rest);
48
51
  case "inbox":
49
52
  return inbox(rest);
50
53
  case "router":
@@ -52,7 +55,7 @@ export async function agentSlackCommand(argv) {
52
55
  case "listen":
53
56
  return listen();
54
57
  default:
55
- throw new Error(`mnemom agent slack: unknown verb '${verb}'. Use manifest | login | logout | status | post | inbox | listen | router.`);
58
+ throw new Error(`mnemom agent slack: unknown verb '${verb}'. Use manifest | login | logout | status | post | delete | inbox | listen | router.`);
56
59
  }
57
60
  }
58
61
  function defaultAppName() {
@@ -170,6 +173,28 @@ async function post(argv) {
170
173
  recordPost({ session, channel: res.channel, ts: res.ts, thread_ts: args.thread });
171
174
  out(JSON.stringify({ ok: true, channel: res.channel, ts: res.ts, as: session ?? null }));
172
175
  }
176
+ /** Delete a message THIS session posted (ownership from the local post index). */
177
+ async function del(argv) {
178
+ let channel;
179
+ let ts;
180
+ for (let i = 0; i < argv.length; i++) {
181
+ if (argv[i] === "--channel" || argv[i] === "-c")
182
+ channel = argv[++i];
183
+ else
184
+ ts = argv[i];
185
+ }
186
+ if (!ts)
187
+ throw new Error("mnemom agent slack delete: pass the message ts to delete.");
188
+ const session = sessionPosterName(process.env);
189
+ if (!session)
190
+ throw new Error("mnemom agent slack delete: not inside a `mnemom agent` session.");
191
+ const own = findOwnPost(session, ts, channel);
192
+ if (!own) {
193
+ throw new Error(`mnemom agent slack delete: ts=${ts} is not a message ${session} posted.`);
194
+ }
195
+ await deleteMessage({ token: botToken(), channel: own.channel, ts: own.ts });
196
+ out(JSON.stringify({ ok: true, deleted: own.ts, channel: own.channel }));
197
+ }
173
198
  // ── inbox (pull) ─────────────────────────────────────────────────────────────
174
199
  // Normalised like the inbox file name, so a session name can never escape offsets/.
175
200
  const offsetFile = (session) => join(slackDir(), "offsets", `${normSession(session)}.off`);
@@ -208,5 +233,9 @@ async function listen() {
208
233
  const session = sessionPosterName(process.env);
209
234
  if (!session)
210
235
  throw new Error("mnemom agent slack listen: not inside a `mnemom agent` session.");
211
- await runChannelServer({ session, botToken: botToken() });
236
+ await runChannelServer({
237
+ session,
238
+ botToken: botToken(),
239
+ allow: loadLauncherConfig().slackAllow ?? [],
240
+ });
212
241
  }
@@ -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
+ }