@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 +25 -25
- package/dist/commands/agents.js +2 -7
- package/dist/commands/api-key.js +1 -3
- package/dist/commands/auth.js +1 -2
- package/dist/commands/card.js +33 -22
- package/dist/commands/governance.js +5 -7
- package/dist/commands/integrity.js +1 -2
- package/dist/commands/license.js +13 -4
- package/dist/commands/listen.d.ts +29 -0
- package/dist/commands/listen.js +201 -0
- package/dist/commands/logs.js +1 -3
- package/dist/commands/org.js +4 -6
- package/dist/commands/protection.js +70 -23
- package/dist/commands/recipes.d.ts +34 -0
- package/dist/commands/recipes.js +80 -0
- package/dist/commands/status.js +6 -169
- package/dist/commands/team.js +5 -10
- package/dist/commands/validate.js +7 -4
- package/dist/commands/webhooks.d.ts +61 -0
- package/dist/commands/webhooks.js +328 -0
- package/dist/index.js +220 -3
- package/dist/lib/api.d.ts +16 -2
- package/dist/lib/api.js +4 -5
- package/dist/lib/auth.js +1 -5
- package/dist/lib/listen-stream.d.ts +40 -0
- package/dist/lib/listen-stream.js +101 -0
- package/dist/lib/webhooks-api.d.ts +80 -0
- package/dist/lib/webhooks-api.js +172 -0
- package/dist/smoltbot-shim.js +3 -3
- package/package.json +1 -1
- package/dist/lib/model-cache.d.ts +0 -16
- package/dist/lib/model-cache.js +0 -137
- package/dist/lib/models.d.ts +0 -41
- package/dist/lib/models.js +0 -357
- package/dist/lib/openclaw.d.ts +0 -221
- package/dist/lib/openclaw.js +0 -474
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
|
|
30
|
-
|
|
31
|
-
| Anthropic | Claude Opus 4.6, Opus 4.5, Sonnet 4.5 | Full (thinking blocks)
|
|
32
|
-
| OpenAI
|
|
33
|
-
| Gemini
|
|
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
|
|
38
|
-
|
|
39
|
-
| `mnemom init`
|
|
40
|
-
| `mnemom status`
|
|
41
|
-
| `mnemom integrity`
|
|
42
|
-
| `mnemom logs [-l N]`
|
|
43
|
-
| `mnemom card show`
|
|
44
|
-
| `mnemom card publish <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
|
|
88
|
-
|
|
89
|
-
| Anthropic reasoning models (Opus, Sonnet) | Full
|
|
90
|
-
| OpenAI GPT-5 Thinking series
|
|
91
|
-
| Gemini 2.5/3 with thinking
|
|
92
|
-
| Non-reasoning models
|
|
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
|
|
97
|
-
|
|
98
|
-
| `observe` | Detect violations, record them, take no action (default)
|
|
99
|
-
| `nudge`
|
|
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
|
|
package/dist/commands/agents.js
CHANGED
|
@@ -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}`);
|
package/dist/commands/api-key.js
CHANGED
|
@@ -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";
|
package/dist/commands/auth.js
CHANGED
|
@@ -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 = {}) {
|
package/dist/commands/card.js
CHANGED
|
@@ -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
|
|
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 &&
|
|
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) ||
|
|
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 &&
|
|
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 ||
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
|
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);
|
package/dist/commands/license.js
CHANGED
|
@@ -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
|
|
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: " +
|
|
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
|
|
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 || {})
|
|
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
|
+
}
|
package/dist/commands/logs.js
CHANGED
|
@@ -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) {
|
package/dist/commands/org.js
CHANGED
|
@@ -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
|
|
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.`) +
|
|
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
|
}
|