@kaitranntt/ccs 7.65.2 → 7.65.3-dev.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.
Files changed (110) hide show
  1. package/README.md +33 -0
  2. package/dist/api/services/profile-lifecycle-service.d.ts.map +1 -1
  3. package/dist/api/services/profile-lifecycle-service.js +4 -0
  4. package/dist/api/services/profile-lifecycle-service.js.map +1 -1
  5. package/dist/api/services/profile-writer.d.ts.map +1 -1
  6. package/dist/api/services/profile-writer.js +3 -0
  7. package/dist/api/services/profile-writer.js.map +1 -1
  8. package/dist/ccs.js +32 -3
  9. package/dist/ccs.js.map +1 -1
  10. package/dist/cliproxy/executor/env-resolver.d.ts +3 -0
  11. package/dist/cliproxy/executor/env-resolver.d.ts.map +1 -1
  12. package/dist/cliproxy/executor/env-resolver.js +19 -1
  13. package/dist/cliproxy/executor/env-resolver.js.map +1 -1
  14. package/dist/cliproxy/executor/index.d.ts.map +1 -1
  15. package/dist/cliproxy/executor/index.js +24 -5
  16. package/dist/cliproxy/executor/index.js.map +1 -1
  17. package/dist/cliproxy/services/variant-settings.d.ts.map +1 -1
  18. package/dist/cliproxy/services/variant-settings.js +11 -0
  19. package/dist/cliproxy/services/variant-settings.js.map +1 -1
  20. package/dist/commands/help-command.js +4 -4
  21. package/dist/commands/help-command.js.map +1 -1
  22. package/dist/commands/install-command.d.ts.map +1 -1
  23. package/dist/commands/install-command.js +16 -3
  24. package/dist/commands/install-command.js.map +1 -1
  25. package/dist/copilot/copilot-executor.d.ts +2 -0
  26. package/dist/copilot/copilot-executor.d.ts.map +1 -1
  27. package/dist/copilot/copilot-executor.js +36 -4
  28. package/dist/copilot/copilot-executor.js.map +1 -1
  29. package/dist/delegation/headless-executor.d.ts.map +1 -1
  30. package/dist/delegation/headless-executor.js +79 -2
  31. package/dist/delegation/headless-executor.js.map +1 -1
  32. package/dist/management/checks/image-analysis-check.d.ts.map +1 -1
  33. package/dist/management/checks/image-analysis-check.js +4 -5
  34. package/dist/management/checks/image-analysis-check.js.map +1 -1
  35. package/dist/management/instance-manager.js +1 -1
  36. package/dist/management/instance-manager.js.map +1 -1
  37. package/dist/ui/assets/{accounts-BHEYnq6b.js → accounts-Dh95PibK.js} +1 -1
  38. package/dist/ui/assets/{alert-dialog-D0EFRcfB.js → alert-dialog-C5RdUHi9.js} +1 -1
  39. package/dist/ui/assets/{api-DhM3BYXr.js → api-C0ROFLme.js} +1 -1
  40. package/dist/ui/assets/{auth-section-DVp8FQGm.js → auth-section-M2azTP3G.js} +1 -1
  41. package/dist/ui/assets/{backups-section-CRo0NZkA.js → backups-section-DIDUVa0t.js} +1 -1
  42. package/dist/ui/assets/{channels-uZ_9CBqO.js → channels-D_5uerEp.js} +1 -1
  43. package/dist/ui/assets/{checkbox-32DNqW_Q.js → checkbox-CgMg7fDH.js} +1 -1
  44. package/dist/ui/assets/{claude-extension-BfXlz5gV.js → claude-extension-DA9wMzPz.js} +1 -1
  45. package/dist/ui/assets/{cliproxy-DjNY9H-U.js → cliproxy-4yUL1fQw.js} +1 -1
  46. package/dist/ui/assets/{cliproxy-ai-providers-5SHLMHiy.js → cliproxy-ai-providers-DedMcdcc.js} +1 -1
  47. package/dist/ui/assets/{cliproxy-control-panel-Zax_m1AC.js → cliproxy-control-panel-B0kwxgNi.js} +1 -1
  48. package/dist/ui/assets/{codex-CRUSpjsu.js → codex-CAWw4ZNl.js} +1 -1
  49. package/dist/ui/assets/{confirm-dialog-DVf5ZmCZ.js → confirm-dialog-Ds0PYz2R.js} +1 -1
  50. package/dist/ui/assets/{copilot-BZrihl_Z.js → copilot-m6i00mFy.js} +1 -1
  51. package/dist/ui/assets/{cursor-BP4nbEk_.js → cursor-COeD0Dgq.js} +1 -1
  52. package/dist/ui/assets/{droid-BG92rdM2.js → droid-CznUyiRx.js} +1 -1
  53. package/dist/ui/assets/{globalenv-section-Cf6dKgSf.js → globalenv-section-FgK1eGWk.js} +1 -1
  54. package/dist/ui/assets/{health-BTy1UZs3.js → health-Cpu6bD6K.js} +1 -1
  55. package/dist/ui/assets/{index-DuRYaONg.js → index-Bhz6T039.js} +1 -1
  56. package/dist/ui/assets/{index-N2ZSJurX.js → index-C7sG68Mi.js} +1 -1
  57. package/dist/ui/assets/index-CcKb4PL_.js +69 -0
  58. package/dist/ui/assets/{index-wg7UtkFv.js → index-DampXntj.js} +1 -1
  59. package/dist/ui/assets/{index-BVeN0dIB.js → index-DgnxlKNk.js} +1 -1
  60. package/dist/ui/assets/{index-DHrTq-0n.js → index-rTSyskt3.js} +1 -1
  61. package/dist/ui/assets/{masked-input-DX9bedLy.js → masked-input-B_l4FMkE.js} +1 -1
  62. package/dist/ui/assets/{proxy-status-widget-DVDMuZK5.js → proxy-status-widget-C7wSbfPC.js} +1 -1
  63. package/dist/ui/assets/{raw-json-settings-editor-panel-Dkt5E6Z_.js → raw-json-settings-editor-panel-CViWFt6t.js} +1 -1
  64. package/dist/ui/assets/{searchable-select-BP3Q1-Yn.js → searchable-select-7-yJbbw2.js} +1 -1
  65. package/dist/ui/assets/{separator-BLGGUlh9.js → separator-DApM4Wa5.js} +1 -1
  66. package/dist/ui/assets/{shared-G0XRyLig.js → shared-Blmm7sMd.js} +1 -1
  67. package/dist/ui/assets/{table-B4lRrWC-.js → table-BwM4zncv.js} +1 -1
  68. package/dist/ui/assets/{updates--A2Sdo7N.js → updates-DJ0ofB67.js} +1 -1
  69. package/dist/ui/index.html +1 -1
  70. package/dist/utils/hooks/get-image-analysis-hook-env.d.ts +26 -0
  71. package/dist/utils/hooks/get-image-analysis-hook-env.d.ts.map +1 -1
  72. package/dist/utils/hooks/get-image-analysis-hook-env.js +79 -1
  73. package/dist/utils/hooks/get-image-analysis-hook-env.js.map +1 -1
  74. package/dist/utils/hooks/image-analysis-backend-resolver.js +5 -5
  75. package/dist/utils/hooks/image-analysis-backend-resolver.js.map +1 -1
  76. package/dist/utils/hooks/image-analysis-runtime-status.d.ts +2 -0
  77. package/dist/utils/hooks/image-analysis-runtime-status.d.ts.map +1 -1
  78. package/dist/utils/hooks/image-analysis-runtime-status.js +15 -11
  79. package/dist/utils/hooks/image-analysis-runtime-status.js.map +1 -1
  80. package/dist/utils/hooks/image-analyzer-hook-installer.d.ts.map +1 -1
  81. package/dist/utils/hooks/image-analyzer-hook-installer.js +60 -27
  82. package/dist/utils/hooks/image-analyzer-hook-installer.js.map +1 -1
  83. package/dist/utils/hooks/image-analyzer-profile-hook-injector.d.ts.map +1 -1
  84. package/dist/utils/hooks/image-analyzer-profile-hook-injector.js +3 -0
  85. package/dist/utils/hooks/image-analyzer-profile-hook-injector.js.map +1 -1
  86. package/dist/utils/hooks/index.d.ts +2 -1
  87. package/dist/utils/hooks/index.d.ts.map +1 -1
  88. package/dist/utils/hooks/index.js +14 -7
  89. package/dist/utils/hooks/index.js.map +1 -1
  90. package/dist/utils/image-analysis/claude-tool-args.d.ts +6 -0
  91. package/dist/utils/image-analysis/claude-tool-args.d.ts.map +1 -0
  92. package/dist/utils/image-analysis/claude-tool-args.js +65 -0
  93. package/dist/utils/image-analysis/claude-tool-args.js.map +1 -0
  94. package/dist/utils/image-analysis/index.d.ts +4 -0
  95. package/dist/utils/image-analysis/index.d.ts.map +1 -1
  96. package/dist/utils/image-analysis/index.js +20 -1
  97. package/dist/utils/image-analysis/index.js.map +1 -1
  98. package/dist/utils/image-analysis/mcp-installer.d.ts +18 -0
  99. package/dist/utils/image-analysis/mcp-installer.d.ts.map +1 -0
  100. package/dist/utils/image-analysis/mcp-installer.js +447 -0
  101. package/dist/utils/image-analysis/mcp-installer.js.map +1 -0
  102. package/dist/web-server/routes/image-analysis-routes.d.ts.map +1 -1
  103. package/dist/web-server/routes/image-analysis-routes.js +30 -5
  104. package/dist/web-server/routes/image-analysis-routes.js.map +1 -1
  105. package/lib/hooks/image-analysis-runtime.cjs +469 -0
  106. package/lib/hooks/image-analyzer-transformer.cjs +27 -418
  107. package/lib/mcp/ccs-image-analysis-server.cjs +440 -0
  108. package/package.json +1 -1
  109. package/scripts/github/normalize-ai-review-output.mjs +95 -33
  110. package/dist/ui/assets/index-Corv1lSo.js +0 -69
@@ -0,0 +1,469 @@
1
+ const fs = require('fs');
2
+ const http = require('http');
3
+ const https = require('https');
4
+ const path = require('path');
5
+
6
+ const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.bmp', '.tiff'];
7
+ const PDF_EXTENSIONS = ['.pdf'];
8
+ const DEFAULT_MODEL = 'gemini-2.5-flash';
9
+ const DEFAULT_TIMEOUT_SEC = 60;
10
+ const MAX_FILE_SIZE_MB = 10;
11
+ const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
12
+ const MAX_PROMPT_TEMPLATE_BYTES = 32 * 1024;
13
+ const SCREENSHOT_NAME_REGEX =
14
+ /(screen[-_ ]?shot|screen[-_ ]?capture|screencap|snapshot|snip|clip|capture)/i;
15
+ const TEMPLATE_FILE_NAMES = {
16
+ default: 'default.txt',
17
+ screenshot: 'screenshot.txt',
18
+ document: 'document.txt',
19
+ };
20
+ const FALLBACK_PROMPTS = {
21
+ default: `Analyze this image/document thoroughly and provide a detailed description.
22
+
23
+ Include:
24
+ 1. Overall content and purpose
25
+ 2. Text content (if any) - transcribe important text verbatim
26
+ 3. Visual elements (diagrams, charts, UI components, icons)
27
+ 4. Layout and structure (sections, hierarchy, flow)
28
+ 5. Colors, styling, notable design elements
29
+ 6. Any actionable information (buttons, links, code snippets)
30
+
31
+ Be comprehensive - this description replaces direct visual access.
32
+ The AI assistant reading this cannot see the original image.`,
33
+ screenshot: `Analyze this screenshot in detail for a developer who cannot see it.
34
+
35
+ Focus on:
36
+ 1. Application/website type and state
37
+ 2. UI elements visible (buttons, inputs, menus, modals)
38
+ 3. All text content - transcribe exactly
39
+ 4. Error messages or notifications (quote exactly)
40
+ 5. Layout and component hierarchy
41
+ 6. Interactive elements and their states
42
+ 7. Console output or logs if visible
43
+ 8. Any code snippets shown
44
+
45
+ Be precise - this enables the assistant to help debug or understand the UI.`,
46
+ document: `Analyze this document/PDF thoroughly for a developer.
47
+
48
+ Extract and provide:
49
+ 1. Document title, type, and structure
50
+ 2. All text content - transcribe in reading order
51
+ 3. Tables - format as markdown tables
52
+ 4. Lists and bullet points - preserve structure
53
+ 5. Code blocks or technical content
54
+ 6. Diagrams or flowcharts - describe in detail
55
+ 7. Headers and section organization
56
+ 8. Any important metadata visible
57
+
58
+ Accuracy in text extraction is critical.`,
59
+ };
60
+
61
+ function debugLog(message, data = {}) {
62
+ if (!process.env.CCS_DEBUG) return;
63
+
64
+ const lines = [`[CCS Hook] ${message}`];
65
+ for (const [key, value] of Object.entries(data)) {
66
+ if (value !== undefined && value !== null && value !== '') {
67
+ lines.push(` ${key}: ${value}`);
68
+ }
69
+ }
70
+ console.error(lines.join('\n'));
71
+ }
72
+
73
+ function parseProviderModels(envValue) {
74
+ if (!envValue) return {};
75
+ return envValue.split(',').reduce((acc, pair) => {
76
+ const [provider, ...modelParts] = pair.split(':');
77
+ const model = modelParts.join(':').trim();
78
+ if (provider && model) {
79
+ acc[provider.trim()] = model;
80
+ }
81
+ return acc;
82
+ }, {});
83
+ }
84
+
85
+ function normalizeTemplateName(value) {
86
+ if (typeof value !== 'string') return null;
87
+ const normalized = value.trim().toLowerCase();
88
+ return Object.prototype.hasOwnProperty.call(TEMPLATE_FILE_NAMES, normalized) ? normalized : null;
89
+ }
90
+
91
+ function selectPromptTemplate(filePath, requestedTemplate) {
92
+ const explicitTemplate = normalizeTemplateName(requestedTemplate);
93
+ if (explicitTemplate) {
94
+ return explicitTemplate;
95
+ }
96
+
97
+ const extension = path.extname(filePath).toLowerCase();
98
+ if (PDF_EXTENSIONS.includes(extension)) {
99
+ return 'document';
100
+ }
101
+
102
+ return SCREENSHOT_NAME_REGEX.test(path.basename(filePath)) ? 'screenshot' : 'default';
103
+ }
104
+
105
+ function readPromptFile(filePath) {
106
+ try {
107
+ const stats = fs.statSync(filePath);
108
+ if (stats.size > MAX_PROMPT_TEMPLATE_BYTES) {
109
+ return null;
110
+ }
111
+ const content = fs.readFileSync(filePath, 'utf8').trim();
112
+ return content.length > 0 ? content : null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ function loadPromptTemplate(filePath, requestedTemplate, focus) {
119
+ const template = selectPromptTemplate(filePath, requestedTemplate);
120
+ const promptsDir = process.env.CCS_IMAGE_ANALYSIS_PROMPTS_DIR || '';
121
+ const promptPath = promptsDir
122
+ ? path.join(promptsDir, TEMPLATE_FILE_NAMES[template])
123
+ : null;
124
+ const promptText = (promptPath && readPromptFile(promptPath)) || FALLBACK_PROMPTS[template];
125
+
126
+ if (!focus || !focus.trim()) {
127
+ return {
128
+ template,
129
+ promptSource: promptPath ? 'installed-or-fallback' : 'bundled-fallback',
130
+ prompt: promptText,
131
+ };
132
+ }
133
+
134
+ return {
135
+ template,
136
+ promptSource: promptPath ? 'installed-or-fallback' : 'bundled-fallback',
137
+ prompt: `${promptText}\n\nSpecific focus:\n${focus.trim()}`,
138
+ };
139
+ }
140
+
141
+ function getCurrentProvider() {
142
+ return process.env.CCS_CURRENT_PROVIDER || '';
143
+ }
144
+
145
+ function getConfiguredModel() {
146
+ const explicitModel = process.env.CCS_IMAGE_ANALYSIS_MODEL;
147
+ if (explicitModel && explicitModel.trim()) {
148
+ return explicitModel.trim();
149
+ }
150
+
151
+ const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
152
+ return providerModels[getCurrentProvider()] || DEFAULT_MODEL;
153
+ }
154
+
155
+ function getModelsToTry() {
156
+ const models = [];
157
+ const seen = new Set();
158
+
159
+ const explicitModel = process.env.CCS_IMAGE_ANALYSIS_MODEL;
160
+ if (explicitModel && explicitModel.trim()) {
161
+ models.push(explicitModel.trim());
162
+ seen.add(explicitModel.trim());
163
+ }
164
+
165
+ const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
166
+ const providerModel = providerModels[getCurrentProvider()];
167
+ if (providerModel && !seen.has(providerModel)) {
168
+ models.push(providerModel);
169
+ seen.add(providerModel);
170
+ }
171
+
172
+ if (models.length === 0) {
173
+ models.push(DEFAULT_MODEL);
174
+ }
175
+
176
+ return models;
177
+ }
178
+
179
+ function getRuntimeBaseUrl() {
180
+ const runtimePath = (process.env.CCS_IMAGE_ANALYSIS_RUNTIME_PATH || '')
181
+ .trim()
182
+ .replace(/\/+$/, '');
183
+ const explicitBaseUrl = process.env.CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL;
184
+ if (explicitBaseUrl && explicitBaseUrl.trim()) {
185
+ const normalizedBaseUrl = explicitBaseUrl.trim().replace(/\/+$/, '');
186
+ if (!runtimePath) {
187
+ return normalizedBaseUrl;
188
+ }
189
+
190
+ try {
191
+ const parsed = new URL(normalizedBaseUrl);
192
+ const normalizedPath = parsed.pathname.replace(/\/+$/, '');
193
+ if (normalizedPath === runtimePath) {
194
+ return normalizedBaseUrl;
195
+ }
196
+
197
+ parsed.pathname = runtimePath;
198
+ return parsed.toString().replace(/\/+$/, '');
199
+ } catch {
200
+ return `${normalizedBaseUrl}${runtimePath}`;
201
+ }
202
+ }
203
+
204
+ const port = Number.parseInt(process.env.CCS_CLIPROXY_PORT || '8317', 10);
205
+ return `http://127.0.0.1:${port}${runtimePath}`;
206
+ }
207
+
208
+ function getRuntimeEndpoint() {
209
+ return `${getRuntimeBaseUrl()}/v1/messages`;
210
+ }
211
+
212
+ function getApiKey() {
213
+ if (Object.prototype.hasOwnProperty.call(process.env, 'CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY')) {
214
+ const explicitApiKey = (process.env.CCS_IMAGE_ANALYSIS_RUNTIME_API_KEY || '').trim();
215
+ return explicitApiKey || 'ccs-internal-managed';
216
+ }
217
+
218
+ return process.env.CCS_CLIPROXY_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || 'ccs-internal-managed';
219
+ }
220
+
221
+ function shouldAllowSelfSigned() {
222
+ const value = `${process.env.CCS_IMAGE_ANALYSIS_RUNTIME_ALLOW_SELF_SIGNED || ''}`.trim().toLowerCase();
223
+ return value === '1' || value === 'true' || value === 'yes';
224
+ }
225
+
226
+ function getTimeoutMs(timeoutMs) {
227
+ if (typeof timeoutMs === 'number' && timeoutMs > 0) {
228
+ return timeoutMs;
229
+ }
230
+
231
+ const timeoutSec = Number.parseInt(
232
+ process.env.CCS_IMAGE_ANALYSIS_TIMEOUT || `${DEFAULT_TIMEOUT_SEC}`,
233
+ 10
234
+ );
235
+ return Math.max(1, Math.min(600, timeoutSec)) * 1000;
236
+ }
237
+
238
+ function isAnalyzableFile(filePath) {
239
+ const ext = path.extname(filePath).toLowerCase();
240
+ return IMAGE_EXTENSIONS.includes(ext) || PDF_EXTENSIONS.includes(ext);
241
+ }
242
+
243
+ function getMediaType(filePath) {
244
+ const ext = path.extname(filePath).toLowerCase();
245
+ return (
246
+ {
247
+ '.jpg': 'image/jpeg',
248
+ '.jpeg': 'image/jpeg',
249
+ '.png': 'image/png',
250
+ '.gif': 'image/gif',
251
+ '.webp': 'image/webp',
252
+ '.heic': 'image/heic',
253
+ '.bmp': 'image/bmp',
254
+ '.tiff': 'image/tiff',
255
+ '.pdf': 'application/pdf',
256
+ }[ext] || 'application/octet-stream'
257
+ );
258
+ }
259
+
260
+ function encodeFileToBase64(filePath) {
261
+ return fs.readFileSync(filePath).toString('base64');
262
+ }
263
+
264
+ function buildContentBlock(base64Data, mediaType) {
265
+ const source = {
266
+ type: 'base64',
267
+ media_type: mediaType,
268
+ data: base64Data,
269
+ };
270
+
271
+ if (mediaType === 'application/pdf') {
272
+ return {
273
+ type: 'document',
274
+ source,
275
+ };
276
+ }
277
+
278
+ return {
279
+ type: 'image',
280
+ source,
281
+ };
282
+ }
283
+
284
+ function extractTextContent(response) {
285
+ if (!response || !Array.isArray(response.content)) {
286
+ return null;
287
+ }
288
+
289
+ const textBlocks = response.content
290
+ .filter((block) => block && block.type === 'text' && typeof block.text === 'string')
291
+ .map((block) => block.text)
292
+ .filter((text) => text.trim());
293
+
294
+ return textBlocks.length > 0 ? textBlocks.join('\n\n') : null;
295
+ }
296
+
297
+ function parseCliProxyResponse(data) {
298
+ const response = JSON.parse(data);
299
+ const text = extractTextContent(response);
300
+ if (!text) {
301
+ throw new Error('No text content in response');
302
+ }
303
+ return text;
304
+ }
305
+
306
+ function analyzeViaCliProxy(base64Data, mediaType, model, prompt, timeoutMs) {
307
+ return new Promise((resolve, reject) => {
308
+ const endpoint = new URL(getRuntimeEndpoint());
309
+ const transport = endpoint.protocol === 'https:' ? https : http;
310
+ const apiKey = getApiKey();
311
+ const requestBody = JSON.stringify({
312
+ model,
313
+ max_tokens: 4096,
314
+ messages: [
315
+ {
316
+ role: 'user',
317
+ content: [
318
+ { type: 'text', text: prompt },
319
+ buildContentBlock(base64Data, mediaType),
320
+ ],
321
+ },
322
+ ],
323
+ });
324
+
325
+ const req = transport.request(
326
+ {
327
+ protocol: endpoint.protocol,
328
+ hostname: endpoint.hostname,
329
+ port: endpoint.port,
330
+ path: `${endpoint.pathname}${endpoint.search}`,
331
+ method: 'POST',
332
+ headers: {
333
+ 'Content-Type': 'application/json',
334
+ 'Content-Length': Buffer.byteLength(requestBody),
335
+ 'x-api-key': apiKey,
336
+ Authorization: `Bearer ${apiKey}`,
337
+ },
338
+ timeout: timeoutMs,
339
+ ...(endpoint.protocol === 'https:' && shouldAllowSelfSigned()
340
+ ? { rejectUnauthorized: false }
341
+ : {}),
342
+ },
343
+ (res) => {
344
+ let data = '';
345
+
346
+ res.on('data', (chunk) => {
347
+ data += chunk;
348
+ });
349
+
350
+ res.on('end', () => {
351
+ if (res.statusCode === 401 || res.statusCode === 403) {
352
+ reject(new Error(`AUTH_ERROR:${res.statusCode}`));
353
+ return;
354
+ }
355
+
356
+ if (res.statusCode === 429) {
357
+ reject(new Error(`RATE_LIMIT:${res.headers['retry-after'] || ''}`));
358
+ return;
359
+ }
360
+
361
+ if (res.statusCode !== 200) {
362
+ reject(new Error(`API_ERROR:${res.statusCode}:${data}`));
363
+ return;
364
+ }
365
+
366
+ try {
367
+ resolve(parseCliProxyResponse(data));
368
+ } catch (error) {
369
+ reject(error);
370
+ }
371
+ });
372
+ }
373
+ );
374
+
375
+ req.on('error', (error) => reject(error));
376
+ req.on('timeout', () => {
377
+ req.destroy();
378
+ reject(new Error('TIMEOUT'));
379
+ });
380
+ req.write(requestBody);
381
+ req.end();
382
+ });
383
+ }
384
+
385
+ async function analyzeWithRetry(base64Data, mediaType, prompt, timeoutMs) {
386
+ const models = getModelsToTry();
387
+ let lastError = null;
388
+
389
+ for (const [index, model] of models.entries()) {
390
+ try {
391
+ debugLog(`Trying model ${index + 1}/${models.length}`, { model });
392
+ const description = await analyzeViaCliProxy(base64Data, mediaType, model, prompt, timeoutMs);
393
+ return { description, model };
394
+ } catch (error) {
395
+ lastError = error;
396
+ const message = error.message || '';
397
+ if (
398
+ index === models.length - 1 ||
399
+ ['AUTH_ERROR', 'RATE_LIMIT', 'TIMEOUT', 'EACCES', 'EPERM', 'ECONNREFUSED'].some((token) =>
400
+ message.includes(token)
401
+ )
402
+ ) {
403
+ throw error;
404
+ }
405
+ }
406
+ }
407
+
408
+ throw lastError || new Error('No models configured for image analysis');
409
+ }
410
+
411
+ async function analyzeFile(filePath, options = {}) {
412
+ const stats = fs.statSync(filePath);
413
+ if (stats.size >= MAX_FILE_SIZE_BYTES) {
414
+ throw new Error(`FILE_TOO_LARGE:${stats.size}`);
415
+ }
416
+
417
+ const timeoutMs = getTimeoutMs(options.timeoutMs);
418
+ const { template, prompt, promptSource } = loadPromptTemplate(
419
+ filePath,
420
+ options.template,
421
+ options.focus
422
+ );
423
+ const model = getConfiguredModel();
424
+
425
+ debugLog('Starting image analysis', {
426
+ file: path.basename(filePath),
427
+ size: `${(stats.size / 1024).toFixed(1)} KB`,
428
+ provider: getCurrentProvider() || 'unknown',
429
+ model,
430
+ modelsToTry: getModelsToTry().join(' -> '),
431
+ timeout: `${timeoutMs / 1000}s`,
432
+ endpoint: getRuntimeEndpoint(),
433
+ template,
434
+ promptSource,
435
+ });
436
+
437
+ const base64Data = encodeFileToBase64(filePath);
438
+ const mediaType = getMediaType(filePath);
439
+ debugLog('File encoded', {
440
+ mediaType,
441
+ base64Length: `${(base64Data.length / 1024).toFixed(1)}KB`,
442
+ });
443
+
444
+ const result = await analyzeWithRetry(base64Data, mediaType, prompt, timeoutMs);
445
+ debugLog('Analysis complete', {
446
+ responseLength: `${result.description.length} chars`,
447
+ model: result.model,
448
+ template,
449
+ });
450
+
451
+ return {
452
+ description: result.description,
453
+ model: result.model,
454
+ fileSize: stats.size,
455
+ mediaType,
456
+ template,
457
+ };
458
+ }
459
+
460
+ module.exports = {
461
+ DEFAULT_MODEL,
462
+ DEFAULT_TIMEOUT_SEC,
463
+ MAX_FILE_SIZE_BYTES,
464
+ analyzeFile,
465
+ getRuntimeEndpoint,
466
+ isAnalyzableFile,
467
+ parseProviderModels,
468
+ selectPromptTemplate,
469
+ };