@mnemom/mnemom 0.11.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.
package/README.md CHANGED
@@ -26,23 +26,23 @@ That's it. `mnemom init` detects your configured AI provider API keys (Anthropic
26
26
 
27
27
  ## Supported Providers
28
28
 
29
- | Provider | Models | Thinking/AIP | Auth |
30
- |----------|--------|-------------|------|
31
- | Anthropic | Claude Opus 4.6, Opus 4.5, Sonnet 4.5 | Full (thinking blocks) | `x-api-key` |
32
- | OpenAI | GPT-5.2, GPT-5.2 Pro, GPT-5 | Via reasoning summaries | `Authorization: Bearer` |
33
- | Gemini | Gemini 2.5 Pro, Gemini 3 Pro | Full (thought parts) | `x-goog-api-key` |
29
+ | Provider | Models | Thinking/AIP | Auth |
30
+ | --------- | ------------------------------------- | ----------------------- | ----------------------- |
31
+ | Anthropic | Claude Opus 4.6, Opus 4.5, Sonnet 4.5 | Full (thinking blocks) | `x-api-key` |
32
+ | OpenAI | GPT-5.2, GPT-5.2 Pro, GPT-5 | Via reasoning summaries | `Authorization: Bearer` |
33
+ | Gemini | Gemini 2.5 Pro, Gemini 3 Pro | Full (thought parts) | `x-goog-api-key` |
34
34
 
35
35
  ## CLI Commands
36
36
 
37
- | Command | Description |
38
- |---------|-------------|
39
- | `mnemom init` | Configure tracing for your AI agent (multi-provider) |
40
- | `mnemom status` | Show agent status, providers, and connection info |
41
- | `mnemom integrity` | Display integrity score and verification stats |
42
- | `mnemom logs [-l N]` | Show recent traces and actions |
43
- | `mnemom card show` | Display active alignment card |
44
- | `mnemom card publish <file>` | Publish alignment card from JSON file |
45
- | `mnemom card validate <file>` | Validate card JSON locally |
37
+ | Command | Description |
38
+ | ----------------------------- | ---------------------------------------------------- |
39
+ | `mnemom init` | Configure tracing for your AI agent (multi-provider) |
40
+ | `mnemom status` | Show agent status, providers, and connection info |
41
+ | `mnemom integrity` | Display integrity score and verification stats |
42
+ | `mnemom logs [-l N]` | Show recent traces and actions |
43
+ | `mnemom card show` | Display active alignment card |
44
+ | `mnemom card publish <file>` | Publish alignment card from JSON file |
45
+ | `mnemom card validate <file>` | Validate card JSON locally |
46
46
 
47
47
  ## How It Works
48
48
 
@@ -84,20 +84,20 @@ What is **not** stored: your prompts, responses, or API key.
84
84
 
85
85
  ### AIP Compatibility Matrix
86
86
 
87
- | Provider/Model | AIP Support | Method |
88
- |----------------|-------------|--------|
89
- | Anthropic reasoning models (Opus, Sonnet) | Full | Thinking blocks analyzed directly |
90
- | OpenAI GPT-5 Thinking series | Partial | Reasoning summaries (reduced confidence) |
91
- | Gemini 2.5/3 with thinking | Full | Thought parts analyzed directly |
92
- | Non-reasoning models | Tracing only | Synthetic clear verdict |
87
+ | Provider/Model | AIP Support | Method |
88
+ | ----------------------------------------- | ------------ | ---------------------------------------- |
89
+ | Anthropic reasoning models (Opus, Sonnet) | Full | Thinking blocks analyzed directly |
90
+ | OpenAI GPT-5 Thinking series | Partial | Reasoning summaries (reduced confidence) |
91
+ | Gemini 2.5/3 with thinking | Full | Thought parts analyzed directly |
92
+ | Non-reasoning models | Tracing only | Synthetic clear verdict |
93
93
 
94
94
  ## Enforcement Modes
95
95
 
96
- | Mode | Behavior |
97
- |------|----------|
98
- | `observe` | Detect violations, record them, take no action (default) |
99
- | `nudge` | Detect violations, inject feedback into the agent's next request via system prompt — the agent sees it and can self-correct |
100
- | `enforce` | Hard block with 403 for non-streaming; falls back to nudge for streaming |
96
+ | Mode | Behavior |
97
+ | --------- | --------------------------------------------------------------------------------------------------------------------------- |
98
+ | `observe` | Detect violations, record them, take no action (default) |
99
+ | `nudge` | Detect violations, inject feedback into the agent's next request via system prompt — the agent sees it and can self-correct |
100
+ | `enforce` | Hard block with 403 for non-streaming; falls back to nudge for streaming |
101
101
 
102
102
  Enforcement works across all providers where AIP is supported.
103
103
 
@@ -23,18 +23,13 @@ export async function agentsListCommand() {
23
23
  const idW = 40;
24
24
  const seenW = 14;
25
25
  const statusW = 14;
26
- const header = "Name".padEnd(nameW) +
27
- "ID".padEnd(idW) +
28
- "Last Seen".padEnd(seenW) +
29
- "Containment";
26
+ const header = "Name".padEnd(nameW) + "ID".padEnd(idW) + "Last Seen".padEnd(seenW) + "Containment";
30
27
  console.log(` ${header}`);
31
28
  console.log(` ${"─".repeat(nameW + idW + seenW + statusW)}`);
32
29
  for (const agent of agents) {
33
30
  const name = (agent.name ?? "-").slice(0, nameW - 2).padEnd(nameW);
34
31
  const id = agent.id.padEnd(idW);
35
- const lastSeen = agent.last_seen
36
- ? new Date(agent.last_seen).toLocaleDateString()
37
- : "-";
32
+ const lastSeen = agent.last_seen ? new Date(agent.last_seen).toLocaleDateString() : "-";
38
33
  const seen = lastSeen.padEnd(seenW);
39
34
  const containment = agent.containment_status ?? "-";
40
35
  console.log(` ${name}${id}${seen}${containment}`);
@@ -54,9 +54,7 @@ function renderScopeBadge(scope) {
54
54
  return `[${scope}]`;
55
55
  }
56
56
  function formatRow(key) {
57
- const created = key.created_at
58
- ? new Date(key.created_at).toISOString().slice(0, 10)
59
- : "?";
57
+ const created = key.created_at ? new Date(key.created_at).toISOString().slice(0, 10) : "?";
60
58
  const lastUsed = key.last_used_at
61
59
  ? new Date(key.last_used_at).toISOString().slice(0, 10)
62
60
  : "never";
@@ -1,5 +1,4 @@
1
- import { getAuthInfo, clearAuthTokens } from "../lib/auth.js";
2
- import { loginWithBrowser, loginWithPassword } from "../lib/auth.js";
1
+ import { getAuthInfo, clearAuthTokens, loginWithBrowser, loginWithPassword } from "../lib/auth.js";
3
2
  import { fmt } from "../lib/format.js";
4
3
  import { askInput } from "../lib/prompt.js";
5
4
  export async function loginCommand(options = {}) {
@@ -7,7 +7,7 @@ import { ALIGNMENT_CARD_MAX_BYTES, getAlignmentCard, putAlignmentCard, resolveAg
7
7
  import { requireAuth } from "../lib/auth.js";
8
8
  import { fmt } from "../lib/format.js";
9
9
  import { askYesNo, isInteractive } from "../lib/prompt.js";
10
- import { evaluatePolicy, } from "@mnemom/policy-engine";
10
+ import { evaluatePolicy } from "@mnemom/policy-engine";
11
11
  function detectFormat(filePath) {
12
12
  const ext = path.extname(filePath).toLowerCase();
13
13
  if (ext === ".yaml" || ext === ".yml")
@@ -201,7 +201,7 @@ export function validateUnifiedCard(card) {
201
201
  message: "Must contain at least one value.",
202
202
  });
203
203
  }
204
- else if (!decl.every(s => typeof s === "string")) {
204
+ else if (!decl.every((s) => typeof s === "string")) {
205
205
  checks.push({
206
206
  name: "values.declared",
207
207
  passed: false,
@@ -238,7 +238,8 @@ export function validateUnifiedCard(card) {
238
238
  }
239
239
  }
240
240
  // hierarchy enum
241
- if (v.hierarchy !== undefined && !VALUE_HIERARCHIES.includes(String(v.hierarchy))) {
241
+ if (v.hierarchy !== undefined &&
242
+ !VALUE_HIERARCHIES.includes(String(v.hierarchy))) {
242
243
  checks.push({
243
244
  name: "values.hierarchy",
244
245
  passed: false,
@@ -247,7 +248,8 @@ export function validateUnifiedCard(card) {
247
248
  }
248
249
  }
249
250
  // ── autonomy.bounded_actions (required, non-empty; disjoint from forbidden_actions) ──
250
- if (!isObj(card.autonomy) || !Array.isArray(card.autonomy.bounded_actions)) {
251
+ if (!isObj(card.autonomy) ||
252
+ !Array.isArray(card.autonomy.bounded_actions)) {
251
253
  checks.push({
252
254
  name: "autonomy.bounded_actions",
253
255
  passed: false,
@@ -428,7 +430,8 @@ export function validateUnifiedCard(card) {
428
430
  message: "Required (non-empty string).",
429
431
  });
430
432
  }
431
- if (cv.severity !== undefined && !CONSCIENCE_SEVERITIES.includes(String(cv.severity))) {
433
+ if (cv.severity !== undefined &&
434
+ !CONSCIENCE_SEVERITIES.includes(String(cv.severity))) {
432
435
  checks.push({
433
436
  name: `conscience.values[${i}].severity`,
434
437
  passed: false,
@@ -676,20 +679,21 @@ export async function cardEditCommand(agentName, options = {}) {
676
679
  if (!original) {
677
680
  console.log(fmt.warn("No alignment card found. Creating a template..."));
678
681
  }
679
- const cardYaml = original || yaml.dump({
680
- card_version: "unified/2026-04-26",
681
- agent_id: agentId,
682
- autonomy_mode: "observe",
683
- integrity_mode: "observe",
684
- principal: { type: "agent", identifier: agentId, relationship: "delegated_authority" },
685
- values: { declared: ["transparency", "safety", "honesty"] },
686
- autonomy: {
687
- bounded_actions: ["respond_to_prompts"],
688
- forbidden_actions: [],
689
- escalation_triggers: [],
690
- },
691
- audit: { retention_days: 30, queryable: false, trace_format: "otel" },
692
- }, { lineWidth: 120, noRefs: true });
682
+ const cardYaml = original ||
683
+ yaml.dump({
684
+ card_version: "unified/2026-04-26",
685
+ agent_id: agentId,
686
+ autonomy_mode: "observe",
687
+ integrity_mode: "observe",
688
+ principal: { type: "agent", identifier: agentId, relationship: "delegated_authority" },
689
+ values: { declared: ["transparency", "safety", "honesty"] },
690
+ autonomy: {
691
+ bounded_actions: ["respond_to_prompts"],
692
+ forbidden_actions: [],
693
+ escalation_triggers: [],
694
+ },
695
+ audit: { retention_days: 30, queryable: false, trace_format: "otel" },
696
+ }, { lineWidth: 120, noRefs: true });
693
697
  // Write to temp file
694
698
  const tmpDir = os.tmpdir();
695
699
  const tmpFile = path.join(tmpDir, `mnemom-card-${agentId}.yaml`);
@@ -703,7 +707,9 @@ export async function cardEditCommand(agentName, options = {}) {
703
707
  try {
704
708
  fs.unlinkSync(tmpFile);
705
709
  }
706
- catch { /* ignore */ }
710
+ catch {
711
+ /* ignore */
712
+ }
707
713
  process.exit(1);
708
714
  }
709
715
  // Read back and compare
@@ -711,7 +717,9 @@ export async function cardEditCommand(agentName, options = {}) {
711
717
  try {
712
718
  fs.unlinkSync(tmpFile);
713
719
  }
714
- catch { /* ignore */ }
720
+ catch {
721
+ /* ignore */
722
+ }
715
723
  if (edited === cardYaml) {
716
724
  console.log("\nNo changes made.\n");
717
725
  return;
@@ -808,7 +816,10 @@ export async function cardEvaluateCommand(file, options) {
808
816
  // 2. Parse tool list from --tools or --tool-manifest
809
817
  let tools = [];
810
818
  if (options.tools) {
811
- tools = options.tools.split(",").map((t) => ({ name: t.trim() })).filter((t) => t.name);
819
+ tools = options.tools
820
+ .split(",")
821
+ .map((t) => ({ name: t.trim() }))
822
+ .filter((t) => t.name);
812
823
  }
813
824
  else if (options.toolManifest) {
814
825
  const manifestPath = path.resolve(options.toolManifest);
@@ -211,12 +211,7 @@ export async function governanceDestinationsListCommand(opts) {
211
211
  }
212
212
  export async function governanceDestinationsAddCommand(opts) {
213
213
  await requireAuth();
214
- const validChannels = [
215
- "webhook",
216
- "slack",
217
- "email",
218
- "pagerduty",
219
- ];
214
+ const validChannels = ["webhook", "slack", "email", "pagerduty"];
220
215
  if (!validChannels.includes(opts.channel)) {
221
216
  throw new Error(`--channel must be one of: ${validChannels.join(", ")}`);
222
217
  }
@@ -305,7 +300,10 @@ export async function governanceRulesAddCommand(opts) {
305
300
  catch {
306
301
  throw new Error("--predicate must be valid JSON");
307
302
  }
308
- const destinationIds = opts.destinations.split(",").map((s) => s.trim()).filter(Boolean);
303
+ const destinationIds = opts.destinations
304
+ .split(",")
305
+ .map((s) => s.trim())
306
+ .filter(Boolean);
309
307
  if (destinationIds.length === 0) {
310
308
  throw new Error("--destinations must list at least one destination ID (comma-separated)");
311
309
  }
@@ -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
  }