@bububuger/spanory 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/alert/evaluate.d.ts +3 -0
- package/dist/alert/evaluate.js +197 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.js +54 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1688 -0
- package/dist/otlp.d.ts +5 -0
- package/dist/otlp.js +12 -0
- package/dist/report/aggregate.d.ts +21 -0
- package/dist/report/aggregate.js +245 -0
- package/dist/runtime/claude/adapter.d.ts +18 -0
- package/dist/runtime/claude/adapter.js +73 -0
- package/dist/runtime/codex/adapter.d.ts +18 -0
- package/dist/runtime/codex/adapter.js +457 -0
- package/dist/runtime/codex/proxy.d.ts +9 -0
- package/dist/runtime/codex/proxy.js +212 -0
- package/dist/runtime/openclaw/adapter.d.ts +18 -0
- package/dist/runtime/openclaw/adapter.js +242 -0
- package/dist/runtime/shared/capabilities.d.ts +30 -0
- package/dist/runtime/shared/capabilities.js +39 -0
- package/dist/runtime/shared/normalize.d.ts +9 -0
- package/dist/runtime/shared/normalize.js +570 -0
- package/package.json +41 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { summarizeAgents, summarizeCache, summarizeCommands, summarizeMcp, summarizeSessions, summarizeTurnDiff, } from '../report/aggregate.js';
|
|
4
|
+
function getMetricFromSessionRow(row, metric, refs = {}) {
|
|
5
|
+
const cacheRow = refs.cacheBySessionId?.get(row.sessionId);
|
|
6
|
+
const agentRow = refs.agentBySessionId?.get(row.sessionId);
|
|
7
|
+
const turnDiffRows = refs.turnDiffBySessionId?.get(row.sessionId) ?? [];
|
|
8
|
+
switch (metric) {
|
|
9
|
+
case 'events':
|
|
10
|
+
return row.events ?? 0;
|
|
11
|
+
case 'turns':
|
|
12
|
+
return row.turns ?? 0;
|
|
13
|
+
case 'usage.total':
|
|
14
|
+
return row.usage?.total ?? 0;
|
|
15
|
+
case 'usage.input':
|
|
16
|
+
return row.usage?.input ?? 0;
|
|
17
|
+
case 'usage.output':
|
|
18
|
+
return row.usage?.output ?? 0;
|
|
19
|
+
case 'cache.read':
|
|
20
|
+
return cacheRow?.cacheReadInputTokens ?? 0;
|
|
21
|
+
case 'cache.creation':
|
|
22
|
+
return cacheRow?.cacheCreationInputTokens ?? 0;
|
|
23
|
+
case 'cache.hit_rate':
|
|
24
|
+
return cacheRow?.cacheHitRate ?? 0;
|
|
25
|
+
case 'subagent.calls':
|
|
26
|
+
return agentRow?.agentTasks ?? 0;
|
|
27
|
+
case 'diff.char_delta.max':
|
|
28
|
+
return turnDiffRows.reduce((max, rowItem) => Math.max(max, Math.abs(Number(rowItem.charDelta ?? 0))), 0);
|
|
29
|
+
default:
|
|
30
|
+
return 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function getMetricFromAgentRow(row, metric) {
|
|
34
|
+
switch (metric) {
|
|
35
|
+
case 'agentTasks':
|
|
36
|
+
return row.agentTasks ?? 0;
|
|
37
|
+
case 'shellCommands':
|
|
38
|
+
return row.shellCommands ?? 0;
|
|
39
|
+
case 'mcpCalls':
|
|
40
|
+
return row.mcpCalls ?? 0;
|
|
41
|
+
case 'usage.total':
|
|
42
|
+
return row.usage?.total ?? 0;
|
|
43
|
+
default:
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function compare(value, op, threshold) {
|
|
48
|
+
if (op === 'gt')
|
|
49
|
+
return value > threshold;
|
|
50
|
+
if (op === 'gte')
|
|
51
|
+
return value >= threshold;
|
|
52
|
+
if (op === 'lt')
|
|
53
|
+
return value < threshold;
|
|
54
|
+
if (op === 'lte')
|
|
55
|
+
return value <= threshold;
|
|
56
|
+
if (op === 'eq')
|
|
57
|
+
return value === threshold;
|
|
58
|
+
throw new Error(`unsupported operator: ${op}`);
|
|
59
|
+
}
|
|
60
|
+
function evaluateSessionRule(rule, sessions) {
|
|
61
|
+
const rows = summarizeSessions(sessions);
|
|
62
|
+
const cacheBySessionId = new Map(summarizeCache(sessions).map((row) => [row.sessionId, row]));
|
|
63
|
+
const agentBySessionId = new Map(summarizeAgents(sessions).map((row) => [row.sessionId, row]));
|
|
64
|
+
const turnDiffBySessionId = new Map();
|
|
65
|
+
for (const row of summarizeTurnDiff(sessions)) {
|
|
66
|
+
const key = row.sessionId;
|
|
67
|
+
const list = turnDiffBySessionId.get(key) ?? [];
|
|
68
|
+
list.push(row);
|
|
69
|
+
turnDiffBySessionId.set(key, list);
|
|
70
|
+
}
|
|
71
|
+
const matched = rows
|
|
72
|
+
.map((row) => {
|
|
73
|
+
const value = getMetricFromSessionRow(row, rule.metric, {
|
|
74
|
+
cacheBySessionId,
|
|
75
|
+
agentBySessionId,
|
|
76
|
+
turnDiffBySessionId,
|
|
77
|
+
});
|
|
78
|
+
return { row, value };
|
|
79
|
+
})
|
|
80
|
+
.filter((x) => compare(x.value, rule.op, Number(rule.threshold)));
|
|
81
|
+
return matched.map((m) => ({
|
|
82
|
+
ruleId: rule.id,
|
|
83
|
+
severity: rule.severity ?? 'warning',
|
|
84
|
+
scope: 'session',
|
|
85
|
+
metric: rule.metric,
|
|
86
|
+
op: rule.op,
|
|
87
|
+
threshold: Number(rule.threshold),
|
|
88
|
+
value: m.value,
|
|
89
|
+
context: {
|
|
90
|
+
sessionId: m.row.sessionId,
|
|
91
|
+
projectId: m.row.projectId,
|
|
92
|
+
runtime: m.row.runtime,
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
function evaluateAgentRule(rule, sessions) {
|
|
97
|
+
const rows = summarizeAgents(sessions);
|
|
98
|
+
const matched = rows
|
|
99
|
+
.map((row) => {
|
|
100
|
+
const value = getMetricFromAgentRow(row, rule.metric);
|
|
101
|
+
return { row, value };
|
|
102
|
+
})
|
|
103
|
+
.filter((x) => compare(x.value, rule.op, Number(rule.threshold)));
|
|
104
|
+
return matched.map((m) => ({
|
|
105
|
+
ruleId: rule.id,
|
|
106
|
+
severity: rule.severity ?? 'warning',
|
|
107
|
+
scope: 'agent',
|
|
108
|
+
metric: rule.metric,
|
|
109
|
+
op: rule.op,
|
|
110
|
+
threshold: Number(rule.threshold),
|
|
111
|
+
value: m.value,
|
|
112
|
+
context: {
|
|
113
|
+
sessionId: m.row.sessionId,
|
|
114
|
+
projectId: m.row.projectId,
|
|
115
|
+
runtime: m.row.runtime,
|
|
116
|
+
},
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
function evaluateMcpRule(rule, sessions) {
|
|
120
|
+
const rows = summarizeMcp(sessions);
|
|
121
|
+
const matched = rows.filter((row) => compare(row.calls ?? 0, rule.op, Number(rule.threshold)));
|
|
122
|
+
return matched.map((row) => ({
|
|
123
|
+
ruleId: rule.id,
|
|
124
|
+
severity: rule.severity ?? 'warning',
|
|
125
|
+
scope: 'mcp',
|
|
126
|
+
metric: 'calls',
|
|
127
|
+
op: rule.op,
|
|
128
|
+
threshold: Number(rule.threshold),
|
|
129
|
+
value: row.calls,
|
|
130
|
+
context: {
|
|
131
|
+
server: row.server,
|
|
132
|
+
},
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
function evaluateCommandRule(rule, sessions) {
|
|
136
|
+
const rows = summarizeCommands(sessions);
|
|
137
|
+
const matched = rows.filter((row) => compare(row.calls ?? 0, rule.op, Number(rule.threshold)));
|
|
138
|
+
return matched.map((row) => ({
|
|
139
|
+
ruleId: rule.id,
|
|
140
|
+
severity: rule.severity ?? 'warning',
|
|
141
|
+
scope: 'command',
|
|
142
|
+
metric: 'calls',
|
|
143
|
+
op: rule.op,
|
|
144
|
+
threshold: Number(rule.threshold),
|
|
145
|
+
value: row.calls,
|
|
146
|
+
context: {
|
|
147
|
+
command: row.command,
|
|
148
|
+
},
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
export async function loadAlertRules(path) {
|
|
152
|
+
const raw = await readFile(path, 'utf-8');
|
|
153
|
+
const parsed = JSON.parse(raw);
|
|
154
|
+
if (!Array.isArray(parsed.rules)) {
|
|
155
|
+
throw new Error('rule file must be JSON with {"rules": [...]}');
|
|
156
|
+
}
|
|
157
|
+
return parsed.rules;
|
|
158
|
+
}
|
|
159
|
+
export function evaluateRules(rules, sessions) {
|
|
160
|
+
const alerts = [];
|
|
161
|
+
for (const rule of rules) {
|
|
162
|
+
if (!rule.id || !rule.scope || !rule.metric || !rule.op) {
|
|
163
|
+
throw new Error(`invalid rule: ${JSON.stringify(rule)}`);
|
|
164
|
+
}
|
|
165
|
+
if (rule.scope === 'session') {
|
|
166
|
+
alerts.push(...evaluateSessionRule(rule, sessions));
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (rule.scope === 'agent') {
|
|
170
|
+
alerts.push(...evaluateAgentRule(rule, sessions));
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (rule.scope === 'mcp') {
|
|
174
|
+
alerts.push(...evaluateMcpRule(rule, sessions));
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (rule.scope === 'command') {
|
|
178
|
+
alerts.push(...evaluateCommandRule(rule, sessions));
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
throw new Error(`unsupported rule scope: ${rule.scope}`);
|
|
182
|
+
}
|
|
183
|
+
return alerts;
|
|
184
|
+
}
|
|
185
|
+
export async function sendAlertWebhook(url, payload, headers = {}) {
|
|
186
|
+
const response = await fetch(url, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: {
|
|
189
|
+
'content-type': 'application/json',
|
|
190
|
+
...headers,
|
|
191
|
+
},
|
|
192
|
+
body: JSON.stringify(payload),
|
|
193
|
+
});
|
|
194
|
+
if (!response.ok) {
|
|
195
|
+
throw new Error(`webhook HTTP ${response.status}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
package/dist/env.d.ts
ADDED
package/dist/env.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
function resolveUserHome() {
|
|
5
|
+
return process.env.HOME || process.env.USERPROFILE || '';
|
|
6
|
+
}
|
|
7
|
+
export function parseSimpleDotEnv(raw) {
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const line of String(raw).split('\n')) {
|
|
10
|
+
let s = line.trim();
|
|
11
|
+
if (!s || s.startsWith('#'))
|
|
12
|
+
continue;
|
|
13
|
+
if (s.startsWith('export ')) {
|
|
14
|
+
s = s.slice('export '.length).trim();
|
|
15
|
+
}
|
|
16
|
+
const idx = s.indexOf('=');
|
|
17
|
+
if (idx <= 0)
|
|
18
|
+
continue;
|
|
19
|
+
const key = s.slice(0, idx).trim();
|
|
20
|
+
if (!key)
|
|
21
|
+
continue;
|
|
22
|
+
let value = s.slice(idx + 1).trim();
|
|
23
|
+
const quoted = (value.startsWith('"') && value.endsWith('"'))
|
|
24
|
+
|| (value.startsWith("'") && value.endsWith("'"));
|
|
25
|
+
if (quoted) {
|
|
26
|
+
value = value.slice(1, -1);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const commentIdx = value.indexOf(' #');
|
|
30
|
+
if (commentIdx >= 0) {
|
|
31
|
+
value = value.slice(0, commentIdx).trimEnd();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
out[key] = value;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
export async function loadUserEnv() {
|
|
39
|
+
const home = resolveUserHome();
|
|
40
|
+
if (!home)
|
|
41
|
+
return;
|
|
42
|
+
const envPath = path.join(home, '.env');
|
|
43
|
+
try {
|
|
44
|
+
const raw = await readFile(envPath, 'utf-8');
|
|
45
|
+
const parsed = parseSimpleDotEnv(raw);
|
|
46
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
47
|
+
if (process.env[k] === undefined)
|
|
48
|
+
process.env[k] = v;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// ignore missing ~/.env
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/index.d.ts
ADDED