@pixelbyte-software/pixcode 1.30.2 → 1.31.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 (202) hide show
  1. package/LICENSE +718 -718
  2. package/README.de.md +248 -248
  3. package/README.ja.md +240 -240
  4. package/README.ko.md +240 -240
  5. package/README.md +295 -285
  6. package/README.ru.md +248 -248
  7. package/README.tr.md +250 -250
  8. package/README.zh-CN.md +240 -240
  9. package/dist/api-docs.html +879 -879
  10. package/dist/assets/index-BRRJ47XQ.css +32 -0
  11. package/dist/assets/index-EQohwyiC.js +837 -0
  12. package/dist/clear-cache.html +85 -85
  13. package/dist/convert-icons.md +52 -52
  14. package/dist/favicon.png +0 -0
  15. package/dist/favicon.svg +7 -8
  16. package/dist/generate-icons.js +48 -48
  17. package/dist/icons/codex-white.svg +3 -3
  18. package/dist/icons/codex.svg +3 -3
  19. package/dist/icons/cursor-white.svg +11 -11
  20. package/dist/icons/icon-128x128.png +0 -0
  21. package/dist/icons/icon-128x128.svg +9 -12
  22. package/dist/icons/icon-144x144.png +0 -0
  23. package/dist/icons/icon-144x144.svg +9 -12
  24. package/dist/icons/icon-152x152.png +0 -0
  25. package/dist/icons/icon-152x152.svg +9 -12
  26. package/dist/icons/icon-192x192.png +0 -0
  27. package/dist/icons/icon-192x192.svg +9 -12
  28. package/dist/icons/icon-384x384.png +0 -0
  29. package/dist/icons/icon-384x384.svg +9 -12
  30. package/dist/icons/icon-512x512.png +0 -0
  31. package/dist/icons/icon-512x512.svg +9 -12
  32. package/dist/icons/icon-72x72.png +0 -0
  33. package/dist/icons/icon-72x72.svg +9 -12
  34. package/dist/icons/icon-96x96.png +0 -0
  35. package/dist/icons/icon-96x96.svg +9 -12
  36. package/dist/icons/icon-template.svg +9 -12
  37. package/dist/icons/qwen-ai-icon.png +0 -0
  38. package/dist/index.html +59 -49
  39. package/dist/logo.png +0 -0
  40. package/dist/logo.svg +11 -16
  41. package/dist/manifest.json +60 -60
  42. package/dist/sw.js +124 -124
  43. package/dist-server/server/cli.js +100 -97
  44. package/dist-server/server/cli.js.map +1 -1
  45. package/dist-server/server/daemon/manager.js +33 -33
  46. package/dist-server/server/daemon-manager.js +62 -62
  47. package/dist-server/server/database/db.js +114 -22
  48. package/dist-server/server/database/db.js.map +1 -1
  49. package/dist-server/server/database/schema.js +122 -89
  50. package/dist-server/server/database/schema.js.map +1 -1
  51. package/dist-server/server/gemini-cli.js +6 -1
  52. package/dist-server/server/gemini-cli.js.map +1 -1
  53. package/dist-server/server/index.js +234 -64
  54. package/dist-server/server/index.js.map +1 -1
  55. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +29 -2
  56. package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
  57. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js +22 -2
  58. package/dist-server/server/modules/providers/list/codex/codex-auth.provider.js.map +1 -1
  59. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +2 -2
  60. package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
  61. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js +14 -2
  62. package/dist-server/server/modules/providers/list/gemini/gemini-auth.provider.js.map +1 -1
  63. package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js +132 -0
  64. package/dist-server/server/modules/providers/list/qwen/qwen-auth.provider.js.map +1 -0
  65. package/dist-server/server/modules/providers/list/qwen/qwen-mcp.provider.js +87 -0
  66. package/dist-server/server/modules/providers/list/qwen/qwen-mcp.provider.js.map +1 -0
  67. package/dist-server/server/modules/providers/list/qwen/qwen-sessions.provider.js +201 -0
  68. package/dist-server/server/modules/providers/list/qwen/qwen-sessions.provider.js.map +1 -0
  69. package/dist-server/server/modules/providers/list/qwen/qwen.provider.js +19 -0
  70. package/dist-server/server/modules/providers/list/qwen/qwen.provider.js.map +1 -0
  71. package/dist-server/server/modules/providers/provider.registry.js +2 -0
  72. package/dist-server/server/modules/providers/provider.registry.js.map +1 -1
  73. package/dist-server/server/modules/providers/provider.routes.js +310 -1
  74. package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
  75. package/dist-server/server/projects.js +197 -6
  76. package/dist-server/server/projects.js.map +1 -1
  77. package/dist-server/server/qwen-code-cli.js +350 -0
  78. package/dist-server/server/qwen-code-cli.js.map +1 -0
  79. package/dist-server/server/qwen-response-handler.js +70 -0
  80. package/dist-server/server/qwen-response-handler.js.map +1 -0
  81. package/dist-server/server/routes/commands.js +25 -25
  82. package/dist-server/server/routes/git.js +17 -17
  83. package/dist-server/server/routes/network.js +116 -0
  84. package/dist-server/server/routes/network.js.map +1 -0
  85. package/dist-server/server/routes/projects.js +43 -0
  86. package/dist-server/server/routes/projects.js.map +1 -1
  87. package/dist-server/server/routes/qwen.js +23 -0
  88. package/dist-server/server/routes/qwen.js.map +1 -0
  89. package/dist-server/server/routes/taskmaster.js +419 -419
  90. package/dist-server/server/routes/telegram.js +119 -0
  91. package/dist-server/server/routes/telegram.js.map +1 -0
  92. package/dist-server/server/services/external-access.js +228 -0
  93. package/dist-server/server/services/external-access.js.map +1 -0
  94. package/dist-server/server/services/install-jobs.js +394 -0
  95. package/dist-server/server/services/install-jobs.js.map +1 -0
  96. package/dist-server/server/services/notification-orchestrator.js +19 -5
  97. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  98. package/dist-server/server/services/provider-credentials.js +154 -0
  99. package/dist-server/server/services/provider-credentials.js.map +1 -0
  100. package/dist-server/server/services/provider-models.js +218 -0
  101. package/dist-server/server/services/provider-models.js.map +1 -0
  102. package/dist-server/server/services/telegram/bot.js +259 -0
  103. package/dist-server/server/services/telegram/bot.js.map +1 -0
  104. package/dist-server/server/services/telegram/translations.js +160 -0
  105. package/dist-server/server/services/telegram/translations.js.map +1 -0
  106. package/dist-server/server/utils/port-access.js +196 -0
  107. package/dist-server/server/utils/port-access.js.map +1 -0
  108. package/dist-server/shared/modelConstants.js +18 -0
  109. package/dist-server/shared/modelConstants.js.map +1 -1
  110. package/package.json +177 -168
  111. package/scripts/fix-node-pty.js +67 -67
  112. package/server/claude-sdk.js +834 -834
  113. package/server/cli.js +940 -937
  114. package/server/constants/config.js +4 -4
  115. package/server/cursor-cli.js +342 -342
  116. package/server/daemon/manager.js +564 -564
  117. package/server/daemon-manager.js +920 -920
  118. package/server/database/db.js +696 -593
  119. package/server/database/schema.js +138 -102
  120. package/server/gemini-cli.js +475 -469
  121. package/server/gemini-response-handler.js +79 -79
  122. package/server/index.js +2730 -2556
  123. package/server/load-env.js +34 -34
  124. package/server/middleware/auth.js +132 -132
  125. package/server/modules/providers/list/claude/claude-auth.provider.ts +145 -123
  126. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  127. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
  128. package/server/modules/providers/list/claude/claude.provider.ts +15 -15
  129. package/server/modules/providers/list/codex/codex-auth.provider.ts +115 -100
  130. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  131. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
  132. package/server/modules/providers/list/codex/codex.provider.ts +15 -15
  133. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -143
  134. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  135. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
  136. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
  137. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +163 -151
  138. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
  139. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
  140. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
  141. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +145 -0
  142. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -0
  143. package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +218 -0
  144. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -0
  145. package/server/modules/providers/provider.registry.ts +38 -36
  146. package/server/modules/providers/provider.routes.ts +583 -217
  147. package/server/modules/providers/services/mcp.service.ts +94 -94
  148. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  149. package/server/modules/providers/services/sessions.service.ts +45 -45
  150. package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
  151. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  152. package/server/modules/providers/tests/mcp.test.ts +293 -293
  153. package/server/openai-codex.js +426 -426
  154. package/server/projects.js +2993 -2792
  155. package/server/qwen-code-cli.js +392 -0
  156. package/server/qwen-response-handler.js +73 -0
  157. package/server/routes/agent.js +1245 -1245
  158. package/server/routes/auth.js +134 -134
  159. package/server/routes/codex.js +19 -19
  160. package/server/routes/commands.js +554 -554
  161. package/server/routes/cursor.js +52 -52
  162. package/server/routes/gemini.js +24 -24
  163. package/server/routes/git.js +1488 -1488
  164. package/server/routes/mcp-utils.js +31 -31
  165. package/server/routes/messages.js +61 -61
  166. package/server/routes/network.js +128 -0
  167. package/server/routes/plugins.js +307 -307
  168. package/server/routes/projects.js +675 -627
  169. package/server/routes/qwen.js +27 -0
  170. package/server/routes/settings.js +286 -286
  171. package/server/routes/taskmaster.js +1471 -1471
  172. package/server/routes/telegram.js +125 -0
  173. package/server/routes/user.js +123 -123
  174. package/server/services/external-access.js +240 -0
  175. package/server/services/install-jobs.js +410 -0
  176. package/server/services/notification-orchestrator.js +242 -227
  177. package/server/services/provider-credentials.js +151 -0
  178. package/server/services/provider-models.js +225 -0
  179. package/server/services/telegram/bot.js +280 -0
  180. package/server/services/telegram/translations.js +170 -0
  181. package/server/services/vapid-keys.js +35 -35
  182. package/server/sessionManager.js +225 -225
  183. package/server/shared/interfaces.ts +54 -54
  184. package/server/shared/types.ts +172 -172
  185. package/server/shared/utils.ts +193 -193
  186. package/server/tsconfig.json +36 -36
  187. package/server/utils/colors.js +21 -21
  188. package/server/utils/commandParser.js +303 -303
  189. package/server/utils/frontmatter.js +18 -18
  190. package/server/utils/gitConfig.js +34 -34
  191. package/server/utils/mcp-detector.js +147 -147
  192. package/server/utils/plugin-loader.js +457 -457
  193. package/server/utils/plugin-process-manager.js +184 -184
  194. package/server/utils/port-access.js +209 -0
  195. package/server/utils/runtime-paths.js +37 -37
  196. package/server/utils/taskmaster-websocket.js +128 -128
  197. package/server/utils/url-detection.js +71 -71
  198. package/server/vite-daemon.js +78 -78
  199. package/shared/modelConstants.js +117 -97
  200. package/shared/networkHosts.js +22 -22
  201. package/dist/assets/index-C2c9QNwK.css +0 -32
  202. package/dist/assets/index-DyXDZED-.js +0 -1277
@@ -1,227 +1,227 @@
1
- import sessionManager from '@/sessionManager.js';
2
- import { getGeminiCliSessionMessages } from '@/projects.js';
3
- import type { IProviderSessions } from '@/shared/interfaces.js';
4
- import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
5
- import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
6
-
7
- const PROVIDER = 'gemini';
8
-
9
- export class GeminiSessionsProvider implements IProviderSessions {
10
- /**
11
- * Normalizes live Gemini stream-json events into the shared message shape.
12
- *
13
- * Gemini history uses a different session file shape, so fetchHistory handles
14
- * that separately after loading raw persisted messages.
15
- */
16
- normalizeMessage(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] {
17
- const raw = readObjectRecord(rawMessage);
18
- if (!raw) {
19
- return [];
20
- }
21
-
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 messages: NormalizedMessage[] = [];
28
- if (content) {
29
- messages.push(createNormalizedMessage({
30
- id: baseId,
31
- sessionId,
32
- timestamp: ts,
33
- provider: PROVIDER,
34
- kind: 'stream_delta',
35
- content,
36
- }));
37
- }
38
- if (raw.delta !== true) {
39
- messages.push(createNormalizedMessage({
40
- sessionId,
41
- timestamp: ts,
42
- provider: PROVIDER,
43
- kind: 'stream_end',
44
- }));
45
- }
46
- return messages;
47
- }
48
-
49
- if (raw.type === 'tool_use') {
50
- return [createNormalizedMessage({
51
- id: baseId,
52
- sessionId,
53
- timestamp: ts,
54
- provider: PROVIDER,
55
- kind: 'tool_use',
56
- toolName: raw.tool_name,
57
- toolInput: raw.parameters || {},
58
- toolId: raw.tool_id || baseId,
59
- })];
60
- }
61
-
62
- if (raw.type === 'tool_result') {
63
- return [createNormalizedMessage({
64
- id: baseId,
65
- sessionId,
66
- timestamp: ts,
67
- provider: PROVIDER,
68
- kind: 'tool_result',
69
- toolId: raw.tool_id || '',
70
- content: raw.output === undefined ? '' : String(raw.output),
71
- isError: raw.status === 'error',
72
- })];
73
- }
74
-
75
- if (raw.type === 'result') {
76
- const messages = [createNormalizedMessage({
77
- sessionId,
78
- timestamp: ts,
79
- provider: PROVIDER,
80
- kind: 'stream_end',
81
- })];
82
- if (raw.stats?.total_tokens) {
83
- messages.push(createNormalizedMessage({
84
- sessionId,
85
- timestamp: ts,
86
- provider: PROVIDER,
87
- kind: 'status',
88
- text: 'Complete',
89
- tokens: raw.stats.total_tokens,
90
- canInterrupt: false,
91
- }));
92
- }
93
- return messages;
94
- }
95
-
96
- if (raw.type === 'error') {
97
- return [createNormalizedMessage({
98
- id: baseId,
99
- sessionId,
100
- timestamp: ts,
101
- provider: PROVIDER,
102
- kind: 'error',
103
- content: raw.error || raw.message || 'Unknown Gemini streaming error',
104
- })];
105
- }
106
-
107
- return [];
108
- }
109
-
110
- /**
111
- * Loads Gemini history from the in-memory session manager first, then falls
112
- * back to Gemini CLI session files on disk.
113
- */
114
- async fetchHistory(
115
- sessionId: string,
116
- options: FetchHistoryOptions = {},
117
- ): Promise<FetchHistoryResult> {
118
- const { limit = null, offset = 0 } = options;
119
-
120
- let rawMessages: AnyRecord[];
121
- try {
122
- rawMessages = sessionManager.getSessionMessages(sessionId) as AnyRecord[];
123
-
124
- if (rawMessages.length === 0) {
125
- rawMessages = await getGeminiCliSessionMessages(sessionId) as AnyRecord[];
126
- }
127
- } catch (error) {
128
- const message = error instanceof Error ? error.message : String(error);
129
- console.warn(`[GeminiProvider] Failed to load session ${sessionId}:`, message);
130
- return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
131
- }
132
-
133
- const normalized: NormalizedMessage[] = [];
134
- for (let i = 0; i < rawMessages.length; i++) {
135
- const raw = rawMessages[i];
136
- const ts = raw.timestamp || new Date().toISOString();
137
- const baseId = raw.uuid || generateMessageId('gemini');
138
-
139
- const role = raw.message?.role || raw.role;
140
- const content = raw.message?.content || raw.content;
141
-
142
- if (!role || !content) {
143
- continue;
144
- }
145
-
146
- const normalizedRole = role === 'user' ? 'user' : 'assistant';
147
-
148
- if (Array.isArray(content)) {
149
- for (let partIdx = 0; partIdx < content.length; partIdx++) {
150
- const part = content[partIdx];
151
- if (part.type === 'text' && part.text) {
152
- normalized.push(createNormalizedMessage({
153
- id: `${baseId}_${partIdx}`,
154
- sessionId,
155
- timestamp: ts,
156
- provider: PROVIDER,
157
- kind: 'text',
158
- role: normalizedRole,
159
- content: part.text,
160
- }));
161
- } else if (part.type === 'tool_use') {
162
- normalized.push(createNormalizedMessage({
163
- id: `${baseId}_${partIdx}`,
164
- sessionId,
165
- timestamp: ts,
166
- provider: PROVIDER,
167
- kind: 'tool_use',
168
- toolName: part.name,
169
- toolInput: part.input,
170
- toolId: part.id || generateMessageId('gemini_tool'),
171
- }));
172
- } else if (part.type === 'tool_result') {
173
- normalized.push(createNormalizedMessage({
174
- id: `${baseId}_${partIdx}`,
175
- sessionId,
176
- timestamp: ts,
177
- provider: PROVIDER,
178
- kind: 'tool_result',
179
- toolId: part.tool_use_id || '',
180
- content: part.content === undefined ? '' : String(part.content),
181
- isError: Boolean(part.is_error),
182
- }));
183
- }
184
- }
185
- } else if (typeof content === 'string' && content.trim()) {
186
- normalized.push(createNormalizedMessage({
187
- id: baseId,
188
- sessionId,
189
- timestamp: ts,
190
- provider: PROVIDER,
191
- kind: 'text',
192
- role: normalizedRole,
193
- content,
194
- }));
195
- }
196
- }
197
-
198
- const toolResultMap = new Map<string, NormalizedMessage>();
199
- for (const msg of normalized) {
200
- if (msg.kind === 'tool_result' && msg.toolId) {
201
- toolResultMap.set(msg.toolId, msg);
202
- }
203
- }
204
- for (const msg of normalized) {
205
- if (msg.kind === 'tool_use' && msg.toolId && toolResultMap.has(msg.toolId)) {
206
- const toolResult = toolResultMap.get(msg.toolId);
207
- if (toolResult) {
208
- msg.toolResult = { content: toolResult.content, isError: toolResult.isError };
209
- }
210
- }
211
- }
212
-
213
- const start = Math.max(0, offset);
214
- const pageLimit = limit === null ? null : Math.max(0, limit);
215
- const messages = pageLimit === null
216
- ? normalized.slice(start)
217
- : normalized.slice(start, start + pageLimit);
218
-
219
- return {
220
- messages,
221
- total: normalized.length,
222
- hasMore: pageLimit === null ? false : start + pageLimit < normalized.length,
223
- offset: start,
224
- limit: pageLimit,
225
- };
226
- }
227
- }
1
+ import sessionManager from '@/sessionManager.js';
2
+ import { getGeminiCliSessionMessages } from '@/projects.js';
3
+ import type { IProviderSessions } from '@/shared/interfaces.js';
4
+ import type { AnyRecord, FetchHistoryOptions, FetchHistoryResult, NormalizedMessage } from '@/shared/types.js';
5
+ import { createNormalizedMessage, generateMessageId, readObjectRecord } from '@/shared/utils.js';
6
+
7
+ const PROVIDER = 'gemini';
8
+
9
+ export class GeminiSessionsProvider implements IProviderSessions {
10
+ /**
11
+ * Normalizes live Gemini stream-json events into the shared message shape.
12
+ *
13
+ * Gemini history uses a different session file shape, so fetchHistory handles
14
+ * that separately after loading raw persisted messages.
15
+ */
16
+ normalizeMessage(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] {
17
+ const raw = readObjectRecord(rawMessage);
18
+ if (!raw) {
19
+ return [];
20
+ }
21
+
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 messages: NormalizedMessage[] = [];
28
+ if (content) {
29
+ messages.push(createNormalizedMessage({
30
+ id: baseId,
31
+ sessionId,
32
+ timestamp: ts,
33
+ provider: PROVIDER,
34
+ kind: 'stream_delta',
35
+ content,
36
+ }));
37
+ }
38
+ if (raw.delta !== true) {
39
+ messages.push(createNormalizedMessage({
40
+ sessionId,
41
+ timestamp: ts,
42
+ provider: PROVIDER,
43
+ kind: 'stream_end',
44
+ }));
45
+ }
46
+ return messages;
47
+ }
48
+
49
+ if (raw.type === 'tool_use') {
50
+ return [createNormalizedMessage({
51
+ id: baseId,
52
+ sessionId,
53
+ timestamp: ts,
54
+ provider: PROVIDER,
55
+ kind: 'tool_use',
56
+ toolName: raw.tool_name,
57
+ toolInput: raw.parameters || {},
58
+ toolId: raw.tool_id || baseId,
59
+ })];
60
+ }
61
+
62
+ if (raw.type === 'tool_result') {
63
+ return [createNormalizedMessage({
64
+ id: baseId,
65
+ sessionId,
66
+ timestamp: ts,
67
+ provider: PROVIDER,
68
+ kind: 'tool_result',
69
+ toolId: raw.tool_id || '',
70
+ content: raw.output === undefined ? '' : String(raw.output),
71
+ isError: raw.status === 'error',
72
+ })];
73
+ }
74
+
75
+ if (raw.type === 'result') {
76
+ const messages = [createNormalizedMessage({
77
+ sessionId,
78
+ timestamp: ts,
79
+ provider: PROVIDER,
80
+ kind: 'stream_end',
81
+ })];
82
+ if (raw.stats?.total_tokens) {
83
+ messages.push(createNormalizedMessage({
84
+ sessionId,
85
+ timestamp: ts,
86
+ provider: PROVIDER,
87
+ kind: 'status',
88
+ text: 'Complete',
89
+ tokens: raw.stats.total_tokens,
90
+ canInterrupt: false,
91
+ }));
92
+ }
93
+ return messages;
94
+ }
95
+
96
+ if (raw.type === 'error') {
97
+ return [createNormalizedMessage({
98
+ id: baseId,
99
+ sessionId,
100
+ timestamp: ts,
101
+ provider: PROVIDER,
102
+ kind: 'error',
103
+ content: raw.error || raw.message || 'Unknown Gemini streaming error',
104
+ })];
105
+ }
106
+
107
+ return [];
108
+ }
109
+
110
+ /**
111
+ * Loads Gemini history from the in-memory session manager first, then falls
112
+ * back to Gemini CLI session files on disk.
113
+ */
114
+ async fetchHistory(
115
+ sessionId: string,
116
+ options: FetchHistoryOptions = {},
117
+ ): Promise<FetchHistoryResult> {
118
+ const { limit = null, offset = 0 } = options;
119
+
120
+ let rawMessages: AnyRecord[];
121
+ try {
122
+ rawMessages = sessionManager.getSessionMessages(sessionId) as AnyRecord[];
123
+
124
+ if (rawMessages.length === 0) {
125
+ rawMessages = await getGeminiCliSessionMessages(sessionId) as AnyRecord[];
126
+ }
127
+ } catch (error) {
128
+ const message = error instanceof Error ? error.message : String(error);
129
+ console.warn(`[GeminiProvider] Failed to load session ${sessionId}:`, message);
130
+ return { messages: [], total: 0, hasMore: false, offset: 0, limit: null };
131
+ }
132
+
133
+ const normalized: NormalizedMessage[] = [];
134
+ for (let i = 0; i < rawMessages.length; i++) {
135
+ const raw = rawMessages[i];
136
+ const ts = raw.timestamp || new Date().toISOString();
137
+ const baseId = raw.uuid || generateMessageId('gemini');
138
+
139
+ const role = raw.message?.role || raw.role;
140
+ const content = raw.message?.content || raw.content;
141
+
142
+ if (!role || !content) {
143
+ continue;
144
+ }
145
+
146
+ const normalizedRole = role === 'user' ? 'user' : 'assistant';
147
+
148
+ if (Array.isArray(content)) {
149
+ for (let partIdx = 0; partIdx < content.length; partIdx++) {
150
+ const part = content[partIdx];
151
+ if (part.type === 'text' && part.text) {
152
+ normalized.push(createNormalizedMessage({
153
+ id: `${baseId}_${partIdx}`,
154
+ sessionId,
155
+ timestamp: ts,
156
+ provider: PROVIDER,
157
+ kind: 'text',
158
+ role: normalizedRole,
159
+ content: part.text,
160
+ }));
161
+ } else if (part.type === 'tool_use') {
162
+ normalized.push(createNormalizedMessage({
163
+ id: `${baseId}_${partIdx}`,
164
+ sessionId,
165
+ timestamp: ts,
166
+ provider: PROVIDER,
167
+ kind: 'tool_use',
168
+ toolName: part.name,
169
+ toolInput: part.input,
170
+ toolId: part.id || generateMessageId('gemini_tool'),
171
+ }));
172
+ } else if (part.type === 'tool_result') {
173
+ normalized.push(createNormalizedMessage({
174
+ id: `${baseId}_${partIdx}`,
175
+ sessionId,
176
+ timestamp: ts,
177
+ provider: PROVIDER,
178
+ kind: 'tool_result',
179
+ toolId: part.tool_use_id || '',
180
+ content: part.content === undefined ? '' : String(part.content),
181
+ isError: Boolean(part.is_error),
182
+ }));
183
+ }
184
+ }
185
+ } else if (typeof content === 'string' && content.trim()) {
186
+ normalized.push(createNormalizedMessage({
187
+ id: baseId,
188
+ sessionId,
189
+ timestamp: ts,
190
+ provider: PROVIDER,
191
+ kind: 'text',
192
+ role: normalizedRole,
193
+ content,
194
+ }));
195
+ }
196
+ }
197
+
198
+ const toolResultMap = new Map<string, NormalizedMessage>();
199
+ for (const msg of normalized) {
200
+ if (msg.kind === 'tool_result' && msg.toolId) {
201
+ toolResultMap.set(msg.toolId, msg);
202
+ }
203
+ }
204
+ for (const msg of normalized) {
205
+ if (msg.kind === 'tool_use' && msg.toolId && toolResultMap.has(msg.toolId)) {
206
+ const toolResult = toolResultMap.get(msg.toolId);
207
+ if (toolResult) {
208
+ msg.toolResult = { content: toolResult.content, isError: toolResult.isError };
209
+ }
210
+ }
211
+ }
212
+
213
+ const start = Math.max(0, offset);
214
+ const pageLimit = limit === null ? null : Math.max(0, limit);
215
+ const messages = pageLimit === null
216
+ ? normalized.slice(start)
217
+ : normalized.slice(start, start + pageLimit);
218
+
219
+ return {
220
+ messages,
221
+ total: normalized.length,
222
+ hasMore: pageLimit === null ? false : start + pageLimit < normalized.length,
223
+ offset: start,
224
+ limit: pageLimit,
225
+ };
226
+ }
227
+ }
@@ -1,15 +1,15 @@
1
- import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
2
- import { GeminiProviderAuth } from '@/modules/providers/list/gemini/gemini-auth.provider.js';
3
- import { GeminiMcpProvider } from '@/modules/providers/list/gemini/gemini-mcp.provider.js';
4
- import { GeminiSessionsProvider } from '@/modules/providers/list/gemini/gemini-sessions.provider.js';
5
- import type { IProviderAuth, IProviderSessions } from '@/shared/interfaces.js';
6
-
7
- export class GeminiProvider extends AbstractProvider {
8
- readonly mcp = new GeminiMcpProvider();
9
- readonly auth: IProviderAuth = new GeminiProviderAuth();
10
- readonly sessions: IProviderSessions = new GeminiSessionsProvider();
11
-
12
- constructor() {
13
- super('gemini');
14
- }
15
- }
1
+ import { AbstractProvider } from '@/modules/providers/shared/base/abstract.provider.js';
2
+ import { GeminiProviderAuth } from '@/modules/providers/list/gemini/gemini-auth.provider.js';
3
+ import { GeminiMcpProvider } from '@/modules/providers/list/gemini/gemini-mcp.provider.js';
4
+ import { GeminiSessionsProvider } from '@/modules/providers/list/gemini/gemini-sessions.provider.js';
5
+ import type { IProviderAuth, IProviderSessions } from '@/shared/interfaces.js';
6
+
7
+ export class GeminiProvider extends AbstractProvider {
8
+ readonly mcp = new GeminiMcpProvider();
9
+ readonly auth: IProviderAuth = new GeminiProviderAuth();
10
+ readonly sessions: IProviderSessions = new GeminiSessionsProvider();
11
+
12
+ constructor() {
13
+ super('gemini');
14
+ }
15
+ }
@@ -0,0 +1,145 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+
5
+ import spawn from 'cross-spawn';
6
+
7
+ import type { IProviderAuth } from '@/shared/interfaces.js';
8
+ import type { ProviderAuthStatus } from '@/shared/types.js';
9
+ import { readObjectRecord, readOptionalString } from '@/shared/utils.js';
10
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
11
+ // @ts-ignore — plain-JS module, typed via inference
12
+ import { getProviderCredentials } from '@/services/provider-credentials.js';
13
+
14
+ type QwenCredentialsStatus = {
15
+ authenticated: boolean;
16
+ email: string | null;
17
+ method: string | null;
18
+ error?: string;
19
+ };
20
+
21
+ /**
22
+ * Qwen Code auth checker.
23
+ *
24
+ * Qwen supports three credential surfaces — in priority order:
25
+ * 1. `QWEN_API_KEY` env var (classic API key)
26
+ * 2. `OPENAI_API_KEY` + `OPENAI_BASE_URL` env vars (OpenAI-compatible BYOK
27
+ * — what the docs recommend for ModelScope, OpenRouter, and self-hosted)
28
+ * 3. Local OAuth tokens at `~/.qwen/oauth_creds.json` (Qwen OAuth /
29
+ * Alibaba Cloud Coding Plan cached credentials)
30
+ * 4. `~/.qwen/settings.json` with an embedded `auth.method` + API key
31
+ */
32
+ export class QwenProviderAuth implements IProviderAuth {
33
+ private checkInstalled(): boolean {
34
+ const cliPath = process.env.QWEN_PATH || 'qwen';
35
+ try {
36
+ const result = spawn.sync(cliPath, ['--version'], { stdio: 'ignore', timeout: 5000 });
37
+ return !result.error && result.status === 0;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ async getStatus(): Promise<ProviderAuthStatus> {
44
+ const installed = this.checkInstalled();
45
+
46
+ if (!installed) {
47
+ return {
48
+ installed,
49
+ provider: 'qwen',
50
+ authenticated: false,
51
+ email: null,
52
+ method: null,
53
+ error: 'Qwen Code CLI is not installed',
54
+ };
55
+ }
56
+
57
+ const credentials = await this.checkCredentials();
58
+
59
+ return {
60
+ installed,
61
+ provider: 'qwen',
62
+ authenticated: credentials.authenticated,
63
+ email: credentials.email,
64
+ method: credentials.method,
65
+ error: credentials.authenticated ? undefined : credentials.error || 'Not authenticated',
66
+ };
67
+ }
68
+
69
+ private async checkCredentials(): Promise<QwenCredentialsStatus> {
70
+ // Pixcode-managed credentials come first. If the user saved an API key
71
+ // through our Settings > Agents > API Key form, we trust that no
72
+ // matter what the environment or ~/.qwen/* files say. Base URL is
73
+ // optional — some users point at Alibaba's default endpoint.
74
+ try {
75
+ const creds = await getProviderCredentials('qwen');
76
+ if (creds?.apiKey) {
77
+ const label = creds.baseUrl
78
+ ? `API Key · ${(() => { try { return new URL(creds.baseUrl).host; } catch { return creds.baseUrl; } })()}`
79
+ : 'API Key Auth';
80
+ return { authenticated: true, email: label, method: 'pixcode_store' };
81
+ }
82
+ } catch { /* fall through */ }
83
+
84
+ if (process.env.QWEN_API_KEY?.trim()) {
85
+ return { authenticated: true, email: 'API Key Auth', method: 'api_key' };
86
+ }
87
+ // Loosened: base URL is optional. Qwen Code also works with the default
88
+ // Alibaba endpoint baked into the CLI. Previously required both env vars
89
+ // and silently failed when the user only pasted an API key.
90
+ if (process.env.OPENAI_API_KEY?.trim()) {
91
+ return { authenticated: true, email: 'OpenAI-Compatible Auth', method: 'api_key' };
92
+ }
93
+
94
+ // OAuth credentials file — written by `/auth` interactive flow.
95
+ try {
96
+ const credsPath = path.join(os.homedir(), '.qwen', 'oauth_creds.json');
97
+ const content = await readFile(credsPath, 'utf8');
98
+ const creds = readObjectRecord(JSON.parse(content)) ?? {};
99
+ const accessToken = readOptionalString(creds.access_token);
100
+ if (accessToken) {
101
+ const email = readOptionalString(creds.email)
102
+ ?? readOptionalString(creds.user_email)
103
+ ?? 'OAuth Session';
104
+ return { authenticated: true, email, method: 'credentials_file' };
105
+ }
106
+ } catch { /* fall through to settings.json */ }
107
+
108
+ // settings.json — the API-key / OpenAI-compat authoring path.
109
+ try {
110
+ const settingsPath = path.join(os.homedir(), '.qwen', 'settings.json');
111
+ const content = await readFile(settingsPath, 'utf8');
112
+ const settings = readObjectRecord(JSON.parse(content)) ?? {};
113
+ const auth = readObjectRecord(settings.auth) ?? {};
114
+ const method = readOptionalString(auth.method) ?? readOptionalString(settings.authMethod);
115
+ const apiKey = readOptionalString(auth.apiKey) ?? readOptionalString(settings.apiKey);
116
+
117
+ if (apiKey) {
118
+ return {
119
+ authenticated: true,
120
+ email: method ? `${method} (settings.json)` : 'API Key Auth',
121
+ method: 'settings_file',
122
+ };
123
+ }
124
+
125
+ if (method && method.toLowerCase().includes('oauth')) {
126
+ // Settings file says OAuth is configured but the creds file isn't
127
+ // readable — treat as a soft "not authenticated yet" rather than an
128
+ // install error, because the browser flow may be mid-flight.
129
+ return {
130
+ authenticated: false,
131
+ email: null,
132
+ method: 'credentials_file',
133
+ error: 'OAuth not yet complete — run `qwen` and finish `/auth`.',
134
+ };
135
+ }
136
+ } catch { /* no settings yet */ }
137
+
138
+ return {
139
+ authenticated: false,
140
+ email: null,
141
+ method: null,
142
+ error: 'Qwen Code is not configured',
143
+ };
144
+ }
145
+ }