oc-inspector 1.0.0 → 1.2.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/bin/cli.mjs +323 -45
- package/package.json +1 -1
- package/src/dashboard.mjs +391 -49
- package/src/history.mjs +200 -0
- package/src/pricing.mjs +175 -0
- package/src/proxy.mjs +80 -2
- package/src/server.mjs +42 -8
package/bin/cli.mjs
CHANGED
|
@@ -1,36 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* CLI entry point for
|
|
4
|
+
* CLI entry point for oc-inspector.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* npx
|
|
7
|
+
* npx oc-inspector [command] [options]
|
|
8
|
+
*
|
|
9
|
+
* Commands:
|
|
10
|
+
* start (default) Start the inspector proxy + dashboard
|
|
11
|
+
* enable Enable interception (patch OpenClaw config)
|
|
12
|
+
* disable Disable interception (restore config)
|
|
13
|
+
* status Show interception status
|
|
14
|
+
* stats Show token usage statistics
|
|
15
|
+
* providers List detected providers
|
|
8
16
|
*
|
|
9
17
|
* Options:
|
|
10
18
|
* --port <number> Port for the inspector proxy (default: 18800)
|
|
11
19
|
* --open Auto-open the dashboard in a browser
|
|
12
20
|
* --config <path> Custom path to openclaw.json
|
|
21
|
+
* --json Output as JSON (for stats/status)
|
|
13
22
|
* --help Show help message
|
|
14
23
|
*/
|
|
15
24
|
|
|
16
|
-
import { startServer } from "../src/server.mjs";
|
|
17
|
-
|
|
18
25
|
const args = process.argv.slice(2);
|
|
19
26
|
|
|
20
|
-
/**
|
|
27
|
+
/** Simple arg parser. */
|
|
21
28
|
function parseArgs(argv) {
|
|
22
|
-
const opts = { port: 18800, open: false, config: undefined, help: false };
|
|
29
|
+
const opts = { command: "start", port: 18800, open: false, config: undefined, json: false, days: 7, help: false };
|
|
30
|
+
const commands = new Set(["start", "enable", "disable", "status", "stats", "providers", "history"]);
|
|
23
31
|
for (let i = 0; i < argv.length; i++) {
|
|
24
32
|
const arg = argv[i];
|
|
25
|
-
if (arg
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
33
|
+
if (commands.has(arg) && i === 0) { opts.command = arg; continue; }
|
|
34
|
+
if (arg === "--port" && argv[i + 1]) { opts.port = parseInt(argv[++i], 10); continue; }
|
|
35
|
+
if (arg === "--open") { opts.open = true; continue; }
|
|
36
|
+
if (arg === "--config" && argv[i + 1]) { opts.config = argv[++i]; continue; }
|
|
37
|
+
if (arg === "--json") { opts.json = true; continue; }
|
|
38
|
+
if (arg === "--days" && argv[i + 1]) { opts.days = parseInt(argv[++i], 10); continue; }
|
|
39
|
+
if (arg === "--help" || arg === "-h") { opts.help = true; continue; }
|
|
40
|
+
// First non-flag arg is command
|
|
41
|
+
if (!arg.startsWith("-") && !opts._cmdSet) { opts.command = arg; opts._cmdSet = true; }
|
|
34
42
|
}
|
|
35
43
|
return opts;
|
|
36
44
|
}
|
|
@@ -39,51 +47,321 @@ const opts = parseArgs(args);
|
|
|
39
47
|
|
|
40
48
|
if (opts.help) {
|
|
41
49
|
console.log(`
|
|
42
|
-
oc-inspector — Real-time API traffic inspector for OpenClaw
|
|
50
|
+
\x1b[38;5;208m🦞 oc-inspector\x1b[0m — Real-time API traffic inspector for OpenClaw
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
npx oc-inspector [options]
|
|
52
|
+
\x1b[1mUsage:\x1b[0m
|
|
53
|
+
npx oc-inspector [command] [options]
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
\x1b[1mCommands:\x1b[0m
|
|
56
|
+
start Start the inspector proxy + dashboard (default)
|
|
57
|
+
enable Enable interception (patches OpenClaw config)
|
|
58
|
+
disable Disable interception (restores config)
|
|
59
|
+
status Show current interception status
|
|
60
|
+
stats Show token usage statistics from running inspector
|
|
61
|
+
history Show daily usage history (persisted across restarts)
|
|
62
|
+
providers List detected providers and target URLs
|
|
63
|
+
|
|
64
|
+
\x1b[1mOptions:\x1b[0m
|
|
48
65
|
--port <number> Port for the inspector proxy (default: 18800)
|
|
49
66
|
--open Auto-open the dashboard in a browser
|
|
50
67
|
--config <path> Custom path to openclaw.json
|
|
68
|
+
--json Output as JSON (for stats, status, providers, history)
|
|
69
|
+
--days <number> Number of days to show in history (default: 7)
|
|
51
70
|
--help, -h Show this help message
|
|
52
71
|
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
\x1b[1mExamples:\x1b[0m
|
|
73
|
+
npx oc-inspector # Start inspector
|
|
74
|
+
npx oc-inspector --open # Start + open browser
|
|
75
|
+
npx oc-inspector enable # Enable interception
|
|
76
|
+
npx oc-inspector disable # Disable interception
|
|
77
|
+
npx oc-inspector stats # Show live token stats
|
|
78
|
+
npx oc-inspector stats --json # Stats as JSON
|
|
79
|
+
npx oc-inspector history # Daily history (last 7 days)
|
|
80
|
+
npx oc-inspector history --days 30 # Last 30 days
|
|
55
81
|
`);
|
|
56
82
|
process.exit(0);
|
|
57
83
|
}
|
|
58
84
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
// ── Remote commands: talk to a running inspector ──
|
|
86
|
+
const remoteCommands = new Set(["stats", "providers", "history"]);
|
|
87
|
+
if (remoteCommands.has(opts.command)) {
|
|
88
|
+
await runRemoteCommand(opts);
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Local commands: modify config directly ──
|
|
93
|
+
if (opts.command === "enable" || opts.command === "disable" || opts.command === "status") {
|
|
94
|
+
await runLocalCommand(opts);
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Start command ──
|
|
99
|
+
if (opts.command === "start") {
|
|
100
|
+
await runStart(opts);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ═══════════════════════════════════════════════════════════════
|
|
104
|
+
// Command implementations
|
|
105
|
+
// ═══════════════════════════════════════════════════════════════
|
|
62
106
|
|
|
63
|
-
|
|
64
|
-
const {
|
|
65
|
-
port: opts.port,
|
|
66
|
-
configPath: opts.config,
|
|
67
|
-
open: opts.open,
|
|
68
|
-
});
|
|
107
|
+
async function runStart(opts) {
|
|
108
|
+
const { startServer } = await import("../src/server.mjs");
|
|
69
109
|
|
|
70
|
-
console.log(` \x1b[32m✓\x1b[0m Dashboard: ${url}`);
|
|
71
|
-
console.log(` \x1b[32m✓\x1b[0m Config: ${openclawDir}/openclaw.json`);
|
|
72
|
-
console.log(` \x1b[32m✓\x1b[0m Proxy port: ${opts.port}`);
|
|
73
110
|
console.log("");
|
|
74
|
-
console.log("
|
|
111
|
+
console.log(" \x1b[38;5;208m🦞 OpenClaw Inspector\x1b[0m");
|
|
75
112
|
console.log("");
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const { url, openclawDir } = await startServer({
|
|
116
|
+
port: opts.port,
|
|
117
|
+
configPath: opts.config,
|
|
118
|
+
open: opts.open,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
console.log(` \x1b[32m✓\x1b[0m Dashboard: ${url}`);
|
|
122
|
+
console.log(` \x1b[32m✓\x1b[0m Config: ${openclawDir}/openclaw.json`);
|
|
123
|
+
console.log(` \x1b[32m✓\x1b[0m Proxy port: ${opts.port}`);
|
|
124
|
+
console.log("");
|
|
125
|
+
console.log(" Press \x1b[1mCtrl+C\x1b[0m to stop");
|
|
126
|
+
console.log("");
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error(`\x1b[31m ✗ Failed to start: ${err.message}\x1b[0m`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
process.on("SIGINT", () => { console.log("\n Shutting down..."); process.exit(0); });
|
|
133
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
79
134
|
}
|
|
80
135
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
process.exit(0);
|
|
85
|
-
});
|
|
136
|
+
async function runLocalCommand(opts) {
|
|
137
|
+
const { detect, readConfig, enable, disable, status } = await import("../src/config.mjs");
|
|
138
|
+
const oc = detect(opts.config);
|
|
86
139
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
140
|
+
if (!oc.exists) {
|
|
141
|
+
console.error(`\x1b[31m ✗ OpenClaw config not found at ${oc.configPath}\x1b[0m`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (opts.command === "status") {
|
|
146
|
+
const st = status(oc.dir);
|
|
147
|
+
if (opts.json) {
|
|
148
|
+
console.log(JSON.stringify(st, null, 2));
|
|
149
|
+
} else {
|
|
150
|
+
const dot = st.enabled ? "\x1b[32m●\x1b[0m" : "\x1b[90m●\x1b[0m";
|
|
151
|
+
const label = st.enabled ? "\x1b[32menabled\x1b[0m" : "\x1b[90mdisabled\x1b[0m";
|
|
152
|
+
console.log(`\n ${dot} Interception: ${label}`);
|
|
153
|
+
if (st.enabled) {
|
|
154
|
+
console.log(` Port: ${st.port}`);
|
|
155
|
+
console.log(` Providers: ${st.providers.join(", ")}`);
|
|
156
|
+
}
|
|
157
|
+
console.log("");
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (opts.command === "enable") {
|
|
163
|
+
console.log(" Enabling interception...");
|
|
164
|
+
const result = enable({ configPath: oc.configPath, openclawDir: oc.dir, port: opts.port });
|
|
165
|
+
if (result.ok) {
|
|
166
|
+
console.log(` \x1b[32m✓\x1b[0m ${result.message}`);
|
|
167
|
+
console.log(` Providers: ${result.providers.join(", ")}`);
|
|
168
|
+
} else {
|
|
169
|
+
console.error(` \x1b[31m✗ ${result.message}\x1b[0m`);
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (opts.command === "disable") {
|
|
175
|
+
console.log(" Disabling interception...");
|
|
176
|
+
const result = disable({ configPath: oc.configPath, openclawDir: oc.dir });
|
|
177
|
+
if (result.ok) {
|
|
178
|
+
console.log(` \x1b[32m✓\x1b[0m ${result.message}`);
|
|
179
|
+
} else {
|
|
180
|
+
console.error(` \x1b[31m✗ ${result.message}\x1b[0m`);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function runRemoteCommand(opts) {
|
|
187
|
+
const base = `http://127.0.0.1:${opts.port}`;
|
|
188
|
+
|
|
189
|
+
if (opts.command === "stats") {
|
|
190
|
+
const data = await fetchApi(`${base}/api/stats`);
|
|
191
|
+
if (!data) return;
|
|
192
|
+
if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; }
|
|
193
|
+
printStats(data);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (opts.command === "providers") {
|
|
198
|
+
const data = await fetchApi(`${base}/api/providers`);
|
|
199
|
+
if (!data) return;
|
|
200
|
+
if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; }
|
|
201
|
+
console.log(`\n \x1b[1mActive Providers\x1b[0m\n`);
|
|
202
|
+
for (const p of data.providers) {
|
|
203
|
+
console.log(` \x1b[36m${p.name.padEnd(20)}\x1b[0m ${p.url}`);
|
|
204
|
+
}
|
|
205
|
+
console.log("");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (opts.command === "history") {
|
|
210
|
+
const data = await fetchApi(`${base}/api/history?days=${opts.days}`);
|
|
211
|
+
if (!data) return;
|
|
212
|
+
if (opts.json) { console.log(JSON.stringify(data, null, 2)); return; }
|
|
213
|
+
printHistory(data.days || []);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ═══════════════════════════════════════════════════════════════
|
|
219
|
+
// Helpers
|
|
220
|
+
// ═══════════════════════════════════════════════════════════════
|
|
221
|
+
|
|
222
|
+
async function fetchApi(url) {
|
|
223
|
+
try {
|
|
224
|
+
const res = await fetch(url);
|
|
225
|
+
return await res.json();
|
|
226
|
+
} catch {
|
|
227
|
+
console.error(` \x1b[31m✗ Cannot connect to inspector at ${url}\x1b[0m`);
|
|
228
|
+
console.error(` Is the inspector running? Start with: npx oc-inspector`);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function fmtNum(n) {
|
|
234
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + "M";
|
|
235
|
+
if (n >= 1_000) return (n / 1_000).toFixed(1) + "k";
|
|
236
|
+
return String(n);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function printStats(s) {
|
|
240
|
+
const bar = (label, val, max, width = 24) => {
|
|
241
|
+
const pct = max > 0 ? Math.min(val / max, 1) : 0;
|
|
242
|
+
const filled = Math.round(pct * width);
|
|
243
|
+
const empty = width - filled;
|
|
244
|
+
return `${label} ${"█".repeat(filled)}${"░".repeat(empty)} ${fmtNum(val)}`;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
console.log("");
|
|
248
|
+
console.log(" \x1b[38;5;208m🦞 OpenClaw Inspector — Stats\x1b[0m");
|
|
249
|
+
console.log(" " + "─".repeat(50));
|
|
250
|
+
console.log("");
|
|
251
|
+
|
|
252
|
+
// Totals
|
|
253
|
+
console.log(` \x1b[1mRequests:\x1b[0m ${s.totalRequests}${s.errors > 0 ? ` (\x1b[31m${s.errors} errors\x1b[0m)` : ""}`);
|
|
254
|
+
console.log(` \x1b[1mTokens:\x1b[0m ${fmtNum(s.totalTokens)} total (in: ${fmtNum(s.totalInputTokens)} out: ${fmtNum(s.totalOutputTokens)})`);
|
|
255
|
+
if (s.totalCachedTokens > 0) {
|
|
256
|
+
console.log(` \x1b[1mCached:\x1b[0m ${fmtNum(s.totalCachedTokens)}`);
|
|
257
|
+
}
|
|
258
|
+
console.log(` \x1b[1mCost:\x1b[0m \x1b[32m$${(s.totalCost || 0).toFixed(4)}\x1b[0m`);
|
|
259
|
+
if (s.totalDuration > 0) {
|
|
260
|
+
const avgMs = Math.round(s.totalDuration / s.totalRequests);
|
|
261
|
+
console.log(` \x1b[1mAvg latency:\x1b[0m ${avgMs}ms`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// By provider
|
|
265
|
+
const providers = Object.entries(s.byProvider);
|
|
266
|
+
if (providers.length > 0) {
|
|
267
|
+
console.log("");
|
|
268
|
+
console.log(" \x1b[1mBy Provider\x1b[0m");
|
|
269
|
+
console.log(" " + "─".repeat(50));
|
|
270
|
+
const maxTokens = Math.max(...providers.map(([, v]) => v.inputTokens + v.outputTokens));
|
|
271
|
+
for (const [name, v] of providers.sort((a, b) => (b[1].cost || 0) - (a[1].cost || 0) || (b[1].inputTokens + b[1].outputTokens) - (a[1].inputTokens + a[1].outputTokens))) {
|
|
272
|
+
const total = v.inputTokens + v.outputTokens;
|
|
273
|
+
const costStr = v.cost > 0 ? ` \x1b[32m$${v.cost.toFixed(4)}\x1b[0m` : "";
|
|
274
|
+
console.log(` \x1b[36m${name.padEnd(18)}\x1b[0m ${bar("", total, maxTokens)} (${v.requests} reqs)${costStr}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// By model
|
|
279
|
+
const models = Object.entries(s.byModel);
|
|
280
|
+
if (models.length > 0) {
|
|
281
|
+
console.log("");
|
|
282
|
+
console.log(" \x1b[1mBy Model\x1b[0m");
|
|
283
|
+
console.log(" " + "─".repeat(50));
|
|
284
|
+
const maxTokens = Math.max(...models.map(([, v]) => v.inputTokens + v.outputTokens));
|
|
285
|
+
for (const [name, v] of models.sort((a, b) => (b[1].cost || 0) - (a[1].cost || 0) || (b[1].inputTokens + b[1].outputTokens) - (a[1].inputTokens + a[1].outputTokens))) {
|
|
286
|
+
const total = v.inputTokens + v.outputTokens;
|
|
287
|
+
const shortName = name.length > 28 ? name.slice(0, 27) + "…" : name;
|
|
288
|
+
const costStr = v.cost > 0 ? ` \x1b[32m$${v.cost.toFixed(4)}\x1b[0m` : "";
|
|
289
|
+
console.log(` \x1b[33m${shortName.padEnd(30)}\x1b[0m ${bar("", total, maxTokens)} (${v.requests} reqs)${costStr}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log("");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function printHistory(days) {
|
|
297
|
+
console.log("");
|
|
298
|
+
console.log(" \x1b[38;5;208m🦞 OpenClaw Inspector — History\x1b[0m");
|
|
299
|
+
console.log(" " + "═".repeat(64));
|
|
300
|
+
|
|
301
|
+
if (!days.length) {
|
|
302
|
+
console.log("\n \x1b[90mNo history data yet.\x1b[0m\n");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Summary table
|
|
307
|
+
console.log("");
|
|
308
|
+
console.log(" \x1b[1m Date Reqs Input Output Cached Cost\x1b[0m");
|
|
309
|
+
console.log(" " + "─".repeat(64));
|
|
310
|
+
|
|
311
|
+
let grandReqs = 0, grandIn = 0, grandOut = 0, grandCached = 0, grandCost = 0;
|
|
312
|
+
|
|
313
|
+
for (const d of days) {
|
|
314
|
+
const reqs = String(d.totalRequests).padStart(5);
|
|
315
|
+
const inp = fmtNum(d.totalInputTokens).padStart(8);
|
|
316
|
+
const out = fmtNum(d.totalOutputTokens).padStart(8);
|
|
317
|
+
const cached = fmtNum(d.totalCachedTokens).padStart(8);
|
|
318
|
+
const cost = ("$" + (d.totalCost || 0).toFixed(4)).padStart(9);
|
|
319
|
+
const isToday = d.date === new Date().toISOString().slice(0, 10);
|
|
320
|
+
const dateStr = isToday ? `\x1b[1m${d.date}\x1b[0m \x1b[33m⬤\x1b[0m` : `${d.date} `;
|
|
321
|
+
console.log(` ${dateStr} ${reqs} ${inp} ${out} ${cached} \x1b[32m${cost}\x1b[0m`);
|
|
322
|
+
|
|
323
|
+
grandReqs += d.totalRequests;
|
|
324
|
+
grandIn += d.totalInputTokens;
|
|
325
|
+
grandOut += d.totalOutputTokens;
|
|
326
|
+
grandCached += d.totalCachedTokens;
|
|
327
|
+
grandCost += d.totalCost || 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log(" " + "─".repeat(64));
|
|
331
|
+
const tReqs = String(grandReqs).padStart(5);
|
|
332
|
+
const tIn = fmtNum(grandIn).padStart(8);
|
|
333
|
+
const tOut = fmtNum(grandOut).padStart(8);
|
|
334
|
+
const tCached = fmtNum(grandCached).padStart(8);
|
|
335
|
+
const tCost = ("$" + grandCost.toFixed(4)).padStart(9);
|
|
336
|
+
console.log(` \x1b[1m TOTAL ${tReqs} ${tIn} ${tOut} ${tCached} \x1b[32m${tCost}\x1b[0m\x1b[0m`);
|
|
337
|
+
|
|
338
|
+
// Per-model breakdown across all days
|
|
339
|
+
const modelTotals = {};
|
|
340
|
+
for (const d of days) {
|
|
341
|
+
for (const [name, v] of Object.entries(d.byModel || {})) {
|
|
342
|
+
if (!modelTotals[name]) modelTotals[name] = { requests: 0, inputTokens: 0, outputTokens: 0, cost: 0, provider: v.provider };
|
|
343
|
+
modelTotals[name].requests += v.requests;
|
|
344
|
+
modelTotals[name].inputTokens += v.inputTokens;
|
|
345
|
+
modelTotals[name].outputTokens += v.outputTokens;
|
|
346
|
+
modelTotals[name].cost += v.cost || 0;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const models = Object.entries(modelTotals);
|
|
350
|
+
if (models.length > 0) {
|
|
351
|
+
console.log("");
|
|
352
|
+
console.log(" \x1b[1mModel Totals (all days)\x1b[0m");
|
|
353
|
+
console.log(" " + "─".repeat(64));
|
|
354
|
+
const maxCost = Math.max(...models.map(([, v]) => v.cost));
|
|
355
|
+
for (const [name, v] of models.sort((a, b) => b[1].cost - a[1].cost)) {
|
|
356
|
+
const shortName = name.length > 28 ? name.slice(0, 27) + "…" : name;
|
|
357
|
+
const pct = maxCost > 0 ? Math.min(v.cost / maxCost, 1) : 0;
|
|
358
|
+
const filled = Math.round(pct * 16);
|
|
359
|
+
const empty = 16 - filled;
|
|
360
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
361
|
+
const costStr = v.cost > 0 ? `$${v.cost.toFixed(4)}` : "$0";
|
|
362
|
+
console.log(` \x1b[33m${shortName.padEnd(30)}\x1b[0m ${bar} \x1b[32m${costStr.padStart(9)}\x1b[0m (${v.requests} reqs)`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.log("");
|
|
367
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oc-inspector",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Real-time API traffic inspector for OpenClaw — intercepts LLM provider requests, shows token usage, costs, and message flow in a live web dashboard.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|