@siteboon/claude-code-ui 1.25.2 → 1.26.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.
Files changed (43) hide show
  1. package/README.de.md +239 -0
  2. package/README.ja.md +115 -230
  3. package/README.ko.md +116 -231
  4. package/README.md +2 -1
  5. package/README.ru.md +75 -54
  6. package/README.zh-CN.md +121 -238
  7. package/dist/assets/index-C08k8QbP.css +32 -0
  8. package/dist/assets/{index-DF_FFT3b.js → index-DnXcHp5q.js} +249 -242
  9. package/dist/index.html +2 -2
  10. package/dist/sw.js +59 -3
  11. package/package.json +3 -2
  12. package/server/claude-sdk.js +106 -62
  13. package/server/cli.js +10 -7
  14. package/server/cursor-cli.js +59 -73
  15. package/server/database/db.js +142 -1
  16. package/server/database/init.sql +28 -1
  17. package/server/gemini-cli.js +46 -48
  18. package/server/gemini-response-handler.js +12 -73
  19. package/server/index.js +82 -55
  20. package/server/middleware/auth.js +2 -2
  21. package/server/openai-codex.js +43 -28
  22. package/server/projects.js +1 -1
  23. package/server/providers/claude/adapter.js +278 -0
  24. package/server/providers/codex/adapter.js +248 -0
  25. package/server/providers/cursor/adapter.js +353 -0
  26. package/server/providers/gemini/adapter.js +186 -0
  27. package/server/providers/registry.js +44 -0
  28. package/server/providers/types.js +119 -0
  29. package/server/providers/utils.js +29 -0
  30. package/server/routes/agent.js +7 -5
  31. package/server/routes/cli-auth.js +38 -0
  32. package/server/routes/codex.js +1 -19
  33. package/server/routes/gemini.js +0 -30
  34. package/server/routes/git.js +48 -20
  35. package/server/routes/messages.js +61 -0
  36. package/server/routes/plugins.js +5 -1
  37. package/server/routes/settings.js +99 -1
  38. package/server/routes/taskmaster.js +2 -2
  39. package/server/services/notification-orchestrator.js +227 -0
  40. package/server/services/vapid-keys.js +35 -0
  41. package/server/utils/plugin-loader.js +53 -4
  42. package/shared/networkHosts.js +22 -0
  43. package/dist/assets/index-WNTmA_ug.css +0 -32
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Cursor provider adapter.
3
+ *
4
+ * Normalizes Cursor CLI session history into NormalizedMessage format.
5
+ * @module adapters/cursor
6
+ */
7
+
8
+ import path from 'path';
9
+ import os from 'os';
10
+ import crypto from 'crypto';
11
+ import { createNormalizedMessage, generateMessageId } from '../types.js';
12
+
13
+ const PROVIDER = 'cursor';
14
+
15
+ /**
16
+ * Load raw blobs from Cursor's SQLite store.db, parse the DAG structure,
17
+ * and return sorted message blobs in chronological order.
18
+ * @param {string} sessionId
19
+ * @param {string} projectPath - Absolute project path (used to compute cwdId hash)
20
+ * @returns {Promise<Array<{id: string, sequence: number, rowid: number, content: object}>>}
21
+ */
22
+ async function loadCursorBlobs(sessionId, projectPath) {
23
+ // Lazy-import sqlite so the module doesn't fail if sqlite3 is unavailable
24
+ const { default: sqlite3 } = await import('sqlite3');
25
+ const { open } = await import('sqlite');
26
+
27
+ const cwdId = crypto.createHash('md5').update(projectPath || process.cwd()).digest('hex');
28
+ const storeDbPath = path.join(os.homedir(), '.cursor', 'chats', cwdId, sessionId, 'store.db');
29
+
30
+ const db = await open({
31
+ filename: storeDbPath,
32
+ driver: sqlite3.Database,
33
+ mode: sqlite3.OPEN_READONLY,
34
+ });
35
+
36
+ try {
37
+ const allBlobs = await db.all('SELECT rowid, id, data FROM blobs');
38
+
39
+ const blobMap = new Map();
40
+ const parentRefs = new Map();
41
+ const childRefs = new Map();
42
+ const jsonBlobs = [];
43
+
44
+ for (const blob of allBlobs) {
45
+ blobMap.set(blob.id, blob);
46
+
47
+ if (blob.data && blob.data[0] === 0x7B) {
48
+ try {
49
+ const parsed = JSON.parse(blob.data.toString('utf8'));
50
+ jsonBlobs.push({ ...blob, parsed });
51
+ } catch {
52
+ // skip unparseable blobs
53
+ }
54
+ } else if (blob.data) {
55
+ const parents = [];
56
+ let i = 0;
57
+ while (i < blob.data.length - 33) {
58
+ if (blob.data[i] === 0x0A && blob.data[i + 1] === 0x20) {
59
+ const parentHash = blob.data.slice(i + 2, i + 34).toString('hex');
60
+ if (blobMap.has(parentHash)) {
61
+ parents.push(parentHash);
62
+ }
63
+ i += 34;
64
+ } else {
65
+ i++;
66
+ }
67
+ }
68
+ if (parents.length > 0) {
69
+ parentRefs.set(blob.id, parents);
70
+ for (const parentId of parents) {
71
+ if (!childRefs.has(parentId)) childRefs.set(parentId, []);
72
+ childRefs.get(parentId).push(blob.id);
73
+ }
74
+ }
75
+ }
76
+ }
77
+
78
+ // Topological sort (DFS)
79
+ const visited = new Set();
80
+ const sorted = [];
81
+ function visit(nodeId) {
82
+ if (visited.has(nodeId)) return;
83
+ visited.add(nodeId);
84
+ for (const pid of (parentRefs.get(nodeId) || [])) visit(pid);
85
+ const b = blobMap.get(nodeId);
86
+ if (b) sorted.push(b);
87
+ }
88
+ for (const blob of allBlobs) {
89
+ if (!parentRefs.has(blob.id)) visit(blob.id);
90
+ }
91
+ for (const blob of allBlobs) visit(blob.id);
92
+
93
+ // Order JSON blobs by DAG appearance
94
+ const messageOrder = new Map();
95
+ let orderIndex = 0;
96
+ for (const blob of sorted) {
97
+ if (blob.data && blob.data[0] !== 0x7B) {
98
+ for (const jb of jsonBlobs) {
99
+ try {
100
+ const idBytes = Buffer.from(jb.id, 'hex');
101
+ if (blob.data.includes(idBytes) && !messageOrder.has(jb.id)) {
102
+ messageOrder.set(jb.id, orderIndex++);
103
+ }
104
+ } catch { /* skip */ }
105
+ }
106
+ }
107
+ }
108
+
109
+ const sortedJsonBlobs = jsonBlobs.sort((a, b) => {
110
+ const oa = messageOrder.get(a.id) ?? Number.MAX_SAFE_INTEGER;
111
+ const ob = messageOrder.get(b.id) ?? Number.MAX_SAFE_INTEGER;
112
+ return oa !== ob ? oa - ob : a.rowid - b.rowid;
113
+ });
114
+
115
+ const messages = [];
116
+ for (let idx = 0; idx < sortedJsonBlobs.length; idx++) {
117
+ const blob = sortedJsonBlobs[idx];
118
+ const parsed = blob.parsed;
119
+ if (!parsed) continue;
120
+ const role = parsed?.role || parsed?.message?.role;
121
+ if (role === 'system') continue;
122
+ messages.push({
123
+ id: blob.id,
124
+ sequence: idx + 1,
125
+ rowid: blob.rowid,
126
+ content: parsed,
127
+ });
128
+ }
129
+
130
+ return messages;
131
+ } finally {
132
+ await db.close();
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Normalize a realtime NDJSON event from Cursor CLI into NormalizedMessage(s).
138
+ * History uses normalizeCursorBlobs (SQLite DAG), this handles streaming NDJSON.
139
+ * @param {object|string} raw - A parsed NDJSON event or a raw text line
140
+ * @param {string} sessionId
141
+ * @returns {import('../types.js').NormalizedMessage[]}
142
+ */
143
+ export function normalizeMessage(raw, sessionId) {
144
+ // Structured assistant message with content array
145
+ if (raw && typeof raw === 'object' && raw.type === 'assistant' && raw.message?.content?.[0]?.text) {
146
+ return [createNormalizedMessage({ kind: 'stream_delta', content: raw.message.content[0].text, sessionId, provider: PROVIDER })];
147
+ }
148
+ // Plain string line (non-JSON output)
149
+ if (typeof raw === 'string' && raw.trim()) {
150
+ return [createNormalizedMessage({ kind: 'stream_delta', content: raw, sessionId, provider: PROVIDER })];
151
+ }
152
+ return [];
153
+ }
154
+
155
+ /**
156
+ * @type {import('../types.js').ProviderAdapter}
157
+ */
158
+ export const cursorAdapter = {
159
+ normalizeMessage,
160
+ /**
161
+ * Fetch session history for Cursor from SQLite store.db.
162
+ */
163
+ async fetchHistory(sessionId, opts = {}) {
164
+ const { projectPath = '', limit = null, offset = 0 } = opts;
165
+
166
+ try {
167
+ const blobs = await loadCursorBlobs(sessionId, projectPath);
168
+ const allNormalized = cursorAdapter.normalizeCursorBlobs(blobs, sessionId);
169
+
170
+ // Apply pagination
171
+ if (limit !== null && limit > 0) {
172
+ const start = offset;
173
+ const page = allNormalized.slice(start, start + limit);
174
+ return {
175
+ messages: page,
176
+ total: allNormalized.length,
177
+ hasMore: start + limit < allNormalized.length,
178
+ offset,
179
+ limit,
180
+ };
181
+ }
182
+
183
+ return {
184
+ messages: allNormalized,
185
+ total: allNormalized.length,
186
+ hasMore: false,
187
+ offset: 0,
188
+ limit: null,
189
+ };
190
+ } catch (error) {
191
+ // DB doesn't exist or is unreadable — return empty
192
+ console.warn(`[CursorAdapter] Failed to load session ${sessionId}:`, error.message);
193
+ return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
194
+ }
195
+ },
196
+
197
+ /**
198
+ * Normalize raw Cursor blob messages into NormalizedMessage[].
199
+ * @param {any[]} blobs - Raw cursor blobs from store.db ({id, sequence, rowid, content})
200
+ * @param {string} sessionId
201
+ * @returns {import('../types.js').NormalizedMessage[]}
202
+ */
203
+ normalizeCursorBlobs(blobs, sessionId) {
204
+ const messages = [];
205
+ const toolUseMap = new Map();
206
+
207
+ // Use a fixed base timestamp so messages have stable, monotonically-increasing
208
+ // timestamps based on their sequence number rather than wall-clock time.
209
+ const baseTime = Date.now();
210
+
211
+ for (let i = 0; i < blobs.length; i++) {
212
+ const blob = blobs[i];
213
+ const content = blob.content;
214
+ const ts = new Date(baseTime + (blob.sequence ?? i) * 100).toISOString();
215
+ const baseId = blob.id || generateMessageId('cursor');
216
+
217
+ try {
218
+ if (!content?.role || !content?.content) {
219
+ // Try nested message format
220
+ if (content?.message?.role && content?.message?.content) {
221
+ if (content.message.role === 'system') continue;
222
+ const role = content.message.role === 'user' ? 'user' : 'assistant';
223
+ let text = '';
224
+ if (Array.isArray(content.message.content)) {
225
+ text = content.message.content
226
+ .map(p => typeof p === 'string' ? p : p?.text || '')
227
+ .filter(Boolean)
228
+ .join('\n');
229
+ } else if (typeof content.message.content === 'string') {
230
+ text = content.message.content;
231
+ }
232
+ if (text?.trim()) {
233
+ messages.push(createNormalizedMessage({
234
+ id: baseId,
235
+ sessionId,
236
+ timestamp: ts,
237
+ provider: PROVIDER,
238
+ kind: 'text',
239
+ role,
240
+ content: text,
241
+ sequence: blob.sequence,
242
+ rowid: blob.rowid,
243
+ }));
244
+ }
245
+ }
246
+ continue;
247
+ }
248
+
249
+ if (content.role === 'system') continue;
250
+
251
+ // Tool results
252
+ if (content.role === 'tool') {
253
+ const toolItems = Array.isArray(content.content) ? content.content : [];
254
+ for (const item of toolItems) {
255
+ if (item?.type !== 'tool-result') continue;
256
+ const toolCallId = item.toolCallId || content.id;
257
+ messages.push(createNormalizedMessage({
258
+ id: `${baseId}_tr`,
259
+ sessionId,
260
+ timestamp: ts,
261
+ provider: PROVIDER,
262
+ kind: 'tool_result',
263
+ toolId: toolCallId,
264
+ content: item.result || '',
265
+ isError: false,
266
+ }));
267
+ }
268
+ continue;
269
+ }
270
+
271
+ const role = content.role === 'user' ? 'user' : 'assistant';
272
+
273
+ if (Array.isArray(content.content)) {
274
+ for (let partIdx = 0; partIdx < content.content.length; partIdx++) {
275
+ const part = content.content[partIdx];
276
+
277
+ if (part?.type === 'text' && part?.text) {
278
+ messages.push(createNormalizedMessage({
279
+ id: `${baseId}_${partIdx}`,
280
+ sessionId,
281
+ timestamp: ts,
282
+ provider: PROVIDER,
283
+ kind: 'text',
284
+ role,
285
+ content: part.text,
286
+ sequence: blob.sequence,
287
+ rowid: blob.rowid,
288
+ }));
289
+ } else if (part?.type === 'reasoning' && part?.text) {
290
+ messages.push(createNormalizedMessage({
291
+ id: `${baseId}_${partIdx}`,
292
+ sessionId,
293
+ timestamp: ts,
294
+ provider: PROVIDER,
295
+ kind: 'thinking',
296
+ content: part.text,
297
+ }));
298
+ } else if (part?.type === 'tool-call' || part?.type === 'tool_use') {
299
+ const toolName = (part.toolName || part.name || 'Unknown Tool') === 'ApplyPatch'
300
+ ? 'Edit' : (part.toolName || part.name || 'Unknown Tool');
301
+ const toolId = part.toolCallId || part.id || `tool_${i}_${partIdx}`;
302
+ messages.push(createNormalizedMessage({
303
+ id: `${baseId}_${partIdx}`,
304
+ sessionId,
305
+ timestamp: ts,
306
+ provider: PROVIDER,
307
+ kind: 'tool_use',
308
+ toolName,
309
+ toolInput: part.args || part.input,
310
+ toolId,
311
+ }));
312
+ toolUseMap.set(toolId, messages[messages.length - 1]);
313
+ }
314
+ }
315
+ } else if (typeof content.content === 'string' && content.content.trim()) {
316
+ messages.push(createNormalizedMessage({
317
+ id: baseId,
318
+ sessionId,
319
+ timestamp: ts,
320
+ provider: PROVIDER,
321
+ kind: 'text',
322
+ role,
323
+ content: content.content,
324
+ sequence: blob.sequence,
325
+ rowid: blob.rowid,
326
+ }));
327
+ }
328
+ } catch (error) {
329
+ console.warn('Error normalizing cursor blob:', error);
330
+ }
331
+ }
332
+
333
+ // Attach tool results to tool_use messages
334
+ for (const msg of messages) {
335
+ if (msg.kind === 'tool_result' && msg.toolId && toolUseMap.has(msg.toolId)) {
336
+ const toolUse = toolUseMap.get(msg.toolId);
337
+ toolUse.toolResult = {
338
+ content: msg.content,
339
+ isError: msg.isError,
340
+ };
341
+ }
342
+ }
343
+
344
+ // Sort by sequence/rowid
345
+ messages.sort((a, b) => {
346
+ if (a.sequence !== undefined && b.sequence !== undefined) return a.sequence - b.sequence;
347
+ if (a.rowid !== undefined && b.rowid !== undefined) return a.rowid - b.rowid;
348
+ return new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime();
349
+ });
350
+
351
+ return messages;
352
+ },
353
+ };
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Gemini provider adapter.
3
+ *
4
+ * Normalizes Gemini CLI session history into NormalizedMessage format.
5
+ * @module adapters/gemini
6
+ */
7
+
8
+ import sessionManager from '../../sessionManager.js';
9
+ import { getGeminiCliSessionMessages } from '../../projects.js';
10
+ import { createNormalizedMessage, generateMessageId } from '../types.js';
11
+
12
+ const PROVIDER = 'gemini';
13
+
14
+ /**
15
+ * Normalize a realtime NDJSON event from Gemini CLI into NormalizedMessage(s).
16
+ * Handles: message (delta/final), tool_use, tool_result, result, error.
17
+ * @param {object} raw - A parsed NDJSON event
18
+ * @param {string} sessionId
19
+ * @returns {import('../types.js').NormalizedMessage[]}
20
+ */
21
+ export function normalizeMessage(raw, sessionId) {
22
+ const ts = raw.timestamp || new Date().toISOString();
23
+ const baseId = raw.uuid || generateMessageId('gemini');
24
+
25
+ if (raw.type === 'message' && raw.role === 'assistant') {
26
+ const content = raw.content || '';
27
+ const msgs = [];
28
+ if (content) {
29
+ msgs.push(createNormalizedMessage({ id: baseId, sessionId, timestamp: ts, provider: PROVIDER, kind: 'stream_delta', content }));
30
+ }
31
+ // If not a delta, also send stream_end
32
+ if (raw.delta !== true) {
33
+ msgs.push(createNormalizedMessage({ sessionId, timestamp: ts, provider: PROVIDER, kind: 'stream_end' }));
34
+ }
35
+ return msgs;
36
+ }
37
+
38
+ if (raw.type === 'tool_use') {
39
+ return [createNormalizedMessage({
40
+ id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
41
+ kind: 'tool_use', toolName: raw.tool_name, toolInput: raw.parameters || {},
42
+ toolId: raw.tool_id || baseId,
43
+ })];
44
+ }
45
+
46
+ if (raw.type === 'tool_result') {
47
+ return [createNormalizedMessage({
48
+ id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
49
+ kind: 'tool_result', toolId: raw.tool_id || '',
50
+ content: raw.output === undefined ? '' : String(raw.output),
51
+ isError: raw.status === 'error',
52
+ })];
53
+ }
54
+
55
+ if (raw.type === 'result') {
56
+ const msgs = [createNormalizedMessage({ sessionId, timestamp: ts, provider: PROVIDER, kind: 'stream_end' })];
57
+ if (raw.stats?.total_tokens) {
58
+ msgs.push(createNormalizedMessage({
59
+ sessionId, timestamp: ts, provider: PROVIDER,
60
+ kind: 'status', text: 'Complete', tokens: raw.stats.total_tokens, canInterrupt: false,
61
+ }));
62
+ }
63
+ return msgs;
64
+ }
65
+
66
+ if (raw.type === 'error') {
67
+ return [createNormalizedMessage({
68
+ id: baseId, sessionId, timestamp: ts, provider: PROVIDER,
69
+ kind: 'error', content: raw.error || raw.message || 'Unknown Gemini streaming error',
70
+ })];
71
+ }
72
+
73
+ return [];
74
+ }
75
+
76
+ /**
77
+ * @type {import('../types.js').ProviderAdapter}
78
+ */
79
+ export const geminiAdapter = {
80
+ normalizeMessage,
81
+ /**
82
+ * Fetch session history for Gemini.
83
+ * First tries in-memory session manager, then falls back to CLI sessions on disk.
84
+ */
85
+ async fetchHistory(sessionId, opts = {}) {
86
+ let rawMessages;
87
+ try {
88
+ rawMessages = sessionManager.getSessionMessages(sessionId);
89
+
90
+ // Fallback to Gemini CLI sessions on disk
91
+ if (rawMessages.length === 0) {
92
+ rawMessages = await getGeminiCliSessionMessages(sessionId);
93
+ }
94
+ } catch (error) {
95
+ console.warn(`[GeminiAdapter] Failed to load session ${sessionId}:`, error.message);
96
+ return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
97
+ }
98
+
99
+ const normalized = [];
100
+ for (let i = 0; i < rawMessages.length; i++) {
101
+ const raw = rawMessages[i];
102
+ const ts = raw.timestamp || new Date().toISOString();
103
+ const baseId = raw.uuid || generateMessageId('gemini');
104
+
105
+ // sessionManager format: { type: 'message', message: { role, content }, timestamp }
106
+ // CLI format: { role: 'user'|'gemini'|'assistant', content: string|array }
107
+ const role = raw.message?.role || raw.role;
108
+ const content = raw.message?.content || raw.content;
109
+
110
+ if (!role || !content) continue;
111
+
112
+ const normalizedRole = (role === 'user') ? 'user' : 'assistant';
113
+
114
+ if (Array.isArray(content)) {
115
+ for (let partIdx = 0; partIdx < content.length; partIdx++) {
116
+ const part = content[partIdx];
117
+ if (part.type === 'text' && part.text) {
118
+ normalized.push(createNormalizedMessage({
119
+ id: `${baseId}_${partIdx}`,
120
+ sessionId,
121
+ timestamp: ts,
122
+ provider: PROVIDER,
123
+ kind: 'text',
124
+ role: normalizedRole,
125
+ content: part.text,
126
+ }));
127
+ } else if (part.type === 'tool_use') {
128
+ normalized.push(createNormalizedMessage({
129
+ id: `${baseId}_${partIdx}`,
130
+ sessionId,
131
+ timestamp: ts,
132
+ provider: PROVIDER,
133
+ kind: 'tool_use',
134
+ toolName: part.name,
135
+ toolInput: part.input,
136
+ toolId: part.id || generateMessageId('gemini_tool'),
137
+ }));
138
+ } else if (part.type === 'tool_result') {
139
+ normalized.push(createNormalizedMessage({
140
+ id: `${baseId}_${partIdx}`,
141
+ sessionId,
142
+ timestamp: ts,
143
+ provider: PROVIDER,
144
+ kind: 'tool_result',
145
+ toolId: part.tool_use_id || '',
146
+ content: part.content === undefined ? '' : String(part.content),
147
+ isError: Boolean(part.is_error),
148
+ }));
149
+ }
150
+ }
151
+ } else if (typeof content === 'string' && content.trim()) {
152
+ normalized.push(createNormalizedMessage({
153
+ id: baseId,
154
+ sessionId,
155
+ timestamp: ts,
156
+ provider: PROVIDER,
157
+ kind: 'text',
158
+ role: normalizedRole,
159
+ content,
160
+ }));
161
+ }
162
+ }
163
+
164
+ // Attach tool results to tool_use messages
165
+ const toolResultMap = new Map();
166
+ for (const msg of normalized) {
167
+ if (msg.kind === 'tool_result' && msg.toolId) {
168
+ toolResultMap.set(msg.toolId, msg);
169
+ }
170
+ }
171
+ for (const msg of normalized) {
172
+ if (msg.kind === 'tool_use' && msg.toolId && toolResultMap.has(msg.toolId)) {
173
+ const tr = toolResultMap.get(msg.toolId);
174
+ msg.toolResult = { content: tr.content, isError: tr.isError };
175
+ }
176
+ }
177
+
178
+ return {
179
+ messages: normalized,
180
+ total: normalized.length,
181
+ hasMore: false,
182
+ offset: 0,
183
+ limit: null,
184
+ };
185
+ },
186
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Provider Registry
3
+ *
4
+ * Centralizes provider adapter lookup. All code that needs a provider adapter
5
+ * should go through this registry instead of importing individual adapters directly.
6
+ *
7
+ * @module providers/registry
8
+ */
9
+
10
+ import { claudeAdapter } from './claude/adapter.js';
11
+ import { cursorAdapter } from './cursor/adapter.js';
12
+ import { codexAdapter } from './codex/adapter.js';
13
+ import { geminiAdapter } from './gemini/adapter.js';
14
+
15
+ /**
16
+ * @typedef {import('./types.js').ProviderAdapter} ProviderAdapter
17
+ * @typedef {import('./types.js').SessionProvider} SessionProvider
18
+ */
19
+
20
+ /** @type {Map<string, ProviderAdapter>} */
21
+ const providers = new Map();
22
+
23
+ // Register built-in providers
24
+ providers.set('claude', claudeAdapter);
25
+ providers.set('cursor', cursorAdapter);
26
+ providers.set('codex', codexAdapter);
27
+ providers.set('gemini', geminiAdapter);
28
+
29
+ /**
30
+ * Get a provider adapter by name.
31
+ * @param {string} name - Provider name (e.g., 'claude', 'cursor', 'codex', 'gemini')
32
+ * @returns {ProviderAdapter | undefined}
33
+ */
34
+ export function getProvider(name) {
35
+ return providers.get(name);
36
+ }
37
+
38
+ /**
39
+ * Get all registered provider names.
40
+ * @returns {string[]}
41
+ */
42
+ export function getAllProviders() {
43
+ return Array.from(providers.keys());
44
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Provider Types & Interface
3
+ *
4
+ * Defines the normalized message format and the provider adapter interface.
5
+ * All providers normalize their native formats into NormalizedMessage
6
+ * before sending over REST or WebSocket.
7
+ *
8
+ * @module providers/types
9
+ */
10
+
11
+ // ─── Session Provider ────────────────────────────────────────────────────────
12
+
13
+ /**
14
+ * @typedef {'claude' | 'cursor' | 'codex' | 'gemini'} SessionProvider
15
+ */
16
+
17
+ // ─── Message Kind ────────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * @typedef {'text' | 'tool_use' | 'tool_result' | 'thinking' | 'stream_delta' | 'stream_end'
21
+ * | 'error' | 'complete' | 'status' | 'permission_request' | 'permission_cancelled'
22
+ * | 'session_created' | 'interactive_prompt' | 'task_notification'} MessageKind
23
+ */
24
+
25
+ // ─── NormalizedMessage ───────────────────────────────────────────────────────
26
+
27
+ /**
28
+ * @typedef {Object} NormalizedMessage
29
+ * @property {string} id - Unique message id (for dedup between server + realtime)
30
+ * @property {string} sessionId
31
+ * @property {string} timestamp - ISO 8601
32
+ * @property {SessionProvider} provider
33
+ * @property {MessageKind} kind
34
+ *
35
+ * Additional fields depending on kind:
36
+ * - text: role ('user'|'assistant'), content, images?
37
+ * - tool_use: toolName, toolInput, toolId
38
+ * - tool_result: toolId, content, isError
39
+ * - thinking: content
40
+ * - stream_delta: content
41
+ * - stream_end: (no extra fields)
42
+ * - error: content
43
+ * - complete: (no extra fields)
44
+ * - status: text, tokens?, canInterrupt?
45
+ * - permission_request: requestId, toolName, input, context?
46
+ * - permission_cancelled: requestId
47
+ * - session_created: newSessionId
48
+ * - interactive_prompt: content
49
+ * - task_notification: status, summary
50
+ */
51
+
52
+ // ─── Fetch History ───────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * @typedef {Object} FetchHistoryOptions
56
+ * @property {string} [projectName] - Project name (required for Claude)
57
+ * @property {string} [projectPath] - Absolute project path (required for Cursor cwdId hash)
58
+ * @property {number|null} [limit] - Page size (null = all messages)
59
+ * @property {number} [offset] - Pagination offset (default: 0)
60
+ */
61
+
62
+ /**
63
+ * @typedef {Object} FetchHistoryResult
64
+ * @property {NormalizedMessage[]} messages - Normalized messages
65
+ * @property {number} total - Total number of messages in the session
66
+ * @property {boolean} hasMore - Whether more messages exist before the current page
67
+ * @property {number} offset - Current offset
68
+ * @property {number|null} limit - Page size used
69
+ * @property {object} [tokenUsage] - Token usage data (provider-specific)
70
+ */
71
+
72
+ // ─── Provider Adapter Interface ──────────────────────────────────────────────
73
+
74
+ /**
75
+ * Every provider adapter MUST implement this interface.
76
+ *
77
+ * @typedef {Object} ProviderAdapter
78
+ *
79
+ * @property {(sessionId: string, opts?: FetchHistoryOptions) => Promise<FetchHistoryResult>} fetchHistory
80
+ * Read persisted session messages from disk/database and return them as NormalizedMessage[].
81
+ * The backend calls this from the unified GET /api/sessions/:id/messages endpoint.
82
+ *
83
+ * Provider implementations:
84
+ * - Claude: reads ~/.claude/projects/{projectName}/*.jsonl
85
+ * - Cursor: reads from SQLite store.db (via normalizeCursorBlobs helper)
86
+ * - Codex: reads ~/.codex/sessions/*.jsonl
87
+ * - Gemini: reads from in-memory sessionManager or ~/.gemini/tmp/ JSON files
88
+ *
89
+ * @property {(raw: any, sessionId: string) => NormalizedMessage[]} normalizeMessage
90
+ * Normalize a provider-specific event (JSONL entry or live SDK event) into NormalizedMessage[].
91
+ * Used by provider files to convert both history and realtime events.
92
+ */
93
+
94
+ // ─── Runtime Helpers ─────────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Generate a unique message ID.
98
+ * Uses crypto.randomUUID() to avoid collisions across server restarts and workers.
99
+ * @param {string} [prefix='msg'] - Optional prefix
100
+ * @returns {string}
101
+ */
102
+ export function generateMessageId(prefix = 'msg') {
103
+ return `${prefix}_${crypto.randomUUID()}`;
104
+ }
105
+
106
+ /**
107
+ * Create a NormalizedMessage with common fields pre-filled.
108
+ * @param {Partial<NormalizedMessage> & {kind: MessageKind, provider: SessionProvider}} fields
109
+ * @returns {NormalizedMessage}
110
+ */
111
+ export function createNormalizedMessage(fields) {
112
+ return {
113
+ ...fields,
114
+ id: fields.id || generateMessageId(fields.kind),
115
+ sessionId: fields.sessionId || '',
116
+ timestamp: fields.timestamp || new Date().toISOString(),
117
+ provider: fields.provider,
118
+ };
119
+ }