@mnemom/mnemom 0.10.0 → 0.12.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 (40) hide show
  1. package/README.md +25 -25
  2. package/dist/commands/agents.js +2 -7
  3. package/dist/commands/api-key.d.ts +37 -0
  4. package/dist/commands/api-key.js +179 -0
  5. package/dist/commands/auth.js +1 -2
  6. package/dist/commands/card.js +33 -22
  7. package/dist/commands/governance.d.ts +85 -0
  8. package/dist/commands/governance.js +329 -0
  9. package/dist/commands/integrity.js +1 -2
  10. package/dist/commands/license.js +13 -4
  11. package/dist/commands/listen.d.ts +29 -0
  12. package/dist/commands/listen.js +201 -0
  13. package/dist/commands/logs.js +1 -3
  14. package/dist/commands/org.js +4 -6
  15. package/dist/commands/protection.js +70 -23
  16. package/dist/commands/recipes.d.ts +34 -0
  17. package/dist/commands/recipes.js +80 -0
  18. package/dist/commands/status.js +6 -169
  19. package/dist/commands/team.js +5 -10
  20. package/dist/commands/validate.js +7 -4
  21. package/dist/commands/webhooks.d.ts +61 -0
  22. package/dist/commands/webhooks.js +328 -0
  23. package/dist/index.js +489 -1
  24. package/dist/lib/api.d.ts +199 -2
  25. package/dist/lib/api.js +207 -0
  26. package/dist/lib/auth.js +1 -5
  27. package/dist/lib/format.d.ts +4 -0
  28. package/dist/lib/format.js +6 -0
  29. package/dist/lib/listen-stream.d.ts +40 -0
  30. package/dist/lib/listen-stream.js +101 -0
  31. package/dist/lib/webhooks-api.d.ts +80 -0
  32. package/dist/lib/webhooks-api.js +172 -0
  33. package/dist/smoltbot-shim.js +3 -3
  34. package/package.json +1 -1
  35. package/dist/lib/model-cache.d.ts +0 -16
  36. package/dist/lib/model-cache.js +0 -137
  37. package/dist/lib/models.d.ts +0 -41
  38. package/dist/lib/models.js +0 -357
  39. package/dist/lib/openclaw.d.ts +0 -221
  40. package/dist/lib/openclaw.js +0 -474
@@ -0,0 +1,329 @@
1
+ /**
2
+ * `mnemom governance` — operator CLI for governance signals (ADR-048).
3
+ *
4
+ * Subcommands:
5
+ *
6
+ * signals list | show | ack | resolve | dismiss
7
+ * destinations list | add | remove | test
8
+ * rules list | add | remove
9
+ *
10
+ * The signal surface is operator-actionable: cron-driven sideband
11
+ * detections, future protection / posture observations. Distinct from
12
+ * `mnemom advisories list` (now narrowed to runtime.* / manual.* per
13
+ * ADR-048 §1).
14
+ */
15
+ import chalk from "chalk";
16
+ import { acknowledgeGovernanceSignal, createGovernanceDestination, createGovernanceRule, deleteGovernanceDestination, deleteGovernanceRule, dismissGovernanceSignal, getGovernanceSignal, listGovernanceDestinations, listGovernanceRules, listGovernanceSignalsForAgent, listGovernanceSignalsForOrg, listGovernanceSignalsForTeam, resolveGovernanceSignal, testGovernanceDestination, } from "../lib/api.js";
17
+ import { requireAuth } from "../lib/auth.js";
18
+ // ─── Formatting helpers ──────────────────────────────────────────────────
19
+ function shortId(id) {
20
+ return id.length <= 16 ? id : id.slice(0, 13) + "...";
21
+ }
22
+ function severityColor(s) {
23
+ switch (s) {
24
+ case "critical":
25
+ return chalk.bgRed.white;
26
+ case "high":
27
+ return chalk.red;
28
+ case "warn":
29
+ return chalk.yellow;
30
+ default:
31
+ return chalk.cyan;
32
+ }
33
+ }
34
+ function statusBadge(s) {
35
+ switch (s) {
36
+ case "open":
37
+ return chalk.bold.red("OPEN");
38
+ case "acknowledged":
39
+ return chalk.yellow("ACK");
40
+ case "resolved":
41
+ return chalk.green("RESOLVED");
42
+ case "dismissed":
43
+ return chalk.dim("DISMISSED");
44
+ case "expired":
45
+ return chalk.dim("expired");
46
+ default:
47
+ return chalk.dim(s.toUpperCase());
48
+ }
49
+ }
50
+ function formatSignalRow(s) {
51
+ const sev = severityColor(s.severity)(s.severity.toUpperCase().padEnd(8));
52
+ return [
53
+ chalk.dim(shortId(s.id).padEnd(16)),
54
+ sev,
55
+ chalk.cyan(s.source.padEnd(22)),
56
+ chalk.magenta((s.pattern_type || "—").slice(0, 28).padEnd(28)),
57
+ statusBadge(s.status).padEnd(20),
58
+ chalk.dim(s.detected_at.replace("T", " ").replace(/\.\d+Z$/, "Z")),
59
+ ].join(" ");
60
+ }
61
+ // ─── signals list ────────────────────────────────────────────────────────
62
+ export async function governanceSignalsListCommand(opts) {
63
+ await requireAuth();
64
+ const filters = {
65
+ source: opts.source,
66
+ severity: opts.severity,
67
+ status: opts.status,
68
+ scope: opts.scope,
69
+ pattern_type: opts.patternType,
70
+ since: opts.since,
71
+ limit: opts.limit ? parseInt(opts.limit, 10) : 100,
72
+ };
73
+ let result;
74
+ if (opts.org) {
75
+ result = await listGovernanceSignalsForOrg(opts.org, filters);
76
+ }
77
+ else if (opts.team) {
78
+ result = await listGovernanceSignalsForTeam(opts.team, filters);
79
+ }
80
+ else if (opts.agent) {
81
+ result = await listGovernanceSignalsForAgent(opts.agent, filters);
82
+ }
83
+ else {
84
+ throw new Error("One of --org, --team, --agent is required.");
85
+ }
86
+ if (opts.json) {
87
+ console.log(JSON.stringify(result, null, 2));
88
+ return;
89
+ }
90
+ const signals = result.signals;
91
+ if (signals.length === 0) {
92
+ console.log(chalk.dim("(no governance signals match)"));
93
+ return;
94
+ }
95
+ console.log([
96
+ chalk.bold("ID".padEnd(16)),
97
+ chalk.bold("SEVERITY".padEnd(8)),
98
+ chalk.bold("SOURCE".padEnd(22)),
99
+ chalk.bold("PATTERN".padEnd(28)),
100
+ chalk.bold("STATUS".padEnd(20)),
101
+ chalk.bold("DETECTED"),
102
+ ].join(" "));
103
+ for (const s of signals)
104
+ console.log(formatSignalRow(s));
105
+ console.log("");
106
+ console.log(chalk.dim(`${signals.length} signal(s).`));
107
+ }
108
+ // ─── signals show ────────────────────────────────────────────────────────
109
+ export async function governanceSignalsShowCommand(signalId, opts) {
110
+ await requireAuth();
111
+ const signal = await getGovernanceSignal(signalId);
112
+ if (opts.json) {
113
+ console.log(JSON.stringify(signal, null, 2));
114
+ return;
115
+ }
116
+ console.log(chalk.bold(`Signal ${signal.id}`));
117
+ console.log(` scope: ${signal.scope} (${signal.scope_id})`);
118
+ console.log(` source: ${chalk.cyan(signal.source)}`);
119
+ console.log(` pattern: ${chalk.magenta(signal.pattern_type)}`);
120
+ console.log(` severity: ${severityColor(signal.severity)(signal.severity)}`);
121
+ console.log(` status: ${statusBadge(signal.status)}`);
122
+ console.log(` detected: ${signal.detected_at} by ${signal.detected_by}`);
123
+ console.log(` org: ${signal.org_id}`);
124
+ if (signal.team_id)
125
+ console.log(` team: ${signal.team_id}`);
126
+ console.log(` agents: ${signal.agent_ids.length === 0 ? "(none)" : signal.agent_ids.join(", ")}`);
127
+ if (signal.acknowledged_at) {
128
+ console.log(` ack: ${signal.acknowledged_at} (${signal.acknowledged_actor_role ?? "?"})${signal.acknowledged_by ? ` by ${signal.acknowledged_by}` : ""}`);
129
+ }
130
+ if (signal.resolved_at) {
131
+ console.log(` resolved: ${signal.resolved_at} (${signal.resolution_status ?? "?"})${signal.action_taken ? ` — ${signal.action_taken}` : ""}`);
132
+ }
133
+ if (Object.keys(signal.detail ?? {}).length > 0) {
134
+ console.log(chalk.dim(" detail:"));
135
+ console.log(JSON.stringify(signal.detail, null, 2)
136
+ .split("\n")
137
+ .map((l) => " " + l)
138
+ .join("\n"));
139
+ }
140
+ }
141
+ // ─── signals ack/resolve/dismiss ─────────────────────────────────────────
142
+ export async function governanceSignalsAckCommand(signalId, opts) {
143
+ await requireAuth();
144
+ const signal = await acknowledgeGovernanceSignal(signalId, {
145
+ action_taken: opts.action,
146
+ });
147
+ if (opts.json) {
148
+ console.log(JSON.stringify(signal, null, 2));
149
+ return;
150
+ }
151
+ console.log(chalk.green(`✓ Acknowledged ${signal.id} as ${signal.acknowledged_actor_role}.`));
152
+ }
153
+ export async function governanceSignalsResolveCommand(signalId, opts) {
154
+ await requireAuth();
155
+ const valid = [
156
+ "action_taken",
157
+ "wont_fix",
158
+ "duplicate",
159
+ "false_positive",
160
+ "self_resolved",
161
+ ];
162
+ if (!valid.includes(opts.status)) {
163
+ throw new Error(`--status must be one of: ${valid.join(", ")}`);
164
+ }
165
+ const signal = await resolveGovernanceSignal(signalId, {
166
+ resolution_status: opts.status,
167
+ action_taken: opts.action,
168
+ });
169
+ if (opts.json) {
170
+ console.log(JSON.stringify(signal, null, 2));
171
+ return;
172
+ }
173
+ console.log(chalk.green(`✓ Resolved ${signal.id} (${signal.resolution_status}).`));
174
+ }
175
+ export async function governanceSignalsDismissCommand(signalId, opts) {
176
+ await requireAuth();
177
+ const signal = await dismissGovernanceSignal(signalId, { reason: opts.reason });
178
+ if (opts.json) {
179
+ console.log(JSON.stringify(signal, null, 2));
180
+ return;
181
+ }
182
+ console.log(chalk.dim(`✓ Dismissed ${signal.id}.`));
183
+ }
184
+ // ─── destinations ────────────────────────────────────────────────────────
185
+ function formatDestinationRow(d) {
186
+ return [
187
+ chalk.dim(shortId(d.id).padEnd(16)),
188
+ chalk.cyan(d.channel.padEnd(10)),
189
+ (d.display_name ?? "(unnamed)").slice(0, 32).padEnd(32),
190
+ d.enabled ? chalk.green("enabled") : chalk.dim("disabled"),
191
+ d.last_test_status === "ok"
192
+ ? chalk.green(`tested ✓ ${d.last_tested_at?.slice(0, 19) ?? ""}`)
193
+ : d.last_test_status === "failed"
194
+ ? chalk.red(`tested ✗ ${d.last_test_error?.slice(0, 64) ?? ""}`)
195
+ : chalk.dim("untested"),
196
+ ].join(" ");
197
+ }
198
+ export async function governanceDestinationsListCommand(opts) {
199
+ await requireAuth();
200
+ const result = await listGovernanceDestinations(opts.org);
201
+ if (opts.json) {
202
+ console.log(JSON.stringify(result, null, 2));
203
+ return;
204
+ }
205
+ if (result.destinations.length === 0) {
206
+ console.log(chalk.dim("(no destinations configured for this org)"));
207
+ return;
208
+ }
209
+ for (const d of result.destinations)
210
+ console.log(formatDestinationRow(d));
211
+ }
212
+ export async function governanceDestinationsAddCommand(opts) {
213
+ await requireAuth();
214
+ const validChannels = ["webhook", "slack", "email", "pagerduty"];
215
+ if (!validChannels.includes(opts.channel)) {
216
+ throw new Error(`--channel must be one of: ${validChannels.join(", ")}`);
217
+ }
218
+ let config;
219
+ try {
220
+ config = JSON.parse(opts.config);
221
+ }
222
+ catch {
223
+ throw new Error("--config must be valid JSON");
224
+ }
225
+ let filter;
226
+ if (opts.filter) {
227
+ try {
228
+ filter = JSON.parse(opts.filter);
229
+ }
230
+ catch {
231
+ throw new Error("--filter must be valid JSON");
232
+ }
233
+ }
234
+ const dest = await createGovernanceDestination(opts.org, {
235
+ channel: opts.channel,
236
+ config,
237
+ filter,
238
+ display_name: opts.name,
239
+ });
240
+ if (opts.json) {
241
+ console.log(JSON.stringify(dest, null, 2));
242
+ return;
243
+ }
244
+ console.log(chalk.green(`✓ Created destination ${dest.id} (${dest.channel}).`));
245
+ }
246
+ export async function governanceDestinationsRemoveCommand(destinationId, opts) {
247
+ await requireAuth();
248
+ await deleteGovernanceDestination(opts.org, destinationId);
249
+ if (opts.json) {
250
+ console.log(JSON.stringify({ ok: true, removed: destinationId }, null, 2));
251
+ return;
252
+ }
253
+ console.log(chalk.green(`✓ Removed destination ${destinationId}.`));
254
+ }
255
+ export async function governanceDestinationsTestCommand(destinationId, opts) {
256
+ await requireAuth();
257
+ const result = await testGovernanceDestination(opts.org, destinationId);
258
+ if (opts.json) {
259
+ console.log(JSON.stringify(result, null, 2));
260
+ return;
261
+ }
262
+ if (result.result.ok) {
263
+ console.log(chalk.green(`✓ Test signal delivered via ${result.channel} (${result.result.attempts} attempt).`));
264
+ }
265
+ else {
266
+ console.log(chalk.red(`✗ Test failed via ${result.channel}: ${result.result.last_error ?? "unknown"}`));
267
+ process.exitCode = 1;
268
+ }
269
+ }
270
+ // ─── rules ───────────────────────────────────────────────────────────────
271
+ function formatRuleRow(r) {
272
+ return [
273
+ chalk.dim(shortId(r.id).padEnd(16)),
274
+ r.name.slice(0, 32).padEnd(32),
275
+ r.enabled ? chalk.green("enabled") : chalk.dim("disabled"),
276
+ chalk.dim(`fired ${r.fire_count}×`),
277
+ r.last_fired_at ? chalk.dim(`last ${r.last_fired_at.slice(0, 19)}`) : "",
278
+ ].join(" ");
279
+ }
280
+ export async function governanceRulesListCommand(opts) {
281
+ await requireAuth();
282
+ const result = await listGovernanceRules(opts.org);
283
+ if (opts.json) {
284
+ console.log(JSON.stringify(result, null, 2));
285
+ return;
286
+ }
287
+ if (result.rules.length === 0) {
288
+ console.log(chalk.dim("(no escalation rules configured for this org)"));
289
+ return;
290
+ }
291
+ for (const r of result.rules)
292
+ console.log(formatRuleRow(r));
293
+ }
294
+ export async function governanceRulesAddCommand(opts) {
295
+ await requireAuth();
296
+ let predicate;
297
+ try {
298
+ predicate = JSON.parse(opts.predicate);
299
+ }
300
+ catch {
301
+ throw new Error("--predicate must be valid JSON");
302
+ }
303
+ const destinationIds = opts.destinations
304
+ .split(",")
305
+ .map((s) => s.trim())
306
+ .filter(Boolean);
307
+ if (destinationIds.length === 0) {
308
+ throw new Error("--destinations must list at least one destination ID (comma-separated)");
309
+ }
310
+ const rule = await createGovernanceRule(opts.org, {
311
+ name: opts.name,
312
+ predicate,
313
+ destination_ids: destinationIds,
314
+ });
315
+ if (opts.json) {
316
+ console.log(JSON.stringify(rule, null, 2));
317
+ return;
318
+ }
319
+ console.log(chalk.green(`✓ Created rule ${rule.id} ("${rule.name}").`));
320
+ }
321
+ export async function governanceRulesRemoveCommand(ruleId, opts) {
322
+ await requireAuth();
323
+ await deleteGovernanceRule(opts.org, ruleId);
324
+ if (opts.json) {
325
+ console.log(JSON.stringify({ ok: true, removed: ruleId }, null, 2));
326
+ return;
327
+ }
328
+ console.log(chalk.green(`✓ Removed rule ${ruleId}.`));
329
+ }
@@ -1,5 +1,4 @@
1
- import { resolveAgentId } from "../lib/api.js";
2
- import { getIntegrity } from "../lib/api.js";
1
+ import { resolveAgentId, getIntegrity } from "../lib/api.js";
3
2
  import { fmt } from "../lib/format.js";
4
3
  export async function integrityCommand(agentName) {
5
4
  const agentId = await resolveAgentId(agentName);
@@ -72,10 +72,15 @@ export async function licenseActivateCommand(jwt) {
72
72
  saveLicenseJwt(jwt);
73
73
  // Display info
74
74
  const expiresAt = claims.exp ? new Date(claims.exp * 1000) : null;
75
- const daysRemaining = expiresAt ? Math.floor((expiresAt.getTime() - Date.now()) / 86400000) : "unknown";
75
+ const daysRemaining = expiresAt
76
+ ? Math.floor((expiresAt.getTime() - Date.now()) / 86400000)
77
+ : "unknown";
76
78
  console.log(" License ID: " + (claims.license_id || "unknown"));
77
79
  console.log(" Plan: " + (claims.plan_id || "unknown"));
78
- console.log(" Features: " + Object.keys(claims.feature_flags || {}).filter((k) => claims.feature_flags[k]).join(", ") || "none");
80
+ console.log(" Features: " +
81
+ Object.keys(claims.feature_flags || {})
82
+ .filter((k) => claims.feature_flags[k])
83
+ .join(", ") || "none");
79
84
  console.log(" Expires: " + (expiresAt ? expiresAt.toISOString() : "unknown"));
80
85
  console.log(" Days remaining: " + daysRemaining);
81
86
  console.log(" Max activations: " + (claims.max_activations || "unknown"));
@@ -95,14 +100,18 @@ export async function licenseStatusCommand() {
95
100
  process.exit(1);
96
101
  }
97
102
  const expiresAt = claims.exp ? new Date(claims.exp * 1000) : null;
98
- const daysRemaining = expiresAt ? Math.floor((expiresAt.getTime() - Date.now()) / 86400000) : null;
103
+ const daysRemaining = expiresAt
104
+ ? Math.floor((expiresAt.getTime() - Date.now()) / 86400000)
105
+ : null;
99
106
  const isExpired = daysRemaining !== null && daysRemaining <= 0;
100
107
  console.log(fmt.header("Enterprise License Status"));
101
108
  console.log();
102
109
  console.log(` ${fmt.label("License ID: ", String(claims.license_id || "unknown"))}`);
103
110
  console.log(` ${fmt.label("Account: ", String(claims.account_id || "unknown"))}`);
104
111
  console.log(` ${fmt.label("Plan: ", String(claims.plan_id || "unknown"))}`);
105
- console.log(` ${fmt.label("Features: ", Object.keys(claims.feature_flags || {}).filter((k) => claims.feature_flags[k]).join(", ") || "none")}`);
112
+ console.log(` ${fmt.label("Features: ", Object.keys(claims.feature_flags || {})
113
+ .filter((k) => claims.feature_flags[k])
114
+ .join(", ") || "none")}`);
106
115
  console.log(` ${fmt.label("Expires: ", expiresAt ? expiresAt.toISOString() : "unknown")}`);
107
116
  console.log(` ${fmt.label("Days remaining: ", String(daysRemaining ?? "unknown"))}`);
108
117
  console.log(` ${fmt.label("Status: ", isExpired ? "EXPIRED" : "Active")}`);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * `mnemom listen <org_id>` — Stripe-style live webhook event stream
3
+ * (Track 2 W3.4b).
4
+ *
5
+ * mnemom listen <org_id>
6
+ * [--forward-to <url>] # POST each event to this local URL
7
+ * [--secret <hex>] # local signing secret for re-signed HMAC
8
+ * [--filter <a,b,c>] # only forward/print these event types
9
+ * [--since <event_id>] # resume from this cursor on first connect
10
+ * [--json] # emit raw JSON lines instead of formatted summary
11
+ *
12
+ * Connects to `${API_BASE}/v1/orgs/${orgId}/webhooks/listen` (SSE)
13
+ * and reconnects with `Last-Event-ID` on stream close. Each event is
14
+ * printed (formatted summary or raw JSON) and, if `--forward-to` is
15
+ * set, re-signed with the local secret and POST'd to the receiver
16
+ * URL — letting customers write/test webhook receivers locally
17
+ * against real production events.
18
+ */
19
+ interface ListenOptions {
20
+ forwardTo?: string;
21
+ secret?: string;
22
+ filter?: string;
23
+ since?: string;
24
+ json?: boolean;
25
+ /** Test-mode hook: forces a single iteration (no reconnect loop). */
26
+ __once?: boolean;
27
+ }
28
+ export declare function listenCommand(orgId: string, opts?: ListenOptions): Promise<void>;
29
+ export {};
@@ -0,0 +1,201 @@
1
+ /**
2
+ * `mnemom listen <org_id>` — Stripe-style live webhook event stream
3
+ * (Track 2 W3.4b).
4
+ *
5
+ * mnemom listen <org_id>
6
+ * [--forward-to <url>] # POST each event to this local URL
7
+ * [--secret <hex>] # local signing secret for re-signed HMAC
8
+ * [--filter <a,b,c>] # only forward/print these event types
9
+ * [--since <event_id>] # resume from this cursor on first connect
10
+ * [--json] # emit raw JSON lines instead of formatted summary
11
+ *
12
+ * Connects to `${API_BASE}/v1/orgs/${orgId}/webhooks/listen` (SSE)
13
+ * and reconnects with `Last-Event-ID` on stream close. Each event is
14
+ * printed (formatted summary or raw JSON) and, if `--forward-to` is
15
+ * set, re-signed with the local secret and POST'd to the receiver
16
+ * URL — letting customers write/test webhook receivers locally
17
+ * against real production events.
18
+ */
19
+ import { getApiUrl } from "../lib/config.js";
20
+ import { resolveAuth, forceRefreshAccessToken, requireAuth } from "../lib/auth.js";
21
+ import { fmt } from "../lib/format.js";
22
+ import { parseSseStream, reSignDelivery } from "../lib/listen-stream.js";
23
+ const API_BASE = getApiUrl();
24
+ async function authHeaders() {
25
+ const cred = await resolveAuth();
26
+ switch (cred.type) {
27
+ case "jwt":
28
+ return { Authorization: `Bearer ${cred.token}` };
29
+ case "api-key":
30
+ return { "X-Mnemom-Api-Key": cred.key };
31
+ case "none":
32
+ return {};
33
+ }
34
+ }
35
+ function parseFilter(value) {
36
+ if (!value)
37
+ return null;
38
+ const set = new Set(value
39
+ .split(",")
40
+ .map((s) => s.trim())
41
+ .filter(Boolean));
42
+ return set.size > 0 ? set : null;
43
+ }
44
+ async function forwardEvent(url, rawBody, signingSecret, eventId) {
45
+ const start = Date.now();
46
+ const { timestamp, signature } = reSignDelivery(rawBody, signingSecret);
47
+ try {
48
+ const res = await fetch(url, {
49
+ method: "POST",
50
+ headers: {
51
+ "Content-Type": "application/json",
52
+ "X-Webhook-Id": eventId,
53
+ "X-Webhook-Timestamp": timestamp,
54
+ "X-Webhook-Signature": signature,
55
+ },
56
+ body: rawBody,
57
+ });
58
+ return { status: res.status, latencyMs: Date.now() - start };
59
+ }
60
+ catch (err) {
61
+ return {
62
+ status: null,
63
+ error: err instanceof Error ? err.message : String(err),
64
+ latencyMs: Date.now() - start,
65
+ };
66
+ }
67
+ }
68
+ function summarizeEvent(eventId, eventType, fwd) {
69
+ const ts = new Date().toISOString().slice(11, 19);
70
+ if (!fwd)
71
+ return `[${ts}] ${eventType.padEnd(30)} ${eventId}`;
72
+ const statusStr = fwd.status !== null ? String(fwd.status) : "ERR";
73
+ const okSign = fwd.status !== null && fwd.status >= 200 && fwd.status < 300 ? "✓" : "✗";
74
+ return (`[${ts}] ${eventType.padEnd(30)} ${eventId.padEnd(24)} → ${okSign} ${statusStr} (${fwd.latencyMs}ms)` +
75
+ (fwd.error ? ` ${fwd.error}` : ""));
76
+ }
77
+ /**
78
+ * One connect → consume cycle. Returns the cursor after the cycle ends
79
+ * (either from the `close` event or from the last received event_id),
80
+ * so callers can reconnect with it.
81
+ */
82
+ async function consumeOnce(orgId, cursor, opts, filter, log) {
83
+ const url = new URL(`${API_BASE}/v1/orgs/${orgId}/webhooks/listen`);
84
+ if (cursor)
85
+ url.searchParams.set("since", cursor);
86
+ const headers = {
87
+ ...(await authHeaders()),
88
+ Accept: "text/event-stream",
89
+ };
90
+ if (cursor)
91
+ headers["Last-Event-ID"] = cursor;
92
+ let res = await fetch(url.toString(), { headers });
93
+ if (res.status === 401) {
94
+ const refreshed = await forceRefreshAccessToken();
95
+ if (refreshed) {
96
+ const retryHeaders = {
97
+ ...(await authHeaders()),
98
+ Accept: "text/event-stream",
99
+ ...(cursor ? { "Last-Event-ID": cursor } : {}),
100
+ };
101
+ res = await fetch(url.toString(), { headers: retryHeaders });
102
+ }
103
+ }
104
+ if (!res.ok) {
105
+ const msg = await res.text().catch(() => "");
106
+ throw new Error(`${res.status} ${msg.slice(0, 200)}`);
107
+ }
108
+ if (!res.body)
109
+ throw new Error("response body missing");
110
+ let latestCursor = cursor;
111
+ for await (const frame of parseSseStream(res.body)) {
112
+ if (frame.event === "ready") {
113
+ // Connection healthy; nothing to print.
114
+ continue;
115
+ }
116
+ if (frame.event === "close") {
117
+ // Server-initiated graceful close; cursor in the data.
118
+ try {
119
+ const parsed = JSON.parse(frame.data);
120
+ if (parsed.cursor)
121
+ latestCursor = parsed.cursor;
122
+ }
123
+ catch {
124
+ /* fall through */
125
+ }
126
+ return latestCursor;
127
+ }
128
+ if (frame.event === "error") {
129
+ log(fmt.error(`server error frame: ${frame.data}`));
130
+ continue;
131
+ }
132
+ if (frame.event !== "webhook.event")
133
+ continue;
134
+ let payload;
135
+ try {
136
+ payload = JSON.parse(frame.data);
137
+ }
138
+ catch {
139
+ log(fmt.warn(`skipped unparseable data: ${frame.data.slice(0, 80)}`));
140
+ continue;
141
+ }
142
+ if (filter && payload.event_type && !filter.has(payload.event_type))
143
+ continue;
144
+ if (frame.id)
145
+ latestCursor = frame.id;
146
+ if (opts.json) {
147
+ log(frame.data);
148
+ }
149
+ let fwdResult = null;
150
+ if (opts.forwardTo) {
151
+ if (!opts.secret) {
152
+ log(fmt.error("--forward-to requires --secret to re-sign the local delivery"));
153
+ }
154
+ else {
155
+ fwdResult = await forwardEvent(opts.forwardTo, frame.data, opts.secret, payload.event_id ?? "unknown");
156
+ }
157
+ }
158
+ if (!opts.json) {
159
+ log(summarizeEvent(payload.event_id ?? "unknown", payload.event_type ?? "?", fwdResult));
160
+ }
161
+ }
162
+ return latestCursor;
163
+ }
164
+ const RECONNECT_BACKOFF_MS = [1000, 2000, 5000, 10000];
165
+ export async function listenCommand(orgId, opts = {}) {
166
+ await requireAuth();
167
+ if (!orgId) {
168
+ console.log(fmt.error("Usage: mnemom listen <org_id>") + "\n");
169
+ process.exit(1);
170
+ }
171
+ const filter = parseFilter(opts.filter);
172
+ let cursor = opts.since ?? null;
173
+ if (!opts.json) {
174
+ const filterStr = filter ? ` (filter: ${[...filter].join(", ")})` : "";
175
+ const forwardStr = opts.forwardTo ? `\n Forward → ${opts.forwardTo}` : "";
176
+ console.log(fmt.header(`Listening for webhook events — org ${orgId}${filterStr}`) +
177
+ forwardStr +
178
+ "\n Press Ctrl-C to stop." +
179
+ "\n");
180
+ }
181
+ let consecutiveFailures = 0;
182
+ while (true) {
183
+ try {
184
+ const next = await consumeOnce(orgId, cursor, opts, filter, (msg) => console.log(msg));
185
+ cursor = next;
186
+ consecutiveFailures = 0;
187
+ if (opts.__once)
188
+ return;
189
+ }
190
+ catch (err) {
191
+ const msg = err instanceof Error ? err.message : String(err);
192
+ console.log(fmt.warn(`Stream closed: ${msg}`));
193
+ if (opts.__once)
194
+ return;
195
+ const delay = RECONNECT_BACKOFF_MS[Math.min(consecutiveFailures, RECONNECT_BACKOFF_MS.length - 1)];
196
+ console.log(fmt.warn(`Reconnecting in ${delay}ms…`));
197
+ await new Promise((r) => setTimeout(r, delay));
198
+ consecutiveFailures += 1;
199
+ }
200
+ }
201
+ }
@@ -36,9 +36,7 @@ export async function logsCommand(options = {}) {
36
36
  }
37
37
  function displayTrace(trace) {
38
38
  const timestamp = formatTimestamp(trace.timestamp);
39
- const statusMsg = trace.verified
40
- ? fmt.success(timestamp)
41
- : fmt.error(`${timestamp} [VIOLATION]`);
39
+ const statusMsg = trace.verified ? fmt.success(timestamp) : fmt.error(`${timestamp} [VIOLATION]`);
42
40
  console.log(`\n ${statusMsg}`);
43
41
  console.log(` ${fmt.label("Action:", ` ${trace.action}`)}`);
44
42
  if (trace.tool_name) {
@@ -34,10 +34,7 @@ export async function orgListCommand(opts) {
34
34
  const idW = 24;
35
35
  const roleW = 10;
36
36
  const ownerW = 8;
37
- const header = "Name".padEnd(nameW) +
38
- "ID".padEnd(idW) +
39
- "Role".padEnd(roleW) +
40
- "Owner".padEnd(ownerW);
37
+ const header = "Name".padEnd(nameW) + "ID".padEnd(idW) + "Role".padEnd(roleW) + "Owner".padEnd(ownerW);
41
38
  console.log(` ${header}`);
42
39
  console.log(` ${"─".repeat(nameW + idW + roleW + ownerW)}`);
43
40
  for (const org of orgs) {
@@ -60,7 +57,7 @@ export async function orgListCommand(opts) {
60
57
  */
61
58
  export async function orgShowCommand(orgIdArg, opts) {
62
59
  await requireAuth();
63
- let target = null;
60
+ let target;
64
61
  try {
65
62
  if (opts.personal) {
66
63
  const ref = await getMyPersonalOrg();
@@ -81,7 +78,8 @@ export async function orgShowCommand(orgIdArg, opts) {
81
78
  target = orgs[0];
82
79
  }
83
80
  else {
84
- console.log(fmt.warn(`You have ${orgs.length} memberships. Specify an org_id or pass --personal.`) + "\n");
81
+ console.log(fmt.warn(`You have ${orgs.length} memberships. Specify an org_id or pass --personal.`) +
82
+ "\n");
85
83
  process.exit(1);
86
84
  return;
87
85
  }