diff --git a/.env.example b/.env.example
index d1f2fa95..6b34f69e 100755
--- a/.env.example
+++ b/.env.example
@@ -13,3 +13,8 @@ VITE_PORT=5173
# Uncomment the following line if you have a custom claude cli path other than the default "claude"
# CLAUDE_CLI_PATH=claude
+
+# Claude Code context window size (maximum tokens per session)
+# Note: VITE_ prefix makes it available to frontend
+VITE_CONTEXT_WINDOW=160000
+CONTEXT_WINDOW=160000
diff --git a/.gitignore b/.gitignore
index 84b56c6f..2ff70c19 100755
--- a/.gitignore
+++ b/.gitignore
@@ -105,7 +105,9 @@ temp/
.taskmaster/
.cline/
.windsurf/
+.serena/
CLAUDE.md
+.mcp.json
# Database files
@@ -126,5 +128,5 @@ dev-debug.log
# OS specific
# Task files
-# tasks.json
-# tasks/
+tasks.json
+tasks/
diff --git a/.playwright-mcp/1-input-with-text.png b/.playwright-mcp/1-input-with-text.png
new file mode 100644
index 00000000..19569dfd
Binary files /dev/null and b/.playwright-mcp/1-input-with-text.png differ
diff --git a/.playwright-mcp/2-slash-command-menu-open.png b/.playwright-mcp/2-slash-command-menu-open.png
new file mode 100644
index 00000000..0828a4f1
Binary files /dev/null and b/.playwright-mcp/2-slash-command-menu-open.png differ
diff --git a/.playwright-mcp/3-after-fix-with-text.png b/.playwright-mcp/3-after-fix-with-text.png
new file mode 100644
index 00000000..0433ec22
Binary files /dev/null and b/.playwright-mcp/3-after-fix-with-text.png differ
diff --git a/.playwright-mcp/4-clear-button-final-position.png b/.playwright-mcp/4-clear-button-final-position.png
new file mode 100644
index 00000000..c8d02442
Binary files /dev/null and b/.playwright-mcp/4-clear-button-final-position.png differ
diff --git a/.playwright-mcp/5-slash-menu-no-clear-button.png b/.playwright-mcp/5-slash-menu-no-clear-button.png
new file mode 100644
index 00000000..73d21bd0
Binary files /dev/null and b/.playwright-mcp/5-slash-menu-no-clear-button.png differ
diff --git a/.playwright-mcp/6-latest-build-with-text.png b/.playwright-mcp/6-latest-build-with-text.png
new file mode 100644
index 00000000..fa3252a2
Binary files /dev/null and b/.playwright-mcp/6-latest-build-with-text.png differ
diff --git a/.playwright-mcp/after-sw-cleanup.png b/.playwright-mcp/after-sw-cleanup.png
new file mode 100644
index 00000000..28057388
Binary files /dev/null and b/.playwright-mcp/after-sw-cleanup.png differ
diff --git a/.playwright-mcp/input-area-scrolled.png b/.playwright-mcp/input-area-scrolled.png
new file mode 100644
index 00000000..d563ebfe
Binary files /dev/null and b/.playwright-mcp/input-area-scrolled.png differ
diff --git a/.playwright-mcp/login-screen-working.png b/.playwright-mcp/login-screen-working.png
new file mode 100644
index 00000000..16f96aa7
Binary files /dev/null and b/.playwright-mcp/login-screen-working.png differ
diff --git a/.playwright-mcp/page-after-restart.png b/.playwright-mcp/page-after-restart.png
new file mode 100644
index 00000000..28057388
Binary files /dev/null and b/.playwright-mcp/page-after-restart.png differ
diff --git a/.playwright-mcp/session-with-commands-button-desktop.png b/.playwright-mcp/session-with-commands-button-desktop.png
new file mode 100644
index 00000000..fe482d2a
Binary files /dev/null and b/.playwright-mcp/session-with-commands-button-desktop.png differ
diff --git a/.playwright-mcp/slash-command-desktop-initial.png b/.playwright-mcp/slash-command-desktop-initial.png
new file mode 100644
index 00000000..28057388
Binary files /dev/null and b/.playwright-mcp/slash-command-desktop-initial.png differ
diff --git a/.playwright-mcp/slash-command-menu-desktop.png b/.playwright-mcp/slash-command-menu-desktop.png
new file mode 100644
index 00000000..c651a490
Binary files /dev/null and b/.playwright-mcp/slash-command-menu-desktop.png differ
diff --git a/.playwright-mcp/slash-command-menu-mobile.png b/.playwright-mcp/slash-command-menu-mobile.png
new file mode 100644
index 00000000..fdbdde5c
Binary files /dev/null and b/.playwright-mcp/slash-command-menu-mobile.png differ
diff --git a/package-lock.json b/package-lock.json
index eb3b5911..c556d127 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.8.12",
"license": "MIT",
"dependencies": {
+ "@anthropic-ai/claude-agent-sdk": "^0.1.13",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",
@@ -16,11 +17,13 @@
"@codemirror/lang-markdown": "^6.3.3",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/theme-one-dark": "^6.1.2",
- "@siteboon/claude-code-ui": "^1.8.4",
+ "@esbuild/darwin-arm64": "^0.25.11",
"@tailwindcss/typography": "^0.5.16",
"@uiw/react-codemirror": "^4.23.13",
"@xterm/addon-clipboard": "^0.1.0",
+ "@xterm/addon-fit": "^0.10.0",
"@xterm/addon-webgl": "^0.18.0",
+ "@xterm/xterm": "^5.5.0",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.2.0",
"chokidar": "^4.0.3",
@@ -29,6 +32,8 @@
"cors": "^2.8.5",
"cross-spawn": "^7.0.3",
"express": "^4.18.2",
+ "fuse.js": "^6.6.2",
+ "gray-matter": "^4.0.3",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.515.0",
"mime-types": "^3.0.1",
@@ -43,9 +48,7 @@
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7",
"tailwind-merge": "^3.3.1",
- "ws": "^8.14.2",
- "xterm": "^5.3.0",
- "xterm-addon-fit": "^0.8.0"
+ "ws": "^8.14.2"
},
"bin": {
"claude-code-ui": "server/index.js"
@@ -91,6 +94,26 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@anthropic-ai/claude-agent-sdk": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.1.13.tgz",
+ "integrity": "sha512-9/+/iVdVQx2o3INUxwNWuZQOhff3ISXgSc/G7jfD85qtEN/7ZK/uOnAtCH1PChMNBN5CGXgVKNMce++52tfZ5A==",
+ "license": "SEE LICENSE IN README.md",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "^0.33.5",
+ "@img/sharp-darwin-x64": "^0.33.5",
+ "@img/sharp-linux-arm": "^0.33.5",
+ "@img/sharp-linux-arm64": "^0.33.5",
+ "@img/sharp-linux-x64": "^0.33.5",
+ "@img/sharp-win32-x64": "^0.33.5"
+ },
+ "peerDependencies": {
+ "zod": "^3.24.1"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -638,15 +661,13 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.8",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
- "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
+ "version": "0.25.11",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz",
+ "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==",
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
- "optional": true,
"os": [
"darwin"
],
@@ -1018,6 +1039,114 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
+ "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
+ "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
+ "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
+ "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
+ "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
+ "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz",
@@ -1052,6 +1181,22 @@
"url": "https://opencollective.com/libvips"
}
},
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
+ "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz",
@@ -1086,6 +1231,50 @@
"url": "https://opencollective.com/libvips"
}
},
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
+ "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.0.5"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
+ "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.0.4"
+ }
+ },
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz",
@@ -1132,6 +1321,28 @@
"@img/sharp-libvips-linux-s390x": "1.2.0"
}
},
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
+ "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.0.4"
+ }
+ },
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz",
@@ -1238,6 +1449,25 @@
"url": "https://opencollective.com/libvips"
}
},
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
+ "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/@inquirer/ansi": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.0.tgz",
@@ -2506,50 +2736,6 @@
"win32"
]
},
- "node_modules/@siteboon/claude-code-ui": {
- "version": "1.8.4",
- "resolved": "https://registry.npmjs.org/@siteboon/claude-code-ui/-/claude-code-ui-1.8.4.tgz",
- "integrity": "sha512-9moBlMDNF/6IfIcqShavxdq0TI9aNuY3+33YZcnvYagWsZMdJ/7d5tgDwAZEp3Uup/nHU+bdrkiXmFfLcRQLCQ==",
- "license": "MIT",
- "dependencies": {
- "@codemirror/lang-css": "^6.3.1",
- "@codemirror/lang-html": "^6.4.9",
- "@codemirror/lang-javascript": "^6.2.4",
- "@codemirror/lang-json": "^6.0.1",
- "@codemirror/lang-markdown": "^6.3.3",
- "@codemirror/lang-python": "^6.2.1",
- "@codemirror/theme-one-dark": "^6.1.2",
- "@tailwindcss/typography": "^0.5.16",
- "@uiw/react-codemirror": "^4.23.13",
- "@xterm/addon-clipboard": "^0.1.0",
- "@xterm/addon-webgl": "^0.18.0",
- "bcrypt": "^6.0.0",
- "better-sqlite3": "^12.2.0",
- "chokidar": "^4.0.3",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "cors": "^2.8.5",
- "cross-spawn": "^7.0.3",
- "express": "^4.18.2",
- "jsonwebtoken": "^9.0.2",
- "lucide-react": "^0.515.0",
- "mime-types": "^3.0.1",
- "multer": "^2.0.1",
- "node-fetch": "^2.7.0",
- "node-pty": "^1.1.0-beta34",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "react-dropzone": "^14.2.3",
- "react-markdown": "^10.1.0",
- "react-router-dom": "^6.8.1",
- "sqlite": "^5.1.1",
- "sqlite3": "^5.1.7",
- "tailwind-merge": "^3.3.1",
- "ws": "^8.14.2",
- "xterm": "^5.3.0",
- "xterm-addon-fit": "^0.8.0"
- }
- },
"node_modules/@tailwindcss/typography": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
@@ -2806,6 +2992,15 @@
"@xterm/xterm": "^5.4.0"
}
},
+ "node_modules/@xterm/addon-fit": {
+ "version": "0.10.0",
+ "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
+ "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@xterm/xterm": "^5.0.0"
+ }
+ },
"node_modules/@xterm/addon-webgl": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.18.0.tgz",
@@ -2819,8 +3014,7 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/abbrev": {
"version": "2.0.0",
@@ -2983,6 +3177,15 @@
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
"license": "MIT"
},
+ "node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -4483,6 +4686,23 @@
"@esbuild/win32-x64": "0.25.8"
}
},
+ "node_modules/esbuild/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.8",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
+ "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -4525,7 +4745,6 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "dev": true,
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
@@ -4717,6 +4936,18 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
+ "node_modules/extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extendable": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
@@ -4948,6 +5179,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/fuse.js": {
+ "version": "6.6.2",
+ "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz",
+ "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/gauge": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz",
@@ -5215,6 +5455,21 @@
"devOptional": true,
"license": "ISC"
},
+ "node_modules/gray-matter": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
+ "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-yaml": "^3.13.1",
+ "kind-of": "^6.0.2",
+ "section-matter": "^1.0.0",
+ "strip-bom-string": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.0"
+ }
+ },
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
@@ -5644,6 +5899,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/is-extendable": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
+ "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -5855,6 +6119,19 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
+ "node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -5936,6 +6213,15 @@
"safe-buffer": "^5.0.1"
}
},
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
+ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/lilconfig": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
@@ -8770,6 +9056,19 @@
"loose-envify": "^1.1.0"
}
},
+ "node_modules/section-matter": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
+ "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
+ "license": "MIT",
+ "dependencies": {
+ "extend-shallow": "^2.0.1",
+ "kind-of": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -9381,6 +9680,12 @@
"integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==",
"dev": true
},
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/sqlite": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/sqlite/-/sqlite-5.1.1.tgz",
@@ -9932,6 +10237,15 @@
"node": ">=8"
}
},
+ "node_modules/strip-bom-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
+ "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/strip-final-newline": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
@@ -10674,18 +10988,18 @@
}
},
"node_modules/vite": {
- "version": "7.0.5",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz",
- "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==",
+ "version": "7.1.8",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.8.tgz",
+ "integrity": "sha512-oBXvfSHEOL8jF+R9Am7h59Up07kVVGH1NrFGFoEG6bPDZP3tGpQhvkBpy5x7U6+E6wZCu9OihsWgJqDbQIm8LQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
- "fdir": "^6.4.6",
- "picomatch": "^4.0.2",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
"postcss": "^8.5.6",
- "rollup": "^4.40.0",
- "tinyglobby": "^0.2.14"
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
@@ -10749,11 +11063,14 @@
}
},
"node_modules/vite/node_modules/fdir": {
- "version": "6.4.6",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
- "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
"peerDependencies": {
"picomatch": "^3 || ^4"
},
@@ -11038,23 +11355,6 @@
"node": ">=0.4"
}
},
- "node_modules/xterm": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
- "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==",
- "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.",
- "license": "MIT"
- },
- "node_modules/xterm-addon-fit": {
- "version": "0.8.0",
- "resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz",
- "integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==",
- "deprecated": "This package is now deprecated. Move to @xterm/addon-fit instead.",
- "license": "MIT",
- "peerDependencies": {
- "xterm": "^5.0.0"
- }
- },
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
@@ -11184,6 +11484,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
"node_modules/zwitch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
diff --git a/package.json b/package.json
index 91dec5d5..468ee1e8 100644
--- a/package.json
+++ b/package.json
@@ -73,8 +73,8 @@
"sqlite3": "^5.1.7",
"tailwind-merge": "^3.3.1",
"ws": "^8.14.2",
- "xterm": "^5.3.0",
- "xterm-addon-fit": "^0.8.0"
+ "@xterm/xterm": "^5.5.0",
+ "@xterm/addon-fit": "^0.10.0"
},
"devDependencies": {
"@types/react": "^18.2.43",
diff --git a/public/clear-cache.html b/public/clear-cache.html
new file mode 100644
index 00000000..47da67fb
--- /dev/null
+++ b/public/clear-cache.html
@@ -0,0 +1,85 @@
+
+
+
+ Clear Cache - Claude Code UI
+
+
+
+ Clear Cache & Service Worker
+ If you're seeing a blank page or old content, click the button below to clear all cached data.
+
+
+
+
+
+
+
+
diff --git a/server/claude-cli.js b/server/claude-cli.js
deleted file mode 100755
index 2e685d76..00000000
--- a/server/claude-cli.js
+++ /dev/null
@@ -1,397 +0,0 @@
-import { spawn } from 'child_process';
-import crossSpawn from 'cross-spawn';
-import { promises as fs } from 'fs';
-import path from 'path';
-import os from 'os';
-
-// Use cross-spawn on Windows for better command execution
-const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
-
-let activeClaudeProcesses = new Map(); // Track active processes by session ID
-
-async function spawnClaude(command, options = {}, ws) {
- return new Promise(async (resolve, reject) => {
- const { sessionId, projectPath, cwd, resume, toolsSettings, permissionMode, images } = options;
- let capturedSessionId = sessionId; // Track session ID throughout the process
- let sessionCreatedSent = false; // Track if we've already sent session-created event
-
- // Use tools settings passed from frontend, or defaults
- const settings = toolsSettings || {
- allowedTools: [],
- disallowedTools: [],
- skipPermissions: false
- };
-
- // Build Claude CLI command - start with print/resume flags first
- const args = [];
-
- // Use cwd (actual project directory) instead of projectPath (Claude's metadata directory)
- const workingDir = cwd || process.cwd();
-
- // Handle images by saving them to temporary files and passing paths to Claude
- const tempImagePaths = [];
- let tempDir = null;
- if (images && images.length > 0) {
- try {
- // Create temp directory in the project directory so Claude can access it
- tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
- await fs.mkdir(tempDir, { recursive: true });
-
- // Save each image to a temp file
- for (const [index, image] of images.entries()) {
- // Extract base64 data and mime type
- const matches = image.data.match(/^data:([^;]+);base64,(.+)$/);
- if (!matches) {
- console.error('Invalid image data format');
- continue;
- }
-
- const [, mimeType, base64Data] = matches;
- const extension = mimeType.split('/')[1] || 'png';
- const filename = `image_${index}.${extension}`;
- const filepath = path.join(tempDir, filename);
-
- // Write base64 data to file
- await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
- tempImagePaths.push(filepath);
- }
-
- // Include the full image paths in the prompt for Claude to reference
- // Only modify the command if we actually have images and a command
- if (tempImagePaths.length > 0 && command && command.trim()) {
- const imageNote = `\n\n[Images provided at the following paths:]\n${tempImagePaths.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
- const modifiedCommand = command + imageNote;
-
- // Update the command in args - now that --print and command are separate
- const printIndex = args.indexOf('--print');
- if (printIndex !== -1 && printIndex + 1 < args.length && args[printIndex + 1] === command) {
- args[printIndex + 1] = modifiedCommand;
- }
- }
-
-
- } catch (error) {
- console.error('Error processing images for Claude:', error);
- }
- }
-
- // Add resume flag if resuming
- if (resume && sessionId) {
- args.push('--resume', sessionId);
- }
-
- // Add basic flags
- args.push('--output-format', 'stream-json', '--verbose');
-
- // Add MCP config flag only if MCP servers are configured
- try {
- console.log('🔍 Starting MCP config check...');
- // Use already imported modules (fs.promises is imported as fs, path, os)
- const fsSync = await import('fs'); // Import synchronous fs methods
- console.log('✅ Successfully imported fs sync methods');
-
- // Check for MCP config in ~/.claude.json
- const claudeConfigPath = path.join(os.homedir(), '.claude.json');
-
- console.log(`🔍 Checking for MCP configs in: ${claudeConfigPath}`);
- console.log(` Claude config exists: ${fsSync.existsSync(claudeConfigPath)}`);
-
- let hasMcpServers = false;
-
- // Check Claude config for MCP servers
- if (fsSync.existsSync(claudeConfigPath)) {
- try {
- const claudeConfig = JSON.parse(fsSync.readFileSync(claudeConfigPath, 'utf8'));
-
- // Check global MCP servers
- if (claudeConfig.mcpServers && Object.keys(claudeConfig.mcpServers).length > 0) {
- console.log(`✅ Found ${Object.keys(claudeConfig.mcpServers).length} global MCP servers`);
- hasMcpServers = true;
- }
-
- // Check project-specific MCP servers
- if (!hasMcpServers && claudeConfig.claudeProjects) {
- const currentProjectPath = process.cwd();
- const projectConfig = claudeConfig.claudeProjects[currentProjectPath];
- if (projectConfig && projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0) {
- console.log(`✅ Found ${Object.keys(projectConfig.mcpServers).length} project MCP servers`);
- hasMcpServers = true;
- }
- }
- } catch (e) {
- console.log(`❌ Failed to parse Claude config:`, e.message);
- }
- }
-
- console.log(`🔍 hasMcpServers result: ${hasMcpServers}`);
-
- if (hasMcpServers) {
- // Use Claude config file if it has MCP servers
- let configPath = null;
-
- if (fsSync.existsSync(claudeConfigPath)) {
- try {
- const claudeConfig = JSON.parse(fsSync.readFileSync(claudeConfigPath, 'utf8'));
-
- // Check if we have any MCP servers (global or project-specific)
- const hasGlobalServers = claudeConfig.mcpServers && Object.keys(claudeConfig.mcpServers).length > 0;
- const currentProjectPath = process.cwd();
- const projectConfig = claudeConfig.claudeProjects && claudeConfig.claudeProjects[currentProjectPath];
- const hasProjectServers = projectConfig && projectConfig.mcpServers && Object.keys(projectConfig.mcpServers).length > 0;
-
- if (hasGlobalServers || hasProjectServers) {
- configPath = claudeConfigPath;
- }
- } catch (e) {
- // No valid config found
- }
- }
-
- if (configPath) {
- console.log(`📡 Adding MCP config: ${configPath}`);
- args.push('--mcp-config', configPath);
- } else {
- console.log('⚠️ MCP servers detected but no valid config file found');
- }
- }
- } catch (error) {
- // If there's any error checking for MCP configs, don't add the flag
- console.log('❌ MCP config check failed:', error.message);
- console.log('📍 Error stack:', error.stack);
- console.log('Note: MCP config check failed, proceeding without MCP support');
- }
-
- // Add model for new sessions
- if (!resume) {
- args.push('--model', 'sonnet');
- }
-
- // Add permission mode if specified (works for both new and resumed sessions)
- if (permissionMode && permissionMode !== 'default') {
- args.push('--permission-mode', permissionMode);
- console.log('🔒 Using permission mode:', permissionMode);
- }
-
- // Add tools settings flags
- // Don't use --dangerously-skip-permissions when in plan mode
- if (settings.skipPermissions && permissionMode !== 'plan') {
- args.push('--dangerously-skip-permissions');
- console.log('⚠️ Using --dangerously-skip-permissions (skipping other tool settings)');
- } else {
- // Only add allowed/disallowed tools if not skipping permissions
-
- // Collect all allowed tools, including plan mode defaults
- let allowedTools = [...(settings.allowedTools || [])];
-
- // Add plan mode specific tools
- if (permissionMode === 'plan') {
- const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite'];
- // Add plan mode tools that aren't already in the allowed list
- for (const tool of planModeTools) {
- if (!allowedTools.includes(tool)) {
- allowedTools.push(tool);
- }
- }
- console.log('📝 Plan mode: Added default allowed tools:', planModeTools);
- }
-
- // Add allowed tools
- if (allowedTools.length > 0) {
- for (const tool of allowedTools) {
- args.push('--allowedTools', tool);
- console.log('✅ Allowing tool:', tool);
- }
- }
-
- // Add disallowed tools
- if (settings.disallowedTools && settings.disallowedTools.length > 0) {
- for (const tool of settings.disallowedTools) {
- args.push('--disallowedTools', tool);
- console.log('❌ Disallowing tool:', tool);
- }
- }
-
- // Log when skip permissions is disabled due to plan mode
- if (settings.skipPermissions && permissionMode === 'plan') {
- console.log('📝 Skip permissions disabled due to plan mode');
- }
- }
-
- // Add print flag with command if we have a command
- if (command && command.trim()) {
-
- // Separate arguments for better cross-platform compatibility
- // This prevents issues with spaces and quotes on Windows
- args.push('--print');
- // Use `--` so user input is always treated as text, not options
- args.push('--');
- args.push(command);
- }
-
- console.log('Spawning Claude CLI:', 'claude', args.map(arg => {
- const cleanArg = arg.replace(/\n/g, '\\n').replace(/\r/g, '\\r');
- return cleanArg.includes(' ') ? `"${cleanArg}"` : cleanArg;
- }).join(' '));
- console.log('Working directory:', workingDir);
- console.log('Session info - Input sessionId:', sessionId, 'Resume:', resume);
- console.log('🔍 Full command args:', JSON.stringify(args, null, 2));
- console.log('🔍 Final Claude command will be: claude ' + args.join(' '));
-
- // Use Claude CLI from environment variable or default to 'claude'
- const claudePath = process.env.CLAUDE_CLI_PATH || 'claude';
- console.log('🔍 Using Claude CLI path:', claudePath);
-
- const claudeProcess = spawnFunction(claudePath, args, {
- cwd: workingDir,
- stdio: ['pipe', 'pipe', 'pipe'],
- env: { ...process.env } // Inherit all environment variables
- });
-
- // Attach temp file info to process for cleanup later
- claudeProcess.tempImagePaths = tempImagePaths;
- claudeProcess.tempDir = tempDir;
-
- // Store process reference for potential abort
- const processKey = capturedSessionId || sessionId || Date.now().toString();
- activeClaudeProcesses.set(processKey, claudeProcess);
-
- // Handle stdout (streaming JSON responses)
- claudeProcess.stdout.on('data', (data) => {
- const rawOutput = data.toString();
- console.log('📤 Claude CLI stdout:', rawOutput);
-
- const lines = rawOutput.split('\n').filter(line => line.trim());
-
- for (const line of lines) {
- try {
- const response = JSON.parse(line);
- console.log('📄 Parsed JSON response:', response);
-
- // Capture session ID if it's in the response
- if (response.session_id && !capturedSessionId) {
- capturedSessionId = response.session_id;
- console.log('📝 Captured session ID:', capturedSessionId);
-
- // Update process key with captured session ID
- if (processKey !== capturedSessionId) {
- activeClaudeProcesses.delete(processKey);
- activeClaudeProcesses.set(capturedSessionId, claudeProcess);
- }
-
- // Send session-created event only once for new sessions
- if (!sessionId && !sessionCreatedSent) {
- sessionCreatedSent = true;
- ws.send(JSON.stringify({
- type: 'session-created',
- sessionId: capturedSessionId
- }));
- }
- }
-
- // Send parsed response to WebSocket
- ws.send(JSON.stringify({
- type: 'claude-response',
- data: response
- }));
- } catch (parseError) {
- console.log('📄 Non-JSON response:', line);
- // If not JSON, send as raw text
- ws.send(JSON.stringify({
- type: 'claude-output',
- data: line
- }));
- }
- }
- });
-
- // Handle stderr
- claudeProcess.stderr.on('data', (data) => {
- console.error('Claude CLI stderr:', data.toString());
- ws.send(JSON.stringify({
- type: 'claude-error',
- error: data.toString()
- }));
- });
-
- // Handle process completion
- claudeProcess.on('close', async (code) => {
- console.log(`Claude CLI process exited with code ${code}`);
-
- // Clean up process reference
- const finalSessionId = capturedSessionId || sessionId || processKey;
- activeClaudeProcesses.delete(finalSessionId);
-
- ws.send(JSON.stringify({
- type: 'claude-complete',
- exitCode: code,
- isNewSession: !sessionId && !!command // Flag to indicate this was a new session
- }));
-
- // Clean up temporary image files if any
- if (claudeProcess.tempImagePaths && claudeProcess.tempImagePaths.length > 0) {
- for (const imagePath of claudeProcess.tempImagePaths) {
- await fs.unlink(imagePath).catch(err =>
- console.error(`Failed to delete temp image ${imagePath}:`, err)
- );
- }
- if (claudeProcess.tempDir) {
- await fs.rm(claudeProcess.tempDir, { recursive: true, force: true }).catch(err =>
- console.error(`Failed to delete temp directory ${claudeProcess.tempDir}:`, err)
- );
- }
- }
-
- if (code === 0) {
- resolve();
- } else {
- reject(new Error(`Claude CLI exited with code ${code}`));
- }
- });
-
- // Handle process errors
- claudeProcess.on('error', (error) => {
- console.error('Claude CLI process error:', error);
-
- // Clean up process reference on error
- const finalSessionId = capturedSessionId || sessionId || processKey;
- activeClaudeProcesses.delete(finalSessionId);
-
- ws.send(JSON.stringify({
- type: 'claude-error',
- error: error.message
- }));
-
- reject(error);
- });
-
- // Handle stdin for interactive mode
- if (command) {
- // For --print mode with arguments, we don't need to write to stdin
- claudeProcess.stdin.end();
- } else {
- // For interactive mode, we need to write the command to stdin if provided later
- // Keep stdin open for interactive session
- if (command !== undefined) {
- claudeProcess.stdin.write(command + '\n');
- claudeProcess.stdin.end();
- }
- // If no command provided, stdin stays open for interactive use
- }
- });
-}
-
-function abortClaudeSession(sessionId) {
- const process = activeClaudeProcesses.get(sessionId);
- if (process) {
- console.log(`🛑 Aborting Claude session: ${sessionId}`);
- process.kill('SIGTERM');
- activeClaudeProcesses.delete(sessionId);
- return true;
- }
- return false;
-}
-
-export {
- spawnClaude,
- abortClaudeSession
-};
diff --git a/server/claude-sdk.js b/server/claude-sdk.js
new file mode 100644
index 00000000..204c0a89
--- /dev/null
+++ b/server/claude-sdk.js
@@ -0,0 +1,522 @@
+/**
+ * Claude SDK Integration
+ *
+ * This module provides SDK-based integration with Claude using the @anthropic-ai/claude-agent-sdk.
+ * It mirrors the interface of claude-cli.js but uses the SDK internally for better performance
+ * and maintainability.
+ *
+ * Key features:
+ * - Direct SDK integration without child processes
+ * - Session management with abort capability
+ * - Options mapping between CLI and SDK formats
+ * - WebSocket message streaming
+ */
+
+import { query } from '@anthropic-ai/claude-agent-sdk';
+import { promises as fs } from 'fs';
+import path from 'path';
+import os from 'os';
+
+// Session tracking: Map of session IDs to active query instances
+const activeSessions = new Map();
+
+/**
+ * Maps CLI options to SDK-compatible options format
+ * @param {Object} options - CLI options
+ * @returns {Object} SDK-compatible options
+ */
+function mapCliOptionsToSDK(options = {}) {
+ const { sessionId, cwd, toolsSettings, permissionMode, images } = options;
+
+ const sdkOptions = {};
+
+ // Map working directory
+ if (cwd) {
+ sdkOptions.cwd = cwd;
+ }
+
+ // Map permission mode
+ if (permissionMode && permissionMode !== 'default') {
+ sdkOptions.permissionMode = permissionMode;
+ }
+
+ // Map tool settings
+ const settings = toolsSettings || {
+ allowedTools: [],
+ disallowedTools: [],
+ skipPermissions: false
+ };
+
+ // Handle tool permissions
+ if (settings.skipPermissions && permissionMode !== 'plan') {
+ // When skipping permissions, use bypassPermissions mode
+ sdkOptions.permissionMode = 'bypassPermissions';
+ } else {
+ // Map allowed tools
+ let allowedTools = [...(settings.allowedTools || [])];
+
+ // Add plan mode default tools
+ if (permissionMode === 'plan') {
+ const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite'];
+ for (const tool of planModeTools) {
+ if (!allowedTools.includes(tool)) {
+ allowedTools.push(tool);
+ }
+ }
+ }
+
+ if (allowedTools.length > 0) {
+ sdkOptions.allowedTools = allowedTools;
+ }
+
+ // Map disallowed tools
+ if (settings.disallowedTools && settings.disallowedTools.length > 0) {
+ sdkOptions.disallowedTools = settings.disallowedTools;
+ }
+ }
+
+ // Map model (default to sonnet)
+ // Map model (default to sonnet)
+ sdkOptions.model = options.model || 'sonnet';
+
+ // Map resume session
+ if (sessionId) {
+ sdkOptions.resume = sessionId;
+ }
+
+ return sdkOptions;
+}
+
+/**
+ * Adds a session to the active sessions map
+ * @param {string} sessionId - Session identifier
+ * @param {Object} queryInstance - SDK query instance
+ * @param {Array} tempImagePaths - Temp image file paths for cleanup
+ * @param {string} tempDir - Temp directory for cleanup
+ */
+function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null) {
+ activeSessions.set(sessionId, {
+ instance: queryInstance,
+ startTime: Date.now(),
+ status: 'active',
+ tempImagePaths,
+ tempDir
+ });
+}
+
+/**
+ * Removes a session from the active sessions map
+ * @param {string} sessionId - Session identifier
+ */
+function removeSession(sessionId) {
+ activeSessions.delete(sessionId);
+}
+
+/**
+ * Gets a session from the active sessions map
+ * @param {string} sessionId - Session identifier
+ * @returns {Object|undefined} Session data or undefined
+ */
+function getSession(sessionId) {
+ return activeSessions.get(sessionId);
+}
+
+/**
+ * Gets all active session IDs
+ * @returns {Array} Array of active session IDs
+ */
+function getAllSessions() {
+ return Array.from(activeSessions.keys());
+}
+
+/**
+ * Transforms SDK messages to WebSocket format expected by frontend
+ * @param {Object} sdkMessage - SDK message object
+ * @returns {Object} Transformed message ready for WebSocket
+ */
+function transformMessage(sdkMessage) {
+ // SDK messages are already in a format compatible with the frontend
+ // The CLI sends them wrapped in {type: 'claude-response', data: message}
+ // We'll do the same here to maintain compatibility
+ return sdkMessage;
+}
+
+/**
+ * Extracts token usage from SDK result messages
+ * @param {Object} resultMessage - SDK result message
+ * @returns {Object|null} Token budget object or null
+ */
+function extractTokenBudget(resultMessage) {
+ if (resultMessage.type !== 'result' || !resultMessage.modelUsage) {
+ return null;
+ }
+
+ // Get the first model's usage data
+ const modelKey = Object.keys(resultMessage.modelUsage)[0];
+ const modelData = resultMessage.modelUsage[modelKey];
+
+ if (!modelData) {
+ return null;
+ }
+
+ // Use cumulative tokens if available (tracks total for the session)
+ // Otherwise fall back to per-request tokens
+ const inputTokens = modelData.cumulativeInputTokens || modelData.inputTokens || 0;
+ const outputTokens = modelData.cumulativeOutputTokens || modelData.outputTokens || 0;
+ const cacheReadTokens = modelData.cumulativeCacheReadInputTokens || modelData.cacheReadInputTokens || 0;
+ const cacheCreationTokens = modelData.cumulativeCacheCreationInputTokens || modelData.cacheCreationInputTokens || 0;
+
+ // Total used = input + output + cache tokens
+ const totalUsed = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
+
+ // Use configured context window budget from environment (default 160000)
+ // This is the user's budget limit, not the model's context window
+ const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000;
+
+ console.log(`📊 Token calculation: input=${inputTokens}, output=${outputTokens}, cache=${cacheReadTokens + cacheCreationTokens}, total=${totalUsed}/${contextWindow}`);
+
+ return {
+ used: totalUsed,
+ total: contextWindow
+ };
+}
+
+/**
+ * Handles image processing for SDK queries
+ * Saves base64 images to temporary files and returns modified prompt with file paths
+ * @param {string} command - Original user prompt
+ * @param {Array} images - Array of image objects with base64 data
+ * @param {string} cwd - Working directory for temp file creation
+ * @returns {Promise