@cloudcli-ai/cloudcli 1.30.0 → 1.31.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.md +1 -1
- package/dist/assets/index-B37ufcoi.js +1262 -0
- package/dist/assets/{index-DI22MZMb.css → index-DF8TTVHl.css} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/server/index.js +115 -955
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/middleware/auth.js +1 -1
- package/dist-server/server/middleware/auth.js.map +1 -1
- package/dist-server/server/modules/database/connection.js +125 -0
- package/dist-server/server/modules/database/connection.js.map +1 -0
- package/dist-server/server/modules/database/index.js +13 -0
- package/dist-server/server/modules/database/index.js.map +1 -0
- package/dist-server/server/modules/database/init-db.js +18 -0
- package/dist-server/server/modules/database/init-db.js.map +1 -0
- package/dist-server/server/modules/database/migrations.js +370 -0
- package/dist-server/server/modules/database/migrations.js.map +1 -0
- package/dist-server/server/modules/database/repositories/api-keys.js +72 -0
- package/dist-server/server/modules/database/repositories/api-keys.js.map +1 -0
- package/dist-server/server/modules/database/repositories/app-config.js +47 -0
- package/dist-server/server/modules/database/repositories/app-config.js.map +1 -0
- package/dist-server/server/modules/database/repositories/credentials.js +68 -0
- package/dist-server/server/modules/database/repositories/credentials.js.map +1 -0
- package/dist-server/server/modules/database/repositories/github-tokens.js +54 -0
- package/dist-server/server/modules/database/repositories/github-tokens.js.map +1 -0
- package/dist-server/server/modules/database/repositories/notification-preferences.js +72 -0
- package/dist-server/server/modules/database/repositories/notification-preferences.js.map +1 -0
- package/dist-server/server/modules/database/repositories/projects.db.integration.test.js +64 -0
- package/dist-server/server/modules/database/repositories/projects.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/database/repositories/projects.db.js +160 -0
- package/dist-server/server/modules/database/repositories/projects.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/push-subscriptions.js +49 -0
- package/dist-server/server/modules/database/repositories/push-subscriptions.js.map +1 -0
- package/dist-server/server/modules/database/repositories/scan-state.db.js +31 -0
- package/dist-server/server/modules/database/repositories/scan-state.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +109 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/users.js +88 -0
- package/dist-server/server/modules/database/repositories/users.js.map +1 -0
- package/dist-server/server/modules/database/repositories/vapid-keys.js +38 -0
- package/dist-server/server/modules/database/repositories/vapid-keys.js.map +1 -0
- package/dist-server/server/modules/database/schema.js +143 -0
- package/dist-server/server/modules/database/schema.js.map +1 -0
- package/dist-server/server/modules/projects/index.js +4 -0
- package/dist-server/server/modules/projects/index.js.map +1 -0
- package/dist-server/server/modules/projects/projects.routes.js +188 -0
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js +219 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-delete.service.js +65 -0
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-management.service.js +93 -0
- package/dist-server/server/modules/projects/services/project-management.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-star.service.js +60 -0
- package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js +167 -0
- package/dist-server/server/modules/projects/services/projects-has-taskmaster.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js +169 -0
- package/dist-server/server/modules/projects/services/projects-with-sessions-fetch.service.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js +126 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-management.service.test.js +85 -0
- package/dist-server/server/modules/projects/tests/project-management.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-star.service.test.js +95 -0
- package/dist-server/server/modules/projects/tests/project-star.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js +87 -0
- package/dist-server/server/modules/projects/tests/projects-has-taskmaster.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/index.js +4 -0
- package/dist-server/server/modules/providers/index.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +63 -0
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +144 -7
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +71 -0
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +225 -13
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js +127 -0
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js +2 -0
- package/dist-server/server/modules/providers/list/cursor/cursor.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js +296 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/gemini/gemini-sessions.provider.js +277 -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.provider.js +2 -0
- package/dist-server/server/modules/providers/list/gemini/gemini.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +172 -0
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +868 -0
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +56 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +234 -0
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/sessions.service.js +78 -3
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/shared/base/abstract.provider.js.map +1 -1
- package/dist-server/server/modules/websocket/index.js +3 -0
- package/dist-server/server/modules/websocket/index.js.map +1 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +188 -0
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js +52 -0
- package/dist-server/server/modules/websocket/services/plugin-websocket-proxy.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +334 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-auth.service.js +32 -0
- package/dist-server/server/modules/websocket/services/websocket-auth.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-server.service.js +36 -0
- package/dist-server/server/modules/websocket/services/websocket-server.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-state.service.js +14 -0
- package/dist-server/server/modules/websocket/services/websocket-state.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/websocket-writer.service.js +32 -0
- package/dist-server/server/modules/websocket/services/websocket-writer.service.js.map +1 -0
- package/dist-server/server/routes/agent.js +10 -17
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/auth.js +3 -1
- package/dist-server/server/routes/auth.js.map +1 -1
- package/dist-server/server/routes/commands.js +1 -1
- package/dist-server/server/routes/commands.js.map +1 -1
- package/dist-server/server/routes/gemini.js +2 -2
- package/dist-server/server/routes/gemini.js.map +1 -1
- package/dist-server/server/routes/git.js +33 -29
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/routes/settings.js +1 -1
- package/dist-server/server/routes/settings.js.map +1 -1
- package/dist-server/server/routes/taskmaster.js +97 -110
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/dist-server/server/routes/user.js +1 -1
- package/dist-server/server/routes/user.js.map +1 -1
- package/dist-server/server/services/notification-orchestrator.js +2 -2
- package/dist-server/server/services/notification-orchestrator.js.map +1 -1
- package/dist-server/server/services/vapid-keys.js +2 -1
- package/dist-server/server/services/vapid-keys.js.map +1 -1
- package/dist-server/server/shared/types.js +0 -1
- package/dist-server/server/shared/types.js.map +1 -1
- package/dist-server/server/shared/utils.js +408 -7
- package/dist-server/server/shared/utils.js.map +1 -1
- package/dist-server/server/utils/taskmaster-websocket.js +18 -12
- package/dist-server/server/utils/taskmaster-websocket.js.map +1 -1
- package/dist-server/shared/modelConstants.js +2 -1
- package/dist-server/shared/modelConstants.js.map +1 -1
- package/package.json +3 -1
- package/server/index.js +146 -1012
- package/server/middleware/auth.js +1 -1
- package/server/modules/database/connection.ts +143 -0
- package/server/modules/database/index.ts +12 -0
- package/server/modules/database/init-db.ts +17 -0
- package/server/modules/database/migrations.ts +442 -0
- package/server/modules/database/repositories/api-keys.ts +119 -0
- package/server/modules/database/repositories/app-config.ts +53 -0
- package/server/modules/database/repositories/credentials.ts +106 -0
- package/server/modules/database/repositories/github-tokens.ts +100 -0
- package/server/modules/database/repositories/notification-preferences.ts +103 -0
- package/server/modules/database/repositories/projects.db.integration.test.ts +72 -0
- package/server/modules/database/repositories/projects.db.ts +183 -0
- package/server/modules/database/repositories/push-subscriptions.ts +80 -0
- package/server/modules/database/repositories/scan-state.db.ts +42 -0
- package/server/modules/database/repositories/sessions.db.ts +174 -0
- package/server/modules/database/repositories/users.ts +140 -0
- package/server/modules/database/repositories/vapid-keys.ts +57 -0
- package/server/modules/database/schema.ts +152 -0
- package/server/modules/projects/index.ts +6 -0
- package/server/modules/projects/projects.routes.ts +247 -0
- package/server/modules/projects/services/project-clone.service.ts +321 -0
- package/server/modules/projects/services/project-delete.service.ts +75 -0
- package/server/modules/projects/services/project-management.service.ts +150 -0
- package/server/modules/projects/services/project-star.service.ts +78 -0
- package/server/modules/projects/services/projects-has-taskmaster.service.ts +248 -0
- package/server/modules/projects/services/projects-with-sessions-fetch.service.ts +285 -0
- package/server/modules/projects/tests/project-clone.service.test.ts +183 -0
- package/server/modules/projects/tests/project-management.service.test.ts +117 -0
- package/server/modules/projects/tests/project-star.service.test.ts +123 -0
- package/server/modules/projects/tests/projects-has-taskmaster.service.test.ts +105 -0
- package/server/modules/providers/index.ts +4 -0
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +110 -0
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +182 -13
- package/server/modules/providers/list/claude/claude.provider.ts +3 -1
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +119 -0
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +262 -17
- package/server/modules/providers/list/codex/codex.provider.ts +3 -1
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +176 -0
- package/server/modules/providers/list/cursor/cursor.provider.ts +3 -1
- package/server/modules/providers/list/gemini/gemini-session-synchronizer.provider.ts +401 -0
- package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +327 -13
- package/server/modules/providers/list/gemini/gemini.provider.ts +3 -1
- package/server/modules/providers/provider.routes.ts +220 -12
- package/server/modules/providers/services/session-conversations-search.service.ts +1167 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +74 -0
- package/server/modules/providers/services/sessions-watcher.service.ts +283 -0
- package/server/modules/providers/services/sessions.service.ts +89 -4
- package/server/modules/providers/shared/base/abstract.provider.ts +8 -1
- package/server/modules/websocket/README.md +267 -0
- package/server/modules/websocket/index.ts +2 -0
- package/server/modules/websocket/services/chat-websocket.service.ts +271 -0
- package/server/modules/websocket/services/plugin-websocket-proxy.service.ts +65 -0
- package/server/modules/websocket/services/shell-websocket.service.ts +453 -0
- package/server/modules/websocket/services/websocket-auth.service.ts +54 -0
- package/server/modules/websocket/services/websocket-server.service.ts +58 -0
- package/server/modules/websocket/services/websocket-state.service.ts +16 -0
- package/server/modules/websocket/services/websocket-writer.service.ts +38 -0
- package/server/routes/agent.js +11 -16
- package/server/routes/auth.js +4 -2
- package/server/routes/commands.js +1 -1
- package/server/routes/gemini.js +3 -2
- package/server/routes/git.js +33 -28
- package/server/routes/settings.js +1 -1
- package/server/routes/taskmaster.js +108 -111
- package/server/routes/user.js +1 -1
- package/server/services/notification-orchestrator.js +3 -2
- package/server/services/vapid-keys.js +2 -1
- package/server/shared/interfaces.ts +39 -1
- package/server/shared/types.ts +185 -30
- package/server/shared/utils.ts +485 -9
- package/server/utils/taskmaster-websocket.js +19 -13
- package/shared/modelConstants.js +2 -1
- package/dist/assets/index-RX-W_g_Q.js +0 -1262
- package/dist-server/server/database/db.js +0 -535
- package/dist-server/server/database/db.js.map +0 -1
- package/dist-server/server/database/schema.js +0 -97
- package/dist-server/server/database/schema.js.map +0 -1
- package/dist-server/server/projects.js +0 -2325
- package/dist-server/server/projects.js.map +0 -1
- package/dist-server/server/routes/codex.js +0 -18
- package/dist-server/server/routes/codex.js.map +0 -1
- package/dist-server/server/routes/messages.js +0 -56
- package/dist-server/server/routes/messages.js.map +0 -1
- package/dist-server/server/routes/projects.js +0 -505
- package/dist-server/server/routes/projects.js.map +0 -1
- package/server/database/db.js +0 -593
- package/server/database/schema.js +0 -102
- package/server/projects.js +0 -2555
- package/server/routes/codex.js +0 -19
- package/server/routes/messages.js +0 -61
- package/server/routes/projects.js +0 -548
|
@@ -1,11 +1,249 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import fsSync from 'node:fs';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
|
|
5
|
+
import { sessionsDb } from '@/modules/database/index.js';
|
|
3
6
|
import type { IProviderSessions } from '@/shared/interfaces.js';
|
|
4
7
|
import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
|
|
5
8
|
import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
|
|
6
9
|
|
|
7
10
|
const PROVIDER = 'gemini';
|
|
8
11
|
|
|
12
|
+
type GeminiHistoryResult = {
|
|
13
|
+
messages: AnyRecord[];
|
|
14
|
+
tokenUsage?: unknown;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function mapGeminiRole(value: unknown): 'user' | 'assistant' | null {
|
|
18
|
+
if (value === 'user') {
|
|
19
|
+
return 'user';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (value === 'gemini' || value === 'assistant') {
|
|
23
|
+
return 'assistant';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function extractGeminiTextContent(content: unknown): string {
|
|
30
|
+
if (typeof content === 'string') {
|
|
31
|
+
return content;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!Array.isArray(content)) {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return content
|
|
39
|
+
.map((part) => {
|
|
40
|
+
if (typeof part === 'string') {
|
|
41
|
+
return part;
|
|
42
|
+
}
|
|
43
|
+
if (!part || typeof part !== 'object') {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const record = part as AnyRecord;
|
|
48
|
+
if (typeof record.text === 'string') {
|
|
49
|
+
return record.text;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return '';
|
|
53
|
+
})
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.join('\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function extractGeminiThoughts(thoughts: unknown): string {
|
|
59
|
+
if (!Array.isArray(thoughts)) {
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return thoughts
|
|
64
|
+
.map((item) => {
|
|
65
|
+
if (!item || typeof item !== 'object') {
|
|
66
|
+
return '';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const record = item as AnyRecord;
|
|
70
|
+
const subject = typeof record.subject === 'string' ? record.subject.trim() : '';
|
|
71
|
+
const description = typeof record.description === 'string' ? record.description.trim() : '';
|
|
72
|
+
|
|
73
|
+
if (subject && description) {
|
|
74
|
+
return `${subject}: ${description}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return description || subject;
|
|
78
|
+
})
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildGeminiTokenUsage(tokens: unknown): AnyRecord | undefined {
|
|
84
|
+
if (!tokens || typeof tokens !== 'object') {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const record = tokens as AnyRecord;
|
|
89
|
+
const input = Number(record.input || 0);
|
|
90
|
+
const output = Number(record.output || 0);
|
|
91
|
+
const cached = Number(record.cached || 0);
|
|
92
|
+
const thoughts = Number(record.thoughts || 0);
|
|
93
|
+
const tool = Number(record.tool || 0);
|
|
94
|
+
|
|
95
|
+
const totalFromFields = input + output + cached + thoughts + tool;
|
|
96
|
+
const total = Number(record.total || totalFromFields || 0);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
used: total,
|
|
100
|
+
total: total,
|
|
101
|
+
breakdown: {
|
|
102
|
+
input,
|
|
103
|
+
output,
|
|
104
|
+
cached,
|
|
105
|
+
thoughts,
|
|
106
|
+
tool,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function getGeminiLegacySessionMessages(sessionFilePath: string): Promise<GeminiHistoryResult> {
|
|
112
|
+
try {
|
|
113
|
+
const data = await fs.readFile(sessionFilePath, 'utf8');
|
|
114
|
+
const session = JSON.parse(data) as AnyRecord;
|
|
115
|
+
const sourceMessages = Array.isArray(session.messages) ? session.messages : [];
|
|
116
|
+
|
|
117
|
+
const messages: AnyRecord[] = [];
|
|
118
|
+
for (const msg of sourceMessages) {
|
|
119
|
+
const role = mapGeminiRole(msg.type ?? msg.role);
|
|
120
|
+
if (!role) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
messages.push({
|
|
125
|
+
type: 'message',
|
|
126
|
+
uuid: typeof msg.id === 'string' ? msg.id : undefined,
|
|
127
|
+
message: { role, content: msg.content },
|
|
128
|
+
timestamp: msg.timestamp || null,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { messages };
|
|
133
|
+
} catch {
|
|
134
|
+
return { messages: [] };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function getGeminiJsonlSessionMessages(sessionFilePath: string): Promise<GeminiHistoryResult> {
|
|
139
|
+
const messages: AnyRecord[] = [];
|
|
140
|
+
let tokenUsage: AnyRecord | undefined;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const fileStream = fsSync.createReadStream(sessionFilePath);
|
|
144
|
+
const lineReader = readline.createInterface({
|
|
145
|
+
input: fileStream,
|
|
146
|
+
crlfDelay: Infinity,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
for await (const line of lineReader) {
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
if (!trimmed) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let entry: AnyRecord;
|
|
156
|
+
try {
|
|
157
|
+
entry = JSON.parse(trimmed) as AnyRecord;
|
|
158
|
+
} catch {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Metadata/update lines (e.g. {$set:{lastUpdated:...}}) do not represent chat messages.
|
|
163
|
+
if (entry.$set) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const role = mapGeminiRole(entry.type);
|
|
168
|
+
if (role) {
|
|
169
|
+
const textContent = extractGeminiTextContent(entry.content);
|
|
170
|
+
if (textContent.trim()) {
|
|
171
|
+
messages.push({
|
|
172
|
+
type: 'message',
|
|
173
|
+
uuid: typeof entry.id === 'string' ? entry.id : undefined,
|
|
174
|
+
message: { role, content: textContent },
|
|
175
|
+
timestamp: entry.timestamp || null,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const thinkingContent = extractGeminiThoughts(entry.thoughts);
|
|
180
|
+
if (thinkingContent.trim()) {
|
|
181
|
+
messages.push({
|
|
182
|
+
type: 'thinking',
|
|
183
|
+
uuid: typeof entry.id === 'string' ? `${entry.id}_thinking` : undefined,
|
|
184
|
+
message: { role: 'assistant', content: thinkingContent },
|
|
185
|
+
timestamp: entry.timestamp || null,
|
|
186
|
+
isReasoning: true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (role === 'assistant') {
|
|
191
|
+
const usage = buildGeminiTokenUsage(entry.tokens);
|
|
192
|
+
if (usage) {
|
|
193
|
+
tokenUsage = usage;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (entry.type === 'tool_use') {
|
|
201
|
+
messages.push({
|
|
202
|
+
type: 'tool_use',
|
|
203
|
+
uuid: typeof entry.id === 'string' ? entry.id : undefined,
|
|
204
|
+
timestamp: entry.timestamp || null,
|
|
205
|
+
toolName: entry.tool_name || entry.name || 'Tool',
|
|
206
|
+
toolInput: entry.parameters ?? entry.input ?? entry.arguments ?? '',
|
|
207
|
+
toolCallId: entry.tool_id || entry.toolCallId || entry.id,
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (entry.type === 'tool_result') {
|
|
213
|
+
messages.push({
|
|
214
|
+
type: 'tool_result',
|
|
215
|
+
uuid: typeof entry.id === 'string' ? entry.id : undefined,
|
|
216
|
+
timestamp: entry.timestamp || null,
|
|
217
|
+
toolCallId: entry.tool_id || entry.toolCallId || entry.id || '',
|
|
218
|
+
output: entry.output ?? entry.result ?? '',
|
|
219
|
+
isError: Boolean(entry.error) || entry.status === 'error',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
return { messages: [] };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
messages.sort(
|
|
228
|
+
(a, b) => new Date(a.timestamp || 0).getTime() - new Date(b.timestamp || 0).getTime(),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
return { messages, tokenUsage };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function getGeminiCliSessionMessages(sessionId: string): Promise<GeminiHistoryResult> {
|
|
235
|
+
const sessionFilePath = sessionsDb.getSessionById(sessionId)?.jsonl_path;
|
|
236
|
+
if (!sessionFilePath) {
|
|
237
|
+
return { messages: [] };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (sessionFilePath.endsWith('.jsonl')) {
|
|
241
|
+
return getGeminiJsonlSessionMessages(sessionFilePath);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return getGeminiLegacySessionMessages(sessionFilePath);
|
|
245
|
+
}
|
|
246
|
+
|
|
9
247
|
export class GeminiSessionsProvider implements IProviderSessions {
|
|
10
248
|
/**
|
|
11
249
|
* Normalizes live Gemini stream-json events into the shared message shape.
|
|
@@ -108,8 +346,7 @@ export class GeminiSessionsProvider implements IProviderSessions {
|
|
|
108
346
|
}
|
|
109
347
|
|
|
110
348
|
/**
|
|
111
|
-
* Loads Gemini history from
|
|
112
|
-
* back to Gemini CLI session files on disk.
|
|
349
|
+
* Loads Gemini history from Gemini CLI session files on disk.
|
|
113
350
|
*/
|
|
114
351
|
async fetchHistory(
|
|
115
352
|
sessionId: string,
|
|
@@ -117,28 +354,73 @@ export class GeminiSessionsProvider implements IProviderSessions {
|
|
|
117
354
|
): Promise<FetchHistoryResult> {
|
|
118
355
|
const { limit = null, offset = 0 } = options;
|
|
119
356
|
|
|
120
|
-
let
|
|
357
|
+
let result: GeminiHistoryResult;
|
|
121
358
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (rawMessages.length === 0) {
|
|
125
|
-
rawMessages = await getGeminiCliSessionMessages(sessionId) as AnyRecord[];
|
|
126
|
-
}
|
|
359
|
+
result = await getGeminiCliSessionMessages(sessionId);
|
|
127
360
|
} catch (error) {
|
|
128
361
|
const message = error instanceof Error ? error.message : String(error);
|
|
129
362
|
console.warn(`[GeminiProvider] Failed to load session ${sessionId}:`, message);
|
|
130
363
|
return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
|
|
131
364
|
}
|
|
132
365
|
|
|
366
|
+
const rawMessages = result.messages;
|
|
133
367
|
const normalized: NormalizedMessage[] = [];
|
|
368
|
+
|
|
134
369
|
for (let i = 0; i < rawMessages.length; i++) {
|
|
135
370
|
const raw = rawMessages[i];
|
|
136
371
|
const ts = raw.timestamp || new Date().toISOString();
|
|
137
372
|
const baseId = raw.uuid || generateMessageId('gemini');
|
|
138
373
|
|
|
374
|
+
if (raw.type === 'thinking' || raw.isReasoning) {
|
|
375
|
+
const thinkingContent = typeof raw.message?.content === 'string'
|
|
376
|
+
? raw.message.content
|
|
377
|
+
: typeof raw.content === 'string'
|
|
378
|
+
? raw.content
|
|
379
|
+
: '';
|
|
380
|
+
|
|
381
|
+
if (thinkingContent.trim()) {
|
|
382
|
+
normalized.push(createNormalizedMessage({
|
|
383
|
+
id: baseId,
|
|
384
|
+
sessionId,
|
|
385
|
+
timestamp: ts,
|
|
386
|
+
provider: PROVIDER,
|
|
387
|
+
kind: 'thinking',
|
|
388
|
+
content: thinkingContent,
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (raw.type === 'tool_use' || raw.toolName) {
|
|
395
|
+
normalized.push(createNormalizedMessage({
|
|
396
|
+
id: baseId,
|
|
397
|
+
sessionId,
|
|
398
|
+
timestamp: ts,
|
|
399
|
+
provider: PROVIDER,
|
|
400
|
+
kind: 'tool_use',
|
|
401
|
+
toolName: raw.toolName || 'Tool',
|
|
402
|
+
toolInput: raw.toolInput,
|
|
403
|
+
toolId: raw.toolCallId || baseId,
|
|
404
|
+
}));
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (raw.type === 'tool_result') {
|
|
409
|
+
normalized.push(createNormalizedMessage({
|
|
410
|
+
id: baseId,
|
|
411
|
+
sessionId,
|
|
412
|
+
timestamp: ts,
|
|
413
|
+
provider: PROVIDER,
|
|
414
|
+
kind: 'tool_result',
|
|
415
|
+
toolId: raw.toolCallId || '',
|
|
416
|
+
content: raw.output === undefined ? '' : String(raw.output),
|
|
417
|
+
isError: Boolean(raw.isError),
|
|
418
|
+
}));
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
139
422
|
const role = raw.message?.role || raw.role;
|
|
140
423
|
const content = raw.message?.content || raw.content;
|
|
141
|
-
|
|
142
424
|
if (!role || !content) {
|
|
143
425
|
continue;
|
|
144
426
|
}
|
|
@@ -147,8 +429,26 @@ export class GeminiSessionsProvider implements IProviderSessions {
|
|
|
147
429
|
|
|
148
430
|
if (Array.isArray(content)) {
|
|
149
431
|
for (let partIdx = 0; partIdx < content.length; partIdx++) {
|
|
150
|
-
const part = content[partIdx];
|
|
151
|
-
|
|
432
|
+
const part = content[partIdx] as AnyRecord | string;
|
|
433
|
+
|
|
434
|
+
if (typeof part === 'string' && part.trim()) {
|
|
435
|
+
normalized.push(createNormalizedMessage({
|
|
436
|
+
id: `${baseId}_${partIdx}`,
|
|
437
|
+
sessionId,
|
|
438
|
+
timestamp: ts,
|
|
439
|
+
provider: PROVIDER,
|
|
440
|
+
kind: 'text',
|
|
441
|
+
role: normalizedRole,
|
|
442
|
+
content: part,
|
|
443
|
+
}));
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!part || typeof part !== 'object') {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if ((part.type === 'text' || !part.type) && typeof part.text === 'string' && part.text.trim()) {
|
|
152
452
|
normalized.push(createNormalizedMessage({
|
|
153
453
|
id: `${baseId}_${partIdx}`,
|
|
154
454
|
sessionId,
|
|
@@ -192,6 +492,19 @@ export class GeminiSessionsProvider implements IProviderSessions {
|
|
|
192
492
|
role: normalizedRole,
|
|
193
493
|
content,
|
|
194
494
|
}));
|
|
495
|
+
} else {
|
|
496
|
+
const textContent = extractGeminiTextContent(content);
|
|
497
|
+
if (textContent.trim()) {
|
|
498
|
+
normalized.push(createNormalizedMessage({
|
|
499
|
+
id: baseId,
|
|
500
|
+
sessionId,
|
|
501
|
+
timestamp: ts,
|
|
502
|
+
provider: PROVIDER,
|
|
503
|
+
kind: 'text',
|
|
504
|
+
role: normalizedRole,
|
|
505
|
+
content: textContent,
|
|
506
|
+
}));
|
|
507
|
+
}
|
|
195
508
|
}
|
|
196
509
|
}
|
|
197
510
|
|
|
@@ -222,6 +535,7 @@ export class GeminiSessionsProvider implements IProviderSessions {
|
|
|
222
535
|
hasMore: pageLimit === null ? false : start + pageLimit < normalized.length,
|
|
223
536
|
offset: start,
|
|
224
537
|
limit: pageLimit,
|
|
538
|
+
tokenUsage: result.tokenUsage,
|
|
225
539
|
};
|
|
226
540
|
}
|
|
227
541
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
|
|
2
2
|
import { GeminiProviderAuth } from '@/modules/providers/list/gemini/gemini-auth.provider.js';
|
|
3
3
|
import { GeminiMcpProvider } from '@/modules/providers/list/gemini/gemini-mcp.provider.js';
|
|
4
|
+
import { GeminiSessionSynchronizer } from '@/modules/providers/list/gemini/gemini-session-synchronizer.provider.js';
|
|
4
5
|
import { GeminiSessionsProvider } from '@/modules/providers/list/gemini/gemini-sessions.provider.js';
|
|
5
|
-
import type { IProviderAuth, IProviderSessions } from '@/shared/interfaces.js';
|
|
6
|
+
import type { IProviderAuth, IProviderSessionSynchronizer, IProviderSessions } from '@/shared/interfaces.js';
|
|
6
7
|
|
|
7
8
|
export class GeminiProvider extends AbstractProvider {
|
|
8
9
|
readonly mcp = new GeminiMcpProvider();
|
|
9
10
|
readonly auth: IProviderAuth = new GeminiProviderAuth();
|
|
10
11
|
readonly sessions: IProviderSessions = new GeminiSessionsProvider();
|
|
12
|
+
readonly sessionSynchronizer: IProviderSessionSynchronizer = new GeminiSessionSynchronizer();
|
|
11
13
|
|
|
12
14
|
constructor() {
|
|
13
15
|
super('gemini');
|
|
@@ -2,6 +2,8 @@ import express, { type Request, type Response } from 'express';
|
|
|
2
2
|
|
|
3
3
|
import { providerAuthService } from '@/modules/providers/services/provider-auth.service.js';
|
|
4
4
|
import { providerMcpService } from '@/modules/providers/services/mcp.service.js';
|
|
5
|
+
import { sessionConversationsSearchService } from '@/modules/providers/services/session-conversations-search.service.js';
|
|
6
|
+
import { sessionsService } from '@/modules/providers/services/sessions.service.js';
|
|
5
7
|
import type { LLMProvider, McpScope, McpTransport, UpsertProviderMcpServerInput } from '@/shared/types.js';
|
|
6
8
|
import { AppError, asyncHandler, createApiSuccessResponse } from '@/shared/utils.js';
|
|
7
9
|
|
|
@@ -25,6 +27,20 @@ const readPathParam = (value: unknown, name: string): string => {
|
|
|
25
27
|
const normalizeProviderParam = (value: unknown): string =>
|
|
26
28
|
readPathParam(value, 'provider').trim().toLowerCase();
|
|
27
29
|
|
|
30
|
+
const SESSION_ID_PATTERN = /^[a-zA-Z0-9._-]{1,120}$/;
|
|
31
|
+
|
|
32
|
+
const parseSessionId = (value: unknown): string => {
|
|
33
|
+
const sessionId = readPathParam(value, 'sessionId').trim();
|
|
34
|
+
if (!SESSION_ID_PATTERN.test(sessionId)) {
|
|
35
|
+
throw new AppError('Invalid sessionId.', {
|
|
36
|
+
code: 'INVALID_SESSION_ID',
|
|
37
|
+
statusCode: 400,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return sessionId;
|
|
42
|
+
};
|
|
43
|
+
|
|
28
44
|
const readOptionalQueryString = (value: unknown): string | undefined => {
|
|
29
45
|
if (typeof value !== 'string') {
|
|
30
46
|
return undefined;
|
|
@@ -34,6 +50,29 @@ const readOptionalQueryString = (value: unknown): string | undefined => {
|
|
|
34
50
|
return normalized.length > 0 ? normalized : undefined;
|
|
35
51
|
};
|
|
36
52
|
|
|
53
|
+
const parseOptionalBooleanQuery = (value: unknown, name: string): boolean | undefined => {
|
|
54
|
+
if (value === undefined) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const normalized = readOptionalQueryString(value);
|
|
59
|
+
if (!normalized) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (normalized === 'true') {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (normalized === 'false') {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new AppError(`${name} must be "true" or "false".`, {
|
|
71
|
+
code: 'INVALID_QUERY_PARAMETER',
|
|
72
|
+
statusCode: 400,
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
37
76
|
const parseMcpScope = (value: unknown): McpScope | undefined => {
|
|
38
77
|
if (value === undefined) {
|
|
39
78
|
return undefined;
|
|
@@ -103,19 +142,19 @@ const parseMcpUpsertPayload = (payload: unknown): UpsertProviderMcpServerInput =
|
|
|
103
142
|
args: Array.isArray(body.args) ? body.args.filter((entry): entry is string => typeof entry === 'string') : undefined,
|
|
104
143
|
env: typeof body.env === 'object' && body.env !== null
|
|
105
144
|
? Object.fromEntries(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
145
|
+
Object.entries(body.env as Record<string, unknown>).filter(
|
|
146
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
147
|
+
),
|
|
148
|
+
)
|
|
110
149
|
: undefined,
|
|
111
150
|
cwd: readOptionalQueryString(body.cwd),
|
|
112
151
|
url: readOptionalQueryString(body.url),
|
|
113
152
|
headers: typeof body.headers === 'object' && body.headers !== null
|
|
114
153
|
? Object.fromEntries(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
154
|
+
Object.entries(body.headers as Record<string, unknown>).filter(
|
|
155
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
156
|
+
),
|
|
157
|
+
)
|
|
119
158
|
: undefined,
|
|
120
159
|
envVars: Array.isArray(body.envVars)
|
|
121
160
|
? body.envVars.filter((entry): entry is string => typeof entry === 'string')
|
|
@@ -123,10 +162,10 @@ const parseMcpUpsertPayload = (payload: unknown): UpsertProviderMcpServerInput =
|
|
|
123
162
|
bearerTokenEnvVar: readOptionalQueryString(body.bearerTokenEnvVar),
|
|
124
163
|
envHttpHeaders: typeof body.envHttpHeaders === 'object' && body.envHttpHeaders !== null
|
|
125
164
|
? Object.fromEntries(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
165
|
+
Object.entries(body.envHttpHeaders as Record<string, unknown>).filter(
|
|
166
|
+
(entry): entry is [string, string] => typeof entry[1] === 'string',
|
|
167
|
+
),
|
|
168
|
+
)
|
|
130
169
|
: undefined,
|
|
131
170
|
};
|
|
132
171
|
};
|
|
@@ -143,6 +182,62 @@ const parseProvider = (value: unknown): LLMProvider => {
|
|
|
143
182
|
});
|
|
144
183
|
};
|
|
145
184
|
|
|
185
|
+
const parseSessionRenameSummary = (payload: unknown): string => {
|
|
186
|
+
if (!payload || typeof payload !== 'object') {
|
|
187
|
+
throw new AppError('Request body must be an object.', {
|
|
188
|
+
code: 'INVALID_REQUEST_BODY',
|
|
189
|
+
statusCode: 400,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const body = payload as Record<string, unknown>;
|
|
194
|
+
const summary = typeof body.summary === 'string' ? body.summary.trim() : '';
|
|
195
|
+
if (!summary) {
|
|
196
|
+
throw new AppError('Summary is required.', {
|
|
197
|
+
code: 'INVALID_SESSION_SUMMARY',
|
|
198
|
+
statusCode: 400,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (summary.length > 500) {
|
|
203
|
+
throw new AppError('Summary must not exceed 500 characters.', {
|
|
204
|
+
code: 'INVALID_SESSION_SUMMARY',
|
|
205
|
+
statusCode: 400,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return summary;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const parseSessionSearchQuery = (value: unknown): string => {
|
|
213
|
+
const query = readOptionalQueryString(value) ?? '';
|
|
214
|
+
if (query.length < 2) {
|
|
215
|
+
throw new AppError('Query must be at least 2 characters', {
|
|
216
|
+
code: 'INVALID_SEARCH_QUERY',
|
|
217
|
+
statusCode: 400,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return query;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const parseSessionSearchLimit = (value: unknown): number => {
|
|
225
|
+
const raw = readOptionalQueryString(value);
|
|
226
|
+
if (!raw) {
|
|
227
|
+
return 50;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const parsed = Number.parseInt(raw, 10);
|
|
231
|
+
if (Number.isNaN(parsed)) {
|
|
232
|
+
throw new AppError('limit must be a valid integer.', {
|
|
233
|
+
code: 'INVALID_QUERY_PARAMETER',
|
|
234
|
+
statusCode: 400,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return Math.max(1, Math.min(parsed, 100));
|
|
239
|
+
};
|
|
240
|
+
|
|
146
241
|
router.get(
|
|
147
242
|
'/:provider/auth/status',
|
|
148
243
|
asyncHandler(async (req: Request, res: Response) => {
|
|
@@ -152,6 +247,7 @@ router.get(
|
|
|
152
247
|
}),
|
|
153
248
|
);
|
|
154
249
|
|
|
250
|
+
// ----------------- MCP routes -----------------
|
|
155
251
|
router.get(
|
|
156
252
|
'/:provider/mcp/servers',
|
|
157
253
|
asyncHandler(async (req: Request, res: Response) => {
|
|
@@ -214,4 +310,116 @@ router.post(
|
|
|
214
310
|
}),
|
|
215
311
|
);
|
|
216
312
|
|
|
313
|
+
// ----------------- Session routes -----------------
|
|
314
|
+
router.delete(
|
|
315
|
+
'/sessions/:sessionId',
|
|
316
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
317
|
+
const sessionId = parseSessionId(req.params.sessionId);
|
|
318
|
+
const deletedFromDisk = parseOptionalBooleanQuery(req.query.deletedFromDisk, 'deletedFromDisk') ?? false;
|
|
319
|
+
const result = await sessionsService.deleteSessionById(sessionId, deletedFromDisk);
|
|
320
|
+
res.json(createApiSuccessResponse(result));
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
router.put(
|
|
325
|
+
'/sessions/:sessionId',
|
|
326
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
327
|
+
const sessionId = parseSessionId(req.params.sessionId);
|
|
328
|
+
const summary = parseSessionRenameSummary(req.body);
|
|
329
|
+
const result = sessionsService.renameSessionById(sessionId, summary);
|
|
330
|
+
res.json(createApiSuccessResponse(result));
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
router.get(
|
|
335
|
+
'/sessions/:sessionId/messages',
|
|
336
|
+
asyncHandler(async (req: Request, res: Response) => {
|
|
337
|
+
const sessionId = parseSessionId(req.params.sessionId);
|
|
338
|
+
const limitRaw = readOptionalQueryString(req.query.limit);
|
|
339
|
+
const offsetRaw = readOptionalQueryString(req.query.offset);
|
|
340
|
+
|
|
341
|
+
let limit: number | null = null;
|
|
342
|
+
if (limitRaw !== undefined) {
|
|
343
|
+
const parsedLimit = Number.parseInt(limitRaw, 10);
|
|
344
|
+
if (Number.isNaN(parsedLimit) || parsedLimit < 0) {
|
|
345
|
+
throw new AppError('limit must be a non-negative integer.', {
|
|
346
|
+
code: 'INVALID_QUERY_PARAMETER',
|
|
347
|
+
statusCode: 400,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
limit = parsedLimit;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let offset = 0;
|
|
354
|
+
if (offsetRaw !== undefined) {
|
|
355
|
+
const parsedOffset = Number.parseInt(offsetRaw, 10);
|
|
356
|
+
if (Number.isNaN(parsedOffset) || parsedOffset < 0) {
|
|
357
|
+
throw new AppError('offset must be a non-negative integer.', {
|
|
358
|
+
code: 'INVALID_QUERY_PARAMETER',
|
|
359
|
+
statusCode: 400,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
offset = parsedOffset;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const result = await sessionsService.fetchHistory(sessionId, {
|
|
366
|
+
limit,
|
|
367
|
+
offset,
|
|
368
|
+
});
|
|
369
|
+
res.json(result);
|
|
370
|
+
}),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
router.get('/search/sessions', asyncHandler(async (req: Request, res: Response) => {
|
|
374
|
+
const query = parseSessionSearchQuery(req.query.q);
|
|
375
|
+
const limit = parseSessionSearchLimit(req.query.limit);
|
|
376
|
+
|
|
377
|
+
res.writeHead(200, {
|
|
378
|
+
'Content-Type': 'text/event-stream',
|
|
379
|
+
'Cache-Control': 'no-cache',
|
|
380
|
+
Connection: 'keep-alive',
|
|
381
|
+
'X-Accel-Buffering': 'no',
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
let closed = false;
|
|
385
|
+
const abortController = new AbortController();
|
|
386
|
+
req.on('close', () => {
|
|
387
|
+
closed = true;
|
|
388
|
+
abortController.abort();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
await sessionConversationsSearchService.search({
|
|
393
|
+
query,
|
|
394
|
+
limit,
|
|
395
|
+
signal: abortController.signal,
|
|
396
|
+
onProgress: ({ projectResult, totalMatches, scannedProjects, totalProjects }) => {
|
|
397
|
+
if (closed) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (projectResult) {
|
|
402
|
+
res.write(`event: result\ndata: ${JSON.stringify({ projectResult, totalMatches, scannedProjects, totalProjects })}\n\n`);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
res.write(`event: progress\ndata: ${JSON.stringify({ totalMatches, scannedProjects, totalProjects })}\n\n`);
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
if (!closed) {
|
|
411
|
+
res.write('event: done\ndata: {}\n\n');
|
|
412
|
+
}
|
|
413
|
+
} catch (error) {
|
|
414
|
+
console.error('Error searching conversations:', error);
|
|
415
|
+
if (!closed) {
|
|
416
|
+
res.write(`event: error\ndata: ${JSON.stringify({ error: 'Search failed' })}\n\n`);
|
|
417
|
+
}
|
|
418
|
+
} finally {
|
|
419
|
+
if (!closed) {
|
|
420
|
+
res.end();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}));
|
|
424
|
+
|
|
217
425
|
export default router;
|