oc-inspector 1.0.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/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # oc-inspector
2
+
3
+ Real-time API traffic inspector for [OpenClaw](https://openclaw.ai). Intercepts LLM provider requests (Anthropic, OpenAI, BytePlus, Ollama, and more), shows token usage, costs, and message flow in a live web dashboard.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx oc-inspector
9
+ ```
10
+
11
+ This starts the inspector proxy on port 18800 and opens a web dashboard.
12
+
13
+ ## Features
14
+
15
+ - **One-click enable/disable** — patches OpenClaw config automatically, no manual editing
16
+ - **All providers** — Anthropic, OpenAI, BytePlus, Ollama, Groq, Mistral, xAI, OpenRouter, and any custom provider
17
+ - **Real-time dashboard** — WebSocket-powered live view of every API request and response
18
+ - **Token tracking** — accurate token counts for streaming (SSE) and non-streaming responses
19
+ - **Cost estimation** — per-request cost based on model pricing
20
+ - **Message inspector** — view system prompts, tool calls, thinking blocks, and full conversation history
21
+ - **Zero dependencies on OpenClaw internals** — works as a standalone reverse proxy
22
+
23
+ ## Usage
24
+
25
+ ```bash
26
+ # Start with defaults (port 18800)
27
+ npx oc-inspector
28
+
29
+ # Custom port
30
+ npx oc-inspector --port 9000
31
+
32
+ # Auto-open browser
33
+ npx oc-inspector --open
34
+
35
+ # Custom OpenClaw config path
36
+ npx oc-inspector --config /path/to/openclaw.json
37
+ ```
38
+
39
+ ## How It Works
40
+
41
+ 1. The inspector starts an HTTP reverse proxy on `localhost:18800`
42
+ 2. Click **Enable** in the web UI — it patches `openclaw.json` to route all providers through the proxy and restarts the gateway
43
+ 3. All LLM API traffic flows through the inspector, which logs requests/responses and extracts token usage
44
+ 4. Click **Disable** to restore the original config
45
+
46
+ ## License
47
+
48
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI entry point for @kshidenko/openclaw-inspector.
5
+ *
6
+ * Usage:
7
+ * npx @kshidenko/openclaw-inspector [options]
8
+ *
9
+ * Options:
10
+ * --port <number> Port for the inspector proxy (default: 18800)
11
+ * --open Auto-open the dashboard in a browser
12
+ * --config <path> Custom path to openclaw.json
13
+ * --help Show help message
14
+ */
15
+
16
+ import { startServer } from "../src/server.mjs";
17
+
18
+ const args = process.argv.slice(2);
19
+
20
+ /** Parse a simple --key value or --flag argument list. */
21
+ function parseArgs(argv) {
22
+ const opts = { port: 18800, open: false, config: undefined, help: false };
23
+ for (let i = 0; i < argv.length; i++) {
24
+ 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
+ }
34
+ }
35
+ return opts;
36
+ }
37
+
38
+ const opts = parseArgs(args);
39
+
40
+ if (opts.help) {
41
+ console.log(`
42
+ oc-inspector — Real-time API traffic inspector for OpenClaw
43
+
44
+ Usage:
45
+ npx oc-inspector [options]
46
+
47
+ Options:
48
+ --port <number> Port for the inspector proxy (default: 18800)
49
+ --open Auto-open the dashboard in a browser
50
+ --config <path> Custom path to openclaw.json
51
+ --help, -h Show this help message
52
+
53
+ Once running, open http://localhost:<port> in your browser.
54
+ Click "Enable" to start intercepting OpenClaw API traffic.
55
+ `);
56
+ process.exit(0);
57
+ }
58
+
59
+ console.log("");
60
+ console.log(" \x1b[38;5;208m🦞 OpenClaw Inspector\x1b[0m");
61
+ console.log("");
62
+
63
+ try {
64
+ const { url, openclawDir } = await startServer({
65
+ port: opts.port,
66
+ configPath: opts.config,
67
+ open: opts.open,
68
+ });
69
+
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
+ console.log("");
74
+ console.log(" Press \x1b[1mCtrl+C\x1b[0m to stop");
75
+ console.log("");
76
+ } catch (err) {
77
+ console.error(`\x1b[31m ✗ Failed to start: ${err.message}\x1b[0m`);
78
+ process.exit(1);
79
+ }
80
+
81
+ // Graceful shutdown
82
+ process.on("SIGINT", () => {
83
+ console.log("\n Shutting down...");
84
+ process.exit(0);
85
+ });
86
+
87
+ process.on("SIGTERM", () => {
88
+ process.exit(0);
89
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "oc-inspector",
3
+ "version": "1.0.0",
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
+ "type": "module",
6
+ "bin": {
7
+ "oc-inspector": "./bin/cli.mjs"
8
+ },
9
+ "main": "./src/server.mjs",
10
+ "engines": {
11
+ "node": ">=18"
12
+ },
13
+ "files": [
14
+ "bin/",
15
+ "src/",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "start": "node bin/cli.mjs",
20
+ "dev": "node bin/cli.mjs --open"
21
+ },
22
+ "keywords": [
23
+ "openclaw",
24
+ "llm",
25
+ "inspector",
26
+ "proxy",
27
+ "anthropic",
28
+ "openai",
29
+ "token-usage",
30
+ "api-monitor"
31
+ ],
32
+ "author": "kshidenko",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/kshidenko/openclaw-inspector"
37
+ },
38
+ "dependencies": {
39
+ "ws": "^8.18.0"
40
+ }
41
+ }
package/src/config.mjs ADDED
@@ -0,0 +1,284 @@
1
+ /**
2
+ * OpenClaw config manager for the Inspector.
3
+ *
4
+ * Handles detection of the OpenClaw installation, reading/patching/restoring
5
+ * openclaw.json to route provider traffic through the inspector proxy,
6
+ * and restarting the gateway.
7
+ *
8
+ * @module config
9
+ */
10
+
11
+ import { readFileSync, writeFileSync, existsSync, copyFileSync, unlinkSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { homedir } from "node:os";
14
+ import { execSync } from "node:child_process";
15
+ import { BUILTIN_URLS, detectActiveProviders } from "./providers.mjs";
16
+
17
+ /** Default OpenClaw state directory. */
18
+ const DEFAULT_OPENCLAW_DIR = join(homedir(), ".openclaw");
19
+
20
+ /** Filename for the main config. */
21
+ const CONFIG_FILENAME = "openclaw.json";
22
+
23
+ /** Filename for the inspector state (stores original URLs for restore). */
24
+ const STATE_FILENAME = ".inspector-state.json";
25
+
26
+ /**
27
+ * Detect the OpenClaw directory and config file path.
28
+ *
29
+ * @param {string} [customPath] - Optional explicit path to openclaw.json.
30
+ * @returns {{ dir: string, configPath: string, exists: boolean }}
31
+ *
32
+ * Example:
33
+ * >>> detect()
34
+ * { dir: "/Users/me/.openclaw", configPath: "/Users/me/.openclaw/openclaw.json", exists: true }
35
+ */
36
+ export function detect(customPath) {
37
+ if (customPath) {
38
+ const dir = customPath.endsWith(CONFIG_FILENAME)
39
+ ? customPath.slice(0, -CONFIG_FILENAME.length - 1)
40
+ : customPath;
41
+ const configPath = customPath.endsWith(".json") ? customPath : join(customPath, CONFIG_FILENAME);
42
+ return { dir, configPath, exists: existsSync(configPath) };
43
+ }
44
+
45
+ // Check OPENCLAW_STATE_DIR env, then default
46
+ const stateDir = process.env.OPENCLAW_STATE_DIR || DEFAULT_OPENCLAW_DIR;
47
+ const configPath = join(stateDir, CONFIG_FILENAME);
48
+ return { dir: stateDir, configPath, exists: existsSync(configPath) };
49
+ }
50
+
51
+ /**
52
+ * Read and parse openclaw.json.
53
+ *
54
+ * @param {string} configPath - Full path to openclaw.json.
55
+ * @returns {object} Parsed config object.
56
+ * @throws {Error} If file cannot be read or parsed.
57
+ */
58
+ export function readConfig(configPath) {
59
+ const raw = readFileSync(configPath, "utf-8");
60
+ return JSON.parse(raw);
61
+ }
62
+
63
+ /**
64
+ * Write config object back to openclaw.json.
65
+ *
66
+ * @param {string} configPath - Full path to openclaw.json.
67
+ * @param {object} config - Config object to serialize.
68
+ */
69
+ function writeConfig(configPath, config) {
70
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
71
+ }
72
+
73
+ /**
74
+ * Read the inspector state file (stores original provider URLs).
75
+ *
76
+ * @param {string} openclawDir - Path to ~/.openclaw.
77
+ * @returns {object|null} State object or null if not found.
78
+ */
79
+ function readState(openclawDir) {
80
+ const statePath = join(openclawDir, STATE_FILENAME);
81
+ if (!existsSync(statePath)) return null;
82
+ try {
83
+ return JSON.parse(readFileSync(statePath, "utf-8"));
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Write the inspector state file.
91
+ *
92
+ * @param {string} openclawDir - Path to ~/.openclaw.
93
+ * @param {object} state - State object to persist.
94
+ */
95
+ function writeState(openclawDir, state) {
96
+ const statePath = join(openclawDir, STATE_FILENAME);
97
+ writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
98
+ }
99
+
100
+ /**
101
+ * Remove the inspector state file.
102
+ *
103
+ * @param {string} openclawDir - Path to ~/.openclaw.
104
+ */
105
+ function removeState(openclawDir) {
106
+ const statePath = join(openclawDir, STATE_FILENAME);
107
+ if (existsSync(statePath)) {
108
+ unlinkSync(statePath);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Restart the OpenClaw gateway via CLI.
114
+ *
115
+ * @returns {{ ok: boolean, output: string }}
116
+ */
117
+ export function restartGateway() {
118
+ try {
119
+ const output = execSync("openclaw gateway restart", {
120
+ encoding: "utf-8",
121
+ timeout: 15_000,
122
+ stdio: ["pipe", "pipe", "pipe"],
123
+ });
124
+ return { ok: true, output };
125
+ } catch (err) {
126
+ return { ok: false, output: err.stderr || err.message };
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Enable interception: patch openclaw.json to route all providers through
132
+ * the inspector proxy.
133
+ *
134
+ * Steps:
135
+ * 1. Create backup of openclaw.json
136
+ * 2. Detect active providers (from auth-profiles + config)
137
+ * 3. Save original URLs to .inspector-state.json
138
+ * 4. Patch config: set baseUrl to proxy for each provider
139
+ * 5. Restart gateway
140
+ *
141
+ * @param {object} params
142
+ * @param {string} params.configPath - Path to openclaw.json.
143
+ * @param {string} params.openclawDir - Path to ~/.openclaw.
144
+ * @param {number} params.port - Inspector proxy port.
145
+ * @returns {{ ok: boolean, message: string, providers: string[] }}
146
+ *
147
+ * Example:
148
+ * >>> enable({ configPath: "~/.openclaw/openclaw.json", openclawDir: "~/.openclaw", port: 18800 })
149
+ * { ok: true, message: "Enabled 3 providers", providers: ["anthropic", "byteplus", "ollama"] }
150
+ */
151
+ export function enable({ configPath, openclawDir, port }) {
152
+ const proxyBase = `http://127.0.0.1:${port}`;
153
+
154
+ // 1. Backup
155
+ const backupPath = configPath + ".inspector-backup";
156
+ copyFileSync(configPath, backupPath);
157
+
158
+ // 2. Read config
159
+ const config = readConfig(configPath);
160
+ if (!config.models) config.models = {};
161
+ if (!config.models.providers) config.models.providers = {};
162
+
163
+ const providers = config.models.providers;
164
+ const originals = {}; // provider -> original baseUrl or null (for built-in)
165
+ const enabled = [];
166
+
167
+ // 3. Patch custom providers (already have baseUrl in config)
168
+ for (const [name, cfg] of Object.entries(providers)) {
169
+ if (cfg.baseUrl && !cfg.baseUrl.startsWith(proxyBase)) {
170
+ originals[name] = { baseUrl: cfg.baseUrl, wasCustom: true };
171
+ cfg.baseUrl = `${proxyBase}/${name}`;
172
+ enabled.push(name);
173
+ }
174
+ }
175
+
176
+ // 4. Add built-in providers that have auth but no config entry yet
177
+ const active = detectActiveProviders(openclawDir);
178
+ for (const name of active) {
179
+ if (providers[name]) continue; // Already patched above
180
+ if (!BUILTIN_URLS[name]) continue; // Unknown provider
181
+
182
+ originals[name] = { baseUrl: BUILTIN_URLS[name], wasCustom: false };
183
+ providers[name] = {
184
+ baseUrl: `${proxyBase}/${name}`,
185
+ models: [],
186
+ };
187
+ enabled.push(name);
188
+ }
189
+
190
+ // 5. Save state for restore
191
+ writeState(openclawDir, {
192
+ enabled: true,
193
+ port,
194
+ timestamp: new Date().toISOString(),
195
+ originals,
196
+ backupPath,
197
+ });
198
+
199
+ // 6. Write patched config
200
+ writeConfig(configPath, config);
201
+
202
+ // 7. Restart gateway
203
+ const restart = restartGateway();
204
+
205
+ return {
206
+ ok: restart.ok,
207
+ message: restart.ok
208
+ ? `Enabled ${enabled.length} providers`
209
+ : `Config patched but gateway restart failed: ${restart.output}`,
210
+ providers: enabled,
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Disable interception: restore openclaw.json to original state.
216
+ *
217
+ * @param {object} params
218
+ * @param {string} params.configPath - Path to openclaw.json.
219
+ * @param {string} params.openclawDir - Path to ~/.openclaw.
220
+ * @returns {{ ok: boolean, message: string }}
221
+ */
222
+ export function disable({ configPath, openclawDir }) {
223
+ const state = readState(openclawDir);
224
+
225
+ if (!state || !state.originals) {
226
+ // Try restoring from backup
227
+ const backupPath = configPath + ".inspector-backup";
228
+ if (existsSync(backupPath)) {
229
+ copyFileSync(backupPath, configPath);
230
+ const restart = restartGateway();
231
+ return {
232
+ ok: restart.ok,
233
+ message: restart.ok ? "Restored from backup" : `Restored config but gateway restart failed`,
234
+ };
235
+ }
236
+ return { ok: false, message: "No inspector state found — nothing to restore" };
237
+ }
238
+
239
+ // Read current config and restore originals
240
+ const config = readConfig(configPath);
241
+ const providers = config.models?.providers || {};
242
+
243
+ for (const [name, orig] of Object.entries(state.originals)) {
244
+ if (orig.wasCustom) {
245
+ // Restore original baseUrl
246
+ if (providers[name]) {
247
+ providers[name].baseUrl = orig.baseUrl;
248
+ }
249
+ } else {
250
+ // Remove the entry we added for built-in providers
251
+ delete providers[name];
252
+ }
253
+ }
254
+
255
+ writeConfig(configPath, config);
256
+
257
+ // Clean up state file
258
+ removeState(openclawDir);
259
+
260
+ const restart = restartGateway();
261
+
262
+ return {
263
+ ok: restart.ok,
264
+ message: restart.ok ? "Disabled — config restored" : "Config restored but gateway restart failed",
265
+ };
266
+ }
267
+
268
+ /**
269
+ * Check current interception status.
270
+ *
271
+ * @param {string} openclawDir - Path to ~/.openclaw.
272
+ * @returns {{ enabled: boolean, providers: string[], port: number|null }}
273
+ */
274
+ export function status(openclawDir) {
275
+ const state = readState(openclawDir);
276
+ if (!state || !state.enabled) {
277
+ return { enabled: false, providers: [], port: null };
278
+ }
279
+ return {
280
+ enabled: true,
281
+ providers: Object.keys(state.originals || {}),
282
+ port: state.port || null,
283
+ };
284
+ }