oc-inspector 1.4.0 → 1.5.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/LICENSE +21 -0
- package/README.md +26 -4
- package/bin/cli.mjs +70 -3
- package/package.json +1 -1
- package/src/autostart.mjs +379 -0
- package/src/config.mjs +3 -3
- package/src/server.mjs +3 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kirill Shidenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# oc-inspector
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/oc-inspector) [](LICENSE) [](AGENTS.md)
|
|
4
|
+
|
|
3
5
|
A debugging and monitoring tool for [OpenClaw](https://openclaw.ai) that helps you understand **where your tokens and money are going**.
|
|
4
6
|
|
|
5
7
|
When working with LLM agents you often have no visibility into what's actually happening under the hood — how many tokens each request burns, which models cost the most, what system prompts look like, how tool calls are structured. `oc-inspector` sits between OpenClaw and your LLM providers as a transparent proxy, capturing every request and response in real time so you can see the full picture: token usage, costs, message flow, thinking blocks, tool calls — everything in a clear, human-readable format.
|
|
@@ -31,7 +33,7 @@ Use it to:
|
|
|
31
33
|
npx oc-inspector
|
|
32
34
|
```
|
|
33
35
|
|
|
34
|
-
Starts the inspector daemon in background on `localhost:
|
|
36
|
+
Starts the inspector daemon in background on `localhost:3000` with a live web dashboard.
|
|
35
37
|
|
|
36
38
|
---
|
|
37
39
|
|
|
@@ -110,6 +112,24 @@ oc-inspector stop
|
|
|
110
112
|
oc-inspector restart
|
|
111
113
|
```
|
|
112
114
|
|
|
115
|
+
### Autostart (run on login/boot)
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Install autostart service (launchd on macOS, systemd on Linux)
|
|
119
|
+
oc-inspector install
|
|
120
|
+
|
|
121
|
+
# Install with custom port
|
|
122
|
+
oc-inspector install --port 9000
|
|
123
|
+
|
|
124
|
+
# Remove autostart
|
|
125
|
+
oc-inspector uninstall
|
|
126
|
+
|
|
127
|
+
# Check autostart status
|
|
128
|
+
oc-inspector status
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The inspector will start automatically when you log in (macOS) or boot (Linux), and restart if it crashes.
|
|
132
|
+
|
|
113
133
|
### CLI Commands
|
|
114
134
|
|
|
115
135
|
| Command | Description |
|
|
@@ -127,6 +147,8 @@ oc-inspector restart
|
|
|
127
147
|
| `providers` | List detected providers and their upstream URLs |
|
|
128
148
|
| `config` | Show `.inspector.json` path and contents |
|
|
129
149
|
| `logs` | Show daemon log output |
|
|
150
|
+
| `install` | Install autostart — run inspector on login/boot |
|
|
151
|
+
| `uninstall` | Remove autostart service |
|
|
130
152
|
|
|
131
153
|
### Examples
|
|
132
154
|
|
|
@@ -157,7 +179,7 @@ oc-inspector logs --lines 100
|
|
|
157
179
|
|
|
158
180
|
| Flag | Description | Default |
|
|
159
181
|
|------|-------------|---------|
|
|
160
|
-
| `--port <number>` | Port for the inspector proxy | `
|
|
182
|
+
| `--port <number>` | Port for the inspector proxy | `3000` |
|
|
161
183
|
| `--open` | Auto-open the dashboard in a browser | `false` |
|
|
162
184
|
| `--config <path>` | Custom path to `openclaw.json` | `~/.openclaw/openclaw.json` |
|
|
163
185
|
| `--json` | Output as JSON (for `stats`, `status`, `providers`, `history`, `pricing`) | `false` |
|
|
@@ -169,10 +191,10 @@ oc-inspector logs --lines 100
|
|
|
169
191
|
|
|
170
192
|
## How It Works
|
|
171
193
|
|
|
172
|
-
1. **Start** — `oc-inspector` launches a background daemon with an HTTP reverse proxy on `localhost:
|
|
194
|
+
1. **Start** — `oc-inspector` launches a background daemon with an HTTP reverse proxy on `localhost:3000`. The terminal is free immediately.
|
|
173
195
|
2. **Enable** — Click **Enable** in the web UI (or run `oc-inspector enable`). This:
|
|
174
196
|
- Backs up `openclaw.json`
|
|
175
|
-
- Rewrites each provider's `baseUrl` to route through `http://127.0.0.1:
|
|
197
|
+
- Rewrites each provider's `baseUrl` to route through `http://127.0.0.1:3000/{provider}/...`
|
|
176
198
|
- Restarts the OpenClaw gateway
|
|
177
199
|
3. **Intercept** — All LLM API traffic flows through the inspector:
|
|
178
200
|
- Requests/responses are logged in real time
|
package/bin/cli.mjs
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* logs Tail daemon log file
|
|
22
22
|
*
|
|
23
23
|
* Options:
|
|
24
|
-
* --port <number> Port for the inspector proxy (default:
|
|
24
|
+
* --port <number> Port for the inspector proxy (default: 3000)
|
|
25
25
|
* --open Auto-open the dashboard in a browser
|
|
26
26
|
* --config <path> Custom path to openclaw.json
|
|
27
27
|
* --json Output as JSON (for stats/status)
|
|
@@ -66,7 +66,7 @@ const LOG_FILE = join(INSPECTOR_DIR, "inspector.log");
|
|
|
66
66
|
function parseArgs(argv) {
|
|
67
67
|
const opts = {
|
|
68
68
|
command: "start",
|
|
69
|
-
port:
|
|
69
|
+
port: 3000,
|
|
70
70
|
open: false,
|
|
71
71
|
config: undefined,
|
|
72
72
|
json: false,
|
|
@@ -80,6 +80,7 @@ function parseArgs(argv) {
|
|
|
80
80
|
"enable", "disable", "status",
|
|
81
81
|
"stats", "providers", "history", "pricing", "config",
|
|
82
82
|
"logs", "help", "_serve",
|
|
83
|
+
"install", "uninstall",
|
|
83
84
|
]);
|
|
84
85
|
for (let i = 0; i < argv.length; i++) {
|
|
85
86
|
const arg = argv[i];
|
|
@@ -124,9 +125,11 @@ if (opts.help) {
|
|
|
124
125
|
pricing Show model pricing table
|
|
125
126
|
config Show .inspector.json path and status
|
|
126
127
|
logs Show daemon log output
|
|
128
|
+
install Install autostart (run on login/boot)
|
|
129
|
+
uninstall Remove autostart service
|
|
127
130
|
|
|
128
131
|
\x1b[1mOptions:\x1b[0m
|
|
129
|
-
--port <number> Port for the inspector proxy (default:
|
|
132
|
+
--port <number> Port for the inspector proxy (default: 3000)
|
|
130
133
|
--open Auto-open the dashboard in a browser
|
|
131
134
|
--config <path> Custom path to openclaw.json
|
|
132
135
|
--json Output as JSON (for stats, status, providers, history)
|
|
@@ -148,6 +151,8 @@ if (opts.help) {
|
|
|
148
151
|
npx oc-inspector history --days 30 # Last 30 days
|
|
149
152
|
npx oc-inspector pricing # Show pricing table
|
|
150
153
|
npx oc-inspector logs # Show daemon logs
|
|
154
|
+
npx oc-inspector install # Autostart on login/boot
|
|
155
|
+
npx oc-inspector uninstall # Remove autostart
|
|
151
156
|
`);
|
|
152
157
|
process.exit(0);
|
|
153
158
|
}
|
|
@@ -202,6 +207,12 @@ if (opts.command === "logs") {
|
|
|
202
207
|
process.exit(0);
|
|
203
208
|
}
|
|
204
209
|
|
|
210
|
+
// install / uninstall: autostart management
|
|
211
|
+
if (opts.command === "install" || opts.command === "uninstall") {
|
|
212
|
+
await runAutostart(opts);
|
|
213
|
+
process.exit(0);
|
|
214
|
+
}
|
|
215
|
+
|
|
205
216
|
// Remote commands: talk to a running inspector
|
|
206
217
|
const remoteCommands = new Set(["stats", "providers", "history", "pricing", "config"]);
|
|
207
218
|
if (remoteCommands.has(opts.command)) {
|
|
@@ -523,6 +534,49 @@ async function restoreConfigOnExit(cmdOpts = {}) {
|
|
|
523
534
|
}
|
|
524
535
|
}
|
|
525
536
|
|
|
537
|
+
// ═══════════════════════════════════════════════════════════════
|
|
538
|
+
// Autostart (install / uninstall)
|
|
539
|
+
// ═══════════════════════════════════════════════════════════════
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Handle `install` and `uninstall` commands for system autostart.
|
|
543
|
+
*
|
|
544
|
+
* Uses launchd on macOS, systemd on Linux.
|
|
545
|
+
*
|
|
546
|
+
* @param {object} opts - Parsed CLI options.
|
|
547
|
+
*/
|
|
548
|
+
async function runAutostart(opts) {
|
|
549
|
+
const { install, uninstall, autostartStatus } = await import("../src/autostart.mjs");
|
|
550
|
+
|
|
551
|
+
console.log("");
|
|
552
|
+
|
|
553
|
+
if (opts.command === "install") {
|
|
554
|
+
const result = install({ port: opts.port, config: opts.config });
|
|
555
|
+
if (result.ok) {
|
|
556
|
+
console.log(` \x1b[32m✓\x1b[0m ${result.message}`);
|
|
557
|
+
console.log(` \x1b[90m Service file: ${result.path}\x1b[0m`);
|
|
558
|
+
console.log("");
|
|
559
|
+
console.log(" Inspector will now start automatically on login.");
|
|
560
|
+
console.log(" Use \x1b[36moc-inspector uninstall\x1b[0m to remove.");
|
|
561
|
+
} else {
|
|
562
|
+
console.log(` \x1b[31m✗\x1b[0m ${result.message}`);
|
|
563
|
+
}
|
|
564
|
+
console.log("");
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (opts.command === "uninstall") {
|
|
569
|
+
const result = uninstall();
|
|
570
|
+
if (result.ok) {
|
|
571
|
+
console.log(` \x1b[32m✓\x1b[0m ${result.message}`);
|
|
572
|
+
} else {
|
|
573
|
+
console.log(` \x1b[31m✗\x1b[0m ${result.message}`);
|
|
574
|
+
}
|
|
575
|
+
console.log("");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
526
580
|
// ═══════════════════════════════════════════════════════════════
|
|
527
581
|
// Local commands (enable / disable / status)
|
|
528
582
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -557,6 +611,17 @@ async function runLocalCommand(opts) {
|
|
|
557
611
|
console.log(` Providers: ${st.providers.join(", ")}`);
|
|
558
612
|
}
|
|
559
613
|
|
|
614
|
+
// Autostart status
|
|
615
|
+
try {
|
|
616
|
+
const { autostartStatus } = await import("../src/autostart.mjs");
|
|
617
|
+
const as = autostartStatus();
|
|
618
|
+
const asDot = as.installed ? "\x1b[32m●\x1b[0m" : "\x1b[90m●\x1b[0m";
|
|
619
|
+
const asLabel = as.installed
|
|
620
|
+
? (as.running ? `\x1b[32minstalled & running\x1b[0m (${as.platform})` : `\x1b[33minstalled, not running\x1b[0m (${as.platform})`)
|
|
621
|
+
: `\x1b[90mnot installed\x1b[0m \x1b[90m(use: oc-inspector install)\x1b[0m`;
|
|
622
|
+
console.log(` ${asDot} Autostart: ${asLabel}`);
|
|
623
|
+
} catch { /* autostart module not available */ }
|
|
624
|
+
|
|
560
625
|
if (daemon.alive) {
|
|
561
626
|
console.log(`\n \x1b[90mDashboard: http://127.0.0.1:${st.port || opts.port}\x1b[0m`);
|
|
562
627
|
}
|
|
@@ -673,6 +738,8 @@ function printCommandsHelp() {
|
|
|
673
738
|
console.log(` ${C}oc-inspector providers${R} Active providers list`);
|
|
674
739
|
console.log(` ${C}oc-inspector config${R} Inspector config info`);
|
|
675
740
|
console.log(` ${C}oc-inspector logs${R} Daemon log output`);
|
|
741
|
+
console.log(` ${C}oc-inspector install${R} Autostart on login/boot`);
|
|
742
|
+
console.log(` ${C}oc-inspector uninstall${R} Remove autostart`);
|
|
676
743
|
console.log(` ${C}oc-inspector help${R} Show this help`);
|
|
677
744
|
console.log("");
|
|
678
745
|
console.log(` ${D}Use --json for machine-readable output. See oc-inspector --help for all options.${R}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oc-inspector",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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": {
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autostart management for OpenClaw Inspector.
|
|
3
|
+
*
|
|
4
|
+
* Installs/uninstalls a system service so the inspector starts
|
|
5
|
+
* automatically on login (macOS) or boot (Linux).
|
|
6
|
+
*
|
|
7
|
+
* Supported platforms:
|
|
8
|
+
* - macOS: launchd (~/Library/LaunchAgents/)
|
|
9
|
+
* - Linux: systemd user unit (~/.config/systemd/user/)
|
|
10
|
+
*
|
|
11
|
+
* @module autostart
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { writeFileSync, readFileSync, unlinkSync, existsSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { homedir, platform } from "node:os";
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
|
|
20
|
+
/** launchd plist label / systemd unit name. */
|
|
21
|
+
const SERVICE_ID = "com.openclaw.inspector";
|
|
22
|
+
|
|
23
|
+
/** systemd unit file name. */
|
|
24
|
+
const SYSTEMD_UNIT = "oc-inspector.service";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the absolute path to the CLI entry point (bin/cli.mjs).
|
|
28
|
+
*
|
|
29
|
+
* @returns {string} Absolute path to cli.mjs.
|
|
30
|
+
*/
|
|
31
|
+
function cliPath() {
|
|
32
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
33
|
+
return join(thisFile, "..", "..", "bin", "cli.mjs");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the absolute path to the current Node.js binary.
|
|
38
|
+
*
|
|
39
|
+
* @returns {string} Absolute path to `node`.
|
|
40
|
+
*/
|
|
41
|
+
function nodePath() {
|
|
42
|
+
return process.execPath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ════════════════════════════════════════════════════════════════
|
|
46
|
+
// macOS — launchd
|
|
47
|
+
// ════════════════════════════════════════════════════════════════
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Path to the launchd plist file.
|
|
51
|
+
*
|
|
52
|
+
* @returns {string} ~/Library/LaunchAgents/com.openclaw.inspector.plist
|
|
53
|
+
*/
|
|
54
|
+
function plistPath() {
|
|
55
|
+
return join(homedir(), "Library", "LaunchAgents", `${SERVICE_ID}.plist`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate a launchd plist XML string.
|
|
60
|
+
*
|
|
61
|
+
* The plist tells launchd to run `node bin/cli.mjs _serve --port <port>`
|
|
62
|
+
* at login, keep it alive (restart on crash), and log stdout/stderr.
|
|
63
|
+
*
|
|
64
|
+
* @param {object} opts
|
|
65
|
+
* @param {number} opts.port - Inspector proxy port.
|
|
66
|
+
* @param {string} [opts.config] - Custom openclaw.json path.
|
|
67
|
+
* @returns {string} Plist XML content.
|
|
68
|
+
*/
|
|
69
|
+
function buildPlist({ port, config }) {
|
|
70
|
+
const args = [nodePath(), cliPath(), "_serve", "--port", String(port)];
|
|
71
|
+
if (config) args.push("--config", config);
|
|
72
|
+
|
|
73
|
+
const logDir = join(homedir(), ".openclaw", ".inspector-runtime");
|
|
74
|
+
const stdout = join(logDir, "launchd-stdout.log");
|
|
75
|
+
const stderr = join(logDir, "launchd-stderr.log");
|
|
76
|
+
|
|
77
|
+
const argsXml = args.map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
78
|
+
|
|
79
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
80
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
81
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
82
|
+
<plist version="1.0">
|
|
83
|
+
<dict>
|
|
84
|
+
<key>Label</key>
|
|
85
|
+
<string>${SERVICE_ID}</string>
|
|
86
|
+
|
|
87
|
+
<key>ProgramArguments</key>
|
|
88
|
+
<array>
|
|
89
|
+
${argsXml}
|
|
90
|
+
</array>
|
|
91
|
+
|
|
92
|
+
<key>RunAtLoad</key>
|
|
93
|
+
<true/>
|
|
94
|
+
|
|
95
|
+
<key>KeepAlive</key>
|
|
96
|
+
<true/>
|
|
97
|
+
|
|
98
|
+
<key>StandardOutPath</key>
|
|
99
|
+
<string>${escapeXml(stdout)}</string>
|
|
100
|
+
|
|
101
|
+
<key>StandardErrorPath</key>
|
|
102
|
+
<string>${escapeXml(stderr)}</string>
|
|
103
|
+
|
|
104
|
+
<key>EnvironmentVariables</key>
|
|
105
|
+
<dict>
|
|
106
|
+
<key>PATH</key>
|
|
107
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
108
|
+
</dict>
|
|
109
|
+
</dict>
|
|
110
|
+
</plist>
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Escape special XML characters.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} s - Raw string.
|
|
118
|
+
* @returns {string} XML-safe string.
|
|
119
|
+
*/
|
|
120
|
+
function escapeXml(s) {
|
|
121
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Install the launchd agent on macOS.
|
|
126
|
+
*
|
|
127
|
+
* @param {object} opts
|
|
128
|
+
* @param {number} opts.port - Inspector port.
|
|
129
|
+
* @param {string} [opts.config] - Custom openclaw.json path.
|
|
130
|
+
* @returns {{ ok: boolean, message: string, path: string }}
|
|
131
|
+
*/
|
|
132
|
+
function installLaunchd({ port, config }) {
|
|
133
|
+
const path = plistPath();
|
|
134
|
+
const dir = join(homedir(), "Library", "LaunchAgents");
|
|
135
|
+
mkdirSync(dir, { recursive: true });
|
|
136
|
+
|
|
137
|
+
// Ensure log directory exists
|
|
138
|
+
mkdirSync(join(homedir(), ".openclaw", ".inspector-runtime"), { recursive: true });
|
|
139
|
+
|
|
140
|
+
// Unload if already loaded
|
|
141
|
+
try {
|
|
142
|
+
execSync(`launchctl unload "${path}" 2>/dev/null`, { stdio: "pipe" });
|
|
143
|
+
} catch { /* not loaded — fine */ }
|
|
144
|
+
|
|
145
|
+
// Write plist
|
|
146
|
+
const content = buildPlist({ port, config });
|
|
147
|
+
writeFileSync(path, content, "utf-8");
|
|
148
|
+
|
|
149
|
+
// Load the agent
|
|
150
|
+
try {
|
|
151
|
+
execSync(`launchctl load "${path}"`, { stdio: "pipe" });
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return { ok: false, message: `Plist written but launchctl load failed: ${err.message}`, path };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { ok: true, message: "Autostart installed (launchd)", path };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Uninstall the launchd agent on macOS.
|
|
161
|
+
*
|
|
162
|
+
* @returns {{ ok: boolean, message: string }}
|
|
163
|
+
*/
|
|
164
|
+
function uninstallLaunchd() {
|
|
165
|
+
const path = plistPath();
|
|
166
|
+
if (!existsSync(path)) {
|
|
167
|
+
return { ok: true, message: "Autostart was not installed" };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Unload
|
|
171
|
+
try {
|
|
172
|
+
execSync(`launchctl unload "${path}" 2>/dev/null`, { stdio: "pipe" });
|
|
173
|
+
} catch { /* ignore */ }
|
|
174
|
+
|
|
175
|
+
// Remove file
|
|
176
|
+
try {
|
|
177
|
+
unlinkSync(path);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
return { ok: false, message: `Failed to remove plist: ${err.message}` };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { ok: true, message: "Autostart removed (launchd)" };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check if the launchd agent is installed.
|
|
187
|
+
*
|
|
188
|
+
* @returns {{ installed: boolean, running: boolean, path: string }}
|
|
189
|
+
*/
|
|
190
|
+
function statusLaunchd() {
|
|
191
|
+
const path = plistPath();
|
|
192
|
+
const installed = existsSync(path);
|
|
193
|
+
let running = false;
|
|
194
|
+
if (installed) {
|
|
195
|
+
try {
|
|
196
|
+
const out = execSync(`launchctl list ${SERVICE_ID} 2>/dev/null`, { stdio: "pipe" }).toString();
|
|
197
|
+
running = !out.includes("Could not find");
|
|
198
|
+
} catch {
|
|
199
|
+
running = false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { installed, running, path };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ════════════════════════════════════════════════════════════════
|
|
206
|
+
// Linux — systemd user unit
|
|
207
|
+
// ════════════════════════════════════════════════════════════════
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Path to the systemd user unit file.
|
|
211
|
+
*
|
|
212
|
+
* @returns {string} ~/.config/systemd/user/oc-inspector.service
|
|
213
|
+
*/
|
|
214
|
+
function unitPath() {
|
|
215
|
+
return join(homedir(), ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Generate a systemd user unit file content.
|
|
220
|
+
*
|
|
221
|
+
* @param {object} opts
|
|
222
|
+
* @param {number} opts.port - Inspector proxy port.
|
|
223
|
+
* @param {string} [opts.config] - Custom openclaw.json path.
|
|
224
|
+
* @returns {string} Unit file content.
|
|
225
|
+
*/
|
|
226
|
+
function buildUnit({ port, config }) {
|
|
227
|
+
const args = [cliPath(), "_serve", "--port", String(port)];
|
|
228
|
+
if (config) args.push("--config", config);
|
|
229
|
+
|
|
230
|
+
return `[Unit]
|
|
231
|
+
Description=OpenClaw Inspector — LLM API traffic monitor
|
|
232
|
+
After=network.target
|
|
233
|
+
|
|
234
|
+
[Service]
|
|
235
|
+
Type=simple
|
|
236
|
+
ExecStart=${nodePath()} ${args.join(" ")}
|
|
237
|
+
Restart=on-failure
|
|
238
|
+
RestartSec=5
|
|
239
|
+
Environment=NODE_ENV=production
|
|
240
|
+
|
|
241
|
+
[Install]
|
|
242
|
+
WantedBy=default.target
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Install the systemd user service on Linux.
|
|
248
|
+
*
|
|
249
|
+
* @param {object} opts
|
|
250
|
+
* @param {number} opts.port - Inspector port.
|
|
251
|
+
* @param {string} [opts.config] - Custom openclaw.json path.
|
|
252
|
+
* @returns {{ ok: boolean, message: string, path: string }}
|
|
253
|
+
*/
|
|
254
|
+
function installSystemd({ port, config }) {
|
|
255
|
+
const path = unitPath();
|
|
256
|
+
const dir = join(homedir(), ".config", "systemd", "user");
|
|
257
|
+
mkdirSync(dir, { recursive: true });
|
|
258
|
+
|
|
259
|
+
// Write unit
|
|
260
|
+
const content = buildUnit({ port, config });
|
|
261
|
+
writeFileSync(path, content, "utf-8");
|
|
262
|
+
|
|
263
|
+
// Reload and enable
|
|
264
|
+
try {
|
|
265
|
+
execSync("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
266
|
+
execSync(`systemctl --user enable ${SYSTEMD_UNIT}`, { stdio: "pipe" });
|
|
267
|
+
execSync(`systemctl --user restart ${SYSTEMD_UNIT}`, { stdio: "pipe" });
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return { ok: false, message: `Unit written but systemctl failed: ${err.message}`, path };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return { ok: true, message: "Autostart installed (systemd)", path };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Uninstall the systemd user service on Linux.
|
|
277
|
+
*
|
|
278
|
+
* @returns {{ ok: boolean, message: string }}
|
|
279
|
+
*/
|
|
280
|
+
function uninstallSystemd() {
|
|
281
|
+
const path = unitPath();
|
|
282
|
+
if (!existsSync(path)) {
|
|
283
|
+
return { ok: true, message: "Autostart was not installed" };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
execSync(`systemctl --user stop ${SYSTEMD_UNIT} 2>/dev/null`, { stdio: "pipe" });
|
|
288
|
+
execSync(`systemctl --user disable ${SYSTEMD_UNIT} 2>/dev/null`, { stdio: "pipe" });
|
|
289
|
+
} catch { /* ignore */ }
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
unlinkSync(path);
|
|
293
|
+
execSync("systemctl --user daemon-reload", { stdio: "pipe" });
|
|
294
|
+
} catch (err) {
|
|
295
|
+
return { ok: false, message: `Failed to remove unit: ${err.message}` };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return { ok: true, message: "Autostart removed (systemd)" };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Check if the systemd user service is installed.
|
|
303
|
+
*
|
|
304
|
+
* @returns {{ installed: boolean, running: boolean, path: string }}
|
|
305
|
+
*/
|
|
306
|
+
function statusSystemd() {
|
|
307
|
+
const path = unitPath();
|
|
308
|
+
const installed = existsSync(path);
|
|
309
|
+
let running = false;
|
|
310
|
+
if (installed) {
|
|
311
|
+
try {
|
|
312
|
+
const out = execSync(`systemctl --user is-active ${SYSTEMD_UNIT} 2>/dev/null`, { stdio: "pipe" }).toString().trim();
|
|
313
|
+
running = out === "active";
|
|
314
|
+
} catch {
|
|
315
|
+
running = false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return { installed, running, path };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ════════════════════════════════════════════════════════════════
|
|
322
|
+
// Public API — platform-agnostic
|
|
323
|
+
// ════════════════════════════════════════════════════════════════
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Install autostart service for the current platform.
|
|
327
|
+
*
|
|
328
|
+
* @param {object} opts
|
|
329
|
+
* @param {number} opts.port - Inspector proxy port (default: 3000).
|
|
330
|
+
* @param {string} [opts.config] - Custom openclaw.json path.
|
|
331
|
+
* @returns {{ ok: boolean, message: string, path?: string }}
|
|
332
|
+
*
|
|
333
|
+
* @throws {Error} If the platform is not supported.
|
|
334
|
+
*
|
|
335
|
+
* Example:
|
|
336
|
+
* >>> import { install } from './autostart.mjs';
|
|
337
|
+
* >>> const result = install({ port: 3000 });
|
|
338
|
+
* >>> console.log(result.message);
|
|
339
|
+
*/
|
|
340
|
+
export function install({ port = 3000, config } = {}) {
|
|
341
|
+
const os = platform();
|
|
342
|
+
if (os === "darwin") return installLaunchd({ port, config });
|
|
343
|
+
if (os === "linux") return installSystemd({ port, config });
|
|
344
|
+
return { ok: false, message: `Unsupported platform: ${os}. Only macOS and Linux are supported.` };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Uninstall autostart service for the current platform.
|
|
349
|
+
*
|
|
350
|
+
* @returns {{ ok: boolean, message: string }}
|
|
351
|
+
*
|
|
352
|
+
* Example:
|
|
353
|
+
* >>> import { uninstall } from './autostart.mjs';
|
|
354
|
+
* >>> const result = uninstall();
|
|
355
|
+
* >>> console.log(result.message);
|
|
356
|
+
*/
|
|
357
|
+
export function uninstall() {
|
|
358
|
+
const os = platform();
|
|
359
|
+
if (os === "darwin") return uninstallLaunchd();
|
|
360
|
+
if (os === "linux") return uninstallSystemd();
|
|
361
|
+
return { ok: false, message: `Unsupported platform: ${os}. Only macOS and Linux are supported.` };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Check autostart status for the current platform.
|
|
366
|
+
*
|
|
367
|
+
* @returns {{ installed: boolean, running: boolean, path: string, platform: string }}
|
|
368
|
+
*
|
|
369
|
+
* Example:
|
|
370
|
+
* >>> import { autostartStatus } from './autostart.mjs';
|
|
371
|
+
* >>> const s = autostartStatus();
|
|
372
|
+
* >>> console.log(s.installed, s.running);
|
|
373
|
+
*/
|
|
374
|
+
export function autostartStatus() {
|
|
375
|
+
const os = platform();
|
|
376
|
+
if (os === "darwin") return { ...statusLaunchd(), platform: "launchd" };
|
|
377
|
+
if (os === "linux") return { ...statusSystemd(), platform: "systemd" };
|
|
378
|
+
return { installed: false, running: false, path: "", platform: os };
|
|
379
|
+
}
|
package/src/config.mjs
CHANGED
|
@@ -145,7 +145,7 @@ export function restartGateway() {
|
|
|
145
145
|
* @returns {{ ok: boolean, message: string, providers: string[] }}
|
|
146
146
|
*
|
|
147
147
|
* Example:
|
|
148
|
-
* >>> enable({ configPath: "~/.openclaw/openclaw.json", openclawDir: "~/.openclaw", port:
|
|
148
|
+
* >>> enable({ configPath: "~/.openclaw/openclaw.json", openclawDir: "~/.openclaw", port: 3000 })
|
|
149
149
|
* { ok: true, message: "Enabled 3 providers", providers: ["anthropic", "byteplus", "ollama"] }
|
|
150
150
|
*/
|
|
151
151
|
export function enable({ configPath, openclawDir, port }) {
|
|
@@ -260,7 +260,7 @@ export function disable({ configPath, openclawDir }) {
|
|
|
260
260
|
// Verify backup is clean (doesn't contain proxy URLs)
|
|
261
261
|
try {
|
|
262
262
|
const backupContent = readFileSync(backupPath, "utf-8");
|
|
263
|
-
if (!backupContent.includes("127.0.0.1:
|
|
263
|
+
if (!backupContent.includes("127.0.0.1:3000")) {
|
|
264
264
|
copyFileSync(backupPath, configPath);
|
|
265
265
|
removeState(openclawDir);
|
|
266
266
|
const restart = restartGateway();
|
|
@@ -331,7 +331,7 @@ function cleanProxyUrls(configPath) {
|
|
|
331
331
|
let cleaned = false;
|
|
332
332
|
|
|
333
333
|
for (const [name, cfg] of Object.entries(providers)) {
|
|
334
|
-
if (cfg.baseUrl && cfg.baseUrl.includes("127.0.0.1:
|
|
334
|
+
if (cfg.baseUrl && cfg.baseUrl.includes("127.0.0.1:3000")) {
|
|
335
335
|
if (BUILTIN_URLS[name]) {
|
|
336
336
|
// Known provider — restore builtin URL
|
|
337
337
|
cfg.baseUrl = BUILTIN_URLS[name];
|
package/src/server.mjs
CHANGED
|
@@ -21,16 +21,16 @@ import { initHistory, getRecent, getDay, listDates } from "./history.mjs";
|
|
|
21
21
|
* Start the inspector server.
|
|
22
22
|
*
|
|
23
23
|
* @param {object} options
|
|
24
|
-
* @param {number} options.port - Port to listen on (default
|
|
24
|
+
* @param {number} options.port - Port to listen on (default 3000).
|
|
25
25
|
* @param {string} [options.configPath] - Custom path to openclaw.json.
|
|
26
26
|
* @param {boolean} [options.open] - Open browser on start.
|
|
27
27
|
* @returns {Promise<{ server: http.Server, url: string, openclawDir: string }>}
|
|
28
28
|
*
|
|
29
29
|
* Example:
|
|
30
|
-
* >>> const { url } = await startServer({ port:
|
|
30
|
+
* >>> const { url } = await startServer({ port: 3000 });
|
|
31
31
|
* >>> console.log("Inspector at", url);
|
|
32
32
|
*/
|
|
33
|
-
export async function startServer({ port =
|
|
33
|
+
export async function startServer({ port = 3000, configPath, open = false }) {
|
|
34
34
|
// Detect OpenClaw
|
|
35
35
|
const oc = detect(configPath);
|
|
36
36
|
if (!oc.exists) {
|