@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 +37 -40
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +267 -20
- package/dist/index.mjs +255 -20
- package/dist/setup.js +105 -0
- package/package.json +12 -3
package/SKILL.md
CHANGED
|
@@ -1,57 +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
|
+
---
|
|
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
|
-
##
|
|
12
|
+
## When to Use
|
|
6
13
|
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
- General questions unrelated to spending or budgets
|
|
23
|
+
- Setting up the API key (direct user to dashboard)
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
## How to Run Commands
|
|
20
26
|
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
data
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
data
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
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.",
|