claude-mission-control 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 +182 -0
- package/bin/claude-dashboard.js +5 -0
- package/claude-dashboard.service +11 -0
- package/com.claude-dashboard.plist +18 -0
- package/config.example.json +4 -0
- package/ignore.example.json +3 -0
- package/install.ps1 +46 -0
- package/install.sh +66 -0
- package/lib/collector.js +530 -0
- package/lib/config.js +203 -0
- package/lib/detail.js +134 -0
- package/lib/gitstatus.js +84 -0
- package/lib/history.js +103 -0
- package/lib/ignore.js +42 -0
- package/lib/names.js +54 -0
- package/lib/notify.js +62 -0
- package/lib/opener.js +104 -0
- package/lib/paths.js +39 -0
- package/lib/plan.js +58 -0
- package/lib/pricing.js +72 -0
- package/lib/quota.js +43 -0
- package/lib/registry.js +69 -0
- package/lib/search.js +170 -0
- package/lib/sessions.js +61 -0
- package/lib/tasks.js +54 -0
- package/lib/transcript-view.js +117 -0
- package/lib/transcripts.js +390 -0
- package/lib/usage.js +116 -0
- package/menubar/claude-dash.15s.sh +82 -0
- package/names.example.json +4 -0
- package/package.json +38 -0
- package/public/fonts/JetBrainsMono-Bold.woff2 +0 -0
- package/public/fonts/JetBrainsMono-Medium.woff2 +0 -0
- package/public/fonts/JetBrainsMono-Regular.woff2 +0 -0
- package/public/fonts/OFL.txt +93 -0
- package/public/fonts/Oswald-Variable.woff2 +0 -0
- package/public/icon.svg +6 -0
- package/public/index.html +2045 -0
- package/public/manifest.webmanifest +12 -0
- package/server.js +397 -0
- package/uninstall.ps1 +8 -0
- package/uninstall.sh +18 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Claude Dashboard",
|
|
3
|
+
"short_name": "Claude Dash",
|
|
4
|
+
"description": "Local dashboard for Claude Code: live sessions, transcripts, costs, git status.",
|
|
5
|
+
"start_url": "/",
|
|
6
|
+
"display": "standalone",
|
|
7
|
+
"background_color": "#0d1215",
|
|
8
|
+
"theme_color": "#0d1215",
|
|
9
|
+
"icons": [
|
|
10
|
+
{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" }
|
|
11
|
+
]
|
|
12
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Claude Projects Dashboard — local-only server.
|
|
4
|
+
// Zero dependencies; Node >= 18.
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { Collector } = require('./lib/collector');
|
|
9
|
+
const { openSession, openNewSession } = require('./lib/opener');
|
|
10
|
+
const { projectDetail } = require('./lib/detail');
|
|
11
|
+
const { sessionTranscript } = require('./lib/transcript-view');
|
|
12
|
+
const { searchHistory, searchTitles, searchTranscripts } = require('./lib/search');
|
|
13
|
+
const { sessionTitle } = require('./lib/transcripts');
|
|
14
|
+
const { friendlyName } = require('./lib/names');
|
|
15
|
+
const cfg = require('./lib/config');
|
|
16
|
+
const { isProjectMuted } = require('./lib/notify');
|
|
17
|
+
|
|
18
|
+
const PORT = Number(process.env.CLAUDE_DASH_PORT) || 4517;
|
|
19
|
+
// Default loopback-only. For remote access prefer `tailscale serve` (keeps
|
|
20
|
+
// this binding); CLAUDE_DASH_HOST is the explicit opt-out.
|
|
21
|
+
const HOST = process.env.CLAUDE_DASH_HOST || '127.0.0.1';
|
|
22
|
+
const DEV = process.env.CLAUDE_DASH_DEV === '1';
|
|
23
|
+
const INDEX = path.join(__dirname, 'public', 'index.html');
|
|
24
|
+
|
|
25
|
+
const VERSION = require('./package.json').version;
|
|
26
|
+
const collector = new Collector();
|
|
27
|
+
const sseClients = new Set();
|
|
28
|
+
|
|
29
|
+
let indexCache = null;
|
|
30
|
+
function indexHtml() {
|
|
31
|
+
if (DEV || !indexCache) indexCache = fs.readFileSync(INDEX);
|
|
32
|
+
return indexCache;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function allowedHost(req) {
|
|
36
|
+
if (HOST !== '127.0.0.1') return true; // explicitly opted into remote access
|
|
37
|
+
const h = String(req.headers.host || '');
|
|
38
|
+
return h.startsWith('127.0.0.1') || h.startsWith('localhost');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sameOrigin(req) {
|
|
42
|
+
const origin = req.headers.origin;
|
|
43
|
+
return !origin || /^http:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origin) || HOST !== '127.0.0.1';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readBody(req, res, cb) {
|
|
47
|
+
let body = '';
|
|
48
|
+
req.on('data', (c) => {
|
|
49
|
+
body += c;
|
|
50
|
+
if (body.length > 16384) req.destroy();
|
|
51
|
+
});
|
|
52
|
+
req.on('end', () => {
|
|
53
|
+
try {
|
|
54
|
+
cb(JSON.parse(body || '{}'));
|
|
55
|
+
} catch {
|
|
56
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
57
|
+
res.end('{"ok":false,"error":"bad json"}');
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function json(res, code, value) {
|
|
63
|
+
res.writeHead(code, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
64
|
+
res.end(JSON.stringify(value));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const server = http.createServer((req, res) => {
|
|
68
|
+
if (!allowedHost(req)) {
|
|
69
|
+
res.writeHead(403);
|
|
70
|
+
res.end('forbidden');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const url = req.url.split('?')[0];
|
|
74
|
+
|
|
75
|
+
if (url === '/' || url === '/index.html') {
|
|
76
|
+
res.writeHead(200, {
|
|
77
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
78
|
+
'Content-Security-Policy': "default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; img-src data:; connect-src 'self'",
|
|
79
|
+
});
|
|
80
|
+
res.end(indexHtml());
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// PWA assets, exact names only.
|
|
85
|
+
if (url === '/manifest.webmanifest' || url === '/icon.svg') {
|
|
86
|
+
const type = url === '/icon.svg' ? 'image/svg+xml' : 'application/manifest+json';
|
|
87
|
+
fs.readFile(path.join(__dirname, 'public', url.slice(1)), (err, buf) => {
|
|
88
|
+
if (err) {
|
|
89
|
+
res.writeHead(404);
|
|
90
|
+
res.end();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'max-age=86400' });
|
|
94
|
+
res.end(buf);
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Bundled font files only — no traversal, extension whitelisted.
|
|
100
|
+
if (url.startsWith('/fonts/')) {
|
|
101
|
+
const name = path.basename(url);
|
|
102
|
+
if (!/^[\w-]+\.woff2$/.test(name)) {
|
|
103
|
+
res.writeHead(404);
|
|
104
|
+
res.end();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
fs.readFile(path.join(__dirname, 'public', 'fonts', name), (err, buf) => {
|
|
108
|
+
if (err) {
|
|
109
|
+
res.writeHead(404);
|
|
110
|
+
res.end();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
res.writeHead(200, { 'Content-Type': 'font/woff2', 'Cache-Control': 'max-age=86400' });
|
|
114
|
+
res.end(buf);
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (url === '/api/state') {
|
|
120
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
121
|
+
res.end(JSON.stringify(collector.state));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (url === '/api/health') {
|
|
126
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
127
|
+
res.end(JSON.stringify({ ok: true, pid: process.pid, uptime: process.uptime(), version: VERSION }));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Manual only — the dashboard never phones home on its own. This runs
|
|
132
|
+
// when the user clicks "check for updates" in settings.
|
|
133
|
+
if (url === '/api/update-check') {
|
|
134
|
+
fetch('https://api.github.com/repos/JonImmsWordpressDev/claude-dashboard/releases/latest', {
|
|
135
|
+
headers: { 'User-Agent': 'claude-dashboard', Accept: 'application/vnd.github+json' },
|
|
136
|
+
})
|
|
137
|
+
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(`GitHub responded ${r.status}`))))
|
|
138
|
+
.then((rel) => {
|
|
139
|
+
const latest = String(rel.tag_name || '').replace(/^v/, '');
|
|
140
|
+
json(res, 200, {
|
|
141
|
+
current: VERSION,
|
|
142
|
+
latest,
|
|
143
|
+
upToDate: !latest || latest === VERSION,
|
|
144
|
+
url: rel.html_url || 'https://github.com/JonImmsWordpressDev/claude-dashboard/releases',
|
|
145
|
+
});
|
|
146
|
+
})
|
|
147
|
+
.catch((e) => json(res, 502, { error: String(e.message).slice(0, 120) }));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (url === '/api/events') {
|
|
152
|
+
res.writeHead(200, {
|
|
153
|
+
'Content-Type': 'text/event-stream',
|
|
154
|
+
'Cache-Control': 'no-cache',
|
|
155
|
+
Connection: 'keep-alive',
|
|
156
|
+
});
|
|
157
|
+
res.write('retry: 3000\n\n');
|
|
158
|
+
res.write(`event: state\ndata: ${JSON.stringify(collector.state)}\n\n`);
|
|
159
|
+
sseClients.add(res);
|
|
160
|
+
collector.setClientCount(sseClients.size);
|
|
161
|
+
req.on('close', () => {
|
|
162
|
+
sseClients.delete(res);
|
|
163
|
+
collector.setClientCount(sseClients.size);
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (url === '/api/project') {
|
|
169
|
+
// Path must be one of the known project roots — never an arbitrary path.
|
|
170
|
+
const requested = new URL(req.url, 'http://localhost').searchParams.get('path') || '';
|
|
171
|
+
const known = collector
|
|
172
|
+
.projectPaths()
|
|
173
|
+
.find((p) => p.toLowerCase() === requested.toLowerCase());
|
|
174
|
+
if (!known) {
|
|
175
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
176
|
+
res.end('{"error":"unknown project"}');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
projectDetail(known)
|
|
180
|
+
.then((detail) => {
|
|
181
|
+
detail.sessions = collector.allSessions(known);
|
|
182
|
+
detail.muted = isProjectMuted(known, cfg.readConfig().mutedProjects);
|
|
183
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
184
|
+
res.end(JSON.stringify(detail));
|
|
185
|
+
})
|
|
186
|
+
.catch((e) => {
|
|
187
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify({ error: String(e.message).slice(0, 200) }));
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (url === '/api/stats') {
|
|
194
|
+
json(res, 200, collector.statsSummary());
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (url === '/api/config' && req.method === 'GET') {
|
|
199
|
+
json(res, 200, {
|
|
200
|
+
...cfg.readConfig(),
|
|
201
|
+
version: VERSION,
|
|
202
|
+
errors: collector.state.errors || [],
|
|
203
|
+
terminals: cfg.detectTerminals(),
|
|
204
|
+
resolvedTerminal: cfg.resolvedTerminal(),
|
|
205
|
+
claudeApp: cfg.detectClaudeApp(),
|
|
206
|
+
names: cfg.readNames(),
|
|
207
|
+
ignores: cfg.readIgnores(),
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (req.method === 'POST' && ['/api/config', '/api/names', '/api/ignore'].includes(url)) {
|
|
213
|
+
if (!sameOrigin(req)) {
|
|
214
|
+
json(res, 403, { ok: false, error: 'forbidden' });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
readBody(req, res, (payload) => {
|
|
218
|
+
try {
|
|
219
|
+
if (url === '/api/config') {
|
|
220
|
+
if (payload.pinSession !== undefined) {
|
|
221
|
+
const id = String(payload.pinSession || '');
|
|
222
|
+
if (!collector.findSessionFile(id)) return json(res, 404, { ok: false, error: 'unknown session' });
|
|
223
|
+
const pinned = cfg.togglePin(id);
|
|
224
|
+
collector.assemble();
|
|
225
|
+
return json(res, 200, { ok: true, pinned });
|
|
226
|
+
}
|
|
227
|
+
if (payload.mutePath !== undefined) {
|
|
228
|
+
const known = collector
|
|
229
|
+
.projectPaths()
|
|
230
|
+
.find((p) => p.toLowerCase() === String(payload.mutePath || '').toLowerCase());
|
|
231
|
+
if (!known) return json(res, 404, { ok: false, error: 'unknown project' });
|
|
232
|
+
cfg.setProjectMuted(known, payload.muted !== false);
|
|
233
|
+
} else {
|
|
234
|
+
cfg.updateConfig(payload);
|
|
235
|
+
}
|
|
236
|
+
} else if (url === '/api/names') {
|
|
237
|
+
const known = collector
|
|
238
|
+
.projectPaths()
|
|
239
|
+
.find((p) => p.toLowerCase() === String(payload.path || '').toLowerCase());
|
|
240
|
+
if (!known) return json(res, 404, { ok: false, error: 'unknown project' });
|
|
241
|
+
cfg.setName(known, String(payload.name || ''));
|
|
242
|
+
} else if (payload.remove) {
|
|
243
|
+
cfg.removeIgnore(String(payload.path || ''));
|
|
244
|
+
} else {
|
|
245
|
+
const known = collector
|
|
246
|
+
.projectPaths()
|
|
247
|
+
.find((p) => p.toLowerCase() === String(payload.path || '').toLowerCase());
|
|
248
|
+
if (!known) return json(res, 404, { ok: false, error: 'unknown project' });
|
|
249
|
+
cfg.addIgnore(known);
|
|
250
|
+
}
|
|
251
|
+
collector.assemble(); // reflect the change on the next state push
|
|
252
|
+
json(res, 200, { ok: true });
|
|
253
|
+
} catch (e) {
|
|
254
|
+
json(res, 500, { ok: false, error: String(e.message).slice(0, 200) });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (url === '/api/session') {
|
|
261
|
+
const params = new URL(req.url, 'http://localhost').searchParams;
|
|
262
|
+
const id = params.get('id') || '';
|
|
263
|
+
const after = Math.max(0, Number(params.get('after')) || 0);
|
|
264
|
+
const found = collector.findSessionFile(id);
|
|
265
|
+
if (!found) {
|
|
266
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
267
|
+
res.end('{"error":"unknown session"}');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
sessionTranscript(found.file, after)
|
|
271
|
+
.then((t) => {
|
|
272
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
273
|
+
res.end(JSON.stringify({ sessionId: id, title: found.title, projectName: found.projectName, ...t }));
|
|
274
|
+
})
|
|
275
|
+
.catch((e) => {
|
|
276
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
277
|
+
res.end(JSON.stringify({ error: String(e.message).slice(0, 200) }));
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (url === '/api/search') {
|
|
283
|
+
const q = (new URL(req.url, 'http://localhost').searchParams.get('q') || '').trim();
|
|
284
|
+
if (q.length < 2) {
|
|
285
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
286
|
+
res.end('{"error":"query too short"}');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const deep = new URL(req.url, 'http://localhost').searchParams.get('deep') === '1';
|
|
290
|
+
Promise.all([
|
|
291
|
+
searchHistory(q),
|
|
292
|
+
deep ? searchTranscripts(q, collector.raw.transcriptGroups) : Promise.resolve(null),
|
|
293
|
+
])
|
|
294
|
+
.then(([prompts, transcripts]) => {
|
|
295
|
+
const titles = searchTitles(q, collector.raw.transcriptGroups, sessionTitle);
|
|
296
|
+
for (const r of prompts) r.projectName = friendlyName(r.project);
|
|
297
|
+
for (const r of titles) r.projectName = friendlyName(r.project);
|
|
298
|
+
if (transcripts) for (const r of transcripts.matches) r.projectName = friendlyName(r.project);
|
|
299
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
300
|
+
res.end(JSON.stringify({ q, prompts, titles, transcripts }));
|
|
301
|
+
})
|
|
302
|
+
.catch((e) => {
|
|
303
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
304
|
+
res.end(JSON.stringify({ error: String(e.message).slice(0, 200) }));
|
|
305
|
+
});
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (url === '/api/open' && req.method === 'POST') {
|
|
310
|
+
// Same-origin only: browsers always send Origin on cross-origin POSTs.
|
|
311
|
+
const origin = req.headers.origin;
|
|
312
|
+
if (origin && !/^http:\/\/(127\.0\.0\.1|localhost)(:\d+)?$/.test(origin)) {
|
|
313
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
314
|
+
res.end('{"ok":false,"error":"forbidden"}');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
let body = '';
|
|
318
|
+
req.on('data', (c) => {
|
|
319
|
+
body += c;
|
|
320
|
+
if (body.length > 4096) req.destroy();
|
|
321
|
+
});
|
|
322
|
+
req.on('end', async () => {
|
|
323
|
+
let payload;
|
|
324
|
+
try {
|
|
325
|
+
payload = JSON.parse(body);
|
|
326
|
+
} catch {
|
|
327
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
328
|
+
res.end('{"ok":false,"error":"bad json"}');
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// Fresh session in a known project dir (terminal only).
|
|
332
|
+
if (payload.newSession) {
|
|
333
|
+
const known = collector
|
|
334
|
+
.projectPaths()
|
|
335
|
+
.find((p) => p.toLowerCase() === String(payload.projectPath || '').toLowerCase());
|
|
336
|
+
if (!known) {
|
|
337
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
338
|
+
res.end('{"ok":false,"error":"unknown project"}');
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const result = await openNewSession(known);
|
|
342
|
+
res.writeHead(result.ok ? 200 : 500, { 'Content-Type': 'application/json' });
|
|
343
|
+
res.end(JSON.stringify(result));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const found = collector.findSession(String(payload.sessionId || ''));
|
|
348
|
+
if (!found) {
|
|
349
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
350
|
+
res.end('{"ok":false,"error":"unknown session"}');
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (found.live && payload.target === 'terminal') {
|
|
354
|
+
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
355
|
+
res.end('{"ok":false,"error":"session is already running"}');
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const result = await openSession({
|
|
359
|
+
sessionId: String(payload.sessionId),
|
|
360
|
+
cwd: found.cwd,
|
|
361
|
+
target: String(payload.target || ''),
|
|
362
|
+
});
|
|
363
|
+
res.writeHead(result.ok ? 200 : 500, { 'Content-Type': 'application/json' });
|
|
364
|
+
res.end(JSON.stringify(result));
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
370
|
+
res.end('{"error":"not found"}');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
collector.onChange((state) => {
|
|
374
|
+
const payload = `event: state\ndata: ${JSON.stringify(state)}\n\n`;
|
|
375
|
+
for (const res of sseClients) res.write(payload);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const pingTimer = setInterval(() => {
|
|
379
|
+
for (const res of sseClients) res.write(': ping\n\n');
|
|
380
|
+
}, 25_000);
|
|
381
|
+
pingTimer.unref();
|
|
382
|
+
|
|
383
|
+
collector.start().then(() => {
|
|
384
|
+
server.listen(PORT, HOST, () => {
|
|
385
|
+
console.log(`claude-dashboard listening on http://${HOST}:${PORT}`);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
server.on('error', (err) => {
|
|
390
|
+
console.error(`server error: ${err.message}`);
|
|
391
|
+
process.exit(1);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
process.on('SIGTERM', () => {
|
|
395
|
+
server.close(() => process.exit(0));
|
|
396
|
+
setTimeout(() => process.exit(0), 2000).unref();
|
|
397
|
+
});
|
package/uninstall.ps1
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Remove the Claude Dashboard Scheduled Task (Windows).
|
|
2
|
+
schtasks /End /TN 'ClaudeDashboard' 2>$null | Out-Null
|
|
3
|
+
schtasks /Delete /TN 'ClaudeDashboard' /F 2>$null | Out-Null
|
|
4
|
+
Get-Process -Name node -ErrorAction SilentlyContinue |
|
|
5
|
+
Where-Object { $_.Path -and $_.CommandLine -match 'claude-dashboard' } |
|
|
6
|
+
Stop-Process -ErrorAction SilentlyContinue
|
|
7
|
+
Remove-Item -Path (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'run-hidden.vbs') -ErrorAction SilentlyContinue
|
|
8
|
+
Write-Host 'OK: claude-dashboard scheduled task removed.'
|
package/uninstall.sh
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Remove the Claude Dashboard service (LaunchAgent on macOS, systemd user unit on Linux).
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
LABEL="com.claude-dashboard"
|
|
6
|
+
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
|
|
7
|
+
|
|
8
|
+
if [ "$(uname -s)" = "Linux" ]; then
|
|
9
|
+
systemctl --user disable --now claude-dashboard.service 2>/dev/null || true
|
|
10
|
+
rm -f "$HOME/.config/systemd/user/claude-dashboard.service"
|
|
11
|
+
systemctl --user daemon-reload
|
|
12
|
+
echo "✓ claude-dashboard systemd unit removed. (Repo left in place.)"
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
launchctl bootout "gui/$(id -u)/$LABEL" 2>/dev/null || true
|
|
17
|
+
rm -f "$PLIST"
|
|
18
|
+
echo "✓ claude-dashboard LaunchAgent removed. (Repo and logs left in place.)"
|