@mnemom/mnemom 0.16.2 → 0.16.3

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,59 @@
1
+ /**
2
+ * `mnemom onboard` — self-onboard the CALLING coding agent end-to-end (MNE-933,
3
+ * A2, epic MNE-931). Job #1 of the skill lane.
4
+ *
5
+ * Walks the canonical `become_sovereign` sequence for the agent that is calling
6
+ * right now — scan its trust posture, claim its identity, declare an alignment
7
+ * card, surface its Trust Rating, and hand back a public badge URL — in one
8
+ * command, with no manifest and no hand-assembled sub-command chain.
9
+ *
10
+ * scan_trust → claim → alignment → rating → badge
11
+ *
12
+ * AUTH MODEL (the confirmed MNE-944 contract):
13
+ * - The calling agent authenticates as ITS OWN principal via the device-grant
14
+ * OAuth flow (`loginWithDeviceFlow`) — deliberately NOT the human's full
15
+ * org-membership session (the MNE-870 lesson).
16
+ * - The per-agent `agent:<id>:cards:write` scope is standing, minted server-
17
+ * side at claim; that scope alone authorizes the alignment PUT. No separate
18
+ * protection-capability mint, no protection card, no grant handoff, no
19
+ * dojo bind — those belong to the try-me / dojo flow, not this skill.
20
+ * - Claiming needs a proof of key possession. The claim endpoint's client
21
+ * contract (`claimAgent`) requires a `hash_proof` on every call regardless
22
+ * of the OAuth principal, so `--key` (or `--hash-proof`) is REQUIRED to
23
+ * claim a not-yet-claimed agent; an already-claimed agent skips the claim
24
+ * entirely (idempotency). Claiming lands in the agent's PERSONAL org by
25
+ * default (`getMyPersonalOrg`), never silently into a shared org.
26
+ *
27
+ * HUMAN-IN-THE-LOOP: the only mutation is the agent's own alignment-card
28
+ * declaration, authorized by its own standing scoped token. Every privileged
29
+ * act (protection card, grant, dojo bind, org move) stays server/human-gated
30
+ * and is intentionally out of scope here.
31
+ *
32
+ * The Trust Rating surfaced here is the CLI-reachable `integrity_score`. On a
33
+ * clean-machine first run the agent has 0 traces, so the rating is reported as
34
+ * `provisional` — never a bare perfect score. The SIGNED Trust Rating and the
35
+ * rendered public badge are computed server-side (observer / mnemom-api, post-
36
+ * hoc) and become authoritative once the observer pipeline generates traces.
37
+ *
38
+ * See SKILL-RUNNER-CONTRACT.md §2–§4 for the lifecycle + --json output contract.
39
+ */
40
+ export interface OnboardOptions {
41
+ /** Machine-readable output (implies non-interactive). */
42
+ json?: boolean;
43
+ /** Non-interactive: accept defaults, skip all prompts. */
44
+ yes?: boolean;
45
+ /** The calling agent's id (else agent-config → MNEMOM_AGENT → prompt). */
46
+ agent?: string;
47
+ /** Commander `--no-open` sets this false → never auto-open the badge URL. */
48
+ open?: boolean;
49
+ /** The agent's provider API key — the CLI derives the claim hash proof from it. */
50
+ key?: string;
51
+ /** A pre-computed 64-hex hash proof (advanced/CI; alternative to --key). */
52
+ hashProof?: string;
53
+ }
54
+ /**
55
+ * Entry point for `mnemom onboard`. Throws on a fatal error (caught by the
56
+ * index.ts wrapper, which prints + exits non-zero); non-fatal steps degrade to
57
+ * `skipped`/`error` and the run still reports a `partial` verdict.
58
+ */
59
+ export declare function onboardCommand(options?: OnboardOptions): Promise<void>;
@@ -0,0 +1,395 @@
1
+ /**
2
+ * `mnemom onboard` — self-onboard the CALLING coding agent end-to-end (MNE-933,
3
+ * A2, epic MNE-931). Job #1 of the skill lane.
4
+ *
5
+ * Walks the canonical `become_sovereign` sequence for the agent that is calling
6
+ * right now — scan its trust posture, claim its identity, declare an alignment
7
+ * card, surface its Trust Rating, and hand back a public badge URL — in one
8
+ * command, with no manifest and no hand-assembled sub-command chain.
9
+ *
10
+ * scan_trust → claim → alignment → rating → badge
11
+ *
12
+ * AUTH MODEL (the confirmed MNE-944 contract):
13
+ * - The calling agent authenticates as ITS OWN principal via the device-grant
14
+ * OAuth flow (`loginWithDeviceFlow`) — deliberately NOT the human's full
15
+ * org-membership session (the MNE-870 lesson).
16
+ * - The per-agent `agent:<id>:cards:write` scope is standing, minted server-
17
+ * side at claim; that scope alone authorizes the alignment PUT. No separate
18
+ * protection-capability mint, no protection card, no grant handoff, no
19
+ * dojo bind — those belong to the try-me / dojo flow, not this skill.
20
+ * - Claiming needs a proof of key possession. The claim endpoint's client
21
+ * contract (`claimAgent`) requires a `hash_proof` on every call regardless
22
+ * of the OAuth principal, so `--key` (or `--hash-proof`) is REQUIRED to
23
+ * claim a not-yet-claimed agent; an already-claimed agent skips the claim
24
+ * entirely (idempotency). Claiming lands in the agent's PERSONAL org by
25
+ * default (`getMyPersonalOrg`), never silently into a shared org.
26
+ *
27
+ * HUMAN-IN-THE-LOOP: the only mutation is the agent's own alignment-card
28
+ * declaration, authorized by its own standing scoped token. Every privileged
29
+ * act (protection card, grant, dojo bind, org move) stays server/human-gated
30
+ * and is intentionally out of scope here.
31
+ *
32
+ * The Trust Rating surfaced here is the CLI-reachable `integrity_score`. On a
33
+ * clean-machine first run the agent has 0 traces, so the rating is reported as
34
+ * `provisional` — never a bare perfect score. The SIGNED Trust Rating and the
35
+ * rendered public badge are computed server-side (observer / mnemom-api, post-
36
+ * hoc) and become authoritative once the observer pipeline generates traces.
37
+ *
38
+ * See SKILL-RUNNER-CONTRACT.md §2–§4 for the lifecycle + --json output contract.
39
+ */
40
+ import { getAgent, getIntegrity, claimAgent, putAlignmentCard, getMyPersonalOrg, MnemomApiError, } from "../lib/api.js";
41
+ import { resolveAuth, loginWithDeviceFlow } from "../lib/auth.js";
42
+ import { getAgentId, getAgentName, mergeAgentConfig } from "../lib/agent-config.js";
43
+ import { deriveHashProof } from "./agents.js";
44
+ import { isInteractive, askInput, askYesNo } from "../lib/prompt.js";
45
+ import { getWebsiteUrl } from "../lib/config.js";
46
+ import { openBrowser } from "../lib/oauth.js";
47
+ import { fmt } from "../lib/format.js";
48
+ const FULL_PROOF_RE = /^[0-9a-f]{64}$/;
49
+ /**
50
+ * Entry point for `mnemom onboard`. Throws on a fatal error (caught by the
51
+ * index.ts wrapper, which prints + exits non-zero); non-fatal steps degrade to
52
+ * `skipped`/`error` and the run still reports a `partial` verdict.
53
+ */
54
+ export async function onboardCommand(options = {}) {
55
+ const json = !!options.json;
56
+ const nonInteractive = !!options.yes || json || !isInteractive();
57
+ const autoOpen = options.open !== false; // --no-open → false
58
+ const steps = [];
59
+ // A human-facing log that is silenced in --json mode (the JSON is the output).
60
+ const say = (line = "") => {
61
+ if (!json)
62
+ console.log(line);
63
+ };
64
+ const result = {
65
+ skill: "onboard",
66
+ verdict: "error",
67
+ agent_id: null,
68
+ claimed: false,
69
+ card_published: false,
70
+ rating: null,
71
+ badge_url: null,
72
+ next_step: null,
73
+ steps,
74
+ };
75
+ say(fmt.header("Mnemom — onboard"));
76
+ say();
77
+ say(fmt.dim("Self-onboard this agent: scan → claim → declare → rating → badge."));
78
+ // Resolve the calling agent's id (no network).
79
+ const agentId = await resolveCallingAgentId(options, nonInteractive, say);
80
+ result.agent_id = agentId;
81
+ // ── State 1: scan_trust ─────────────────────────────────────────────────────
82
+ const posture = await scanTrust(agentId, steps, say);
83
+ // ── State 2: claim identity ─────────────────────────────────────────────────
84
+ await ensureAgentSession(nonInteractive, result, json, say);
85
+ if (posture.alreadyClaimed) {
86
+ result.claimed = true;
87
+ steps.push({ step: "claim", status: "skipped", detail: "already claimed" });
88
+ say(fmt.success("Already claimed — nothing to do."));
89
+ }
90
+ else {
91
+ await claimIdentity(agentId, posture.name, options, result, json, steps, say);
92
+ }
93
+ // ── State 3: declare alignment card ─────────────────────────────────────────
94
+ say();
95
+ say(`${fmt.badge("alignment", "cyan")} Declaring your alignment card…`);
96
+ try {
97
+ await putAlignmentCard(agentId, JSON.stringify(buildStarterAlignmentCard(agentId)), "application/json",
98
+ // A STABLE key so a re-run replays the same server-side reservation
99
+ // (idempotent no-op) rather than writing a divergent duplicate card.
100
+ { idempotencyKey: `onboard-alignment-${agentId}` });
101
+ result.card_published = true;
102
+ steps.push({ step: "alignment", status: "ok" });
103
+ say(fmt.success("Alignment card declared."));
104
+ }
105
+ catch (err) {
106
+ const msg = err instanceof Error ? err.message : String(err);
107
+ steps.push({ step: "alignment", status: "error", detail: msg });
108
+ say(fmt.warn(`Alignment card skipped — declare it later with \`mnemom card publish\`. (${msg})`));
109
+ }
110
+ // ── State 4: earn Trust Rating ──────────────────────────────────────────────
111
+ result.rating = await earnRating(agentId, steps, say);
112
+ // ── State 5: badge ──────────────────────────────────────────────────────────
113
+ const badgeUrl = `${getWebsiteUrl().replace(/\/$/, "")}/agents/${encodeURIComponent(agentId)}`;
114
+ result.badge_url = badgeUrl;
115
+ steps.push({ step: "badge", status: "ok", detail: badgeUrl });
116
+ say();
117
+ say(`${fmt.badge("badge", "cyan")} Your public agent page:`);
118
+ say(fmt.label(" Badge:", badgeUrl));
119
+ if (!nonInteractive && autoOpen) {
120
+ const open = await askYesNo("Open your badge page in a browser?", true);
121
+ if (open)
122
+ openBrowser(badgeUrl);
123
+ }
124
+ // ── Report ──────────────────────────────────────────────────────────────────
125
+ result.verdict = result.claimed && result.card_published ? "ok" : "partial";
126
+ result.next_step = result.card_published ? null : "declare";
127
+ say();
128
+ say(renderRecount(result));
129
+ if (json)
130
+ console.log(JSON.stringify(result, null, 2));
131
+ }
132
+ // ── identity resolution ────────────────────────────────────────────────────────
133
+ /**
134
+ * Resolve the calling agent's id: --agent → agent-config (`getAgentId`) →
135
+ * MNEMOM_AGENT → an interactive prompt. Fails CLOSED with a clear, actionable
136
+ * error when nothing is available and we can't prompt — never proceeds silently.
137
+ */
138
+ async function resolveCallingAgentId(options, nonInteractive, say) {
139
+ const fromFlag = options.agent?.trim();
140
+ if (fromFlag)
141
+ return fromFlag;
142
+ const fromConfig = getAgentId();
143
+ if (fromConfig) {
144
+ say(fmt.dim(`Using saved agent id ${fromConfig} (override with --agent).`));
145
+ return fromConfig;
146
+ }
147
+ const fromEnv = process.env.MNEMOM_AGENT?.trim();
148
+ if (fromEnv)
149
+ return fromEnv;
150
+ if (nonInteractive) {
151
+ throw new Error("No agent identity to onboard. Pass --agent <id>, set MNEMOM_AGENT, or run " +
152
+ "`mnemom wrap` first to provision one.");
153
+ }
154
+ const typed = (await askInput("Which agent id should I onboard?")).trim();
155
+ if (!typed) {
156
+ throw new Error("An agent id is required. Pass --agent <id>, set MNEMOM_AGENT, or run `mnemom wrap` first.");
157
+ }
158
+ return typed;
159
+ }
160
+ // ── State 1: scan_trust ────────────────────────────────────────────────────────
161
+ /**
162
+ * Read the calling agent's CURRENT posture before we claim/declare, so the run
163
+ * can report "here is what you look like now" first. A not-yet-visible or
164
+ * unclaimed agent is a cold read that must degrade gracefully (report
165
+ * `unclaimed`/`unknown`, status `ok` with a detail — never crash the run).
166
+ */
167
+ async function scanTrust(agentId, steps, say) {
168
+ say();
169
+ say(`${fmt.badge("scan_trust", "cyan")} Reading your current trust posture…`);
170
+ let alreadyClaimed = false;
171
+ let name;
172
+ const parts = [];
173
+ try {
174
+ const agent = await getAgent(agentId);
175
+ alreadyClaimed = agent.claimed === true || !!agent.claimed_by;
176
+ name = agent.name ?? undefined;
177
+ parts.push(alreadyClaimed ? "claimed" : "unclaimed");
178
+ }
179
+ catch {
180
+ // Cold read: the agent isn't visible to us yet (unclaimed/unknown/offline).
181
+ parts.push("unknown (not yet visible)");
182
+ }
183
+ try {
184
+ const integ = await getIntegrity(agentId);
185
+ parts.push(`integrity_score ${integ.integrity_score.toFixed(2)} (${integ.total_traces} traces)`);
186
+ }
187
+ catch {
188
+ parts.push("integrity unavailable");
189
+ }
190
+ const detail = parts.join(", ");
191
+ steps.push({ step: "scan_trust", status: "ok", detail });
192
+ say(fmt.dim(` ${detail}`));
193
+ return { alreadyClaimed, name: name ?? getAgentName() };
194
+ }
195
+ // ── State 2: claim identity ────────────────────────────────────────────────────
196
+ /**
197
+ * Ensure a device-grant session as the AGENT principal before the claim/declare
198
+ * mutations. If no credential is present, run the device-grant OAuth flow (the
199
+ * agent's own principal — never the human's full org-membership creds, MNE-944 /
200
+ * MNE-870). In non-interactive mode with no session we fail CLOSED with a clear
201
+ * error rather than hanging or silently degrading.
202
+ */
203
+ async function ensureAgentSession(nonInteractive, result, json, say) {
204
+ const cred = await resolveAuth();
205
+ if (cred.type !== "none")
206
+ return;
207
+ if (nonInteractive) {
208
+ result.steps.push({ step: "claim", status: "error", detail: "no session" });
209
+ if (json)
210
+ console.log(JSON.stringify(result, null, 2));
211
+ throw new Error("Authentication required. Run `mnemom login`, or set MNEMOM_TOKEN / MNEMOM_API_KEY, " +
212
+ "then re-run `mnemom onboard`.");
213
+ }
214
+ say();
215
+ say(fmt.dim("Signing this agent in via the device-grant flow (its own principal)…"));
216
+ await loginWithDeviceFlow();
217
+ say(fmt.success("Signed in."));
218
+ }
219
+ /**
220
+ * Claim the agent into the caller's PERSONAL org, deriving the hash proof from
221
+ * `--key` (or accepting `--hash-proof` directly). The claim endpoint requires a
222
+ * proof on every call, so a fresh claim needs one; an already-claimed agent
223
+ * never reaches here (the caller skips claim on `posture.alreadyClaimed`). The
224
+ * 403/409/503 taxonomy is mapped to the same teaching errors `agents.ts` uses.
225
+ */
226
+ async function claimIdentity(agentId, scannedName, options, result, json, steps, say) {
227
+ say();
228
+ say(`${fmt.badge("claim", "cyan")} Claiming your identity…`);
229
+ const hashProof = resolveHashProof(options, scannedName);
230
+ if (!hashProof) {
231
+ steps.push({ step: "claim", status: "error", detail: "no key proof" });
232
+ if (json)
233
+ console.log(JSON.stringify(result, null, 2));
234
+ throw new Error("Claiming needs proof you hold the agent's key. Pass --key <agent-api-key> (the CLI " +
235
+ "derives the proof), or --hash-proof <64-hex> directly.");
236
+ }
237
+ // Land in the agent's personal org by default — never a shared org silently.
238
+ let orgId;
239
+ try {
240
+ orgId = (await getMyPersonalOrg()).org_id;
241
+ }
242
+ catch {
243
+ // Fall back to the server default (personal org) rather than aborting; the
244
+ // claim below still lands personal when orgId is omitted.
245
+ orgId = undefined;
246
+ }
247
+ try {
248
+ const claim = await claimAgent(agentId, { hashProof, orgId });
249
+ result.claimed = claim.claimed;
250
+ steps.push({ step: "claim", status: "ok", detail: agentId });
251
+ mergeAgentConfig({ agent_id: agentId, org_id: claim.org_id ?? orgId });
252
+ say(fmt.success(`Claimed ${claim.agent_id}.`));
253
+ }
254
+ catch (err) {
255
+ if (err instanceof MnemomApiError) {
256
+ const status = err.effectiveStatus;
257
+ // Specific 403 taxonomy + 503 are classified FIRST, before the idempotency
258
+ // branch. A cross-tenant/org-not-member 403 carries an "already claimed"
259
+ // message, so matching that message loosely here would misclassify a
260
+ // genuine failure as a harmless skip (fail-open) — so idempotency is gated
261
+ // on status/code only, and comes last.
262
+ if (status === 403 &&
263
+ (err.code === "invalid_hash_proof" || /hash[ _]?proof/i.test(err.message))) {
264
+ throw teachingClaimError(agentId, "the --key (and agent name) didn't match the agent's stored proof (invalid hash proof).", "Confirm --key is the exact provider key the agent runs with. If the name couldn't be " +
265
+ `auto-resolved, pass it via \`mnemom agents claim ${agentId} --key <key> --name <name>\`.`, json, result, steps);
266
+ }
267
+ if (status === 403 && err.code === "agent_cross_tenant") {
268
+ throw teachingClaimError(agentId, "this agent is already claimed by a different owner.", "Onboard an agent you own, or provision a fresh one with `mnemom wrap`.", json, result, steps);
269
+ }
270
+ if (status === 403 &&
271
+ (err.code === "agent_org_not_member" || /not[ _]?member/i.test(err.code ?? ""))) {
272
+ throw teachingClaimError(agentId, "you are not a member of the target org.", "Run `mnemom org list` to see the orgs you belong to; onboard lands in your personal org by default.", json, result, steps);
273
+ }
274
+ if (status === 503) {
275
+ throw teachingClaimError(agentId, "your personal org is still being set up.", "Give it a moment, then re-run `mnemom onboard`.", json, result, steps);
276
+ }
277
+ // Idempotency (LAST): a same-owner re-claim that raced the scan. Gated on
278
+ // status/code — NOT a loose message match — so the specific 403s above are
279
+ // never swallowed here.
280
+ if (status === 409 || err.code === "agent_already_claimed") {
281
+ result.claimed = true;
282
+ steps.push({ step: "claim", status: "skipped", detail: "already claimed" });
283
+ mergeAgentConfig({ agent_id: agentId, org_id: orgId });
284
+ say(fmt.success("Already claimed — nothing to do."));
285
+ return;
286
+ }
287
+ }
288
+ const msg = err instanceof Error ? err.message : String(err);
289
+ throw teachingClaimError(agentId, msg, null, json, result, steps);
290
+ }
291
+ }
292
+ /** Resolve the claim hash proof from --hash-proof (raw) or --key (derived). */
293
+ function resolveHashProof(options, derivationName) {
294
+ if (options.hashProof) {
295
+ const proof = options.hashProof.trim().toLowerCase();
296
+ if (!FULL_PROOF_RE.test(proof)) {
297
+ throw new Error("--hash-proof must be the full 64-character hex SHA-256 proof.");
298
+ }
299
+ return proof;
300
+ }
301
+ if (options.key)
302
+ return deriveHashProof(options.key, derivationName);
303
+ return undefined;
304
+ }
305
+ /**
306
+ * Build a fatal claim error that also records the failed step and emits the
307
+ * --json envelope before throwing (so a machine caller still gets valid output
308
+ * on the failure path — MNE-442 fail-closed, never a bare hang).
309
+ */
310
+ function teachingClaimError(agentId, reason, hint, json, result, steps) {
311
+ steps.push({ step: "claim", status: "error", detail: reason });
312
+ if (json)
313
+ console.log(JSON.stringify(result, null, 2));
314
+ const message = hint
315
+ ? `Failed to claim ${agentId}: ${reason}\n ${hint}`
316
+ : `Failed to claim ${agentId}: ${reason}`;
317
+ return new Error(message);
318
+ }
319
+ // ── State 4: earn Trust Rating ─────────────────────────────────────────────────
320
+ /**
321
+ * Re-read the agent's integrity and surface it as an HONEST rating: a cold-start
322
+ * agent (0 traces) is `provisional`, never a bare perfect score (MNE-439). The
323
+ * signed Trust Rating remains server-computed; this is the CLI-reachable view.
324
+ */
325
+ async function earnRating(agentId, steps, say) {
326
+ say();
327
+ say(`${fmt.badge("rating", "cyan")} Reading your Trust Rating…`);
328
+ try {
329
+ const integ = await getIntegrity(agentId);
330
+ const rating = {
331
+ score: integ.integrity_score,
332
+ total_traces: integ.total_traces,
333
+ verified_traces: integ.verified_traces,
334
+ status: integ.total_traces === 0 ? "provisional" : "rated",
335
+ };
336
+ steps.push({
337
+ step: "rating",
338
+ status: "ok",
339
+ detail: `${rating.status} — score ${rating.score.toFixed(2)} (${rating.total_traces} traces)`,
340
+ });
341
+ say(fmt.dim(rating.status === "provisional"
342
+ ? ` Provisional — the signed rating lands once the observer pipeline generates traces.`
343
+ : ` Rated — score ${rating.score.toFixed(2)} across ${rating.total_traces} traces.`));
344
+ return rating;
345
+ }
346
+ catch (err) {
347
+ const msg = err instanceof Error ? err.message : String(err);
348
+ steps.push({ step: "rating", status: "error", detail: msg });
349
+ say(fmt.warn(`Trust Rating unavailable right now — check \`mnemom status\` later. (${msg})`));
350
+ return null;
351
+ }
352
+ }
353
+ // ── starter alignment card ──────────────────────────────────────────────────────
354
+ /**
355
+ * A starter alignment card in `observe` mode — the honest, minimal declaration
356
+ * of intent for a freshly-onboarded agent. Mirrors the shape `wrap` seeds so the
357
+ * two skill-runners produce a consistent first card.
358
+ */
359
+ function buildStarterAlignmentCard(agentId) {
360
+ return {
361
+ card_version: "unified/2026-04-26",
362
+ agent_id: agentId,
363
+ autonomy_mode: "observe",
364
+ integrity_mode: "observe",
365
+ principal: { type: "agent", identifier: agentId, relationship: "delegated_authority" },
366
+ values: { declared: ["transparency", "safety", "honesty"] },
367
+ autonomy: {
368
+ bounded_actions: ["respond_to_prompts"],
369
+ forbidden_actions: [],
370
+ escalation_triggers: [],
371
+ },
372
+ audit: { retention_days: 30, queryable: false, trace_format: "otel" },
373
+ };
374
+ }
375
+ // ── report ──────────────────────────────────────────────────────────────────────
376
+ /** The closing recount for human mode. */
377
+ function renderRecount(result) {
378
+ const L = [fmt.section("Onboarding summary"), ""];
379
+ L.push(fmt.label(" Agent: ", result.agent_id ?? "(unknown)"));
380
+ L.push(fmt.label(" Claimed: ", result.claimed ? "yes" : "no"));
381
+ L.push(fmt.label(" Card: ", result.card_published ? "declared" : "not declared"));
382
+ if (result.rating) {
383
+ L.push(fmt.label(" Rating: ", `${result.rating.status} (score ${result.rating.score.toFixed(2)}, ${result.rating.total_traces} traces)`));
384
+ }
385
+ if (result.badge_url)
386
+ L.push(fmt.label(" Badge: ", result.badge_url));
387
+ L.push("");
388
+ if (result.verdict === "ok") {
389
+ L.push(fmt.dim("You're a claimed, value-declared agent. Traces will appear in `mnemom logs`."));
390
+ }
391
+ else {
392
+ L.push(fmt.dim("Partly done — re-run `mnemom onboard` to finish the remaining step(s)."));
393
+ }
394
+ return L.join("\n");
395
+ }
@@ -1,3 +1,16 @@
1
+ /**
2
+ * `mnemom org use <slug|id>` / `mnemom org use --clear`
3
+ *
4
+ * Record (or forget) the ACTIVE ORG — the durable default that org-scoped
5
+ * commands fall back to when `--org` is omitted. `mnemom login` binds no org
6
+ * (the OAuth token carries only scope), so without this every claim landed
7
+ * silently in the caller's personal org. The value is validated against the
8
+ * caller's live memberships before it is persisted; membership drift after
9
+ * that is caught by the server (403 + teaching list) at use time.
10
+ */
11
+ export declare function orgUseCommand(slugOrId: string | undefined, opts: {
12
+ clear?: boolean;
13
+ }): Promise<void>;
1
14
  /**
2
15
  * `mnemom org list`
3
16
  *
@@ -1,6 +1,63 @@
1
1
  import { listMyOrgs, getMyPersonalOrg } from "../lib/api.js";
2
2
  import { requireAuth } from "../lib/auth.js";
3
+ import { getActiveOrg, setActiveOrg } from "../lib/cli-config.js";
3
4
  import { fmt } from "../lib/format.js";
5
+ /**
6
+ * `mnemom org use <slug|id>` / `mnemom org use --clear`
7
+ *
8
+ * Record (or forget) the ACTIVE ORG — the durable default that org-scoped
9
+ * commands fall back to when `--org` is omitted. `mnemom login` binds no org
10
+ * (the OAuth token carries only scope), so without this every claim landed
11
+ * silently in the caller's personal org. The value is validated against the
12
+ * caller's live memberships before it is persisted; membership drift after
13
+ * that is caught by the server (403 + teaching list) at use time.
14
+ */
15
+ export async function orgUseCommand(slugOrId, opts) {
16
+ if (opts.clear) {
17
+ setActiveOrg(null);
18
+ console.log(fmt.success("Active org cleared.") +
19
+ "\n Org-scoped commands now default to your personal org (pass --org to override).\n");
20
+ return;
21
+ }
22
+ if (!slugOrId) {
23
+ // No arg, no --clear → show the current setting (mirrors `git config`).
24
+ const active = getActiveOrg();
25
+ if (active) {
26
+ console.log(fmt.label("Active org:", `${active.name} (${active.slug}, ${active.org_id})`));
27
+ }
28
+ else {
29
+ console.log(fmt.warn("No active org set.") +
30
+ "\n Set one with: mnemom org use <slug> (see: mnemom org list)\n");
31
+ }
32
+ return;
33
+ }
34
+ const cred = await requireAuth();
35
+ if (cred?.type === "api-key") {
36
+ console.log(fmt.warn("API keys are agent-scoped; run mnemom login for org/key management"));
37
+ return;
38
+ }
39
+ let orgs;
40
+ try {
41
+ orgs = await listMyOrgs();
42
+ }
43
+ catch (err) {
44
+ const msg = err instanceof Error ? err.message : String(err);
45
+ console.log(fmt.error(`Failed to list orgs: ${msg}`) + "\n");
46
+ process.exit(1);
47
+ return;
48
+ }
49
+ const needle = slugOrId.trim();
50
+ const match = orgs.find((o) => o.slug === needle) ?? orgs.find((o) => o.org_id === needle);
51
+ if (!match) {
52
+ const choices = orgs.map((o) => ` ${o.slug.padEnd(24)} ${o.name}`).join("\n");
53
+ console.log(fmt.error(`No org of yours matches '${needle}'. Your organizations:\n${choices}`) + "\n");
54
+ process.exit(1);
55
+ return;
56
+ }
57
+ setActiveOrg({ org_id: match.org_id, slug: match.slug, name: match.name });
58
+ console.log(fmt.success(`Active org set to ${match.name} (${match.slug}).`) +
59
+ "\n Org-scoped commands (e.g. agents claim) now default here. Clear with: mnemom org use --clear\n");
60
+ }
4
61
  /**
5
62
  * `mnemom org list`
6
63
  *
@@ -41,15 +98,19 @@ export async function orgListCommand(opts) {
41
98
  const header = "Name".padEnd(nameW) + "ID".padEnd(idW) + "Role".padEnd(roleW) + "Owner".padEnd(ownerW);
42
99
  console.log(` ${header}`);
43
100
  console.log(` ${"─".repeat(nameW + idW + roleW + ownerW)}`);
101
+ const activeOrgId = getActiveOrg()?.org_id;
44
102
  for (const org of orgs) {
45
- const tag = org.is_personal ? " (personal)" : "";
46
- const name = `${org.name}${tag}`.slice(0, nameW - 2).padEnd(nameW);
103
+ const tags = (org.is_personal ? " (personal)" : "") + (org.org_id === activeOrgId ? " *active" : "");
104
+ const name = `${org.name}${tags}`.slice(0, nameW - 2).padEnd(nameW);
47
105
  const id = org.org_id.slice(0, idW - 2).padEnd(idW);
48
106
  const role = (org.role ?? "-").padEnd(roleW);
49
107
  const owner = (org.is_owner ? "yes" : "no").padEnd(ownerW);
50
108
  console.log(` ${name}${id}${role}${owner}`);
51
109
  }
52
110
  console.log(`\n Total: ${orgs.length} organization(s)\n`);
111
+ if (!activeOrgId) {
112
+ console.log(fmt.dim(" Tip: set a default for org-scoped commands with `mnemom org use <slug>`.") + "\n");
113
+ }
53
114
  }
54
115
  /**
55
116
  * `mnemom org show [<org_id>]` or `mnemom org show --personal`
@@ -24,3 +24,13 @@ export declare function protectionValidateCommand(file: string, opts?: {
24
24
  export declare function protectionEditCommand(agentName?: string, options?: {
25
25
  idempotencyKey?: string;
26
26
  }): Promise<void>;
27
+ /**
28
+ * Compare a committed protection-card snapshot against the live CANONICAL
29
+ * (composed) card. Read-only: never writes, never publishes.
30
+ *
31
+ * Exit 0 = no drift, exit 1 = drift or error, so a scheduled job can gate on it.
32
+ */
33
+ export declare function protectionDriftCommand(file: string, agentName?: string, opts?: {
34
+ strict?: boolean;
35
+ json?: boolean;
36
+ }): Promise<void>;