@mnemom/mnemom 0.14.6 → 0.15.1-next.0

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.
@@ -0,0 +1,374 @@
1
+ /**
2
+ * `mnemom try-me <token>` — the universal, deterministic Dojo-onboarding runner
3
+ * (MNE-934, epic MNE-931).
4
+ *
5
+ * Executes the shipped v5.0 briefing manifest's flow so an arbitrary agent/human
6
+ * on a fresh machine doesn't have to improvise it: resolve → name → be born →
7
+ * hand off (human claims in the browser) → set alignment → open a one-time grant
8
+ * (human approves in the browser) → set protection → hand off to the Dojo.
9
+ *
10
+ * DEMO-SAFE BY CONSTRUCTION:
11
+ * - Additive: a NEW verb; it modifies no existing command and touches no
12
+ * backend/website. It consumes Alex's shipped endpoints READ-ONLY (resolve,
13
+ * gateway birth, public claim poll) + the canonical card-write client.
14
+ * - The two privileged human acts stay in the human's browser: the claim
15
+ * sign-in (step 4) and the one-time protection grant (step 6). The command
16
+ * only orchestrates + polls — it never forges the human's session.
17
+ *
18
+ * AUTH MODEL (see the runner report / MNE-934): the canonical card-write PUTs
19
+ * (`/v1/{alignment,protection}/agent/:id`) authorize on the human's ORG
20
+ * MEMBERSHIP (ADR-062) and accept the human's `mnemom login` JWT or
21
+ * `MNEMOM_API_KEY` — NOT the birth token. So before the card writes the runner
22
+ * ensures a CLI session (offering `mnemom login`, one-click after the claim
23
+ * sign-in). That session IS the human's own — it is the headless twin of the
24
+ * host-connector authorization in the MCP flow, not a forged credential.
25
+ */
26
+ import { resolveBriefing, birthAgent, fetchAgentClaimed, buildClaimUrl, buildGrantUrl, buildDojoDeepLink, claimProof, isTokenMode, looksLikeTryMeToken, sleep, } from "../lib/try-me.js";
27
+ import { putAlignmentCard, putProtectionCard, MnemomApiError } from "../lib/api.js";
28
+ import { resolveAuth, loginWithBrowser, loginWithDeviceFlow } from "../lib/auth.js";
29
+ import { openBrowser } from "../lib/oauth.js";
30
+ import { askSelect, askInput, askYesNo, isInteractive } from "../lib/prompt.js";
31
+ import { getApiUrl } from "../lib/config.js";
32
+ import { fmt } from "../lib/format.js";
33
+ const POLL_INTERVAL_MS = 3000;
34
+ /**
35
+ * Entry point for `mnemom try-me <token>`. Throws on fatal misconfiguration
36
+ * (caught by the index.ts wrapper, which prints + exits non-zero); soft failures
37
+ * are surfaced as human guidance.
38
+ */
39
+ export async function tryMeCommand(token, options = {}) {
40
+ const json = !!options.json;
41
+ // JSON output is only coherent non-interactively — no prompts can be shown.
42
+ const nonInteractive = !!options.yes || json || !isInteractive();
43
+ // The name pick is a deliberate human-handoff checkpoint (the manifest marks
44
+ // it human_handoff:true). It must fire on EVERY real run — agent-driven,
45
+ // piped, and inherited-stdin runs all report isTTY=false yet can still answer
46
+ // a prompt — so it is auto-skipped ONLY on an explicit non-interactive
47
+ // request: --yes, --json, or --name (the last handled inside pickName). It is
48
+ // intentionally NOT gated on isInteractive(), unlike the open/login prompts.
49
+ const skipNamePrompt = !!options.yes || json;
50
+ const autoOpen = options.open !== false; // --no-open → false
51
+ const result = { token, version: "", dry_run: !!options.dryRun, steps: [] };
52
+ // A human-facing log that is silenced in --json mode (the JSON is the output).
53
+ const say = (line = "") => {
54
+ if (!json)
55
+ console.log(line);
56
+ };
57
+ if (!looksLikeTryMeToken(token)) {
58
+ throw new Error(`'${token}' doesn't look like a try-me token (expected e.g. tryme_…). ` +
59
+ "Copy the token from your Dojo /try-me invite.");
60
+ }
61
+ // ── State: resolve ────────────────────────────────────────────────────────
62
+ say(fmt.header("Mnemom Dojo — try-me"));
63
+ say();
64
+ say(fmt.dim(`Resolving briefing for token ${token}…`));
65
+ const manifest = await resolveBriefing(token, { apiBase: options.api });
66
+ result.version = manifest.version;
67
+ result.steps.push({ step: "resolve", status: "ok", detail: `manifest v${manifest.version}` });
68
+ if (options.dryRun) {
69
+ if (json) {
70
+ console.log(JSON.stringify(buildDryRunJson(manifest), null, 2));
71
+ }
72
+ else {
73
+ console.log(renderPlan(manifest));
74
+ }
75
+ return;
76
+ }
77
+ const apiBase = (options.api ?? getApiUrl()).replace(/\/$/, "");
78
+ // ── State: name (human checkpoint) ──────────────────────────────────────────
79
+ const name = await pickName(manifest, options, skipNamePrompt, say);
80
+ result.steps.push({ step: "name", status: "ok", detail: name });
81
+ // ── State: birth ────────────────────────────────────────────────────────────
82
+ let agentId;
83
+ if (options.agent) {
84
+ agentId = options.agent;
85
+ result.steps.push({ step: "birth", status: "skipped", detail: `using --resume ${agentId}` });
86
+ say(fmt.dim(`Skipping birth — resuming with agent ${agentId}.`));
87
+ }
88
+ else {
89
+ say();
90
+ say(`${fmt.badge("birth", "cyan")} Registering your identity with Mnemom (your first model call)…`);
91
+ const born = await birthAgent(manifest, name);
92
+ agentId = born.agentId;
93
+ result.agent_id = agentId;
94
+ result.steps.push({ step: "birth", status: "ok", detail: agentId });
95
+ say(fmt.success(`Born as ${fmt.badge(name, "magenta")} → ${agentId}`));
96
+ }
97
+ result.agent_id = agentId;
98
+ // ── State: claim (human handoff, then poll) ─────────────────────────────────
99
+ const proof = isTokenMode(manifest) ? claimProof(manifest) : missingLegacyProof(); // token mode is the shipped reality; legacy is unsupported here
100
+ const claimUrl = buildClaimUrl(manifest, agentId, proof);
101
+ say();
102
+ say(`${fmt.badge("claim", "cyan")} Your turn — sign in and claim ${name} into your org.`);
103
+ await offerToOpen(claimUrl, "the claim page", { nonInteractive, autoOpen, say });
104
+ say(fmt.dim(`(Brand new? You can create your account at ${manifest.handoff.signup_url}.)`));
105
+ say(fmt.dim("Waiting for you to finish signing in / creating your account…"));
106
+ const claimed = await pollUntilClaimed(apiBase, agentId, options.pollTimeout ?? 600, say);
107
+ if (!claimed) {
108
+ result.steps.push({ step: "claim", status: "pending", detail: "not claimed before timeout" });
109
+ if (json)
110
+ console.log(JSON.stringify(result, null, 2));
111
+ throw new Error(`${agentId} wasn't claimed in time. Finish the claim at the link above, then re-run with ` +
112
+ `--resume ${agentId} to resume (your birth credential is reusable within the manifest window).`);
113
+ }
114
+ result.claimed = true;
115
+ result.steps.push({ step: "claim", status: "ok", detail: agentId });
116
+ say(fmt.success("Claimed — you now own this agent."));
117
+ // ── Ensure a CLI session for the card writes (the human's own login) ────────
118
+ await ensureSession({ nonInteractive, autoOpen, agentId, say });
119
+ // ── State: alignment (set directly with the post-claim session) ─────────────
120
+ say();
121
+ say(`${fmt.badge("alignment", "cyan")} Publishing your alignment card — your signed, public statement of intent…`);
122
+ await writeCardWithRetry("alignment", agentId, manifest.declare.alignment_card, options.pollTimeout ?? 600, say);
123
+ result.alignment = "set";
124
+ result.steps.push({ step: "alignment", status: "ok" });
125
+ say(fmt.success("Alignment card set."));
126
+ // ── State: protection (one-time grant in the browser, then write) ───────────
127
+ say();
128
+ say(`${fmt.badge("protection", "cyan")} Your protection card needs a one-time grant (the CISO moment).`);
129
+ const grantUrl = buildGrantUrl(manifest, agentId);
130
+ await offerToOpen(grantUrl, "the one-time protection-grant page", {
131
+ nonInteractive,
132
+ autoOpen,
133
+ say,
134
+ });
135
+ say(fmt.dim("Waiting for your approval, then setting the protection card…"));
136
+ await writeCardWithRetry("protection", agentId, manifest.declare.protection_card, options.pollTimeout ?? 600, say);
137
+ result.protection = "set";
138
+ result.steps.push({ step: "protection", status: "ok" });
139
+ say(fmt.success("Protection card set."));
140
+ // ── State: hand off to the Dojo ─────────────────────────────────────────────
141
+ const dojoUrl = buildDojoDeepLink(manifest, agentId);
142
+ result.dojo_url = dojoUrl;
143
+ say();
144
+ say(`${fmt.badge("dojo", "cyan")} You're verified and armed — opening the Dojo so you can press Begin Sim.`);
145
+ await offerToOpen(dojoUrl, "the Dojo (you're pre-selected)", { nonInteractive, autoOpen, say });
146
+ result.steps.push({ step: "handoff", status: "ok", detail: dojoUrl });
147
+ say();
148
+ say(renderRecount(manifest, name, agentId));
149
+ if (json)
150
+ console.log(JSON.stringify(result, null, 2));
151
+ }
152
+ // ── name (human checkpoint) ──────────────────────────────────────────────────
153
+ async function pickName(manifest, options, skipPrompt, say) {
154
+ if (options.name && options.name.trim())
155
+ return options.name.trim();
156
+ const opts = manifest.handoff.name_options ?? [];
157
+ const fallback = opts[0] ?? "mnemom-dojo-agent";
158
+ if (skipPrompt) {
159
+ say(fmt.dim(`Non-interactive: naming the agent "${fallback}" (override with --name).`));
160
+ return fallback;
161
+ }
162
+ say();
163
+ const TYPE_MY_OWN = "Type my own";
164
+ const choice = await askSelect(manifest.handoff.name_question, [...opts, TYPE_MY_OWN]);
165
+ if (choice && choice !== TYPE_MY_OWN)
166
+ return choice;
167
+ // "Type my own" — or no valid selection — falls through to free-form entry.
168
+ for (;;) {
169
+ const typed = (await askInput("Enter a name for your agent:")).trim();
170
+ if (typed)
171
+ return typed;
172
+ // A non-responsive stream (EOF on a pipe) returns "" on every read; don't
173
+ // spin forever — fall back to the default so an unattended run completes.
174
+ if (!process.stdin.isTTY) {
175
+ say(fmt.warn(`No name entered on a non-interactive stream — using "${fallback}".`));
176
+ return fallback;
177
+ }
178
+ say(fmt.warn("A name is required (it's permanent) — please enter one."));
179
+ }
180
+ }
181
+ /** Token mode is the shipped reality; legacy provider-key claims aren't supported by this runner. */
182
+ function missingLegacyProof() {
183
+ throw new Error("This briefing is a legacy provider-key flow; `mnemom try-me` supports token-mode (mnbt_) " +
184
+ "briefings. Follow the manifest's steps manually, or ask for a token-mode invite.");
185
+ }
186
+ // ── offer-to-open (enter-default = open it for me) ──────────────────────────
187
+ async function offerToOpen(url, what, ctx) {
188
+ ctx.say(fmt.label(` Link:`, url));
189
+ if (!ctx.autoOpen) {
190
+ ctx.say(fmt.dim(` Open ${what} in your browser, then come back here.`));
191
+ return "printed";
192
+ }
193
+ if (ctx.nonInteractive) {
194
+ openBrowser(url);
195
+ ctx.say(fmt.dim(` Opened ${what} in your browser.`));
196
+ return "opened";
197
+ }
198
+ // Enter-default is the FIRST choice — "Open it for me".
199
+ const choice = await askSelect(`Open ${what}?`, ["Open it for me", "I'll open it myself"]);
200
+ if (choice === "I'll open it myself") {
201
+ ctx.say(fmt.dim(` Okay — open the link above when you're ready.`));
202
+ return "printed";
203
+ }
204
+ // Default (enter / first choice / unrecognized) → open it.
205
+ openBrowser(url);
206
+ ctx.say(fmt.dim(` Opened ${what} in your browser.`));
207
+ return "opened";
208
+ }
209
+ // ── claim poll ───────────────────────────────────────────────────────────────
210
+ async function pollUntilClaimed(apiBase, agentId, timeoutSeconds, say) {
211
+ const deadline = Date.now() + timeoutSeconds * 1000;
212
+ let waited = 0;
213
+ for (;;) {
214
+ const status = await fetchAgentClaimed(apiBase, agentId);
215
+ if (status?.claimed)
216
+ return true;
217
+ if (Date.now() >= deadline)
218
+ return false;
219
+ await sleep(POLL_INTERVAL_MS);
220
+ waited += POLL_INTERVAL_MS;
221
+ // A gentle heartbeat every ~30s so a multi-minute signup doesn't look hung.
222
+ if (waited % 30000 === 0)
223
+ say(fmt.dim(` …still waiting for the claim (${waited / 1000}s).`));
224
+ }
225
+ }
226
+ // ── CLI session for the card writes ──────────────────────────────────────────
227
+ /**
228
+ * Ensure the CLI holds the human's session before the card writes. The card-write
229
+ * PUTs authorize on org membership and need the human's JWT/API-key — the
230
+ * headless twin of the MCP host-connector authorization. If unauthenticated, offer
231
+ * a one-click `mnemom login` (one-click since the human just signed in to claim).
232
+ */
233
+ async function ensureSession(ctx) {
234
+ const cred = await resolveAuth();
235
+ if (cred.type !== "none")
236
+ return;
237
+ ctx.say();
238
+ ctx.say(fmt.dim("To set your cards as you, the CLI needs your Mnemom session — this is the headless twin of " +
239
+ "the host-connector authorization (your own login, not a forged credential)."));
240
+ if (ctx.nonInteractive) {
241
+ throw new Error("Not authenticated. Run `mnemom login` (or set MNEMOM_TOKEN / MNEMOM_API_KEY) first, then " +
242
+ `re-run with --resume ${ctx.agentId} to resume after the claim.`);
243
+ }
244
+ const ok = await askYesNo("Sign in now? (one-click — you just signed in to claim)", true);
245
+ if (!ok) {
246
+ throw new Error("A Mnemom session is required to set the cards. Run `mnemom login`, then re-run with " +
247
+ `--resume ${ctx.agentId} to resume.`);
248
+ }
249
+ // No local browser (e.g. SSH) → device flow; otherwise the loopback OAuth flow.
250
+ if (ctx.autoOpen) {
251
+ await loginWithBrowser();
252
+ }
253
+ else {
254
+ await loginWithDeviceFlow();
255
+ }
256
+ ctx.say(fmt.success("Signed in."));
257
+ }
258
+ // ── card writes (retry on first-auth / grant propagation) ───────────────────
259
+ /**
260
+ * Write a card, retrying while the server reports the caller isn't yet authorized
261
+ * (401 / 403). For alignment this absorbs the brief org-membership propagation
262
+ * right after the claim; for protection it ALSO absorbs the one-time grant landing
263
+ * (the manifest's 403 insufficient_scope → keep polling; first 200 → done). Any
264
+ * other status is a real error and is surfaced immediately.
265
+ */
266
+ async function writeCardWithRetry(kind, agentId, card, timeoutSeconds, say) {
267
+ const body = JSON.stringify(card);
268
+ const put = kind === "alignment" ? putAlignmentCard : putProtectionCard;
269
+ const deadline = Date.now() + timeoutSeconds * 1000;
270
+ let waited = 0;
271
+ for (;;) {
272
+ try {
273
+ await put(agentId, body, "application/json");
274
+ return;
275
+ }
276
+ catch (err) {
277
+ const status = err instanceof MnemomApiError ? err.effectiveStatus : undefined;
278
+ const retriable = status === 401 || status === 403;
279
+ if (!retriable || Date.now() >= deadline) {
280
+ if (retriable) {
281
+ throw new Error(`Timed out waiting to set the ${kind} card (last status ${status}). ` +
282
+ (kind === "protection"
283
+ ? "Make sure you approved the one-time grant, then re-run with --resume " +
284
+ agentId +
285
+ "."
286
+ : "Re-run with --resume " + agentId + " to resume."), { cause: err });
287
+ }
288
+ throw err;
289
+ }
290
+ await sleep(POLL_INTERVAL_MS);
291
+ waited += POLL_INTERVAL_MS;
292
+ if (waited % 15000 === 0) {
293
+ say(fmt.dim(` …waiting to set the ${kind} card (${waited / 1000}s)` +
294
+ (kind === "protection"
295
+ ? " — approve the grant in your browser if you haven't."
296
+ : ".")));
297
+ }
298
+ }
299
+ }
300
+ }
301
+ // ── dry-run rendering ────────────────────────────────────────────────────────
302
+ /** Render the resolved plan as human-readable text (no side effects). */
303
+ export function renderPlan(manifest) {
304
+ const L = [];
305
+ const mission = manifest.mission ?? {};
306
+ const credKind = isTokenMode(manifest) ? "birth_token (mnbt_…)" : "provider_key (legacy)";
307
+ L.push(fmt.header("Mnemom Dojo — try-me (dry run)"));
308
+ L.push("");
309
+ L.push(fmt.label(" Manifest:", `v${manifest.version} · token ${manifest.token}`));
310
+ if (mission.name)
311
+ L.push(fmt.label(" Mission: ", `${mission.name} (${mission.scenario_id ?? "?"})`));
312
+ L.push(fmt.label(" Model: ", `${manifest.gateway.provider} / ${manifest.gateway.model} via ${manifest.gateway.endpoint}`));
313
+ L.push(fmt.label(" Credential:", credKind));
314
+ L.push("");
315
+ L.push(fmt.dim(" The runner will, with NO side effects in this dry run:"));
316
+ L.push(` 1. ${fmt.badge("name", "cyan")} ask: "${manifest.handoff.name_question}"`);
317
+ L.push(fmt.dim(` options: ${(manifest.handoff.name_options ?? []).join(", ")}, or type your own`));
318
+ L.push(` 2. ${fmt.badge("birth", "cyan")} POST ${manifest.gateway.endpoint}/v1/messages`);
319
+ L.push(fmt.dim(` headers: ${manifest.gateway.key_header}: <${isTokenMode(manifest) ? "birth_token" : "provider_key"}>, ` +
320
+ `${manifest.gateway.agent_header}: <name> · model ${manifest.gateway.model}`));
321
+ L.push(` 3. ${fmt.badge("claim", "cyan")} open ${manifest.handoff.claim_url_template}`);
322
+ L.push(fmt.dim(" then poll GET /v1/agents/<agent_id> until claimed:true"));
323
+ L.push(` 4. ${fmt.badge("alignment", "cyan")} PUT /v1/alignment/agent/<agent_id> (your session)`);
324
+ L.push(` 5. ${fmt.badge("protection", "cyan")} open ${manifest.handoff.grant_url_template}`);
325
+ L.push(fmt.dim(" then PUT /v1/protection/agent/<agent_id> (poll until the grant lands)"));
326
+ L.push(` 6. ${fmt.badge("dojo", "cyan")} open ${manifest.handoff.dojo_url}?agent=<agent_id> → Begin Sim`);
327
+ L.push("");
328
+ L.push(fmt.dim(" Cards that would be declared:"));
329
+ L.push(fmt.dim(` alignment: ${compactCard(manifest.declare.alignment_card)}`));
330
+ L.push(fmt.dim(` protection: ${compactCard(manifest.declare.protection_card)} (mode ${manifest.declare.protection_mode})`));
331
+ L.push("");
332
+ L.push(fmt.warn("Dry run — nothing was resolved-and-acted-on beyond reading the briefing."));
333
+ return L.join("\n");
334
+ }
335
+ /** One-line preview of a card's top-level keys (avoids dumping the whole object). */
336
+ function compactCard(card) {
337
+ const keys = Object.keys(card ?? {});
338
+ return keys.length ? `{ ${keys.join(", ")} }` : "{}";
339
+ }
340
+ /** Machine-readable dry-run plan. */
341
+ function buildDryRunJson(manifest) {
342
+ return {
343
+ dry_run: true,
344
+ token: manifest.token,
345
+ version: manifest.version,
346
+ mission: manifest.mission ?? null,
347
+ model: { provider: manifest.gateway.provider, name: manifest.gateway.model },
348
+ gateway: {
349
+ endpoint: manifest.gateway.endpoint,
350
+ key_header: manifest.gateway.key_header,
351
+ credential: isTokenMode(manifest) ? "birth_token" : "provider_key",
352
+ },
353
+ handoff: {
354
+ name_question: manifest.handoff.name_question,
355
+ name_options: manifest.handoff.name_options,
356
+ claim_url_template: manifest.handoff.claim_url_template,
357
+ grant_url_template: manifest.handoff.grant_url_template,
358
+ dojo_url: manifest.handoff.dojo_url,
359
+ },
360
+ declare: {
361
+ alignment_card_keys: Object.keys(manifest.declare.alignment_card ?? {}),
362
+ protection_card_keys: Object.keys(manifest.declare.protection_card ?? {}),
363
+ protection_mode: manifest.declare.protection_mode,
364
+ },
365
+ };
366
+ }
367
+ /** The closing recount the human can paste back, personalized from the run. */
368
+ function renderRecount(manifest, name, agentId) {
369
+ const closing = manifest.closing;
370
+ const tmpl = closing?.recount_template ??
371
+ "I ran the Mnemom try-me as {agent_name}: born, claimed, alignment + protection set. Ready for Begin Sim.";
372
+ const filled = tmpl.replace("{agent_name}", name).replace("{org}", "your org");
373
+ return fmt.dim(`Done — ${agentId} is ready. Press Begin Sim in the Dojo.\n\n${filled}`);
374
+ }
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { program } from "commander";
3
3
  import { isEntrypoint } from "./lib/entrypoint.js";
4
4
  import { CLI_VERSION } from "./version.js";
5
5
  import { statusCommand } from "./commands/status.js";
6
- import { integrityCommand } from "./commands/integrity.js";
6
+ import { activityCommand, integrityCommand } from "./commands/activity.js";
7
7
  import { logsCommand } from "./commands/logs.js";
8
8
  import { licenseActivateCommand, licenseStatusCommand, licenseDeactivateCommand, } from "./commands/license.js";
9
9
  import { cardShowCommand, cardPublishCommand, cardValidateCommand, cardEditCommand, cardEvaluateCommand, } from "./commands/card.js";
@@ -19,11 +19,57 @@ import { validateSafeHouseCommand } from "./commands/validate.js";
19
19
  import { apiKeyListCommand, apiKeyCreateCommand, apiKeyRotateCommand, apiKeyRevokeCommand, } from "./commands/api-key.js";
20
20
  import { webhooksListCommand, webhooksGetCommand, webhooksCreateCommand, webhooksUpdateCommand, webhooksDeleteCommand, webhooksRotateSecretCommand, webhooksTriggerCommand, webhooksListDeliveriesCommand, webhooksRedeliverCommand, webhooksReplayCommand, } from "./commands/webhooks.js";
21
21
  import { listenCommand } from "./commands/listen.js";
22
+ import { tryMeCommand } from "./commands/try-me.js";
22
23
  program
23
24
  .name("mnemom")
24
25
  .description("Transparent AI agent tracing")
25
26
  .version(CLI_VERSION)
26
27
  .option("--agent <name>", "Select agent by name (or set MNEMOM_AGENT)");
28
+ // ============================================================================
29
+ // try-me — deterministic Dojo-onboarding skill-runner (MNE-934, epic MNE-931)
30
+ //
31
+ // `mnemom try-me <token>` executes the shipped v5.0 Dojo briefing manifest's
32
+ // flow so a human/agent on a fresh machine (no MCP connector) doesn't improvise
33
+ // it: resolve → name → be born → hand off (human claims in-browser) → set
34
+ // alignment → one-time protection grant (human approves in-browser) → set
35
+ // protection → hand off to the Dojo. Additive + read-only on the dojo backend;
36
+ // the two privileged acts stay in the human's browser (the runner orchestrates
37
+ // + polls). See commands/try-me.ts for the auth model.
38
+ // ============================================================================
39
+ program
40
+ .command("try-me <token>")
41
+ .description("Run the Mnemom Dojo onboarding for a /try-me token (born → claim → declare → spar)")
42
+ .option("--dry-run", "Resolve the briefing and print the plan — no birth, claim, card writes, or sim")
43
+ .option("--json", "Emit machine-readable step outcomes (implies non-interactive)")
44
+ .option("-y, --yes", "Non-interactive: auto-pick defaults and auto-open URLs")
45
+ .option("--name <name>", "Pre-choose the agent's name (skips the name prompt)")
46
+ // NB: a distinct `--resume` (not `--agent`) — the program-level `--agent`
47
+ // option shadows any subcommand `--agent` under commander@12 (see the
48
+ // `advisories` command's MNE-238 note), so a subcommand `--agent` would bind
49
+ // to the parent program and arrive undefined here.
50
+ .option("--resume <agent_id>", "Resume from an already-born agent id (skip birth)")
51
+ .option("--api <url>", "Override the API base used to resolve the token (default: env)")
52
+ .option("--no-open", "Never auto-open URLs in a browser — just print them")
53
+ .option("--poll-timeout <seconds>", "How long to poll the claim/grant before giving up", "600")
54
+ .action(async (token, opts) => {
55
+ try {
56
+ const pollTimeout = opts.pollTimeout ? parseInt(opts.pollTimeout, 10) : 600;
57
+ await tryMeCommand(token, {
58
+ dryRun: opts.dryRun,
59
+ json: opts.json,
60
+ yes: opts.yes,
61
+ name: opts.name,
62
+ agent: opts.resume,
63
+ api: opts.api,
64
+ open: opts.open,
65
+ pollTimeout: isNaN(pollTimeout) ? 600 : pollTimeout,
66
+ });
67
+ }
68
+ catch (error) {
69
+ console.error("Error:", error instanceof Error ? error.message : error);
70
+ process.exit(1);
71
+ }
72
+ });
27
73
  program
28
74
  .command("status")
29
75
  .description("Show agent status and connection info")
@@ -37,11 +83,25 @@ program
37
83
  process.exit(1);
38
84
  }
39
85
  });
86
+ program
87
+ .command("activity")
88
+ .description("Show AAP behavioral activity and activity score (agent-side runtime audit log; distinct from AIP checkpoints)")
89
+ .action(async () => {
90
+ try {
91
+ const opts = program.opts();
92
+ await activityCommand(opts.agent);
93
+ }
94
+ catch (error) {
95
+ console.error("Error:", error instanceof Error ? error.message : error);
96
+ process.exit(1);
97
+ }
98
+ });
40
99
  program
41
100
  .command("integrity")
42
- .description("Display integrity score and verification stats")
101
+ .description("(deprecated, use 'mnemom activity') AAP behavioral activity — note: 'integrity' is reserved for AIP checkpoint surfacing in a future release")
43
102
  .action(async () => {
44
103
  try {
104
+ console.error("Deprecated: use 'mnemom activity' instead. 'mnemom integrity' will surface AIP checkpoints in a future release.");
45
105
  const opts = program.opts();
46
106
  await integrityCommand(opts.agent);
47
107
  }
@@ -962,7 +1022,7 @@ postureCmd
962
1022
  program
963
1023
  .command("login")
964
1024
  .description("Authenticate with your Mnemom account")
965
- .option("--no-browser", "Use email/password prompt instead of browser")
1025
+ .option("--no-browser", "Use the device code flow instead of opening a browser")
966
1026
  .action(async (options) => {
967
1027
  try {
968
1028
  await loginCommand({ noBrowser: options.browser === false });
package/dist/lib/api.js CHANGED
@@ -179,14 +179,13 @@ async function authHeaders() {
179
179
  * Issue an authenticated fetch, and on a 401 response transparently force a
180
180
  * token refresh and retry once.
181
181
  *
182
- * We hit this in the wild when the locally-stored expiresAt diverges from
183
- * the JWT's actual exp claim (e.g. Supabase has been observed reporting
184
- * expires_in values longer than the JWT's exp). Without this retry,
182
+ * We hit this when the locally-cached access token is stale despite the
183
+ * stored expiresAt claiming it's valid server-side revocation, clock skew,
184
+ * or the token being invalidated out from under us. Without this retry,
185
185
  * `whoami` cheerfully reports a "valid" token while every authenticated
186
186
  * call gets 401 — and the user has no path forward besides
187
- * `mnemom logout && mnemom login`. The retry heals stale auth files
188
- * transparently for users who haven't re-logged-in since the
189
- * computeExpiresAt fix landed.
187
+ * `mnemom logout && mnemom login`. The retry runs the OAuth refresh_token
188
+ * grant and heals the stale auth file transparently.
190
189
  *
191
190
  * `buildInit` is invoked fresh for each attempt so the retry picks up the
192
191
  * new Authorization header from the refreshed token. We do NOT mint a new
@@ -3,13 +3,32 @@
3
3
  *
4
4
  * Stores auth tokens in ~/.mnemom/auth.json (UC-9: no more config.json).
5
5
  * License JWTs are stored alongside auth tokens.
6
+ *
7
+ * As of MNE-806, `mnemom login` authenticates against the Mnemom OAuth
8
+ * Authorization Server (see lib/oauth.ts) and persists SCOPED OAuth tokens
9
+ * (access + optional refresh, scope, token_type, and the registered client_id
10
+ * needed to refresh) rather than a full Supabase session. The stored shape
11
+ * stays backward-compatible: `email`/`userId` are optional (opaque OAuth access
12
+ * tokens don't carry identity) and pre-existing Supabase sessions on disk
13
+ * continue to resolve until they expire.
6
14
  */
7
15
  export interface AuthTokens {
8
16
  accessToken: string;
9
- refreshToken: string;
17
+ /** Absent when the AS issues no refresh token for this grant. */
18
+ refreshToken?: string;
10
19
  expiresAt: number;
11
- userId: string;
12
- email: string;
20
+ /** OAuth scope granted to this token (e.g. "mcp:read mcp:write"). */
21
+ scope?: string;
22
+ /** OAuth token_type (typically "Bearer"). */
23
+ tokenType?: string;
24
+ /**
25
+ * The OAuth client_id this token was issued to. Required to refresh (the AS
26
+ * uses public clients, so the client_id is presented at the token endpoint).
27
+ */
28
+ clientId?: string;
29
+ /** Present only for legacy Supabase sessions; opaque OAuth tokens omit these. */
30
+ userId?: string;
31
+ email?: string;
13
32
  }
14
33
  export interface AuthStore {
15
34
  auth?: AuthTokens;
@@ -30,12 +49,6 @@ export type AuthCredential = {
30
49
  } | {
31
50
  type: "none";
32
51
  };
33
- /**
34
- * Compute the effective expiresAt for a freshly issued access token.
35
- * Prefers the JWT's own `exp` claim; falls back to `now + expires_in` if the
36
- * token can't be parsed (e.g. an opaque token).
37
- */
38
- export declare function computeExpiresAt(accessToken: string, expiresInSeconds: number): number;
39
52
  /**
40
53
  * Get a valid access token, or null if not authenticated.
41
54
  *
@@ -81,5 +94,15 @@ export declare function requireAuth(): Promise<AuthCredential & {
81
94
  * Check if the user is logged in (has any credential).
82
95
  */
83
96
  export declare function isLoggedIn(): Promise<boolean>;
97
+ /**
98
+ * Interactive browser login: OAuth 2.1 authorization-code + PKCE with a
99
+ * loopback redirect (the `wrangler login` pattern). Persists the resulting
100
+ * SCOPED tokens (plus the client_id needed to refresh) to ~/.mnemom/auth.json.
101
+ */
84
102
  export declare function loginWithBrowser(): Promise<AuthTokens>;
85
- export declare function loginWithPassword(email: string, password: string): Promise<AuthTokens>;
103
+ /**
104
+ * Headless login: RFC 8628 device authorization grant. Shows a user_code +
105
+ * verification_uri for the user to approve in any browser (possibly on another
106
+ * device), polls until approval, and persists the scoped tokens.
107
+ */
108
+ export declare function loginWithDeviceFlow(): Promise<AuthTokens>;