@cloudcli-ai/cloudcli 1.31.5 → 1.32.0
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-AaSE7-3t.js +1336 -0
- package/dist/assets/index-CKaku72N.css +32 -0
- package/dist/index.html +2 -2
- package/dist-server/server/claude-sdk.js +10 -5
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cursor-cli.js +0 -6
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +160 -34
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +11 -0
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/projects.db.js +12 -0
- package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -1
- package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js +61 -0
- package/dist-server/server/modules/database/repositories/sessions.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +49 -8
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/schema.js +1 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/projects/projects.routes.js +12 -3
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js +14 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +43 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -1
- package/dist-server/server/modules/providers/index.js +1 -0
- package/dist-server/server/modules/providers/index.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +2 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +55 -2
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +153 -14
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +198 -0
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -2
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +21 -6
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js +82 -0
- package/dist-server/server/modules/providers/list/codex/codex-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +8 -30
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js +189 -13
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js +28 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +125 -2
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +24 -8
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +7 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +33 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +2 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +23 -2
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +129 -13
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +7 -5
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +81 -7
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/skills.service.js +11 -0
- package/dist-server/server/modules/providers/services/skills.service.js.map +1 -0
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
- package/dist-server/server/modules/providers/shared/skills/skills.provider.js +45 -0
- package/dist-server/server/modules/providers/shared/skills/skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/tests/skills.test.js +226 -0
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -0
- package/dist-server/server/openai-codex.js +56 -26
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/routes/commands.js +5 -5
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/shared/claude-cli-path.js +103 -0
- package/dist-server/server/shared/claude-cli-path.js.map +1 -0
- package/dist-server/server/shared/claude-cli-path.test.js +45 -0
- package/dist-server/server/shared/claude-cli-path.test.js.map +1 -0
- package/dist-server/server/{utils → shared}/frontmatter.js +3 -3
- package/dist-server/server/shared/frontmatter.js.map +1 -0
- package/dist-server/server/shared/utils.js +82 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/utils/commandParser.js +3 -3
- package/dist-server/server/utils/commandParser.js.map +1 -1
- package/dist-server/shared/modelConstants.js +1 -0
- package/dist-server/shared/modelConstants.js.map +1 -1
- package/package.json +1 -1
- package/server/claude-sdk.js +10 -5
- package/server/cursor-cli.js +0 -8
- package/server/gemini-cli.js +190 -42
- package/server/modules/database/migrations.ts +12 -0
- package/server/modules/database/repositories/projects.db.ts +13 -0
- package/server/modules/database/repositories/sessions.db.integration.test.ts +72 -0
- package/server/modules/database/repositories/sessions.db.ts +60 -9
- package/server/modules/database/schema.ts +1 -0
- package/server/modules/projects/projects.routes.ts +20 -3
- package/server/modules/projects/services/project-delete.service.ts +16 -1
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +64 -0
- package/server/modules/providers/README.md +346 -0
- package/server/modules/providers/index.ts +2 -1
- package/server/modules/providers/list/claude/claude-auth.provider.ts +8 -7
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +68 -2
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +175 -14
- package/server/modules/providers/list/claude/claude-skills.provider.ts +257 -0
- package/server/modules/providers/list/claude/claude.provider.ts +8 -1
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +62 -2
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +25 -6
- package/server/modules/providers/list/codex/codex-skills.provider.ts +100 -0
- package/server/modules/providers/list/codex/codex.provider.ts +8 -1
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +17 -40
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +216 -13
- package/server/modules/providers/list/cursor/cursor-skills.provider.ts +31 -0
- package/server/modules/providers/list/cursor/cursor.provider.ts +8 -1
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +158 -2
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +24 -20
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +7 -1
- package/server/modules/providers/list/gemini/gemini-skills.provider.ts +36 -0
- package/server/modules/providers/list/gemini/gemini.provider.ts +8 -1
- package/server/modules/providers/provider.routes.ts +35 -2
- package/server/modules/providers/services/session-conversations-search.service.ts +162 -13
- package/server/modules/providers/services/sessions-watcher.service.ts +7 -5
- package/server/modules/providers/services/sessions.service.ts +112 -9
- package/server/modules/providers/services/skills.service.ts +15 -0
- package/server/modules/providers/shared/base/abstract.provider.ts +2 -0
- package/server/modules/providers/shared/skills/skills.provider.ts +64 -0
- package/server/modules/providers/tests/skills.test.ts +446 -0
- package/server/openai-codex.js +59 -27
- package/server/routes/commands.js +7 -5
- package/server/shared/claude-cli-path.test.ts +61 -0
- package/server/shared/claude-cli-path.ts +139 -0
- package/server/{utils/frontmatter.js → shared/frontmatter.ts} +3 -3
- package/server/shared/interfaces.ts +19 -0
- package/server/shared/types.ts +78 -0
- package/server/shared/utils.ts +94 -0
- package/server/utils/commandParser.js +5 -3
- package/shared/modelConstants.js +1 -0
- package/dist/assets/index-6rYG_mRo.css +0 -32
- package/dist/assets/index-DqMVUeZS.js +0 -1331
- package/dist-server/server/utils/frontmatter.js.map +0 -1
package/server/gemini-cli.js
CHANGED
|
@@ -1,19 +1,123 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import crossSpawn from 'cross-spawn';
|
|
3
|
-
|
|
4
|
-
// Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
|
|
5
|
-
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
6
2
|
import { promises as fs } from 'fs';
|
|
7
|
-
import path from 'path';
|
|
8
3
|
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import crossSpawn from 'cross-spawn';
|
|
7
|
+
|
|
9
8
|
import sessionManager from './sessionManager.js';
|
|
10
9
|
import GeminiResponseHandler from './gemini-response-handler.js';
|
|
11
10
|
import { notifyRunFailed, notifyRunStopped } from './services/notification-orchestrator.js';
|
|
12
11
|
import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
|
|
13
12
|
import { createNormalizedMessage } from './shared/utils.js';
|
|
14
13
|
|
|
14
|
+
// Use cross-spawn on Windows for correct .cmd resolution (same pattern as cursor-cli.js)
|
|
15
|
+
const spawnFunction = process.platform === 'win32' ? crossSpawn : spawn;
|
|
16
|
+
|
|
15
17
|
let activeGeminiProcesses = new Map(); // Track active processes by session ID
|
|
16
18
|
|
|
19
|
+
function mapGeminiExitCodeToMessage(exitCode) {
|
|
20
|
+
switch (exitCode) {
|
|
21
|
+
case 42:
|
|
22
|
+
return 'Gemini rejected the request input (exit code 42).';
|
|
23
|
+
case 44:
|
|
24
|
+
return 'Gemini sandbox error (exit code 44). Check local sandbox/container settings.';
|
|
25
|
+
case 52:
|
|
26
|
+
return 'Gemini configuration error (exit code 52). Check your Gemini settings files for invalid JSON/config.';
|
|
27
|
+
case 53:
|
|
28
|
+
return 'Gemini conversation turn limit reached (exit code 53). Start a new Gemini session.';
|
|
29
|
+
default:
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const GEMINI_AUTH_ENV_KEYS = [
|
|
35
|
+
'GEMINI_API_KEY',
|
|
36
|
+
'GOOGLE_API_KEY',
|
|
37
|
+
'GOOGLE_CLOUD_PROJECT',
|
|
38
|
+
'GOOGLE_CLOUD_PROJECT_ID',
|
|
39
|
+
'GOOGLE_CLOUD_LOCATION',
|
|
40
|
+
'GOOGLE_APPLICATION_CREDENTIALS'
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function parseEnvFileContent(content) {
|
|
44
|
+
const parsed = {};
|
|
45
|
+
|
|
46
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
47
|
+
const line = rawLine.trim();
|
|
48
|
+
if (!line || line.startsWith('#')) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const exportPrefix = 'export ';
|
|
53
|
+
const normalizedLine = line.startsWith(exportPrefix) ? line.slice(exportPrefix.length).trim() : line;
|
|
54
|
+
const separatorIndex = normalizedLine.indexOf('=');
|
|
55
|
+
|
|
56
|
+
if (separatorIndex <= 0) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const key = normalizedLine.slice(0, separatorIndex).trim();
|
|
61
|
+
if (!key) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let value = normalizedLine.slice(separatorIndex + 1).trim();
|
|
66
|
+
const hasDoubleQuotes = value.startsWith('"') && value.endsWith('"');
|
|
67
|
+
const hasSingleQuotes = value.startsWith('\'') && value.endsWith('\'');
|
|
68
|
+
|
|
69
|
+
if (hasDoubleQuotes || hasSingleQuotes) {
|
|
70
|
+
value = value.slice(1, -1);
|
|
71
|
+
} else {
|
|
72
|
+
// Support inline comments in unquoted values: KEY=value # comment
|
|
73
|
+
value = value.replace(/\s+#.*$/, '').trim();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
parsed[key] = value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return parsed;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function loadGeminiUserLevelEnv() {
|
|
83
|
+
const geminiCliHome = (process.env.GEMINI_CLI_HOME || '').trim() || os.homedir();
|
|
84
|
+
const envCandidates = [
|
|
85
|
+
path.join(geminiCliHome, '.gemini', '.env'),
|
|
86
|
+
path.join(geminiCliHome, '.env')
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const envPath of envCandidates) {
|
|
90
|
+
try {
|
|
91
|
+
await fs.access(envPath);
|
|
92
|
+
const content = await fs.readFile(envPath, 'utf8');
|
|
93
|
+
return parseEnvFileContent(content);
|
|
94
|
+
} catch {
|
|
95
|
+
// Keep scanning for the next candidate.
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function buildGeminiProcessEnv() {
|
|
103
|
+
const processEnv = { ...process.env };
|
|
104
|
+
if (processEnv.GEMINI_API_KEY || processEnv.GOOGLE_API_KEY || processEnv.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
105
|
+
return processEnv;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Gemini CLI docs recommend ~/.gemini/.env for persistent headless auth settings.
|
|
109
|
+
// When the server process was launched without shell profile variables, we still
|
|
110
|
+
// want the spawned CLI process to inherit those user-level credentials.
|
|
111
|
+
const userEnv = await loadGeminiUserLevelEnv();
|
|
112
|
+
for (const key of GEMINI_AUTH_ENV_KEYS) {
|
|
113
|
+
if (!processEnv[key] && userEnv[key]) {
|
|
114
|
+
processEnv[key] = userEnv[key];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return processEnv;
|
|
119
|
+
}
|
|
120
|
+
|
|
17
121
|
async function spawnGemini(command, options = {}, ws) {
|
|
18
122
|
const { sessionId, projectPath, cwd, toolsSettings, permissionMode, images, sessionSummary } = options;
|
|
19
123
|
let capturedSessionId = sessionId; // Track session ID throughout the process
|
|
@@ -100,6 +204,11 @@ async function spawnGemini(command, options = {}, ws) {
|
|
|
100
204
|
args.push('--debug');
|
|
101
205
|
}
|
|
102
206
|
|
|
207
|
+
// This integration runs Gemini in headless mode and cannot answer trust prompts.
|
|
208
|
+
// Skip folder-trust interactivity so authenticated runs don't fail with
|
|
209
|
+
// FatalUntrustedWorkspaceError in previously unseen directories.
|
|
210
|
+
args.push('--skip-trust');
|
|
211
|
+
|
|
103
212
|
// Add MCP config flag only if MCP servers are configured
|
|
104
213
|
try {
|
|
105
214
|
const geminiConfigPath = path.join(os.homedir(), '.gemini.json');
|
|
@@ -154,9 +263,6 @@ async function spawnGemini(command, options = {}, ws) {
|
|
|
154
263
|
|
|
155
264
|
// Try to find gemini in PATH first, then fall back to environment variable
|
|
156
265
|
const geminiPath = process.env.GEMINI_PATH || 'gemini';
|
|
157
|
-
console.log('Spawning Gemini CLI:', geminiPath, args.join(' '));
|
|
158
|
-
console.log('Working directory:', workingDir);
|
|
159
|
-
|
|
160
266
|
let spawnCmd = geminiPath;
|
|
161
267
|
let spawnArgs = args;
|
|
162
268
|
|
|
@@ -168,11 +274,13 @@ async function spawnGemini(command, options = {}, ws) {
|
|
|
168
274
|
spawnArgs = ['-c', 'exec "$0" "$@"', geminiPath, ...args];
|
|
169
275
|
}
|
|
170
276
|
|
|
277
|
+
const spawnEnv = await buildGeminiProcessEnv();
|
|
278
|
+
|
|
171
279
|
return new Promise((resolve, reject) => {
|
|
172
280
|
const geminiProcess = spawnFunction(spawnCmd, spawnArgs, {
|
|
173
281
|
cwd: workingDir,
|
|
174
282
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
175
|
-
env:
|
|
283
|
+
env: spawnEnv
|
|
176
284
|
});
|
|
177
285
|
let terminalNotificationSent = false;
|
|
178
286
|
let terminalFailureReason = null;
|
|
@@ -276,13 +384,44 @@ async function spawnGemini(command, options = {}, ws) {
|
|
|
276
384
|
}
|
|
277
385
|
},
|
|
278
386
|
onInit: (event) => {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
387
|
+
const discoveredSessionId = event?.session_id;
|
|
388
|
+
if (!discoveredSessionId) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// New Gemini sessions announce their canonical ID asynchronously via the
|
|
393
|
+
// initial `init` stream event. Avoid synthetic IDs and only register
|
|
394
|
+
// the session once that real ID is known (same model used by Claude/Codex).
|
|
395
|
+
if (!capturedSessionId) {
|
|
396
|
+
capturedSessionId = discoveredSessionId;
|
|
397
|
+
|
|
398
|
+
sessionManager.createSession(capturedSessionId, cwd || process.cwd());
|
|
399
|
+
if (command) {
|
|
400
|
+
sessionManager.addMessage(capturedSessionId, 'user', command);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (processKey !== capturedSessionId) {
|
|
404
|
+
activeGeminiProcesses.delete(processKey);
|
|
405
|
+
activeGeminiProcesses.set(capturedSessionId, geminiProcess);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
geminiProcess.sessionId = capturedSessionId;
|
|
409
|
+
|
|
410
|
+
if (ws.setSessionId && typeof ws.setSessionId === 'function') {
|
|
411
|
+
ws.setSessionId(capturedSessionId);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!sessionId && !sessionCreatedSent) {
|
|
415
|
+
sessionCreatedSent = true;
|
|
416
|
+
ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'gemini' }));
|
|
284
417
|
}
|
|
285
418
|
}
|
|
419
|
+
|
|
420
|
+
const sess = sessionManager.getSession(capturedSessionId);
|
|
421
|
+
if (sess && !sess.cliSessionId) {
|
|
422
|
+
sess.cliSessionId = discoveredSessionId;
|
|
423
|
+
sessionManager.saveSession(capturedSessionId);
|
|
424
|
+
}
|
|
286
425
|
}
|
|
287
426
|
});
|
|
288
427
|
}
|
|
@@ -292,30 +431,6 @@ async function spawnGemini(command, options = {}, ws) {
|
|
|
292
431
|
const rawOutput = data.toString();
|
|
293
432
|
startTimeout(); // Re-arm the timeout
|
|
294
433
|
|
|
295
|
-
// For new sessions, create a session ID FIRST
|
|
296
|
-
if (!sessionId && !sessionCreatedSent && !capturedSessionId) {
|
|
297
|
-
capturedSessionId = `gemini_${Date.now()}`;
|
|
298
|
-
sessionCreatedSent = true;
|
|
299
|
-
|
|
300
|
-
// Create session in session manager
|
|
301
|
-
sessionManager.createSession(capturedSessionId, cwd || process.cwd());
|
|
302
|
-
|
|
303
|
-
// Save the user message now that we have a session ID
|
|
304
|
-
if (command) {
|
|
305
|
-
sessionManager.addMessage(capturedSessionId, 'user', command);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Update process key with captured session ID
|
|
309
|
-
if (processKey !== capturedSessionId) {
|
|
310
|
-
activeGeminiProcesses.delete(processKey);
|
|
311
|
-
activeGeminiProcesses.set(capturedSessionId, geminiProcess);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
ws.setSessionId && typeof ws.setSessionId === 'function' && ws.setSessionId(capturedSessionId);
|
|
315
|
-
|
|
316
|
-
ws.send(createNormalizedMessage({ kind: 'session_created', newSessionId: capturedSessionId, sessionId: capturedSessionId, provider: 'gemini' }));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
434
|
if (responseHandler) {
|
|
320
435
|
responseHandler.processData(rawOutput);
|
|
321
436
|
} else if (rawOutput) {
|
|
@@ -381,12 +496,38 @@ async function spawnGemini(command, options = {}, ws) {
|
|
|
381
496
|
notifyTerminalState({ code });
|
|
382
497
|
resolve();
|
|
383
498
|
} else {
|
|
384
|
-
|
|
499
|
+
const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
|
|
500
|
+
|
|
501
|
+
// code 127 = shell "command not found" - check installation
|
|
385
502
|
if (code === 127) {
|
|
386
503
|
const installed = await providerAuthService.isProviderInstalled('gemini');
|
|
387
504
|
if (!installed) {
|
|
388
|
-
|
|
389
|
-
ws.send(createNormalizedMessage({ kind: 'error', content:
|
|
505
|
+
terminalFailureReason = 'Gemini CLI is not installed. Please install it first: https://github.com/google-gemini/gemini-cli';
|
|
506
|
+
ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' }));
|
|
507
|
+
}
|
|
508
|
+
} else if (code === 41) {
|
|
509
|
+
// Gemini CLI documents exit code 41 as FatalAuthenticationError.
|
|
510
|
+
// Surface an actionable auth error instead of a generic exit-code message.
|
|
511
|
+
let authErrorSuffix = '';
|
|
512
|
+
try {
|
|
513
|
+
const authStatus = await providerAuthService.getProviderAuthStatus('gemini');
|
|
514
|
+
if (!authStatus?.authenticated && authStatus?.error) {
|
|
515
|
+
authErrorSuffix = ` Details: ${authStatus.error}`;
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
// Keep base remediation text when auth status lookup fails.
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
terminalFailureReason =
|
|
522
|
+
'Gemini authentication failed (exit code 41). '
|
|
523
|
+
+ 'Run `gemini` in a terminal to choose an auth method, or configure a valid `GEMINI_API_KEY`.'
|
|
524
|
+
+ authErrorSuffix;
|
|
525
|
+
ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' }));
|
|
526
|
+
} else {
|
|
527
|
+
const mappedError = mapGeminiExitCodeToMessage(code);
|
|
528
|
+
if (mappedError) {
|
|
529
|
+
terminalFailureReason = mappedError;
|
|
530
|
+
ws.send(createNormalizedMessage({ kind: 'error', content: terminalFailureReason, sessionId: socketSessionId, provider: 'gemini' }));
|
|
390
531
|
}
|
|
391
532
|
}
|
|
392
533
|
|
|
@@ -394,7 +535,14 @@ async function spawnGemini(command, options = {}, ws) {
|
|
|
394
535
|
code,
|
|
395
536
|
error: code === null ? 'Gemini CLI process was terminated or timed out' : null
|
|
396
537
|
});
|
|
397
|
-
reject(
|
|
538
|
+
reject(
|
|
539
|
+
new Error(
|
|
540
|
+
terminalFailureReason
|
|
541
|
+
|| (code === null
|
|
542
|
+
? 'Gemini CLI process was terminated or timed out'
|
|
543
|
+
: `Gemini CLI exited with code ${code}`)
|
|
544
|
+
)
|
|
545
|
+
);
|
|
398
546
|
}
|
|
399
547
|
});
|
|
400
548
|
|
|
@@ -257,8 +257,10 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
|
|
|
257
257
|
|
|
258
258
|
if (!shouldRebuild) {
|
|
259
259
|
addColumnToTableIfNotExists(db, 'sessions', columnNames, 'jsonl_path', 'TEXT');
|
|
260
|
+
addColumnToTableIfNotExists(db, 'sessions', columnNames, 'isArchived', 'BOOLEAN DEFAULT 0');
|
|
260
261
|
addColumnToTableIfNotExists(db, 'sessions', columnNames, 'created_at', 'DATETIME');
|
|
261
262
|
addColumnToTableIfNotExists(db, 'sessions', columnNames, 'updated_at', 'DATETIME');
|
|
263
|
+
db.exec('UPDATE sessions SET isArchived = COALESCE(isArchived, 0)');
|
|
262
264
|
db.exec('UPDATE sessions SET created_at = COALESCE(created_at, CURRENT_TIMESTAMP)');
|
|
263
265
|
db.exec('UPDATE sessions SET updated_at = COALESCE(updated_at, CURRENT_TIMESTAMP)');
|
|
264
266
|
return;
|
|
@@ -284,6 +286,10 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
|
|
|
284
286
|
? 'jsonl_path'
|
|
285
287
|
: 'NULL';
|
|
286
288
|
|
|
289
|
+
const isArchivedExpression = columnNames.includes('isArchived')
|
|
290
|
+
? 'COALESCE(isArchived, 0)'
|
|
291
|
+
: '0';
|
|
292
|
+
|
|
287
293
|
const createdAtExpression = columnNames.includes('created_at')
|
|
288
294
|
? 'COALESCE(created_at, CURRENT_TIMESTAMP)'
|
|
289
295
|
: 'CURRENT_TIMESTAMP';
|
|
@@ -303,6 +309,7 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
|
|
|
303
309
|
custom_name TEXT,
|
|
304
310
|
project_path TEXT,
|
|
305
311
|
jsonl_path TEXT,
|
|
312
|
+
isArchived BOOLEAN DEFAULT 0,
|
|
306
313
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
307
314
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
308
315
|
PRIMARY KEY (session_id),
|
|
@@ -319,6 +326,7 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
|
|
|
319
326
|
${customNameExpression} AS custom_name,
|
|
320
327
|
${projectPathExpression} AS project_path,
|
|
321
328
|
${jsonlPathExpression} AS jsonl_path,
|
|
329
|
+
${isArchivedExpression} AS isArchived,
|
|
322
330
|
${createdAtExpression} AS created_at,
|
|
323
331
|
${updatedAtExpression} AS updated_at,
|
|
324
332
|
rowid AS source_rowid
|
|
@@ -332,6 +340,7 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
|
|
|
332
340
|
custom_name,
|
|
333
341
|
project_path,
|
|
334
342
|
jsonl_path,
|
|
343
|
+
isArchived,
|
|
335
344
|
created_at,
|
|
336
345
|
updated_at,
|
|
337
346
|
ROW_NUMBER() OVER (
|
|
@@ -346,6 +355,7 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
|
|
|
346
355
|
custom_name,
|
|
347
356
|
project_path,
|
|
348
357
|
jsonl_path,
|
|
358
|
+
isArchived,
|
|
349
359
|
created_at,
|
|
350
360
|
updated_at
|
|
351
361
|
)
|
|
@@ -355,6 +365,7 @@ const rebuildSessionsTableWithProjectSchema = (db: Database): void => {
|
|
|
355
365
|
custom_name,
|
|
356
366
|
project_path,
|
|
357
367
|
jsonl_path,
|
|
368
|
+
isArchived,
|
|
358
369
|
created_at,
|
|
359
370
|
updated_at
|
|
360
371
|
FROM ranked_rows
|
|
@@ -421,6 +432,7 @@ export const runMigrations = (db: Database) => {
|
|
|
421
432
|
|
|
422
433
|
db.exec('CREATE INDEX IF NOT EXISTS idx_session_ids_lookup ON sessions(session_id)');
|
|
423
434
|
db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_project_path ON sessions(project_path)');
|
|
435
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_is_archived ON sessions(isArchived)');
|
|
424
436
|
db.exec('CREATE INDEX IF NOT EXISTS idx_projects_is_starred ON projects(isStarred)');
|
|
425
437
|
db.exec('CREATE INDEX IF NOT EXISTS idx_projects_is_archived ON projects(isArchived)');
|
|
426
438
|
|
|
@@ -95,6 +95,19 @@ export const projectsDb = {
|
|
|
95
95
|
`).all() as ProjectRepositoryRow[];
|
|
96
96
|
},
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Archived rows are queried separately so archive-focused UIs can present
|
|
100
|
+
* hidden workspaces without reintroducing them into the active sidebar list.
|
|
101
|
+
*/
|
|
102
|
+
getArchivedProjectPaths(): ProjectRepositoryRow[] {
|
|
103
|
+
const db = getConnection();
|
|
104
|
+
return db.prepare(`
|
|
105
|
+
SELECT project_id, project_path, custom_project_name, isStarred, isArchived
|
|
106
|
+
FROM projects
|
|
107
|
+
WHERE isArchived = 1
|
|
108
|
+
`).all() as ProjectRepositoryRow[];
|
|
109
|
+
},
|
|
110
|
+
|
|
98
111
|
getCustomProjectName(projectPath: string): string | null {
|
|
99
112
|
const db = getConnection();
|
|
100
113
|
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import { closeConnection } from '@/modules/database/connection.js';
|
|
8
|
+
import { initializeDatabase } from '@/modules/database/init-db.js';
|
|
9
|
+
import { sessionsDb } from '@/modules/database/repositories/sessions.db.js';
|
|
10
|
+
|
|
11
|
+
async function withIsolatedDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
12
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
13
|
+
const tempDirectory = await mkdtemp(path.join(tmpdir(), 'sessions-db-'));
|
|
14
|
+
const databasePath = path.join(tempDirectory, 'auth.db');
|
|
15
|
+
|
|
16
|
+
closeConnection();
|
|
17
|
+
process.env.DATABASE_PATH = databasePath;
|
|
18
|
+
await initializeDatabase();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await runTest();
|
|
22
|
+
} finally {
|
|
23
|
+
closeConnection();
|
|
24
|
+
if (previousDatabasePath === undefined) {
|
|
25
|
+
delete process.env.DATABASE_PATH;
|
|
26
|
+
} else {
|
|
27
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
28
|
+
}
|
|
29
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('session archive queries hide archived rows from active project views', async () => {
|
|
34
|
+
await withIsolatedDatabase(() => {
|
|
35
|
+
sessionsDb.createSession('session-active', 'claude', '/workspace/demo-project', 'Active Session');
|
|
36
|
+
sessionsDb.createSession('session-archived', 'claude', '/workspace/demo-project', 'Archived Session');
|
|
37
|
+
sessionsDb.updateSessionIsArchived('session-archived', true);
|
|
38
|
+
|
|
39
|
+
const activeSessions = sessionsDb.getAllSessions();
|
|
40
|
+
const archivedSessions = sessionsDb.getArchivedSessions();
|
|
41
|
+
const activeProjectSessions = sessionsDb.getSessionsByProjectPath('/workspace/demo-project');
|
|
42
|
+
const allProjectSessions = sessionsDb.getSessionsByProjectPathIncludingArchived('/workspace/demo-project');
|
|
43
|
+
|
|
44
|
+
assert.deepEqual(activeSessions.map((session) => session.session_id), ['session-active']);
|
|
45
|
+
assert.deepEqual(archivedSessions.map((session) => session.session_id), ['session-archived']);
|
|
46
|
+
assert.deepEqual(activeProjectSessions.map((session) => session.session_id), ['session-active']);
|
|
47
|
+
assert.deepEqual(
|
|
48
|
+
allProjectSessions.map((session) => session.session_id).sort(),
|
|
49
|
+
['session-active', 'session-archived'],
|
|
50
|
+
);
|
|
51
|
+
assert.equal(sessionsDb.countSessionsByProjectPath('/workspace/demo-project'), 1);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('createSession reactivates archived rows when the session becomes active again', async () => {
|
|
56
|
+
await withIsolatedDatabase(() => {
|
|
57
|
+
sessionsDb.createSession('session-reused', 'claude', '/workspace/demo-project', 'First Name');
|
|
58
|
+
sessionsDb.updateSessionIsArchived('session-reused', true);
|
|
59
|
+
|
|
60
|
+
sessionsDb.createSession('session-reused', 'claude', '/workspace/demo-project', 'Updated Name');
|
|
61
|
+
|
|
62
|
+
const activeSessions = sessionsDb.getAllSessions();
|
|
63
|
+
const archivedSessions = sessionsDb.getArchivedSessions();
|
|
64
|
+
const restoredSession = sessionsDb.getSessionById('session-reused');
|
|
65
|
+
|
|
66
|
+
assert.equal(activeSessions.length, 1);
|
|
67
|
+
assert.equal(activeSessions[0]?.session_id, 'session-reused');
|
|
68
|
+
assert.equal(activeSessions[0]?.custom_name, 'Updated Name');
|
|
69
|
+
assert.equal(archivedSessions.length, 0);
|
|
70
|
+
assert.equal(restoredSession?.isArchived, 0);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -8,13 +8,14 @@ type SessionRow = {
|
|
|
8
8
|
project_path: string | null;
|
|
9
9
|
jsonl_path: string | null;
|
|
10
10
|
custom_name: string | null;
|
|
11
|
+
isArchived: number;
|
|
11
12
|
created_at: string;
|
|
12
13
|
updated_at: string;
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
type SessionMetadataLookupRow = Pick<
|
|
16
17
|
SessionRow,
|
|
17
|
-
'session_id' | 'provider' | 'project_path' | 'jsonl_path' | 'custom_name' | 'created_at' | 'updated_at'
|
|
18
|
+
'session_id' | 'provider' | 'project_path' | 'jsonl_path' | 'custom_name' | 'isArchived' | 'created_at' | 'updated_at'
|
|
18
19
|
>;
|
|
19
20
|
|
|
20
21
|
function normalizeTimestamp(value?: string): string | null {
|
|
@@ -53,13 +54,14 @@ export const sessionsDb = {
|
|
|
53
54
|
projectsDb.createProjectPath(normalizedProjectPath);
|
|
54
55
|
|
|
55
56
|
db.prepare(
|
|
56
|
-
`INSERT INTO sessions (session_id, provider, custom_name, project_path, jsonl_path, created_at, updated_at)
|
|
57
|
-
VALUES (?, ?, ?, ?, ?, COALESCE(?, CURRENT_TIMESTAMP), COALESCE(?, CURRENT_TIMESTAMP))
|
|
57
|
+
`INSERT INTO sessions (session_id, provider, custom_name, project_path, jsonl_path, isArchived, created_at, updated_at)
|
|
58
|
+
VALUES (?, ?, ?, ?, ?, 0, COALESCE(?, CURRENT_TIMESTAMP), COALESCE(?, CURRENT_TIMESTAMP))
|
|
58
59
|
ON CONFLICT(session_id) DO UPDATE SET
|
|
59
60
|
provider = excluded.provider,
|
|
60
61
|
updated_at = excluded.updated_at,
|
|
61
62
|
project_path = excluded.project_path,
|
|
62
63
|
jsonl_path = excluded.jsonl_path,
|
|
64
|
+
isArchived = 0,
|
|
63
65
|
custom_name = COALESCE(excluded.custom_name, sessions.custom_name)`
|
|
64
66
|
).run(
|
|
65
67
|
sessionId,
|
|
@@ -87,7 +89,7 @@ export const sessionsDb = {
|
|
|
87
89
|
const db = getConnection();
|
|
88
90
|
const row = db
|
|
89
91
|
.prepare(
|
|
90
|
-
`SELECT session_id, provider, project_path, jsonl_path, custom_name, created_at, updated_at
|
|
92
|
+
`SELECT session_id, provider, project_path, jsonl_path, custom_name, isArchived, created_at, updated_at
|
|
91
93
|
FROM sessions
|
|
92
94
|
WHERE session_id = ?
|
|
93
95
|
ORDER BY updated_at DESC
|
|
@@ -102,8 +104,25 @@ export const sessionsDb = {
|
|
|
102
104
|
const db = getConnection();
|
|
103
105
|
return db
|
|
104
106
|
.prepare(
|
|
105
|
-
`SELECT session_id, provider, project_path, jsonl_path, custom_name, created_at, updated_at
|
|
106
|
-
FROM sessions
|
|
107
|
+
`SELECT session_id, provider, project_path, jsonl_path, custom_name, isArchived, created_at, updated_at
|
|
108
|
+
FROM sessions
|
|
109
|
+
WHERE isArchived = 0`
|
|
110
|
+
)
|
|
111
|
+
.all() as SessionRow[];
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Archived rows are intentionally queried separately so the caller can render
|
|
116
|
+
* them in a dedicated view without reintroducing them into active session lists.
|
|
117
|
+
*/
|
|
118
|
+
getArchivedSessions(): SessionRow[] {
|
|
119
|
+
const db = getConnection();
|
|
120
|
+
return db
|
|
121
|
+
.prepare(
|
|
122
|
+
`SELECT session_id, provider, project_path, jsonl_path, custom_name, isArchived, created_at, updated_at
|
|
123
|
+
FROM sessions
|
|
124
|
+
WHERE isArchived = 1
|
|
125
|
+
ORDER BY datetime(COALESCE(updated_at, created_at)) DESC, session_id DESC`
|
|
107
126
|
)
|
|
108
127
|
.all() as SessionRow[];
|
|
109
128
|
},
|
|
@@ -113,7 +132,24 @@ export const sessionsDb = {
|
|
|
113
132
|
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
114
133
|
return db
|
|
115
134
|
.prepare(
|
|
116
|
-
`SELECT session_id, provider, project_path, jsonl_path, custom_name, created_at, updated_at
|
|
135
|
+
`SELECT session_id, provider, project_path, jsonl_path, custom_name, isArchived, created_at, updated_at
|
|
136
|
+
FROM sessions
|
|
137
|
+
WHERE project_path = ?
|
|
138
|
+
AND isArchived = 0`
|
|
139
|
+
)
|
|
140
|
+
.all(normalizedProjectPath) as SessionRow[];
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Permanent project deletion must see every session row for the path,
|
|
145
|
+
* including archived ones, so their transcript files can be cleaned up.
|
|
146
|
+
*/
|
|
147
|
+
getSessionsByProjectPathIncludingArchived(projectPath: string): SessionRow[] {
|
|
148
|
+
const db = getConnection();
|
|
149
|
+
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
150
|
+
return db
|
|
151
|
+
.prepare(
|
|
152
|
+
`SELECT session_id, provider, project_path, jsonl_path, custom_name, isArchived, created_at, updated_at
|
|
117
153
|
FROM sessions
|
|
118
154
|
WHERE project_path = ?`
|
|
119
155
|
)
|
|
@@ -125,9 +161,10 @@ export const sessionsDb = {
|
|
|
125
161
|
const normalizedProjectPath = normalizeProjectPath(projectPath);
|
|
126
162
|
return db
|
|
127
163
|
.prepare(
|
|
128
|
-
`SELECT session_id, provider, project_path, jsonl_path, custom_name, created_at, updated_at
|
|
164
|
+
`SELECT session_id, provider, project_path, jsonl_path, custom_name, isArchived, created_at, updated_at
|
|
129
165
|
FROM sessions
|
|
130
166
|
WHERE project_path = ?
|
|
167
|
+
AND isArchived = 0
|
|
131
168
|
ORDER BY datetime(COALESCE(updated_at, created_at)) DESC, session_id DESC
|
|
132
169
|
LIMIT ? OFFSET ?`
|
|
133
170
|
)
|
|
@@ -141,7 +178,8 @@ export const sessionsDb = {
|
|
|
141
178
|
.prepare(
|
|
142
179
|
`SELECT COUNT(*) AS count
|
|
143
180
|
FROM sessions
|
|
144
|
-
WHERE project_path =
|
|
181
|
+
WHERE project_path = ?
|
|
182
|
+
AND isArchived = 0`
|
|
145
183
|
)
|
|
146
184
|
.get(normalizedProjectPath) as { count: number } | undefined;
|
|
147
185
|
|
|
@@ -167,6 +205,19 @@ export const sessionsDb = {
|
|
|
167
205
|
return row?.custom_name ?? null;
|
|
168
206
|
},
|
|
169
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Soft-delete and restore both use the same flag update so callers keep the
|
|
210
|
+
* row, metadata, and file path intact while toggling visibility.
|
|
211
|
+
*/
|
|
212
|
+
updateSessionIsArchived(sessionId: string, isArchived: boolean): void {
|
|
213
|
+
const db = getConnection();
|
|
214
|
+
db.prepare(
|
|
215
|
+
`UPDATE sessions
|
|
216
|
+
SET isArchived = ?
|
|
217
|
+
WHERE session_id = ?`
|
|
218
|
+
).run(isArchived ? 1 : 0, sessionId);
|
|
219
|
+
},
|
|
220
|
+
|
|
170
221
|
deleteSessionById(sessionId: string): boolean {
|
|
171
222
|
const db = getConnection();
|
|
172
223
|
return db.prepare('DELETE FROM sessions WHERE session_id = ?').run(sessionId).changes > 0;
|
|
@@ -86,6 +86,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
86
86
|
custom_name TEXT,
|
|
87
87
|
project_path TEXT,
|
|
88
88
|
jsonl_path TEXT,
|
|
89
|
+
isArchived BOOLEAN DEFAULT 0,
|
|
89
90
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
90
91
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
91
92
|
PRIMARY KEY (session_id),
|
|
@@ -3,9 +3,9 @@ import express from 'express';
|
|
|
3
3
|
import { createProject, updateProjectDisplayName } from '@/modules/projects/services/project-management.service.js';
|
|
4
4
|
import { startCloneProject } from '@/modules/projects/services/project-clone.service.js';
|
|
5
5
|
import { getProjectTaskMaster } from '@/modules/projects/services/projects-has-taskmaster.service.js';
|
|
6
|
-
import { AppError, asyncHandler } from '@/shared/utils.js';
|
|
7
|
-
import { getProjectSessionsPage, getProjectsWithSessions } from '@/modules/projects/services/projects-with-sessions-fetch.service.js';
|
|
8
|
-
import { deleteOrArchiveProject } from '@/modules/projects/services/project-delete.service.js';
|
|
6
|
+
import { AppError, asyncHandler, createApiSuccessResponse } from '@/shared/utils.js';
|
|
7
|
+
import { getArchivedProjectsWithSessions, getProjectSessionsPage, getProjectsWithSessions } from '@/modules/projects/services/projects-with-sessions-fetch.service.js';
|
|
8
|
+
import { deleteOrArchiveProject, restoreArchivedProject } from '@/modules/projects/services/project-delete.service.js';
|
|
9
9
|
import { applyLegacyStarredProjectIds, toggleProjectStar } from '@/modules/projects/services/project-star.service.js';
|
|
10
10
|
|
|
11
11
|
const router = express.Router();
|
|
@@ -73,6 +73,14 @@ router.get(
|
|
|
73
73
|
}),
|
|
74
74
|
);
|
|
75
75
|
|
|
76
|
+
router.get(
|
|
77
|
+
'/archived',
|
|
78
|
+
asyncHandler(async (_req, res) => {
|
|
79
|
+
const projects = await getArchivedProjectsWithSessions();
|
|
80
|
+
res.json(createApiSuccessResponse({ projects }));
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
|
|
76
84
|
router.get(
|
|
77
85
|
'/:projectId/sessions',
|
|
78
86
|
asyncHandler(async (req, res) => {
|
|
@@ -230,6 +238,15 @@ router.post(
|
|
|
230
238
|
}),
|
|
231
239
|
);
|
|
232
240
|
|
|
241
|
+
router.post(
|
|
242
|
+
'/:projectId/restore',
|
|
243
|
+
asyncHandler(async (req, res) => {
|
|
244
|
+
const projectId = typeof req.params.projectId === 'string' ? req.params.projectId : '';
|
|
245
|
+
restoreArchivedProject(projectId);
|
|
246
|
+
res.json(createApiSuccessResponse({ projectId, isArchived: false }));
|
|
247
|
+
}),
|
|
248
|
+
);
|
|
249
|
+
|
|
233
250
|
/**
|
|
234
251
|
* - `force` not set / false: archive project in DB only (`isArchived` = 1; hidden from active list).
|
|
235
252
|
* - `force=true`: remove DB row, delete session rows for that path, remove all `*.jsonl` under the Claude project dir.
|