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 CHANGED
@@ -1,36 +1,44 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * CLI entry point for @kshidenko/openclaw-inspector.
4
+ * CLI entry point for oc-inspector.
5
5
  *
6
6
  * Usage:
7
- * npx @kshidenko/openclaw-inspector [options]
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
- /** Parse a simple --key value or --flag argument list. */
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 === "--port" && argv[i + 1]) {
26
- opts.port = parseInt(argv[++i], 10);
27
- } else if (arg === "--open") {
28
- opts.open = true;
29
- } else if (arg === "--config" && argv[i + 1]) {
30
- opts.config = argv[++i];
31
- } else if (arg === "--help" || arg === "-h") {
32
- opts.help = true;
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
- Usage:
45
- npx oc-inspector [options]
52
+ \x1b[1mUsage:\x1b[0m
53
+ npx oc-inspector [command] [options]
46
54
 
47
- Options:
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
- Once running, open http://localhost:<port> in your browser.
54
- Click "Enable" to start intercepting OpenClaw API traffic.
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
- console.log("");
60
- console.log(" \x1b[38;5;208m🦞 OpenClaw Inspector\x1b[0m");
61
- console.log("");
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
- try {
64
- const { url, openclawDir } = await startServer({
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(" Press \x1b[1mCtrl+C\x1b[0m to stop");
111
+ console.log(" \x1b[38;5;208m🦞 OpenClaw Inspector\x1b[0m");
75
112
  console.log("");
76
- } catch (err) {
77
- console.error(`\x1b[31m ✗ Failed to start: ${err.message}\x1b[0m`);
78
- process.exit(1);
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
- // Graceful shutdown
82
- process.on("SIGINT", () => {
83
- console.log("\n Shutting down...");
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
- process.on("SIGTERM", () => {
88
- process.exit(0);
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.0.0",
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": {