@analytix402/openclaw-skill 0.1.0
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 +47 -0
- package/dist/index.d.mts +60 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +480 -0
- package/dist/index.mjs +447 -0
- package/package.json +35 -0
- package/src/index.ts +335 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytix402 OpenClaw Skill
|
|
3
|
+
*
|
|
4
|
+
* Provides AI agent spend tracking, budget enforcement,
|
|
5
|
+
* and observability for OpenClaw agents.
|
|
6
|
+
*
|
|
7
|
+
* Integrates with the Analytix402 platform to give operators
|
|
8
|
+
* real-time visibility into what their agents are spending.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createClient } from '../../sdk/src/client';
|
|
12
|
+
import type { Analytix402Client } from '../../sdk/src/types';
|
|
13
|
+
|
|
14
|
+
// ============================================================
|
|
15
|
+
// Configuration
|
|
16
|
+
// ============================================================
|
|
17
|
+
|
|
18
|
+
const API_KEY = process.env.ANALYTIX402_API_KEY || '';
|
|
19
|
+
const BASE_URL = (process.env.ANALYTIX402_BASE_URL || 'https://analytix402.com').replace(/\/$/, '');
|
|
20
|
+
const AGENT_ID = process.env.ANALYTIX402_AGENT_ID || `openclaw-${Date.now()}`;
|
|
21
|
+
const AGENT_NAME = process.env.ANALYTIX402_AGENT_NAME || 'OpenClaw Agent';
|
|
22
|
+
const DAILY_BUDGET = parseFloat(process.env.ANALYTIX402_DAILY_BUDGET || '0') || 0;
|
|
23
|
+
const PER_CALL_LIMIT = parseFloat(process.env.ANALYTIX402_PER_CALL_LIMIT || '0') || 0;
|
|
24
|
+
const TRACK_LLM = process.env.ANALYTIX402_TRACK_LLM !== 'false';
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// State
|
|
28
|
+
// ============================================================
|
|
29
|
+
|
|
30
|
+
let client: Analytix402Client | null = null;
|
|
31
|
+
let sessionSpend = 0;
|
|
32
|
+
let sessionCalls = 0;
|
|
33
|
+
let dailySpend = 0;
|
|
34
|
+
const dailySpendDate = new Date().toISOString().slice(0, 10);
|
|
35
|
+
|
|
36
|
+
function getClient(): Analytix402Client {
|
|
37
|
+
if (!client) {
|
|
38
|
+
if (!API_KEY) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'ANALYTIX402_API_KEY is not set. Add it to your OpenClaw skill config.'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
client = createClient({
|
|
44
|
+
apiKey: API_KEY,
|
|
45
|
+
baseUrl: BASE_URL,
|
|
46
|
+
agentId: AGENT_ID,
|
|
47
|
+
agentName: AGENT_NAME,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return client;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ============================================================
|
|
54
|
+
// API helper (for dashboard queries)
|
|
55
|
+
// ============================================================
|
|
56
|
+
|
|
57
|
+
async function apiRequest(method: string, path: string, body?: unknown): Promise<unknown> {
|
|
58
|
+
const url = `${BASE_URL}/api${path}`;
|
|
59
|
+
const headers: Record<string, string> = {
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
62
|
+
'X-API-Key': API_KEY,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const opts: RequestInit = { method, headers };
|
|
66
|
+
if (body && method !== 'GET') {
|
|
67
|
+
opts.body = JSON.stringify(body);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const res = await fetch(url, opts);
|
|
71
|
+
const text = await res.text();
|
|
72
|
+
|
|
73
|
+
let data;
|
|
74
|
+
try {
|
|
75
|
+
data = JSON.parse(text);
|
|
76
|
+
} catch {
|
|
77
|
+
data = { raw: text };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
throw new Error(`Analytix402 API ${res.status}: ${(data as Record<string, string>).error || text}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return data;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================
|
|
88
|
+
// Tool: analytix402_spend_report
|
|
89
|
+
// ============================================================
|
|
90
|
+
|
|
91
|
+
export async function analytix402_spend_report(): Promise<string> {
|
|
92
|
+
const c = getClient();
|
|
93
|
+
|
|
94
|
+
// Send a heartbeat while we're at it
|
|
95
|
+
c.heartbeat('healthy', { sessionCalls, sessionSpend });
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const overview = await apiRequest('GET', '/agents/overview');
|
|
99
|
+
const insights = await apiRequest('GET', '/agents/insights');
|
|
100
|
+
|
|
101
|
+
return JSON.stringify({
|
|
102
|
+
session: {
|
|
103
|
+
totalCalls: sessionCalls,
|
|
104
|
+
totalSpend: `$${sessionSpend.toFixed(4)}`,
|
|
105
|
+
dailySpend: `$${dailySpend.toFixed(4)}`,
|
|
106
|
+
dailyBudget: DAILY_BUDGET > 0 ? `$${DAILY_BUDGET.toFixed(2)}` : 'unlimited',
|
|
107
|
+
budgetRemaining: DAILY_BUDGET > 0
|
|
108
|
+
? `$${(DAILY_BUDGET - dailySpend).toFixed(4)}`
|
|
109
|
+
: 'unlimited',
|
|
110
|
+
},
|
|
111
|
+
platform: overview,
|
|
112
|
+
insights,
|
|
113
|
+
}, null, 2);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return JSON.stringify({
|
|
116
|
+
session: {
|
|
117
|
+
totalCalls: sessionCalls,
|
|
118
|
+
totalSpend: `$${sessionSpend.toFixed(4)}`,
|
|
119
|
+
dailySpend: `$${dailySpend.toFixed(4)}`,
|
|
120
|
+
dailyBudget: DAILY_BUDGET > 0 ? `$${DAILY_BUDGET.toFixed(2)}` : 'unlimited',
|
|
121
|
+
},
|
|
122
|
+
error: error instanceof Error ? error.message : String(error),
|
|
123
|
+
}, null, 2);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================
|
|
128
|
+
// Tool: analytix402_set_budget
|
|
129
|
+
// ============================================================
|
|
130
|
+
|
|
131
|
+
export interface SetBudgetParams {
|
|
132
|
+
daily_limit?: number;
|
|
133
|
+
per_call_limit?: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function analytix402_set_budget(params: SetBudgetParams): string {
|
|
137
|
+
const updates: string[] = [];
|
|
138
|
+
|
|
139
|
+
if (params.daily_limit !== undefined && params.daily_limit >= 0) {
|
|
140
|
+
// Update the runtime budget (persists for this session)
|
|
141
|
+
Object.defineProperty(globalThis, '__ax402_daily_budget', {
|
|
142
|
+
value: params.daily_limit,
|
|
143
|
+
writable: true,
|
|
144
|
+
configurable: true,
|
|
145
|
+
});
|
|
146
|
+
updates.push(`Daily budget set to $${params.daily_limit.toFixed(2)}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (params.per_call_limit !== undefined && params.per_call_limit >= 0) {
|
|
150
|
+
Object.defineProperty(globalThis, '__ax402_per_call_limit', {
|
|
151
|
+
value: params.per_call_limit,
|
|
152
|
+
writable: true,
|
|
153
|
+
configurable: true,
|
|
154
|
+
});
|
|
155
|
+
updates.push(`Per-call limit set to $${params.per_call_limit.toFixed(2)}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (updates.length === 0) {
|
|
159
|
+
return 'No budget parameters provided. Use daily_limit and/or per_call_limit.';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return updates.join('. ') + '.';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================
|
|
166
|
+
// Tool: analytix402_check_budget
|
|
167
|
+
// ============================================================
|
|
168
|
+
|
|
169
|
+
export interface CheckBudgetParams {
|
|
170
|
+
estimated_cost?: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function analytix402_check_budget(params: CheckBudgetParams): string {
|
|
174
|
+
const budget = DAILY_BUDGET ||
|
|
175
|
+
((globalThis as Record<string, unknown>).__ax402_daily_budget as number) || 0;
|
|
176
|
+
const callLimit = PER_CALL_LIMIT ||
|
|
177
|
+
((globalThis as Record<string, unknown>).__ax402_per_call_limit as number) || 0;
|
|
178
|
+
const estimated = params.estimated_cost || 0;
|
|
179
|
+
|
|
180
|
+
const remaining = budget > 0 ? budget - dailySpend : Infinity;
|
|
181
|
+
const allowed = (budget === 0 || remaining >= estimated) &&
|
|
182
|
+
(callLimit === 0 || estimated <= callLimit);
|
|
183
|
+
|
|
184
|
+
return JSON.stringify({
|
|
185
|
+
allowed,
|
|
186
|
+
dailyBudget: budget > 0 ? `$${budget.toFixed(2)}` : 'unlimited',
|
|
187
|
+
dailySpent: `$${dailySpend.toFixed(4)}`,
|
|
188
|
+
remaining: budget > 0 ? `$${remaining.toFixed(4)}` : 'unlimited',
|
|
189
|
+
estimatedCost: `$${estimated.toFixed(4)}`,
|
|
190
|
+
perCallLimit: callLimit > 0 ? `$${callLimit.toFixed(2)}` : 'unlimited',
|
|
191
|
+
withinPerCallLimit: callLimit === 0 || estimated <= callLimit,
|
|
192
|
+
withinDailyBudget: budget === 0 || remaining >= estimated,
|
|
193
|
+
}, null, 2);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============================================================
|
|
197
|
+
// Tool: analytix402_flag_purchase
|
|
198
|
+
// ============================================================
|
|
199
|
+
|
|
200
|
+
export interface FlagPurchaseParams {
|
|
201
|
+
url: string;
|
|
202
|
+
reason?: string;
|
|
203
|
+
estimated_cost?: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function analytix402_flag_purchase(params: FlagPurchaseParams): string {
|
|
207
|
+
const c = getClient();
|
|
208
|
+
|
|
209
|
+
c.track({
|
|
210
|
+
type: 'request',
|
|
211
|
+
method: 'FLAG',
|
|
212
|
+
path: params.url,
|
|
213
|
+
endpoint: params.url,
|
|
214
|
+
statusCode: 0,
|
|
215
|
+
responseTimeMs: 0,
|
|
216
|
+
timestamp: new Date().toISOString(),
|
|
217
|
+
agentId: AGENT_ID,
|
|
218
|
+
metadata: {
|
|
219
|
+
flagged: true,
|
|
220
|
+
reason: params.reason || 'Potential duplicate or unnecessary purchase',
|
|
221
|
+
estimatedCost: params.estimated_cost,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return `Flagged: ${params.url} — ${params.reason || 'potential duplicate purchase'}. This will appear in your Analytix402 dashboard for review.`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ============================================================
|
|
229
|
+
// Hooks: auto-track LLM and API calls
|
|
230
|
+
// ============================================================
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Call this after any LLM invocation to track usage.
|
|
234
|
+
* OpenClaw skills can hook into the agent lifecycle.
|
|
235
|
+
*/
|
|
236
|
+
export function trackLLMCall(params: {
|
|
237
|
+
model: string;
|
|
238
|
+
provider: string;
|
|
239
|
+
inputTokens: number;
|
|
240
|
+
outputTokens: number;
|
|
241
|
+
costUsd?: number;
|
|
242
|
+
durationMs?: number;
|
|
243
|
+
taskId?: string;
|
|
244
|
+
}): void {
|
|
245
|
+
if (!TRACK_LLM) return;
|
|
246
|
+
|
|
247
|
+
const c = getClient();
|
|
248
|
+
const cost = params.costUsd || 0;
|
|
249
|
+
|
|
250
|
+
c.trackLLM({
|
|
251
|
+
model: params.model,
|
|
252
|
+
provider: params.provider,
|
|
253
|
+
inputTokens: params.inputTokens,
|
|
254
|
+
outputTokens: params.outputTokens,
|
|
255
|
+
costUsd: params.costUsd,
|
|
256
|
+
durationMs: params.durationMs,
|
|
257
|
+
taskId: params.taskId,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
sessionSpend += cost;
|
|
261
|
+
sessionCalls += 1;
|
|
262
|
+
|
|
263
|
+
// Reset daily spend if date changed
|
|
264
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
265
|
+
if (today !== dailySpendDate) {
|
|
266
|
+
dailySpend = 0;
|
|
267
|
+
}
|
|
268
|
+
dailySpend += cost;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Call this after any API call to track spend.
|
|
273
|
+
*/
|
|
274
|
+
export function trackAPICall(params: {
|
|
275
|
+
method: string;
|
|
276
|
+
url: string;
|
|
277
|
+
statusCode: number;
|
|
278
|
+
responseTimeMs: number;
|
|
279
|
+
paymentAmount?: number;
|
|
280
|
+
paymentCurrency?: string;
|
|
281
|
+
txHash?: string;
|
|
282
|
+
}): void {
|
|
283
|
+
const c = getClient();
|
|
284
|
+
|
|
285
|
+
const payment = params.paymentAmount
|
|
286
|
+
? {
|
|
287
|
+
amount: String(params.paymentAmount),
|
|
288
|
+
currency: params.paymentCurrency || 'USDC',
|
|
289
|
+
wallet: '',
|
|
290
|
+
status: 'success' as const,
|
|
291
|
+
txHash: params.txHash,
|
|
292
|
+
}
|
|
293
|
+
: undefined;
|
|
294
|
+
|
|
295
|
+
c.track({
|
|
296
|
+
type: 'request',
|
|
297
|
+
method: params.method,
|
|
298
|
+
path: params.url,
|
|
299
|
+
endpoint: params.url,
|
|
300
|
+
statusCode: params.statusCode,
|
|
301
|
+
responseTimeMs: params.responseTimeMs,
|
|
302
|
+
timestamp: new Date().toISOString(),
|
|
303
|
+
agentId: AGENT_ID,
|
|
304
|
+
payment,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
if (params.paymentAmount) {
|
|
308
|
+
sessionSpend += params.paymentAmount;
|
|
309
|
+
dailySpend += params.paymentAmount;
|
|
310
|
+
}
|
|
311
|
+
sessionCalls += 1;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Send a heartbeat — call periodically to show agent is alive.
|
|
316
|
+
*/
|
|
317
|
+
export function sendHeartbeat(status: 'healthy' | 'degraded' | 'error' = 'healthy'): void {
|
|
318
|
+
const c = getClient();
|
|
319
|
+
c.heartbeat(status, {
|
|
320
|
+
sessionCalls,
|
|
321
|
+
sessionSpend,
|
|
322
|
+
dailySpend,
|
|
323
|
+
uptime: process.uptime(),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Gracefully shutdown — flush all pending events.
|
|
329
|
+
*/
|
|
330
|
+
export async function shutdown(): Promise<void> {
|
|
331
|
+
if (client) {
|
|
332
|
+
await client.shutdown();
|
|
333
|
+
client = null;
|
|
334
|
+
}
|
|
335
|
+
}
|