clawtool 0.1.1 → 0.1.3

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 CHANGED
@@ -17,7 +17,7 @@ CLI options:
17
17
  Example:
18
18
 
19
19
  ```bash
20
- npm run dev -- --no-open --port 7357
20
+ npm run dev -- --no-open --port 9527
21
21
  ```
22
22
 
23
23
  ## Build
package/dist/index.js CHANGED
@@ -59,7 +59,7 @@ async function main() {
59
59
  const requestedPort = portArgIndex > -1 ? Number(process.argv[portArgIndex + 1]) : Number.NaN;
60
60
  const port = Number.isFinite(requestedPort) && requestedPort > 0
61
61
  ? requestedPort
62
- : await (0, get_port_1.default)({ port: (0, get_port_1.portNumbers)(7357, 7400) });
62
+ : await (0, get_port_1.default)({ port: (0, get_port_1.portNumbers)(9527, 9600) });
63
63
  await (0, server_1.createServer)(port);
64
64
  const url = `http://127.0.0.1:${port}`;
65
65
  console.log(`ClawTool running at ${url}`);
package/dist/loop.js CHANGED
@@ -45,17 +45,6 @@ class DoctorLoop {
45
45
  try {
46
46
  const sessionId = Date.now().toString();
47
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
48
  this.setState('observing');
60
49
  this.emit('progress', { message: 'Scanning your local OpenClaw setup...' });
61
50
  const observation = await this.deps.collectObservation();
package/dist/planner.js CHANGED
@@ -12,7 +12,7 @@ function buildPlanSteps(findings) {
12
12
  const steps = [
13
13
  {
14
14
  type: 'read',
15
- description: 'Check current gateway status',
15
+ description: 'Checking gateway runtime status',
16
16
  command: 'openclaw gateway status 2>&1',
17
17
  risk: 'low',
18
18
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawtool",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Local-first OpenClaw diagnose and repair tool",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",
package/web/index.html CHANGED
@@ -2,265 +2,307 @@
2
2
  <html lang="en" class="notranslate" translate="no">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <meta name="google" content="notranslate" />
7
7
  <title>ClawTool</title>
8
8
  <style>
9
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;
10
+ --bg: #f5f4ef;
11
+ --surface: #fffdf7;
12
+ --surface-alt: #f3efe5;
13
+ --line: #dfd9c8;
14
+ --text: #213237;
15
+ --muted: #5f7479;
16
+ --accent: #0d6c74;
17
+ --accent-soft: #d9eef0;
18
+ --ok: #1f8a63;
19
+ --warn: #b26b18;
20
+ --danger: #b84040;
21
+ --radius: 16px;
22
+ --radius-sm: 12px;
20
23
  }
24
+
21
25
  * { box-sizing: border-box; }
26
+
22
27
  body {
23
28
  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%);
29
+ min-height: 100vh;
30
+ padding: 24px 12px 80px;
31
+ font-family: 'Avenir Next', 'Trebuchet MS', 'PingFang SC', 'Noto Sans SC', sans-serif;
26
32
  color: var(--text);
27
- padding: 24px 12px 64px;
33
+ background:
34
+ radial-gradient(1000px 560px at 105% -10%, #d8ecec, transparent 60%),
35
+ radial-gradient(860px 500px at -15% 12%, #efe8d8, transparent 56%),
36
+ var(--bg);
28
37
  }
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);
38
+
39
+ .app { max-width: 640px; margin: 0 auto; }
40
+
41
+ .brand {
42
+ background: var(--surface);
43
+ border: 1px solid var(--line);
36
44
  border-radius: var(--radius);
37
- padding: 18px;
38
- box-shadow: 0 4px 24px rgba(15, 23, 42, .05);
45
+ padding: 20px 22px;
46
+ box-shadow: 0 8px 24px rgba(25, 39, 42, 0.06);
39
47
  margin-bottom: 12px;
40
48
  }
41
- .hidden { display: none; }
49
+
50
+ .brand-tag {
51
+ font-size: 11px;
52
+ letter-spacing: .12em;
53
+ text-transform: uppercase;
54
+ color: var(--muted);
55
+ margin-bottom: 8px;
56
+ }
57
+
58
+ .brand h1 {
59
+ margin: 0;
60
+ font-size: 28px;
61
+ letter-spacing: -0.02em;
62
+ }
63
+
64
+ .brand p {
65
+ margin: 8px 0 0;
66
+ color: var(--muted);
67
+ font-size: 14px;
68
+ line-height: 1.5;
69
+ }
70
+
71
+ .card {
72
+ background: var(--surface);
73
+ border: 1px solid var(--line);
74
+ border-radius: var(--radius);
75
+ box-shadow: 0 8px 24px rgba(25, 39, 42, 0.05);
76
+ padding: 16px;
77
+ margin-bottom: 10px;
78
+ animation: rise .22s ease both;
79
+ }
80
+
81
+ .hidden { display: none !important; }
82
+
42
83
  .btn {
43
84
  border: 0;
44
- border-radius: 10px;
45
- padding: 10px 14px;
46
- cursor: pointer;
85
+ border-radius: 11px;
86
+ padding: 11px 14px;
87
+ font-size: 14px;
47
88
  font-weight: 600;
89
+ cursor: pointer;
90
+ transition: transform .15s ease, opacity .15s ease;
48
91
  }
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;
92
+
93
+ .btn:hover { transform: translateY(-1px); }
94
+ .btn:active { transform: translateY(0); }
95
+
96
+ .btn-main { width: 100%; background: var(--accent); color: #fff; }
97
+ .btn-allow { background: var(--ok); color: #fff; }
98
+ .btn-skip { background: #fff5e8; color: var(--warn); border: 1px solid #efd5b1; }
99
+
100
+ .row { display: flex; gap: 8px; margin-top: 10px; }
101
+ .row .btn { flex: 1; }
102
+
103
+ .feed { margin-top: 10px; }
104
+
105
+ .step-title {
106
+ display: flex;
107
+ align-items: center;
108
+ gap: 8px;
62
109
  font-size: 14px;
110
+ font-weight: 600;
111
+ margin-bottom: 8px;
112
+ }
113
+
114
+ .badge {
115
+ display: inline-block;
116
+ padding: 2px 8px;
117
+ border-radius: 999px;
118
+ font-size: 11px;
119
+ font-weight: 700;
120
+ text-transform: uppercase;
121
+ letter-spacing: .04em;
122
+ }
123
+
124
+ .b-read { color: #29527a; background: #e7f1fb; }
125
+ .b-fix { color: #8a5316; background: #f9ecd9; }
126
+ .b-done { color: #1f6a4b; background: #dff5eb; }
127
+
128
+ .mono {
129
+ font-family: Menlo, Consolas, 'Liberation Mono', monospace;
130
+ font-size: 12px;
131
+ line-height: 1.55;
132
+ white-space: pre-wrap;
133
+ background: var(--surface-alt);
134
+ border: 1px solid var(--line);
135
+ border-radius: var(--radius-sm);
136
+ padding: 10px 11px;
137
+ color: #2f474d;
138
+ }
139
+
140
+ .hint { font-size: 13px; color: var(--muted); }
141
+ .ok { color: var(--ok); }
142
+ .err { color: var(--danger); }
143
+
144
+ @keyframes rise {
145
+ from { opacity: 0; transform: translateY(6px); }
146
+ to { opacity: 1; transform: translateY(0); }
147
+ }
148
+
149
+ @media (max-width: 520px) {
150
+ .brand h1 { font-size: 24px; }
151
+ .card { padding: 14px; }
63
152
  }
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
153
  </style>
74
154
  </head>
75
155
  <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>
156
+ <main class="app notranslate">
157
+ <section class="brand">
158
+ <div class="brand-tag" id="brand-tag">Local-First Diagnose Engine</div>
159
+ <h1>ClawTool</h1>
160
+ <p id="brand-sub">Detect and repair OpenClaw issues locally with explicit fix confirmation.</p>
161
+ </section>
162
+
163
+ <section id="start" class="card">
164
+ <div class="hint" id="start-text">No cloud call. No token required. Local execution only.</div>
165
+ <button id="btn-start" class="btn btn-main">Start Scan</button>
166
+ </section>
167
+
168
+ <section id="feed" class="feed hidden"></section>
169
+ </main>
98
170
 
99
171
  <script>
100
172
  (function () {
101
173
  const isZh = /^zh/i.test(navigator.language || '');
102
174
  const T = isZh ? {
103
- subtitle: 'OpenClaw 本地诊断与修复',
104
- privacy: 'ClawTool 仅在本地读取与执行,不上传数据。',
175
+ tag: '本地优先诊断引擎',
176
+ sub: '本地检测并修复 OpenClaw 问题,所有修复步骤都需你确认。',
177
+ startText: '不走云端,不需要 token,仅本地执行。',
105
178
  start: '开始扫描',
106
- inputTitle: '描述你的问题(可选)',
107
- inputPlaceholder: '例如:升级后 gateway 无法启动',
108
- continue: '继续',
109
- skip: '跳过',
110
- waiting: '等待输入...',
111
- confirm: '确认执行修复步骤?',
179
+ confirm: '确认执行这个修复步骤?',
112
180
  allow: '执行',
113
- reject: '跳过',
114
- done: '已完成',
115
- failed: '发生错误'
181
+ skip: '跳过',
182
+ done: '完成',
183
+ error: '发生错误'
116
184
  } : {
117
- subtitle: 'Local OpenClaw diagnostics and repair',
118
- privacy: 'ClawTool only runs local checks and local commands. Nothing is uploaded.',
185
+ tag: 'Local-First Diagnose Engine',
186
+ sub: 'Detect and repair OpenClaw issues locally with explicit fix confirmation.',
187
+ startText: 'No cloud call. No token required. Local execution only.',
119
188
  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
189
  confirm: 'Confirm this fix step?',
126
190
  allow: 'Allow',
127
- reject: 'Skip',
191
+ skip: 'Skip',
128
192
  done: 'Completed',
129
- failed: 'Error occurred'
193
+ error: 'Error occurred'
130
194
  };
131
195
 
132
196
  const el = {
133
- subtitle: document.getElementById('subtitle'),
134
- privacyText: document.getElementById('privacy-text'),
197
+ tag: document.getElementById('brand-tag'),
198
+ sub: document.getElementById('brand-sub'),
199
+ startText: document.getElementById('start-text'),
200
+ startWrap: document.getElementById('start'),
135
201
  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
202
  feed: document.getElementById('feed')
143
203
  };
144
204
 
145
- el.subtitle.textContent = T.subtitle;
146
- el.privacyText.textContent = T.privacy;
205
+ el.tag.textContent = T.tag;
206
+ el.sub.textContent = T.sub;
207
+ el.startText.textContent = T.startText;
147
208
  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
209
 
153
- let sessionId = null;
154
210
  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;
211
+ let sessionId = null;
212
+
213
+ function addCard(innerHtml) {
214
+ const card = document.createElement('article');
215
+ card.className = 'card';
216
+ card.innerHTML = innerHtml;
217
+ el.feed.appendChild(card);
218
+ return card;
219
+ }
220
+
221
+ function typeBadge(type) {
222
+ if (type === 'fix') return 'b-fix';
223
+ if (type === 'done') return 'b-done';
224
+ return 'b-read';
163
225
  }
164
226
 
165
- async function post(path, body) {
166
- await fetch(path, {
227
+ async function postConfirm(confirmed) {
228
+ if (!sessionId) return;
229
+ await fetch('/api/confirm', {
167
230
  method: 'POST',
168
231
  headers: { 'Content-Type': 'application/json' },
169
- body: JSON.stringify(body)
232
+ body: JSON.stringify({ sessionId, confirmed })
170
233
  });
171
234
  }
172
235
 
173
236
  function connect() {
174
237
  eventSource = new EventSource('/api/diagnose?lang=' + (isZh ? 'zh' : 'en'));
175
- eventSource.onmessage = async function (evt) {
238
+ eventSource.onmessage = function (evt) {
176
239
  const payload = JSON.parse(evt.data);
177
240
  const type = payload.type;
178
241
  const data = payload.data || {};
179
242
 
180
243
  if (type === 'session_start') {
181
244
  sessionId = payload.sessionId;
182
- }
183
-
184
- if (type === 'request_input') {
185
- el.inputCard.classList.remove('hidden');
186
- addCard('<div class="status">' + T.waiting + '</div>');
245
+ return;
187
246
  }
188
247
 
189
248
  if (type === 'progress') {
190
- addCard('<div>' + (data.message || '') + '</div>');
249
+ addCard('<div class="hint">' + (data.message || '') + '</div>');
250
+ return;
191
251
  }
192
252
 
193
253
  if (type === 'step_start') {
194
254
  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>' : '')
255
+ const card = addCard(
256
+ '<div class="step-title">'
257
+ + '<span>' + (step.description || 'step') + '</span>'
258
+ + '<span class="badge ' + typeBadge(step.type) + '">' + (step.type || 'read') + '</span>'
259
+ + '</div>'
260
+ + (step.type !== 'read' && step.command ? '<div class="mono" translate="no">' + step.command + '</div>' : '')
200
261
  );
201
262
 
202
263
  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>'
264
+ card.insertAdjacentHTML('beforeend',
265
+ '<div class="hint" style="margin-top:10px">' + T.confirm + '</div>'
266
+ + '<div class="row">'
267
+ + '<button class="btn btn-allow" data-confirm="yes">' + T.allow + '</button>'
268
+ + '<button class="btn btn-skip" data-confirm="no">' + T.skip + '</button>'
269
+ + '</div>'
208
270
  );
209
271
  }
272
+ return;
210
273
  }
211
274
 
212
275
  if (type === 'step_done') {
213
276
  addCard('<div class="mono" translate="no">' + (data.output || '') + '</div>');
277
+ return;
214
278
  }
215
279
 
216
280
  if (type === 'complete') {
217
- addCard('<h3>' + T.done + '</h3><p>' + (data.summary || '') + '</p>');
281
+ addCard('<h3 class="ok">' + T.done + '</h3><p>' + (data.summary || '') + '</p>');
218
282
  if (eventSource) eventSource.close();
283
+ return;
219
284
  }
220
285
 
221
286
  if (type === 'error') {
222
- addCard('<h3 style="color:var(--red)">' + T.failed + '</h3><p>' + (data.message || '') + '</p>');
287
+ addCard('<h3 class="err">' + T.error + '</h3><p>' + (data.message || '') + '</p>');
223
288
  if (eventSource) eventSource.close();
224
289
  }
225
290
  };
226
291
  }
227
292
 
228
293
  el.btnStart.addEventListener('click', function () {
229
- el.start.classList.add('hidden');
294
+ el.startWrap.classList.add('hidden');
230
295
  el.feed.classList.remove('hidden');
231
296
  connect();
232
297
  });
233
298
 
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
299
  document.addEventListener('click', async function (event) {
255
300
  const target = event.target;
256
301
  if (!(target instanceof HTMLElement)) return;
257
302
  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;
303
+ if (!choice) return;
304
+ await postConfirm(choice === 'yes');
305
+ target.closest('.row')?.remove();
264
306
  });
265
307
  })();
266
308
  </script>