@mnemom/mnemom 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,328 @@
1
+ /**
2
+ * `mnemom webhooks ...` commands — Track 2 W3.5.
3
+ *
4
+ * mnemom webhooks list <org_id> — list endpoints
5
+ * mnemom webhooks get <org_id> <endpoint_id> — show one
6
+ * mnemom webhooks create <org_id> --url <u> [--events <list>]
7
+ * [--description <d>]
8
+ * mnemom webhooks update <org_id> <endpoint_id> [--url <u>]
9
+ * [--events <list>]
10
+ * [--active <bool>]
11
+ * mnemom webhooks delete <org_id> <endpoint_id> — remove endpoint
12
+ * mnemom webhooks rotate-secret <org_id> <endpoint_id> — mint new secret
13
+ * mnemom webhooks list-deliveries <org_id> [--endpoint <id>] [--limit <n>]
14
+ * mnemom webhooks redeliver <org_id> <delivery_id> — retry one delivery
15
+ * mnemom webhooks replay <org_id> <event_id> [--endpoint <id>...]
16
+ * mnemom webhooks trigger <org_id> <endpoint_id> — fire a test event
17
+ *
18
+ * Every mutation auto-generates a fresh `Idempotency-Key`. Re-running the
19
+ * same command does NOT replay the cache (you'd need to keep the key
20
+ * around for that — the curl/programmatic clients do); for the typical
21
+ * CLI ergonomic path, each invocation is its own logical mutation.
22
+ */
23
+ import { listWebhookEndpoints, getWebhookEndpoint, createWebhookEndpoint, updateWebhookEndpoint, deleteWebhookEndpoint, rotateWebhookSecret, testWebhookEndpoint, listWebhookDeliveries, redeliverWebhookEvent, replayWebhookEvent, } from "../lib/webhooks-api.js";
24
+ import { requireAuth } from "../lib/auth.js";
25
+ import { fmt } from "../lib/format.js";
26
+ function parseEventList(value) {
27
+ if (!value)
28
+ return undefined;
29
+ return value
30
+ .split(",")
31
+ .map((s) => s.trim())
32
+ .filter(Boolean);
33
+ }
34
+ function fail(msg) {
35
+ console.log(fmt.error(msg) + "\n");
36
+ process.exit(1);
37
+ }
38
+ function shortenSecret(secret) {
39
+ if (!secret)
40
+ return "—";
41
+ if (secret.length <= 16)
42
+ return secret;
43
+ return `${secret.slice(0, 8)}…${secret.slice(-4)}`;
44
+ }
45
+ function formatTime(ts) {
46
+ if (!ts)
47
+ return "—";
48
+ try {
49
+ return new Date(ts).toISOString().replace("T", " ").slice(0, 19) + "Z";
50
+ }
51
+ catch {
52
+ return ts;
53
+ }
54
+ }
55
+ // ─── list ────────────────────────────────────────────────────────────────
56
+ export async function webhooksListCommand(orgId, opts = {}) {
57
+ await requireAuth();
58
+ let endpoints;
59
+ try {
60
+ endpoints = await listWebhookEndpoints(orgId);
61
+ }
62
+ catch (err) {
63
+ fail(`Failed to list endpoints: ${err instanceof Error ? err.message : String(err)}`);
64
+ }
65
+ if (opts.json) {
66
+ console.log(JSON.stringify(endpoints, null, 2));
67
+ return;
68
+ }
69
+ console.log(fmt.header(`Webhook endpoints — org ${orgId}`));
70
+ console.log();
71
+ if (endpoints.length === 0) {
72
+ console.log(" No endpoints configured. Create one with `mnemom webhooks create`.\n");
73
+ return;
74
+ }
75
+ const idW = 26;
76
+ const urlW = 44;
77
+ const eventsW = 8;
78
+ const activeW = 8;
79
+ console.log("Endpoint".padEnd(idW) +
80
+ "URL".padEnd(urlW) +
81
+ "Events".padEnd(eventsW) +
82
+ "Active".padEnd(activeW));
83
+ console.log("─".repeat(idW + urlW + eventsW + activeW));
84
+ for (const ep of endpoints) {
85
+ const url = ep.url.length > urlW - 1 ? ep.url.slice(0, urlW - 4) + "…" : ep.url;
86
+ console.log(ep.endpoint_id.padEnd(idW) +
87
+ url.padEnd(urlW) +
88
+ String(ep.event_types.length).padEnd(eventsW) +
89
+ (ep.is_active ? "yes" : "no").padEnd(activeW));
90
+ }
91
+ console.log();
92
+ }
93
+ // ─── get ─────────────────────────────────────────────────────────────────
94
+ export async function webhooksGetCommand(orgId, endpointId, opts = {}) {
95
+ await requireAuth();
96
+ let ep;
97
+ try {
98
+ ep = await getWebhookEndpoint(orgId, endpointId);
99
+ }
100
+ catch (err) {
101
+ fail(`Failed to fetch endpoint: ${err instanceof Error ? err.message : String(err)}`);
102
+ }
103
+ if (opts.json) {
104
+ console.log(JSON.stringify(ep, null, 2));
105
+ return;
106
+ }
107
+ console.log(fmt.header(`Webhook endpoint ${ep.endpoint_id}`));
108
+ console.log();
109
+ console.log(` URL ${ep.url}`);
110
+ console.log(` Description ${ep.description || "(none)"}`);
111
+ console.log(` Events ${ep.event_types.length === 0 ? "(none)" : ep.event_types.join(", ")}`);
112
+ console.log(` Active ${ep.is_active ? "yes" : "no"}`);
113
+ console.log(` Failures ${ep.consecutive_failures}`);
114
+ if (ep.disabled_at) {
115
+ console.log(` Disabled at ${formatTime(ep.disabled_at)} — ${ep.disabled_reason ?? "(no reason)"}`);
116
+ }
117
+ console.log(` Created ${formatTime(ep.created_at)}`);
118
+ console.log(` Updated ${formatTime(ep.updated_at)}`);
119
+ if (ep.signing_secret) {
120
+ console.log(` Secret ${shortenSecret(ep.signing_secret)} (shown only on create / rotate)`);
121
+ }
122
+ console.log();
123
+ }
124
+ // ─── create ──────────────────────────────────────────────────────────────
125
+ export async function webhooksCreateCommand(orgId, opts = {}) {
126
+ await requireAuth();
127
+ if (!opts.url)
128
+ fail("--url is required (HTTPS only; private/internal IPs blocked)");
129
+ let created;
130
+ try {
131
+ created = await createWebhookEndpoint(orgId, {
132
+ url: opts.url,
133
+ description: opts.description,
134
+ event_types: parseEventList(opts.events),
135
+ });
136
+ }
137
+ catch (err) {
138
+ fail(`Failed to create endpoint: ${err instanceof Error ? err.message : String(err)}`);
139
+ }
140
+ if (opts.json) {
141
+ console.log(JSON.stringify(created, null, 2));
142
+ return;
143
+ }
144
+ console.log(fmt.success("Webhook endpoint created"));
145
+ console.log();
146
+ console.log(` Endpoint ID ${created.endpoint_id}`);
147
+ console.log(` URL ${created.url}`);
148
+ console.log(` Events ${created.event_types.length === 0 ? "(none — explicit per-event opt-in required)" : created.event_types.join(", ")}`);
149
+ if (created.signing_secret) {
150
+ console.log();
151
+ console.log(fmt.warn("Signing secret (shown ONCE — store it now):"));
152
+ console.log(` ${created.signing_secret}`);
153
+ }
154
+ console.log();
155
+ }
156
+ // ─── update ──────────────────────────────────────────────────────────────
157
+ export async function webhooksUpdateCommand(orgId, endpointId, opts = {}) {
158
+ await requireAuth();
159
+ const body = {};
160
+ if (opts.url !== undefined)
161
+ body.url = opts.url;
162
+ if (opts.description !== undefined)
163
+ body.description = opts.description;
164
+ const events = parseEventList(opts.events);
165
+ if (events !== undefined)
166
+ body.event_types = events;
167
+ if (opts.active !== undefined) {
168
+ if (opts.active === "true")
169
+ body.is_active = true;
170
+ else if (opts.active === "false")
171
+ body.is_active = false;
172
+ else
173
+ fail("--active must be 'true' or 'false'");
174
+ }
175
+ if (Object.keys(body).length === 0)
176
+ fail("Nothing to update — pass --url / --events / --description / --active.");
177
+ let updated;
178
+ try {
179
+ updated = await updateWebhookEndpoint(orgId, endpointId, body);
180
+ }
181
+ catch (err) {
182
+ fail(`Failed to update endpoint: ${err instanceof Error ? err.message : String(err)}`);
183
+ }
184
+ if (opts.json) {
185
+ console.log(JSON.stringify(updated, null, 2));
186
+ return;
187
+ }
188
+ console.log(fmt.success(`Updated endpoint ${updated.endpoint_id}`));
189
+ console.log();
190
+ }
191
+ // ─── delete ──────────────────────────────────────────────────────────────
192
+ export async function webhooksDeleteCommand(orgId, endpointId) {
193
+ await requireAuth();
194
+ try {
195
+ await deleteWebhookEndpoint(orgId, endpointId);
196
+ }
197
+ catch (err) {
198
+ fail(`Failed to delete endpoint: ${err instanceof Error ? err.message : String(err)}`);
199
+ }
200
+ console.log(fmt.success(`Deleted endpoint ${endpointId}`));
201
+ console.log();
202
+ }
203
+ // ─── rotate-secret ───────────────────────────────────────────────────────
204
+ export async function webhooksRotateSecretCommand(orgId, endpointId, opts = {}) {
205
+ await requireAuth();
206
+ let result;
207
+ try {
208
+ result = await rotateWebhookSecret(orgId, endpointId);
209
+ }
210
+ catch (err) {
211
+ fail(`Failed to rotate secret: ${err instanceof Error ? err.message : String(err)}`);
212
+ }
213
+ if (opts.json) {
214
+ console.log(JSON.stringify(result, null, 2));
215
+ return;
216
+ }
217
+ console.log(fmt.success(`Rotated signing secret for ${result.endpoint_id}`));
218
+ console.log();
219
+ console.log(fmt.warn("New signing secret (shown ONCE — store it now):"));
220
+ console.log(` ${result.signing_secret}`);
221
+ console.log();
222
+ }
223
+ // ─── trigger (test) ──────────────────────────────────────────────────────
224
+ export async function webhooksTriggerCommand(orgId, endpointId, opts = {}) {
225
+ await requireAuth();
226
+ let result;
227
+ try {
228
+ result = await testWebhookEndpoint(orgId, endpointId);
229
+ }
230
+ catch (err) {
231
+ fail(`Failed to trigger test: ${err instanceof Error ? err.message : String(err)}`);
232
+ }
233
+ if (opts.json) {
234
+ console.log(JSON.stringify(result, null, 2));
235
+ return;
236
+ }
237
+ if (result.success) {
238
+ console.log(fmt.success(`Test delivery succeeded — receiver returned ${result.status} in ${result.latency_ms}ms`));
239
+ }
240
+ else {
241
+ console.log(fmt.error(`Test delivery failed — status ${result.status ?? "(no response)"}`));
242
+ if (result.error)
243
+ console.log(` ${result.error}`);
244
+ }
245
+ console.log();
246
+ }
247
+ // ─── list-deliveries ─────────────────────────────────────────────────────
248
+ export async function webhooksListDeliveriesCommand(orgId, opts = {}) {
249
+ await requireAuth();
250
+ const limit = opts.limit ? parseInt(opts.limit, 10) : undefined;
251
+ const offset = opts.offset ? parseInt(opts.offset, 10) : undefined;
252
+ let deliveries;
253
+ try {
254
+ deliveries = await listWebhookDeliveries(orgId, {
255
+ endpointId: opts.endpoint,
256
+ limit: limit && !Number.isNaN(limit) ? limit : undefined,
257
+ offset: offset && !Number.isNaN(offset) ? offset : undefined,
258
+ });
259
+ }
260
+ catch (err) {
261
+ fail(`Failed to list deliveries: ${err instanceof Error ? err.message : String(err)}`);
262
+ }
263
+ if (opts.json) {
264
+ console.log(JSON.stringify(deliveries, null, 2));
265
+ return;
266
+ }
267
+ console.log(fmt.header(`Webhook deliveries — org ${orgId}`));
268
+ console.log();
269
+ if (deliveries.length === 0) {
270
+ console.log(" No deliveries on record.\n");
271
+ return;
272
+ }
273
+ for (const d of deliveries) {
274
+ const ts = formatTime(d.last_attempt_at ?? d.created_at);
275
+ console.log(` ${d.delivery_id} ${d.status.padEnd(10)} → endpoint ${d.endpoint_id} (${ts})`);
276
+ }
277
+ console.log();
278
+ }
279
+ // ─── redeliver ───────────────────────────────────────────────────────────
280
+ export async function webhooksRedeliverCommand(orgId, deliveryId, opts = {}) {
281
+ await requireAuth();
282
+ let result;
283
+ try {
284
+ result = await redeliverWebhookEvent(orgId, deliveryId);
285
+ }
286
+ catch (err) {
287
+ fail(`Failed to redeliver: ${err instanceof Error ? err.message : String(err)}`);
288
+ }
289
+ if (opts.json) {
290
+ console.log(JSON.stringify(result, null, 2));
291
+ return;
292
+ }
293
+ console.log(fmt.success(`Redelivery queued — new delivery_id ${result.delivery_id} (status ${result.status})`));
294
+ console.log();
295
+ }
296
+ // ─── replay ──────────────────────────────────────────────────────────────
297
+ export async function webhooksReplayCommand(orgId, eventId, opts = {}) {
298
+ await requireAuth();
299
+ let result;
300
+ try {
301
+ result = await replayWebhookEvent(orgId, eventId, { endpointIds: opts.endpoint });
302
+ }
303
+ catch (err) {
304
+ fail(`Failed to replay event: ${err instanceof Error ? err.message : String(err)}`);
305
+ }
306
+ if (opts.json) {
307
+ console.log(JSON.stringify(result, null, 2));
308
+ return;
309
+ }
310
+ console.log(fmt.header(`Replay — event ${eventId} (${result.event_type})`));
311
+ console.log();
312
+ if (result.deliveries.length === 0) {
313
+ console.log(" " + (result.message ?? "No subscribed endpoints to replay to."));
314
+ console.log();
315
+ return;
316
+ }
317
+ for (const d of result.deliveries) {
318
+ console.log(` ✓ ${d.delivery_id} → endpoint ${d.endpoint_id}`);
319
+ }
320
+ if (result.failed_endpoints && result.failed_endpoints.length > 0) {
321
+ console.log();
322
+ console.log(fmt.warn("Failed endpoints:"));
323
+ for (const f of result.failed_endpoints) {
324
+ console.log(` ✗ ${f.endpoint_id}: ${f.error}`);
325
+ }
326
+ }
327
+ console.log();
328
+ }
package/dist/index.js CHANGED
@@ -10,11 +10,13 @@ import { protectionShowCommand, protectionPublishCommand, protectionValidateComm
10
10
  import { agentsListCommand } from "./commands/agents.js";
11
11
  import { orgListCommand, orgShowCommand } from "./commands/org.js";
12
12
  import { teamListCommand, teamShowCommand, teamTemplateCommand, teamPreviewComposeCommand, teamAdminGrantCommand, teamAdminRevokeCommand, teamAdminListCommand, teamCoverageCommand, } from "./commands/team.js";
13
- import { advisoriesListCommand, advisoriesShowCommand, } from "./commands/advisories.js";
13
+ import { advisoriesListCommand, advisoriesShowCommand } from "./commands/advisories.js";
14
14
  import { postureListCommand, postureShowCommand, postureCreateCommand, postureUpdateCommand, postureCloneCommand, postureRevisionsCommand, postureDiffCommand, postureAssignCommand, postureUnassignCommand, posturePreviewComposeCommand, postureDeleteCommand, } from "./commands/posture.js";
15
15
  import { loginCommand, logoutCommand, whoamiCommand } from "./commands/auth.js";
16
16
  import { validateSafeHouseCommand } from "./commands/validate.js";
17
17
  import { apiKeyListCommand, apiKeyCreateCommand, apiKeyRotateCommand, apiKeyRevokeCommand, } from "./commands/api-key.js";
18
+ import { webhooksListCommand, webhooksGetCommand, webhooksCreateCommand, webhooksUpdateCommand, webhooksDeleteCommand, webhooksRotateSecretCommand, webhooksTriggerCommand, webhooksListDeliveriesCommand, webhooksRedeliverCommand, webhooksReplayCommand, } from "./commands/webhooks.js";
19
+ import { listenCommand } from "./commands/listen.js";
18
20
  program
19
21
  .name("mnemom")
20
22
  .description("Transparent AI agent tracing")
@@ -638,7 +640,7 @@ govDestCmd
638
640
  .description("Add a destination (channel + JSON config)")
639
641
  .requiredOption("--org <id>", "Org ID")
640
642
  .requiredOption("--channel <c>", "webhook|slack|email|pagerduty")
641
- .requiredOption("--config <json>", "Channel config (e.g., '{\"url\":\"...\",\"signing_secret\":\"...\"}')")
643
+ .requiredOption("--config <json>", 'Channel config (e.g., \'{"url":"...","signing_secret":"..."}\')')
642
644
  .option("--name <n>", "Display name")
643
645
  .option("--filter <json>", "Filter narrowing (sources/severities/etc)")
644
646
  .option("--json", "Output JSON")
@@ -701,7 +703,7 @@ govRulesCmd
701
703
  .description("Add an escalation rule (predicate JSON + destination IDs)")
702
704
  .requiredOption("--org <id>", "Org ID")
703
705
  .requiredOption("--name <n>", "Rule name")
704
- .requiredOption("--predicate <json>", "Predicate JSON (e.g., '{\"source\":\"sideband.fleet\",\"severity_min\":\"high\"}')")
706
+ .requiredOption("--predicate <json>", 'Predicate JSON (e.g., \'{"source":"sideband.fleet","severity_min":"high"}\')')
705
707
  .requiredOption("--destinations <ids>", "Comma-separated destination IDs")
706
708
  .option("--json", "Output JSON")
707
709
  .action(async (options) => {
@@ -1035,4 +1037,258 @@ apiKeyCmd
1035
1037
  process.exit(1);
1036
1038
  }
1037
1039
  });
1040
+ // ============================================================================
1041
+ // Webhook lifecycle commands (Track 2 W3.5)
1042
+ // ============================================================================
1043
+ const webhooksCmd = program
1044
+ .command("webhooks")
1045
+ .description("Manage webhook endpoints, deliveries, and event replay");
1046
+ webhooksCmd
1047
+ .command("list <org_id>")
1048
+ .description("List webhook endpoints for an org")
1049
+ .option("--json", "Emit raw JSON instead of rendered output")
1050
+ .action(async (orgId, options) => {
1051
+ try {
1052
+ await webhooksListCommand(orgId, options);
1053
+ }
1054
+ catch (error) {
1055
+ console.error("Error:", error instanceof Error ? error.message : error);
1056
+ process.exit(1);
1057
+ }
1058
+ });
1059
+ webhooksCmd
1060
+ .command("get <org_id> <endpoint_id>")
1061
+ .description("Show details for a single webhook endpoint")
1062
+ .option("--json", "Emit raw JSON instead of rendered output")
1063
+ .action(async (orgId, endpointId, options) => {
1064
+ try {
1065
+ await webhooksGetCommand(orgId, endpointId, options);
1066
+ }
1067
+ catch (error) {
1068
+ console.error("Error:", error instanceof Error ? error.message : error);
1069
+ process.exit(1);
1070
+ }
1071
+ });
1072
+ webhooksCmd
1073
+ .command("create <org_id>")
1074
+ .description("Create a new webhook endpoint (signing secret returned ONCE)")
1075
+ .requiredOption("--url <url>", "HTTPS URL to deliver events to (private/internal IPs blocked)")
1076
+ .option("--events <list>", "Comma-separated list of event types to subscribe to (default: none — explicit per-event opt-in)")
1077
+ .option("--description <text>", "Human-readable description")
1078
+ .option("--json", "Emit raw JSON instead of rendered output")
1079
+ .action(async (orgId, options) => {
1080
+ try {
1081
+ await webhooksCreateCommand(orgId, options);
1082
+ }
1083
+ catch (error) {
1084
+ console.error("Error:", error instanceof Error ? error.message : error);
1085
+ process.exit(1);
1086
+ }
1087
+ });
1088
+ webhooksCmd
1089
+ .command("update <org_id> <endpoint_id>")
1090
+ .description("Update fields on an existing webhook endpoint")
1091
+ .option("--url <url>", "Replace the delivery URL")
1092
+ .option("--events <list>", "Replace the event-type subscription list (comma-separated)")
1093
+ .option("--description <text>", "Replace the description")
1094
+ .option("--active <bool>", "Set is_active to 'true' or 'false' (re-enabling resets failure counter)")
1095
+ .option("--json", "Emit raw JSON instead of rendered output")
1096
+ .action(async (orgId, endpointId, options) => {
1097
+ try {
1098
+ await webhooksUpdateCommand(orgId, endpointId, options);
1099
+ }
1100
+ catch (error) {
1101
+ console.error("Error:", error instanceof Error ? error.message : error);
1102
+ process.exit(1);
1103
+ }
1104
+ });
1105
+ webhooksCmd
1106
+ .command("delete <org_id> <endpoint_id>")
1107
+ .description("Permanently delete a webhook endpoint")
1108
+ .action(async (orgId, endpointId) => {
1109
+ try {
1110
+ await webhooksDeleteCommand(orgId, endpointId);
1111
+ }
1112
+ catch (error) {
1113
+ console.error("Error:", error instanceof Error ? error.message : error);
1114
+ process.exit(1);
1115
+ }
1116
+ });
1117
+ webhooksCmd
1118
+ .command("rotate-secret <org_id> <endpoint_id>")
1119
+ .description("Mint a new signing secret (shown ONCE; immediately invalidates the previous secret)")
1120
+ .option("--json", "Emit raw JSON instead of rendered output")
1121
+ .action(async (orgId, endpointId, options) => {
1122
+ try {
1123
+ await webhooksRotateSecretCommand(orgId, endpointId, options);
1124
+ }
1125
+ catch (error) {
1126
+ console.error("Error:", error instanceof Error ? error.message : error);
1127
+ process.exit(1);
1128
+ }
1129
+ });
1130
+ webhooksCmd
1131
+ .command("trigger <org_id> <endpoint_id>")
1132
+ .description("Fire a synthetic test event through the full delivery pipeline (Stripe-equivalent ergonomics)")
1133
+ .option("--json", "Emit raw JSON instead of rendered output")
1134
+ .action(async (orgId, endpointId, options) => {
1135
+ try {
1136
+ await webhooksTriggerCommand(orgId, endpointId, options);
1137
+ }
1138
+ catch (error) {
1139
+ console.error("Error:", error instanceof Error ? error.message : error);
1140
+ process.exit(1);
1141
+ }
1142
+ });
1143
+ webhooksCmd
1144
+ .command("list-deliveries <org_id>")
1145
+ .description("List recent webhook deliveries (optionally scoped to one endpoint)")
1146
+ .option("--endpoint <endpoint_id>", "Scope to a single endpoint")
1147
+ .option("--limit <n>", "Maximum entries to return (default 50, max 100)")
1148
+ .option("--offset <n>", "Skip the first N entries (pagination)")
1149
+ .option("--json", "Emit raw JSON instead of rendered output")
1150
+ .action(async (orgId, options) => {
1151
+ try {
1152
+ await webhooksListDeliveriesCommand(orgId, options);
1153
+ }
1154
+ catch (error) {
1155
+ console.error("Error:", error instanceof Error ? error.message : error);
1156
+ process.exit(1);
1157
+ }
1158
+ });
1159
+ webhooksCmd
1160
+ .command("redeliver <org_id> <delivery_id>")
1161
+ .description("Retry a single previous delivery (re-fires that one delivery row)")
1162
+ .option("--json", "Emit raw JSON instead of rendered output")
1163
+ .action(async (orgId, deliveryId, options) => {
1164
+ try {
1165
+ await webhooksRedeliverCommand(orgId, deliveryId, options);
1166
+ }
1167
+ catch (error) {
1168
+ console.error("Error:", error instanceof Error ? error.message : error);
1169
+ process.exit(1);
1170
+ }
1171
+ });
1172
+ webhooksCmd
1173
+ .command("replay <org_id> <event_id>")
1174
+ .description("Re-fan-out a historical event to all currently-subscribed endpoints (or to a specified subset)")
1175
+ .option("--endpoint <endpoint_id>", "Only replay to the specified endpoint (repeatable)", (value, prev) => (prev ? [...prev, value] : [value]))
1176
+ .option("--json", "Emit raw JSON instead of rendered output")
1177
+ .action(async (orgId, eventId, options) => {
1178
+ try {
1179
+ await webhooksReplayCommand(orgId, eventId, options);
1180
+ }
1181
+ catch (error) {
1182
+ console.error("Error:", error instanceof Error ? error.message : error);
1183
+ process.exit(1);
1184
+ }
1185
+ });
1186
+ // ============================================================================
1187
+ // mnemom listen — live webhook event stream (Track 2 W3.4)
1188
+ // ============================================================================
1189
+ program
1190
+ .command("listen <org_id>")
1191
+ .description("Stream live webhook events for an org (Stripe-equivalent ergonomics)")
1192
+ .option("--forward-to <url>", "POST each event to this local URL (requires --secret for HMAC re-signing)")
1193
+ .option("--secret <hex>", "Local signing secret used to re-sign forwarded deliveries")
1194
+ .option("--filter <list>", "Comma-separated event types to surface (default: all)")
1195
+ .option("--since <event_id>", "Resume from this cursor on first connect")
1196
+ .option("--json", "Emit raw JSON lines instead of a formatted summary")
1197
+ .action(async (orgId, options) => {
1198
+ try {
1199
+ await listenCommand(orgId, options);
1200
+ }
1201
+ catch (error) {
1202
+ console.error("Error:", error instanceof Error ? error.message : error);
1203
+ process.exit(1);
1204
+ }
1205
+ });
1206
+ // ============================================================================
1207
+ // AEGIS-6b — Customer FN/FP recipe report commands.
1208
+ //
1209
+ // `mnemom recipes report-fn <recipe-id>` / `mnemom recipes report-fp <id>`
1210
+ // POST to /v1/recipes/:id/report (AEGIS-6a; mnemom-api#508). The endpoint
1211
+ // requires customer-session auth (JWT cookie/bearer or API key) and a
1212
+ // non-empty summary; the CLI accepts the summary as `--summary` or via
1213
+ // stdin. Idempotency-Key is generated per call by postApi(); the body
1214
+ // fingerprint is folded server-side so a retry with corrected text returns
1215
+ // 422 rather than silently replaying.
1216
+ // ============================================================================
1217
+ import { recipesReportFnCommand, recipesReportFpCommand } from "./commands/recipes.js";
1218
+ const recipesCmd = program
1219
+ .command("recipes")
1220
+ .description("Customer-side recipe reporting (false-negatives, false-positives)");
1221
+ recipesCmd
1222
+ .command("report-fn <recipe-id>")
1223
+ .description("File a false-negative report against an existing recipe (something got through that the recipe should have caught)")
1224
+ .option("--summary <text>", "Human-readable description of what happened (required; pipe via stdin alternatively)")
1225
+ .option("--evidence <text>", "Optional raw payload / log excerpt")
1226
+ .option("--agent <id>", "Optional. Your agent id when the report concerns a specific agent")
1227
+ .option("--checkpoint <id>", "Optional. Related integrity_checkpoints id (helps the reviewer correlate)")
1228
+ .option("--json", "Output JSON")
1229
+ .action(async (recipeId, options) => {
1230
+ try {
1231
+ await recipesReportFnCommand(recipeId, options);
1232
+ }
1233
+ catch (error) {
1234
+ console.error("Error:", error instanceof Error ? error.message : error);
1235
+ process.exit(1);
1236
+ }
1237
+ });
1238
+ recipesCmd
1239
+ .command("report-fp <recipe-id>")
1240
+ .description("File a false-positive report against an existing recipe (the recipe fired on legitimate behaviour)")
1241
+ .option("--summary <text>", "Human-readable description of what happened (required; pipe via stdin alternatively)")
1242
+ .option("--evidence <text>", "Optional raw payload / log excerpt")
1243
+ .option("--agent <id>", "Optional. Your agent id when the report concerns a specific agent")
1244
+ .option("--checkpoint <id>", "Optional. Related integrity_checkpoints id (helps the reviewer correlate)")
1245
+ .option("--json", "Output JSON")
1246
+ .action(async (recipeId, options) => {
1247
+ try {
1248
+ await recipesReportFpCommand(recipeId, options);
1249
+ }
1250
+ catch (error) {
1251
+ console.error("Error:", error instanceof Error ? error.message : error);
1252
+ process.exit(1);
1253
+ }
1254
+ });
1255
+ // ============================================================================
1256
+ // verify-card — cards-as-primitive Phase 5 D1
1257
+ // ============================================================================
1258
+ //
1259
+ // Offline verifier for AAP attestation tokens + transparency-log Merkle
1260
+ // inclusion proofs. Validates that a Mnemom-published canonical card was
1261
+ // in fact composed by Mnemom at the claimed (content_hash, version,
1262
+ // composed_at) point in time. JWKS is cached at ~/.mnemom/cache/aap-jwks.json
1263
+ // for one hour; --strict bypasses the cache.
1264
+ program
1265
+ .command("verify-card")
1266
+ .description("Verify a Mnemom-published canonical card's AAP attestation + Merkle inclusion proof offline.")
1267
+ .argument("<agent_id>", "Mnemom agent id (e.g., smolt-e2ca60ef)")
1268
+ .option("--at <iso>", "Verify the historic posture at this ISO-8601 timestamp (defaults to current live A2A export).")
1269
+ .option("--card-kind <kind>", "alignment | protection (defaults to alignment)", "alignment")
1270
+ .option("--api <url>", "Mnemom API base (defaults to https://api.mnemom.ai)", "https://api.mnemom.ai")
1271
+ .option("--strict", "Bypass JWKS cache; exit non-zero on any verification gap.")
1272
+ .option("--jwks-cache <path>", "Override the JWKS cache path (default ~/.mnemom/cache/aap-jwks.json)")
1273
+ .option("--no-cache", "Skip the JWKS cache for this invocation.")
1274
+ .action(async (agentId, options) => {
1275
+ try {
1276
+ const { verifyCardCommand } = await import("./commands/verify-card.js");
1277
+ const kind = options.cardKind === "protection" ? "protection" : "alignment";
1278
+ const rc = await verifyCardCommand({
1279
+ agentId,
1280
+ at: options.at,
1281
+ cardKind: kind,
1282
+ api: options.api,
1283
+ strict: options.strict,
1284
+ jwksCache: options.jwksCache,
1285
+ noCache: options.cache === false,
1286
+ });
1287
+ process.exit(rc);
1288
+ }
1289
+ catch (error) {
1290
+ console.error("Error:", error instanceof Error ? error.message : error);
1291
+ process.exit(1);
1292
+ }
1293
+ });
1038
1294
  program.parse();
package/dist/lib/api.d.ts CHANGED
@@ -18,7 +18,7 @@ export interface Agent {
18
18
  created_at: string;
19
19
  }
20
20
  /**
21
- * Per docs.mnemom.ai/api-reference/openapi.json#components/schemas/IntegrityScore:
21
+ * Per api.mnemom.ai/openapi.json#components/schemas/IntegrityScore:
22
22
  *
23
23
  * { agent_id, total_traces, verified_traces, violation_count, integrity_score }
24
24
  *
@@ -344,7 +344,7 @@ export declare function testPolicyHistorical(agentId: string, policyJson: Record
344
344
  agent_id: string;
345
345
  policy_name: string;
346
346
  total_traces: number;
347
- results: any[];
347
+ results: unknown[];
348
348
  summary: {
349
349
  pass: number;
350
350
  warn: number;
@@ -828,4 +828,18 @@ export declare function rotateApiKey(keyId: string): Promise<ApiKeyCreated>;
828
828
  * audit; `is_active` flips to false and `revoked_at` is timestamped.
829
829
  */
830
830
  export declare function revokeApiKey(keyId: string): Promise<void>;
831
+ export interface RecipeReportInput {
832
+ type: "fn" | "fp";
833
+ summary: string;
834
+ evidence?: string;
835
+ agent_id?: string;
836
+ checkpoint_id?: string;
837
+ }
838
+ export interface RecipeReportResult {
839
+ ok: true;
840
+ candidate_id: string;
841
+ type: "fn" | "fp";
842
+ related_recipe_id: string;
843
+ }
844
+ export declare function reportRecipeFnFp(recipeId: string, input: RecipeReportInput): Promise<RecipeReportResult>;
831
845
  export {};
package/dist/lib/api.js CHANGED
@@ -1260,11 +1260,7 @@ export const API_KEY_SCOPES = [
1260
1260
  "admin:org",
1261
1261
  "admin:platform",
1262
1262
  ];
1263
- export const DEFAULT_API_KEY_SCOPES = [
1264
- "gateway",
1265
- "api:read",
1266
- "api:write",
1267
- ];
1263
+ export const DEFAULT_API_KEY_SCOPES = ["gateway", "api:read", "api:write"];
1268
1264
  /**
1269
1265
  * Recognize legacy two-scope sets so the CLI can annotate pre-ADR-049
1270
1266
  * keys appropriately. Mirror of mnemom-api `expandLegacyScopes` logic
@@ -1370,3 +1366,6 @@ export async function revokeApiKey(keyId) {
1370
1366
  throw new Error(`Failed to revoke api key: ${response.status} ${body}`);
1371
1367
  }
1372
1368
  }
1369
+ export async function reportRecipeFnFp(recipeId, input) {
1370
+ return postApi(`/v1/recipes/${encodeURIComponent(recipeId)}/report`, input);
1371
+ }