@zzusp/ccsm 1.0.1 → 1.0.2

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.
Files changed (70) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +236 -236
  3. package/bin/cli.mjs +52 -52
  4. package/dist/assets/{DiskUsage-CKhggLs5.js → DiskUsage-BY6XwffG.js} +2 -2
  5. package/dist/assets/DiskUsage-BY6XwffG.js.map +1 -0
  6. package/dist/assets/{ImportPage-wge4VhZ-.js → ImportPage-Cwq5bx7G.js} +2 -2
  7. package/dist/assets/ImportPage-Cwq5bx7G.js.map +1 -0
  8. package/dist/assets/MarkdownContent-BFu7Nkk_.js +2 -0
  9. package/dist/assets/MarkdownContent-BFu7Nkk_.js.map +1 -0
  10. package/dist/assets/{ProjectMemory-Q4XX40j_.js → ProjectMemory-CcE3KbUK.js} +2 -2
  11. package/dist/assets/ProjectMemory-CcE3KbUK.js.map +1 -0
  12. package/dist/assets/index-CrWxV6sb.css +1 -0
  13. package/dist/assets/index-DTbWl1jb.js +11 -0
  14. package/dist/assets/index-DTbWl1jb.js.map +1 -0
  15. package/dist/assets/markdown-Bag5rX3T.js +30 -0
  16. package/dist/assets/markdown-Bag5rX3T.js.map +1 -0
  17. package/dist/index.html +26 -26
  18. package/package.json +85 -83
  19. package/server/index.ts +130 -130
  20. package/server/lib/active-sessions.test.ts +119 -119
  21. package/server/lib/active-sessions.ts +95 -95
  22. package/server/lib/bundle.test.ts +182 -182
  23. package/server/lib/bundle.ts +86 -86
  24. package/server/lib/claude-paths.test.ts +126 -126
  25. package/server/lib/claude-paths.ts +43 -43
  26. package/server/lib/cleanup-suggestions.ts +131 -131
  27. package/server/lib/constants.ts +8 -8
  28. package/server/lib/delete-project.ts +100 -100
  29. package/server/lib/delete.test.ts +244 -244
  30. package/server/lib/delete.ts +192 -192
  31. package/server/lib/disk-usage.ts +81 -81
  32. package/server/lib/encode-cwd.ts +24 -24
  33. package/server/lib/export-bundle.ts +236 -236
  34. package/server/lib/export-import-bundle.test.ts +337 -337
  35. package/server/lib/fs-size.ts +38 -38
  36. package/server/lib/import-bundle.ts +488 -488
  37. package/server/lib/load-memory.ts +120 -120
  38. package/server/lib/load-session.ts +209 -209
  39. package/server/lib/modified-files.test.ts +280 -280
  40. package/server/lib/modified-files.ts +228 -228
  41. package/server/lib/open-folder.ts +47 -47
  42. package/server/lib/parse-jsonl.ts +160 -139
  43. package/server/lib/port.ts +23 -23
  44. package/server/lib/safe-id.test.ts +41 -41
  45. package/server/lib/safe-id.ts +6 -6
  46. package/server/lib/safe-remove.test.ts +73 -73
  47. package/server/lib/safe-remove.ts +25 -25
  48. package/server/lib/scan.ts +289 -286
  49. package/server/lib/search-all.ts +130 -130
  50. package/server/lib/search-session.ts +203 -203
  51. package/server/lib/system-tags.ts +20 -20
  52. package/server/lib/update.ts +67 -67
  53. package/server/lib/version.test.ts +39 -39
  54. package/server/lib/version.ts +117 -117
  55. package/server/routes/disk-cleanup.ts +54 -54
  56. package/server/routes/disk.ts +9 -9
  57. package/server/routes/import.ts +87 -87
  58. package/server/routes/projects.ts +104 -104
  59. package/server/routes/search.ts +79 -79
  60. package/server/routes/sessions.ts +130 -130
  61. package/server/routes/version.ts +34 -34
  62. package/server/types.ts +1 -1
  63. package/shared/constants.ts +7 -7
  64. package/shared/types.ts +513 -511
  65. package/dist/assets/DiskUsage-CKhggLs5.js.map +0 -1
  66. package/dist/assets/ImportPage-wge4VhZ-.js.map +0 -1
  67. package/dist/assets/ProjectMemory-Q4XX40j_.js.map +0 -1
  68. package/dist/assets/index-7aMrnHJG.js +0 -7
  69. package/dist/assets/index-7aMrnHJG.js.map +0 -1
  70. 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-7aMrnHJG.js"></script>
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-BOeI_J4B.css">
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,85 @@
1
- {
2
- "name": "@zzusp/ccsm",
3
- "version": "1.0.1",
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
- "release": "release-it",
49
- "release:dry": "release-it --dry-run",
50
- "prepare": "husky"
51
- },
52
- "dependencies": {
53
- "@hono/node-server": "^1.13.7",
54
- "hono": "^4.6.14",
55
- "tsx": "^4.19.2"
56
- },
57
- "devDependencies": {
58
- "@commitlint/cli": "^21.0.2",
59
- "@commitlint/config-conventional": "^21.0.2",
60
- "@fontsource-variable/geist-mono": "^5.2.7",
61
- "@fontsource-variable/plus-jakarta-sans": "^5.2.8",
62
- "@release-it/conventional-changelog": "^11.0.1",
63
- "@tailwindcss/vite": "^4.0.0",
64
- "@tanstack/react-query": "^5.62.7",
65
- "@types/node": "^22.10.5",
66
- "@types/react": "^19.0.2",
67
- "@types/react-dom": "^19.0.2",
68
- "@vitejs/plugin-react": "^4.3.4",
69
- "concurrently": "^9.1.2",
70
- "husky": "^9.1.7",
71
- "motion": "^12.38.0",
72
- "playwright": "^1.59.1",
73
- "react": "^19.0.0",
74
- "react-dom": "^19.0.0",
75
- "react-router-dom": "^7.1.1",
76
- "recharts": "^2.15.0",
77
- "release-it": "^20.2.0",
78
- "tailwindcss": "^4.0.0",
79
- "typescript": "^5.7.2",
80
- "vite": "^6.0.7",
81
- "vitest": "^4.1.8"
82
- }
83
- }
1
+ {
2
+ "name": "@zzusp/ccsm",
3
+ "version": "1.0.2",
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
+ "release": "release-it",
49
+ "release:dry": "release-it --dry-run",
50
+ "prepare": "husky"
51
+ },
52
+ "dependencies": {
53
+ "@hono/node-server": "^1.13.7",
54
+ "hono": "^4.6.14",
55
+ "tsx": "^4.19.2"
56
+ },
57
+ "devDependencies": {
58
+ "@commitlint/cli": "^21.0.2",
59
+ "@commitlint/config-conventional": "^21.0.2",
60
+ "@fontsource-variable/geist-mono": "^5.2.7",
61
+ "@fontsource-variable/plus-jakarta-sans": "^5.2.8",
62
+ "@release-it/conventional-changelog": "^11.0.1",
63
+ "@tailwindcss/vite": "^4.0.0",
64
+ "@tanstack/react-query": "^5.62.7",
65
+ "@types/node": "^22.10.5",
66
+ "@types/react": "^19.0.2",
67
+ "@types/react-dom": "^19.0.2",
68
+ "@vitejs/plugin-react": "^4.3.4",
69
+ "concurrently": "^9.1.2",
70
+ "husky": "^9.1.7",
71
+ "motion": "^12.38.0",
72
+ "playwright": "^1.59.1",
73
+ "react": "^19.0.0",
74
+ "react-dom": "^19.0.0",
75
+ "react-markdown": "^10.1.0",
76
+ "react-router-dom": "^7.1.1",
77
+ "recharts": "^2.15.0",
78
+ "release-it": "^20.2.0",
79
+ "remark-gfm": "^4.0.1",
80
+ "tailwindcss": "^4.0.0",
81
+ "typescript": "^5.7.2",
82
+ "vite": "^6.0.7",
83
+ "vitest": "^4.1.8"
84
+ }
85
+ }
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
+ }