@mnemom/mnemom 0.12.0 → 0.13.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.
@@ -29,7 +29,10 @@ export declare function cardShowCommand(agentName?: string): Promise<void>;
29
29
  export declare function cardPublishCommand(file: string, agentName?: string, options?: {
30
30
  idempotencyKey?: string;
31
31
  }): Promise<void>;
32
- export declare function cardValidateCommand(file: string): Promise<void>;
32
+ export declare function cardValidateCommand(file: string, opts?: {
33
+ offline?: boolean;
34
+ agent?: string;
35
+ }): Promise<void>;
33
36
  export declare function cardEditCommand(agentName?: string, options?: {
34
37
  idempotencyKey?: string;
35
38
  }): 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 { ALIGNMENT_CARD_MAX_BYTES, getAlignmentCard, putAlignmentCard, resolveAgentId, } from "../lib/api.js";
6
+ import { ALIGNMENT_CARD_MAX_BYTES, getAlignmentCard, putAlignmentCard, 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";
@@ -194,6 +194,28 @@ export function validateUnifiedCard(card) {
194
194
  else {
195
195
  const v = card.values;
196
196
  const decl = v.declared;
197
+ // A declared entry is a non-empty STRING or a parameterized OBJECT
198
+ // { id, ...string params } — mirrors mnemom-api/src/composition/validate.ts
199
+ // `declaredValueRefId`. (#8 DELTA-1: the old `decl.every(typeof === "string")`
200
+ // rejected the valid object form, making the CLI STRICTER than the server —
201
+ // a false-negative that blocked good cards in pre-flight.)
202
+ const declRefId = (entry) => {
203
+ if (typeof entry === "string")
204
+ return entry.length > 0 ? entry : null;
205
+ if (!isObj(entry))
206
+ return null;
207
+ const o = entry;
208
+ if (typeof o.id !== "string" || o.id.length === 0)
209
+ return null;
210
+ for (const [k, pv] of Object.entries(o)) {
211
+ if (k === "id")
212
+ continue;
213
+ if (typeof pv !== "string")
214
+ return null;
215
+ }
216
+ return o.id;
217
+ };
218
+ const declIds = [];
197
219
  if (decl.length === 0) {
198
220
  checks.push({
199
221
  name: "values.declared",
@@ -201,21 +223,32 @@ export function validateUnifiedCard(card) {
201
223
  message: "Must contain at least one value.",
202
224
  });
203
225
  }
204
- else if (!decl.every((s) => typeof s === "string")) {
205
- checks.push({
206
- name: "values.declared",
207
- passed: false,
208
- message: "All entries must be strings.",
209
- });
210
- }
211
226
  else {
212
- checks.push({
213
- name: "values.declared",
214
- passed: true,
215
- message: `${decl.length} value(s) declared`,
227
+ let badRef = false;
228
+ decl.forEach((entry, i) => {
229
+ const id = declRefId(entry);
230
+ if (id === null) {
231
+ checks.push({
232
+ name: `values.declared[${i}]`,
233
+ passed: false,
234
+ message: "Must be a non-empty string or an object with a non-empty string `id` and string-only parameters.",
235
+ });
236
+ badRef = true;
237
+ }
238
+ else {
239
+ declIds.push(id);
240
+ }
216
241
  });
242
+ if (!badRef) {
243
+ checks.push({
244
+ name: "values.declared",
245
+ passed: true,
246
+ message: `${decl.length} value(s) declared`,
247
+ });
248
+ }
217
249
  }
218
- // definitions ⊆ declared (ADR-039 Decision 10)
250
+ // definitions ⊆ declared (ADR-039 Decision 10) — build the subset Set from
251
+ // the RESOLVED declared ids (string entries + each object's `id`).
219
252
  if (v.definitions !== undefined) {
220
253
  if (!isObj(v.definitions)) {
221
254
  checks.push({
@@ -225,7 +258,7 @@ export function validateUnifiedCard(card) {
225
258
  });
226
259
  }
227
260
  else {
228
- const declSet = new Set(decl.filter((s) => typeof s === "string"));
261
+ const declSet = new Set(declIds);
229
262
  for (const key of Object.keys(v.definitions)) {
230
263
  if (!declSet.has(key)) {
231
264
  checks.push({
@@ -626,7 +659,27 @@ export async function cardPublishCommand(file, agentName, options = {}) {
626
659
  process.exit(1);
627
660
  }
628
661
  }
629
- export async function cardValidateCommand(file) {
662
+ const AGENT_ID_RE = /^(smolt-[0-9a-f]{8}|mnm-[0-9a-f-]{36})$/;
663
+ /**
664
+ * Soft agent resolution for `validate`: returns an agent id WITHOUT exiting the
665
+ * process (unlike resolveAgentId). Returns null when no agent is configured, or
666
+ * the name can't be resolved (not authenticated / not found) — the caller then
667
+ * falls back to offline validation.
668
+ */
669
+ async function softResolveAgentId(agent) {
670
+ const name = agent ?? process.env.MNEMOM_AGENT;
671
+ if (!name)
672
+ return null;
673
+ if (AGENT_ID_RE.test(name))
674
+ return name;
675
+ try {
676
+ return (await getAgentByName(name))?.id ?? null;
677
+ }
678
+ catch {
679
+ return null;
680
+ }
681
+ }
682
+ export async function cardValidateCommand(file, opts = {}) {
630
683
  // Resolve file path
631
684
  const filePath = path.resolve(file);
632
685
  if (!fs.existsSync(filePath)) {
@@ -643,7 +696,35 @@ export async function cardValidateCommand(file) {
643
696
  console.log("\n" + fmt.error(`Could not parse file: ${msg}`) + "\n");
644
697
  process.exit(1);
645
698
  }
646
- // Validate the parsed object
699
+ // Prefer server-authoritative validation (composes against the agent's
700
+ // org/platform floor — catches conflicts the offline validator cannot) when
701
+ // online + an agent is available. Fall back to the local validator on
702
+ // 401/network. `--offline` forces local-only (#9).
703
+ if (!opts.offline) {
704
+ const agentId = await softResolveAgentId(opts.agent);
705
+ if (agentId) {
706
+ const contentType = parsed.format === "yaml" ? "text/yaml" : "application/json";
707
+ try {
708
+ const result = await previewComposeAgentCard(agentId, "alignment", parsed.raw, contentType);
709
+ renderServerCardValidation(result, filePath, parsed.format);
710
+ return;
711
+ }
712
+ catch (err) {
713
+ if (err instanceof MnemomApiError && err.status !== 401) {
714
+ // 403 / 5xx — a genuine server error, not the offline-fallback case.
715
+ console.log("\n" + fmt.error(`Server validation failed: ${err.message}`) + "\n");
716
+ process.exit(1);
717
+ }
718
+ // 401 or network error → fall through to offline validation.
719
+ process.stderr.write(fmt.warn("offline validation — server rules may differ") + "\n");
720
+ }
721
+ }
722
+ else if (!opts.agent && !process.env.MNEMOM_AGENT) {
723
+ process.stderr.write(fmt.dim("tip: pass --agent <name> to validate against the server (org/platform floor)") +
724
+ "\n");
725
+ }
726
+ }
727
+ // Offline / fallback path: the local hand-rolled validator (also `--offline`).
647
728
  const checks = validateUnifiedCard(parsed.parsed);
648
729
  const allPassed = checks.every((c) => c.passed);
649
730
  const passCount = checks.filter((c) => c.passed).length;
@@ -670,6 +751,38 @@ export async function cardValidateCommand(file) {
670
751
  process.exit(1);
671
752
  }
672
753
  }
754
+ /** Render a server-authoritative preview-compose result; exit 1 if invalid. */
755
+ function renderServerCardValidation(result, filePath, format) {
756
+ console.log(fmt.header("Card Validation (server-authoritative)"));
757
+ console.log();
758
+ console.log(fmt.label(" File:", ` ${filePath}`));
759
+ console.log(fmt.label(" Format:", ` ${format.toUpperCase()}`));
760
+ console.log();
761
+ if (result.valid) {
762
+ console.log(fmt.success("Server accepted and composed the card."));
763
+ const conflicts = result.conflicts ?? [];
764
+ if (conflicts.length > 0) {
765
+ console.log(fmt.warn(`${conflicts.length} field(s) tightened by the org/platform floor:`));
766
+ console.log(fmt.json(conflicts));
767
+ }
768
+ const coherence = result.coherence_violations ?? [];
769
+ if (coherence.length > 0) {
770
+ console.log(fmt.warn(`${coherence.length} coherence finding(s):`));
771
+ console.log(fmt.json(coherence));
772
+ }
773
+ console.log();
774
+ return;
775
+ }
776
+ console.log(fmt.error(`Server rejected the card${result.error?.code ? ` (${result.error.code})` : ""}:`));
777
+ if (result.error?.message)
778
+ console.log(` ${result.error.message}`);
779
+ if (result.error?.details !== undefined) {
780
+ console.log();
781
+ console.log(fmt.json(result.error.details));
782
+ }
783
+ console.log();
784
+ process.exit(1);
785
+ }
673
786
  export async function cardEditCommand(agentName, options = {}) {
674
787
  const agentId = await resolveAgentId(agentName);
675
788
  await requireAuth();
@@ -1,4 +1,4 @@
1
- import { resolveAgentId, getIntegrity } from "../lib/api.js";
1
+ import { resolveAgentId, getIntegrity, MnemomApiError } from "../lib/api.js";
2
2
  import { fmt } from "../lib/format.js";
3
3
  export async function integrityCommand(agentName) {
4
4
  const agentId = await resolveAgentId(agentName);
@@ -25,8 +25,11 @@ export async function integrityCommand(agentName) {
25
25
  }
26
26
  }
27
27
  catch (error) {
28
- const message = error instanceof Error ? error.message : String(error);
29
- if (message.includes("404") || message.includes("not found")) {
28
+ // A 404 means no integrity record exists yet — render the friendly empty state.
29
+ // Branch on effectiveStatus (not status): the enforce hook rewrites an
30
+ // undocumented 404 to a synthetic 500 carrying spec_deviation.original_status,
31
+ // and effectiveStatus surfaces the true status (=== status when documented).
32
+ if (error instanceof MnemomApiError && error.effectiveStatus === 404) {
30
33
  console.log(fmt.header("Integrity Score"));
31
34
  console.log(` ${fmt.label("Score: ", "N/A")}`);
32
35
  console.log(` ${fmt.label("Total: ", "0 traces")}`);
@@ -35,6 +38,7 @@ export async function integrityCommand(agentName) {
35
38
  console.log("\nNo traces recorded yet. Start using Claude to build your integrity score.\n");
36
39
  }
37
40
  else {
41
+ const message = error instanceof Error ? error.message : String(error);
38
42
  console.log("\n" + fmt.error(`Failed to fetch integrity score: ${message}`) + "\n");
39
43
  process.exit(1);
40
44
  }
@@ -1,10 +1,7 @@
1
1
  import { getLicenseJwt, saveLicenseJwt, clearLicenseJwt } from "../lib/auth.js";
2
- import { API_BASE } from "../lib/api.js";
2
+ import { validateLicense, MnemomApiError } from "../lib/api.js";
3
3
  import { fmt } from "../lib/format.js";
4
- /** Sanitize file-sourced data before use in outbound HTTP requests. */
5
- function sanitizeForHttp(data) {
6
- return String(data).trim();
7
- }
4
+ import { CLI_VERSION } from "../version.js";
8
5
  /**
9
6
  * Decode a JWT payload without verifying the signature.
10
7
  */
@@ -34,39 +31,37 @@ export async function licenseActivateCommand(jwt) {
34
31
  process.exit(1);
35
32
  }
36
33
  console.log("\nActivating enterprise license...\n");
37
- // Validate against API (if reachable)
34
+ // Validate against the API via the canonical lib helper (UNAUTH by design —
35
+ // the JWT is the credential). validateLicense throws MnemomApiError on non-2xx,
36
+ // so we render `.message` (the old hand-rolled `err.error` string-coerced the
37
+ // nested {code,message} envelope object → the live "[object Object]" bug).
38
38
  const hostname = (await import("node:os")).hostname();
39
39
  try {
40
- const licenseUrl = new URL(`${API_BASE}/v1/license/validate`).href;
41
- const response = await fetch(licenseUrl, {
42
- method: "POST",
43
- headers: { "Content-Type": "application/json" },
44
- body: JSON.stringify({
45
- license: jwt,
46
- instance_id: hostname,
47
- instance_metadata: {
48
- hostname,
49
- platform: process.platform,
50
- cli_version: "2.1.0",
51
- },
52
- }),
40
+ const result = await validateLicense(jwt, hostname, {
41
+ hostname,
42
+ platform: process.platform,
43
+ cli_version: CLI_VERSION,
53
44
  });
54
- if (response.ok) {
55
- const result = (await response.json());
45
+ if (result.valid) {
56
46
  console.log(" License validated successfully!\n");
57
- if (result.warning) {
58
- console.log(` Warning: ${result.warning}\n`);
59
- }
60
47
  }
61
48
  else {
62
- const err = (await response.json());
63
- console.log(` Warning: Validation returned ${response.status}: ${err.error || "unknown"}`);
64
- console.log(" License stored locally (will retry validation).\n");
49
+ // 2xx body with valid:false — grace period / activation limit reached.
50
+ console.log(" Note: server returned valid:false (grace period or activation limit).\n");
51
+ }
52
+ if (result.warning) {
53
+ console.log(` Warning: ${result.warning}\n`);
65
54
  }
66
55
  }
67
- catch {
68
- console.log(" Warning: Could not reach API for validation.");
69
- console.log(" License stored locally (offline mode).\n");
56
+ catch (err) {
57
+ if (err instanceof MnemomApiError) {
58
+ console.log(` Warning: Validation returned ${err.status}: ${err.message}`);
59
+ console.log(" License stored locally (will retry validation).\n");
60
+ }
61
+ else {
62
+ console.log(" Warning: Could not reach API for validation.");
63
+ console.log(" License stored locally (offline mode).\n");
64
+ }
70
65
  }
71
66
  // Store in auth store
72
67
  saveLicenseJwt(jwt);
@@ -131,24 +126,17 @@ export async function licenseDeactivateCommand() {
131
126
  console.log("\nNo enterprise license to deactivate.\n");
132
127
  return;
133
128
  }
134
- // Try to deactivate via API
129
+ // Try to deactivate via API (best-effort, fire-and-forget — reuses the
130
+ // validate endpoint with deactivating:true; local removal below is the source
131
+ // of truth). Ignore both the result and any MnemomApiError.
135
132
  const claims = decodeJwtPayload(licenseJwt);
136
133
  if (claims) {
137
134
  try {
138
135
  const hostname = (await import("node:os")).hostname();
139
- const deactivateUrl = new URL(`${API_BASE}/v1/license/validate`).href;
140
- await fetch(deactivateUrl, {
141
- method: "POST",
142
- headers: { "Content-Type": "application/json" },
143
- body: sanitizeForHttp(JSON.stringify({
144
- license: String(licenseJwt),
145
- instance_id: hostname,
146
- instance_metadata: { deactivating: true },
147
- })),
148
- });
136
+ await validateLicense(String(licenseJwt), hostname, { deactivating: true });
149
137
  }
150
138
  catch {
151
- // Best-effort
139
+ // Best-effort — ignore.
152
140
  }
153
141
  }
154
142
  // Remove from auth store
@@ -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();
@@ -23,7 +23,7 @@
23
23
  * filed" anchor and the candidate_id is the primary track-handle.
24
24
  */
25
25
  import chalk from "chalk";
26
- import { reportRecipeFnFp, } from "../lib/api.js";
26
+ import { reportRecipeFnFp } from "../lib/api.js";
27
27
  import { requireAuth } from "../lib/auth.js";
28
28
  async function readSummary(opts) {
29
29
  if (typeof opts.summary === "string" && opts.summary.trim()) {
@@ -42,7 +42,7 @@ async function readSummary(opts) {
42
42
  if (piped)
43
43
  return piped;
44
44
  }
45
- throw new Error("summary required: pass --summary \"…\" or pipe text on stdin.");
45
+ throw new Error('summary required: pass --summary "" or pipe text on stdin.');
46
46
  }
47
47
  async function runReport(recipeId, type, opts) {
48
48
  await requireAuth();
@@ -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",