clawtool 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/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # ClawTool
2
+
3
+ Local-first diagnose and repair tool for OpenClaw.
4
+
5
+ ## Run
6
+
7
+ ```bash
8
+ npm install
9
+ npm run dev
10
+ ```
11
+
12
+ CLI options:
13
+
14
+ - `--no-open`: do not auto-open browser
15
+ - `--port <number>`: force a specific port
16
+
17
+ Example:
18
+
19
+ ```bash
20
+ npm run dev -- --no-open --port 7357
21
+ ```
22
+
23
+ ## Build
24
+
25
+ ```bash
26
+ npm run build
27
+ npm start
28
+ ```
29
+
30
+ ## Health endpoint
31
+
32
+ The local server exposes:
33
+
34
+ - `GET /api/health`
35
+
36
+ ## Current scope (v1 local)
37
+
38
+ - Local observation only
39
+ - Deterministic local rules
40
+ - Guided fix confirmation before command execution
41
+ - Single-page bilingual UI (zh/en)
42
+ - No cloud API dependency
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.executeStep = executeStep;
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_util_1 = require("node:util");
9
+ const node_os_1 = __importDefault(require("node:os"));
10
+ const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
11
+ async function executeStep(step, dryRun = false) {
12
+ if (!step.command) {
13
+ return { output: '(no command)', skipped: false };
14
+ }
15
+ if (dryRun) {
16
+ return { output: '[dry-run: command not executed]', skipped: false };
17
+ }
18
+ try {
19
+ const { stdout, stderr } = await execAsync(step.command, {
20
+ timeout: 20000,
21
+ shell: node_os_1.default.platform() === 'win32' ? 'cmd.exe' : '/bin/sh',
22
+ env: { ...process.env, PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:' + (process.env.PATH || '') },
23
+ });
24
+ const output = (stdout + (stderr ? '\n' + stderr : '')).trim();
25
+ return { output: output || '(no output)', skipped: false };
26
+ }
27
+ catch (error) {
28
+ const err = error;
29
+ const merged = `${err.stdout || ''}${err.stderr || ''}`.trim();
30
+ return { output: merged || `Error: ${err.message || 'unknown'}`, skipped: false };
31
+ }
32
+ }
package/dist/index.js ADDED
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const get_port_1 = __importStar(require("get-port"));
38
+ const node_child_process_1 = require("node:child_process");
39
+ const node_util_1 = require("node:util");
40
+ const server_1 = require("./server");
41
+ const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
42
+ async function openBrowser(url) {
43
+ const platform = process.platform;
44
+ const cmd = platform === 'darwin'
45
+ ? `open "${url}"`
46
+ : platform === 'win32'
47
+ ? `start "" "${url}"`
48
+ : `xdg-open "${url}"`;
49
+ try {
50
+ await execAsync(cmd);
51
+ }
52
+ catch {
53
+ console.log(`Open browser manually: ${url}`);
54
+ }
55
+ }
56
+ async function main() {
57
+ const noOpen = process.argv.includes('--no-open');
58
+ const portArgIndex = process.argv.indexOf('--port');
59
+ const requestedPort = portArgIndex > -1 ? Number(process.argv[portArgIndex + 1]) : Number.NaN;
60
+ const port = Number.isFinite(requestedPort) && requestedPort > 0
61
+ ? requestedPort
62
+ : await (0, get_port_1.default)({ port: (0, get_port_1.portNumbers)(7357, 7400) });
63
+ await (0, server_1.createServer)(port);
64
+ const url = `http://127.0.0.1:${port}`;
65
+ console.log(`ClawTool running at ${url}`);
66
+ if (!noOpen) {
67
+ await openBrowser(url);
68
+ }
69
+ }
70
+ main().catch((error) => {
71
+ console.error('Failed to start ClawTool:', error);
72
+ process.exit(1);
73
+ });
package/dist/loop.js ADDED
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DoctorLoop = void 0;
4
+ const observe_1 = require("./observe");
5
+ const rules_1 = require("./rules");
6
+ const planner_1 = require("./planner");
7
+ const executor_1 = require("./executor");
8
+ const defaultDeps = {
9
+ collectObservation: observe_1.collectObservation,
10
+ runRules: rules_1.runRules,
11
+ buildPlanSteps: planner_1.buildPlanSteps,
12
+ executeStep: executor_1.executeStep,
13
+ };
14
+ class DoctorLoop {
15
+ constructor(callback, deps) {
16
+ this.state = 'idle';
17
+ this.stopped = false;
18
+ this.userDescription = '';
19
+ this.callback = callback;
20
+ this.deps = { ...defaultDeps, ...deps };
21
+ }
22
+ stop() {
23
+ this.stopped = true;
24
+ }
25
+ provideInput(field, value) {
26
+ if (field === 'userDescription' && this.pendingDescription) {
27
+ this.pendingDescription(value || '');
28
+ this.pendingDescription = undefined;
29
+ }
30
+ }
31
+ confirmStep(confirmed) {
32
+ if (this.pendingConfirm) {
33
+ this.pendingConfirm(confirmed);
34
+ this.pendingConfirm = undefined;
35
+ }
36
+ }
37
+ emit(type, data) {
38
+ this.callback({ type, data });
39
+ }
40
+ setState(state) {
41
+ this.state = state;
42
+ this.emit('state_change', { state });
43
+ }
44
+ async start() {
45
+ try {
46
+ const sessionId = Date.now().toString();
47
+ this.emit('session_start', { sessionId });
48
+ this.setState('waiting_user_description');
49
+ this.emit('request_input', {
50
+ field: 'userDescription',
51
+ instructions: 'Describe your issue or skip.',
52
+ allowSkip: true,
53
+ });
54
+ this.userDescription = await new Promise((resolve) => {
55
+ this.pendingDescription = resolve;
56
+ });
57
+ if (this.stopped)
58
+ return;
59
+ this.setState('observing');
60
+ this.emit('progress', { message: 'Scanning your local OpenClaw setup...' });
61
+ const observation = await this.deps.collectObservation();
62
+ const findings = this.deps.runRules(observation);
63
+ if (this.stopped)
64
+ return;
65
+ const steps = this.deps.buildPlanSteps(findings, observation);
66
+ this.setState('running');
67
+ for (let i = 0; i < steps.length; i++) {
68
+ const step = steps[i];
69
+ if (this.stopped)
70
+ return;
71
+ if (step.type === 'done') {
72
+ const warnings = step.warnings || [];
73
+ const fixed = Boolean(step.fixed);
74
+ if (fixed)
75
+ this.setState('fixed');
76
+ else if (warnings.length > 0)
77
+ this.setState('degraded');
78
+ else
79
+ this.setState('healthy');
80
+ this.emit('complete', {
81
+ fixed,
82
+ healthy: !fixed && warnings.length === 0,
83
+ degraded: !fixed && warnings.length > 0,
84
+ summary: step.summary || 'Completed',
85
+ warnings,
86
+ problem: step.problem || null,
87
+ fix: step.fix || null,
88
+ });
89
+ return;
90
+ }
91
+ this.emit('step_start', { step, index: i });
92
+ if (step.type === 'fix') {
93
+ this.setState('waiting');
94
+ this.emit('confirm_needed', { step });
95
+ const confirmed = await new Promise((resolve) => {
96
+ this.pendingConfirm = resolve;
97
+ });
98
+ this.setState('running');
99
+ if (!confirmed) {
100
+ this.emit('step_done', { step, output: '[Skipped by user]', skipped: true, index: i });
101
+ continue;
102
+ }
103
+ }
104
+ const result = await this.deps.executeStep(step);
105
+ this.emit('step_done', { step, output: result.output, skipped: result.skipped, index: i });
106
+ }
107
+ this.setState('not_fixed');
108
+ this.emit('complete', {
109
+ fixed: false,
110
+ healthy: false,
111
+ degraded: false,
112
+ summary: 'Plan ended without explicit completion step.',
113
+ warnings: [],
114
+ });
115
+ }
116
+ catch (error) {
117
+ this.setState('error');
118
+ this.emit('error', { message: error.message || 'Unknown error' });
119
+ }
120
+ }
121
+ }
122
+ exports.DoctorLoop = DoctorLoop;
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runCommand = runCommand;
7
+ exports.collectObservation = collectObservation;
8
+ const node_child_process_1 = require("node:child_process");
9
+ const node_util_1 = require("node:util");
10
+ const node_os_1 = __importDefault(require("node:os"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const node_fs_1 = __importDefault(require("node:fs"));
13
+ const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
14
+ async function runCommand(cmd, timeout = 10000) {
15
+ try {
16
+ const { stdout, stderr } = await execAsync(cmd, {
17
+ timeout,
18
+ shell: node_os_1.default.platform() === 'win32' ? 'cmd.exe' : '/bin/sh',
19
+ env: { ...process.env, PATH: '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:' + (process.env.PATH || '') },
20
+ });
21
+ return (stdout + (stderr ? '\n' + stderr : '')).trim() || '(no output)';
22
+ }
23
+ catch (error) {
24
+ const err = error;
25
+ const merged = `${err.stdout || ''}${err.stderr || ''}`.trim();
26
+ return merged || `[command failed: ${err.message || 'unknown'}]`;
27
+ }
28
+ }
29
+ function readFileIfExists(filePath, fallback) {
30
+ try {
31
+ return node_fs_1.default.existsSync(filePath) ? node_fs_1.default.readFileSync(filePath, 'utf8') : fallback;
32
+ }
33
+ catch {
34
+ return fallback;
35
+ }
36
+ }
37
+ async function collectObservation(run = runCommand) {
38
+ const homeDir = node_os_1.default.homedir();
39
+ const configPath = node_path_1.default.join(homeDir, '.openclaw', 'openclaw.json');
40
+ const plistPath = node_path_1.default.join(homeDir, 'Library', 'LaunchAgents', 'ai.openclaw.gateway.plist');
41
+ const logPath = node_path_1.default.join(homeDir, '.openclaw', 'logs', 'gateway.err.log');
42
+ const openclawVersion = await run('openclaw --version 2>&1');
43
+ const latestVersion = await run('npm view openclaw version 2>&1');
44
+ return {
45
+ timestamp: new Date().toISOString(),
46
+ openclawStatus: await run('openclaw status 2>&1'),
47
+ gatewayStatus: await run('openclaw gateway status 2>&1'),
48
+ gatewayStatusJson: await run('openclaw gateway status --json 2>&1'),
49
+ configContent: readFileIfExists(configPath, '[config file not found]'),
50
+ configPath,
51
+ portCheck: await run('lsof -i :18789 2>&1 || echo "[port not in use]"'),
52
+ processCheck: await run('ps aux | grep -i "[o]penclaw"'),
53
+ plistContent: readFileIfExists(plistPath, '[plist not found]'),
54
+ plistPath,
55
+ recentLogs: await run(`tail -n 120 ${logPath} 2>&1 || echo "[log unavailable]"`),
56
+ logPath,
57
+ nodeVersion: await run('node -v 2>&1'),
58
+ npmVersion: await run('npm -v 2>&1'),
59
+ openclawVersion,
60
+ systemInfo: await run('sw_vers 2>/dev/null || uname -a'),
61
+ homeDir,
62
+ officialDoctorOutput: await run('openclaw doctor 2>&1'),
63
+ desktopAppVersion: await run('defaults read /Applications/OpenClaw.app/Contents/Info.plist CFBundleShortVersionString 2>&1'),
64
+ desktopAppRunning: await run('pgrep -f "OpenClaw.app" 2>&1'),
65
+ devicesList: await run('openclaw devices list 2>&1'),
66
+ sessionIntegrity: await run(`ls ${node_path_1.default.join(homeDir, '.openclaw', 'sessions')} 2>&1 | head -20`),
67
+ versionGap: `current: ${openclawVersion.trim()} | latest: ${latestVersion.trim()}`,
68
+ errors: [],
69
+ };
70
+ }
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildPlanSteps = buildPlanSteps;
4
+ const severityWeight = {
5
+ critical: 0,
6
+ high: 1,
7
+ warning: 2,
8
+ info: 3,
9
+ };
10
+ function buildPlanSteps(findings) {
11
+ const sorted = [...findings].sort((a, b) => severityWeight[a.severity] - severityWeight[b.severity]);
12
+ const steps = [
13
+ {
14
+ type: 'read',
15
+ description: 'Check current gateway status',
16
+ command: 'openclaw gateway status 2>&1',
17
+ risk: 'low',
18
+ },
19
+ ];
20
+ for (const finding of sorted) {
21
+ if (finding.fix) {
22
+ steps.push({
23
+ type: 'fix',
24
+ description: finding.title,
25
+ command: finding.fix,
26
+ risk: finding.severity === 'critical' ? 'medium' : 'low',
27
+ });
28
+ }
29
+ }
30
+ if (sorted.length === 0) {
31
+ steps.push({
32
+ type: 'done',
33
+ description: 'No critical findings',
34
+ fixed: false,
35
+ summary: 'No fix was needed. System appears healthy from local checks.',
36
+ warnings: [],
37
+ problem: null,
38
+ fix: null,
39
+ });
40
+ return steps;
41
+ }
42
+ steps.push({
43
+ type: 'done',
44
+ description: 'Plan finished',
45
+ fixed: true,
46
+ summary: 'Applied local repair plan based on deterministic findings.',
47
+ warnings: [],
48
+ problem: sorted[0]?.description || null,
49
+ fix: sorted[0]?.fix || null,
50
+ });
51
+ return steps;
52
+ }
package/dist/rules.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runRules = runRules;
4
+ function runRules(obs) {
5
+ const findings = [];
6
+ const gw = (obs.gatewayStatus || '').toLowerCase();
7
+ if (gw.includes('not running')) {
8
+ findings.push({
9
+ id: 'gateway-not-running',
10
+ severity: 'critical',
11
+ title: 'Gateway is not running',
12
+ description: 'OpenClaw gateway is stopped.',
13
+ fix: 'openclaw gateway start',
14
+ });
15
+ }
16
+ if ((obs.configContent || '').includes('[config file not found]')) {
17
+ findings.push({
18
+ id: 'config-missing',
19
+ severity: 'critical',
20
+ title: 'Config file missing',
21
+ description: 'OpenClaw config file is missing.',
22
+ fix: 'openclaw doctor --yes',
23
+ });
24
+ }
25
+ if (/http_proxy|https_proxy/i.test(obs.plistContent || '')) {
26
+ findings.push({
27
+ id: 'proxy-in-plist',
28
+ severity: 'high',
29
+ title: 'Proxy set in LaunchAgent plist',
30
+ description: 'Proxy env vars may break provider connectivity.',
31
+ fix: 'openclaw gateway restart',
32
+ });
33
+ }
34
+ if (/syntaxerror/i.test(obs.recentLogs || '') && /json5/i.test(obs.recentLogs || '')) {
35
+ findings.push({
36
+ id: 'config-json5-error',
37
+ severity: 'high',
38
+ title: 'Config parse error detected in logs',
39
+ description: 'JSON5 syntax errors prevent gateway startup.',
40
+ fix: 'openclaw doctor --yes',
41
+ });
42
+ }
43
+ return findings;
44
+ }
package/dist/server.js ADDED
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createApp = createApp;
7
+ exports.createServer = createServer;
8
+ const express_1 = __importDefault(require("express"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_http_1 = __importDefault(require("node:http"));
12
+ const loop_1 = require("./loop");
13
+ function createApp() {
14
+ const app = (0, express_1.default)();
15
+ app.use(express_1.default.json({ limit: '3mb' }));
16
+ const activeSessions = new Map();
17
+ app.get('/', (_req, res) => {
18
+ const htmlPath = node_path_1.default.join(__dirname, '..', 'web', 'index.html');
19
+ if (!node_fs_1.default.existsSync(htmlPath)) {
20
+ res.status(404).send('web/index.html not found');
21
+ return;
22
+ }
23
+ res.sendFile(htmlPath);
24
+ });
25
+ app.get('/api/diagnose', (req, res) => {
26
+ const sessionId = Date.now().toString();
27
+ res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
28
+ res.setHeader('Cache-Control', 'no-cache');
29
+ res.setHeader('Connection', 'keep-alive');
30
+ res.flushHeaders();
31
+ const sendEvent = (event) => {
32
+ const payload = JSON.stringify({ type: event.type, data: event.data, sessionId });
33
+ res.write(`data: ${payload}\n\n`);
34
+ };
35
+ const loop = new loop_1.DoctorLoop((event) => sendEvent(event));
36
+ activeSessions.set(sessionId, { loop, res });
37
+ loop.start().catch((error) => {
38
+ sendEvent({ type: 'error', data: { message: error.message } });
39
+ });
40
+ req.on('close', () => {
41
+ loop.stop();
42
+ activeSessions.delete(sessionId);
43
+ try {
44
+ res.end();
45
+ }
46
+ catch {
47
+ // ignore close race
48
+ }
49
+ });
50
+ });
51
+ app.post('/api/input', (req, res) => {
52
+ const { sessionId, field, value } = req.body;
53
+ if (!sessionId || !field) {
54
+ res.status(400).json({ error: 'sessionId and field are required' });
55
+ return;
56
+ }
57
+ const session = activeSessions.get(sessionId);
58
+ if (!session) {
59
+ res.status(404).json({ error: 'Session not found' });
60
+ return;
61
+ }
62
+ session.loop.provideInput(field, value || '');
63
+ res.json({ ok: true });
64
+ });
65
+ app.post('/api/confirm', (req, res) => {
66
+ const { sessionId, confirmed } = req.body;
67
+ if (!sessionId || typeof confirmed !== 'boolean') {
68
+ res.status(400).json({ error: 'sessionId and confirmed are required' });
69
+ return;
70
+ }
71
+ const session = activeSessions.get(sessionId);
72
+ if (!session) {
73
+ res.status(404).json({ error: 'Session not found' });
74
+ return;
75
+ }
76
+ session.loop.confirmStep(confirmed);
77
+ res.json({ ok: true });
78
+ });
79
+ app.post('/api/fix', (_req, res) => {
80
+ res.json({ ok: true });
81
+ });
82
+ app.get('/api/health', (_req, res) => {
83
+ res.json({ ok: true, name: 'clawtool', sessions: activeSessions.size });
84
+ });
85
+ return app;
86
+ }
87
+ async function createServer(port) {
88
+ const app = createApp();
89
+ const server = node_http_1.default.createServer(app);
90
+ return new Promise((resolve) => {
91
+ server.listen(port, '127.0.0.1', () => resolve(server));
92
+ });
93
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "clawtool",
3
+ "version": "0.1.0",
4
+ "description": "Local-first OpenClaw diagnose and repair tool",
5
+ "type": "commonjs",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "clawtool": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "web",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "dev": "tsx src/index.ts",
18
+ "start": "node dist/index.js",
19
+ "test": "vitest run",
20
+ "prepublishOnly": "npm run test && npm run build"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "keywords": [
26
+ "openclaw",
27
+ "diagnostics",
28
+ "repair",
29
+ "local"
30
+ ],
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "express": "^4.21.2",
34
+ "get-port": "^7.1.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/express": "^4.17.23",
38
+ "@types/node": "^22.13.14",
39
+ "@types/supertest": "^7.2.0",
40
+ "supertest": "^7.2.2",
41
+ "tsx": "^4.20.5",
42
+ "typescript": "^5.8.2",
43
+ "vitest": "^3.2.4"
44
+ }
45
+ }
package/web/index.html ADDED
@@ -0,0 +1,268 @@
1
+ <!doctype html>
2
+ <html lang="en" class="notranslate" translate="no">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="google" content="notranslate" />
7
+ <title>ClawTool</title>
8
+ <style>
9
+ :root {
10
+ --bg: #f3f5f9;
11
+ --card: #ffffff;
12
+ --text: #111827;
13
+ --muted: #6b7280;
14
+ --border: #e5e7eb;
15
+ --blue: #2563eb;
16
+ --green: #059669;
17
+ --amber: #d97706;
18
+ --red: #dc2626;
19
+ --radius: 14px;
20
+ }
21
+ * { box-sizing: border-box; }
22
+ body {
23
+ margin: 0;
24
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', 'PingFang SC', 'Noto Sans SC', sans-serif;
25
+ background: radial-gradient(circle at top right, #e0ecff, #f3f5f9 35%);
26
+ color: var(--text);
27
+ padding: 24px 12px 64px;
28
+ }
29
+ .wrap { max-width: 560px; margin: 0 auto; }
30
+ .title { text-align: center; margin-bottom: 18px; }
31
+ .title h1 { margin: 0; font-size: 28px; }
32
+ .title p { margin: 8px 0 0; color: var(--muted); }
33
+ .card {
34
+ background: var(--card);
35
+ border: 1px solid var(--border);
36
+ border-radius: var(--radius);
37
+ padding: 18px;
38
+ box-shadow: 0 4px 24px rgba(15, 23, 42, .05);
39
+ margin-bottom: 12px;
40
+ }
41
+ .hidden { display: none; }
42
+ .btn {
43
+ border: 0;
44
+ border-radius: 10px;
45
+ padding: 10px 14px;
46
+ cursor: pointer;
47
+ font-weight: 600;
48
+ }
49
+ .btn-primary { background: var(--blue); color: #fff; width: 100%; }
50
+ .btn-ghost { background: #fff; border: 1px solid var(--border); }
51
+ .btn-ok { background: var(--green); color: #fff; }
52
+ .btn-skip { background: #fff7ed; color: #9a3412; border: 1px solid #fed7aa; }
53
+ textarea {
54
+ width: 100%;
55
+ min-height: 100px;
56
+ resize: vertical;
57
+ border: 1px solid var(--border);
58
+ border-radius: 10px;
59
+ padding: 10px;
60
+ margin-bottom: 10px;
61
+ font-family: inherit;
62
+ font-size: 14px;
63
+ }
64
+ .row { display: flex; gap: 8px; }
65
+ .row .btn { flex: 1; }
66
+ .feed-item { font-size: 14px; line-height: 1.6; }
67
+ .mono { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px; color: #334155; white-space: pre-wrap; }
68
+ .badge { display: inline-block; font-size: 11px; padding: 2px 8px; border-radius: 999px; margin-left: 6px; }
69
+ .b-read { background: #e0e7ff; color: #3730a3; }
70
+ .b-fix { background: #fef3c7; color: #92400e; }
71
+ .b-done { background: #dcfce7; color: #166534; }
72
+ .status { color: var(--muted); font-size: 13px; margin-top: 8px; }
73
+ </style>
74
+ </head>
75
+ <body>
76
+ <div class="wrap notranslate">
77
+ <div class="title">
78
+ <h1>🦞 ClawTool</h1>
79
+ <p id="subtitle">Local OpenClaw diagnostics and repair</p>
80
+ </div>
81
+
82
+ <div id="start" class="card">
83
+ <p id="privacy-text">ClawTool only runs local checks and local commands. Nothing is uploaded.</p>
84
+ <button id="btn-start" class="btn btn-primary">Start Scan</button>
85
+ </div>
86
+
87
+ <div id="input-card" class="card hidden">
88
+ <h3 id="input-title">Describe your issue (optional)</h3>
89
+ <textarea id="input-description" placeholder="Example: gateway not running after upgrade"></textarea>
90
+ <div class="row">
91
+ <button id="btn-submit-input" class="btn btn-ok">Continue</button>
92
+ <button id="btn-skip-input" class="btn btn-ghost">Skip</button>
93
+ </div>
94
+ </div>
95
+
96
+ <div id="feed" class="hidden"></div>
97
+ </div>
98
+
99
+ <script>
100
+ (function () {
101
+ const isZh = /^zh/i.test(navigator.language || '');
102
+ const T = isZh ? {
103
+ subtitle: 'OpenClaw 本地诊断与修复',
104
+ privacy: 'ClawTool 仅在本地读取与执行,不上传数据。',
105
+ start: '开始扫描',
106
+ inputTitle: '描述你的问题(可选)',
107
+ inputPlaceholder: '例如:升级后 gateway 无法启动',
108
+ continue: '继续',
109
+ skip: '跳过',
110
+ waiting: '等待输入...',
111
+ confirm: '确认执行修复步骤?',
112
+ allow: '执行',
113
+ reject: '跳过',
114
+ done: '已完成',
115
+ failed: '发生错误'
116
+ } : {
117
+ subtitle: 'Local OpenClaw diagnostics and repair',
118
+ privacy: 'ClawTool only runs local checks and local commands. Nothing is uploaded.',
119
+ start: 'Start Scan',
120
+ inputTitle: 'Describe your issue (optional)',
121
+ inputPlaceholder: 'Example: gateway not running after upgrade',
122
+ continue: 'Continue',
123
+ skip: 'Skip',
124
+ waiting: 'Waiting for input...',
125
+ confirm: 'Confirm this fix step?',
126
+ allow: 'Allow',
127
+ reject: 'Skip',
128
+ done: 'Completed',
129
+ failed: 'Error occurred'
130
+ };
131
+
132
+ const el = {
133
+ subtitle: document.getElementById('subtitle'),
134
+ privacyText: document.getElementById('privacy-text'),
135
+ btnStart: document.getElementById('btn-start'),
136
+ start: document.getElementById('start'),
137
+ inputCard: document.getElementById('input-card'),
138
+ inputTitle: document.getElementById('input-title'),
139
+ inputDescription: document.getElementById('input-description'),
140
+ btnSubmitInput: document.getElementById('btn-submit-input'),
141
+ btnSkipInput: document.getElementById('btn-skip-input'),
142
+ feed: document.getElementById('feed')
143
+ };
144
+
145
+ el.subtitle.textContent = T.subtitle;
146
+ el.privacyText.textContent = T.privacy;
147
+ el.btnStart.textContent = T.start;
148
+ el.inputTitle.textContent = T.inputTitle;
149
+ el.inputDescription.placeholder = T.inputPlaceholder;
150
+ el.btnSubmitInput.textContent = T.continue;
151
+ el.btnSkipInput.textContent = T.skip;
152
+
153
+ let sessionId = null;
154
+ let eventSource = null;
155
+ let pendingConfirm = null;
156
+
157
+ function addCard(html) {
158
+ const div = document.createElement('div');
159
+ div.className = 'card feed-item';
160
+ div.innerHTML = html;
161
+ el.feed.appendChild(div);
162
+ return div;
163
+ }
164
+
165
+ async function post(path, body) {
166
+ await fetch(path, {
167
+ method: 'POST',
168
+ headers: { 'Content-Type': 'application/json' },
169
+ body: JSON.stringify(body)
170
+ });
171
+ }
172
+
173
+ function connect() {
174
+ eventSource = new EventSource('/api/diagnose?lang=' + (isZh ? 'zh' : 'en'));
175
+ eventSource.onmessage = async function (evt) {
176
+ const payload = JSON.parse(evt.data);
177
+ const type = payload.type;
178
+ const data = payload.data || {};
179
+
180
+ if (type === 'session_start') {
181
+ sessionId = payload.sessionId;
182
+ }
183
+
184
+ if (type === 'request_input') {
185
+ el.inputCard.classList.remove('hidden');
186
+ addCard('<div class="status">' + T.waiting + '</div>');
187
+ }
188
+
189
+ if (type === 'progress') {
190
+ addCard('<div>' + (data.message || '') + '</div>');
191
+ }
192
+
193
+ if (type === 'step_start') {
194
+ const step = data.step || {};
195
+ const typeBadge = step.type === 'fix' ? 'b-fix' : step.type === 'done' ? 'b-done' : 'b-read';
196
+ const stepCard = addCard(
197
+ '<div><strong>' + (step.description || 'step') + '</strong>' +
198
+ '<span class="badge ' + typeBadge + '">' + (step.type || '') + '</span></div>' +
199
+ (step.command ? '<div class="mono" translate="no">' + step.command + '</div>' : '')
200
+ );
201
+
202
+ if (step.type === 'fix') {
203
+ pendingConfirm = step;
204
+ stepCard.insertAdjacentHTML('beforeend',
205
+ '<div class="status">' + T.confirm + '</div>' +
206
+ '<div class="row"><button class="btn btn-ok" data-confirm="yes">' + T.allow + '</button>' +
207
+ '<button class="btn btn-skip" data-confirm="no">' + T.reject + '</button></div>'
208
+ );
209
+ }
210
+ }
211
+
212
+ if (type === 'step_done') {
213
+ addCard('<div class="mono" translate="no">' + (data.output || '') + '</div>');
214
+ }
215
+
216
+ if (type === 'complete') {
217
+ addCard('<h3>' + T.done + '</h3><p>' + (data.summary || '') + '</p>');
218
+ if (eventSource) eventSource.close();
219
+ }
220
+
221
+ if (type === 'error') {
222
+ addCard('<h3 style="color:var(--red)">' + T.failed + '</h3><p>' + (data.message || '') + '</p>');
223
+ if (eventSource) eventSource.close();
224
+ }
225
+ };
226
+ }
227
+
228
+ el.btnStart.addEventListener('click', function () {
229
+ el.start.classList.add('hidden');
230
+ el.feed.classList.remove('hidden');
231
+ connect();
232
+ });
233
+
234
+ el.btnSubmitInput.addEventListener('click', async function () {
235
+ if (!sessionId) return;
236
+ await post('/api/input', {
237
+ sessionId,
238
+ field: 'userDescription',
239
+ value: el.inputDescription.value
240
+ });
241
+ el.inputCard.classList.add('hidden');
242
+ });
243
+
244
+ el.btnSkipInput.addEventListener('click', async function () {
245
+ if (!sessionId) return;
246
+ await post('/api/input', {
247
+ sessionId,
248
+ field: 'userDescription',
249
+ value: ''
250
+ });
251
+ el.inputCard.classList.add('hidden');
252
+ });
253
+
254
+ document.addEventListener('click', async function (event) {
255
+ const target = event.target;
256
+ if (!(target instanceof HTMLElement)) return;
257
+ const choice = target.getAttribute('data-confirm');
258
+ if (!choice || !sessionId || !pendingConfirm) return;
259
+ await post('/api/confirm', {
260
+ sessionId,
261
+ confirmed: choice === 'yes'
262
+ });
263
+ pendingConfirm = null;
264
+ });
265
+ })();
266
+ </script>
267
+ </body>
268
+ </html>