@cloudcli-ai/cloudcli 1.35.1 → 1.36.1
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 +17 -18
- package/README.ja.md +16 -17
- package/README.ko.md +17 -18
- package/README.md +19 -20
- package/README.ru.md +17 -18
- package/README.tr.md +17 -18
- package/README.zh-CN.md +17 -18
- package/README.zh-TW.md +17 -18
- package/dist/api-docs.html +8 -3
- package/dist/assets/{index-D1IdCTT6.js → index-Bz9vXV2m.js} +265 -260
- package/dist/assets/index-DLmhQ15L.css +32 -0
- package/dist/assets/{vendor-xterm-CJZjLICi.js → vendor-xterm-CS4rQ5Mr.js} +12 -12
- package/dist/index.html +3 -3
- package/dist-server/server/claude-sdk.js +62 -116
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/cli.js +1 -4
- package/dist-server/server/cli.js.map +1 -1
- package/dist-server/server/cursor-cli.js +12 -11
- package/dist-server/server/cursor-cli.js.map +1 -1
- package/dist-server/server/index.js +10 -141
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/assets/assets.routes.js +89 -0
- package/dist-server/server/modules/assets/assets.routes.js.map +1 -0
- package/dist-server/server/modules/assets/index.js +4 -0
- package/dist-server/server/modules/assets/index.js.map +1 -0
- package/dist-server/server/modules/assets/services/image-assets.service.js +57 -0
- package/dist-server/server/modules/assets/services/image-assets.service.js.map +1 -0
- package/dist-server/server/modules/assets/tests/image-assets.service.test.js +36 -0
- package/dist-server/server/modules/assets/tests/image-assets.service.test.js.map +1 -0
- package/dist-server/server/modules/browser-use/browser-use.service.js +6 -3
- package/dist-server/server/modules/browser-use/browser-use.service.js.map +1 -1
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js +2 -2
- package/dist-server/server/modules/database/tests/sessions-provider-mapping.test.js.map +1 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
- package/dist-server/server/modules/notifications/services/notification-orchestrator.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-clone.service.js +2 -1
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +70 -1
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +28 -0
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +52 -11
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +54 -1
- 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 +36 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js +3 -17
- package/dist-server/server/modules/providers/list/cursor/cursor-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +7 -1
- 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 +49 -13
- package/dist-server/server/modules/providers/list/cursor/cursor-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js +0 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +145 -14
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +24 -5
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +10 -21
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.registry.js +0 -2
- package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +0 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +11 -13
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +2 -2
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +3 -77
- 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 +0 -1
- 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 +0 -13
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +96 -0
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/mcp.test.js +3 -29
- package/dist-server/server/modules/providers/tests/mcp.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +65 -1
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +117 -0
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-image-history.test.js +147 -0
- package/dist-server/server/modules/providers/tests/provider-image-history.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +8 -8
- package/dist-server/server/modules/providers/tests/skills.test.js +3 -26
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +14 -0
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +36 -2
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +3 -12
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js +33 -0
- package/dist-server/server/modules/websocket/tests/chat-image-filter.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +37 -4
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
- package/dist-server/server/openai-codex.js +16 -9
- package/dist-server/server/openai-codex.js.map +1 -1
- package/dist-server/server/opencode-cli.js +62 -7
- package/dist-server/server/opencode-cli.js.map +1 -1
- package/dist-server/server/opencode-cli.test.js +103 -3
- package/dist-server/server/opencode-cli.test.js.map +1 -1
- package/dist-server/server/routes/agent.js +22 -21
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/commands.js +1 -2
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/git.js +179 -53
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/git.test.js +80 -0
- package/dist-server/server/routes/git.test.js.map +1 -0
- package/dist-server/server/routes/taskmaster.js +3 -1
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/routes/user.js +2 -1
- package/dist-server/server/routes/user.js.map +1 -1
- package/dist-server/server/shared/image-attachments.js +270 -0
- package/dist-server/server/shared/image-attachments.js.map +1 -0
- package/dist-server/server/shared/tests/image-attachments.test.js +220 -0
- package/dist-server/server/shared/tests/image-attachments.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +43 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/utils/gitConfig.js +2 -1
- package/dist-server/server/utils/gitConfig.js.map +1 -1
- package/dist-server/server/utils/plugin-process-manager.js +2 -1
- package/dist-server/server/utils/plugin-process-manager.js.map +1 -1
- package/electron/launcher/launcher.js +1 -1
- package/package.json +3 -3
- package/public/api-docs.html +8 -3
- package/server/claude-sdk.js +69 -131
- package/server/cli.js +1 -4
- package/server/cursor-cli.js +13 -12
- package/server/index.js +11 -159
- package/server/modules/assets/assets.routes.ts +103 -0
- package/server/modules/assets/index.ts +3 -0
- package/server/modules/assets/services/image-assets.service.ts +82 -0
- package/server/modules/assets/tests/image-assets.service.test.ts +46 -0
- package/server/modules/browser-use/browser-use.service.ts +7 -3
- package/server/modules/database/tests/sessions-provider-mapping.test.ts +2 -2
- package/server/modules/notifications/services/notification-orchestrator.service.js +0 -1
- package/server/modules/projects/services/project-clone.service.ts +3 -1
- package/server/modules/providers/README.md +1 -7
- package/server/modules/providers/list/claude/claude-models.provider.ts +73 -1
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +30 -0
- package/server/modules/providers/list/codex/codex-models.provider.ts +59 -11
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +60 -1
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +41 -1
- package/server/modules/providers/list/cursor/cursor-models.provider.ts +3 -17
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +7 -1
- package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +56 -15
- package/server/modules/providers/list/opencode/opencode-auth.provider.ts +0 -1
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +193 -18
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +25 -4
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +10 -21
- package/server/modules/providers/provider.registry.ts +0 -2
- package/server/modules/providers/provider.routes.ts +0 -1
- package/server/modules/providers/services/provider-capabilities.service.ts +13 -13
- package/server/modules/providers/services/provider-models.service.ts +2 -2
- package/server/modules/providers/services/session-conversations-search.service.ts +3 -93
- package/server/modules/providers/services/session-synchronizer.service.ts +0 -1
- package/server/modules/providers/services/sessions-watcher.service.ts +0 -14
- package/server/modules/providers/tests/codex-sessions.test.ts +111 -0
- package/server/modules/providers/tests/mcp.test.ts +3 -34
- package/server/modules/providers/tests/opencode-models.test.ts +69 -0
- package/server/modules/providers/tests/opencode-sessions.test.ts +139 -0
- package/server/modules/providers/tests/provider-image-history.test.ts +180 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +8 -8
- package/server/modules/providers/tests/skills.test.ts +3 -42
- package/server/modules/websocket/services/chat-run-registry.service.ts +16 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +41 -2
- package/server/modules/websocket/services/shell-websocket.service.ts +1 -11
- package/server/modules/websocket/tests/chat-image-filter.test.ts +44 -0
- package/server/modules/websocket/tests/chat-run-registry.test.ts +42 -4
- package/server/openai-codex.js +18 -8
- package/server/opencode-cli.js +64 -7
- package/server/opencode-cli.test.js +112 -3
- package/server/routes/agent.js +22 -21
- package/server/routes/commands.js +1 -2
- package/server/routes/git.js +201 -60
- package/server/routes/git.test.js +106 -0
- package/server/routes/taskmaster.js +3 -1
- package/server/routes/user.js +2 -1
- package/server/shared/image-attachments.ts +341 -0
- package/server/shared/tests/image-attachments.test.ts +298 -0
- package/server/shared/types.ts +8 -1
- package/server/shared/utils.ts +45 -0
- package/server/utils/gitConfig.js +2 -1
- package/server/utils/plugin-process-manager.js +3 -1
- package/dist/assets/index-BvX30NW5.css +0 -32
- package/dist/icons/gemini-ai-icon.svg +0 -1
- package/dist-server/server/gemini-cli.js +0 -551
- package/dist-server/server/gemini-cli.js.map +0 -1
- package/dist-server/server/gemini-response-handler.js +0 -106
- package/dist-server/server/gemini-response-handler.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +0 -254
- package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js +0 -82
- package/dist-server/server/modules/providers/list/gemini/gemini-mcp.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js +0 -24
- package/dist-server/server/modules/providers/list/gemini/gemini-models.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +0 -312
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +0 -474
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js +0 -40
- package/dist-server/server/modules/providers/list/gemini/gemini-skills.provider.js.map +0 -1
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js +0 -19
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +0 -1
- package/dist-server/server/routes/gemini.js +0 -21
- package/dist-server/server/routes/gemini.js.map +0 -1
- package/dist-server/server/sessionManager.js +0 -194
- package/dist-server/server/sessionManager.js.map +0 -1
- package/server/gemini-cli.js +0 -638
- package/server/gemini-response-handler.js +0 -117
- package/server/modules/providers/list/gemini/gemini-auth.provider.ts +0 -307
- package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +0 -110
- package/server/modules/providers/list/gemini/gemini-models.provider.ts +0 -39
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +0 -405
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +0 -540
- package/server/modules/providers/list/gemini/gemini-skills.provider.ts +0 -44
- package/server/modules/providers/list/gemini/gemini.provider.ts +0 -27
- package/server/routes/gemini.js +0 -25
- package/server/sessionManager.js +0 -226
package/server/index.js
CHANGED
|
@@ -5,8 +5,10 @@ import fs, { promises as fsPromises } from 'fs';
|
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import os from 'os';
|
|
7
7
|
import http from 'http';
|
|
8
|
-
import { spawn } from 'child_process';
|
|
9
8
|
|
|
9
|
+
// cross-spawn is a drop-in for child_process.spawn that resolves .cmd
|
|
10
|
+
// shims/PATHEXT on Windows and delegates to the native spawn elsewhere.
|
|
11
|
+
import spawn from 'cross-spawn';
|
|
10
12
|
import express from 'express';
|
|
11
13
|
import cors from 'cors';
|
|
12
14
|
import mime from 'mime-types';
|
|
@@ -33,15 +35,10 @@ import {
|
|
|
33
35
|
queryCodex,
|
|
34
36
|
abortCodexSession,
|
|
35
37
|
} from './openai-codex.js';
|
|
36
|
-
import {
|
|
37
|
-
spawnGemini,
|
|
38
|
-
abortGeminiSession,
|
|
39
|
-
} from './gemini-cli.js';
|
|
40
38
|
import {
|
|
41
39
|
spawnOpenCode,
|
|
42
40
|
abortOpenCodeSession,
|
|
43
41
|
} from './opencode-cli.js';
|
|
44
|
-
import sessionManager from './sessionManager.js';
|
|
45
42
|
import {
|
|
46
43
|
stripAnsiSequences,
|
|
47
44
|
normalizeDetectedUrl,
|
|
@@ -59,11 +56,11 @@ import agentRoutes from './routes/agent.js';
|
|
|
59
56
|
import projectModuleRoutes from './modules/projects/projects.routes.js';
|
|
60
57
|
import notificationRoutes from './modules/notifications/notifications.routes.js';
|
|
61
58
|
import userRoutes from './routes/user.js';
|
|
62
|
-
import geminiRoutes from './routes/gemini.js';
|
|
63
59
|
import pluginsRoutes from './routes/plugins.js';
|
|
64
60
|
import providerRoutes from './modules/providers/provider.routes.js';
|
|
65
61
|
import voiceRoutes from './voice-proxy.js';
|
|
66
62
|
import browserUseRoutes from './modules/browser-use/browser-use.routes.js';
|
|
63
|
+
import { assetsRoutes } from './modules/assets/index.js';
|
|
67
64
|
import browserUseMcpRoutes from './modules/browser-use/browser-use-mcp.routes.js';
|
|
68
65
|
import { browserUseService } from './modules/browser-use/browser-use.service.js';
|
|
69
66
|
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
|
|
@@ -116,14 +113,12 @@ const wss = createWebSocketServer(server, {
|
|
|
116
113
|
claude: queryClaudeSDK,
|
|
117
114
|
cursor: spawnCursor,
|
|
118
115
|
codex: queryCodex,
|
|
119
|
-
gemini: spawnGemini,
|
|
120
116
|
opencode: spawnOpenCode,
|
|
121
117
|
},
|
|
122
118
|
abortFns: {
|
|
123
119
|
claude: abortClaudeSDKSession,
|
|
124
120
|
cursor: abortCursorSession,
|
|
125
121
|
codex: abortCodexSession,
|
|
126
|
-
gemini: abortGeminiSession,
|
|
127
122
|
opencode: abortOpenCodeSession,
|
|
128
123
|
},
|
|
129
124
|
resolveToolApproval,
|
|
@@ -132,14 +127,11 @@ const wss = createWebSocketServer(server, {
|
|
|
132
127
|
shell: {
|
|
133
128
|
resolveProviderSessionId: (sessionId, provider) => {
|
|
134
129
|
const dbSession = sessionsDb.getSessionById(sessionId);
|
|
135
|
-
const legacyGeminiSession =
|
|
136
|
-
provider === 'gemini' ? sessionManager.getSession(sessionId) : null;
|
|
137
|
-
|
|
138
130
|
if (dbSession) {
|
|
139
|
-
return dbSession.provider_session_id ??
|
|
131
|
+
return dbSession.provider_session_id ?? null;
|
|
140
132
|
}
|
|
141
133
|
|
|
142
|
-
return
|
|
134
|
+
return null;
|
|
143
135
|
},
|
|
144
136
|
stripAnsiSequences,
|
|
145
137
|
normalizeDetectedUrl,
|
|
@@ -185,6 +177,9 @@ app.use('/api/auth', authRoutes);
|
|
|
185
177
|
// Projects API Routes (protected)
|
|
186
178
|
app.use('/api/projects', authenticateToken, projectModuleRoutes);
|
|
187
179
|
|
|
180
|
+
// Chat image asset upload/serving (global ~/.cloudcli/assets store, protected)
|
|
181
|
+
app.use('/api/assets', authenticateToken, assetsRoutes);
|
|
182
|
+
|
|
188
183
|
// Git API Routes (protected)
|
|
189
184
|
app.use('/api/git', authenticateToken, gitRoutes);
|
|
190
185
|
|
|
@@ -208,9 +203,6 @@ app.use('/api/notifications', authenticateToken, notificationRoutes);
|
|
|
208
203
|
// User API Routes (protected)
|
|
209
204
|
app.use('/api/user', authenticateToken, userRoutes);
|
|
210
205
|
|
|
211
|
-
// Gemini API Routes (protected)
|
|
212
|
-
app.use('/api/gemini', authenticateToken, geminiRoutes);
|
|
213
|
-
|
|
214
206
|
// Plugins API Routes (protected)
|
|
215
207
|
app.use('/api/plugins', authenticateToken, pluginsRoutes);
|
|
216
208
|
|
|
@@ -1072,92 +1064,8 @@ const uploadFilesHandler = async (req, res) => {
|
|
|
1072
1064
|
|
|
1073
1065
|
app.post('/api/projects/:projectId/files/upload', authenticateToken, uploadFilesHandler);
|
|
1074
1066
|
|
|
1075
|
-
//
|
|
1076
|
-
//
|
|
1077
|
-
// so we just leave the param rename for consistency with the rest of the API.
|
|
1078
|
-
app.post('/api/projects/:projectId/upload-images', authenticateToken, async (req, res) => {
|
|
1079
|
-
try {
|
|
1080
|
-
const multer = (await import('multer')).default;
|
|
1081
|
-
const path = (await import('path')).default;
|
|
1082
|
-
const fs = (await import('fs')).promises;
|
|
1083
|
-
const os = (await import('os')).default;
|
|
1084
|
-
|
|
1085
|
-
// Configure multer for image uploads
|
|
1086
|
-
const storage = multer.diskStorage({
|
|
1087
|
-
destination: async (req, file, cb) => {
|
|
1088
|
-
const uploadDir = path.join(os.tmpdir(), 'claude-ui-uploads', String(req.user.id));
|
|
1089
|
-
await fs.mkdir(uploadDir, { recursive: true });
|
|
1090
|
-
cb(null, uploadDir);
|
|
1091
|
-
},
|
|
1092
|
-
filename: (req, file, cb) => {
|
|
1093
|
-
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
1094
|
-
const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
1095
|
-
cb(null, uniqueSuffix + '-' + sanitizedName);
|
|
1096
|
-
}
|
|
1097
|
-
});
|
|
1098
|
-
|
|
1099
|
-
const fileFilter = (req, file, cb) => {
|
|
1100
|
-
const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
|
|
1101
|
-
if (allowedMimes.includes(file.mimetype)) {
|
|
1102
|
-
cb(null, true);
|
|
1103
|
-
} else {
|
|
1104
|
-
cb(new Error('Invalid file type. Only JPEG, PNG, GIF, WebP, and SVG are allowed.'));
|
|
1105
|
-
}
|
|
1106
|
-
};
|
|
1107
|
-
|
|
1108
|
-
const upload = multer({
|
|
1109
|
-
storage,
|
|
1110
|
-
fileFilter,
|
|
1111
|
-
limits: {
|
|
1112
|
-
fileSize: 5 * 1024 * 1024, // 5MB
|
|
1113
|
-
files: 5
|
|
1114
|
-
}
|
|
1115
|
-
});
|
|
1116
|
-
|
|
1117
|
-
// Handle multipart form data
|
|
1118
|
-
upload.array('images', 5)(req, res, async (err) => {
|
|
1119
|
-
if (err) {
|
|
1120
|
-
return res.status(400).json({ error: err.message });
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
if (!req.files || req.files.length === 0) {
|
|
1124
|
-
return res.status(400).json({ error: 'No image files provided' });
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
try {
|
|
1128
|
-
// Process uploaded images
|
|
1129
|
-
const processedImages = await Promise.all(
|
|
1130
|
-
req.files.map(async (file) => {
|
|
1131
|
-
// Read file and convert to base64
|
|
1132
|
-
const buffer = await fs.readFile(file.path);
|
|
1133
|
-
const base64 = buffer.toString('base64');
|
|
1134
|
-
const mimeType = file.mimetype;
|
|
1135
|
-
|
|
1136
|
-
// Clean up temp file immediately
|
|
1137
|
-
await fs.unlink(file.path);
|
|
1138
|
-
|
|
1139
|
-
return {
|
|
1140
|
-
name: file.originalname,
|
|
1141
|
-
data: `data:${mimeType};base64,${base64}`,
|
|
1142
|
-
size: file.size,
|
|
1143
|
-
mimeType: mimeType
|
|
1144
|
-
};
|
|
1145
|
-
})
|
|
1146
|
-
);
|
|
1147
|
-
|
|
1148
|
-
res.json({ images: processedImages });
|
|
1149
|
-
} catch (error) {
|
|
1150
|
-
console.error('Error processing images:', error);
|
|
1151
|
-
// Clean up any remaining files
|
|
1152
|
-
await Promise.all(req.files.map(f => fs.unlink(f.path).catch(() => { })));
|
|
1153
|
-
res.status(500).json({ error: 'Failed to process images' });
|
|
1154
|
-
}
|
|
1155
|
-
});
|
|
1156
|
-
} catch (error) {
|
|
1157
|
-
console.error('Error in image upload endpoint:', error);
|
|
1158
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
1159
|
-
}
|
|
1160
|
-
});
|
|
1067
|
+
// Chat image uploads moved to POST /api/assets/images (server/modules/assets),
|
|
1068
|
+
// which stores them in the global ~/.cloudcli/assets folder.
|
|
1161
1069
|
|
|
1162
1070
|
// Get token usage for a specific session. `projectId` is the DB primary key;
|
|
1163
1071
|
// the Claude branch below resolves it to an absolute path via the DB.
|
|
@@ -1197,62 +1105,6 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate
|
|
|
1197
1105
|
});
|
|
1198
1106
|
}
|
|
1199
1107
|
|
|
1200
|
-
if (provider === 'gemini') {
|
|
1201
|
-
const session = sessionsDb.getSessionById(safeSessionId);
|
|
1202
|
-
const sessionFilePath = session?.jsonl_path;
|
|
1203
|
-
if (!sessionFilePath) {
|
|
1204
|
-
return res.json({
|
|
1205
|
-
used: 0,
|
|
1206
|
-
inputTokens: 0,
|
|
1207
|
-
outputTokens: 0,
|
|
1208
|
-
breakdown: { input: 0, output: 0 },
|
|
1209
|
-
unsupported: true,
|
|
1210
|
-
message: 'Token usage tracking not available for this Gemini session'
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
let fileContent;
|
|
1215
|
-
try {
|
|
1216
|
-
fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
|
|
1217
|
-
} catch (error) {
|
|
1218
|
-
if (error.code === 'ENOENT') {
|
|
1219
|
-
return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
|
|
1220
|
-
}
|
|
1221
|
-
throw error;
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
const lines = fileContent.trim().split('\n');
|
|
1225
|
-
let inputTokens = 0;
|
|
1226
|
-
let outputTokens = 0;
|
|
1227
|
-
let totalTokens = 0;
|
|
1228
|
-
|
|
1229
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1230
|
-
try {
|
|
1231
|
-
const entry = JSON.parse(lines[i]);
|
|
1232
|
-
if (!entry.tokens || typeof entry.tokens !== 'object') {
|
|
1233
|
-
continue;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
inputTokens = Number(entry.tokens.input || 0);
|
|
1237
|
-
outputTokens = Number(entry.tokens.output || 0);
|
|
1238
|
-
totalTokens = Number(entry.tokens.total || inputTokens + outputTokens || 0);
|
|
1239
|
-
break;
|
|
1240
|
-
} catch {
|
|
1241
|
-
continue;
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
return res.json({
|
|
1246
|
-
used: totalTokens,
|
|
1247
|
-
inputTokens,
|
|
1248
|
-
outputTokens,
|
|
1249
|
-
breakdown: {
|
|
1250
|
-
input: inputTokens,
|
|
1251
|
-
output: outputTokens
|
|
1252
|
-
}
|
|
1253
|
-
});
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
1108
|
if (provider === 'opencode') {
|
|
1257
1109
|
const dbPath = getOpenCodeDatabasePath();
|
|
1258
1110
|
if (!fs.existsSync(dbPath)) {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fsSync, { promises as fs } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import mime from 'mime-types';
|
|
5
|
+
import multer from 'multer';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildStoredImageRecords,
|
|
9
|
+
ensureImageAssetsDir,
|
|
10
|
+
isAllowedImageMimeType,
|
|
11
|
+
resolveImageAssetFile,
|
|
12
|
+
} from '@/modules/assets/services/image-assets.service.js';
|
|
13
|
+
|
|
14
|
+
const router = express.Router();
|
|
15
|
+
|
|
16
|
+
// Multer writes uploads straight into the global assets folder; the service
|
|
17
|
+
// owns the folder location and the response record shape.
|
|
18
|
+
const storage = multer.diskStorage({
|
|
19
|
+
destination: (req, file, cb) => {
|
|
20
|
+
ensureImageAssetsDir()
|
|
21
|
+
.then((assetsDir) => cb(null, assetsDir))
|
|
22
|
+
.catch((error) => cb(error as Error, ''));
|
|
23
|
+
},
|
|
24
|
+
filename: (req, file, cb) => {
|
|
25
|
+
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
|
|
26
|
+
const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
27
|
+
cb(null, `${uniqueSuffix}-${sanitizedName}`);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const upload = multer({
|
|
32
|
+
storage,
|
|
33
|
+
fileFilter: (req, file, cb) => {
|
|
34
|
+
if (isAllowedImageMimeType(file.mimetype)) {
|
|
35
|
+
cb(null, true);
|
|
36
|
+
} else {
|
|
37
|
+
cb(new Error('Invalid file type. Only JPEG, PNG, GIF, WebP, and SVG are allowed.'));
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
limits: {
|
|
41
|
+
fileSize: 5 * 1024 * 1024, // 5MB
|
|
42
|
+
files: 5,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Stores chat image attachments in the global `~/.cloudcli/assets` folder and
|
|
48
|
+
* returns their absolute paths for use in provider prompts and chat history.
|
|
49
|
+
*/
|
|
50
|
+
router.post('/images', (req, res) => {
|
|
51
|
+
upload.array('images', 5)(req, res, (err: unknown) => {
|
|
52
|
+
if (err) {
|
|
53
|
+
const message = err instanceof Error ? err.message : 'Upload failed';
|
|
54
|
+
return res.status(400).json({ error: message });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const files = Array.isArray(req.files) ? req.files : [];
|
|
58
|
+
if (files.length === 0) {
|
|
59
|
+
return res.status(400).json({ error: 'No image files provided' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
res.json({ images: buildStoredImageRecords(files) });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Serves one stored image asset by filename. Only files directly inside the
|
|
68
|
+
* global assets folder are reachable; traversal attempts resolve to null.
|
|
69
|
+
*/
|
|
70
|
+
router.get('/images/:filename', async (req, res) => {
|
|
71
|
+
const resolved = resolveImageAssetFile(req.params.filename);
|
|
72
|
+
if (!resolved) {
|
|
73
|
+
return res.status(400).json({ error: 'Invalid asset filename' });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await fs.access(resolved);
|
|
78
|
+
} catch {
|
|
79
|
+
return res.status(404).json({ error: 'Asset not found' });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const contentType = mime.lookup(resolved) || 'application/octet-stream';
|
|
83
|
+
res.setHeader('Content-Type', contentType);
|
|
84
|
+
// Stored-XSS hardening: never let the browser sniff a different type, and
|
|
85
|
+
// force SVGs (which can carry scripts when rendered as a document) to
|
|
86
|
+
// download instead of rendering inline. The chat UI is unaffected — it
|
|
87
|
+
// fetches assets as blobs and shows them through <img>, where SVG scripts
|
|
88
|
+
// never execute.
|
|
89
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
90
|
+
if (contentType === 'image/svg+xml') {
|
|
91
|
+
res.setHeader('Content-Disposition', 'attachment');
|
|
92
|
+
}
|
|
93
|
+
const fileStream = fsSync.createReadStream(resolved);
|
|
94
|
+
fileStream.pipe(res);
|
|
95
|
+
fileStream.on('error', (error) => {
|
|
96
|
+
console.error('Error streaming image asset:', error);
|
|
97
|
+
if (!res.headersSent) {
|
|
98
|
+
res.status(500).json({ error: 'Error reading asset' });
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export default router;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { getGlobalImageAssetsDir, toPosixPath } from '@/shared/image-attachments.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Image mime types accepted for chat attachment uploads. SVG is allowed for
|
|
8
|
+
* storage/preview even though some providers (Claude API) skip it at send time.
|
|
9
|
+
*/
|
|
10
|
+
const ALLOWED_IMAGE_MIME_TYPES = new Set([
|
|
11
|
+
'image/jpeg',
|
|
12
|
+
'image/png',
|
|
13
|
+
'image/gif',
|
|
14
|
+
'image/webp',
|
|
15
|
+
'image/svg+xml',
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
// Used only by this service and the assets routes via the barrel file.
|
|
19
|
+
type StoredImageAsset = {
|
|
20
|
+
/** Original upload filename, for display. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Absolute posix-normalized path inside the global assets folder. */
|
|
23
|
+
path: string;
|
|
24
|
+
size: number;
|
|
25
|
+
mimeType: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Shape of one multer-stored file; kept local because only this module reads it.
|
|
29
|
+
type UploadedImageFile = {
|
|
30
|
+
originalname: string;
|
|
31
|
+
filename: string;
|
|
32
|
+
size: number;
|
|
33
|
+
mimetype: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Returns whether one uploaded mime type may be stored as a chat image asset. */
|
|
37
|
+
export function isAllowedImageMimeType(mimeType: string): boolean {
|
|
38
|
+
return ALLOWED_IMAGE_MIME_TYPES.has(mimeType);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Creates the global `~/.cloudcli/assets` folder if needed and returns it. */
|
|
42
|
+
export async function ensureImageAssetsDir(): Promise<string> {
|
|
43
|
+
const assetsDir = getGlobalImageAssetsDir();
|
|
44
|
+
await fs.mkdir(assetsDir, { recursive: true });
|
|
45
|
+
return assetsDir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Maps multer-stored upload files to the attachment records returned to the
|
|
50
|
+
* chat composer. The absolute path is what providers receive and what session
|
|
51
|
+
* history carries back to the UI.
|
|
52
|
+
*/
|
|
53
|
+
export function buildStoredImageRecords(files: UploadedImageFile[]): StoredImageAsset[] {
|
|
54
|
+
const assetsDir = getGlobalImageAssetsDir();
|
|
55
|
+
return files.map((file) => ({
|
|
56
|
+
name: file.originalname,
|
|
57
|
+
path: toPosixPath(path.join(assetsDir, file.filename)),
|
|
58
|
+
size: file.size,
|
|
59
|
+
mimeType: file.mimetype,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolves one asset filename to its absolute path inside the global assets
|
|
65
|
+
* folder, or null when the name is empty, contains path separators/traversal,
|
|
66
|
+
* or would escape the folder. This is the only lookup the serving route uses,
|
|
67
|
+
* so nothing outside `~/.cloudcli/assets` can ever be read through it.
|
|
68
|
+
*/
|
|
69
|
+
export function resolveImageAssetFile(filename: string): string | null {
|
|
70
|
+
const trimmed = typeof filename === 'string' ? filename.trim() : '';
|
|
71
|
+
if (!trimmed || trimmed.includes('/') || trimmed.includes('\\') || trimmed.includes('..')) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const assetsDir = path.resolve(getGlobalImageAssetsDir());
|
|
76
|
+
const resolved = path.resolve(assetsDir, trimmed);
|
|
77
|
+
if (!resolved.startsWith(assetsDir + path.sep)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return resolved;
|
|
82
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
buildStoredImageRecords,
|
|
8
|
+
isAllowedImageMimeType,
|
|
9
|
+
resolveImageAssetFile,
|
|
10
|
+
} from '@/modules/assets/services/image-assets.service.js';
|
|
11
|
+
|
|
12
|
+
const ASSETS_DIR = path.join(os.homedir(), '.cloudcli', 'assets');
|
|
13
|
+
|
|
14
|
+
test('isAllowedImageMimeType accepts image formats and rejects the rest', () => {
|
|
15
|
+
assert.equal(isAllowedImageMimeType('image/png'), true);
|
|
16
|
+
assert.equal(isAllowedImageMimeType('image/svg+xml'), true);
|
|
17
|
+
assert.equal(isAllowedImageMimeType('application/pdf'), false);
|
|
18
|
+
assert.equal(isAllowedImageMimeType('text/html'), false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('buildStoredImageRecords returns absolute posix paths in the assets dir', () => {
|
|
22
|
+
const records = buildStoredImageRecords([
|
|
23
|
+
{ originalname: 'shot.png', filename: '123-456-shot.png', size: 42, mimetype: 'image/png' },
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
assert.equal(records.length, 1);
|
|
27
|
+
assert.equal(records[0].name, 'shot.png');
|
|
28
|
+
assert.equal(records[0].size, 42);
|
|
29
|
+
assert.equal(records[0].mimeType, 'image/png');
|
|
30
|
+
assert.equal(records[0].path, `${ASSETS_DIR.replace(/\\/g, '/')}/123-456-shot.png`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('resolveImageAssetFile resolves plain filenames inside the assets dir', () => {
|
|
34
|
+
const resolved = resolveImageAssetFile('123-shot.png');
|
|
35
|
+
assert.equal(resolved, path.join(path.resolve(ASSETS_DIR), '123-shot.png'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('resolveImageAssetFile rejects traversal and separator attempts', () => {
|
|
39
|
+
assert.equal(resolveImageAssetFile(''), null);
|
|
40
|
+
assert.equal(resolveImageAssetFile(' '), null);
|
|
41
|
+
assert.equal(resolveImageAssetFile('../auth.db'), null);
|
|
42
|
+
assert.equal(resolveImageAssetFile('..'), null);
|
|
43
|
+
assert.equal(resolveImageAssetFile('sub/dir.png'), null);
|
|
44
|
+
assert.equal(resolveImageAssetFile('sub\\dir.png'), null);
|
|
45
|
+
assert.equal(resolveImageAssetFile('a..b/../c.png'), null);
|
|
46
|
+
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createRequire } from 'node:module';
|
|
2
2
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
3
|
import fs from 'node:fs';
|
|
5
4
|
import os from 'node:os';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
|
|
7
|
+
// cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution.
|
|
8
|
+
import spawn from 'cross-spawn';
|
|
9
|
+
|
|
8
10
|
import { appConfigDb } from '@/modules/database/index.js';
|
|
9
11
|
import { providerMcpService } from '@/modules/providers/index.js';
|
|
10
12
|
import { getModuleDir } from '@/utils/runtime-paths.js';
|
|
@@ -270,8 +272,10 @@ function runCommand(command: string, args: string[]): Promise<void> {
|
|
|
270
272
|
}, INSTALL_COMMAND_TIMEOUT_MS);
|
|
271
273
|
timer.unref?.();
|
|
272
274
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
// stdio config above guarantees the pipes exist; cross-spawn's types
|
|
276
|
+
// just don't narrow them the way node's spawn overloads do.
|
|
277
|
+
child.stdout?.on('data', (chunk) => output.push(String(chunk)));
|
|
278
|
+
child.stderr?.on('data', (chunk) => output.push(String(chunk)));
|
|
275
279
|
child.on('error', (error) => finish(() => reject(error)));
|
|
276
280
|
child.on('close', (code) => finish(() => {
|
|
277
281
|
if (code === 0) {
|
|
@@ -100,9 +100,9 @@ test('assignProviderSessionId merges a watcher-created duplicate into the app ro
|
|
|
100
100
|
|
|
101
101
|
test('legacy provider-keyed rows stay resolvable through both lookups', async () => {
|
|
102
102
|
await withIsolatedDatabase(() => {
|
|
103
|
-
sessionsDb.createSession('legacy-1', '
|
|
103
|
+
sessionsDb.createSession('legacy-1', 'opencode', '/workspace/demo');
|
|
104
104
|
|
|
105
|
-
assert.equal(sessionsDb.getSessionById('legacy-1')?.provider, '
|
|
105
|
+
assert.equal(sessionsDb.getSessionById('legacy-1')?.provider, 'opencode');
|
|
106
106
|
assert.equal(sessionsDb.getSessionByProviderSessionId('legacy-1')?.session_id, 'legacy-1');
|
|
107
107
|
});
|
|
108
108
|
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
1
|
import { access, mkdir, rm } from 'node:fs/promises';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
|
|
4
|
+
// cross-spawn: drop-in spawn with Windows .cmd/PATHEXT resolution.
|
|
5
|
+
import spawn from 'cross-spawn';
|
|
6
|
+
|
|
5
7
|
import { githubTokensDb } from '@/modules/database/index.js';
|
|
6
8
|
import { createProject } from '@/modules/projects/services/project-management.service.js';
|
|
7
9
|
import type { WorkspacePathValidationResult } from '@/shared/types.js';
|
|
@@ -36,7 +36,6 @@ Current provider ids in this repo are:
|
|
|
36
36
|
- `claude`
|
|
37
37
|
- `codex`
|
|
38
38
|
- `cursor`
|
|
39
|
-
- `gemini`
|
|
40
39
|
- `opencode`
|
|
41
40
|
|
|
42
41
|
Those ids are mirrored in backend unions and frontend provider constants. If
|
|
@@ -56,8 +55,7 @@ server/modules/providers/list/<provider>/
|
|
|
56
55
|
<provider>-session-synchronizer.provider.ts
|
|
57
56
|
```
|
|
58
57
|
|
|
59
|
-
The existing provider folders are `claude`, `codex`, `cursor`, `
|
|
60
|
-
`opencode`.
|
|
58
|
+
The existing provider folders are `claude`, `codex`, `cursor`, and `opencode`.
|
|
61
59
|
|
|
62
60
|
## What Each Facet Does
|
|
63
61
|
|
|
@@ -123,7 +121,6 @@ Current MCP formats in this repo are:
|
|
|
123
121
|
| Claude | `.mcp.json` in user / local / project locations | `user`, `local`, `project` | `stdio`, `http`, `sse` |
|
|
124
122
|
| Codex | `.codex/config.toml` | `user`, `project` | `stdio`, `http` |
|
|
125
123
|
| Cursor | `.cursor/mcp.json` | `user`, `project` | `stdio`, `http` |
|
|
126
|
-
| Gemini | `.gemini/settings.json` | `user`, `project` | `stdio`, `http` |
|
|
127
124
|
| OpenCode | `~/.config/opencode/opencode.json` or `<workspace>/opencode.json` (`.jsonc` is read when present) | `user`, `project` | `stdio`, `http` |
|
|
128
125
|
|
|
129
126
|
5. Implement skills.
|
|
@@ -144,7 +141,6 @@ Current skill discovery roots are:
|
|
|
144
141
|
| Claude | `~/.claude/skills` | `<workspace>/.claude/skills` | `/` | Also discovers Claude plugin skills from enabled plugin installs. Command skills live under `commands/`; markdown skills live under `skills/` and are scanned recursively. |
|
|
145
142
|
| Codex | `~/.agents/skills`, `~/.codex/skills/.system`, `/etc/codex/skills` | `<workspace>/.agents/skills`, `path.dirname(workspacePath)/.agents/skills`, topmost git root `.agents/skills` | `$` | Overlapping roots are deduplicated before scanning. |
|
|
146
143
|
| Cursor | `~/.cursor/skills` | `<workspace>/.cursor/skills`, `<workspace>/.agents/skills` | `/` | Uses slash-style commands. |
|
|
147
|
-
| Gemini | `~/.gemini/skills`, `~/.agents/skills` | `<workspace>/.gemini/skills`, `<workspace>/.agents/skills` | `/` | Uses slash-style commands. |
|
|
148
144
|
| OpenCode | `~/.config/opencode/skills`, `~/.claude/skills`, `~/.agents/skills` | Cwd-to-topmost-git-root `.opencode/skills`, `.claude/skills`, and `.agents/skills` | `/` | Reuses OpenCode, Claude, and Agents skill locations. Overlapping roots are deduplicated before scanning. |
|
|
149
145
|
|
|
150
146
|
Command forms currently used by the providers are:
|
|
@@ -153,7 +149,6 @@ Command forms currently used by the providers are:
|
|
|
153
149
|
- Claude plugin skills: `/plugin-name:skill-name`
|
|
154
150
|
- Codex skills: `$skill-name`
|
|
155
151
|
- Cursor skills: `/skill-name`
|
|
156
|
-
- Gemini skills: `/skill-name`
|
|
157
152
|
- OpenCode skills: `/skill-name`
|
|
158
153
|
|
|
159
154
|
6. Implement sessions.
|
|
@@ -191,7 +186,6 @@ Current session sync roots are:
|
|
|
191
186
|
| Claude | `~/.claude/projects/**/*.jsonl` | Uses `~/.claude/history.jsonl` for name lookup and the trailing `ai-title`, `last-prompt`, or `custom-title` entries for title recovery. |
|
|
192
187
|
| Codex | `~/.codex/sessions/**/*.jsonl` | Uses `~/.codex/session_index.jsonl` for title lookup and the last `task_complete` message for a fallback title. |
|
|
193
188
|
| Cursor | `~/.cursor/projects/**/*.jsonl` | Uses sibling `worker.log` to recover `workspacePath`, then derives the session title from the first user prompt. |
|
|
194
|
-
| Gemini | `~/.gemini/tmp/**/*.jsonl` | Current full scans only index temp JSONL chat artifacts. Single-file sync also accepts legacy `.json` files. |
|
|
195
189
|
| OpenCode | `~/.local/share/opencode/opencode.db` | Reads active sessions/messages/parts from OpenCode's shared SQLite database and stores `jsonl_path` as `null` so deleting one app session cannot remove the shared DB. |
|
|
196
190
|
|
|
197
191
|
8. Register the provider.
|