@zzusp/ccsm 1.0.1 → 1.0.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/LICENSE +21 -21
- package/README.md +236 -236
- package/bin/cli.mjs +52 -52
- package/dist/assets/{DiskUsage-CKhggLs5.js → DiskUsage-BY6XwffG.js} +2 -2
- package/dist/assets/DiskUsage-BY6XwffG.js.map +1 -0
- package/dist/assets/{ImportPage-wge4VhZ-.js → ImportPage-Cwq5bx7G.js} +2 -2
- package/dist/assets/ImportPage-Cwq5bx7G.js.map +1 -0
- package/dist/assets/MarkdownContent-BFu7Nkk_.js +2 -0
- package/dist/assets/MarkdownContent-BFu7Nkk_.js.map +1 -0
- package/dist/assets/{ProjectMemory-Q4XX40j_.js → ProjectMemory-CcE3KbUK.js} +2 -2
- package/dist/assets/ProjectMemory-CcE3KbUK.js.map +1 -0
- package/dist/assets/index-CrWxV6sb.css +1 -0
- package/dist/assets/index-DTbWl1jb.js +11 -0
- package/dist/assets/index-DTbWl1jb.js.map +1 -0
- package/dist/assets/markdown-Bag5rX3T.js +30 -0
- package/dist/assets/markdown-Bag5rX3T.js.map +1 -0
- package/dist/index.html +26 -26
- package/package.json +81 -83
- package/server/index.ts +130 -130
- package/server/lib/active-sessions.test.ts +119 -119
- package/server/lib/active-sessions.ts +95 -95
- package/server/lib/bundle.test.ts +182 -182
- package/server/lib/bundle.ts +86 -86
- package/server/lib/claude-paths.test.ts +126 -126
- package/server/lib/claude-paths.ts +43 -43
- package/server/lib/cleanup-suggestions.ts +131 -131
- package/server/lib/constants.ts +8 -8
- package/server/lib/delete-project.ts +100 -100
- package/server/lib/delete.test.ts +244 -244
- package/server/lib/delete.ts +192 -192
- package/server/lib/disk-usage.ts +81 -81
- package/server/lib/encode-cwd.ts +24 -24
- package/server/lib/export-bundle.ts +236 -236
- package/server/lib/export-import-bundle.test.ts +337 -337
- package/server/lib/fs-size.ts +38 -38
- package/server/lib/import-bundle.ts +488 -488
- package/server/lib/load-memory.ts +120 -120
- package/server/lib/load-session.ts +209 -209
- package/server/lib/modified-files.test.ts +280 -280
- package/server/lib/modified-files.ts +228 -228
- package/server/lib/open-folder.ts +47 -47
- package/server/lib/parse-jsonl.ts +160 -139
- package/server/lib/port.ts +23 -23
- package/server/lib/safe-id.test.ts +41 -41
- package/server/lib/safe-id.ts +6 -6
- package/server/lib/safe-remove.test.ts +73 -73
- package/server/lib/safe-remove.ts +25 -25
- package/server/lib/scan.ts +289 -286
- package/server/lib/search-all.ts +130 -130
- package/server/lib/search-session.ts +203 -203
- package/server/lib/system-tags.ts +20 -20
- package/server/lib/update.ts +67 -67
- package/server/lib/version.test.ts +39 -39
- package/server/lib/version.ts +117 -117
- package/server/routes/disk-cleanup.ts +54 -54
- package/server/routes/disk.ts +9 -9
- package/server/routes/import.ts +87 -87
- package/server/routes/projects.ts +104 -104
- package/server/routes/search.ts +79 -79
- package/server/routes/sessions.ts +130 -130
- package/server/routes/version.ts +34 -34
- package/server/types.ts +1 -1
- package/shared/constants.ts +7 -7
- package/shared/types.ts +513 -511
- package/dist/assets/DiskUsage-CKhggLs5.js.map +0 -1
- package/dist/assets/ImportPage-wge4VhZ-.js.map +0 -1
- package/dist/assets/ProjectMemory-Q4XX40j_.js.map +0 -1
- package/dist/assets/index-7aMrnHJG.js +0 -7
- package/dist/assets/index-7aMrnHJG.js.map +0 -1
- package/dist/assets/index-BOeI_J4B.css +0 -1
package/dist/index.html
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<meta name="color-scheme" content="light dark" />
|
|
7
|
-
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
8
|
-
<title>Claude Sessions · archive</title>
|
|
9
|
-
<script>
|
|
10
|
-
// Theme bootstrapping — runs before React mounts to prevent FOUC
|
|
11
|
-
(function () {
|
|
12
|
-
try {
|
|
13
|
-
var saved = localStorage.getItem("theme");
|
|
14
|
-
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
15
|
-
var dark = saved ? saved === "dark" : prefersDark;
|
|
16
|
-
if (dark) document.documentElement.classList.add("dark");
|
|
17
|
-
} catch (_) {}
|
|
18
|
-
})();
|
|
19
|
-
</script>
|
|
20
|
-
<script type="module" crossorigin src="/assets/index-
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="color-scheme" content="light dark" />
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
8
|
+
<title>Claude Sessions · archive</title>
|
|
9
|
+
<script>
|
|
10
|
+
// Theme bootstrapping — runs before React mounts to prevent FOUC
|
|
11
|
+
(function () {
|
|
12
|
+
try {
|
|
13
|
+
var saved = localStorage.getItem("theme");
|
|
14
|
+
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
15
|
+
var dark = saved ? saved === "dark" : prefersDark;
|
|
16
|
+
if (dark) document.documentElement.classList.add("dark");
|
|
17
|
+
} catch (_) {}
|
|
18
|
+
})();
|
|
19
|
+
</script>
|
|
20
|
+
<script type="module" crossorigin src="/assets/index-DTbWl1jb.js"></script>
|
|
21
21
|
<link rel="modulepreload" crossorigin href="/assets/react-CPkiFScu.js">
|
|
22
22
|
<link rel="modulepreload" crossorigin href="/assets/query-CS7JQ86v.js">
|
|
23
23
|
<link rel="modulepreload" crossorigin href="/assets/router-DwaHAh1G.js">
|
|
24
24
|
<link rel="modulepreload" crossorigin href="/assets/vendor-Cs8vYp-N.js">
|
|
25
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
26
|
-
</head>
|
|
27
|
-
<body class="antialiased">
|
|
28
|
-
<div id="root"></div>
|
|
29
|
-
</body>
|
|
30
|
-
</html>
|
|
25
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CrWxV6sb.css">
|
|
26
|
+
</head>
|
|
27
|
+
<body class="antialiased">
|
|
28
|
+
<div id="root"></div>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,83 +1,81 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@zzusp/ccsm",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Local web UI to view and clean up Claude Code session history (~/.claude/)",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/zzusp/claude-code-session.git"
|
|
9
|
-
},
|
|
10
|
-
"homepage": "https://github.com/zzusp/claude-code-session#readme",
|
|
11
|
-
"bugs": "https://github.com/zzusp/claude-code-session/issues",
|
|
12
|
-
"type": "module",
|
|
13
|
-
"engines": {
|
|
14
|
-
"node": ">=22"
|
|
15
|
-
},
|
|
16
|
-
"bin": {
|
|
17
|
-
"ccsm": "bin/cli.mjs"
|
|
18
|
-
},
|
|
19
|
-
"files": [
|
|
20
|
-
"bin/",
|
|
21
|
-
"server/",
|
|
22
|
-
"shared/",
|
|
23
|
-
"dist/",
|
|
24
|
-
"README.md",
|
|
25
|
-
"LICENSE"
|
|
26
|
-
],
|
|
27
|
-
"keywords": [
|
|
28
|
-
"claude",
|
|
29
|
-
"claude-code",
|
|
30
|
-
"anthropic",
|
|
31
|
-
"session-manager",
|
|
32
|
-
"cli"
|
|
33
|
-
],
|
|
34
|
-
"publishConfig": {
|
|
35
|
-
"access": "public",
|
|
36
|
-
"registry": "https://registry.npmjs.org/"
|
|
37
|
-
},
|
|
38
|
-
"scripts": {
|
|
39
|
-
"dev": "concurrently -k -n server,web -c blue,magenta \"npm:dev:server\" \"npm:dev:web\"",
|
|
40
|
-
"dev:server": "node --import tsx --watch server/index.ts",
|
|
41
|
-
"dev:web": "node scripts/wait-for-server.mjs && vite",
|
|
42
|
-
"build": "vite build",
|
|
43
|
-
"start": "tsx server/index.ts",
|
|
44
|
-
"typecheck": "tsc -b",
|
|
45
|
-
"test": "vitest run",
|
|
46
|
-
"test:watch": "vitest",
|
|
47
|
-
"prepublishOnly": "npm run build",
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"@
|
|
59
|
-
"@
|
|
60
|
-
"@
|
|
61
|
-
"@
|
|
62
|
-
"@
|
|
63
|
-
"@
|
|
64
|
-
"@
|
|
65
|
-
"@
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"react": "^
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@zzusp/ccsm",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Local web UI to view and clean up Claude Code session history (~/.claude/)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/zzusp/claude-code-session.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/zzusp/claude-code-session#readme",
|
|
11
|
+
"bugs": "https://github.com/zzusp/claude-code-session/issues",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"ccsm": "bin/cli.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin/",
|
|
21
|
+
"server/",
|
|
22
|
+
"shared/",
|
|
23
|
+
"dist/",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"claude",
|
|
29
|
+
"claude-code",
|
|
30
|
+
"anthropic",
|
|
31
|
+
"session-manager",
|
|
32
|
+
"cli"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"registry": "https://registry.npmjs.org/"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"dev": "concurrently -k -n server,web -c blue,magenta \"npm:dev:server\" \"npm:dev:web\"",
|
|
40
|
+
"dev:server": "node --import tsx --watch server/index.ts",
|
|
41
|
+
"dev:web": "node scripts/wait-for-server.mjs && vite",
|
|
42
|
+
"build": "vite build",
|
|
43
|
+
"start": "tsx server/index.ts",
|
|
44
|
+
"typecheck": "tsc -b",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"prepublishOnly": "npm run build",
|
|
48
|
+
"prepare": "husky"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@hono/node-server": "^1.13.7",
|
|
52
|
+
"hono": "^4.6.14",
|
|
53
|
+
"tsx": "^4.19.2"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@commitlint/cli": "^21.0.2",
|
|
57
|
+
"@commitlint/config-conventional": "^21.0.2",
|
|
58
|
+
"@fontsource-variable/geist-mono": "^5.2.7",
|
|
59
|
+
"@fontsource-variable/plus-jakarta-sans": "^5.2.8",
|
|
60
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
61
|
+
"@tanstack/react-query": "^5.62.7",
|
|
62
|
+
"@types/node": "^22.10.5",
|
|
63
|
+
"@types/react": "^19.0.2",
|
|
64
|
+
"@types/react-dom": "^19.0.2",
|
|
65
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
66
|
+
"concurrently": "^9.1.2",
|
|
67
|
+
"husky": "^9.1.7",
|
|
68
|
+
"motion": "^12.38.0",
|
|
69
|
+
"playwright": "^1.59.1",
|
|
70
|
+
"react": "^19.0.0",
|
|
71
|
+
"react-dom": "^19.0.0",
|
|
72
|
+
"react-markdown": "^10.1.0",
|
|
73
|
+
"react-router-dom": "^7.1.1",
|
|
74
|
+
"recharts": "^2.15.0",
|
|
75
|
+
"remark-gfm": "^4.0.1",
|
|
76
|
+
"tailwindcss": "^4.0.0",
|
|
77
|
+
"typescript": "^5.7.2",
|
|
78
|
+
"vite": "^6.0.7",
|
|
79
|
+
"vitest": "^4.1.8"
|
|
80
|
+
}
|
|
81
|
+
}
|
package/server/index.ts
CHANGED
|
@@ -1,130 +1,130 @@
|
|
|
1
|
-
import { serve } from '@hono/node-server';
|
|
2
|
-
import { serveStatic } from '@hono/node-server/serve-static';
|
|
3
|
-
import { Hono } from 'hono';
|
|
4
|
-
import { spawn } from 'node:child_process';
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { parseArgs } from 'node:util';
|
|
9
|
-
import { PATHS } from './lib/claude-paths.ts';
|
|
10
|
-
import { findAvailablePort } from './lib/port.ts';
|
|
11
|
-
import { diskRoute } from './routes/disk.ts';
|
|
12
|
-
import { diskCleanupRoute } from './routes/disk-cleanup.ts';
|
|
13
|
-
import { importRoute } from './routes/import.ts';
|
|
14
|
-
import { projectsRoute } from './routes/projects.ts';
|
|
15
|
-
import { searchRoute } from './routes/search.ts';
|
|
16
|
-
import { sessionsRoute } from './routes/sessions.ts';
|
|
17
|
-
import { versionRoute } from './routes/version.ts';
|
|
18
|
-
|
|
19
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
const projectRoot = path.resolve(__dirname, '..');
|
|
21
|
-
const distDir = path.join(projectRoot, 'dist');
|
|
22
|
-
|
|
23
|
-
const PORT_RANGE_START = 3131;
|
|
24
|
-
const PORT_RANGE_END = 3140;
|
|
25
|
-
const DEFAULT_HOST = '127.0.0.1';
|
|
26
|
-
|
|
27
|
-
const app = new Hono();
|
|
28
|
-
|
|
29
|
-
app.onError((err, c) => {
|
|
30
|
-
console.error('[server] unhandled error', err);
|
|
31
|
-
if (c.req.path.startsWith('/api/')) {
|
|
32
|
-
return c.json({ error: err.message || 'internal error' }, 500);
|
|
33
|
-
}
|
|
34
|
-
return c.text('internal error', 500);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
app.get('/api/health', (c) =>
|
|
38
|
-
c.json({
|
|
39
|
-
ok: true,
|
|
40
|
-
claudeRoot: PATHS.root,
|
|
41
|
-
claudeRootExists: fs.existsSync(PATHS.root),
|
|
42
|
-
platform: process.platform,
|
|
43
|
-
node: process.version,
|
|
44
|
-
pid: process.pid,
|
|
45
|
-
}),
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
app.route('/api/projects', projectsRoute);
|
|
49
|
-
app.route('/api/sessions', sessionsRoute);
|
|
50
|
-
app.route('/api/disk-usage', diskRoute);
|
|
51
|
-
app.route('/api/disk-cleanup', diskCleanupRoute);
|
|
52
|
-
app.route('/api/search', searchRoute);
|
|
53
|
-
app.route('/api/import', importRoute);
|
|
54
|
-
app.route('/api/version', versionRoute);
|
|
55
|
-
|
|
56
|
-
if (fs.existsSync(distDir)) {
|
|
57
|
-
app.use('/*', serveStatic({ root: path.relative(process.cwd(), distDir) || '.' }));
|
|
58
|
-
app.get('*', serveStatic({ path: path.relative(process.cwd(), path.join(distDir, 'index.html')) }));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function parseCliArgs() {
|
|
62
|
-
try {
|
|
63
|
-
return parseArgs({
|
|
64
|
-
args: process.argv.slice(2),
|
|
65
|
-
options: {
|
|
66
|
-
port: { type: 'string', short: 'p' },
|
|
67
|
-
host: { type: 'string' },
|
|
68
|
-
open: { type: 'boolean', short: 'o' },
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
} catch (err) {
|
|
72
|
-
console.error(`[server] ${(err as Error).message}`);
|
|
73
|
-
console.error('[server] run "ccsm --help" for usage');
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const { values } = parseCliArgs();
|
|
79
|
-
const host = values.host ?? DEFAULT_HOST;
|
|
80
|
-
const isLoopback = host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
81
|
-
|
|
82
|
-
let port: number;
|
|
83
|
-
if (values.port !== undefined) {
|
|
84
|
-
const requested = Number(values.port);
|
|
85
|
-
if (!Number.isInteger(requested) || requested < 1 || requested > 65535) {
|
|
86
|
-
console.error(`[server] invalid --port "${values.port}" (expected an integer 1..65535)`);
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
try {
|
|
90
|
-
port = await findAvailablePort(requested, requested, host);
|
|
91
|
-
} catch {
|
|
92
|
-
console.error(`[server] port ${requested} on ${host} is already in use`);
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
} else {
|
|
96
|
-
port = await findAvailablePort(PORT_RANGE_START, PORT_RANGE_END, host);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
serve({ fetch: app.fetch, hostname: host, port }, (info) => {
|
|
100
|
-
console.log(`[server] listening on http://${info.address}:${info.port}`);
|
|
101
|
-
console.log(`[server] claudeRoot = ${PATHS.root}`);
|
|
102
|
-
if (!isLoopback) {
|
|
103
|
-
console.warn(
|
|
104
|
-
`[server] WARNING: bound to ${host} (not loopback). The UI is now reachable from your network ` +
|
|
105
|
-
`and has NO authentication — anyone who can reach this host:port can read and delete your ` +
|
|
106
|
-
`Claude Code history. Only do this on a network you trust.`,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
if (!fs.existsSync(distDir)) {
|
|
110
|
-
console.log('[server] dist/ not built yet — run "npm run build" (or open the Vite dev server: npm run dev:web)');
|
|
111
|
-
}
|
|
112
|
-
if (values.open) {
|
|
113
|
-
const browseHost = isLoopback ? (host === '::1' ? '[::1]' : host) : 'localhost';
|
|
114
|
-
openInBrowser(`http://${browseHost}:${info.port}`);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
function openInBrowser(url: string): void {
|
|
119
|
-
let cmd: string;
|
|
120
|
-
if (process.platform === 'win32') cmd = 'explorer.exe';
|
|
121
|
-
else if (process.platform === 'darwin') cmd = 'open';
|
|
122
|
-
else cmd = 'xdg-open';
|
|
123
|
-
try {
|
|
124
|
-
const child = spawn(cmd, [url], { detached: true, stdio: 'ignore' });
|
|
125
|
-
child.on('error', (err) => console.error('[server] could not open browser:', err.message));
|
|
126
|
-
child.unref();
|
|
127
|
-
} catch (err) {
|
|
128
|
-
console.error('[server] could not open browser:', (err as Error).message);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
1
|
+
import { serve } from '@hono/node-server';
|
|
2
|
+
import { serveStatic } from '@hono/node-server/serve-static';
|
|
3
|
+
import { Hono } from 'hono';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { parseArgs } from 'node:util';
|
|
9
|
+
import { PATHS } from './lib/claude-paths.ts';
|
|
10
|
+
import { findAvailablePort } from './lib/port.ts';
|
|
11
|
+
import { diskRoute } from './routes/disk.ts';
|
|
12
|
+
import { diskCleanupRoute } from './routes/disk-cleanup.ts';
|
|
13
|
+
import { importRoute } from './routes/import.ts';
|
|
14
|
+
import { projectsRoute } from './routes/projects.ts';
|
|
15
|
+
import { searchRoute } from './routes/search.ts';
|
|
16
|
+
import { sessionsRoute } from './routes/sessions.ts';
|
|
17
|
+
import { versionRoute } from './routes/version.ts';
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const projectRoot = path.resolve(__dirname, '..');
|
|
21
|
+
const distDir = path.join(projectRoot, 'dist');
|
|
22
|
+
|
|
23
|
+
const PORT_RANGE_START = 3131;
|
|
24
|
+
const PORT_RANGE_END = 3140;
|
|
25
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
26
|
+
|
|
27
|
+
const app = new Hono();
|
|
28
|
+
|
|
29
|
+
app.onError((err, c) => {
|
|
30
|
+
console.error('[server] unhandled error', err);
|
|
31
|
+
if (c.req.path.startsWith('/api/')) {
|
|
32
|
+
return c.json({ error: err.message || 'internal error' }, 500);
|
|
33
|
+
}
|
|
34
|
+
return c.text('internal error', 500);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.get('/api/health', (c) =>
|
|
38
|
+
c.json({
|
|
39
|
+
ok: true,
|
|
40
|
+
claudeRoot: PATHS.root,
|
|
41
|
+
claudeRootExists: fs.existsSync(PATHS.root),
|
|
42
|
+
platform: process.platform,
|
|
43
|
+
node: process.version,
|
|
44
|
+
pid: process.pid,
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
app.route('/api/projects', projectsRoute);
|
|
49
|
+
app.route('/api/sessions', sessionsRoute);
|
|
50
|
+
app.route('/api/disk-usage', diskRoute);
|
|
51
|
+
app.route('/api/disk-cleanup', diskCleanupRoute);
|
|
52
|
+
app.route('/api/search', searchRoute);
|
|
53
|
+
app.route('/api/import', importRoute);
|
|
54
|
+
app.route('/api/version', versionRoute);
|
|
55
|
+
|
|
56
|
+
if (fs.existsSync(distDir)) {
|
|
57
|
+
app.use('/*', serveStatic({ root: path.relative(process.cwd(), distDir) || '.' }));
|
|
58
|
+
app.get('*', serveStatic({ path: path.relative(process.cwd(), path.join(distDir, 'index.html')) }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseCliArgs() {
|
|
62
|
+
try {
|
|
63
|
+
return parseArgs({
|
|
64
|
+
args: process.argv.slice(2),
|
|
65
|
+
options: {
|
|
66
|
+
port: { type: 'string', short: 'p' },
|
|
67
|
+
host: { type: 'string' },
|
|
68
|
+
open: { type: 'boolean', short: 'o' },
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error(`[server] ${(err as Error).message}`);
|
|
73
|
+
console.error('[server] run "ccsm --help" for usage');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const { values } = parseCliArgs();
|
|
79
|
+
const host = values.host ?? DEFAULT_HOST;
|
|
80
|
+
const isLoopback = host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
81
|
+
|
|
82
|
+
let port: number;
|
|
83
|
+
if (values.port !== undefined) {
|
|
84
|
+
const requested = Number(values.port);
|
|
85
|
+
if (!Number.isInteger(requested) || requested < 1 || requested > 65535) {
|
|
86
|
+
console.error(`[server] invalid --port "${values.port}" (expected an integer 1..65535)`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
port = await findAvailablePort(requested, requested, host);
|
|
91
|
+
} catch {
|
|
92
|
+
console.error(`[server] port ${requested} on ${host} is already in use`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
port = await findAvailablePort(PORT_RANGE_START, PORT_RANGE_END, host);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
serve({ fetch: app.fetch, hostname: host, port }, (info) => {
|
|
100
|
+
console.log(`[server] listening on http://${info.address}:${info.port}`);
|
|
101
|
+
console.log(`[server] claudeRoot = ${PATHS.root}`);
|
|
102
|
+
if (!isLoopback) {
|
|
103
|
+
console.warn(
|
|
104
|
+
`[server] WARNING: bound to ${host} (not loopback). The UI is now reachable from your network ` +
|
|
105
|
+
`and has NO authentication — anyone who can reach this host:port can read and delete your ` +
|
|
106
|
+
`Claude Code history. Only do this on a network you trust.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (!fs.existsSync(distDir)) {
|
|
110
|
+
console.log('[server] dist/ not built yet — run "npm run build" (or open the Vite dev server: npm run dev:web)');
|
|
111
|
+
}
|
|
112
|
+
if (values.open) {
|
|
113
|
+
const browseHost = isLoopback ? (host === '::1' ? '[::1]' : host) : 'localhost';
|
|
114
|
+
openInBrowser(`http://${browseHost}:${info.port}`);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
function openInBrowser(url: string): void {
|
|
119
|
+
let cmd: string;
|
|
120
|
+
if (process.platform === 'win32') cmd = 'explorer.exe';
|
|
121
|
+
else if (process.platform === 'darwin') cmd = 'open';
|
|
122
|
+
else cmd = 'xdg-open';
|
|
123
|
+
try {
|
|
124
|
+
const child = spawn(cmd, [url], { detached: true, stdio: 'ignore' });
|
|
125
|
+
child.on('error', (err) => console.error('[server] could not open browser:', err.message));
|
|
126
|
+
child.unref();
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error('[server] could not open browser:', (err as Error).message);
|
|
129
|
+
}
|
|
130
|
+
}
|