@cloudcli-ai/cloudcli 1.29.3 → 1.29.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{index-D-9h5kNf.css → index-BBAE1OJ_.css} +1 -1
- package/dist/assets/{index-y8yqvoLv.js → index-Ci1GOyV7.js} +200 -200
- package/dist/index.html +2 -2
- package/dist-server/server/claude-sdk.js +11 -3
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cursor-cli.js +7 -1
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +15 -1
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/index.js +7 -76
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/openai-codex.js +7 -1
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/projects.js +41 -47
- package/dist-server/server/projects.js.map +1 -1
- package/dist-server/server/providers/claude/status.js +121 -0
- package/dist-server/server/providers/claude/status.js.map +1 -0
- package/dist-server/server/providers/codex/status.js +70 -0
- package/dist-server/server/providers/codex/status.js.map +1 -0
- package/dist-server/server/providers/cursor/adapter.js +5 -10
- package/dist-server/server/providers/cursor/adapter.js.map +1 -1
- package/dist-server/server/providers/cursor/status.js +118 -0
- package/dist-server/server/providers/cursor/status.js.map +1 -0
- package/dist-server/server/providers/gemini/status.js +104 -0
- package/dist-server/server/providers/gemini/status.js.map +1 -0
- package/dist-server/server/providers/registry.js +21 -2
- package/dist-server/server/providers/registry.js.map +1 -1
- package/dist-server/server/providers/types.js +11 -0
- package/dist-server/server/providers/types.js.map +1 -1
- package/dist-server/server/routes/cli-auth.js +14 -395
- package/dist-server/server/routes/cli-auth.js.map +1 -1
- package/dist-server/server/routes/cursor.js +6 -22
- package/dist-server/server/routes/cursor.js.map +1 -1
- package/dist-server/server/utils/colors.js +20 -0
- package/dist-server/server/utils/colors.js.map +1 -0
- package/dist-server/server/utils/url-detection.js +58 -0
- package/dist-server/server/utils/url-detection.js.map +1 -0
- package/package.json +4 -6
- package/server/claude-sdk.js +13 -4
- package/server/cursor-cli.js +8 -1
- package/server/gemini-cli.js +17 -1
- package/server/index.js +7 -83
- package/server/openai-codex.js +9 -1
- package/server/projects.js +38 -44
- package/server/providers/claude/status.js +136 -0
- package/server/providers/codex/status.js +78 -0
- package/server/providers/cursor/adapter.js +5 -10
- package/server/providers/cursor/status.js +128 -0
- package/server/providers/gemini/status.js +111 -0
- package/server/providers/registry.js +25 -2
- package/server/providers/types.js +13 -0
- package/server/routes/cli-auth.js +14 -421
- package/server/routes/cursor.js +6 -22
- package/server/utils/colors.js +21 -0
- package/server/utils/url-detection.js +71 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const ANSI_ESCAPE_SEQUENCE_REGEX = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g;
|
|
2
|
+
const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/;
|
|
3
|
+
function stripAnsiSequences(value = '') {
|
|
4
|
+
return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
|
|
5
|
+
}
|
|
6
|
+
function normalizeDetectedUrl(url) {
|
|
7
|
+
if (!url || typeof url !== 'string')
|
|
8
|
+
return null;
|
|
9
|
+
const cleaned = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, '');
|
|
10
|
+
if (!cleaned)
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
const parsed = new URL(cleaned);
|
|
14
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return parsed.toString();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function extractUrlsFromText(value = '') {
|
|
24
|
+
const directMatches = value.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/gi) || [];
|
|
25
|
+
// Handle wrapped terminal URLs split across lines by terminal width.
|
|
26
|
+
const wrappedMatches = [];
|
|
27
|
+
const continuationRegex = /^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/;
|
|
28
|
+
const lines = value.split(/\r?\n/);
|
|
29
|
+
for (let i = 0; i < lines.length; i++) {
|
|
30
|
+
const line = lines[i].trim();
|
|
31
|
+
const startMatch = line.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/i);
|
|
32
|
+
if (!startMatch)
|
|
33
|
+
continue;
|
|
34
|
+
let combined = startMatch[0];
|
|
35
|
+
let j = i + 1;
|
|
36
|
+
while (j < lines.length) {
|
|
37
|
+
const continuation = lines[j].trim();
|
|
38
|
+
if (!continuation)
|
|
39
|
+
break;
|
|
40
|
+
if (!continuationRegex.test(continuation))
|
|
41
|
+
break;
|
|
42
|
+
combined += continuation;
|
|
43
|
+
j++;
|
|
44
|
+
}
|
|
45
|
+
wrappedMatches.push(combined.replace(/\r?\n\s*/g, ''));
|
|
46
|
+
}
|
|
47
|
+
return Array.from(new Set([...directMatches, ...wrappedMatches]));
|
|
48
|
+
}
|
|
49
|
+
function shouldAutoOpenUrlFromOutput(value = '') {
|
|
50
|
+
const normalized = value.toLowerCase();
|
|
51
|
+
return (normalized.includes('browser didn\'t open') ||
|
|
52
|
+
normalized.includes('open this url') ||
|
|
53
|
+
normalized.includes('continue in your browser') ||
|
|
54
|
+
normalized.includes('press enter to open') ||
|
|
55
|
+
normalized.includes('open_url:'));
|
|
56
|
+
}
|
|
57
|
+
export { ANSI_ESCAPE_SEQUENCE_REGEX, TRAILING_URL_PUNCTUATION_REGEX, stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput };
|
|
58
|
+
//# sourceMappingURL=url-detection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url-detection.js","sourceRoot":"","sources":["../../../server/utils/url-detection.js"],"names":[],"mappings":"AAAA,MAAM,0BAA0B,GAAG,kEAAkE,CAAC;AACtG,MAAM,8BAA8B,GAAG,iBAAiB,CAAC;AAEzD,SAAS,kBAAkB,CAAC,KAAK,GAAG,EAAE;IAClC,OAAO,KAAK,CAAC,OAAO,CAAC,0BAA0B,EAAE,EAAE,CAAC,CAAC;AACzD,CAAC;AAED,SAAS,oBAAoB,CAAC,GAAG;IAC7B,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAEjD,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,8BAA8B,EAAE,EAAE,CAAC,CAAC;IACvE,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,IAAI,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YAC9D,OAAO,IAAI,CAAC;QAChB,CAAC;QACD,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,IAAI,CAAC;IAChB,CAAC;AACL,CAAC;AAED,SAAS,mBAAmB,CAAC,KAAK,GAAG,EAAE;IACnC,MAAM,aAAa,GAAG,KAAK,CAAC,KAAK,CAAC,oCAAoC,CAAC,IAAI,EAAE,CAAC;IAE9E,qEAAqE;IACrE,MAAM,cAAc,GAAG,EAAE,CAAC;IAC1B,MAAM,iBAAiB,GAAG,0CAA0C,CAAC;IACrE,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7B,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,mCAAmC,CAAC,CAAC;QACnE,IAAI,CAAC,UAAU;YAAE,SAAS;QAE1B,IAAI,QAAQ,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YACtB,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;YACrC,IAAI,CAAC,YAAY;gBAAE,MAAM;YACzB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,YAAY,CAAC;gBAAE,MAAM;YACjD,QAAQ,IAAI,YAAY,CAAC;YACzB,CAAC,EAAE,CAAC;QACR,CAAC;QAED,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,aAAa,EAAE,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;AACtE,CAAC;AAED,SAAS,2BAA2B,CAAC,KAAK,GAAG,EAAE;IAC3C,MAAM,UAAU,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IACvC,OAAO,CACH,UAAU,CAAC,QAAQ,CAAC,sBAAsB,CAAC;QAC3C,UAAU,CAAC,QAAQ,CAAC,eAAe,CAAC;QACpC,UAAU,CAAC,QAAQ,CAAC,0BAA0B,CAAC;QAC/C,UAAU,CAAC,QAAQ,CAAC,qBAAqB,CAAC;QAC1C,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC,CACnC,CAAC;AACN,CAAC;AAED,OAAO,EACH,0BAA0B,EAC1B,8BAA8B,EAC9B,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,2BAA2B,EAC9B,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudcli-ai/cloudcli",
|
|
3
|
-
"version": "1.29.
|
|
3
|
+
"version": "1.29.5",
|
|
4
4
|
"description": "A web-based UI for Claude Code CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist-server/server/index.js",
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"mime-types": "^3.0.1",
|
|
105
105
|
"multer": "^2.0.1",
|
|
106
106
|
"node-fetch": "^2.7.0",
|
|
107
|
-
"node-pty": "^1.
|
|
107
|
+
"node-pty": "^1.2.0-beta.12",
|
|
108
108
|
"react": "^18.2.0",
|
|
109
109
|
"react-dom": "^18.2.0",
|
|
110
110
|
"react-dropzone": "^14.2.3",
|
|
@@ -117,15 +117,13 @@
|
|
|
117
117
|
"rehype-raw": "^7.0.0",
|
|
118
118
|
"remark-gfm": "^4.0.0",
|
|
119
119
|
"remark-math": "^6.0.0",
|
|
120
|
-
"sqlite": "^5.1.1",
|
|
121
|
-
"sqlite3": "^5.1.7",
|
|
122
120
|
"tailwind-merge": "^3.3.1",
|
|
123
121
|
"web-push": "^3.6.7",
|
|
124
122
|
"ws": "^8.14.2"
|
|
125
123
|
},
|
|
126
124
|
"devDependencies": {
|
|
127
|
-
"@commitlint/cli": "^20.
|
|
128
|
-
"@commitlint/config-conventional": "^20.
|
|
125
|
+
"@commitlint/cli": "^20.5.0",
|
|
126
|
+
"@commitlint/config-conventional": "^20.5.0",
|
|
129
127
|
"@eslint/js": "^9.39.3",
|
|
130
128
|
"@release-it/conventional-changelog": "^10.0.5",
|
|
131
129
|
"@types/node": "^22.19.7",
|
package/server/claude-sdk.js
CHANGED
|
@@ -26,13 +26,14 @@ import {
|
|
|
26
26
|
} from './services/notification-orchestrator.js';
|
|
27
27
|
import { claudeAdapter } from './providers/claude/adapter.js';
|
|
28
28
|
import { createNormalizedMessage } from './providers/types.js';
|
|
29
|
+
import { getStatusChecker } from './providers/registry.js';
|
|
29
30
|
|
|
30
31
|
const activeSessions = new Map();
|
|
31
32
|
const pendingToolApprovals = new Map();
|
|
32
33
|
|
|
33
34
|
const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
|
|
34
35
|
|
|
35
|
-
const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion']);
|
|
36
|
+
const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion', 'ExitPlanMode']);
|
|
36
37
|
|
|
37
38
|
function createRequestId() {
|
|
38
39
|
if (typeof crypto.randomUUID === 'function') {
|
|
@@ -148,6 +149,10 @@ function mapCliOptionsToSDK(options = {}) {
|
|
|
148
149
|
|
|
149
150
|
const sdkOptions = {};
|
|
150
151
|
|
|
152
|
+
if (process.env.CLAUDE_CLI_PATH) {
|
|
153
|
+
sdkOptions.pathToClaudeCodeExecutable = process.env.CLAUDE_CLI_PATH;
|
|
154
|
+
}
|
|
155
|
+
|
|
151
156
|
// Map working directory
|
|
152
157
|
if (cwd) {
|
|
153
158
|
sdkOptions.cwd = cwd;
|
|
@@ -701,8 +706,14 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
701
706
|
// Clean up temporary image files on error
|
|
702
707
|
await cleanupTempFiles(tempImagePaths, tempDir);
|
|
703
708
|
|
|
709
|
+
// Check if Claude CLI is installed for a clearer error message
|
|
710
|
+
const installed = getStatusChecker('claude')?.checkInstalled() ?? true;
|
|
711
|
+
const errorContent = !installed
|
|
712
|
+
? 'Claude Code is not installed. Please install it first: https://docs.anthropic.com/en/docs/claude-code'
|
|
713
|
+
: error.message;
|
|
714
|
+
|
|
704
715
|
// Send error to WebSocket
|
|
705
|
-
ws.send(createNormalizedMessage({ kind: 'error', content:
|
|
716
|
+
ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'claude' }));
|
|
706
717
|
notifyRunFailed({
|
|
707
718
|
userId: ws?.userId || null,
|
|
708
719
|
provider: 'claude',
|
|
@@ -710,8 +721,6 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
710
721
|
sessionName: sessionSummary,
|
|
711
722
|
error
|
|
712
723
|
});
|
|
713
|
-
|
|
714
|
-
throw error;
|
|
715
724
|
}
|
|
716
725
|
}
|
|
717
726
|
|
package/server/cursor-cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import crossSpawn from 'cross-spawn';
|
|
|
3
3
|
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
4
4
|
import { cursorAdapter } from './providers/cursor/adapter.js';
|
|
5
5
|
import { createNormalizedMessage } from './providers/types.js';
|
|
6
|
+
import { getStatusChecker } from './providers/registry.js';
|
|
6
7
|
|
|
7
8
|
// Use cross-spawn on Windows for better command execution
|
|
8
9
|
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
@@ -294,7 +295,13 @@ async function spawnCursor(command, options = {}, ws) {
|
|
|
294
295
|
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
295
296
|
activeCursorProcesses.delete(finalSessionId);
|
|
296
297
|
|
|
297
|
-
|
|
298
|
+
// Check if Cursor CLI is installed for a clearer error message
|
|
299
|
+
const installed = getStatusChecker('cursor')?.checkInstalled() ?? true;
|
|
300
|
+
const errorContent = !installed
|
|
301
|
+
? 'Cursor CLI is not installed. Please install it from https://cursor.com'
|
|
302
|
+
: error.message;
|
|
303
|
+
|
|
304
|
+
ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: capturedSessionId || sessionId || null, provider: 'cursor' }));
|
|
298
305
|
notifyTerminalState({ error });
|
|
299
306
|
|
|
300
307
|
settleOnce(() => reject(error));
|
package/server/gemini-cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import sessionManager from './sessionManager.js';
|
|
|
10
10
|
import GeminiResponseHandler from './gemini-response-handler.js';
|
|
11
11
|
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
12
12
|
import { createNormalizedMessage } from './providers/types.js';
|
|
13
|
+
import { getStatusChecker } from './providers/registry.js';
|
|
13
14
|
|
|
14
15
|
let activeGeminiProcesses = new Map(); // Track active processes by session ID
|
|
15
16
|
|
|
@@ -380,6 +381,15 @@ async function spawnGemini(command, options = {}, ws) {
|
|
|
380
381
|
notifyTerminalState({ code });
|
|
381
382
|
resolve();
|
|
382
383
|
} else {
|
|
384
|
+
// code 127 = shell "command not found" — check installation
|
|
385
|
+
if (code === 127) {
|
|
386
|
+
const installed = getStatusChecker('gemini')?.checkInstalled() ?? true;
|
|
387
|
+
if (!installed) {
|
|
388
|
+
const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
|
|
389
|
+
ws.send(createNormalizedMessage({ kind: 'error', content: 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli', sessionId: socketSessionId, provider: 'gemini' }));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
383
393
|
notifyTerminalState({
|
|
384
394
|
code,
|
|
385
395
|
error: code === null ? 'Gemini CLI process was terminated or timed out' : null
|
|
@@ -394,8 +404,14 @@ async function spawnGemini(command, options = {}, ws) {
|
|
|
394
404
|
const finalSessionId = capturedSessionId || sessionId || processKey;
|
|
395
405
|
activeGeminiProcesses.delete(finalSessionId);
|
|
396
406
|
|
|
407
|
+
// Check if Gemini CLI is installed for a clearer error message
|
|
408
|
+
const installed = getStatusChecker('gemini')?.checkInstalled() ?? true;
|
|
409
|
+
const errorContent = !installed
|
|
410
|
+
? 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli'
|
|
411
|
+
: error.message;
|
|
412
|
+
|
|
397
413
|
const errorSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
|
|
398
|
-
ws.send(createNormalizedMessage({ kind: 'error', content:
|
|
414
|
+
ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: errorSessionId, provider: 'gemini' }));
|
|
399
415
|
notifyTerminalState({ error });
|
|
400
416
|
|
|
401
417
|
reject(error);
|
package/server/index.js
CHANGED
|
@@ -11,25 +11,7 @@ const __dirname = getModuleDir(import.meta.url);
|
|
|
11
11
|
const APP_ROOT = findAppRoot(__dirname);
|
|
12
12
|
const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
const colors = {
|
|
16
|
-
reset: '\x1b[0m',
|
|
17
|
-
bright: '\x1b[1m',
|
|
18
|
-
cyan: '\x1b[36m',
|
|
19
|
-
green: '\x1b[32m',
|
|
20
|
-
yellow: '\x1b[33m',
|
|
21
|
-
blue: '\x1b[34m',
|
|
22
|
-
dim: '\x1b[2m',
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const c = {
|
|
26
|
-
info: (text) => `${colors.cyan}${text}${colors.reset}`,
|
|
27
|
-
ok: (text) => `${colors.green}${text}${colors.reset}`,
|
|
28
|
-
warn: (text) => `${colors.yellow}${text}${colors.reset}`,
|
|
29
|
-
tip: (text) => `${colors.blue}${text}${colors.reset}`,
|
|
30
|
-
bright: (text) => `${colors.bright}${text}${colors.reset}`,
|
|
31
|
-
dim: (text) => `${colors.dim}${text}${colors.reset}`,
|
|
32
|
-
};
|
|
14
|
+
import { c } from './utils/colors.js';
|
|
33
15
|
|
|
34
16
|
console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
|
|
35
17
|
|
|
@@ -226,68 +208,7 @@ const server = http.createServer(app);
|
|
|
226
208
|
const ptySessionsMap = new Map();
|
|
227
209
|
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
228
210
|
const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
|
|
229
|
-
|
|
230
|
-
const TRAILING_URL_PUNCTUATION_REGEX = /[)\]}>.,;:!?]+$/;
|
|
231
|
-
|
|
232
|
-
function stripAnsiSequences(value = '') {
|
|
233
|
-
return value.replace(ANSI_ESCAPE_SEQUENCE_REGEX, '');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function normalizeDetectedUrl(url) {
|
|
237
|
-
if (!url || typeof url !== 'string') return null;
|
|
238
|
-
|
|
239
|
-
const cleaned = url.trim().replace(TRAILING_URL_PUNCTUATION_REGEX, '');
|
|
240
|
-
if (!cleaned) return null;
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
const parsed = new URL(cleaned);
|
|
244
|
-
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
return parsed.toString();
|
|
248
|
-
} catch {
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function extractUrlsFromText(value = '') {
|
|
254
|
-
const directMatches = value.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/gi) || [];
|
|
255
|
-
|
|
256
|
-
// Handle wrapped terminal URLs split across lines by terminal width.
|
|
257
|
-
const wrappedMatches = [];
|
|
258
|
-
const continuationRegex = /^[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]+$/;
|
|
259
|
-
const lines = value.split(/\r?\n/);
|
|
260
|
-
for (let i = 0; i < lines.length; i++) {
|
|
261
|
-
const line = lines[i].trim();
|
|
262
|
-
const startMatch = line.match(/https?:\/\/[^\s<>"'`\\\x1b\x07]+/i);
|
|
263
|
-
if (!startMatch) continue;
|
|
264
|
-
|
|
265
|
-
let combined = startMatch[0];
|
|
266
|
-
let j = i + 1;
|
|
267
|
-
while (j < lines.length) {
|
|
268
|
-
const continuation = lines[j].trim();
|
|
269
|
-
if (!continuation) break;
|
|
270
|
-
if (!continuationRegex.test(continuation)) break;
|
|
271
|
-
combined += continuation;
|
|
272
|
-
j++;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
wrappedMatches.push(combined.replace(/\r?\n\s*/g, ''));
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return Array.from(new Set([...directMatches, ...wrappedMatches]));
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function shouldAutoOpenUrlFromOutput(value = '') {
|
|
282
|
-
const normalized = value.toLowerCase();
|
|
283
|
-
return (
|
|
284
|
-
normalized.includes('browser didn\'t open') ||
|
|
285
|
-
normalized.includes('open this url') ||
|
|
286
|
-
normalized.includes('continue in your browser') ||
|
|
287
|
-
normalized.includes('press enter to open') ||
|
|
288
|
-
normalized.includes('open_url:')
|
|
289
|
-
);
|
|
290
|
-
}
|
|
211
|
+
import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
|
|
291
212
|
|
|
292
213
|
// Single WebSocket server that handles both paths
|
|
293
214
|
const wss = new WebSocketServer({
|
|
@@ -573,12 +494,15 @@ app.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) =
|
|
|
573
494
|
}
|
|
574
495
|
});
|
|
575
496
|
|
|
576
|
-
// Delete project endpoint
|
|
497
|
+
// Delete project endpoint
|
|
498
|
+
// force=true to allow removal even when sessions exist
|
|
499
|
+
// deleteData=true to also delete session/memory files on disk (destructive)
|
|
577
500
|
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
|
|
578
501
|
try {
|
|
579
502
|
const { projectName } = req.params;
|
|
580
503
|
const force = req.query.force === 'true';
|
|
581
|
-
|
|
504
|
+
const deleteData = req.query.deleteData === 'true';
|
|
505
|
+
await deleteProject(projectName, force, deleteData);
|
|
582
506
|
res.json({ success: true });
|
|
583
507
|
} catch (error) {
|
|
584
508
|
res.status(500).json({ error: error.message });
|
package/server/openai-codex.js
CHANGED
|
@@ -17,6 +17,7 @@ import { Codex } from '@openai/codex-sdk';
|
|
|
17
17
|
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
18
18
|
import { codexAdapter } from './providers/codex/adapter.js';
|
|
19
19
|
import { createNormalizedMessage } from './providers/types.js';
|
|
20
|
+
import { getStatusChecker } from './providers/registry.js';
|
|
20
21
|
|
|
21
22
|
// Track active sessions
|
|
22
23
|
const activeCodexSessions = new Map();
|
|
@@ -308,7 +309,14 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
308
309
|
|
|
309
310
|
if (!wasAborted) {
|
|
310
311
|
console.error('[Codex] Error:', error);
|
|
311
|
-
|
|
312
|
+
|
|
313
|
+
// Check if Codex SDK is available for a clearer error message
|
|
314
|
+
const installed = getStatusChecker('codex')?.checkInstalled() ?? true;
|
|
315
|
+
const errorContent = !installed
|
|
316
|
+
? 'Codex CLI is not configured. Please set up authentication first.'
|
|
317
|
+
: error.message;
|
|
318
|
+
|
|
319
|
+
sendMessage(ws, createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: currentSessionId, provider: 'codex' }));
|
|
312
320
|
if (!terminalFailure) {
|
|
313
321
|
notifyRunFailed({
|
|
314
322
|
userId: ws?.userId || null,
|
package/server/projects.js
CHANGED
|
@@ -62,8 +62,7 @@ import fsSync from 'fs';
|
|
|
62
62
|
import path from 'path';
|
|
63
63
|
import readline from 'readline';
|
|
64
64
|
import crypto from 'crypto';
|
|
65
|
-
import
|
|
66
|
-
import { open } from 'sqlite';
|
|
65
|
+
import Database from 'better-sqlite3';
|
|
67
66
|
import os from 'os';
|
|
68
67
|
import sessionManager from './sessionManager.js';
|
|
69
68
|
import { applyCustomSessionNames } from './database/db.js';
|
|
@@ -1164,8 +1163,9 @@ async function isProjectEmpty(projectName) {
|
|
|
1164
1163
|
}
|
|
1165
1164
|
}
|
|
1166
1165
|
|
|
1167
|
-
//
|
|
1168
|
-
|
|
1166
|
+
// Remove a project from the UI.
|
|
1167
|
+
// When deleteData=true, also delete session/memory files on disk (destructive).
|
|
1168
|
+
async function deleteProject(projectName, force = false, deleteData = false) {
|
|
1169
1169
|
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
1170
1170
|
|
|
1171
1171
|
try {
|
|
@@ -1175,48 +1175,50 @@ async function deleteProject(projectName, force = false) {
|
|
|
1175
1175
|
}
|
|
1176
1176
|
|
|
1177
1177
|
const config = await loadProjectConfig();
|
|
1178
|
-
let projectPath = config[projectName]?.path || config[projectName]?.originalPath;
|
|
1179
1178
|
|
|
1180
|
-
//
|
|
1181
|
-
if (
|
|
1182
|
-
projectPath =
|
|
1183
|
-
|
|
1179
|
+
// Destructive path: delete underlying data when explicitly requested
|
|
1180
|
+
if (deleteData) {
|
|
1181
|
+
let projectPath = config[projectName]?.path || config[projectName]?.originalPath;
|
|
1182
|
+
if (!projectPath) {
|
|
1183
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1184
|
+
}
|
|
1184
1185
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1186
|
+
// Remove the Claude project directory (session logs, memory, subagent data)
|
|
1187
|
+
await fs.rm(projectDir, { recursive: true, force: true });
|
|
1187
1188
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1189
|
+
// Delete Codex sessions associated with this project
|
|
1190
|
+
if (projectPath) {
|
|
1191
|
+
try {
|
|
1192
|
+
const codexSessions = await getCodexSessions(projectPath, { limit: 0 });
|
|
1193
|
+
for (const session of codexSessions) {
|
|
1194
|
+
try {
|
|
1195
|
+
await deleteCodexSession(session.id);
|
|
1196
|
+
} catch (err) {
|
|
1197
|
+
console.warn(`Failed to delete Codex session ${session.id}:`, err.message);
|
|
1198
|
+
}
|
|
1197
1199
|
}
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
console.warn('Failed to delete Codex sessions:', err.message);
|
|
1198
1202
|
}
|
|
1199
|
-
} catch (err) {
|
|
1200
|
-
console.warn('Failed to delete Codex sessions:', err.message);
|
|
1201
|
-
}
|
|
1202
1203
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1204
|
+
// Delete Cursor sessions directory if it exists
|
|
1205
|
+
try {
|
|
1206
|
+
const hash = crypto.createHash('md5').update(projectPath).digest('hex');
|
|
1207
|
+
const cursorProjectDir = path.join(os.homedir(), '.cursor', 'chats', hash);
|
|
1208
|
+
await fs.rm(cursorProjectDir, { recursive: true, force: true });
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
// Cursor dir may not exist, ignore
|
|
1211
|
+
}
|
|
1210
1212
|
}
|
|
1211
1213
|
}
|
|
1212
1214
|
|
|
1213
|
-
//
|
|
1215
|
+
// Always remove from project config
|
|
1214
1216
|
delete config[projectName];
|
|
1215
1217
|
await saveProjectConfig(config);
|
|
1216
1218
|
|
|
1217
1219
|
return true;
|
|
1218
1220
|
} catch (error) {
|
|
1219
|
-
console.error(`Error
|
|
1221
|
+
console.error(`Error removing project ${projectName}:`, error);
|
|
1220
1222
|
throw error;
|
|
1221
1223
|
}
|
|
1222
1224
|
}
|
|
@@ -1305,16 +1307,10 @@ async function getCursorSessions(projectPath) {
|
|
|
1305
1307
|
} catch (_) { }
|
|
1306
1308
|
|
|
1307
1309
|
// Open SQLite database
|
|
1308
|
-
const db =
|
|
1309
|
-
filename: storeDbPath,
|
|
1310
|
-
driver: sqlite3.Database,
|
|
1311
|
-
mode: sqlite3.OPEN_READONLY
|
|
1312
|
-
});
|
|
1310
|
+
const db = new Database(storeDbPath, { readonly: true, fileMustExist: true });
|
|
1313
1311
|
|
|
1314
1312
|
// Get metadata from meta table
|
|
1315
|
-
const metaRows =
|
|
1316
|
-
SELECT key, value FROM meta
|
|
1317
|
-
`);
|
|
1313
|
+
const metaRows = db.prepare('SELECT key, value FROM meta').all();
|
|
1318
1314
|
|
|
1319
1315
|
// Parse metadata
|
|
1320
1316
|
let metadata = {};
|
|
@@ -1336,11 +1332,9 @@ async function getCursorSessions(projectPath) {
|
|
|
1336
1332
|
}
|
|
1337
1333
|
|
|
1338
1334
|
// Get message count
|
|
1339
|
-
const messageCountResult =
|
|
1340
|
-
SELECT COUNT(*) as count FROM blobs
|
|
1341
|
-
`);
|
|
1335
|
+
const messageCountResult = db.prepare('SELECT COUNT(*) as count FROM blobs').get();
|
|
1342
1336
|
|
|
1343
|
-
|
|
1337
|
+
db.close();
|
|
1344
1338
|
|
|
1345
1339
|
// Extract session info
|
|
1346
1340
|
const sessionName = metadata.title || metadata.sessionTitle || 'Untitled Session';
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Provider Status
|
|
3
|
+
*
|
|
4
|
+
* Checks whether Claude Code CLI is installed and whether the user
|
|
5
|
+
* has valid authentication credentials.
|
|
6
|
+
*
|
|
7
|
+
* @module providers/claude/status
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execFileSync } from 'child_process';
|
|
11
|
+
import { promises as fs } from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import os from 'os';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if Claude Code CLI is installed and available.
|
|
17
|
+
* Uses CLAUDE_CLI_PATH env var if set, otherwise looks for 'claude' in PATH.
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
export function checkInstalled() {
|
|
21
|
+
const cliPath = process.env.CLAUDE_CLI_PATH || 'claude';
|
|
22
|
+
try {
|
|
23
|
+
execFileSync(cliPath, ['--version'], { stdio: 'ignore', timeout: 5000 });
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Full status check: installation + authentication.
|
|
32
|
+
* @returns {Promise<import('../types.js').ProviderStatus>}
|
|
33
|
+
*/
|
|
34
|
+
export async function checkStatus() {
|
|
35
|
+
const installed = checkInstalled();
|
|
36
|
+
|
|
37
|
+
if (!installed) {
|
|
38
|
+
return {
|
|
39
|
+
installed,
|
|
40
|
+
authenticated: false,
|
|
41
|
+
email: null,
|
|
42
|
+
method: null,
|
|
43
|
+
error: 'Claude Code CLI is not installed'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const credentialsResult = await checkCredentials();
|
|
48
|
+
|
|
49
|
+
if (credentialsResult.authenticated) {
|
|
50
|
+
return {
|
|
51
|
+
installed,
|
|
52
|
+
authenticated: true,
|
|
53
|
+
email: credentialsResult.email || 'Authenticated',
|
|
54
|
+
method: credentialsResult.method || null,
|
|
55
|
+
error: null
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
installed,
|
|
61
|
+
authenticated: false,
|
|
62
|
+
email: credentialsResult.email || null,
|
|
63
|
+
method: credentialsResult.method || null,
|
|
64
|
+
error: credentialsResult.error || 'Not authenticated'
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Internal helpers ───────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
async function loadSettingsEnv() {
|
|
71
|
+
try {
|
|
72
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
73
|
+
const content = await fs.readFile(settingsPath, 'utf8');
|
|
74
|
+
const settings = JSON.parse(content);
|
|
75
|
+
|
|
76
|
+
if (settings?.env && typeof settings.env === 'object') {
|
|
77
|
+
return settings.env;
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// Ignore missing or malformed settings.
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Checks Claude authentication credentials.
|
|
88
|
+
*
|
|
89
|
+
* Priority 1: ANTHROPIC_API_KEY environment variable
|
|
90
|
+
* Priority 1b: ~/.claude/settings.json env values
|
|
91
|
+
* Priority 2: ~/.claude/.credentials.json OAuth tokens
|
|
92
|
+
*/
|
|
93
|
+
async function checkCredentials() {
|
|
94
|
+
if (process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY.trim()) {
|
|
95
|
+
return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const settingsEnv = await loadSettingsEnv();
|
|
99
|
+
|
|
100
|
+
if (typeof settingsEnv.ANTHROPIC_API_KEY === 'string' && settingsEnv.ANTHROPIC_API_KEY.trim()) {
|
|
101
|
+
return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof settingsEnv.ANTHROPIC_AUTH_TOKEN === 'string' && settingsEnv.ANTHROPIC_AUTH_TOKEN.trim()) {
|
|
105
|
+
return { authenticated: true, email: 'Configured via settings.json', method: 'api_key' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const credPath = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
110
|
+
const content = await fs.readFile(credPath, 'utf8');
|
|
111
|
+
const creds = JSON.parse(content);
|
|
112
|
+
|
|
113
|
+
const oauth = creds.claudeAiOauth;
|
|
114
|
+
if (oauth && oauth.accessToken) {
|
|
115
|
+
const isExpired = oauth.expiresAt && Date.now() >= oauth.expiresAt;
|
|
116
|
+
if (!isExpired) {
|
|
117
|
+
return {
|
|
118
|
+
authenticated: true,
|
|
119
|
+
email: creds.email || creds.user || null,
|
|
120
|
+
method: 'credentials_file'
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
authenticated: false,
|
|
126
|
+
email: creds.email || creds.user || null,
|
|
127
|
+
method: 'credentials_file',
|
|
128
|
+
error: 'OAuth token has expired. Please re-authenticate with claude login'
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { authenticated: false, email: null, method: null };
|
|
133
|
+
} catch {
|
|
134
|
+
return { authenticated: false, email: null, method: null };
|
|
135
|
+
}
|
|
136
|
+
}
|