@analytix402/openclaw-skill 0.2.0 → 0.2.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.
- package/SKILL.md +37 -50
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +264 -18
- package/dist/index.mjs +252 -18
- package/package.json +4 -1
package/SKILL.md
CHANGED
|
@@ -1,67 +1,54 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
name: analytix402
|
|
3
|
+
description: "Monitor and control your AI agent's API spend and LLM costs via the Analytix402 dashboard. Use when: user asks about spending, costs, budget, or wants a spending report."
|
|
4
|
+
homepage: https://analytix402.com
|
|
5
|
+
metadata: { "openclaw": { "emoji": "📊" } }
|
|
6
|
+
---
|
|
2
7
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Description
|
|
6
|
-
|
|
7
|
-
Analytix402 gives your OpenClaw agent financial visibility and guardrails. Track every API call, LLM invocation, and x402 payment your agent makes. Set budget limits, detect duplicate purchases, and get alerts before costs spiral.
|
|
8
|
-
|
|
9
|
-
**What it does:**
|
|
10
|
-
- Tracks all outbound API calls and x402 payments automatically
|
|
11
|
-
- Monitors LLM token usage and costs across OpenAI, Anthropic, and other providers
|
|
12
|
-
- Enforces daily budget limits and per-call spend caps
|
|
13
|
-
- Detects duplicate API purchases to prevent waste
|
|
14
|
-
- Sends heartbeats so you know your agent is alive and healthy
|
|
15
|
-
- Provides a real-time dashboard at analytix402.com
|
|
8
|
+
# Analytix402 Spend Tracking
|
|
16
9
|
|
|
17
|
-
|
|
10
|
+
Monitor, control, and optimize your AI agent's API spend and LLM costs in real-time.
|
|
18
11
|
|
|
19
|
-
|
|
12
|
+
## When to Use
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
- "What's my spending?" / "Show my spend report"
|
|
15
|
+
- "How much have I spent today?"
|
|
16
|
+
- "What's my budget?"
|
|
17
|
+
- "Show agent overview"
|
|
18
|
+
- "Show spending insights"
|
|
24
19
|
|
|
25
|
-
|
|
20
|
+
## When NOT to Use
|
|
26
21
|
|
|
27
|
-
|
|
22
|
+
- General questions unrelated to spending or budgets
|
|
23
|
+
- Setting up the API key (direct user to dashboard)
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
## How to Run Commands
|
|
30
26
|
|
|
31
|
-
|
|
32
|
-
# Required
|
|
33
|
-
ANALYTIX402_API_KEY: ax_live_your_key_here
|
|
27
|
+
CRITICAL: Do NOT create new scripts. Do NOT suggest the user create scripts. Do NOT use node -e. Pre-built scripts already exist. Just run them using the exec tool.
|
|
34
28
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
ANALYTIX402_BASE_URL: https://analytix402.com # Custom endpoint
|
|
39
|
-
ANALYTIX402_DAILY_BUDGET: 50.00 # Daily spend limit in USD (0 = unlimited)
|
|
40
|
-
ANALYTIX402_PER_CALL_LIMIT: 5.00 # Per-call cap in USD (0 = unlimited)
|
|
41
|
-
ANALYTIX402_TRACK_LLM: true # LLM tracking (enabled by default)
|
|
29
|
+
### Full Spend Report (use this when user asks about spending)
|
|
30
|
+
```
|
|
31
|
+
cd C:\Users\samue\.openclaw\workspace && node spend-report.js
|
|
42
32
|
```
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
### analytix402_set_budget
|
|
50
|
-
Set or update the daily budget limit for this agent session.
|
|
51
|
-
|
|
52
|
-
### analytix402_check_budget
|
|
53
|
-
Check remaining budget before making an expensive API call.
|
|
54
|
-
|
|
55
|
-
### analytix402_flag_purchase
|
|
56
|
-
Flag a potential duplicate or unnecessary purchase for review.
|
|
34
|
+
### Check Budget
|
|
35
|
+
```
|
|
36
|
+
cd C:\Users\samue\.openclaw\workspace && node check-budget.js
|
|
37
|
+
```
|
|
57
38
|
|
|
58
|
-
|
|
39
|
+
### Set Daily Budget (replace 10 and 1 with desired values)
|
|
40
|
+
```
|
|
41
|
+
cd C:\Users\samue\.openclaw\workspace && node set-budget.js 10 1
|
|
42
|
+
```
|
|
59
43
|
|
|
60
|
-
|
|
61
|
-
2. Open the [Agents dashboard](https://analytix402.com/agents.html) — your agent should appear within seconds
|
|
62
|
-
3. The agent stays visible with a heartbeat every 60 seconds
|
|
44
|
+
## IMPORTANT RULES
|
|
63
45
|
|
|
64
|
-
|
|
46
|
+
1. ALWAYS use the `exec` tool — do NOT ask the user to run commands manually
|
|
47
|
+
2. ALWAYS use the EXACT commands above — do NOT modify them
|
|
48
|
+
3. Do NOT create new script files — the scripts already exist
|
|
49
|
+
4. Do NOT suggest workarounds — just run the command
|
|
50
|
+
5. The output is JSON — present it in a clean, readable format to the user
|
|
51
|
+
6. For spend reports: highlight total spend, daily spend, budget status, fleet health, and number of requests
|
|
65
52
|
|
|
66
53
|
## Tags
|
|
67
54
|
|
package/dist/index.d.mts
CHANGED
|
@@ -56,5 +56,7 @@ declare function sendHeartbeat(status?: 'healthy' | 'degraded' | 'error'): void;
|
|
|
56
56
|
* Gracefully shutdown — flush all pending events.
|
|
57
57
|
*/
|
|
58
58
|
declare function shutdown(): Promise<void>;
|
|
59
|
+
declare function register(ctx?: Record<string, unknown>): void;
|
|
60
|
+
declare function activate(ctx?: Record<string, unknown>): void;
|
|
59
61
|
|
|
60
|
-
export { type CheckBudgetParams, type FlagPurchaseParams, type SetBudgetParams, analytix402_check_budget, analytix402_flag_purchase, analytix402_set_budget, analytix402_spend_report, sendHeartbeat, shutdown, trackAPICall, trackLLMCall };
|
|
62
|
+
export { type CheckBudgetParams, type FlagPurchaseParams, type SetBudgetParams, activate, analytix402_check_budget, analytix402_flag_purchase, analytix402_set_budget, analytix402_spend_report, register, sendHeartbeat, shutdown, trackAPICall, trackLLMCall };
|
package/dist/index.d.ts
CHANGED
|
@@ -56,5 +56,7 @@ declare function sendHeartbeat(status?: 'healthy' | 'degraded' | 'error'): void;
|
|
|
56
56
|
* Gracefully shutdown — flush all pending events.
|
|
57
57
|
*/
|
|
58
58
|
declare function shutdown(): Promise<void>;
|
|
59
|
+
declare function register(ctx?: Record<string, unknown>): void;
|
|
60
|
+
declare function activate(ctx?: Record<string, unknown>): void;
|
|
59
61
|
|
|
60
|
-
export { type CheckBudgetParams, type FlagPurchaseParams, type SetBudgetParams, analytix402_check_budget, analytix402_flag_purchase, analytix402_set_budget, analytix402_spend_report, sendHeartbeat, shutdown, trackAPICall, trackLLMCall };
|
|
62
|
+
export { type CheckBudgetParams, type FlagPurchaseParams, type SetBudgetParams, activate, analytix402_check_budget, analytix402_flag_purchase, analytix402_set_budget, analytix402_spend_report, register, sendHeartbeat, shutdown, trackAPICall, trackLLMCall };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,15 +17,25 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
33
|
+
activate: () => activate,
|
|
23
34
|
analytix402_check_budget: () => analytix402_check_budget,
|
|
24
35
|
analytix402_flag_purchase: () => analytix402_flag_purchase,
|
|
25
36
|
analytix402_set_budget: () => analytix402_set_budget,
|
|
26
37
|
analytix402_spend_report: () => analytix402_spend_report,
|
|
38
|
+
register: () => register,
|
|
27
39
|
sendHeartbeat: () => sendHeartbeat,
|
|
28
40
|
shutdown: () => shutdown,
|
|
29
41
|
trackAPICall: () => trackAPICall,
|
|
@@ -31,6 +43,45 @@ __export(index_exports, {
|
|
|
31
43
|
});
|
|
32
44
|
module.exports = __toCommonJS(index_exports);
|
|
33
45
|
var import_sdk = require("@analytix402/sdk");
|
|
46
|
+
var fs = __toESM(require("fs"));
|
|
47
|
+
var path = __toESM(require("path"));
|
|
48
|
+
function loadEnvFile() {
|
|
49
|
+
const thisDir = path.dirname(
|
|
50
|
+
typeof __filename !== "undefined" ? __filename : ""
|
|
51
|
+
);
|
|
52
|
+
const candidates = [
|
|
53
|
+
...thisDir ? [
|
|
54
|
+
path.resolve(thisDir, "..", ".env"),
|
|
55
|
+
// plugin root (extensions/openclaw-skill/.env)
|
|
56
|
+
path.resolve(thisDir, ".env")
|
|
57
|
+
// dist/.env
|
|
58
|
+
] : [],
|
|
59
|
+
path.join(process.cwd(), ".env"),
|
|
60
|
+
// cwd
|
|
61
|
+
path.resolve(process.env.HOME || process.env.USERPROFILE || "", ".openclaw", "workspace", ".env"),
|
|
62
|
+
path.resolve(process.env.HOME || process.env.USERPROFILE || "", ".openclaw", ".env")
|
|
63
|
+
];
|
|
64
|
+
for (const envPath of candidates) {
|
|
65
|
+
try {
|
|
66
|
+
if (!fs.existsSync(envPath)) continue;
|
|
67
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
68
|
+
for (const line of content.split("\n")) {
|
|
69
|
+
const trimmed = line.trim();
|
|
70
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
71
|
+
const eqIdx = trimmed.indexOf("=");
|
|
72
|
+
if (eqIdx < 0) continue;
|
|
73
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
74
|
+
const val = trimmed.slice(eqIdx + 1).trim();
|
|
75
|
+
if (key.startsWith("ANALYTIX402_") && !process.env[key]) {
|
|
76
|
+
process.env[key] = val;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
break;
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
loadEnvFile();
|
|
34
85
|
var API_KEY = process.env.ANALYTIX402_API_KEY || "";
|
|
35
86
|
var BASE_URL = (process.env.ANALYTIX402_BASE_URL || "https://analytix402.com").replace(/\/$/, "");
|
|
36
87
|
var AGENT_ID = process.env.ANALYTIX402_AGENT_ID || `openclaw-${Date.now()}`;
|
|
@@ -61,6 +112,172 @@ function getClient() {
|
|
|
61
112
|
}
|
|
62
113
|
return client;
|
|
63
114
|
}
|
|
115
|
+
var MODEL_PRICING = {
|
|
116
|
+
// Anthropic
|
|
117
|
+
"claude-opus-4-6": { input: 15, output: 75 },
|
|
118
|
+
"claude-sonnet-4-5-20250929": { input: 3, output: 15 },
|
|
119
|
+
"claude-haiku-4-5-20251001": { input: 0.25, output: 1.25 },
|
|
120
|
+
"claude-3-5-sonnet-20241022": { input: 3, output: 15 },
|
|
121
|
+
"claude-3-5-haiku-20241022": { input: 0.25, output: 1.25 },
|
|
122
|
+
"claude-3-opus-20240229": { input: 15, output: 75 },
|
|
123
|
+
// OpenAI
|
|
124
|
+
"gpt-4o": { input: 2.5, output: 10 },
|
|
125
|
+
"gpt-4o-2024-11-20": { input: 2.5, output: 10 },
|
|
126
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
127
|
+
"gpt-4-turbo": { input: 10, output: 30 },
|
|
128
|
+
"o1": { input: 15, output: 60 },
|
|
129
|
+
"o1-mini": { input: 1.1, output: 4.4 },
|
|
130
|
+
"o3-mini": { input: 1.1, output: 4.4 }
|
|
131
|
+
};
|
|
132
|
+
function calculateCost(model, inputTokens, outputTokens) {
|
|
133
|
+
const pricing = MODEL_PRICING[model] || Object.entries(MODEL_PRICING).find(([key]) => model.startsWith(key))?.[1];
|
|
134
|
+
if (!pricing) return 0;
|
|
135
|
+
return (inputTokens * pricing.input + outputTokens * pricing.output) / 1e6;
|
|
136
|
+
}
|
|
137
|
+
var LLM_API_HOSTS = ["api.anthropic.com", "api.openai.com"];
|
|
138
|
+
var originalFetch = globalThis.fetch;
|
|
139
|
+
function createStreamingProxy(response, host, startTime) {
|
|
140
|
+
const reader = response.body.getReader();
|
|
141
|
+
const decoder = new TextDecoder();
|
|
142
|
+
let inputTokens = 0;
|
|
143
|
+
let outputTokens = 0;
|
|
144
|
+
let model = "unknown";
|
|
145
|
+
let sseBuffer = "";
|
|
146
|
+
function processSSEEvent(eventData) {
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(eventData);
|
|
149
|
+
if (host === "api.anthropic.com") {
|
|
150
|
+
if (parsed.type === "message_start" && parsed.message) {
|
|
151
|
+
model = parsed.message.model || model;
|
|
152
|
+
inputTokens = parsed.message.usage?.input_tokens || 0;
|
|
153
|
+
}
|
|
154
|
+
if (parsed.type === "message_delta" && parsed.usage) {
|
|
155
|
+
outputTokens = parsed.usage.output_tokens || 0;
|
|
156
|
+
}
|
|
157
|
+
} else if (host === "api.openai.com") {
|
|
158
|
+
if (parsed.model) {
|
|
159
|
+
model = parsed.model;
|
|
160
|
+
}
|
|
161
|
+
if (parsed.usage) {
|
|
162
|
+
inputTokens = parsed.usage.prompt_tokens || 0;
|
|
163
|
+
outputTokens = parsed.usage.completion_tokens || 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function processSSEChunk(text) {
|
|
170
|
+
sseBuffer += text;
|
|
171
|
+
const lines = sseBuffer.split("\n");
|
|
172
|
+
sseBuffer = lines.pop() || "";
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
if (line.startsWith("data: ")) {
|
|
175
|
+
const data = line.slice(6).trim();
|
|
176
|
+
if (data && data !== "[DONE]") {
|
|
177
|
+
processSSEEvent(data);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const stream = new ReadableStream({
|
|
183
|
+
async pull(controller) {
|
|
184
|
+
try {
|
|
185
|
+
const { done, value } = await reader.read();
|
|
186
|
+
if (done) {
|
|
187
|
+
if (sseBuffer.trim()) {
|
|
188
|
+
processSSEChunk("\n");
|
|
189
|
+
}
|
|
190
|
+
if (inputTokens > 0 || outputTokens > 0) {
|
|
191
|
+
const durationMs = Date.now() - startTime;
|
|
192
|
+
const cost = calculateCost(model, inputTokens, outputTokens);
|
|
193
|
+
trackLLMCall({
|
|
194
|
+
model,
|
|
195
|
+
provider: host === "api.anthropic.com" ? "anthropic" : "openai",
|
|
196
|
+
inputTokens,
|
|
197
|
+
outputTokens,
|
|
198
|
+
costUsd: cost,
|
|
199
|
+
durationMs
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
controller.close();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
processSSEChunk(decoder.decode(value, { stream: true }));
|
|
206
|
+
controller.enqueue(value);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
controller.error(err);
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
cancel() {
|
|
212
|
+
reader.cancel();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
return new Response(stream, {
|
|
216
|
+
status: response.status,
|
|
217
|
+
statusText: response.statusText,
|
|
218
|
+
headers: response.headers
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function installFetchInterceptor() {
|
|
222
|
+
if (!TRACK_LLM || !originalFetch) return;
|
|
223
|
+
globalThis.fetch = async function interceptedFetch(input, init) {
|
|
224
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
225
|
+
let host;
|
|
226
|
+
try {
|
|
227
|
+
host = new URL(url).host;
|
|
228
|
+
} catch {
|
|
229
|
+
host = "";
|
|
230
|
+
}
|
|
231
|
+
if (!LLM_API_HOSTS.some((h) => host === h)) {
|
|
232
|
+
return originalFetch(input, init);
|
|
233
|
+
}
|
|
234
|
+
const start = Date.now();
|
|
235
|
+
const response = await originalFetch(input, init);
|
|
236
|
+
const contentType = response.headers.get("content-type") || "";
|
|
237
|
+
if (contentType.includes("text/event-stream") && response.body) {
|
|
238
|
+
return createStreamingProxy(response, host, start);
|
|
239
|
+
}
|
|
240
|
+
if (contentType.includes("application/json")) {
|
|
241
|
+
const clone = response.clone();
|
|
242
|
+
clone.text().then((text) => {
|
|
243
|
+
try {
|
|
244
|
+
const body = JSON.parse(text);
|
|
245
|
+
const durationMs = Date.now() - start;
|
|
246
|
+
if (host === "api.anthropic.com" && body.usage) {
|
|
247
|
+
const inputTokens = body.usage.input_tokens || 0;
|
|
248
|
+
const outputTokens = body.usage.output_tokens || 0;
|
|
249
|
+
const model = body.model || "unknown";
|
|
250
|
+
const cost = calculateCost(model, inputTokens, outputTokens);
|
|
251
|
+
trackLLMCall({
|
|
252
|
+
model,
|
|
253
|
+
provider: "anthropic",
|
|
254
|
+
inputTokens,
|
|
255
|
+
outputTokens,
|
|
256
|
+
costUsd: cost,
|
|
257
|
+
durationMs
|
|
258
|
+
});
|
|
259
|
+
} else if (host === "api.openai.com" && body.usage) {
|
|
260
|
+
const inputTokens = body.usage.prompt_tokens || 0;
|
|
261
|
+
const outputTokens = body.usage.completion_tokens || 0;
|
|
262
|
+
const model = body.model || "unknown";
|
|
263
|
+
const cost = calculateCost(model, inputTokens, outputTokens);
|
|
264
|
+
trackLLMCall({
|
|
265
|
+
model,
|
|
266
|
+
provider: "openai",
|
|
267
|
+
inputTokens,
|
|
268
|
+
outputTokens,
|
|
269
|
+
costUsd: cost,
|
|
270
|
+
durationMs
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
}).catch(() => {
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return response;
|
|
279
|
+
};
|
|
280
|
+
}
|
|
64
281
|
if (API_KEY) {
|
|
65
282
|
try {
|
|
66
283
|
getClient();
|
|
@@ -70,36 +287,49 @@ if (API_KEY) {
|
|
|
70
287
|
} else {
|
|
71
288
|
console.warn('[Analytix402] No API key found. Run "npx @analytix402/openclaw-skill init" to set up.');
|
|
72
289
|
}
|
|
73
|
-
|
|
74
|
-
|
|
290
|
+
installFetchInterceptor();
|
|
291
|
+
async function apiRequest(method, path2, body) {
|
|
292
|
+
const url = `${BASE_URL}/api${path2}`;
|
|
75
293
|
const headers = {
|
|
76
294
|
"Content-Type": "application/json",
|
|
77
295
|
"X-API-Key": API_KEY
|
|
78
296
|
};
|
|
79
|
-
const
|
|
297
|
+
const controller = new AbortController();
|
|
298
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
299
|
+
const opts = { method, headers, signal: controller.signal };
|
|
80
300
|
if (body && method !== "GET") {
|
|
81
301
|
opts.body = JSON.stringify(body);
|
|
82
302
|
}
|
|
83
|
-
const res = await fetch(url, opts);
|
|
84
|
-
const text = await res.text();
|
|
85
|
-
let data;
|
|
86
303
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
data
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
304
|
+
const res = await fetch(url, opts);
|
|
305
|
+
const text = await res.text();
|
|
306
|
+
let data;
|
|
307
|
+
try {
|
|
308
|
+
data = JSON.parse(text);
|
|
309
|
+
} catch {
|
|
310
|
+
data = { raw: text };
|
|
311
|
+
}
|
|
312
|
+
if (!res.ok) {
|
|
313
|
+
throw new Error(`Analytix402 API ${res.status}: ${data.error || text}`);
|
|
314
|
+
}
|
|
315
|
+
return data;
|
|
316
|
+
} finally {
|
|
317
|
+
clearTimeout(timeout);
|
|
93
318
|
}
|
|
94
|
-
return data;
|
|
95
319
|
}
|
|
96
320
|
async function analytix402_spend_report() {
|
|
97
321
|
const c = getClient();
|
|
98
|
-
c.heartbeat("healthy", { sessionCalls, sessionSpend });
|
|
99
322
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
323
|
+
c.heartbeat("healthy", { sessionCalls, sessionSpend });
|
|
324
|
+
} catch {
|
|
325
|
+
}
|
|
326
|
+
let result;
|
|
327
|
+
try {
|
|
328
|
+
const [overview, insights] = await Promise.all([
|
|
329
|
+
apiRequest("GET", "/agents/overview").catch(() => null),
|
|
330
|
+
apiRequest("GET", "/agents/insights").catch(() => null)
|
|
331
|
+
]);
|
|
332
|
+
result = JSON.stringify({
|
|
103
333
|
session: {
|
|
104
334
|
totalCalls: sessionCalls,
|
|
105
335
|
totalSpend: `$${sessionSpend.toFixed(4)}`,
|
|
@@ -111,7 +341,7 @@ async function analytix402_spend_report() {
|
|
|
111
341
|
insights
|
|
112
342
|
}, null, 2);
|
|
113
343
|
} catch (error) {
|
|
114
|
-
|
|
344
|
+
result = JSON.stringify({
|
|
115
345
|
session: {
|
|
116
346
|
totalCalls: sessionCalls,
|
|
117
347
|
totalSpend: `$${sessionSpend.toFixed(4)}`,
|
|
@@ -121,6 +351,8 @@ async function analytix402_spend_report() {
|
|
|
121
351
|
error: error instanceof Error ? error.message : String(error)
|
|
122
352
|
}, null, 2);
|
|
123
353
|
}
|
|
354
|
+
await shutdown();
|
|
355
|
+
return result;
|
|
124
356
|
}
|
|
125
357
|
function analytix402_set_budget(params) {
|
|
126
358
|
const updates = [];
|
|
@@ -243,12 +475,26 @@ async function shutdown() {
|
|
|
243
475
|
client = null;
|
|
244
476
|
}
|
|
245
477
|
}
|
|
478
|
+
function register(ctx) {
|
|
479
|
+
if (API_KEY) {
|
|
480
|
+
try {
|
|
481
|
+
getClient();
|
|
482
|
+
} catch (err) {
|
|
483
|
+
console.warn("[Analytix402] Failed to connect on register:", err instanceof Error ? err.message : err);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function activate(ctx) {
|
|
488
|
+
return register(ctx);
|
|
489
|
+
}
|
|
246
490
|
// Annotate the CommonJS export names for ESM import in node:
|
|
247
491
|
0 && (module.exports = {
|
|
492
|
+
activate,
|
|
248
493
|
analytix402_check_budget,
|
|
249
494
|
analytix402_flag_purchase,
|
|
250
495
|
analytix402_set_budget,
|
|
251
496
|
analytix402_spend_report,
|
|
497
|
+
register,
|
|
252
498
|
sendHeartbeat,
|
|
253
499
|
shutdown,
|
|
254
500
|
trackAPICall,
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { createClient } from "@analytix402/sdk";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
function loadEnvFile() {
|
|
6
|
+
const thisDir = path.dirname(
|
|
7
|
+
typeof __filename !== "undefined" ? __filename : ""
|
|
8
|
+
);
|
|
9
|
+
const candidates = [
|
|
10
|
+
...thisDir ? [
|
|
11
|
+
path.resolve(thisDir, "..", ".env"),
|
|
12
|
+
// plugin root (extensions/openclaw-skill/.env)
|
|
13
|
+
path.resolve(thisDir, ".env")
|
|
14
|
+
// dist/.env
|
|
15
|
+
] : [],
|
|
16
|
+
path.join(process.cwd(), ".env"),
|
|
17
|
+
// cwd
|
|
18
|
+
path.resolve(process.env.HOME || process.env.USERPROFILE || "", ".openclaw", "workspace", ".env"),
|
|
19
|
+
path.resolve(process.env.HOME || process.env.USERPROFILE || "", ".openclaw", ".env")
|
|
20
|
+
];
|
|
21
|
+
for (const envPath of candidates) {
|
|
22
|
+
try {
|
|
23
|
+
if (!fs.existsSync(envPath)) continue;
|
|
24
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
25
|
+
for (const line of content.split("\n")) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
28
|
+
const eqIdx = trimmed.indexOf("=");
|
|
29
|
+
if (eqIdx < 0) continue;
|
|
30
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
31
|
+
const val = trimmed.slice(eqIdx + 1).trim();
|
|
32
|
+
if (key.startsWith("ANALYTIX402_") && !process.env[key]) {
|
|
33
|
+
process.env[key] = val;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
loadEnvFile();
|
|
3
42
|
var API_KEY = process.env.ANALYTIX402_API_KEY || "";
|
|
4
43
|
var BASE_URL = (process.env.ANALYTIX402_BASE_URL || "https://analytix402.com").replace(/\/$/, "");
|
|
5
44
|
var AGENT_ID = process.env.ANALYTIX402_AGENT_ID || `openclaw-${Date.now()}`;
|
|
@@ -30,6 +69,172 @@ function getClient() {
|
|
|
30
69
|
}
|
|
31
70
|
return client;
|
|
32
71
|
}
|
|
72
|
+
var MODEL_PRICING = {
|
|
73
|
+
// Anthropic
|
|
74
|
+
"claude-opus-4-6": { input: 15, output: 75 },
|
|
75
|
+
"claude-sonnet-4-5-20250929": { input: 3, output: 15 },
|
|
76
|
+
"claude-haiku-4-5-20251001": { input: 0.25, output: 1.25 },
|
|
77
|
+
"claude-3-5-sonnet-20241022": { input: 3, output: 15 },
|
|
78
|
+
"claude-3-5-haiku-20241022": { input: 0.25, output: 1.25 },
|
|
79
|
+
"claude-3-opus-20240229": { input: 15, output: 75 },
|
|
80
|
+
// OpenAI
|
|
81
|
+
"gpt-4o": { input: 2.5, output: 10 },
|
|
82
|
+
"gpt-4o-2024-11-20": { input: 2.5, output: 10 },
|
|
83
|
+
"gpt-4o-mini": { input: 0.15, output: 0.6 },
|
|
84
|
+
"gpt-4-turbo": { input: 10, output: 30 },
|
|
85
|
+
"o1": { input: 15, output: 60 },
|
|
86
|
+
"o1-mini": { input: 1.1, output: 4.4 },
|
|
87
|
+
"o3-mini": { input: 1.1, output: 4.4 }
|
|
88
|
+
};
|
|
89
|
+
function calculateCost(model, inputTokens, outputTokens) {
|
|
90
|
+
const pricing = MODEL_PRICING[model] || Object.entries(MODEL_PRICING).find(([key]) => model.startsWith(key))?.[1];
|
|
91
|
+
if (!pricing) return 0;
|
|
92
|
+
return (inputTokens * pricing.input + outputTokens * pricing.output) / 1e6;
|
|
93
|
+
}
|
|
94
|
+
var LLM_API_HOSTS = ["api.anthropic.com", "api.openai.com"];
|
|
95
|
+
var originalFetch = globalThis.fetch;
|
|
96
|
+
function createStreamingProxy(response, host, startTime) {
|
|
97
|
+
const reader = response.body.getReader();
|
|
98
|
+
const decoder = new TextDecoder();
|
|
99
|
+
let inputTokens = 0;
|
|
100
|
+
let outputTokens = 0;
|
|
101
|
+
let model = "unknown";
|
|
102
|
+
let sseBuffer = "";
|
|
103
|
+
function processSSEEvent(eventData) {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(eventData);
|
|
106
|
+
if (host === "api.anthropic.com") {
|
|
107
|
+
if (parsed.type === "message_start" && parsed.message) {
|
|
108
|
+
model = parsed.message.model || model;
|
|
109
|
+
inputTokens = parsed.message.usage?.input_tokens || 0;
|
|
110
|
+
}
|
|
111
|
+
if (parsed.type === "message_delta" && parsed.usage) {
|
|
112
|
+
outputTokens = parsed.usage.output_tokens || 0;
|
|
113
|
+
}
|
|
114
|
+
} else if (host === "api.openai.com") {
|
|
115
|
+
if (parsed.model) {
|
|
116
|
+
model = parsed.model;
|
|
117
|
+
}
|
|
118
|
+
if (parsed.usage) {
|
|
119
|
+
inputTokens = parsed.usage.prompt_tokens || 0;
|
|
120
|
+
outputTokens = parsed.usage.completion_tokens || 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function processSSEChunk(text) {
|
|
127
|
+
sseBuffer += text;
|
|
128
|
+
const lines = sseBuffer.split("\n");
|
|
129
|
+
sseBuffer = lines.pop() || "";
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
if (line.startsWith("data: ")) {
|
|
132
|
+
const data = line.slice(6).trim();
|
|
133
|
+
if (data && data !== "[DONE]") {
|
|
134
|
+
processSSEEvent(data);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const stream = new ReadableStream({
|
|
140
|
+
async pull(controller) {
|
|
141
|
+
try {
|
|
142
|
+
const { done, value } = await reader.read();
|
|
143
|
+
if (done) {
|
|
144
|
+
if (sseBuffer.trim()) {
|
|
145
|
+
processSSEChunk("\n");
|
|
146
|
+
}
|
|
147
|
+
if (inputTokens > 0 || outputTokens > 0) {
|
|
148
|
+
const durationMs = Date.now() - startTime;
|
|
149
|
+
const cost = calculateCost(model, inputTokens, outputTokens);
|
|
150
|
+
trackLLMCall({
|
|
151
|
+
model,
|
|
152
|
+
provider: host === "api.anthropic.com" ? "anthropic" : "openai",
|
|
153
|
+
inputTokens,
|
|
154
|
+
outputTokens,
|
|
155
|
+
costUsd: cost,
|
|
156
|
+
durationMs
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
controller.close();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
processSSEChunk(decoder.decode(value, { stream: true }));
|
|
163
|
+
controller.enqueue(value);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
controller.error(err);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
cancel() {
|
|
169
|
+
reader.cancel();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
return new Response(stream, {
|
|
173
|
+
status: response.status,
|
|
174
|
+
statusText: response.statusText,
|
|
175
|
+
headers: response.headers
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
function installFetchInterceptor() {
|
|
179
|
+
if (!TRACK_LLM || !originalFetch) return;
|
|
180
|
+
globalThis.fetch = async function interceptedFetch(input, init) {
|
|
181
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
182
|
+
let host;
|
|
183
|
+
try {
|
|
184
|
+
host = new URL(url).host;
|
|
185
|
+
} catch {
|
|
186
|
+
host = "";
|
|
187
|
+
}
|
|
188
|
+
if (!LLM_API_HOSTS.some((h) => host === h)) {
|
|
189
|
+
return originalFetch(input, init);
|
|
190
|
+
}
|
|
191
|
+
const start = Date.now();
|
|
192
|
+
const response = await originalFetch(input, init);
|
|
193
|
+
const contentType = response.headers.get("content-type") || "";
|
|
194
|
+
if (contentType.includes("text/event-stream") && response.body) {
|
|
195
|
+
return createStreamingProxy(response, host, start);
|
|
196
|
+
}
|
|
197
|
+
if (contentType.includes("application/json")) {
|
|
198
|
+
const clone = response.clone();
|
|
199
|
+
clone.text().then((text) => {
|
|
200
|
+
try {
|
|
201
|
+
const body = JSON.parse(text);
|
|
202
|
+
const durationMs = Date.now() - start;
|
|
203
|
+
if (host === "api.anthropic.com" && body.usage) {
|
|
204
|
+
const inputTokens = body.usage.input_tokens || 0;
|
|
205
|
+
const outputTokens = body.usage.output_tokens || 0;
|
|
206
|
+
const model = body.model || "unknown";
|
|
207
|
+
const cost = calculateCost(model, inputTokens, outputTokens);
|
|
208
|
+
trackLLMCall({
|
|
209
|
+
model,
|
|
210
|
+
provider: "anthropic",
|
|
211
|
+
inputTokens,
|
|
212
|
+
outputTokens,
|
|
213
|
+
costUsd: cost,
|
|
214
|
+
durationMs
|
|
215
|
+
});
|
|
216
|
+
} else if (host === "api.openai.com" && body.usage) {
|
|
217
|
+
const inputTokens = body.usage.prompt_tokens || 0;
|
|
218
|
+
const outputTokens = body.usage.completion_tokens || 0;
|
|
219
|
+
const model = body.model || "unknown";
|
|
220
|
+
const cost = calculateCost(model, inputTokens, outputTokens);
|
|
221
|
+
trackLLMCall({
|
|
222
|
+
model,
|
|
223
|
+
provider: "openai",
|
|
224
|
+
inputTokens,
|
|
225
|
+
outputTokens,
|
|
226
|
+
costUsd: cost,
|
|
227
|
+
durationMs
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
}).catch(() => {
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return response;
|
|
236
|
+
};
|
|
237
|
+
}
|
|
33
238
|
if (API_KEY) {
|
|
34
239
|
try {
|
|
35
240
|
getClient();
|
|
@@ -39,36 +244,49 @@ if (API_KEY) {
|
|
|
39
244
|
} else {
|
|
40
245
|
console.warn('[Analytix402] No API key found. Run "npx @analytix402/openclaw-skill init" to set up.');
|
|
41
246
|
}
|
|
42
|
-
|
|
43
|
-
|
|
247
|
+
installFetchInterceptor();
|
|
248
|
+
async function apiRequest(method, path2, body) {
|
|
249
|
+
const url = `${BASE_URL}/api${path2}`;
|
|
44
250
|
const headers = {
|
|
45
251
|
"Content-Type": "application/json",
|
|
46
252
|
"X-API-Key": API_KEY
|
|
47
253
|
};
|
|
48
|
-
const
|
|
254
|
+
const controller = new AbortController();
|
|
255
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
256
|
+
const opts = { method, headers, signal: controller.signal };
|
|
49
257
|
if (body && method !== "GET") {
|
|
50
258
|
opts.body = JSON.stringify(body);
|
|
51
259
|
}
|
|
52
|
-
const res = await fetch(url, opts);
|
|
53
|
-
const text = await res.text();
|
|
54
|
-
let data;
|
|
55
260
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
data
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
261
|
+
const res = await fetch(url, opts);
|
|
262
|
+
const text = await res.text();
|
|
263
|
+
let data;
|
|
264
|
+
try {
|
|
265
|
+
data = JSON.parse(text);
|
|
266
|
+
} catch {
|
|
267
|
+
data = { raw: text };
|
|
268
|
+
}
|
|
269
|
+
if (!res.ok) {
|
|
270
|
+
throw new Error(`Analytix402 API ${res.status}: ${data.error || text}`);
|
|
271
|
+
}
|
|
272
|
+
return data;
|
|
273
|
+
} finally {
|
|
274
|
+
clearTimeout(timeout);
|
|
62
275
|
}
|
|
63
|
-
return data;
|
|
64
276
|
}
|
|
65
277
|
async function analytix402_spend_report() {
|
|
66
278
|
const c = getClient();
|
|
67
|
-
c.heartbeat("healthy", { sessionCalls, sessionSpend });
|
|
68
279
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
280
|
+
c.heartbeat("healthy", { sessionCalls, sessionSpend });
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
let result;
|
|
284
|
+
try {
|
|
285
|
+
const [overview, insights] = await Promise.all([
|
|
286
|
+
apiRequest("GET", "/agents/overview").catch(() => null),
|
|
287
|
+
apiRequest("GET", "/agents/insights").catch(() => null)
|
|
288
|
+
]);
|
|
289
|
+
result = JSON.stringify({
|
|
72
290
|
session: {
|
|
73
291
|
totalCalls: sessionCalls,
|
|
74
292
|
totalSpend: `$${sessionSpend.toFixed(4)}`,
|
|
@@ -80,7 +298,7 @@ async function analytix402_spend_report() {
|
|
|
80
298
|
insights
|
|
81
299
|
}, null, 2);
|
|
82
300
|
} catch (error) {
|
|
83
|
-
|
|
301
|
+
result = JSON.stringify({
|
|
84
302
|
session: {
|
|
85
303
|
totalCalls: sessionCalls,
|
|
86
304
|
totalSpend: `$${sessionSpend.toFixed(4)}`,
|
|
@@ -90,6 +308,8 @@ async function analytix402_spend_report() {
|
|
|
90
308
|
error: error instanceof Error ? error.message : String(error)
|
|
91
309
|
}, null, 2);
|
|
92
310
|
}
|
|
311
|
+
await shutdown();
|
|
312
|
+
return result;
|
|
93
313
|
}
|
|
94
314
|
function analytix402_set_budget(params) {
|
|
95
315
|
const updates = [];
|
|
@@ -212,11 +432,25 @@ async function shutdown() {
|
|
|
212
432
|
client = null;
|
|
213
433
|
}
|
|
214
434
|
}
|
|
435
|
+
function register(ctx) {
|
|
436
|
+
if (API_KEY) {
|
|
437
|
+
try {
|
|
438
|
+
getClient();
|
|
439
|
+
} catch (err) {
|
|
440
|
+
console.warn("[Analytix402] Failed to connect on register:", err instanceof Error ? err.message : err);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
function activate(ctx) {
|
|
445
|
+
return register(ctx);
|
|
446
|
+
}
|
|
215
447
|
export {
|
|
448
|
+
activate,
|
|
216
449
|
analytix402_check_budget,
|
|
217
450
|
analytix402_flag_purchase,
|
|
218
451
|
analytix402_set_budget,
|
|
219
452
|
analytix402_spend_report,
|
|
453
|
+
register,
|
|
220
454
|
sendHeartbeat,
|
|
221
455
|
shutdown,
|
|
222
456
|
trackAPICall,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@analytix402/openclaw-skill",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Analytix402 skill for OpenClaw — monitor, control, and optimize your AI agent's spend",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"prepublishOnly": "npm run build:index && npm run build:setup"
|
|
26
26
|
},
|
|
27
27
|
"openclaw": {
|
|
28
|
+
"extensions": [
|
|
29
|
+
"dist/index.js"
|
|
30
|
+
],
|
|
28
31
|
"tools": {
|
|
29
32
|
"analytix402_spend_report": {
|
|
30
33
|
"description": "Get a summary of your agent's spend — total cost, breakdown by API and LLM provider, and efficiency score.",
|