@cloudcli-ai/cloudcli 1.31.5 → 1.33.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/README.de.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/README.tr.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/api-docs.html +6 -7
- package/dist/assets/index-MOIvpy53.css +32 -0
- package/dist/assets/{index-DqMVUeZS.js → index-skXSjBje.js} +290 -270
- package/dist/index.html +2 -2
- package/dist/modelConstants.js +841 -0
- package/dist-server/server/claude-sdk.js +67 -39
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cursor-cli.js +6 -9
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/gemini-cli.js +163 -35
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/gemini-response-handler.js +34 -0
- package/dist-server/server/gemini-response-handler.js.map +1 -1
- package/dist-server/server/index.js +131 -19
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/database/index.js +1 -0
- package/dist-server/server/modules/database/index.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/project-management.service.js +1 -0
- package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +47 -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 +27 -7
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +141 -0
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -0
- 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 +4 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +84 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -0
- 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 +50 -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 +4 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +754 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -0
- 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 +187 -24
- 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 +4 -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-models.provider.js +27 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +1 -0
- 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 +10 -10
- 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 +4 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +92 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js +181 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-mcp.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +267 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +115 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +410 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js +62 -0
- package/dist-server/server/modules/providers/list/opencode/opencode-skills.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/opencode/opencode.provider.js +19 -0
- package/dist-server/server/modules/providers/list/opencode/opencode.provider.js.map +1 -0
- package/dist-server/server/modules/providers/provider.registry.js +2 -0
- package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +65 -3
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/mcp.service.js +1 -9
- package/dist-server/server/modules/providers/services/mcp.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +199 -0
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -0
- 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/session-synchronizer.service.js +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +13 -4
- 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/mcp.test.js +73 -6
- package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +66 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +262 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +270 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/skills.test.js +259 -0
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +12 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +4 -4
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +9 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/openai-codex.js +87 -29
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/opencode-cli.js +287 -0
- package/dist-server/server/opencode-cli.js.map +1 -0
- package/dist-server/server/opencode-cli.test.js +84 -0
- package/dist-server/server/opencode-cli.test.js.map +1 -0
- package/dist-server/server/routes/agent.js +21 -8
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/commands.js +204 -211
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/cursor.js +2 -2
- package/dist-server/server/routes/cursor.js.map +1 -1
- package/dist-server/server/routes/settings.js +0 -10
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/routes/tests/commands.test.js +76 -0
- package/dist-server/server/routes/tests/commands.test.js.map +1 -0
- 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 +368 -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/package.json +3 -1
- package/public/api-docs.html +878 -0
- package/public/modelConstants.js +841 -0
- package/server/claude-sdk.js +74 -40
- package/server/cursor-cli.js +6 -11
- package/server/gemini-cli.js +197 -43
- package/server/gemini-response-handler.js +38 -0
- package/server/index.js +144 -19
- package/server/modules/database/index.ts +1 -0
- 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/project-management.service.ts +2 -0
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +71 -1
- package/server/modules/providers/README.md +354 -0
- package/server/modules/providers/index.ts +2 -1
- package/server/modules/providers/list/claude/claude-auth.provider.ts +38 -13
- package/server/modules/providers/list/claude/claude-models.provider.ts +191 -0
- 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 +11 -1
- package/server/modules/providers/list/codex/codex-models.provider.ts +125 -0
- 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 +60 -0
- package/server/modules/providers/list/codex/codex.provider.ts +11 -1
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +820 -0
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +17 -40
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +216 -26
- package/server/modules/providers/list/cursor/cursor-skills.provider.ts +31 -0
- package/server/modules/providers/list/cursor/cursor.provider.ts +11 -1
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +158 -2
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +42 -0
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +24 -20
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +10 -11
- package/server/modules/providers/list/gemini/gemini-skills.provider.ts +36 -0
- package/server/modules/providers/list/gemini/gemini.provider.ts +11 -1
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +111 -0
- package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +228 -0
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +339 -0
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +157 -0
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +506 -0
- package/server/modules/providers/list/opencode/opencode-skills.provider.ts +78 -0
- package/server/modules/providers/list/opencode/opencode.provider.ts +27 -0
- package/server/modules/providers/provider.registry.ts +2 -0
- package/server/modules/providers/provider.routes.ts +97 -4
- package/server/modules/providers/services/mcp.service.ts +1 -12
- package/server/modules/providers/services/provider-models.service.ts +325 -0
- package/server/modules/providers/services/session-conversations-search.service.ts +162 -13
- package/server/modules/providers/services/session-synchronizer.service.ts +1 -0
- package/server/modules/providers/services/sessions-watcher.service.ts +14 -4
- 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 +4 -0
- package/server/modules/providers/shared/skills/skills.provider.ts +64 -0
- package/server/modules/providers/tests/mcp.test.ts +93 -6
- package/server/modules/providers/tests/opencode-models.test.ts +73 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +334 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +318 -0
- package/server/modules/providers/tests/skills.test.ts +512 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +15 -1
- package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +4 -4
- package/server/modules/websocket/services/shell-websocket.service.ts +9 -0
- package/server/openai-codex.js +99 -30
- package/server/opencode-cli.js +336 -0
- package/server/opencode-cli.test.js +95 -0
- package/server/routes/agent.js +22 -8
- package/server/routes/commands.js +256 -233
- package/server/routes/cursor.js +2 -2
- package/server/routes/settings.js +1 -10
- package/server/routes/tests/commands.test.js +82 -0
- 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 +64 -0
- package/server/shared/types.ts +166 -1
- package/server/shared/utils.ts +478 -0
- package/server/utils/commandParser.js +5 -3
- package/dist/assets/index-6rYG_mRo.css +0 -32
- package/dist-server/server/utils/frontmatter.js.map +0 -1
- package/dist-server/shared/modelConstants.js +0 -98
- package/dist-server/shared/modelConstants.js.map +0 -1
- package/shared/modelConstants.js +0 -106
package/server/index.js
CHANGED
|
@@ -10,8 +10,9 @@ import { spawn } from 'child_process';
|
|
|
10
10
|
import express from 'express';
|
|
11
11
|
import cors from 'cors';
|
|
12
12
|
import mime from 'mime-types';
|
|
13
|
+
import Database from 'better-sqlite3';
|
|
13
14
|
|
|
14
|
-
import { AppError, WORKSPACES_ROOT, validateWorkspacePath } from '@/shared/utils.js';
|
|
15
|
+
import { AppError, WORKSPACES_ROOT, getOpenCodeDatabasePath, validateWorkspacePath } from '@/shared/utils.js';
|
|
15
16
|
import { closeSessionsWatcher, initializeSessionsWatcher } from '@/modules/providers/index.js';
|
|
16
17
|
import { createWebSocketServer } from '@/modules/websocket/index.js';
|
|
17
18
|
|
|
@@ -45,6 +46,12 @@ import {
|
|
|
45
46
|
isGeminiSessionActive,
|
|
46
47
|
getActiveGeminiSessions,
|
|
47
48
|
} from './gemini-cli.js';
|
|
49
|
+
import {
|
|
50
|
+
spawnOpenCode,
|
|
51
|
+
abortOpenCodeSession,
|
|
52
|
+
isOpenCodeSessionActive,
|
|
53
|
+
getActiveOpenCodeSessions,
|
|
54
|
+
} from './opencode-cli.js';
|
|
48
55
|
import sessionManager from './sessionManager.js';
|
|
49
56
|
import {
|
|
50
57
|
stripAnsiSequences,
|
|
@@ -66,7 +73,7 @@ import geminiRoutes from './routes/gemini.js';
|
|
|
66
73
|
import pluginsRoutes from './routes/plugins.js';
|
|
67
74
|
import providerRoutes from './modules/providers/provider.routes.js';
|
|
68
75
|
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
|
|
69
|
-
import { initializeDatabase, projectsDb } from './modules/database/index.js';
|
|
76
|
+
import { initializeDatabase, projectsDb, sessionsDb } from './modules/database/index.js';
|
|
70
77
|
import { configureWebPush } from './services/vapid-keys.js';
|
|
71
78
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
72
79
|
import { IS_PLATFORM } from './constants/config.js';
|
|
@@ -94,21 +101,25 @@ const wss = createWebSocketServer(server, {
|
|
|
94
101
|
spawnCursor,
|
|
95
102
|
queryCodex,
|
|
96
103
|
spawnGemini,
|
|
104
|
+
spawnOpenCode,
|
|
97
105
|
abortClaudeSDKSession,
|
|
98
106
|
abortCursorSession,
|
|
99
107
|
abortCodexSession,
|
|
100
108
|
abortGeminiSession,
|
|
109
|
+
abortOpenCodeSession,
|
|
101
110
|
resolveToolApproval,
|
|
102
111
|
isClaudeSDKSessionActive,
|
|
103
112
|
isCursorSessionActive,
|
|
104
113
|
isCodexSessionActive,
|
|
105
114
|
isGeminiSessionActive,
|
|
115
|
+
isOpenCodeSessionActive,
|
|
106
116
|
reconnectSessionWriter,
|
|
107
117
|
getPendingApprovalsForSession,
|
|
108
118
|
getActiveClaudeSDKSessions,
|
|
109
119
|
getActiveCursorSessions,
|
|
110
120
|
getActiveCodexSessions,
|
|
111
121
|
getActiveGeminiSessions,
|
|
122
|
+
getActiveOpenCodeSessions,
|
|
112
123
|
},
|
|
113
124
|
shell: {
|
|
114
125
|
getSessionById: (sessionId) => sessionManager.getSession(sessionId),
|
|
@@ -1131,23 +1142,129 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1131
1142
|
return res.json({
|
|
1132
1143
|
used: 0,
|
|
1133
1144
|
total: 0,
|
|
1134
|
-
|
|
1145
|
+
inputTokens: 0,
|
|
1146
|
+
outputTokens: 0,
|
|
1147
|
+
breakdown: { input: 0, output: 0 },
|
|
1135
1148
|
unsupported: true,
|
|
1136
1149
|
message: 'Token usage tracking not available for Cursor sessions'
|
|
1137
1150
|
});
|
|
1138
1151
|
}
|
|
1139
1152
|
|
|
1140
|
-
// Handle Gemini sessions - they are raw logs in our current setup
|
|
1141
1153
|
if (provider === 'gemini') {
|
|
1154
|
+
const session = sessionsDb.getSessionById(safeSessionId);
|
|
1155
|
+
const sessionFilePath = session?.jsonl_path;
|
|
1156
|
+
if (!sessionFilePath) {
|
|
1157
|
+
return res.json({
|
|
1158
|
+
used: 0,
|
|
1159
|
+
inputTokens: 0,
|
|
1160
|
+
outputTokens: 0,
|
|
1161
|
+
breakdown: { input: 0, output: 0 },
|
|
1162
|
+
unsupported: true,
|
|
1163
|
+
message: 'Token usage tracking not available for this Gemini session'
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
let fileContent;
|
|
1168
|
+
try {
|
|
1169
|
+
fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
if (error.code === 'ENOENT') {
|
|
1172
|
+
return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
|
|
1173
|
+
}
|
|
1174
|
+
throw error;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const lines = fileContent.trim().split('\n');
|
|
1178
|
+
let inputTokens = 0;
|
|
1179
|
+
let outputTokens = 0;
|
|
1180
|
+
let totalTokens = 0;
|
|
1181
|
+
|
|
1182
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1183
|
+
try {
|
|
1184
|
+
const entry = JSON.parse(lines[i]);
|
|
1185
|
+
if (!entry.tokens || typeof entry.tokens !== 'object') {
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
inputTokens = Number(entry.tokens.input || 0);
|
|
1190
|
+
outputTokens = Number(entry.tokens.output || 0);
|
|
1191
|
+
totalTokens = Number(entry.tokens.total || inputTokens + outputTokens || 0);
|
|
1192
|
+
break;
|
|
1193
|
+
} catch {
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1142
1198
|
return res.json({
|
|
1143
|
-
used:
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1199
|
+
used: totalTokens,
|
|
1200
|
+
inputTokens,
|
|
1201
|
+
outputTokens,
|
|
1202
|
+
breakdown: {
|
|
1203
|
+
input: inputTokens,
|
|
1204
|
+
output: outputTokens
|
|
1205
|
+
}
|
|
1148
1206
|
});
|
|
1149
1207
|
}
|
|
1150
1208
|
|
|
1209
|
+
if (provider === 'opencode') {
|
|
1210
|
+
const dbPath = getOpenCodeDatabasePath();
|
|
1211
|
+
if (!fs.existsSync(dbPath)) {
|
|
1212
|
+
return res.status(404).json({ error: 'OpenCode database not found' });
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
1216
|
+
try {
|
|
1217
|
+
const columns = db.prepare('PRAGMA table_info(session)').all();
|
|
1218
|
+
const columnNames = new Set(columns.map((column) => column.name));
|
|
1219
|
+
const requiredColumns = ['tokens_input', 'tokens_output', 'tokens_reasoning', 'tokens_cache_read', 'tokens_cache_write'];
|
|
1220
|
+
if (!requiredColumns.every((column) => columnNames.has(column))) {
|
|
1221
|
+
return res.json({
|
|
1222
|
+
used: 0,
|
|
1223
|
+
inputTokens: 0,
|
|
1224
|
+
outputTokens: 0,
|
|
1225
|
+
breakdown: { input: 0, output: 0 },
|
|
1226
|
+
unsupported: true,
|
|
1227
|
+
message: 'Token usage tracking is not available in this OpenCode database schema'
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const row = db.prepare(`
|
|
1232
|
+
SELECT
|
|
1233
|
+
tokens_input AS inputTokens,
|
|
1234
|
+
tokens_output AS outputTokens,
|
|
1235
|
+
tokens_reasoning AS reasoningTokens,
|
|
1236
|
+
tokens_cache_read AS cacheReadTokens,
|
|
1237
|
+
tokens_cache_write AS cacheWriteTokens
|
|
1238
|
+
FROM session
|
|
1239
|
+
WHERE id = ?
|
|
1240
|
+
`).get(safeSessionId);
|
|
1241
|
+
|
|
1242
|
+
if (!row) {
|
|
1243
|
+
return res.status(404).json({ error: 'OpenCode session not found', sessionId: safeSessionId });
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
const inputTokens = Number(row.inputTokens || 0) + Number(row.cacheReadTokens || 0);
|
|
1247
|
+
const outputTokens = Number(row.outputTokens || 0);
|
|
1248
|
+
const totalUsed = Number(row.inputTokens || 0)
|
|
1249
|
+
+ outputTokens
|
|
1250
|
+
+ Number(row.reasoningTokens || 0)
|
|
1251
|
+
+ Number(row.cacheReadTokens || 0)
|
|
1252
|
+
+ Number(row.cacheWriteTokens || 0);
|
|
1253
|
+
|
|
1254
|
+
return res.json({
|
|
1255
|
+
used: totalUsed,
|
|
1256
|
+
inputTokens,
|
|
1257
|
+
outputTokens,
|
|
1258
|
+
breakdown: {
|
|
1259
|
+
input: inputTokens,
|
|
1260
|
+
output: outputTokens
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
} finally {
|
|
1264
|
+
db.close();
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1151
1268
|
// Handle Codex sessions
|
|
1152
1269
|
if (provider === 'codex') {
|
|
1153
1270
|
const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
|
|
@@ -1188,6 +1305,8 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1188
1305
|
throw error;
|
|
1189
1306
|
}
|
|
1190
1307
|
const lines = fileContent.trim().split('\n');
|
|
1308
|
+
let inputTokens = 0;
|
|
1309
|
+
let outputTokens = 0;
|
|
1191
1310
|
let totalTokens = 0;
|
|
1192
1311
|
let contextWindow = 200000; // Default for Codex/OpenAI
|
|
1193
1312
|
|
|
@@ -1200,7 +1319,9 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1200
1319
|
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
|
1201
1320
|
const tokenInfo = entry.payload.info;
|
|
1202
1321
|
if (tokenInfo.total_token_usage) {
|
|
1203
|
-
|
|
1322
|
+
inputTokens = tokenInfo.total_token_usage.input_tokens || 0;
|
|
1323
|
+
outputTokens = tokenInfo.total_token_usage.output_tokens || 0;
|
|
1324
|
+
totalTokens = tokenInfo.total_token_usage.total_tokens || inputTokens + outputTokens;
|
|
1204
1325
|
}
|
|
1205
1326
|
if (tokenInfo.model_context_window) {
|
|
1206
1327
|
contextWindow = tokenInfo.model_context_window;
|
|
@@ -1215,7 +1336,13 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1215
1336
|
|
|
1216
1337
|
return res.json({
|
|
1217
1338
|
used: totalTokens,
|
|
1218
|
-
total: contextWindow
|
|
1339
|
+
total: contextWindow,
|
|
1340
|
+
inputTokens,
|
|
1341
|
+
outputTokens,
|
|
1342
|
+
breakdown: {
|
|
1343
|
+
input: inputTokens,
|
|
1344
|
+
output: outputTokens
|
|
1345
|
+
}
|
|
1219
1346
|
});
|
|
1220
1347
|
}
|
|
1221
1348
|
|
|
@@ -1258,8 +1385,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1258
1385
|
const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
|
|
1259
1386
|
const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
|
|
1260
1387
|
let inputTokens = 0;
|
|
1261
|
-
let
|
|
1262
|
-
let cacheReadTokens = 0;
|
|
1388
|
+
let outputTokens = 0;
|
|
1263
1389
|
|
|
1264
1390
|
// Find the latest assistant message with usage data (scan from end)
|
|
1265
1391
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -1272,8 +1398,7 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1272
1398
|
|
|
1273
1399
|
// Use token counts from latest assistant message only
|
|
1274
1400
|
inputTokens = usage.input_tokens || 0;
|
|
1275
|
-
|
|
1276
|
-
cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
1401
|
+
outputTokens = usage.output_tokens || 0;
|
|
1277
1402
|
|
|
1278
1403
|
break; // Stop after finding the latest assistant message
|
|
1279
1404
|
}
|
|
@@ -1283,16 +1408,16 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1283
1408
|
}
|
|
1284
1409
|
}
|
|
1285
1410
|
|
|
1286
|
-
|
|
1287
|
-
const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
|
|
1411
|
+
const totalUsed = inputTokens + outputTokens;
|
|
1288
1412
|
|
|
1289
1413
|
res.json({
|
|
1290
1414
|
used: totalUsed,
|
|
1291
1415
|
total: contextWindow,
|
|
1416
|
+
inputTokens,
|
|
1417
|
+
outputTokens,
|
|
1292
1418
|
breakdown: {
|
|
1293
1419
|
input: inputTokens,
|
|
1294
|
-
|
|
1295
|
-
cacheRead: cacheReadTokens
|
|
1420
|
+
output: outputTokens
|
|
1296
1421
|
}
|
|
1297
1422
|
});
|
|
1298
1423
|
} catch (error) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { initializeDatabase } from '@/modules/database/init-db.js';
|
|
2
|
+
export { closeConnection, getConnection, getDatabasePath } from '@/modules/database/connection.js';
|
|
2
3
|
export { apiKeysDb } from '@/modules/database/repositories/api-keys.js';
|
|
3
4
|
export { appConfigDb } from '@/modules/database/repositories/app-config.js';
|
|
4
5
|
export { credentialsDb } from '@/modules/database/repositories/credentials.js';
|
|
@@ -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.
|
|
@@ -42,7 +42,7 @@ async function unlinkJsonlIfExists(filePath: string): Promise<void> {
|
|
|
42
42
|
* Loads all session rows for the project path and removes each distinct `jsonl_path` file on disk.
|
|
43
43
|
*/
|
|
44
44
|
export async function deleteSessionJsonlFilesForProjectPath(projectPath: string): Promise<void> {
|
|
45
|
-
const sessions = sessionsDb.
|
|
45
|
+
const sessions = sessionsDb.getSessionsByProjectPathIncludingArchived(projectPath);
|
|
46
46
|
const paths = uniqueJsonlPathsFromSessions(sessions);
|
|
47
47
|
|
|
48
48
|
for (const filePath of paths) {
|
|
@@ -73,3 +73,18 @@ export async function deleteOrArchiveProject(projectId: string, force: boolean):
|
|
|
73
73
|
sessionsDb.deleteSessionsByProjectPath(row.project_path);
|
|
74
74
|
projectsDb.deleteProjectById(projectId);
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Restores one archived project row back into the active project list.
|
|
79
|
+
*/
|
|
80
|
+
export function restoreArchivedProject(projectId: string): void {
|
|
81
|
+
const row = projectsDb.getProjectById(projectId);
|
|
82
|
+
if (!row) {
|
|
83
|
+
throw new AppError(`Unknown projectId: ${projectId}`, {
|
|
84
|
+
code: 'PROJECT_NOT_FOUND',
|
|
85
|
+
statusCode: 404,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
projectsDb.updateProjectIsArchivedById(projectId, false);
|
|
90
|
+
}
|
|
@@ -33,6 +33,7 @@ type ProjectApiView = {
|
|
|
33
33
|
cursorSessions: [];
|
|
34
34
|
codexSessions: [];
|
|
35
35
|
geminiSessions: [];
|
|
36
|
+
opencodeSessions: [];
|
|
36
37
|
sessionMeta: {
|
|
37
38
|
hasMore: false;
|
|
38
39
|
total: 0;
|
|
@@ -84,6 +85,7 @@ function mapProjectRowToApiView(projectRow: ProjectRepositoryRow): ProjectApiVie
|
|
|
84
85
|
cursorSessions: [],
|
|
85
86
|
codexSessions: [],
|
|
86
87
|
geminiSessions: [],
|
|
88
|
+
opencodeSessions: [],
|
|
87
89
|
sessionMeta: {
|
|
88
90
|
hasMore: false,
|
|
89
91
|
total: 0,
|