@analytix402/openclaw-skill 0.1.6 → 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,57 +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
+ ---
7
+
8
+ # Analytix402 Spend Tracking
2
9
 
3
10
  Monitor, control, and optimize your AI agent's API spend and LLM costs in real-time.
4
11
 
5
- ## Description
12
+ ## When to Use
6
13
 
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.
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"
8
19
 
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
20
+ ## When NOT to Use
16
21
 
17
- ## Configuration
22
+ - General questions unrelated to spending or budgets
23
+ - Setting up the API key (direct user to dashboard)
18
24
 
19
- Add these to your OpenClaw agent's `.env` file:
25
+ ## How to Run Commands
20
26
 
21
- ```yaml
22
- # Required
23
- 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.
24
28
 
25
- # Optional
26
- ANALYTIX402_AGENT_ID: my-openclaw-agent # Unique ID shown in dashboard
27
- ANALYTIX402_AGENT_NAME: My OpenClaw Agent # Display name (defaults to agent ID)
28
- ANALYTIX402_BASE_URL: https://analytix402.com # Custom endpoint
29
- ANALYTIX402_DAILY_BUDGET: 50.00 # Daily spend limit in USD (0 = unlimited)
30
- ANALYTIX402_PER_CALL_LIMIT: 5.00 # Per-call cap in USD (0 = unlimited)
31
- 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
32
32
  ```
33
33
 
34
- ## Tools
35
-
36
- ### analytix402_spend_report
37
- Get a summary of your agent's spend — total cost, breakdown by API and LLM provider, and efficiency score.
38
-
39
- ### analytix402_set_budget
40
- Set or update the daily budget limit for this agent session.
41
-
42
- ### analytix402_check_budget
43
- Check remaining budget before making an expensive API call.
44
-
45
- ### analytix402_flag_purchase
46
- 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
+ ```
47
38
 
48
- ## 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
+ ```
49
43
 
50
- 1. Start your OpenClaw agent — the skill connects automatically on load
51
- 2. Open the [Agents dashboard](https://analytix402.com/agents.html) — your agent should appear within seconds
52
- 3. The agent stays visible with a heartbeat every 60 seconds
44
+ ## IMPORTANT RULES
53
45
 
54
- 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
55
52
 
56
53
  ## Tags
57
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()}`;
@@ -47,7 +98,7 @@ function getClient() {
47
98
  if (!client) {
48
99
  if (!API_KEY) {
49
100
  throw new Error(
50
- "ANALYTIX402_API_KEY is not set. Add it to your OpenClaw skill config."
101
+ 'ANALYTIX402_API_KEY is not set. Run "npx @analytix402/openclaw-skill init" to configure.'
51
102
  );
52
103
  }
53
104
  client = (0, import_sdk.createClient)({
@@ -61,44 +112,224 @@ 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();
67
284
  } catch (err) {
68
285
  console.warn("[Analytix402] Failed to connect on startup:", err instanceof Error ? err.message : err);
69
286
  }
287
+ } else {
288
+ console.warn('[Analytix402] No API key found. Run "npx @analytix402/openclaw-skill init" to set up.');
70
289
  }
71
- async function apiRequest(method, path, body) {
72
- const url = `${BASE_URL}/api${path}`;
290
+ installFetchInterceptor();
291
+ async function apiRequest(method, path2, body) {
292
+ const url = `${BASE_URL}/api${path2}`;
73
293
  const headers = {
74
294
  "Content-Type": "application/json",
75
- "Authorization": `Bearer ${API_KEY}`,
76
295
  "X-API-Key": API_KEY
77
296
  };
78
- const opts = { method, headers };
297
+ const controller = new AbortController();
298
+ const timeout = setTimeout(() => controller.abort(), 5e3);
299
+ const opts = { method, headers, signal: controller.signal };
79
300
  if (body && method !== "GET") {
80
301
  opts.body = JSON.stringify(body);
81
302
  }
82
- const res = await fetch(url, opts);
83
- const text = await res.text();
84
- let data;
85
303
  try {
86
- data = JSON.parse(text);
87
- } catch {
88
- data = { raw: text };
89
- }
90
- if (!res.ok) {
91
- 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);
92
318
  }
93
- return data;
94
319
  }
95
320
  async function analytix402_spend_report() {
96
321
  const c = getClient();
97
- c.heartbeat("healthy", { sessionCalls, sessionSpend });
98
322
  try {
99
- const overview = await apiRequest("GET", "/agents/overview");
100
- const insights = await apiRequest("GET", "/agents/insights");
101
- 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({
102
333
  session: {
103
334
  totalCalls: sessionCalls,
104
335
  totalSpend: `$${sessionSpend.toFixed(4)}`,
@@ -110,7 +341,7 @@ async function analytix402_spend_report() {
110
341
  insights
111
342
  }, null, 2);
112
343
  } catch (error) {
113
- return JSON.stringify({
344
+ result = JSON.stringify({
114
345
  session: {
115
346
  totalCalls: sessionCalls,
116
347
  totalSpend: `$${sessionSpend.toFixed(4)}`,
@@ -120,6 +351,8 @@ async function analytix402_spend_report() {
120
351
  error: error instanceof Error ? error.message : String(error)
121
352
  }, null, 2);
122
353
  }
354
+ await shutdown();
355
+ return result;
123
356
  }
124
357
  function analytix402_set_budget(params) {
125
358
  const updates = [];
@@ -242,12 +475,26 @@ async function shutdown() {
242
475
  client = null;
243
476
  }
244
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
+ }
245
490
  // Annotate the CommonJS export names for ESM import in node:
246
491
  0 && (module.exports = {
492
+ activate,
247
493
  analytix402_check_budget,
248
494
  analytix402_flag_purchase,
249
495
  analytix402_set_budget,
250
496
  analytix402_spend_report,
497
+ register,
251
498
  sendHeartbeat,
252
499
  shutdown,
253
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()}`;
@@ -16,7 +55,7 @@ function getClient() {
16
55
  if (!client) {
17
56
  if (!API_KEY) {
18
57
  throw new Error(
19
- "ANALYTIX402_API_KEY is not set. Add it to your OpenClaw skill config."
58
+ 'ANALYTIX402_API_KEY is not set. Run "npx @analytix402/openclaw-skill init" to configure.'
20
59
  );
21
60
  }
22
61
  client = createClient({
@@ -30,44 +69,224 @@ 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();
36
241
  } catch (err) {
37
242
  console.warn("[Analytix402] Failed to connect on startup:", err instanceof Error ? err.message : err);
38
243
  }
244
+ } else {
245
+ console.warn('[Analytix402] No API key found. Run "npx @analytix402/openclaw-skill init" to set up.');
39
246
  }
40
- async function apiRequest(method, path, body) {
41
- const url = `${BASE_URL}/api${path}`;
247
+ installFetchInterceptor();
248
+ async function apiRequest(method, path2, body) {
249
+ const url = `${BASE_URL}/api${path2}`;
42
250
  const headers = {
43
251
  "Content-Type": "application/json",
44
- "Authorization": `Bearer ${API_KEY}`,
45
252
  "X-API-Key": API_KEY
46
253
  };
47
- const opts = { method, headers };
254
+ const controller = new AbortController();
255
+ const timeout = setTimeout(() => controller.abort(), 5e3);
256
+ const opts = { method, headers, signal: controller.signal };
48
257
  if (body && method !== "GET") {
49
258
  opts.body = JSON.stringify(body);
50
259
  }
51
- const res = await fetch(url, opts);
52
- const text = await res.text();
53
- let data;
54
260
  try {
55
- data = JSON.parse(text);
56
- } catch {
57
- data = { raw: text };
58
- }
59
- if (!res.ok) {
60
- 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);
61
275
  }
62
- return data;
63
276
  }
64
277
  async function analytix402_spend_report() {
65
278
  const c = getClient();
66
- c.heartbeat("healthy", { sessionCalls, sessionSpend });
67
279
  try {
68
- const overview = await apiRequest("GET", "/agents/overview");
69
- const insights = await apiRequest("GET", "/agents/insights");
70
- 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({
71
290
  session: {
72
291
  totalCalls: sessionCalls,
73
292
  totalSpend: `$${sessionSpend.toFixed(4)}`,
@@ -79,7 +298,7 @@ async function analytix402_spend_report() {
79
298
  insights
80
299
  }, null, 2);
81
300
  } catch (error) {
82
- return JSON.stringify({
301
+ result = JSON.stringify({
83
302
  session: {
84
303
  totalCalls: sessionCalls,
85
304
  totalSpend: `$${sessionSpend.toFixed(4)}`,
@@ -89,6 +308,8 @@ async function analytix402_spend_report() {
89
308
  error: error instanceof Error ? error.message : String(error)
90
309
  }, null, 2);
91
310
  }
311
+ await shutdown();
312
+ return result;
92
313
  }
93
314
  function analytix402_set_budget(params) {
94
315
  const updates = [];
@@ -211,11 +432,25 @@ async function shutdown() {
211
432
  client = null;
212
433
  }
213
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
+ }
214
447
  export {
448
+ activate,
215
449
  analytix402_check_budget,
216
450
  analytix402_flag_purchase,
217
451
  analytix402_set_budget,
218
452
  analytix402_spend_report,
453
+ register,
219
454
  sendHeartbeat,
220
455
  shutdown,
221
456
  trackAPICall,
package/dist/setup.js ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/setup.ts
27
+ var readline = __toESM(require("readline"));
28
+ var fs = __toESM(require("fs"));
29
+ var path = __toESM(require("path"));
30
+ var rl = readline.createInterface({ input: process.stdin, output: process.stdout });
31
+ function ask(question, fallback = "") {
32
+ return new Promise((resolve2) => {
33
+ rl.question(question, (answer) => {
34
+ resolve2(answer.trim() || fallback);
35
+ });
36
+ });
37
+ }
38
+ async function validateKey(apiKey, baseUrl) {
39
+ try {
40
+ const res = await fetch(`${baseUrl}/api/agents/overview`, {
41
+ headers: { "X-API-Key": apiKey }
42
+ });
43
+ return res.ok;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+ async function main() {
49
+ console.log("\n Analytix402 \u2014 Setup\n");
50
+ const apiKey = await ask(" API key (ax_live_... or ax_test_...): ");
51
+ if (!apiKey) {
52
+ console.log("\n No API key provided. Exiting.\n");
53
+ rl.close();
54
+ process.exit(1);
55
+ }
56
+ if (!apiKey.startsWith("ax_live_") && !apiKey.startsWith("ax_test_")) {
57
+ console.log(" Warning: key should start with ax_live_ or ax_test_\n");
58
+ }
59
+ const agentName = await ask(" Agent display name [OpenClaw Agent]: ", "OpenClaw Agent");
60
+ const dailyBudget = await ask(" Daily budget in USD (0 = unlimited) [0]: ", "0");
61
+ const baseUrl = await ask(" API endpoint [https://analytix402.com]: ", "https://analytix402.com");
62
+ rl.close();
63
+ console.log("\n Validating API key...");
64
+ const valid = await validateKey(apiKey, baseUrl.replace(/\/$/, ""));
65
+ if (valid) {
66
+ console.log(" Connected successfully.\n");
67
+ } else {
68
+ console.log(" Could not reach the dashboard \u2014 the key may still be correct.");
69
+ console.log(" (The server might be starting up or unreachable right now.)\n");
70
+ }
71
+ const envPath = path.resolve(process.cwd(), ".env");
72
+ const lines = [];
73
+ if (fs.existsSync(envPath)) {
74
+ const existing = fs.readFileSync(envPath, "utf-8");
75
+ for (const line of existing.split("\n")) {
76
+ if (!line.startsWith("ANALYTIX402_")) {
77
+ lines.push(line);
78
+ }
79
+ }
80
+ while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
81
+ lines.pop();
82
+ }
83
+ if (lines.length > 0) {
84
+ lines.push("");
85
+ }
86
+ }
87
+ lines.push(`ANALYTIX402_API_KEY=${apiKey}`);
88
+ if (agentName !== "OpenClaw Agent") {
89
+ lines.push(`ANALYTIX402_AGENT_NAME=${agentName}`);
90
+ }
91
+ if (dailyBudget !== "0") {
92
+ lines.push(`ANALYTIX402_DAILY_BUDGET=${dailyBudget}`);
93
+ }
94
+ if (baseUrl !== "https://analytix402.com") {
95
+ lines.push(`ANALYTIX402_BASE_URL=${baseUrl}`);
96
+ }
97
+ lines.push("");
98
+ fs.writeFileSync(envPath, lines.join("\n"), "utf-8");
99
+ console.log(` Wrote config to ${envPath}`);
100
+ console.log(" You're all set. The skill will connect automatically on next start.\n");
101
+ }
102
+ main().catch((err) => {
103
+ console.error("Setup failed:", err);
104
+ process.exit(1);
105
+ });
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@analytix402/openclaw-skill",
3
- "version": "0.1.6",
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",
7
7
  "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "openclaw-skill": "./dist/setup.js"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "types": "./dist/index.d.ts",
@@ -13,12 +16,18 @@
13
16
  }
14
17
  },
15
18
  "scripts": {
16
- "build": "tsup src/index.ts --format cjs,esm --dts",
19
+ "build": "tsup src/index.ts --format cjs,esm --dts && tsup src/setup.ts --format cjs --no-dts --outDir dist",
20
+ "build:index": "tsup src/index.ts --format cjs,esm --dts",
21
+ "build:setup": "tsup src/setup.ts --format cjs --no-dts --outDir dist",
17
22
  "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
18
23
  "typecheck": "tsc --noEmit",
19
- "prepublishOnly": "npm run build"
24
+ "postinstall": "echo \"\n Run 'npx @analytix402/openclaw-skill init' to configure your API key.\n\"",
25
+ "prepublishOnly": "npm run build:index && npm run build:setup"
20
26
  },
21
27
  "openclaw": {
28
+ "extensions": [
29
+ "dist/index.js"
30
+ ],
22
31
  "tools": {
23
32
  "analytix402_spend_report": {
24
33
  "description": "Get a summary of your agent's spend — total cost, breakdown by API and LLM provider, and efficiency score.",