@mnemom/mnemom 0.9.1 → 0.10.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.
package/dist/lib/api.js CHANGED
@@ -52,6 +52,7 @@ export async function postApi(endpoint, body, opts = {}) {
52
52
  headers["X-Mnemom-Api-Key"] = cred.key;
53
53
  }
54
54
  const response = await fetch(url, {
55
+ // lgtm[js/file-data-url]
55
56
  method: "POST",
56
57
  headers,
57
58
  body: JSON.stringify(body),
@@ -127,12 +128,313 @@ export async function listAgents() {
127
128
  if (response.status === 401) {
128
129
  throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
129
130
  }
130
- const err = await response.json().catch(() => ({ error: "unknown" }));
131
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
131
132
  throw new Error(err.message || `Failed to list agents: ${response.status}`);
132
133
  }
133
- const data = await response.json();
134
+ const data = (await response.json());
134
135
  return data.agents ?? [];
135
136
  }
137
+ /**
138
+ * GET /v1/orgs — list every org the user is a member of, including the
139
+ * personal-org-of-one (per ADR-044 Option A, GitHub model). Personal sorts
140
+ * first; multi-user orgs follow.
141
+ */
142
+ export async function listMyOrgs() {
143
+ const url = validateUrl(`${API_BASE}/v1/orgs`);
144
+ const response = await fetchWithAuthRetry(url, async () => ({
145
+ headers: await authHeaders(),
146
+ }));
147
+ if (!response.ok) {
148
+ if (response.status === 401) {
149
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
150
+ }
151
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
152
+ throw new Error(err.message || `API request failed: ${response.status}`);
153
+ }
154
+ const data = (await response.json());
155
+ return data.orgs ?? [];
156
+ }
157
+ /**
158
+ * GET /v1/auth/me/personal-org — accessor for the user's personal org.
159
+ * Idempotent: lazily provisions for legacy accounts that pre-date the
160
+ * mnemom-api migration 161 backfill.
161
+ */
162
+ export async function getMyPersonalOrg() {
163
+ const url = validateUrl(`${API_BASE}/v1/auth/me/personal-org`);
164
+ const response = await fetchWithAuthRetry(url, async () => ({
165
+ headers: await authHeaders(),
166
+ }));
167
+ if (!response.ok) {
168
+ if (response.status === 401) {
169
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
170
+ }
171
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
172
+ throw new Error(err.message || `API request failed: ${response.status}`);
173
+ }
174
+ return (await response.json());
175
+ }
176
+ /**
177
+ * GET /v1/orgs/:org_id/teams for every org the user is a member of,
178
+ * concatenated. The CLI consumer that just wants "every team I can see"
179
+ * doesn't need to know about org boundaries; team_id is unique anyway.
180
+ * Returns the flat list with org_name attached for display.
181
+ */
182
+ export async function listMyTeams() {
183
+ const orgs = await listMyOrgs();
184
+ const teams = [];
185
+ for (const org of orgs) {
186
+ const url = validateUrl(`${API_BASE}/v1/orgs/${org.org_id}/teams`);
187
+ const response = await fetchWithAuthRetry(url, async () => ({
188
+ headers: await authHeaders(),
189
+ }));
190
+ if (!response.ok) {
191
+ // Skip orgs the user can't read teams from (e.g., role doesn't permit).
192
+ // The list call should succeed at the member level, but tolerate edge cases.
193
+ continue;
194
+ }
195
+ const data = (await response.json());
196
+ for (const t of data.teams ?? []) {
197
+ teams.push({ ...t, org_id: t.org_id ?? org.org_id, org_name: org.name });
198
+ }
199
+ }
200
+ return teams;
201
+ }
202
+ /**
203
+ * GET /v1/teams/:team_id — fetch a single team's row. Returns the team
204
+ * detail including org_id (which the CLI uses to disambiguate team_id
205
+ * collisions across orgs in error messages).
206
+ */
207
+ export async function getTeam(teamId) {
208
+ const url = validateUrl(`${API_BASE}/v1/teams/${teamId}`);
209
+ const response = await fetchWithAuthRetry(url, async () => ({
210
+ headers: await authHeaders(),
211
+ }));
212
+ if (!response.ok) {
213
+ if (response.status === 401) {
214
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
215
+ }
216
+ if (response.status === 404) {
217
+ throw new Error(`Team '${teamId}' not found or not accessible.`);
218
+ }
219
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
220
+ throw new Error(err.message || `API request failed: ${response.status}`);
221
+ }
222
+ return (await response.json());
223
+ }
224
+ /**
225
+ * GET /v1/teams/:team_id/(alignment|protection)-template — read the
226
+ * current team-scope template + its enabled flag. Returns the wire body
227
+ * verbatim. Empty for teams that have no template set yet (template:
228
+ * null, enabled: false).
229
+ */
230
+ export async function getTeamTemplate(teamId, kind) {
231
+ const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/${kind}-template`);
232
+ const response = await fetchWithAuthRetry(url, async () => ({
233
+ headers: { ...(await authHeaders()), Accept: "application/json" },
234
+ }));
235
+ if (!response.ok) {
236
+ if (response.status === 401) {
237
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
238
+ }
239
+ if (response.status === 403) {
240
+ throw new Error(`Forbidden: not a member of team '${teamId}'s org.`);
241
+ }
242
+ if (response.status === 404) {
243
+ throw new Error(`Team '${teamId}' not found.`);
244
+ }
245
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
246
+ throw new Error(err.message || `API request failed: ${response.status}`);
247
+ }
248
+ return (await response.json());
249
+ }
250
+ /**
251
+ * PUT /v1/teams/:team_id/(alignment|protection)-template — write the
252
+ * team-scope template. Body is YAML or JSON template content; the API
253
+ * accepts either via Content-Type. Idempotency-Key is required (per the
254
+ * idempotency-with-body-hash discipline in the API).
255
+ *
256
+ * On success returns the post-write team template body, including
257
+ * agents_flagged_for_recompose so the caller can surface the fan-out
258
+ * count to the user.
259
+ */
260
+ export async function putTeamTemplate(teamId, kind, yamlBody) {
261
+ const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/${kind}-template`);
262
+ const response = await fetchWithAuthRetry(url, async () => ({
263
+ method: "PUT",
264
+ headers: {
265
+ ...(await authHeaders()),
266
+ "Content-Type": "text/yaml",
267
+ Accept: "application/json",
268
+ "Idempotency-Key": newIdempotencyKey(),
269
+ },
270
+ body: yamlBody,
271
+ }));
272
+ if (!response.ok) {
273
+ if (response.status === 401) {
274
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
275
+ }
276
+ if (response.status === 403) {
277
+ throw new Error(`Forbidden: org admin or owner role required to write a team template.`);
278
+ }
279
+ if (response.status === 404) {
280
+ throw new Error(`Team '${teamId}' not found.`);
281
+ }
282
+ if (response.status === 413) {
283
+ throw new Error(`Template too large (server limit: 128 KiB alignment / 64 KiB protection).`);
284
+ }
285
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
286
+ throw new Error(err.message || `API request failed: ${response.status}`);
287
+ }
288
+ return (await response.json());
289
+ }
290
+ /**
291
+ * DELETE /v1/teams/:team_id/(alignment|protection)-template — clear the
292
+ * template. Idempotent (deleting an already-cleared template is a 200).
293
+ * Returns the post-delete team template body with deleted=true and the
294
+ * recompose fan-out count.
295
+ */
296
+ export async function deleteTeamTemplate(teamId, kind) {
297
+ const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/${kind}-template`);
298
+ const response = await fetchWithAuthRetry(url, async () => ({
299
+ method: "DELETE",
300
+ headers: {
301
+ ...(await authHeaders()),
302
+ Accept: "application/json",
303
+ "Idempotency-Key": newIdempotencyKey(),
304
+ },
305
+ }));
306
+ if (!response.ok) {
307
+ if (response.status === 401) {
308
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
309
+ }
310
+ if (response.status === 403) {
311
+ throw new Error(`Forbidden: org admin or owner role required to clear a team template.`);
312
+ }
313
+ if (response.status === 404) {
314
+ throw new Error(`Team '${teamId}' not found.`);
315
+ }
316
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
317
+ throw new Error(err.message || `API request failed: ${response.status}`);
318
+ }
319
+ return (await response.json());
320
+ }
321
+ /**
322
+ * POST /v1/teams/:team_id/(alignment|protection)-template/preview-compose
323
+ * — dry-run the composer with the supplied draft against the team's
324
+ * current org+platform context. Returns the composed canonical output
325
+ * plus per-field conflicts where the draft was tightened by the
326
+ * org/platform floor. No DB writes.
327
+ */
328
+ export async function previewComposeTeamTemplate(teamId, kind, yamlBody) {
329
+ const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/${kind}-template/preview-compose`);
330
+ const response = await fetchWithAuthRetry(url, async () => ({
331
+ method: "POST",
332
+ headers: {
333
+ ...(await authHeaders()),
334
+ "Content-Type": "text/yaml",
335
+ Accept: "application/json",
336
+ },
337
+ body: yamlBody,
338
+ }));
339
+ if (!response.ok) {
340
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
341
+ throw new Error(err.message || `Preview failed: ${response.status}`);
342
+ }
343
+ const body = (await response.json());
344
+ return { composed: body.composed, conflicts: body.conflicts };
345
+ }
346
+ /**
347
+ * POST /v1/teams/:team_id/admins — grant team_admin role on a team.
348
+ *
349
+ * Per ADR-046 §3: callable by org_owner / org_admin only. The target
350
+ * user must already be a member of the team's org; cross-org grants 403.
351
+ * Idempotent — re-granting an already-active grant returns 200 with
352
+ * `idempotent_noop: true`.
353
+ */
354
+ export async function grantTeamAdmin(teamId, userId) {
355
+ const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/admins`);
356
+ const response = await fetchWithAuthRetry(url, async () => ({
357
+ method: "POST",
358
+ headers: {
359
+ ...(await authHeaders()),
360
+ "Content-Type": "application/json",
361
+ Accept: "application/json",
362
+ "Idempotency-Key": newIdempotencyKey(),
363
+ },
364
+ body: JSON.stringify({ user_id: sanitizeForHttp(userId) }),
365
+ }));
366
+ if (!response.ok) {
367
+ if (response.status === 401) {
368
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
369
+ }
370
+ if (response.status === 403) {
371
+ throw new Error(`Forbidden: org owner or admin role required, and target user must be in the team's org.`);
372
+ }
373
+ if (response.status === 404) {
374
+ throw new Error(`Team '${teamId}' not found.`);
375
+ }
376
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
377
+ throw new Error(err.message || `API request failed: ${response.status}`);
378
+ }
379
+ return (await response.json());
380
+ }
381
+ /**
382
+ * DELETE /v1/teams/:team_id/admins/:user_id — revoke a team_admin grant.
383
+ *
384
+ * Per ADR-046 §3: callable by org_owner / org_admin OR self-revoke (the
385
+ * team_admin revokes their own grant — the only self-mutation). Idempotent
386
+ * — revoking an absent or already-revoked grant returns 200 with
387
+ * `idempotent_noop: true` and no audit row.
388
+ */
389
+ export async function revokeTeamAdmin(teamId, userId) {
390
+ const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/admins/${userId}`);
391
+ const response = await fetchWithAuthRetry(url, async () => ({
392
+ method: "DELETE",
393
+ headers: {
394
+ ...(await authHeaders()),
395
+ Accept: "application/json",
396
+ "Idempotency-Key": newIdempotencyKey(),
397
+ },
398
+ }));
399
+ if (!response.ok) {
400
+ if (response.status === 401) {
401
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
402
+ }
403
+ if (response.status === 403) {
404
+ throw new Error(`Forbidden: revoke requires org owner/admin, or self-revoke with an active grant.`);
405
+ }
406
+ if (response.status === 404) {
407
+ throw new Error(`Team '${teamId}' not found.`);
408
+ }
409
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
410
+ throw new Error(err.message || `API request failed: ${response.status}`);
411
+ }
412
+ return (await response.json());
413
+ }
414
+ /**
415
+ * GET /v1/teams/:team_id/admins — list active team_admin grants on a team.
416
+ * Any member of the team's org can read.
417
+ */
418
+ export async function listTeamAdmins(teamId) {
419
+ const url = validateUrl(`${API_BASE}/v1/teams/${teamId}/admins`);
420
+ const response = await fetchWithAuthRetry(url, async () => ({
421
+ headers: { ...(await authHeaders()), Accept: "application/json" },
422
+ }));
423
+ if (!response.ok) {
424
+ if (response.status === 401) {
425
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
426
+ }
427
+ if (response.status === 403) {
428
+ throw new Error(`Forbidden: requires membership in the team's org.`);
429
+ }
430
+ if (response.status === 404) {
431
+ throw new Error(`Team '${teamId}' not found.`);
432
+ }
433
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
434
+ throw new Error(err.message || `API request failed: ${response.status}`);
435
+ }
436
+ return (await response.json());
437
+ }
136
438
  /**
137
439
  * Look up an agent in the authenticated user's account by name.
138
440
  * Tries exact match first, then single partial match.
@@ -143,14 +445,14 @@ export async function listAgents() {
143
445
  export async function getAgentByName(name) {
144
446
  const agents = await listAgents();
145
447
  const lower = name.toLowerCase();
146
- const exact = agents.find(a => a.name?.toLowerCase() === lower);
448
+ const exact = agents.find((a) => a.name?.toLowerCase() === lower);
147
449
  if (exact)
148
450
  return exact;
149
- const partials = agents.filter(a => a.name?.toLowerCase().includes(lower));
451
+ const partials = agents.filter((a) => a.name?.toLowerCase().includes(lower));
150
452
  if (partials.length === 1)
151
453
  return partials[0];
152
454
  if (partials.length > 1) {
153
- const names = partials.map(a => a.name ?? a.id).join(", ");
455
+ const names = partials.map((a) => a.name ?? a.id).join(", ");
154
456
  throw new Error(`Multiple agents match '${name}': ${names}. Use a more specific name.`);
155
457
  }
156
458
  return null;
@@ -491,3 +793,372 @@ export async function resolveAgentId(agentName) {
491
793
  console.error("List your agents with: mnemom agents\n");
492
794
  process.exit(1);
493
795
  }
796
+ /** GET /v1/postures — list visible postures. */
797
+ export async function listPostures(opts) {
798
+ const params = new URLSearchParams();
799
+ if (opts.orgId)
800
+ params.set("org_id", opts.orgId);
801
+ if (opts.includePlatform === false)
802
+ params.set("include_platform", "false");
803
+ if (opts.includeDeleted)
804
+ params.set("include_deleted", "true");
805
+ const qs = params.toString();
806
+ const url = validateUrl(`${API_BASE}/v1/postures${qs ? `?${qs}` : ""}`);
807
+ const response = await fetchWithAuthRetry(url, async () => ({
808
+ headers: { ...(await authHeaders()), Accept: "application/json" },
809
+ }));
810
+ if (!response.ok) {
811
+ if (response.status === 401) {
812
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
813
+ }
814
+ if (response.status === 403) {
815
+ throw new Error(`Forbidden: not a member of org '${opts.orgId}'.`);
816
+ }
817
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
818
+ throw new Error(err.message || `API request failed: ${response.status}`);
819
+ }
820
+ const data = (await response.json());
821
+ return data.postures ?? [];
822
+ }
823
+ /** GET /v1/postures/:id — read posture with current revision body. */
824
+ export async function getPosture(postureId) {
825
+ const url = validateUrl(`${API_BASE}/v1/postures/${postureId}`);
826
+ const response = await fetchWithAuthRetry(url, async () => ({
827
+ headers: { ...(await authHeaders()), Accept: "application/json" },
828
+ }));
829
+ if (!response.ok) {
830
+ if (response.status === 401) {
831
+ throw new Error("Not authenticated. Run `mnemom login` or set MNEMOM_API_KEY.");
832
+ }
833
+ if (response.status === 404)
834
+ throw new Error(`Posture '${postureId}' not found.`);
835
+ if (response.status === 403)
836
+ throw new Error(`Forbidden: not visible to your account.`);
837
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
838
+ throw new Error(err.message || `API request failed: ${response.status}`);
839
+ }
840
+ return (await response.json());
841
+ }
842
+ /** GET /v1/postures/:id/revisions — list revisions, newest first. */
843
+ export async function listPostureRevisions(postureId) {
844
+ const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/revisions`);
845
+ const response = await fetchWithAuthRetry(url, async () => ({
846
+ headers: { ...(await authHeaders()), Accept: "application/json" },
847
+ }));
848
+ if (!response.ok) {
849
+ if (response.status === 401)
850
+ throw new Error("Not authenticated.");
851
+ if (response.status === 404)
852
+ throw new Error(`Posture '${postureId}' not found.`);
853
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
854
+ throw new Error(err.message || `API request failed: ${response.status}`);
855
+ }
856
+ const data = (await response.json());
857
+ return data.revisions ?? [];
858
+ }
859
+ /** GET /v1/postures/:id/diff?from=N&to=M — structural diff. */
860
+ export async function diffPostureRevisions(postureId, fromNo, toNo) {
861
+ const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/diff?from=${fromNo}&to=${toNo}`);
862
+ const response = await fetchWithAuthRetry(url, async () => ({
863
+ headers: { ...(await authHeaders()), Accept: "application/json" },
864
+ }));
865
+ if (!response.ok) {
866
+ if (response.status === 401)
867
+ throw new Error("Not authenticated.");
868
+ if (response.status === 404) {
869
+ const errBody = (await response.json().catch(() => ({})));
870
+ throw new Error(errBody.message || `Posture or revision not found.`);
871
+ }
872
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
873
+ throw new Error(err.message || `Diff failed: ${response.status}`);
874
+ }
875
+ return (await response.json());
876
+ }
877
+ /** POST /v1/postures — create new posture (org-scope only via REST). */
878
+ export async function createPosture(input) {
879
+ const url = validateUrl(`${API_BASE}/v1/postures`);
880
+ const response = await fetchWithAuthRetry(url, async () => ({
881
+ method: "POST",
882
+ headers: {
883
+ ...(await authHeaders()),
884
+ "Content-Type": "application/json",
885
+ Accept: "application/json",
886
+ "Idempotency-Key": newIdempotencyKey(),
887
+ },
888
+ body: JSON.stringify(input),
889
+ }));
890
+ if (!response.ok) {
891
+ if (response.status === 401)
892
+ throw new Error("Not authenticated.");
893
+ if (response.status === 403) {
894
+ throw new Error(`Forbidden: org owner/admin role required (or scope=platform reserved for migrations).`);
895
+ }
896
+ if (response.status === 409)
897
+ throw new Error(`A posture with this slug already exists in the org.`);
898
+ if (response.status === 413)
899
+ throw new Error(`Posture body too large (server limit: 256 KiB).`);
900
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
901
+ throw new Error(err.message || `Create failed: ${response.status}`);
902
+ }
903
+ return (await response.json());
904
+ }
905
+ /** PUT /v1/postures/:id — write a new revision (forward-only). */
906
+ export async function updatePosture(postureId, input) {
907
+ const url = validateUrl(`${API_BASE}/v1/postures/${postureId}`);
908
+ const response = await fetchWithAuthRetry(url, async () => ({
909
+ method: "PUT",
910
+ headers: {
911
+ ...(await authHeaders()),
912
+ "Content-Type": "application/json",
913
+ Accept: "application/json",
914
+ "Idempotency-Key": newIdempotencyKey(),
915
+ },
916
+ body: JSON.stringify(input),
917
+ }));
918
+ if (!response.ok) {
919
+ if (response.status === 401)
920
+ throw new Error("Not authenticated.");
921
+ if (response.status === 403) {
922
+ throw new Error(`Forbidden: org owner/admin required, or this is a Mnemom-shipped platform default (immutable — clone first).`);
923
+ }
924
+ if (response.status === 404)
925
+ throw new Error(`Posture '${postureId}' not found.`);
926
+ if (response.status === 413)
927
+ throw new Error(`Posture body too large (server limit: 256 KiB).`);
928
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
929
+ throw new Error(err.message || `Update failed: ${response.status}`);
930
+ }
931
+ return (await response.json());
932
+ }
933
+ /** POST /v1/postures/:id/clone — clone source to org-scope. */
934
+ export async function clonePosture(postureId, input) {
935
+ const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/clone`);
936
+ const response = await fetchWithAuthRetry(url, async () => ({
937
+ method: "POST",
938
+ headers: {
939
+ ...(await authHeaders()),
940
+ "Content-Type": "application/json",
941
+ Accept: "application/json",
942
+ "Idempotency-Key": newIdempotencyKey(),
943
+ },
944
+ body: JSON.stringify(input),
945
+ }));
946
+ if (!response.ok) {
947
+ if (response.status === 401)
948
+ throw new Error("Not authenticated.");
949
+ if (response.status === 403)
950
+ throw new Error(`Forbidden: org owner/admin required on target org.`);
951
+ if (response.status === 404)
952
+ throw new Error(`Source posture '${postureId}' not found.`);
953
+ if (response.status === 409)
954
+ throw new Error(`A posture with this slug already exists in the target org.`);
955
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
956
+ throw new Error(err.message || `Clone failed: ${response.status}`);
957
+ }
958
+ return (await response.json());
959
+ }
960
+ /** DELETE /v1/postures/:id — soft-delete (refuses if assigned). */
961
+ export async function deletePosture(postureId) {
962
+ const url = validateUrl(`${API_BASE}/v1/postures/${postureId}`);
963
+ const response = await fetchWithAuthRetry(url, async () => ({
964
+ method: "DELETE",
965
+ headers: {
966
+ ...(await authHeaders()),
967
+ Accept: "application/json",
968
+ "Idempotency-Key": newIdempotencyKey(),
969
+ },
970
+ }));
971
+ if (!response.ok) {
972
+ if (response.status === 401)
973
+ throw new Error("Not authenticated.");
974
+ if (response.status === 403)
975
+ throw new Error(`Forbidden: platform defaults are immutable.`);
976
+ if (response.status === 404)
977
+ throw new Error(`Posture '${postureId}' not found.`);
978
+ if (response.status === 409) {
979
+ throw new Error(`Cannot delete a posture that is currently assigned to one or more teams. Unassign first.`);
980
+ }
981
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
982
+ throw new Error(err.message || `Delete failed: ${response.status}`);
983
+ }
984
+ }
985
+ /** POST /v1/postures/:id/assign — assign to a team. */
986
+ export async function assignPosture(postureId, teamId, pinRevisionNo) {
987
+ const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/assign`);
988
+ const response = await fetchWithAuthRetry(url, async () => ({
989
+ method: "POST",
990
+ headers: {
991
+ ...(await authHeaders()),
992
+ "Content-Type": "application/json",
993
+ Accept: "application/json",
994
+ "Idempotency-Key": newIdempotencyKey(),
995
+ },
996
+ body: JSON.stringify({ team_id: teamId, pin_revision_no: pinRevisionNo ?? null }),
997
+ }));
998
+ if (!response.ok) {
999
+ if (response.status === 401)
1000
+ throw new Error("Not authenticated.");
1001
+ if (response.status === 403)
1002
+ throw new Error(`Forbidden: org owner/admin required on team's org.`);
1003
+ if (response.status === 404) {
1004
+ const errBody = (await response.json().catch(() => ({})));
1005
+ throw new Error(errBody.message || `Posture, team, or pinned revision not found.`);
1006
+ }
1007
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
1008
+ throw new Error(err.message || `Assign failed: ${response.status}`);
1009
+ }
1010
+ return (await response.json());
1011
+ }
1012
+ /** DELETE /v1/postures/:id/assignments/:team_id — remove an assignment. */
1013
+ export async function unassignPosture(postureId, teamId) {
1014
+ const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/assignments/${teamId}`);
1015
+ const response = await fetchWithAuthRetry(url, async () => ({
1016
+ method: "DELETE",
1017
+ headers: {
1018
+ ...(await authHeaders()),
1019
+ Accept: "application/json",
1020
+ "Idempotency-Key": newIdempotencyKey(),
1021
+ },
1022
+ }));
1023
+ if (!response.ok) {
1024
+ if (response.status === 401)
1025
+ throw new Error("Not authenticated.");
1026
+ if (response.status === 403)
1027
+ throw new Error(`Forbidden: org owner/admin required.`);
1028
+ if (response.status === 404) {
1029
+ throw new Error(`No matching assignment to remove (posture '${postureId}' is not assigned to team '${teamId}').`);
1030
+ }
1031
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
1032
+ throw new Error(err.message || `Unassign failed: ${response.status}`);
1033
+ }
1034
+ }
1035
+ /** POST /v1/postures/:id/preview-compose — preview effective for a team. */
1036
+ export async function previewComposePosture(postureId, teamId) {
1037
+ const url = validateUrl(`${API_BASE}/v1/postures/${postureId}/preview-compose`);
1038
+ const response = await fetchWithAuthRetry(url, async () => ({
1039
+ method: "POST",
1040
+ headers: {
1041
+ ...(await authHeaders()),
1042
+ "Content-Type": "application/json",
1043
+ Accept: "application/json",
1044
+ },
1045
+ body: JSON.stringify({ team_id: teamId }),
1046
+ }));
1047
+ if (!response.ok) {
1048
+ if (response.status === 401)
1049
+ throw new Error("Not authenticated.");
1050
+ if (response.status === 404) {
1051
+ const errBody = (await response.json().catch(() => ({})));
1052
+ throw new Error(errBody.message || `Posture or team not found.`);
1053
+ }
1054
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
1055
+ throw new Error(err.message || `Preview-compose failed: ${response.status}`);
1056
+ }
1057
+ return (await response.json());
1058
+ }
1059
+ /**
1060
+ * GET /v1/agents/:id/sideband-advisories
1061
+ *
1062
+ * Lists pending_advisories rows where source LIKE 'sideband.%' for the
1063
+ * given agent. Default page 50, max 200. Optional ?since=<ISO> filter.
1064
+ */
1065
+ export async function listSidebandAdvisoriesForAgent(agentId, opts = {}) {
1066
+ const params = new URLSearchParams();
1067
+ if (opts.limit)
1068
+ params.set("limit", String(opts.limit));
1069
+ if (opts.since)
1070
+ params.set("since", opts.since);
1071
+ const qs = params.toString() ? `?${params.toString()}` : "";
1072
+ const url = validateUrl(`${API_BASE}/v1/agents/${encodeURIComponent(agentId)}/sideband-advisories${qs}`);
1073
+ const response = await fetchWithAuthRetry(url, async () => ({
1074
+ method: "GET",
1075
+ headers: { ...(await authHeaders()), Accept: "application/json" },
1076
+ }));
1077
+ if (!response.ok) {
1078
+ if (response.status === 401)
1079
+ throw new Error("Not authenticated.");
1080
+ if (response.status === 403)
1081
+ throw new Error("Permission denied (need org membership).");
1082
+ if (response.status === 404)
1083
+ throw new Error(`Agent '${agentId}' not found.`);
1084
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
1085
+ throw new Error(err.message || `List advisories failed: ${response.status}`);
1086
+ }
1087
+ return (await response.json());
1088
+ }
1089
+ /**
1090
+ * GET /v1/teams/:id/sideband-advisories
1091
+ *
1092
+ * Lists pending_advisories rows for any agent that is a current member of
1093
+ * the team. Default page 100, max 500. Optional ?since=<ISO> filter.
1094
+ */
1095
+ export async function listSidebandAdvisoriesForTeam(teamId, opts = {}) {
1096
+ const params = new URLSearchParams();
1097
+ if (opts.limit)
1098
+ params.set("limit", String(opts.limit));
1099
+ if (opts.since)
1100
+ params.set("since", opts.since);
1101
+ const qs = params.toString() ? `?${params.toString()}` : "";
1102
+ const url = validateUrl(`${API_BASE}/v1/teams/${encodeURIComponent(teamId)}/sideband-advisories${qs}`);
1103
+ const response = await fetchWithAuthRetry(url, async () => ({
1104
+ method: "GET",
1105
+ headers: { ...(await authHeaders()), Accept: "application/json" },
1106
+ }));
1107
+ if (!response.ok) {
1108
+ if (response.status === 401)
1109
+ throw new Error("Not authenticated.");
1110
+ if (response.status === 403)
1111
+ throw new Error("Permission denied (need org membership).");
1112
+ if (response.status === 404)
1113
+ throw new Error(`Team '${teamId}' not found.`);
1114
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
1115
+ throw new Error(err.message || `List advisories failed: ${response.status}`);
1116
+ }
1117
+ return (await response.json());
1118
+ }
1119
+ /**
1120
+ * GET /v1/teams/:id/sideband-coverage
1121
+ *
1122
+ * Compliance-grade proof-of-coverage for a team — last 30 days of
1123
+ * per-axis sweep aggregates + heartbeat. Returns per-axis summary +
1124
+ * raw rows.
1125
+ */
1126
+ export async function getTeamSidebandCoverage(teamId) {
1127
+ const url = validateUrl(`${API_BASE}/v1/teams/${encodeURIComponent(teamId)}/sideband-coverage`);
1128
+ const response = await fetchWithAuthRetry(url, async () => ({
1129
+ method: "GET",
1130
+ headers: { ...(await authHeaders()), Accept: "application/json" },
1131
+ }));
1132
+ if (!response.ok) {
1133
+ if (response.status === 401)
1134
+ throw new Error("Not authenticated.");
1135
+ if (response.status === 403)
1136
+ throw new Error("Permission denied (need org membership).");
1137
+ if (response.status === 404)
1138
+ throw new Error(`Team '${teamId}' not found.`);
1139
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
1140
+ throw new Error(err.message || `Coverage fetch failed: ${response.status}`);
1141
+ }
1142
+ return (await response.json());
1143
+ }
1144
+ /**
1145
+ * GET /v1/admin/safe-house/harness-state — fetch the most recent
1146
+ * harness run per lane (full + fast). Powers `mnemom validate
1147
+ * safe-house`. Admin-gated; returns 403 for non-mnemom-staff.
1148
+ */
1149
+ export async function getSafeHouseHarnessState() {
1150
+ const url = validateUrl(`${API_BASE}/v1/admin/safe-house/harness-state`);
1151
+ const response = await fetchWithAuthRetry(url, async () => ({
1152
+ method: "GET",
1153
+ headers: { ...(await authHeaders()), Accept: "application/json" },
1154
+ }));
1155
+ if (!response.ok) {
1156
+ if (response.status === 401)
1157
+ throw new Error("Not authenticated. Run `mnemom login`.");
1158
+ if (response.status === 403)
1159
+ throw new Error("Permission denied — `mnemom validate safe-house` is mnemom-staff only.");
1160
+ const err = (await response.json().catch(() => ({ error: "unknown" })));
1161
+ throw new Error(err.message || `Harness state fetch failed: ${response.status}`);
1162
+ }
1163
+ return (await response.json());
1164
+ }