@mnemom/mnemom 0.12.1 → 0.14.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.
@@ -1,4 +1,4 @@
1
- import { resolveAgentId, getTraces } from "../lib/api.js";
1
+ import { resolveAgentId, getTraces, MnemomApiError } from "../lib/api.js";
2
2
  import { getGatewayUrl } from "../lib/config.js";
3
3
  import { fmt } from "../lib/format.js";
4
4
  export async function logsCommand(options = {}) {
@@ -23,12 +23,16 @@ export async function logsCommand(options = {}) {
23
23
  console.log(`Dashboard: https://mnemon.ai/dashboard/${agentId}\n`);
24
24
  }
25
25
  catch (error) {
26
- const message = error instanceof Error ? error.message : String(error);
27
- if (message.includes("404") || message.includes("not found")) {
26
+ // A 404 means the agent isn't registered yet — treat as the empty state.
27
+ // Branch on effectiveStatus (not status): the enforce hook rewrites an
28
+ // undocumented 404 to a synthetic 500 carrying spec_deviation.original_status;
29
+ // effectiveStatus surfaces the true status (=== status when documented).
30
+ if (error instanceof MnemomApiError && error.effectiveStatus === 404) {
28
31
  console.log(fmt.header("No traces found"));
29
32
  console.log("\nStart using Claude to generate traces.\n");
30
33
  }
31
34
  else {
35
+ const message = error instanceof Error ? error.message : String(error);
32
36
  console.log("\n" + fmt.error(`Failed to fetch traces: ${message}`) + "\n");
33
37
  process.exit(1);
34
38
  }
@@ -36,16 +40,28 @@ export async function logsCommand(options = {}) {
36
40
  }
37
41
  function displayTrace(trace) {
38
42
  const timestamp = formatTimestamp(trace.timestamp);
39
- const statusMsg = trace.verified ? fmt.success(timestamp) : fmt.error(`${timestamp} [VIOLATION]`);
43
+ // `verification` is null when the trace hasn't been verified yet that is NOT
44
+ // a violation. Only flag [VIOLATION] when verification exists AND verified===false.
45
+ // (The old flat `trace.verified` was always undefined on the nested wire → every
46
+ // trace was mis-flagged [VIOLATION] and the action rendered as "[object Object]".)
47
+ const verified = trace.verification?.verified ?? true;
48
+ const statusMsg = verified ? fmt.success(timestamp) : fmt.error(`${timestamp} [VIOLATION]`);
40
49
  console.log(`\n ${statusMsg}`);
41
- console.log(` ${fmt.label("Action:", ` ${trace.action}`)}`);
42
- if (trace.tool_name) {
43
- console.log(` ${fmt.label("Tool: ", ` ${trace.tool_name}`)}`);
50
+ // action.name is the canonical label (the tool name lives here in the AIP-nested
51
+ // shape); fall back to the action type, then a dash.
52
+ const actionLabel = trace.action?.name ?? trace.action?.type ?? "—";
53
+ console.log(` ${fmt.label("Action:", ` ${actionLabel}`)}`);
54
+ if (trace.action?.category) {
55
+ console.log(` ${fmt.label("Type: ", ` ${trace.action.category}`)}`);
44
56
  }
45
- if (trace.reasoning) {
46
- const preview = truncate(trace.reasoning, 60);
57
+ const reasoning = trace.decision?.selection_reasoning;
58
+ if (reasoning) {
59
+ const preview = truncate(reasoning, 60);
47
60
  console.log(` ${fmt.label("Reason:", ` ${preview}`)}`);
48
61
  }
62
+ if (trace.verification && trace.verification.violations.length > 0) {
63
+ console.log(` ${fmt.label("Issues:", ` ${trace.verification.violations.join(", ")}`)}`);
64
+ }
49
65
  }
50
66
  function formatTimestamp(iso) {
51
67
  try {
@@ -16,7 +16,10 @@ export declare function protectionShowCommand(agentName?: string): Promise<void>
16
16
  export declare function protectionPublishCommand(file: string, agentName?: string, options?: {
17
17
  idempotencyKey?: string;
18
18
  }): Promise<void>;
19
- export declare function protectionValidateCommand(file: string): Promise<void>;
19
+ export declare function protectionValidateCommand(file: string, opts?: {
20
+ offline?: boolean;
21
+ agent?: string;
22
+ }): Promise<void>;
20
23
  export declare function protectionEditCommand(agentName?: string, options?: {
21
24
  idempotencyKey?: string;
22
25
  }): Promise<void>;
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import * as os from "node:os";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import yaml from "js-yaml";
6
- import { PROTECTION_CARD_MAX_BYTES, getProtectionCard, putProtectionCard, resolveAgentId, } from "../lib/api.js";
6
+ import { PROTECTION_CARD_MAX_BYTES, getProtectionCard, putProtectionCard, resolveAgentId, getAgentByName, previewComposeAgentCard, MnemomApiError, } from "../lib/api.js";
7
7
  import { requireAuth } from "../lib/auth.js";
8
8
  import { fmt } from "../lib/format.js";
9
9
  import { askYesNo, isInteractive } from "../lib/prompt.js";
@@ -443,7 +443,28 @@ export async function protectionPublishCommand(file, agentName, options = {}) {
443
443
  process.exit(1);
444
444
  }
445
445
  }
446
- export async function protectionValidateCommand(file) {
446
+ // Agent-resolution regex (distinct from the trusted-sources AGENT_ID_RE above,
447
+ // which validates card-embedded agent ids). This matches the gateway agent-id
448
+ // shapes used by resolveAgentId so we can use a value directly without a lookup.
449
+ const RESOLVE_AGENT_ID_RE = /^(smolt-[0-9a-f]{8}|mnm-[0-9a-f-]{36})$/;
450
+ /**
451
+ * Soft agent resolution for `validate`: returns an agent id WITHOUT exiting the
452
+ * process. null ⇒ no agent configured / unresolvable → offline fallback.
453
+ */
454
+ async function softResolveAgentId(agent) {
455
+ const name = agent ?? process.env.MNEMOM_AGENT;
456
+ if (!name)
457
+ return null;
458
+ if (RESOLVE_AGENT_ID_RE.test(name))
459
+ return name;
460
+ try {
461
+ return (await getAgentByName(name))?.id ?? null;
462
+ }
463
+ catch {
464
+ return null;
465
+ }
466
+ }
467
+ export async function protectionValidateCommand(file, opts = {}) {
447
468
  const filePath = path.resolve(file);
448
469
  if (!fs.existsSync(filePath)) {
449
470
  console.log("\n" + fmt.error(`File not found: ${filePath}`) + "\n");
@@ -458,6 +479,30 @@ export async function protectionValidateCommand(file) {
458
479
  console.log("\n" + fmt.error(`Could not parse file: ${msg}`) + "\n");
459
480
  process.exit(1);
460
481
  }
482
+ // Prefer server-authoritative validation when online + an agent is available;
483
+ // fall back to the local validator on 401/network. `--offline` forces local. (#9)
484
+ if (!opts.offline) {
485
+ const agentId = await softResolveAgentId(opts.agent);
486
+ if (agentId) {
487
+ const contentType = parsed.format === "yaml" ? "text/yaml" : "application/json";
488
+ try {
489
+ const result = await previewComposeAgentCard(agentId, "protection", parsed.raw, contentType);
490
+ renderServerProtectionValidation(result, filePath, parsed.format);
491
+ return;
492
+ }
493
+ catch (err) {
494
+ if (err instanceof MnemomApiError && err.status !== 401) {
495
+ console.log("\n" + fmt.error(`Server validation failed: ${err.message}`) + "\n");
496
+ process.exit(1);
497
+ }
498
+ process.stderr.write(fmt.warn("offline validation — server rules may differ") + "\n");
499
+ }
500
+ }
501
+ else if (!opts.agent && !process.env.MNEMOM_AGENT) {
502
+ process.stderr.write(fmt.dim("tip: pass --agent <name> to validate against the server (org/platform floor)") +
503
+ "\n");
504
+ }
505
+ }
461
506
  const checks = validateProtectionCard(parsed.parsed);
462
507
  const allPassed = checks.every((c) => c.passed);
463
508
  const passCount = checks.filter((c) => c.passed).length;
@@ -484,6 +529,33 @@ export async function protectionValidateCommand(file) {
484
529
  process.exit(1);
485
530
  }
486
531
  }
532
+ /** Render a server-authoritative preview-compose result; exit 1 if invalid. */
533
+ function renderServerProtectionValidation(result, filePath, format) {
534
+ console.log(fmt.header("Protection Card Validation (server-authoritative)"));
535
+ console.log();
536
+ console.log(fmt.label(" File:", ` ${filePath}`));
537
+ console.log(fmt.label(" Format:", ` ${format.toUpperCase()}`));
538
+ console.log();
539
+ if (result.valid) {
540
+ console.log(fmt.success("Server accepted and composed the card."));
541
+ const conflicts = result.conflicts ?? [];
542
+ if (conflicts.length > 0) {
543
+ console.log(fmt.warn(`${conflicts.length} field(s) tightened by the org/platform floor:`));
544
+ console.log(fmt.json(conflicts));
545
+ }
546
+ console.log();
547
+ return;
548
+ }
549
+ console.log(fmt.error(`Server rejected the card${result.error?.code ? ` (${result.error.code})` : ""}:`));
550
+ if (result.error?.message)
551
+ console.log(` ${result.error.message}`);
552
+ if (result.error?.details !== undefined) {
553
+ console.log();
554
+ console.log(fmt.json(result.error.details));
555
+ }
556
+ console.log();
557
+ process.exit(1);
558
+ }
487
559
  export async function protectionEditCommand(agentName, options = {}) {
488
560
  const agentId = await resolveAgentId(agentName);
489
561
  await requireAuth();
@@ -506,8 +578,11 @@ export async function protectionEditCommand(agentName, options = {}) {
506
578
  },
507
579
  trusted_sources: { domains: [], agent_ids: [], ip_ranges: [] },
508
580
  }, { lineWidth: 120, noRefs: true });
509
- const tmpDir = os.tmpdir();
510
- const tmpFile = path.join(tmpDir, `mnemom-protection-${agentId}.yaml`);
581
+ // Per-invocation temp dir via mkdtemp (mode 0700, unpredictable suffix) so
582
+ // the editor file can't be pre-created or symlink-raced by another user in
583
+ // the shared os.tmpdir().
584
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mnemom-protection-"));
585
+ const tmpFile = path.join(tmpDir, `${agentId}.yaml`);
511
586
  fs.writeFileSync(tmpFile, cardYaml);
512
587
  const editor = process.env.EDITOR || process.env.VISUAL || "vi";
513
588
  console.log(`Opening ${editor}...`);
@@ -1,5 +1,5 @@
1
1
  import { getGatewayUrl } from "../lib/config.js";
2
- import { resolveAgentId, getAgent, getIntegrity, getTraces } from "../lib/api.js";
2
+ import { resolveAgentId, getAgent, getIntegrity, getTraces, MnemomApiError } from "../lib/api.js";
3
3
  import { isLoggedIn, getAuthInfo } from "../lib/auth.js";
4
4
  import { fmt } from "../lib/format.js";
5
5
  const DASHBOARD_URL = "https://mnemom.ai";
@@ -133,8 +133,10 @@ async function checkApiConnectivity(agentId) {
133
133
  };
134
134
  }
135
135
  catch (error) {
136
- const message = error instanceof Error ? error.message : String(error);
137
- if (message.includes("404") || message.includes("not found")) {
136
+ // 404 = agent not registered yet. Branch on effectiveStatus (enforce hook may
137
+ // rewrite an undocumented 404 synthetic 500 + spec_deviation.original_status;
138
+ // effectiveStatus === status when documented). getAgent → fetchApi → MnemomApiError.
139
+ if (error instanceof MnemomApiError && error.effectiveStatus === 404) {
138
140
  return {
139
141
  name: "API",
140
142
  status: "warning",
@@ -142,7 +144,13 @@ async function checkApiConnectivity(agentId) {
142
144
  details: "Will register on first traced API call",
143
145
  };
144
146
  }
145
- if (message.includes("timeout") || message.includes("TIMEOUT")) {
147
+ const message = error instanceof Error ? error.message : String(error);
148
+ // Transport-level timeout (no HTTP status, not an envelope error) — detect by
149
+ // the AbortSignal.timeout error name, with a message fallback for runtimes that
150
+ // don't set it.
151
+ if ((error instanceof Error && error.name === "TimeoutError") ||
152
+ message.includes("timeout") ||
153
+ message.includes("TIMEOUT")) {
146
154
  return {
147
155
  name: "API",
148
156
  status: "error",
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import { program } from "commander";
3
+ export { program };
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { pathToFileURL } from "node:url";
2
3
  import { program } from "commander";
4
+ import { CLI_VERSION } from "./version.js";
3
5
  import { statusCommand } from "./commands/status.js";
4
6
  import { integrityCommand } from "./commands/integrity.js";
5
7
  import { logsCommand } from "./commands/logs.js";
@@ -7,7 +9,7 @@ import { licenseActivateCommand, licenseStatusCommand, licenseDeactivateCommand,
7
9
  import { cardShowCommand, cardPublishCommand, cardValidateCommand, cardEditCommand, cardEvaluateCommand, } from "./commands/card.js";
8
10
  import { policyInitCommand, policyValidateCommand, policyPublishCommand, policyListCommand, policyTestCommand, policyEvaluateCommand, } from "./commands/policy.js";
9
11
  import { protectionShowCommand, protectionPublishCommand, protectionValidateCommand, protectionEditCommand, } from "./commands/protection.js";
10
- import { agentsListCommand } from "./commands/agents.js";
12
+ import { agentsListCommand, agentsClaimCommand } from "./commands/agents.js";
11
13
  import { orgListCommand, orgShowCommand } from "./commands/org.js";
12
14
  import { teamListCommand, teamShowCommand, teamTemplateCommand, teamPreviewComposeCommand, teamAdminGrantCommand, teamAdminRevokeCommand, teamAdminListCommand, teamCoverageCommand, } from "./commands/team.js";
13
15
  import { advisoriesListCommand, advisoriesShowCommand } from "./commands/advisories.js";
@@ -20,7 +22,7 @@ import { listenCommand } from "./commands/listen.js";
20
22
  program
21
23
  .name("mnemom")
22
24
  .description("Transparent AI agent tracing")
23
- .version("0.9.1")
25
+ .version(CLI_VERSION)
24
26
  .option("--agent <name>", "Select agent by name (or set MNEMOM_AGENT)");
25
27
  program
26
28
  .command("status")
@@ -149,10 +151,12 @@ cardCmd
149
151
  cardCmd
150
152
  .command("validate")
151
153
  .argument("<file>", "Path to alignment card file (YAML or JSON)")
152
- .description("Validate alignment card locally")
153
- .action(async (file) => {
154
+ .description("Validate an alignment card (server-authoritative when --agent is set; --offline forces local)")
155
+ .option("--offline", "Validate locally only (skip the server preview-compose)")
156
+ .action(async (file, subOpts) => {
154
157
  try {
155
- await cardValidateCommand(file);
158
+ const opts = program.opts();
159
+ await cardValidateCommand(file, { offline: subOpts.offline, agent: opts.agent });
156
160
  }
157
161
  catch (error) {
158
162
  console.error("Error:", error instanceof Error ? error.message : error);
@@ -224,10 +228,12 @@ protectionCmd
224
228
  protectionCmd
225
229
  .command("validate")
226
230
  .argument("<file>", "Path to protection card file (YAML or JSON)")
227
- .description("Validate protection card locally")
228
- .action(async (file) => {
231
+ .description("Validate a protection card (server-authoritative when --agent is set; --offline forces local)")
232
+ .option("--offline", "Validate locally only (skip the server preview-compose)")
233
+ .action(async (file, subOpts) => {
229
234
  try {
230
- await protectionValidateCommand(file);
235
+ const opts = program.opts();
236
+ await protectionValidateCommand(file, { offline: subOpts.offline, agent: opts.agent });
231
237
  }
232
238
  catch (error) {
233
239
  console.error("Error:", error instanceof Error ? error.message : error);
@@ -290,12 +296,36 @@ policyCmd
290
296
  // ============================================================================
291
297
  // Agent management
292
298
  // ============================================================================
293
- program
299
+ const agentsCmd = program
294
300
  .command("agents")
295
- .description("List agents in your account")
296
- .action(async () => {
301
+ .description("List agents across the orgs you belong to (ADR-062 org-scoped)")
302
+ .option("--org <id>", "Scope to a single org (default: all orgs you belong to)")
303
+ .action(async (opts) => {
297
304
  try {
298
- await agentsListCommand();
305
+ await agentsListCommand({ org: opts.org });
306
+ }
307
+ catch (error) {
308
+ console.error("Error:", error instanceof Error ? error.message : error);
309
+ process.exit(1);
310
+ }
311
+ });
312
+ agentsCmd
313
+ .command("claim <id-or-name>")
314
+ .description("Claim an agent into an org (ADR-062 claim-to-org)")
315
+ .option("--org <slug>", "Org slug or id to claim into (default: your personal org)")
316
+ .option("--key <key>", "The agent's provider API key — the CLI derives the hash proof from it")
317
+ .option("--hash-proof <hex>", "A pre-computed 64-hex SHA-256 proof (advanced/CI; alternative to --key)")
318
+ .option("--name <name>", "Provisioned agent name used in proof derivation (defaults to a name-shaped positional arg)")
319
+ .option("--json", "Output JSON instead of human-readable text")
320
+ .action(async (idOrName, opts) => {
321
+ try {
322
+ await agentsClaimCommand(idOrName, {
323
+ org: opts.org,
324
+ key: opts.key,
325
+ hashProof: opts.hashProof,
326
+ name: opts.name,
327
+ json: opts.json,
328
+ });
299
329
  }
300
330
  catch (error) {
301
331
  console.error("Error:", error instanceof Error ? error.message : error);
@@ -1291,4 +1321,13 @@ program
1291
1321
  process.exit(1);
1292
1322
  }
1293
1323
  });
1294
- program.parse();
1324
+ // Export the fully-assembled commander program so tooling (e.g. the
1325
+ // command-tree snapshot generator in scripts/gen-command-tree.mjs) can
1326
+ // statically introspect the command surface WITHOUT executing the CLI.
1327
+ export { program };
1328
+ // Parse argv only when invoked as the CLI entrypoint — not when imported.
1329
+ // (ESM has no `require.main`; compare this module's URL to argv[1].)
1330
+ const invokedDirectly = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
1331
+ if (invokedDirectly) {
1332
+ program.parse();
1333
+ }