diff --git a/package-lock.json b/package-lock.json index 5c28f150..bd246816 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@anthropic-ai/claude-code": "^1.0.70", + "@anthropic-ai/claude-code": "^2.1.19", "@anthropic-ai/sdk": "^0.54.0", "@google/genai": "^1.11.0", "@modelcontextprotocol/sdk": "^1.17.0", @@ -95,7 +95,9 @@ "node": ">=20.19.0" }, "optionalDependencies": { + "@tailwindcss/oxide-linux-arm64-gnu": "^4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "^4.1.11", + "lightningcss-linux-arm64-gnu": "^1.31.1", "lightningcss-linux-x64-gnu": "^1.30.1" } }, @@ -112,9 +114,9 @@ } }, "node_modules/@anthropic-ai/claude-code": { - "version": "1.0.70", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-1.0.70.tgz", - "integrity": "sha512-gJ/bdT/XQ/hp5EKM0QoOWj/eKmK3wvs1TotTLq1unqahiB6B+EAQeRy/uvxv2Ua9nI8p5Bogw8hXB1uUmAHb+A==", + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-code/-/claude-code-2.1.19.tgz", + "integrity": "sha512-/bUlQuX/6nKr1Zqfi/9Q6xf7WonUBk72ZfKKENU4WVrIFWqTv/0JJsoW/dHol9QBNHvyfKIeBbYu4avHNRAnuQ==", "license": "SEE LICENSE IN README.md", "bin": { "claude": "cli.js" @@ -128,6 +130,8 @@ "@img/sharp-linux-arm": "^0.33.5", "@img/sharp-linux-arm64": "^0.33.5", "@img/sharp-linux-x64": "^0.33.5", + "@img/sharp-linuxmusl-arm64": "^0.33.5", + "@img/sharp-linuxmusl-x64": "^0.33.5", "@img/sharp-win32-x64": "^0.33.5" } }, @@ -2299,6 +2303,38 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "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", @@ -2365,6 +2401,50 @@ "@img/sharp-libvips-linux-x64": "1.0.4" } }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "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-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "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-linuxmusl-x64": "1.0.4" + } + }, "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", @@ -4117,6 +4197,23 @@ "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tailwindcss/oxide-darwin-arm64": { "version": "4.1.11", "cpu": [ @@ -4132,6 +4229,90 @@ "node": ">= 10" } }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", @@ -4148,6 +4329,104 @@ "node": ">= 10" } }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide/node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tailwindcss/typography": { "version": "0.5.16", "dev": true, @@ -9191,6 +9470,110 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", @@ -9211,6 +9594,90 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "license": "MIT" diff --git a/package.json b/package.json index 62a7c3d1..009b36ed 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "scripts": { "dev": "NODE_ENV=development tsx watch src/server.ts", + "dev:api": "NODE_ENV=development API_ONLY=true tsx watch src/server.ts", "dev:web": "NODE_ENV=development npx vite", "clean": "rm -rf dist", "build": "npm run clean && NODE_ENV=production npm run build:web && tsc && tsc-alias && npm run build:mcp", @@ -54,7 +55,7 @@ "postinstall": "node scripts/postinstall.js" }, "dependencies": { - "@anthropic-ai/claude-code": "^1.0.70", + "@anthropic-ai/claude-code": "^2.1.19", "@anthropic-ai/sdk": "^0.54.0", "@google/genai": "^1.11.0", "@modelcontextprotocol/sdk": "^1.17.0", @@ -133,7 +134,9 @@ "vitest": "^3.2.4" }, "optionalDependencies": { + "@tailwindcss/oxide-linux-arm64-gnu": "^4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "^4.1.11", + "lightningcss-linux-arm64-gnu": "^1.31.1", "lightningcss-linux-x64-gnu": "^1.30.1" } } diff --git a/src/cui-server.ts b/src/cui-server.ts index a8045462..c51522b3 100644 --- a/src/cui-server.ts +++ b/src/cui-server.ts @@ -192,11 +192,12 @@ export class CUIServer { this.processManager.setMCPConfigPath(mcpConfigPath); this.logger.debug('MCP config generated and set', { path: mcpConfigPath }); } catch (error) { - const isTestEnv = process.env.NODE_ENV === 'test'; - - if (isTestEnv) { - this.logger.warn('MCP config generation failed in test environment, proceeding without MCP', { - error: error instanceof Error ? error.message : String(error) + const isNonProduction = process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development'; + + if (isNonProduction) { + this.logger.warn('MCP config generation failed, proceeding without MCP', { + error: error instanceof Error ? error.message : String(error), + env: process.env.NODE_ENV }); // Don't set MCP config path - conversations will run without MCP } else { @@ -253,20 +254,22 @@ export class CUIServer { // Start Express server const isDev = process.env.NODE_ENV === 'development'; - this.logger.debug('Creating HTTP server listener', { - useViteExpress: isDev, - environment: process.env.NODE_ENV + const apiOnly = process.env.API_ONLY === 'true'; // Skip Vite for API-only mode + this.logger.debug('Creating HTTP server listener', { + useViteExpress: isDev && !apiOnly, + apiOnly, + environment: process.env.NODE_ENV }); - - // Import ViteExpress dynamically if in development mode - if (isDev && !ViteExpress) { + + // Import ViteExpress dynamically if in development mode (and not API-only) + if (isDev && !apiOnly && !ViteExpress) { const viteExpressModule = await import('vite-express'); ViteExpress = viteExpressModule.default; } - + await new Promise((resolve, reject) => { - // Use ViteExpress only in development - if (isDev && ViteExpress) { + // Use ViteExpress only in development (and not API-only) + if (isDev && !apiOnly && ViteExpress) { try { this.server = this.app.listen(this.port, this.host, () => { this.logger.debug('Server successfully bound to port (dev mode)', { diff --git a/src/routes/conversation.routes.ts b/src/routes/conversation.routes.ts index 4ecc3feb..8196c8ba 100644 --- a/src/routes/conversation.routes.ts +++ b/src/routes/conversation.routes.ts @@ -115,8 +115,10 @@ export function createConversationRoutes( const { streamingId, systemInit } = await processManager.startConversation(conversationConfig); - // Update original session with continuation session ID if resuming - if (req.body.resumedSessionId) { + // Update original session with continuation session ID if resuming AND session ID changed + // Note: Claude CLI 2.x reuses the same session ID by default when resuming + // Claude CLI 1.x may create a new session ID when resuming + if (req.body.resumedSessionId && systemInit.session_id !== req.body.resumedSessionId) { try { await sessionInfoService.updateSessionInfo(req.body.resumedSessionId, { continuation_session_id: systemInit.session_id @@ -131,8 +133,11 @@ export function createConversationRoutes( error: error instanceof Error ? error.message : String(error) }); } - - // Register the resumed session with conversation status manager including previous messages + } + + // Register the resumed session with conversation status manager including previous messages + // This must happen regardless of whether session ID changed (Claude 2.x reuses same ID) + if (req.body.resumedSessionId) { try { conversationStatusManager.registerActiveSession( streamingId, @@ -146,9 +151,10 @@ export function createConversationRoutes( ); logger.debug('Registered resumed session with inherited messages', { requestId, - newSessionId: systemInit.session_id, + sessionId: systemInit.session_id, streamingId, - inheritedMessageCount: previousMessages.length + inheritedMessageCount: previousMessages.length, + sessionIdChanged: systemInit.session_id !== req.body.resumedSessionId }); } catch (error) { logger.warn('Failed to register resumed session with status manager', { diff --git a/src/services/claude-process-manager.ts b/src/services/claude-process-manager.ts index 9d1e37b5..8552eafa 100644 --- a/src/services/claude-process-manager.ts +++ b/src/services/claude-process-manager.ts @@ -58,19 +58,20 @@ export class ClaudeProcessManager extends EventEmitter { } /** - * Find the Claude executable from node_modules - * Since @anthropic-ai/claude-code is a dependency, claude should be in node_modules/.bin + * Find the Claude executable + * Priority: node_modules (bundled) > PATH > global install */ private findClaudeExecutable(): string { + // First: find claude from node_modules (bundled version for consistency) // When running as an npm package, find claude relative to this module // __dirname will be something like /path/to/node_modules/cui-server/dist/services const packageRoot = path.resolve(__dirname, '..', '..'); const claudePath = path.join(packageRoot, 'node_modules', '.bin', 'claude'); - + if (existsSync(claudePath)) { return claudePath; } - + // Try from the parent node_modules (when cui-server is installed as a dependency) // packageRoot -> /node_modules/cui-server // parent -> /node_modules, so /node_modules/.bin/claude @@ -78,24 +79,14 @@ export class ClaudeProcessManager extends EventEmitter { if (existsSync(parentModulesPath)) { return parentModulesPath; } - + // Fallback: try from current working directory (for local development) const cwdPath = path.join(process.cwd(), 'node_modules', '.bin', 'claude'); if (existsSync(cwdPath)) { return cwdPath; } - - // Final fallback: try to locate on PATH - const pathEnv = process.env.PATH || ''; - const pathDirs = pathEnv.split(path.delimiter); - for (const dir of pathDirs) { - const candidate = path.join(dir, 'claude'); - if (existsSync(candidate)) { - return candidate; - } - } - - throw new Error('Claude executable not found in node_modules. Ensure @anthropic-ai/claude-code is installed.'); + + throw new Error('Claude executable not found. Ensure @anthropic-ai/claude-code is installed globally or in node_modules.'); } /** diff --git a/src/web/chat/components/ConversationView/ConversationView.tsx b/src/web/chat/components/ConversationView/ConversationView.tsx index 8a5d0c63..0cd83051 100644 --- a/src/web/chat/components/ConversationView/ConversationView.tsx +++ b/src/web/chat/components/ConversationView/ConversationView.tsx @@ -169,8 +169,25 @@ export function ConversationView() { permissionMode }); - // Navigate immediately to the new session - navigate(`/c/${response.sessionId}`); + // Add optimistic user message immediately + const messageId = `optimistic-${Date.now()}`; + const optimisticUserMessage: ChatMessage = { + id: messageId, + messageId: messageId, + type: 'user', + content: message, + timestamp: new Date().toISOString(), + workingDirectory: workingDirectory || currentWorkingDirectory, + }; + addMessage(optimisticUserMessage); + + // Set streamingId immediately to start receiving messages + setStreamingId(response.streamingId); + + // Navigate to the new session (may be same sessionId in Claude 2.x) + if (response.sessionId !== sessionId) { + navigate(`/c/${response.sessionId}`); + } } catch (err: any) { setError(err.message || 'Failed to send message'); } diff --git a/src/web/chat/contexts/ConversationsContext.tsx b/src/web/chat/contexts/ConversationsContext.tsx index 622df22a..772c1ad8 100644 --- a/src/web/chat/contexts/ConversationsContext.tsx +++ b/src/web/chat/contexts/ConversationsContext.tsx @@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from import { api } from '../services/api'; import { useStreamStatus } from './StreamStatusContext'; import type { ConversationSummary, WorkingDirectory, ConversationSummaryWithLiveStatus } from '../types'; +import { isPathUnderHome } from '../utils/path-utils'; interface RecentDirectory { lastDate: string; @@ -59,20 +60,24 @@ export function ConversationsProvider({ children }: { children: ReactNode }) { const updateRecentDirectories = (convs: ConversationSummary[], apiDirectories?: Record | null) => { const newDirectories: Record = {}; - - // First, add API directories if available + + // First, add API directories if available (filter to home paths only) if (apiDirectories) { - Object.assign(newDirectories, apiDirectories); + Object.entries(apiDirectories).forEach(([path, dir]) => { + if (isPathUnderHome(path)) { + newDirectories[path] = dir; + } + }); } - - // Then, process conversations and merge with API data + + // Then, process conversations and merge with API data (filter to home paths only) convs.forEach(conv => { - if (conv.projectPath) { + if (conv.projectPath && isPathUnderHome(conv.projectPath)) { const pathParts = conv.projectPath.split('/'); const shortname = pathParts[pathParts.length - 1] || conv.projectPath; - + // If API didn't provide this directory, or if conversation is more recent - if (!newDirectories[conv.projectPath] || + if (!newDirectories[conv.projectPath] || new Date(conv.updatedAt) > new Date(newDirectories[conv.projectPath].lastDate)) { newDirectories[conv.projectPath] = { lastDate: conv.updatedAt, @@ -81,7 +86,7 @@ export function ConversationsProvider({ children }: { children: ReactNode }) { } } }); - + setRecentDirectories(newDirectories); }; diff --git a/src/web/chat/hooks/useStreaming.ts b/src/web/chat/hooks/useStreaming.ts index b1384221..2bbf0794 100644 --- a/src/web/chat/hooks/useStreaming.ts +++ b/src/web/chat/hooks/useStreaming.ts @@ -47,8 +47,13 @@ export function useStreaming( }, []); const connect = useCallback(async () => { - // Guard against multiple connections - if (!streamingId || readerRef.current || abortControllerRef.current) { + // Guard against connection without streamingId + if (!streamingId) { + return; + } + + // If already connecting/connected, skip + if (abortControllerRef.current) { return; } @@ -139,10 +144,26 @@ export function useStreaming( }, [streamingId, disconnect]); useEffect(() => { + // Clean up previous connection before establishing new one + if (readerRef.current) { + readerRef.current.cancel().catch(() => {}); + readerRef.current = null; + } + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + setIsConnected(false); + if (streamingId) { - connect(); - } else { - disconnect(); + // Small delay to ensure cleanup is complete + const timeoutId = setTimeout(() => { + connect(); + }, 50); + return () => { + clearTimeout(timeoutId); + disconnect(); + }; } return () => { diff --git a/src/web/chat/services/api.ts b/src/web/chat/services/api.ts index b161cba6..1be91cb4 100644 --- a/src/web/chat/services/api.ts +++ b/src/web/chat/services/api.ts @@ -13,6 +13,7 @@ import type { CommandsResponse, } from '../types'; import { getAuthToken } from '../../hooks/useAuth'; +import { hostPathToVmPath, vmPathToHostPath } from '../utils/path-utils'; type GeminiHealthResponse = { status: 'healthy' | 'unhealthy'; message: string; apiKeyValid: boolean }; class ApiService { @@ -74,25 +75,55 @@ class ApiService { const searchParams = new URLSearchParams(); if (params?.limit) searchParams.append('limit', params.limit.toString()); if (params?.offset) searchParams.append('offset', params.offset.toString()); - if (params?.projectPath) searchParams.append('projectPath', params.projectPath); + // Convert host path to VM path for filtering + if (params?.projectPath) searchParams.append('projectPath', hostPathToVmPath(params.projectPath)); if (params?.hasContinuation !== undefined) searchParams.append('hasContinuation', params.hasContinuation.toString()); if (params?.archived !== undefined) searchParams.append('archived', params.archived.toString()); if (params?.pinned !== undefined) searchParams.append('pinned', params.pinned.toString()); searchParams.append('sortBy', 'updated'); searchParams.append('order', 'desc'); - return this.apiCall(`/api/conversations?${searchParams}`); + const response = await this.apiCall<{ conversations: ConversationSummary[]; total: number }>(`/api/conversations?${searchParams}`); + + // Convert VM paths back to host paths for display + return { + ...response, + conversations: response.conversations.map((conv) => ({ + ...conv, + projectPath: vmPathToHostPath(conv.projectPath), + })), + }; } async getConversationDetails(sessionId: string): Promise { - return this.apiCall(`/api/conversations/${sessionId}`); + const response = await this.apiCall(`/api/conversations/${sessionId}`); + + // Convert VM path back to host path for display + return { + ...response, + projectPath: vmPathToHostPath(response.projectPath), + }; } async startConversation(request: StartConversationRequest): Promise { - return this.apiCall('/api/conversations/start', { + // Convert host path to VM path before sending to API + const vmRequest = { + ...request, + workingDirectory: request.workingDirectory + ? hostPathToVmPath(request.workingDirectory) + : request.workingDirectory, + }; + + const response = await this.apiCall('/api/conversations/start', { method: 'POST', - body: JSON.stringify(request), + body: JSON.stringify(vmRequest), }); + + // Convert VM path back to host path for display + return { + ...response, + cwd: response.cwd ? vmPathToHostPath(response.cwd) : response.cwd, + }; } @@ -107,7 +138,16 @@ class ApiService { } async getWorkingDirectories(): Promise { - return this.apiCall('/api/working-directories'); + const response = await this.apiCall('/api/working-directories'); + + // Convert VM paths back to host paths for display + return { + ...response, + directories: response.directories.map((dir) => ({ + ...dir, + path: vmPathToHostPath(dir.path), + })), + }; } async getPermissions(params?: { @@ -149,19 +189,30 @@ class ApiService { async listDirectory(params: FileSystemListQuery): Promise { const searchParams = new URLSearchParams(); - searchParams.append('path', params.path); + // Convert host path to VM path + searchParams.append('path', hostPathToVmPath(params.path)); if (params.recursive !== undefined) searchParams.append('recursive', params.recursive.toString()); if (params.respectGitignore !== undefined) searchParams.append('respectGitignore', params.respectGitignore.toString()); - - return this.apiCall(`/api/filesystem/list?${searchParams}`); + + const response = await this.apiCall(`/api/filesystem/list?${searchParams}`); + + // Convert VM paths back to host paths for display + return { + ...response, + entries: response.entries.map((entry) => ({ + ...entry, + path: vmPathToHostPath(entry.path), + })), + }; } async getCommands(workingDirectory?: string): Promise { const searchParams = new URLSearchParams(); if (workingDirectory) { - searchParams.append('workingDirectory', workingDirectory); + // Convert host path to VM path + searchParams.append('workingDirectory', hostPathToVmPath(workingDirectory)); } - + return this.apiCall(`/api/system/commands?${searchParams}`); } @@ -181,7 +232,8 @@ class ApiService { async readFile(path: string): Promise<{ content: string }> { const searchParams = new URLSearchParams(); - searchParams.append('path', path); + // Convert host path to VM path + searchParams.append('path', hostPathToVmPath(path)); return this.apiCall(`/api/filesystem/read?${searchParams}`); } diff --git a/vite.config.mts b/vite.config.mts index dde5da0d..6e262691 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -44,6 +44,12 @@ export default defineConfig({ server: { port: 3000, host: '0.0.0.0', - allowedHosts: ['wenbo-macbook.dala-cobia.ts.net', 'cui.wenbo.io', 'localhost', '127.0.0.1', 'cui1.wenbo.io', 'cui2.wenbo.io', 'cui.tai.chat', 'measurements-struggle-slight-campbell.trycloudflare.com'], + allowedHosts: ['wenbo-macbook.dala-cobia.ts.net', 'cui.wenbo.io', 'localhost', '127.0.0.1', 'cui1.wenbo.io', 'cui2.wenbo.io', 'cui.tai.chat', 'measurements-struggle-slight-campbell.trycloudflare.com', 'alpha.u2032338.nyat.app'], + proxy: { + '/api': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + }, } })