@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 CHANGED
@@ -1,67 +1,54 @@
1
- # Analytix402
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
- Monitor, control, and optimize your AI agent's API spend and LLM costs in real-time.
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
- ## Quick Start
10
+ Monitor, control, and optimize your AI agent's API spend and LLM costs in real-time.
18
11
 
19
- After installing, run the setup wizard:
12
+ ## When to Use
20
13
 
21
- ```bash
22
- npx @analytix402/openclaw-skill init
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
- It will ask for your API key, validate it, and write your `.env` file.
20
+ ## When NOT to Use
26
21
 
27
- ## Configuration
22
+ - General questions unrelated to spending or budgets
23
+ - Setting up the API key (direct user to dashboard)
28
24
 
29
- Or add these to your OpenClaw agent's `.env` file manually:
25
+ ## How to Run Commands
30
26
 
31
- ```yaml
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
- # Optional
36
- ANALYTIX402_AGENT_ID: my-openclaw-agent # Unique ID shown in dashboard
37
- ANALYTIX402_AGENT_NAME: My OpenClaw Agent # Display name (defaults to agent ID)
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
- ## Tools
45
-
46
- ### analytix402_spend_report
47
- Get a summary of your agent's spend — total cost, breakdown by API and LLM provider, and efficiency score.
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
- ## Verify it works
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
- 1. Start your OpenClaw agent — the skill connects automatically on load
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
- No need to call any tools first. The skill registers itself immediately on startup.
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
- async function apiRequest(method, path, body) {
74
- const url = `${BASE_URL}/api${path}`;
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 opts = { method, headers };
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
- data = JSON.parse(text);
88
- } catch {
89
- data = { raw: text };
90
- }
91
- if (!res.ok) {
92
- throw new Error(`Analytix402 API ${res.status}: ${data.error || text}`);
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
- const overview = await apiRequest("GET", "/agents/overview");
101
- const insights = await apiRequest("GET", "/agents/insights");
102
- return JSON.stringify({
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
- return JSON.stringify({
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
- async function apiRequest(method, path, body) {
43
- const url = `${BASE_URL}/api${path}`;
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 opts = { method, headers };
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
- data = JSON.parse(text);
57
- } catch {
58
- data = { raw: text };
59
- }
60
- if (!res.ok) {
61
- throw new Error(`Analytix402 API ${res.status}: ${data.error || text}`);
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
- const overview = await apiRequest("GET", "/agents/overview");
70
- const insights = await apiRequest("GET", "/agents/insights");
71
- return JSON.stringify({
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
- return JSON.stringify({
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.0",
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.",