@mnemom/mnemom 0.16.2 → 0.17.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/agents.d.ts +14 -0
  3. package/dist/commands/agents.js +100 -2
  4. package/dist/commands/card.d.ts +43 -0
  5. package/dist/commands/card.js +153 -102
  6. package/dist/commands/code-config.d.ts +17 -0
  7. package/dist/commands/code-config.js +147 -0
  8. package/dist/commands/code-doctor.d.ts +18 -0
  9. package/dist/commands/code-doctor.js +138 -0
  10. package/dist/commands/code-setup.d.ts +97 -0
  11. package/dist/commands/code-setup.js +330 -0
  12. package/dist/commands/code.d.ts +133 -0
  13. package/dist/commands/code.js +661 -0
  14. package/dist/commands/logs.js +11 -1
  15. package/dist/commands/onboard.d.ts +59 -0
  16. package/dist/commands/onboard.js +395 -0
  17. package/dist/commands/org.d.ts +13 -0
  18. package/dist/commands/org.js +63 -2
  19. package/dist/commands/protection.d.ts +10 -0
  20. package/dist/commands/protection.js +109 -0
  21. package/dist/commands/status.js +5 -0
  22. package/dist/commands/try-me.js +9 -0
  23. package/dist/commands/usage.d.ts +35 -0
  24. package/dist/commands/usage.js +265 -0
  25. package/dist/commands/wrap.d.ts +28 -0
  26. package/dist/commands/wrap.js +331 -0
  27. package/dist/index.js +315 -7
  28. package/dist/lib/agent-config.d.ts +27 -0
  29. package/dist/lib/agent-config.js +86 -0
  30. package/dist/lib/api.d.ts +139 -1
  31. package/dist/lib/api.js +132 -183
  32. package/dist/lib/cli-config.d.ts +33 -0
  33. package/dist/lib/cli-config.js +70 -0
  34. package/dist/lib/code-config.d.ts +78 -0
  35. package/dist/lib/code-config.js +281 -0
  36. package/dist/lib/code.d.ts +154 -0
  37. package/dist/lib/code.js +252 -0
  38. package/dist/lib/config.d.ts +10 -0
  39. package/dist/lib/config.js +39 -3
  40. package/dist/lib/keyed-identity.d.ts +35 -0
  41. package/dist/lib/keyed-identity.js +363 -0
  42. package/dist/lib/protection-drift.d.ts +117 -0
  43. package/dist/lib/protection-drift.js +180 -0
  44. package/dist/lib/skills.js +25 -12
  45. package/dist/lib/version-gate.d.ts +37 -0
  46. package/dist/lib/version-gate.js +84 -0
  47. package/dist/rc-proxy.mjs +341 -0
  48. package/package.json +9 -7
package/dist/lib/api.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getApiUrl } from "./config.js";
3
3
  import { forceRefreshAccessToken, resolveAuth } from "./auth.js";
4
+ import { mnemomFetch } from "./version-gate.js";
4
5
  export const API_BASE = getApiUrl();
6
+ const NOT_AUTHENTICATED_MSG = "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.";
5
7
  /** Sanitize file-sourced data before use in outbound HTTP requests. */
6
8
  function sanitizeForHttp(data) {
7
9
  return String(data).trim();
@@ -112,7 +114,7 @@ export async function readApiError(response, fallback) {
112
114
  }
113
115
  async function fetchApi(endpoint) {
114
116
  const url = validateUrl(`${API_BASE}${endpoint}`);
115
- const response = await fetch(url);
117
+ const response = await mnemomFetch(url);
116
118
  if (!response.ok) {
117
119
  throw await readApiError(response, "API request failed");
118
120
  }
@@ -120,18 +122,12 @@ async function fetchApi(endpoint) {
120
122
  }
121
123
  export async function postApi(endpoint, body, opts = {}) {
122
124
  const url = validateUrl(`${API_BASE}${endpoint}`);
123
- const cred = await resolveAuth();
124
125
  const headers = {
125
126
  "Content-Type": "application/json",
126
127
  "Idempotency-Key": opts.idempotencyKey ?? newIdempotencyKey(),
128
+ ...(await authHeaders()),
127
129
  };
128
- if (cred.type === "jwt") {
129
- headers["Authorization"] = `Bearer ${cred.token}`;
130
- }
131
- else if (cred.type === "api-key") {
132
- headers["X-Mnemom-Api-Key"] = cred.key;
133
- }
134
- const response = await fetch(url, {
130
+ const response = await mnemomFetch(url, {
135
131
  // lgtm[js/file-data-url]
136
132
  method: "POST",
137
133
  headers,
@@ -193,13 +189,13 @@ async function authHeaders() {
193
189
  * server-side reservation by design.
194
190
  */
195
191
  async function fetchWithAuthRetry(url, buildInit) {
196
- const first = await fetch(url, await buildInit());
192
+ const first = await mnemomFetch(url, await buildInit());
197
193
  if (first.status !== 401)
198
194
  return first;
199
195
  const refreshed = await forceRefreshAccessToken();
200
196
  if (!refreshed)
201
197
  return first;
202
- return fetch(url, await buildInit());
198
+ return mnemomFetch(url, await buildInit());
203
199
  }
204
200
  export async function getAgent(id) {
205
201
  return fetchApi(`/v1/agents/${id}`);
@@ -212,17 +208,7 @@ export async function getAgent(id) {
212
208
  * routing here avoids the false "Not authenticated" error (MNE-194 F10).
213
209
  */
214
210
  export async function listAgents() {
215
- const url = validateUrl(`${API_BASE}/v1/agents?limit=100`);
216
- const response = await fetchWithAuthRetry(url, async () => ({
217
- headers: await authHeaders(),
218
- }));
219
- if (!response.ok) {
220
- if (response.status === 401) {
221
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
222
- }
223
- throw await readApiError(response, "Failed to list agents");
224
- }
225
- const data = (await response.json());
211
+ const data = await gFetch("/v1/agents?limit=100", {}, { fallback: "Failed to list agents" });
226
212
  return data.agents ?? [];
227
213
  }
228
214
  /**
@@ -231,17 +217,7 @@ export async function listAgents() {
231
217
  * first; multi-user orgs follow.
232
218
  */
233
219
  export async function listMyOrgs() {
234
- const url = validateUrl(`${API_BASE}/v1/orgs`);
235
- const response = await fetchWithAuthRetry(url, async () => ({
236
- headers: await authHeaders(),
237
- }));
238
- if (!response.ok) {
239
- if (response.status === 401) {
240
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
241
- }
242
- throw await readApiError(response, "API request failed");
243
- }
244
- const data = (await response.json());
220
+ const data = await gFetch("/v1/orgs", {}, { fallback: "API request failed" });
245
221
  return data.orgs ?? [];
246
222
  }
247
223
  /**
@@ -249,17 +225,7 @@ export async function listMyOrgs() {
249
225
  * member of the org may read it; non-members get 403.
250
226
  */
251
227
  export async function fetchOrgFleet(orgId) {
252
- const url = validateUrl(`${API_BASE}/v1/orgs/${encodeURIComponent(orgId)}/agents`);
253
- const response = await fetchWithAuthRetry(url, async () => ({
254
- headers: { ...(await authHeaders()), Accept: "application/json" },
255
- }));
256
- if (!response.ok) {
257
- if (response.status === 401) {
258
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
259
- }
260
- throw await readApiError(response, "Failed to list org agents");
261
- }
262
- const data = (await response.json());
228
+ const data = await gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/agents`, {}, { fallback: "Failed to list org agents" });
263
229
  return data.agents ?? [];
264
230
  }
265
231
  function fleetToRow(a, org) {
@@ -351,7 +317,7 @@ export async function claimAgent(agentId, body, opts = {}) {
351
317
  }));
352
318
  if (!response.ok) {
353
319
  if (response.status === 401) {
354
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
320
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
355
321
  }
356
322
  throw await readApiError(response, "Failed to claim agent");
357
323
  }
@@ -366,22 +332,52 @@ export async function claimAgent(agentId, body, opts = {}) {
366
332
  };
367
333
  }
368
334
  /**
369
- * GET /v1/auth/me/personal-orgaccessor for the user's personal org.
370
- * Idempotent: lazily provisions for legacy accounts that pre-date the
371
- * mnemom-api migration 161 backfill.
335
+ * POST /v1/agents/:id/moverole-based relocation between orgs.
336
+ *
337
+ * The role-based counterpart to claimAgent's re-claim: the server requires
338
+ * the caller to be an owner/admin of BOTH the agent's current org and
339
+ * `destOrgId` — no hash proof / agent key involved. Moving to the current
340
+ * org is an idempotent no-op (`moved: false`). An Idempotency-Key is minted
341
+ * (held across the 401-refresh retry) like every mutation.
372
342
  */
373
- export async function getMyPersonalOrg() {
374
- const url = validateUrl(`${API_BASE}/v1/auth/me/personal-org`);
343
+ export async function moveAgent(agentId, destOrgId, opts = {}) {
344
+ const url = validateUrl(`${API_BASE}/v1/agents/${encodeURIComponent(agentId)}/move`);
345
+ const idempotencyKey = opts.idempotencyKey ?? newIdempotencyKey();
375
346
  const response = await fetchWithAuthRetry(url, async () => ({
376
- headers: await authHeaders(),
347
+ method: "POST",
348
+ headers: {
349
+ ...(await authHeaders()),
350
+ "Content-Type": "application/json",
351
+ Accept: "application/json",
352
+ "Idempotency-Key": idempotencyKey,
353
+ },
354
+ body: JSON.stringify({ dest_org_id: sanitizeForHttp(destOrgId) }),
377
355
  }));
378
356
  if (!response.ok) {
379
357
  if (response.status === 401) {
380
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
358
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
381
359
  }
382
- throw await readApiError(response, "API request failed");
360
+ throw await readApiError(response, "Failed to move agent");
383
361
  }
384
- return (await response.json());
362
+ const data = (await response.json());
363
+ return {
364
+ moved: data.moved ?? true,
365
+ agent_id: data.agent_id ?? agentId,
366
+ from_org_id: data.from_org_id ?? null,
367
+ to_org_id: data.to_org_id ?? null,
368
+ };
369
+ }
370
+ /**
371
+ * GET /v1/auth/me/personal-org — accessor for the user's personal org.
372
+ * Idempotent: lazily provisions for legacy accounts that pre-date the
373
+ * mnemom-api migration 161 backfill.
374
+ */
375
+ export async function getMyPersonalOrg() {
376
+ return gFetch("/v1/auth/me/personal-org", {}, { fallback: "API request failed" });
377
+ }
378
+ export async function getMuBalance(orgId) {
379
+ const q = orgId ? `?org_id=${encodeURIComponent(orgId)}` : "";
380
+ return gFetch(`/v1/billing/mu${q}`, {}, { fallback: "Could not read MU balance" });
385
381
  }
386
382
  /**
387
383
  * GET /v1/orgs/:org_id/teams for every org the user is a member of,
@@ -421,7 +417,7 @@ export async function getTeam(teamId) {
421
417
  }));
422
418
  if (!response.ok) {
423
419
  if (response.status === 401) {
424
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
420
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
425
421
  }
426
422
  if (response.status === 404) {
427
423
  throw new MnemomApiError(404, `Team '${teamId}' not found or not accessible.`);
@@ -443,7 +439,7 @@ export async function getTeamTemplate(teamId, kind) {
443
439
  }));
444
440
  if (!response.ok) {
445
441
  if (response.status === 401) {
446
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
442
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
447
443
  }
448
444
  if (response.status === 403) {
449
445
  throw new MnemomApiError(403, `Forbidden: not a member of team '${teamId}'s org.`);
@@ -479,7 +475,7 @@ export async function putTeamTemplate(teamId, kind, yamlBody) {
479
475
  }));
480
476
  if (!response.ok) {
481
477
  if (response.status === 401) {
482
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
478
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
483
479
  }
484
480
  if (response.status === 403) {
485
481
  throw new MnemomApiError(403, `Forbidden: org admin or owner role required to write a team template.`);
@@ -512,7 +508,7 @@ export async function deleteTeamTemplate(teamId, kind) {
512
508
  }));
513
509
  if (!response.ok) {
514
510
  if (response.status === 401) {
515
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
511
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
516
512
  }
517
513
  if (response.status === 403) {
518
514
  throw new MnemomApiError(403, `Forbidden: org admin or owner role required to clear a team template.`);
@@ -570,7 +566,7 @@ export async function grantTeamAdmin(teamId, userId) {
570
566
  }));
571
567
  if (!response.ok) {
572
568
  if (response.status === 401) {
573
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
569
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
574
570
  }
575
571
  if (response.status === 403) {
576
572
  throw new MnemomApiError(403, `Forbidden: org owner or admin role required, and target user must be in the team's org.`);
@@ -602,7 +598,7 @@ export async function revokeTeamAdmin(teamId, userId) {
602
598
  }));
603
599
  if (!response.ok) {
604
600
  if (response.status === 401) {
605
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
601
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
606
602
  }
607
603
  if (response.status === 403) {
608
604
  throw new MnemomApiError(403, `Forbidden: revoke requires org owner/admin, or self-revoke with an active grant.`);
@@ -625,7 +621,7 @@ export async function listTeamAdmins(teamId) {
625
621
  }));
626
622
  if (!response.ok) {
627
623
  if (response.status === 401) {
628
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
624
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
629
625
  }
630
626
  if (response.status === 403) {
631
627
  throw new MnemomApiError(403, `Forbidden: requires membership in the team's org.`);
@@ -672,17 +668,10 @@ export async function getAgentByName(name) {
672
668
  * path from PR #200 so a stale local expiresAt heals transparently.
673
669
  */
674
670
  export async function getIntegrity(id) {
675
- const url = validateUrl(`${API_BASE}/v1/integrity/${id}`);
676
- const response = await fetchWithAuthRetry(url, async () => ({
677
- headers: await authHeaders(),
678
- }));
679
- if (!response.ok) {
680
- throw await readApiError(response, "API request failed");
681
- }
671
+ const body = await gFetch(`/v1/integrity/${id}`, {}, { fallback: "API request failed" });
682
672
  // Accept both the canonical docs shape (object) and the RPC-wrapped shape
683
673
  // (array of one row). The latter is what prod actually returns today on
684
674
  // the RPC path; flagged for a follow-up API normalization.
685
- const body = (await response.json());
686
675
  const row = Array.isArray(body) ? (body[0] ?? emptyIntegrityRow(id)) : body;
687
676
  // Fill in agent_id when the API didn't (RPC path) so the field is always
688
677
  // populated for consumers regardless of which API path served the request.
@@ -714,14 +703,7 @@ function emptyIntegrityRow(agentId) {
714
703
  * don't need a special case for it.
715
704
  */
716
705
  export async function getTraces(id, limit = 10) {
717
- const url = validateUrl(`${API_BASE}/v1/traces?agent_id=${id}&limit=${limit}`);
718
- const response = await fetchWithAuthRetry(url, async () => ({
719
- headers: await authHeaders(),
720
- }));
721
- if (!response.ok) {
722
- throw await readApiError(response, "API request failed");
723
- }
724
- const data = (await response.json());
706
+ const data = await gFetch(`/v1/traces?agent_id=${id}&limit=${limit}`, {}, { fallback: "API request failed" });
725
707
  // Accept both the envelope shape (current API) and a bare array (defensive
726
708
  // — staging is mid-deploy when a CLI smoke runs against an older API).
727
709
  if (Array.isArray(data))
@@ -741,7 +723,7 @@ export async function getAlignmentCard(agentId, format = "yaml") {
741
723
  // W1.2b). The legacy `/v1/agents/:id/alignment-card` only 308-redirects to
742
724
  // this — first-party callers target canonical directly (no legacy shapes).
743
725
  const url = validateUrl(`${API_BASE}/v1/alignment/agent/${encodeURIComponent(agentId)}`);
744
- const response = await fetch(url, {
726
+ const response = await mnemomFetch(url, {
745
727
  headers: { Accept: accept, ...(await authHeaders()) },
746
728
  });
747
729
  if (!response.ok) {
@@ -801,7 +783,7 @@ export async function getProtectionCard(agentId, format = "yaml") {
801
783
  // Canonical surface (ADR-062 / W1.2b); legacy `/v1/agents/:id/protection-card`
802
784
  // only 308-redirects here. First-party callers use canonical directly.
803
785
  const url = validateUrl(`${API_BASE}/v1/protection/agent/${encodeURIComponent(agentId)}`);
804
- const response = await fetch(url, {
786
+ const response = await mnemomFetch(url, {
805
787
  headers: { Accept: accept, ...(await authHeaders()) },
806
788
  });
807
789
  if (!response.ok) {
@@ -869,7 +851,7 @@ export async function previewComposeAgentCard(agentId, kind, body, contentType =
869
851
  }
870
852
  // 401 → unauthenticated: signal the caller to fall back to offline validation.
871
853
  if (response.status === 401) {
872
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
854
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
873
855
  }
874
856
  // 400 / 422 → the card is invalid: an expected validation outcome, not an
875
857
  // exception. Surface the care-framed findings structurally.
@@ -917,7 +899,7 @@ export async function resolveAgentId(agentName) {
917
899
  catch (err) {
918
900
  const msg = err instanceof Error ? err.message : String(err);
919
901
  if (msg.includes("Not authenticated")) {
920
- console.error(`\nNot authenticated. Run \`mnemom login\` or set MNEMOM_API_KEY.\n`);
902
+ console.error(`\n${NOT_AUTHENTICATED_MSG}\n`);
921
903
  process.exit(1);
922
904
  }
923
905
  }
@@ -945,7 +927,7 @@ export async function listPostures(opts) {
945
927
  }));
946
928
  if (!response.ok) {
947
929
  if (response.status === 401) {
948
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
930
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
949
931
  }
950
932
  if (response.status === 403) {
951
933
  throw new MnemomApiError(403, `Forbidden: not a member of org '${opts.orgId}'.`);
@@ -963,7 +945,7 @@ export async function getPosture(postureId) {
963
945
  }));
964
946
  if (!response.ok) {
965
947
  if (response.status === 401) {
966
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
948
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
967
949
  }
968
950
  if (response.status === 404)
969
951
  throw new MnemomApiError(404, `Posture '${postureId}' not found.`);
@@ -975,18 +957,7 @@ export async function getPosture(postureId) {
975
957
  }
976
958
  /** GET /v1/postures/:id/revisions — list revisions, newest first. */
977
959
  export async function listPostureRevisions(postureId) {
978
- const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/revisions`);
979
- const response = await fetchWithAuthRetry(url, async () => ({
980
- headers: { ...(await authHeaders()), Accept: "application/json" },
981
- }));
982
- if (!response.ok) {
983
- if (response.status === 401)
984
- throw new MnemomApiError(401, "Not authenticated.");
985
- if (response.status === 404)
986
- throw new MnemomApiError(404, `Posture '${postureId}' not found.`);
987
- throw await readApiError(response, "API request failed");
988
- }
989
- const data = (await response.json());
960
+ const data = await gFetch(`/v1/postures/${postureId}/revisions`, {}, { notFoundLabel: `Posture '${postureId}'`, fallback: "API request failed" });
990
961
  return data.revisions ?? [];
991
962
  }
992
963
  /** GET /v1/postures/:id/diff?from=N&to=M — structural diff. */
@@ -997,7 +968,7 @@ export async function diffPostureRevisions(postureId, fromNo, toNo) {
997
968
  }));
998
969
  if (!response.ok) {
999
970
  if (response.status === 401)
1000
- throw new MnemomApiError(401, "Not authenticated.");
971
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1001
972
  if (response.status === 404) {
1002
973
  const errBody = await parseApiErrorBody(response);
1003
974
  throw new MnemomApiError(404, errBody.message || `Posture or revision not found.`, {
@@ -1025,7 +996,7 @@ export async function createPosture(input) {
1025
996
  }));
1026
997
  if (!response.ok) {
1027
998
  if (response.status === 401)
1028
- throw new MnemomApiError(401, "Not authenticated.");
999
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1029
1000
  if (response.status === 403) {
1030
1001
  throw new MnemomApiError(403, `Forbidden: org owner/admin role required (or scope=platform reserved for migrations).`);
1031
1002
  }
@@ -1052,7 +1023,7 @@ export async function updatePosture(postureId, input) {
1052
1023
  }));
1053
1024
  if (!response.ok) {
1054
1025
  if (response.status === 401)
1055
- throw new MnemomApiError(401, "Not authenticated.");
1026
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1056
1027
  if (response.status === 403) {
1057
1028
  throw new MnemomApiError(403, `Forbidden: org owner/admin required, or this is a Mnemom-shipped platform default (immutable — clone first).`);
1058
1029
  }
@@ -1079,7 +1050,7 @@ export async function clonePosture(postureId, input) {
1079
1050
  }));
1080
1051
  if (!response.ok) {
1081
1052
  if (response.status === 401)
1082
- throw new MnemomApiError(401, "Not authenticated.");
1053
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1083
1054
  if (response.status === 403)
1084
1055
  throw new MnemomApiError(403, `Forbidden: org owner/admin required on target org.`);
1085
1056
  if (response.status === 404)
@@ -1103,7 +1074,7 @@ export async function deletePosture(postureId) {
1103
1074
  }));
1104
1075
  if (!response.ok) {
1105
1076
  if (response.status === 401)
1106
- throw new MnemomApiError(401, "Not authenticated.");
1077
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1107
1078
  if (response.status === 403)
1108
1079
  throw new MnemomApiError(403, `Forbidden: platform defaults are immutable.`);
1109
1080
  if (response.status === 404)
@@ -1129,7 +1100,7 @@ export async function assignPosture(postureId, teamId, pinRevisionNo) {
1129
1100
  }));
1130
1101
  if (!response.ok) {
1131
1102
  if (response.status === 401)
1132
- throw new MnemomApiError(401, "Not authenticated.");
1103
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1133
1104
  if (response.status === 403)
1134
1105
  throw new MnemomApiError(403, `Forbidden: org owner/admin required on team's org.`);
1135
1106
  if (response.status === 404) {
@@ -1153,7 +1124,7 @@ export async function unassignPosture(postureId, teamId) {
1153
1124
  }));
1154
1125
  if (!response.ok) {
1155
1126
  if (response.status === 401)
1156
- throw new MnemomApiError(401, "Not authenticated.");
1127
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1157
1128
  if (response.status === 403)
1158
1129
  throw new MnemomApiError(403, `Forbidden: org owner/admin required.`);
1159
1130
  if (response.status === 404) {
@@ -1176,7 +1147,7 @@ export async function previewComposePosture(postureId, teamId) {
1176
1147
  }));
1177
1148
  if (!response.ok) {
1178
1149
  if (response.status === 401)
1179
- throw new MnemomApiError(401, "Not authenticated.");
1150
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1180
1151
  if (response.status === 404) {
1181
1152
  const errBody = await parseApiErrorBody(response);
1182
1153
  throw new MnemomApiError(404, errBody.message || `Posture or team not found.`, {
@@ -1202,21 +1173,7 @@ export async function listSidebandAdvisoriesForAgent(agentId, opts = {}) {
1202
1173
  if (opts.since)
1203
1174
  params.set("since", opts.since);
1204
1175
  const qs = params.toString() ? `?${params.toString()}` : "";
1205
- const url = validateUrl(`${API_BASE}/v1/agents/${encodeURIComponent(agentId)}/sideband-advisories${qs}`);
1206
- const response = await fetchWithAuthRetry(url, async () => ({
1207
- method: "GET",
1208
- headers: { ...(await authHeaders()), Accept: "application/json" },
1209
- }));
1210
- if (!response.ok) {
1211
- if (response.status === 401)
1212
- throw new MnemomApiError(401, "Not authenticated.");
1213
- if (response.status === 403)
1214
- throw new MnemomApiError(403, "Permission denied (need org membership).");
1215
- if (response.status === 404)
1216
- throw new MnemomApiError(404, `Agent '${agentId}' not found.`);
1217
- throw await readApiError(response, "List advisories failed");
1218
- }
1219
- return (await response.json());
1176
+ return gFetch(`/v1/agents/${encodeURIComponent(agentId)}/sideband-advisories${qs}`, {}, { notFoundLabel: `Agent '${agentId}'`, fallback: "List advisories failed" });
1220
1177
  }
1221
1178
  /**
1222
1179
  * GET /v1/teams/:id/sideband-advisories
@@ -1231,21 +1188,7 @@ export async function listSidebandAdvisoriesForTeam(teamId, opts = {}) {
1231
1188
  if (opts.since)
1232
1189
  params.set("since", opts.since);
1233
1190
  const qs = params.toString() ? `?${params.toString()}` : "";
1234
- const url = validateUrl(`${API_BASE}/v1/teams/${encodeURIComponent(teamId)}/sideband-advisories${qs}`);
1235
- const response = await fetchWithAuthRetry(url, async () => ({
1236
- method: "GET",
1237
- headers: { ...(await authHeaders()), Accept: "application/json" },
1238
- }));
1239
- if (!response.ok) {
1240
- if (response.status === 401)
1241
- throw new MnemomApiError(401, "Not authenticated.");
1242
- if (response.status === 403)
1243
- throw new MnemomApiError(403, "Permission denied (need org membership).");
1244
- if (response.status === 404)
1245
- throw new MnemomApiError(404, `Team '${teamId}' not found.`);
1246
- throw await readApiError(response, "List advisories failed");
1247
- }
1248
- return (await response.json());
1191
+ return gFetch(`/v1/teams/${encodeURIComponent(teamId)}/sideband-advisories${qs}`, {}, { notFoundLabel: `Team '${teamId}'`, fallback: "List advisories failed" });
1249
1192
  }
1250
1193
  /**
1251
1194
  * GET /v1/teams/:id/sideband-coverage
@@ -1255,21 +1198,7 @@ export async function listSidebandAdvisoriesForTeam(teamId, opts = {}) {
1255
1198
  * raw rows.
1256
1199
  */
1257
1200
  export async function getTeamSidebandCoverage(teamId) {
1258
- const url = validateUrl(`${API_BASE}/v1/teams/${encodeURIComponent(teamId)}/sideband-coverage`);
1259
- const response = await fetchWithAuthRetry(url, async () => ({
1260
- method: "GET",
1261
- headers: { ...(await authHeaders()), Accept: "application/json" },
1262
- }));
1263
- if (!response.ok) {
1264
- if (response.status === 401)
1265
- throw new MnemomApiError(401, "Not authenticated.");
1266
- if (response.status === 403)
1267
- throw new MnemomApiError(403, "Permission denied (need org membership).");
1268
- if (response.status === 404)
1269
- throw new MnemomApiError(404, `Team '${teamId}' not found.`);
1270
- throw await readApiError(response, "Coverage fetch failed");
1271
- }
1272
- return (await response.json());
1201
+ return gFetch(`/v1/teams/${encodeURIComponent(teamId)}/sideband-coverage`, {}, { notFoundLabel: `Team '${teamId}'`, fallback: "Coverage fetch failed" });
1273
1202
  }
1274
1203
  /**
1275
1204
  * GET /v1/admin/safe-house/harness-state — fetch the most recent
@@ -1284,7 +1213,7 @@ export async function getSafeHouseHarnessState() {
1284
1213
  }));
1285
1214
  if (!response.ok) {
1286
1215
  if (response.status === 401)
1287
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login`.");
1216
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1288
1217
  if (response.status === 403)
1289
1218
  throw new MnemomApiError(403, "Permission denied — `mnemom validate safe-house` is mnemom-staff only.");
1290
1219
  throw await readApiError(response, "Harness state fetch failed");
@@ -1310,7 +1239,7 @@ function buildSignalListQuery(opts = {}) {
1310
1239
  const qs = params.toString();
1311
1240
  return qs ? `?${qs}` : "";
1312
1241
  }
1313
- async function gFetch(path, init = {}, notFoundLabel) {
1242
+ async function gFetch(path, init = {}, opts = {}) {
1314
1243
  const url = validateUrl(`${API_BASE}${path}`);
1315
1244
  const response = await fetchWithAuthRetry(url, async () => ({
1316
1245
  method: init.method ?? "GET",
@@ -1324,56 +1253,56 @@ async function gFetch(path, init = {}, notFoundLabel) {
1324
1253
  }));
1325
1254
  if (!response.ok) {
1326
1255
  if (response.status === 401)
1327
- throw new MnemomApiError(401, "Not authenticated.");
1256
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1328
1257
  if (response.status === 403)
1329
1258
  throw new MnemomApiError(403, "Permission denied (need org admin / membership).");
1330
- if (response.status === 404 && notFoundLabel)
1331
- throw new MnemomApiError(404, `${notFoundLabel} not found.`);
1332
- throw await readApiError(response, "Request failed");
1259
+ if (response.status === 404 && opts.notFoundLabel)
1260
+ throw new MnemomApiError(404, `${opts.notFoundLabel} not found.`);
1261
+ throw await readApiError(response, opts.fallback ?? "Request failed");
1333
1262
  }
1334
1263
  return (await response.json());
1335
1264
  }
1336
1265
  export async function listGovernanceSignalsForOrg(orgId, opts = {}) {
1337
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Org");
1266
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/signals${buildSignalListQuery(opts)}`, {}, { notFoundLabel: "Org" });
1338
1267
  }
1339
1268
  export async function listGovernanceSignalsForTeam(teamId, opts = {}) {
1340
- return gFetch(`/v1/teams/${encodeURIComponent(teamId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Team");
1269
+ return gFetch(`/v1/teams/${encodeURIComponent(teamId)}/governance/signals${buildSignalListQuery(opts)}`, {}, { notFoundLabel: "Team" });
1341
1270
  }
1342
1271
  export async function listGovernanceSignalsForAgent(agentId, opts = {}) {
1343
- return gFetch(`/v1/agents/${encodeURIComponent(agentId)}/governance/signals${buildSignalListQuery(opts)}`, {}, "Agent");
1272
+ return gFetch(`/v1/agents/${encodeURIComponent(agentId)}/governance/signals${buildSignalListQuery(opts)}`, {}, { notFoundLabel: "Agent" });
1344
1273
  }
1345
1274
  export async function getGovernanceSignal(id) {
1346
- return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}`, {}, "Signal");
1275
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}`, {}, { notFoundLabel: "Signal" });
1347
1276
  }
1348
1277
  export async function acknowledgeGovernanceSignal(id, body = {}) {
1349
- return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/acknowledge`, { method: "POST", body: JSON.stringify(body) }, "Signal");
1278
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/acknowledge`, { method: "POST", body: JSON.stringify(body) }, { notFoundLabel: "Signal" });
1350
1279
  }
1351
1280
  export async function resolveGovernanceSignal(id, body) {
1352
- return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/resolve`, { method: "POST", body: JSON.stringify(body) }, "Signal");
1281
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/resolve`, { method: "POST", body: JSON.stringify(body) }, { notFoundLabel: "Signal" });
1353
1282
  }
1354
1283
  export async function dismissGovernanceSignal(id, body = {}) {
1355
- return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/dismiss`, { method: "POST", body: JSON.stringify(body) }, "Signal");
1284
+ return gFetch(`/v1/governance/signals/${encodeURIComponent(id)}/dismiss`, { method: "POST", body: JSON.stringify(body) }, { notFoundLabel: "Signal" });
1356
1285
  }
1357
1286
  export async function listGovernanceDestinations(orgId) {
1358
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, {}, "Org");
1287
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, {}, { notFoundLabel: "Org" });
1359
1288
  }
1360
1289
  export async function createGovernanceDestination(orgId, body) {
1361
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, { method: "POST", body: JSON.stringify(body) }, "Org");
1290
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations`, { method: "POST", body: JSON.stringify(body) }, { notFoundLabel: "Org" });
1362
1291
  }
1363
1292
  export async function deleteGovernanceDestination(orgId, destinationId) {
1364
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}`, { method: "DELETE" }, "Destination");
1293
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}`, { method: "DELETE" }, { notFoundLabel: "Destination" });
1365
1294
  }
1366
1295
  export async function testGovernanceDestination(orgId, destinationId) {
1367
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}/test`, { method: "POST", body: JSON.stringify({}) }, "Destination");
1296
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/notification-destinations/${encodeURIComponent(destinationId)}/test`, { method: "POST", body: JSON.stringify({}) }, { notFoundLabel: "Destination" });
1368
1297
  }
1369
1298
  export async function listGovernanceRules(orgId) {
1370
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, {}, "Org");
1299
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, {}, { notFoundLabel: "Org" });
1371
1300
  }
1372
1301
  export async function createGovernanceRule(orgId, body) {
1373
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, { method: "POST", body: JSON.stringify(body) }, "Org");
1302
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules`, { method: "POST", body: JSON.stringify(body) }, { notFoundLabel: "Org" });
1374
1303
  }
1375
1304
  export async function deleteGovernanceRule(orgId, ruleId) {
1376
- return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules/${encodeURIComponent(ruleId)}`, { method: "DELETE" }, "Rule");
1305
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/governance/escalation-rules/${encodeURIComponent(ruleId)}`, { method: "DELETE" }, { notFoundLabel: "Rule" });
1377
1306
  }
1378
1307
  // ─── api-keys (ADR-049) ─────────────────────────────────────────────────
1379
1308
  /**
@@ -1408,17 +1337,7 @@ export function isLegacyScopeSet(scopes) {
1408
1337
  * GET /v1/api-keys — list the caller's active personal API keys.
1409
1338
  */
1410
1339
  export async function listApiKeys() {
1411
- const url = validateUrl(`${API_BASE}/v1/api-keys`);
1412
- const response = await fetchWithAuthRetry(url, async () => ({
1413
- headers: await authHeaders(),
1414
- }));
1415
- if (!response.ok) {
1416
- if (response.status === 401) {
1417
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1418
- }
1419
- throw await readApiError(response, "Failed to list api keys");
1420
- }
1421
- const data = (await response.json());
1340
+ const data = await gFetch("/v1/api-keys", {}, { fallback: "Failed to list api keys" });
1422
1341
  return data.keys ?? [];
1423
1342
  }
1424
1343
  /**
@@ -1437,7 +1356,7 @@ export async function createApiKey(name, scopes) {
1437
1356
  }));
1438
1357
  if (!response.ok) {
1439
1358
  if (response.status === 401) {
1440
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1359
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1441
1360
  }
1442
1361
  const parsed = await parseApiErrorBody(response);
1443
1362
  const detail = parsed.message || `HTTP ${response.status}`;
@@ -1465,7 +1384,7 @@ export async function rotateApiKey(keyId) {
1465
1384
  }));
1466
1385
  if (!response.ok) {
1467
1386
  if (response.status === 401) {
1468
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1387
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1469
1388
  }
1470
1389
  if (response.status === 404)
1471
1390
  throw new MnemomApiError(404, `Key not found: ${keyId}`);
@@ -1486,7 +1405,7 @@ export async function revokeApiKey(keyId) {
1486
1405
  }));
1487
1406
  if (!response.ok) {
1488
1407
  if (response.status === 401) {
1489
- throw new MnemomApiError(401, "Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
1408
+ throw new MnemomApiError(401, NOT_AUTHENTICATED_MSG);
1490
1409
  }
1491
1410
  if (response.status === 404)
1492
1411
  throw new MnemomApiError(404, `Key not found: ${keyId}`);
@@ -1514,7 +1433,7 @@ export async function revokeApiKey(keyId) {
1514
1433
  */
1515
1434
  export async function validateLicense(jwt, instanceId, instanceMetadata = {}) {
1516
1435
  const url = validateUrl(`${API_BASE}/v1/license/validate`);
1517
- const response = await fetch(url, {
1436
+ const response = await mnemomFetch(url, {
1518
1437
  method: "POST",
1519
1438
  headers: { "Content-Type": "application/json" },
1520
1439
  body: JSON.stringify({
@@ -1531,3 +1450,33 @@ export async function validateLicense(jwt, instanceId, instanceMetadata = {}) {
1531
1450
  export async function reportRecipeFnFp(recipeId, input) {
1532
1451
  return postApi(`/v1/recipes/${encodeURIComponent(recipeId)}/report`, input);
1533
1452
  }
1453
+ /** The only windows the endpoint accepts (`days` enum in `openapi.json`). */
1454
+ export const USAGE_ALLOWED_DAYS = [7, 30, 90];
1455
+ /**
1456
+ * GET /v1/orgs/:org_id/usage/by-person — consumption by person/provider/model.
1457
+ *
1458
+ * Role-gated to owner/admin/auditor; member and viewer get a real 403.
1459
+ *
1460
+ * Gated by USAGE_ATTRIBUTION_API_ENABLED, which returns **404, not 403**, when
1461
+ * off — deliberately, so a disabled endpoint is indistinguishable from one that
1462
+ * does not exist. That makes 404 ambiguous by design (flag off OR unknown org
1463
+ * OR no access), so no `notFoundLabel` is passed here: a caller must not tell
1464
+ * the user "org not found" on what is most often just the flag being off.
1465
+ */
1466
+ export async function getOrgUsage(orgId, opts = {}) {
1467
+ const params = new URLSearchParams();
1468
+ if (opts.days !== undefined)
1469
+ params.set("days", String(opts.days));
1470
+ if (opts.personId)
1471
+ params.set("person_id", opts.personId);
1472
+ if (opts.provider)
1473
+ params.set("provider", opts.provider);
1474
+ if (opts.model)
1475
+ params.set("model", opts.model);
1476
+ if (opts.limit !== undefined)
1477
+ params.set("limit", String(opts.limit));
1478
+ if (opts.cursor)
1479
+ params.set("cursor", opts.cursor);
1480
+ const qs = params.toString() ? `?${params.toString()}` : "";
1481
+ return gFetch(`/v1/orgs/${encodeURIComponent(orgId)}/usage/by-person${qs}`, {}, { fallback: "Failed to fetch usage" });
1482
+ }