@mnemom/mnemom 0.16.1 → 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,331 @@
1
+ /**
2
+ * `mnemom wrap` — instrument an existing production agent through the Mnemom gateway (MNE-935, A4).
3
+ *
4
+ * Asks the developer for their provider + framework + agent name, makes a birth
5
+ * call to the gateway to provision the agent identity, seeds starter alignment +
6
+ * protection cards, and emits a drop-in code example. No reasoning change to the
7
+ * host agent.
8
+ *
9
+ * AUTH MODEL: Two credentials are involved:
10
+ * - Mnemom auth (MNEMOM_API_KEY or `mnemom login` JWT) — used for card writes
11
+ * - Provider API key (ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY) — used
12
+ * for the one-time gateway birth call; this is the same key the user's agent
13
+ * already holds, so the birth requires no new credential
14
+ *
15
+ * See SKILL-RUNNER-CONTRACT.md §2–§4 for the lifecycle + output contract.
16
+ */
17
+ import { requireAuth } from "../lib/auth.js";
18
+ import { putAlignmentCard, putProtectionCard, MnemomApiError } from "../lib/api.js";
19
+ import { mergeAgentConfig } from "../lib/agent-config.js";
20
+ import { askSelect, askInput, isInteractive } from "../lib/prompt.js";
21
+ import { getGatewayUrl } from "../lib/config.js";
22
+ import { fmt } from "../lib/format.js";
23
+ const AGENT_HEADER = "x-mnemom-agent";
24
+ const PROVIDERS = {
25
+ anthropic: {
26
+ label: "Anthropic",
27
+ keyEnv: "ANTHROPIC_API_KEY",
28
+ keyHeader: "x-api-key",
29
+ defaultModel: "claude-haiku-4-5-20251001",
30
+ basePath: "/anthropic",
31
+ },
32
+ openai: {
33
+ label: "OpenAI",
34
+ keyEnv: "OPENAI_API_KEY",
35
+ keyHeader: "Authorization",
36
+ defaultModel: "gpt-4o-mini",
37
+ basePath: "/openai",
38
+ },
39
+ gemini: {
40
+ label: "Google Gemini",
41
+ keyEnv: "GOOGLE_API_KEY",
42
+ keyHeader: "x-goog-api-key",
43
+ defaultModel: "gemini-2.0-flash",
44
+ basePath: "/gemini",
45
+ },
46
+ };
47
+ export async function wrapCommand(options = {}) {
48
+ const json = !!options.json;
49
+ const nonInteractive = !!options.yes || json || !isInteractive();
50
+ const steps = [];
51
+ const say = (line = "") => {
52
+ if (!json)
53
+ console.log(line);
54
+ };
55
+ say(fmt.header("Mnemom — wrap"));
56
+ say();
57
+ say(fmt.dim("Instrument your existing agent through the Mnemom gateway."));
58
+ say();
59
+ // Phase 1 — Resolve + Auth (Mnemom session for card writes)
60
+ await requireAuth();
61
+ const gatewayUrl = getGatewayUrl().replace(/\/$/, "");
62
+ const provider = await resolveProvider(options, nonInteractive);
63
+ const framework = await resolveFramework(options, nonInteractive);
64
+ const agentName = await resolveAgentName(options, nonInteractive);
65
+ const providerKey = await resolveProviderKey(options, PROVIDERS[provider], nonInteractive, say);
66
+ steps.push({
67
+ step: "resolve",
68
+ status: "ok",
69
+ detail: `provider=${provider} framework=${framework} name=${agentName}`,
70
+ });
71
+ // Phase 2 — Birth: one gateway call to provision the identity
72
+ say();
73
+ say(`${fmt.badge("birth", "cyan")} Provisioning ${fmt.badge(agentName, "magenta")} through the gateway…`);
74
+ let agentId;
75
+ try {
76
+ agentId = await birthThroughGateway(gatewayUrl, provider, agentName, providerKey);
77
+ steps.push({ step: "birth", status: "ok", detail: agentId });
78
+ say(fmt.success(`Provisioned → ${agentId}`));
79
+ }
80
+ catch (err) {
81
+ const msg = err instanceof Error ? err.message : String(err);
82
+ steps.push({ step: "birth", status: "error", detail: msg });
83
+ if (json) {
84
+ console.log(JSON.stringify({
85
+ skill: "wrap",
86
+ verdict: "error",
87
+ agent_id: null,
88
+ gateway_base_url: gatewayUrl,
89
+ next_step: null,
90
+ steps,
91
+ }, null, 2));
92
+ }
93
+ throw new Error(`Gateway birth failed: ${msg}`, { cause: err });
94
+ }
95
+ mergeAgentConfig({ agent_id: agentId, agent_name: agentName, gateway_url: gatewayUrl });
96
+ // Phase 2 — Seed alignment card
97
+ say();
98
+ say(`${fmt.badge("alignment", "cyan")} Seeding starter alignment card…`);
99
+ try {
100
+ await putAlignmentCard(agentId, JSON.stringify(buildStarterAlignmentCard(agentId)), "application/json");
101
+ steps.push({ step: "alignment", status: "ok" });
102
+ say(fmt.success("Alignment card set."));
103
+ }
104
+ catch (err) {
105
+ const msg = err instanceof Error ? err.message : String(err);
106
+ steps.push({ step: "alignment", status: "error", detail: msg });
107
+ say(fmt.warn(`Alignment card skipped — set it later with \`mnemom card publish\`. (${msg})`));
108
+ }
109
+ // Phase 2 — Seed protection card
110
+ say();
111
+ say(`${fmt.badge("protection", "cyan")} Seeding starter protection card…`);
112
+ try {
113
+ await putProtectionCard(agentId, JSON.stringify(buildStarterProtectionCard(agentId)), "application/json");
114
+ steps.push({ step: "protection", status: "ok" });
115
+ say(fmt.success("Protection card set."));
116
+ }
117
+ catch (err) {
118
+ const msg = err instanceof Error ? err.message : String(err);
119
+ steps.push({ step: "protection", status: "error", detail: msg });
120
+ say(fmt.warn(`Protection card skipped — set it later with \`mnemom protection publish\`. (${msg})`));
121
+ }
122
+ // Phase 3 — Report
123
+ say();
124
+ say(renderCodeExample(framework, provider, gatewayUrl, agentName));
125
+ say();
126
+ say(renderClaimInstructions(agentId));
127
+ if (json) {
128
+ console.log(JSON.stringify({
129
+ skill: "wrap",
130
+ verdict: "ok",
131
+ agent_id: agentId,
132
+ gateway_base_url: gatewayUrl,
133
+ next_step: "claim",
134
+ steps,
135
+ }, null, 2));
136
+ }
137
+ }
138
+ // ── Phase 1 resolvers ─────────────────────────────────────────────────────────
139
+ async function resolveProvider(options, nonInteractive) {
140
+ if (options.provider) {
141
+ const p = options.provider.toLowerCase();
142
+ if (!PROVIDERS[p]) {
143
+ throw new Error(`Unknown provider "${options.provider}". Choose: anthropic, openai, gemini`);
144
+ }
145
+ return p;
146
+ }
147
+ if (nonInteractive)
148
+ return "anthropic";
149
+ const choice = await askSelect("Which AI provider does your agent use?", [
150
+ "Anthropic (Claude)",
151
+ "OpenAI (GPT)",
152
+ "Google Gemini",
153
+ ]);
154
+ if (choice?.startsWith("OpenAI"))
155
+ return "openai";
156
+ if (choice?.startsWith("Google"))
157
+ return "gemini";
158
+ return "anthropic";
159
+ }
160
+ async function resolveFramework(options, nonInteractive) {
161
+ if (options.framework) {
162
+ const f = options.framework.toLowerCase();
163
+ if (f === "python" || f === "py")
164
+ return "python";
165
+ if (f === "node" || f === "nodejs" || f === "js" || f === "ts" || f === "typescript")
166
+ return "node";
167
+ throw new Error(`Unknown framework "${options.framework}". Choose: python, node`);
168
+ }
169
+ if (nonInteractive)
170
+ return "python";
171
+ const choice = await askSelect("Which language/framework is your agent using?", [
172
+ "Python",
173
+ "Node.js / TypeScript",
174
+ ]);
175
+ return choice?.startsWith("Node") ? "node" : "python";
176
+ }
177
+ async function resolveAgentName(options, nonInteractive) {
178
+ if (options.name?.trim())
179
+ return options.name.trim();
180
+ if (nonInteractive)
181
+ return "my-agent";
182
+ const name = (await askInput("What should we call this agent? (x-mnemom-agent value)")).trim();
183
+ return name || "my-agent";
184
+ }
185
+ async function resolveProviderKey(options, prov, nonInteractive, say) {
186
+ if (options.providerKey?.trim())
187
+ return options.providerKey.trim();
188
+ const fromEnv = process.env[prov.keyEnv]?.trim();
189
+ if (fromEnv) {
190
+ say(fmt.dim(`Using ${prov.keyEnv} from environment.`));
191
+ return fromEnv;
192
+ }
193
+ if (nonInteractive) {
194
+ throw new Error(`No ${prov.label} API key found. Set ${prov.keyEnv} or pass --provider-key <key>.`);
195
+ }
196
+ const key = (await askInput(`Enter your ${prov.label} API key (one-time birth call — never stored):`)).trim();
197
+ if (!key)
198
+ throw new Error(`A ${prov.label} API key is required to birth the agent.`);
199
+ return key;
200
+ }
201
+ // ── Birth call ────────────────────────────────────────────────────────────────
202
+ async function birthThroughGateway(gatewayUrl, provider, agentName, providerKey) {
203
+ const prov = PROVIDERS[provider];
204
+ const authValue = provider === "openai" ? `Bearer ${providerKey}` : providerKey;
205
+ const headers = {
206
+ "Content-Type": "application/json",
207
+ Accept: "application/json",
208
+ [prov.keyHeader]: authValue,
209
+ [AGENT_HEADER]: agentName,
210
+ };
211
+ if (provider === "anthropic") {
212
+ headers["anthropic-version"] = "2023-06-01";
213
+ }
214
+ let url;
215
+ let body;
216
+ switch (provider) {
217
+ case "openai":
218
+ url = `${gatewayUrl}${prov.basePath}/v1/chat/completions`;
219
+ body = JSON.stringify({
220
+ model: prov.defaultModel,
221
+ max_tokens: 16,
222
+ messages: [{ role: "user", content: "hello" }],
223
+ });
224
+ break;
225
+ case "gemini":
226
+ url = `${gatewayUrl}${prov.basePath}/v1beta/models/${prov.defaultModel}:generateContent`;
227
+ body = JSON.stringify({ contents: [{ parts: [{ text: "hello" }] }] });
228
+ break;
229
+ case "anthropic":
230
+ default:
231
+ url = `${gatewayUrl}${prov.basePath}/v1/messages`;
232
+ body = JSON.stringify({
233
+ model: prov.defaultModel,
234
+ max_tokens: 16,
235
+ messages: [{ role: "user", content: "hello" }],
236
+ });
237
+ }
238
+ const response = await fetch(url, { method: "POST", headers, body });
239
+ const agentId = response.headers.get(AGENT_HEADER);
240
+ if (!response.ok && !agentId) {
241
+ const text = await response.text().catch(() => "");
242
+ throw new MnemomApiError(response.status, `Gateway returned ${response.status}: ${text.slice(0, 300)}`);
243
+ }
244
+ if (!agentId || !/^mnm-/.test(agentId)) {
245
+ throw new Error(`Gateway returned no mnm- agent id on the ${AGENT_HEADER} response header (status ${response.status}).`);
246
+ }
247
+ return agentId;
248
+ }
249
+ // ── Starter cards ─────────────────────────────────────────────────────────────
250
+ function buildStarterAlignmentCard(agentId) {
251
+ return {
252
+ card_version: "unified/2026-04-26",
253
+ agent_id: agentId,
254
+ autonomy_mode: "observe",
255
+ integrity_mode: "observe",
256
+ principal: { type: "agent", identifier: agentId, relationship: "delegated_authority" },
257
+ values: { declared: ["transparency", "safety", "honesty"] },
258
+ autonomy: {
259
+ bounded_actions: ["respond_to_prompts"],
260
+ forbidden_actions: [],
261
+ escalation_triggers: [],
262
+ },
263
+ audit: { retention_days: 30, queryable: false, trace_format: "otel" },
264
+ };
265
+ }
266
+ function buildStarterProtectionCard(agentId) {
267
+ return {
268
+ card_version: "protection/2026-04-26",
269
+ agent_id: agentId,
270
+ mode: "observe",
271
+ thresholds: { warn: 0.3, quarantine: 0.6, block: 0.9 },
272
+ screen_surfaces: {
273
+ incoming: true,
274
+ outgoing: true,
275
+ tool_calls: true,
276
+ tool_responses: true,
277
+ },
278
+ trusted_sources: { domains: [], agent_ids: [], ip_ranges: [] },
279
+ };
280
+ }
281
+ // ── Code example rendering ────────────────────────────────────────────────────
282
+ function renderCodeExample(framework, provider, gatewayUrl, agentName) {
283
+ const prov = PROVIDERS[provider];
284
+ const lines = [fmt.section("Drop-in gateway wrapper"), ""];
285
+ if (framework === "python") {
286
+ renderPythonSnippet(lines, provider, prov, gatewayUrl, agentName);
287
+ }
288
+ else {
289
+ renderNodeSnippet(lines, provider, prov, gatewayUrl, agentName);
290
+ }
291
+ lines.push("", fmt.dim("No other changes needed — your agent logic is unchanged."));
292
+ return lines.join("\n");
293
+ }
294
+ function renderPythonSnippet(lines, provider, prov, gatewayUrl, agentName) {
295
+ const base = `${gatewayUrl}${prov.basePath}`;
296
+ if (provider === "anthropic") {
297
+ lines.push(" import anthropic", "", " client = anthropic.Anthropic(", ` base_url="${base}",`, ` default_headers={"x-mnemom-agent": "${agentName}"},`, " )");
298
+ }
299
+ else if (provider === "openai") {
300
+ lines.push(" from openai import OpenAI", "", " client = OpenAI(", ` base_url="${base}/v1",`, ` default_headers={"x-mnemom-agent": "${agentName}"},`, " )");
301
+ }
302
+ else {
303
+ lines.push(" # Google Gemini via Mnemom gateway", ` # Set base URL to: ${base}`, ` # Add header: x-mnemom-agent: ${agentName}`);
304
+ }
305
+ }
306
+ function renderNodeSnippet(lines, provider, prov, gatewayUrl, agentName) {
307
+ const base = `${gatewayUrl}${prov.basePath}`;
308
+ if (provider === "anthropic") {
309
+ lines.push(' import Anthropic from "@anthropic-ai/sdk";', "", " const client = new Anthropic({", ` baseURL: "${base}",`, ` defaultHeaders: { "x-mnemom-agent": "${agentName}" },`, " });");
310
+ }
311
+ else if (provider === "openai") {
312
+ lines.push(' import OpenAI from "openai";', "", " const client = new OpenAI({", ` baseURL: "${base}/v1",`, ` defaultHeaders: { "x-mnemom-agent": "${agentName}" },`, " });");
313
+ }
314
+ else {
315
+ lines.push(" // Google Gemini via Mnemom gateway", ` // Set base URL to: ${base}`, ` // Add header: "x-mnemom-agent": "${agentName}"`);
316
+ }
317
+ }
318
+ function renderClaimInstructions(agentId) {
319
+ const lines = [
320
+ fmt.section("Next step — claim this agent"),
321
+ "",
322
+ fmt.label(" Agent ID:", agentId),
323
+ "",
324
+ fmt.dim(" Claim this agent so you own its identity on the platform:"),
325
+ "",
326
+ ` mnemom agents claim ${agentId}`,
327
+ "",
328
+ fmt.dim(" After claiming, traces appear in `mnemom logs`."),
329
+ ];
330
+ return lines.join("\n");
331
+ }
package/dist/index.js CHANGED
@@ -8,9 +8,9 @@ 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";
10
10
  import { policyInitCommand, policyValidateCommand, policyPublishCommand, policyListCommand, policyTestCommand, policyEvaluateCommand, } from "./commands/policy.js";
11
- import { protectionShowCommand, protectionPublishCommand, protectionValidateCommand, protectionEditCommand, } from "./commands/protection.js";
12
- import { agentsListCommand, agentsClaimCommand } from "./commands/agents.js";
13
- import { orgListCommand, orgShowCommand } from "./commands/org.js";
11
+ import { protectionShowCommand, protectionPublishCommand, protectionValidateCommand, protectionEditCommand, protectionDriftCommand, } from "./commands/protection.js";
12
+ import { agentsListCommand, agentsClaimCommand, agentsMoveCommand } from "./commands/agents.js";
13
+ import { orgListCommand, orgShowCommand, orgUseCommand } from "./commands/org.js";
14
14
  import { teamListCommand, teamShowCommand, teamTemplateCommand, teamPreviewComposeCommand, teamAdminGrantCommand, teamAdminRevokeCommand, teamAdminListCommand, teamCoverageCommand, } from "./commands/team.js";
15
15
  import { advisoriesListCommand, advisoriesShowCommand } from "./commands/advisories.js";
16
16
  import { postureListCommand, postureShowCommand, postureCreateCommand, postureUpdateCommand, postureCloneCommand, postureRevisionsCommand, postureDiffCommand, postureAssignCommand, postureUnassignCommand, posturePreviewComposeCommand, postureDeleteCommand, } from "./commands/posture.js";
@@ -20,7 +20,10 @@ import { apiKeyListCommand, apiKeyCreateCommand, apiKeyRotateCommand, apiKeyRevo
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
22
  import { tryMeCommand } from "./commands/try-me.js";
23
+ import { wrapCommand } from "./commands/wrap.js";
24
+ import { onboardCommand } from "./commands/onboard.js";
23
25
  import { skillsListCommand, skillsDescribeCommand } from "./commands/skills.js";
26
+ import { usageCommand, parseNumericFlag, parseDaysFlag } from "./commands/usage.js";
24
27
  program
25
28
  .name("mnemom")
26
29
  .description("Transparent AI agent tracing")
@@ -71,6 +74,78 @@ program
71
74
  process.exit(1);
72
75
  }
73
76
  });
77
+ // ── wrap (MNE-935, A4) — instrument an existing agent through the gateway ────
78
+ //
79
+ // `mnemom wrap` is the "bring your production agent" skill: it asks for
80
+ // provider + framework + agent name, births the agent through the gateway,
81
+ // seeds starter alignment + protection cards, and emits a drop-in code snippet.
82
+ // Extends the skill-runner contract from A1 (MNE-932). See commands/wrap.ts.
83
+ // ─────────────────────────────────────────────────────────────────────────────
84
+ program
85
+ .command("wrap")
86
+ .description("Instrument an existing agent through the Mnemom gateway (born → cards → snippet)")
87
+ .option("--provider <provider>", "AI provider: anthropic, openai, gemini (default: prompt)")
88
+ .option("--framework <framework>", "Language/framework: python, node (default: prompt)")
89
+ .option("--name <name>", "Agent name used as the x-mnemom-agent header value")
90
+ .option("--provider-key <key>", "Provider API key for the one-time birth call (default: reads env)")
91
+ .option("-y, --yes", "Non-interactive: accept defaults, skip all prompts")
92
+ .option("--json", "Emit machine-readable result (skill / verdict / agent_id / steps)")
93
+ .action(async (opts) => {
94
+ try {
95
+ await wrapCommand({
96
+ provider: opts.provider,
97
+ framework: opts.framework,
98
+ name: opts.name,
99
+ providerKey: opts.providerKey,
100
+ yes: opts.yes,
101
+ json: opts.json,
102
+ });
103
+ }
104
+ catch (error) {
105
+ console.error("Error:", error instanceof Error ? error.message : error);
106
+ process.exit(1);
107
+ }
108
+ });
109
+ // ── onboard (MNE-933, A2) — self-onboard the calling agent end-to-end ────────
110
+ //
111
+ // `mnemom onboard` runs the sovereignty path for the CALLING agent itself:
112
+ // scan its trust posture → claim its identity → declare an alignment card →
113
+ // surface its Trust Rating → hand back a public badge URL. One command, no
114
+ // manifest. Additive + human-in-the-loop preserving: the only mutation is the
115
+ // agent's own alignment-card declaration (its standing scoped token). The agent
116
+ // authenticates as its OWN device-grant principal (MNE-944). The Trust Rating
117
+ // is PROVISIONAL until the observer pipeline generates traces — the signed
118
+ // rating + rendered badge are computed server-side, post-hoc. See
119
+ // commands/onboard.ts for the auth model.
120
+ // ─────────────────────────────────────────────────────────────────────────────
121
+ program
122
+ .command("onboard")
123
+ .description("Self-onboard the calling agent (scan → claim → declare → rating → badge); the Trust Rating is provisional until the observer pipeline generates traces")
124
+ .option("--json", "Emit machine-readable step outcomes (implies non-interactive)")
125
+ .option("-y, --yes", "Non-interactive: accept defaults, skip all prompts")
126
+ .option("--key <key>", "The agent's provider API key — the CLI derives the claim hash proof")
127
+ .option("--hash-proof <hex>", "A pre-computed 64-hex hash proof (advanced/CI; alternative to --key)")
128
+ .option("--no-open", "Never auto-open the badge URL in a browser — just print it")
129
+ .action(async (opts) => {
130
+ try {
131
+ // The calling agent is selected via the program-level `--agent` (which
132
+ // shadows any subcommand `--agent` under commander@12 — the MNE-238
133
+ // class), mirroring how `status`/`logs` read it.
134
+ const parentOpts = program.opts();
135
+ await onboardCommand({
136
+ json: opts.json,
137
+ yes: opts.yes,
138
+ key: opts.key,
139
+ hashProof: opts.hashProof,
140
+ open: opts.open,
141
+ agent: parentOpts.agent,
142
+ });
143
+ }
144
+ catch (error) {
145
+ console.error("Error:", error instanceof Error ? error.message : error);
146
+ process.exit(1);
147
+ }
148
+ });
74
149
  // ── skills (MNE-1324) — discover the zero-install skill surface ──────────────
75
150
  const skills = program
76
151
  .command("skills")
@@ -346,6 +421,25 @@ protectionCmd
346
421
  process.exit(1);
347
422
  }
348
423
  });
424
+ protectionCmd
425
+ .command("drift")
426
+ .argument("<file>", "Path to a committed protection-card snapshot (YAML or JSON)")
427
+ .description("Compare a committed snapshot against the live canonical card (read-only)")
428
+ .option("--strict", "Also fail on live fields the snapshot does not record")
429
+ .option("--json", "Emit the drift result as JSON")
430
+ .action(async (file, subOpts) => {
431
+ try {
432
+ const opts = program.opts();
433
+ await protectionDriftCommand(file, opts.agent, {
434
+ strict: subOpts.strict,
435
+ json: subOpts.json,
436
+ });
437
+ }
438
+ catch (error) {
439
+ console.error("Error:", error instanceof Error ? error.message : error);
440
+ process.exit(1);
441
+ }
442
+ });
349
443
  // ============================================================================
350
444
  // Policy commands (removed — stubs with migration guidance)
351
445
  // ============================================================================
@@ -418,15 +512,27 @@ const agentsCmd = program
418
512
  agentsCmd
419
513
  .command("claim <id-or-name>")
420
514
  .description("Claim an agent into an org (ADR-062 claim-to-org)")
421
- .option("--org <slug>", "Org slug or id to claim into (default: your personal org)")
515
+ .option("--org <slug>", "Org slug or id to claim into (default: your ACTIVE org from `mnemom org use`, else your personal org)")
422
516
  .option("--key <key>", "The agent's provider API key — the CLI derives the hash proof from it")
423
517
  .option("--hash-proof <hex>", "A pre-computed 64-hex SHA-256 proof (advanced/CI; alternative to --key)")
424
518
  .option("--name <name>", "Provisioned agent name for proof derivation — auto-resolved from the agent id when possible; pass only to override")
425
519
  .option("--json", "Output JSON instead of human-readable text")
426
520
  .action(async (idOrName, opts) => {
427
521
  try {
522
+ // `agentsCmd` (the parent `agents` command) ALSO defines `--org` (for
523
+ // `mnemom agents --org <id>` list-scoping). Commander@12 binds a flag
524
+ // shared by a parent and a subcommand to whichever of the two owns it
525
+ // and runs first in the parse chain — here that's the parent — so
526
+ // `agents claim <id> --org <slug>` had its --org silently swallowed by
527
+ // `agentsCmd` before this subcommand's own `--org` option ever saw it
528
+ // (MNE-2496: confirmed live, the agent always landed in the caller's
529
+ // personal org regardless of --org). Same shadow class as the global
530
+ // `--agent` fix at MNE-238 (see the `advisories list`/`show` actions
531
+ // below) — resolve the local value first, falling back to the
532
+ // parent's captured value so either parse order still works.
533
+ const org = opts.org ?? agentsCmd.opts().org;
428
534
  await agentsClaimCommand(idOrName, {
429
- org: opts.org,
535
+ org,
430
536
  key: opts.key,
431
537
  hashProof: opts.hashProof,
432
538
  name: opts.name,
@@ -438,6 +544,21 @@ agentsCmd
438
544
  process.exit(1);
439
545
  }
440
546
  });
547
+ agentsCmd
548
+ .command("move <id-or-name>")
549
+ .description("Move an agent to another org — requires owner/admin in BOTH the current and destination org " +
550
+ "(role-based; no agent key needed, unlike re-claim)")
551
+ .option("--to <slug-or-id>", "Destination org (required; never defaults to the active org)")
552
+ .option("--json", "Output JSON instead of human-readable text")
553
+ .action(async (idOrName, opts) => {
554
+ try {
555
+ await agentsMoveCommand(idOrName, { to: opts.to, json: opts.json });
556
+ }
557
+ catch (error) {
558
+ console.error("Error:", error instanceof Error ? error.message : error);
559
+ process.exit(1);
560
+ }
561
+ });
441
562
  // ============================================================================
442
563
  // Organization management (ADR-044, Piece 1 of T1-3.1)
443
564
  // ============================================================================
@@ -457,6 +578,21 @@ orgCmd
457
578
  process.exit(1);
458
579
  }
459
580
  });
581
+ orgCmd
582
+ .command("use [slug-or-id]")
583
+ .description("Set the ACTIVE org — the default for org-scoped commands like `agents claim` " +
584
+ "(login binds no org; without this, claims land in your personal org). " +
585
+ "No argument shows the current setting.")
586
+ .option("--clear", "Forget the active org (org-scoped commands revert to your personal org)")
587
+ .action(async (slugOrId, options) => {
588
+ try {
589
+ await orgUseCommand(slugOrId, { clear: options.clear });
590
+ }
591
+ catch (error) {
592
+ console.error("Error:", error instanceof Error ? error.message : error);
593
+ process.exit(1);
594
+ }
595
+ });
460
596
  orgCmd
461
597
  .command("show [org_id]")
462
598
  .description("Show details of an org (default: your personal org if --personal, else single membership)")
@@ -1060,7 +1196,8 @@ postureCmd
1060
1196
  // ============================================================================
1061
1197
  program
1062
1198
  .command("login")
1063
- .description("Authenticate with your Mnemom account")
1199
+ .description("Authenticate with your Mnemom account (note: login binds no org — set a default " +
1200
+ "for org-scoped commands with `mnemom org use <slug>`)")
1064
1201
  .option("--no-browser", "Use the device code flow instead of opening a browser")
1065
1202
  .action(async (options) => {
1066
1203
  try {
@@ -1443,6 +1580,55 @@ program
1443
1580
  process.exit(1);
1444
1581
  }
1445
1582
  });
1583
+ // ============================================================================
1584
+ // Usage attribution (MNE-3215 W6 / issue #1221)
1585
+ //
1586
+ // `mnemom usage --org <id>` shows per-person token and request consumption
1587
+ // for an org window. Gated by USAGE_ATTRIBUTION_API_ENABLED on the API side
1588
+ // (defaults off in every environment). Reports consumption only — no currency,
1589
+ // no spend, no cost.
1590
+ //
1591
+ // Every flag below maps 1:1 onto a query parameter the endpoint documents in
1592
+ // the API's `openapi.json` (`days`, `person_id`, `provider`, `model`, `limit`,
1593
+ // `cursor`). It is deliberately not a friendlier invented vocabulary: the first
1594
+ // version of this command offered `--period` and `--page`, neither of which the
1595
+ // endpoint accepts, so the server silently ignored both and returned an
1596
+ // unfiltered first page.
1597
+ // ============================================================================
1598
+ program
1599
+ .command("usage")
1600
+ .description("Show per-person token and request consumption for an org")
1601
+ .requiredOption("--org <id>", "Org ID to show consumption for")
1602
+ .option("--days <7|30|90>", "Reporting window in days (default: 30)")
1603
+ .option("--person <id>", "Filter to one person's consumption")
1604
+ .option("--provider <name>", "Filter to one provider")
1605
+ .option("--model <name>", "Filter to one model")
1606
+ .option("--limit <n>", "Max rows per page (default: 100, max 200)")
1607
+ .option("--cursor <cursor>", "Continue from a previous page's cursor")
1608
+ .option("--json", "Emit raw JSON instead of rendered output")
1609
+ .action(async (options) => {
1610
+ try {
1611
+ // A miskeyed numeric flag (e.g. `--limit foo`, `--limit 2abc`,
1612
+ // `--limit 0`, `--days 45`) warns and falls back to the default rather
1613
+ // than silently acting on a value the user did not type. Both live in
1614
+ // commands/usage.ts so their reject-and-warn arms are unit-testable
1615
+ // without driving Commander.
1616
+ await usageCommand({
1617
+ org: options.org,
1618
+ days: parseDaysFlag(options.days),
1619
+ person: options.person,
1620
+ provider: options.provider,
1621
+ model: options.model,
1622
+ limit: parseNumericFlag("--limit", options.limit),
1623
+ cursor: options.cursor,
1624
+ json: options.json,
1625
+ });
1626
+ }
1627
+ catch (error) {
1628
+ console.error("Error:", error instanceof Error ? error.message : error);
1629
+ process.exit(1);
1630
+ }
1631
+ });
1446
1632
  // Export the fully-assembled commander program so tooling (e.g. the
1447
1633
  // command-tree snapshot generator in scripts/gen-command-tree.mjs) can
1448
1634
  // statically introspect the command surface WITHOUT executing the CLI.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Agent identity config store — ~/.mnemom/agent.json.
3
+ *
4
+ * Persists the calling agent's resolved identity so skill verbs (try-me,
5
+ * onboard, wrap) write on first success and read thereafter — no re-prompt for
6
+ * identity on subsequent invocations (MNE-937).
7
+ *
8
+ * Co-located with auth.ts → auth.json. Separate by design: auth.json holds
9
+ * bearer credentials (wiped on logout); agent.json holds identity metadata
10
+ * that survives re-login and provider key rotation.
11
+ */
12
+ export interface AgentConfig {
13
+ agent_id?: string;
14
+ agent_name?: string;
15
+ org_id?: string;
16
+ gateway_url?: string;
17
+ }
18
+ /** Load the agent config; a missing or corrupt file returns `{}`. */
19
+ export declare function loadAgentConfig(): AgentConfig;
20
+ /** Persist the full agent config (replaces the file). */
21
+ export declare function saveAgentConfig(config: AgentConfig): void;
22
+ /** Shallow-merge `partial` into the existing config and persist. */
23
+ export declare function mergeAgentConfig(partial: Partial<AgentConfig>): void;
24
+ export declare function getAgentId(): string | undefined;
25
+ export declare function getAgentName(): string | undefined;
26
+ export declare function getOrgId(): string | undefined;
27
+ export declare function getGatewayUrl(): string | undefined;