@kaitranntt/ccs 7.65.3 → 7.66.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 (226) hide show
  1. package/README.md +88 -750
  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/auth/auth-commands.d.ts +1 -0
  9. package/dist/auth/auth-commands.d.ts.map +1 -1
  10. package/dist/auth/auth-commands.js +11 -0
  11. package/dist/auth/auth-commands.js.map +1 -1
  12. package/dist/auth/commands/backup-command.d.ts +3 -0
  13. package/dist/auth/commands/backup-command.d.ts.map +1 -0
  14. package/dist/auth/commands/backup-command.js +126 -0
  15. package/dist/auth/commands/backup-command.js.map +1 -0
  16. package/dist/auth/commands/index.d.ts +1 -0
  17. package/dist/auth/commands/index.d.ts.map +1 -1
  18. package/dist/auth/commands/index.js +3 -1
  19. package/dist/auth/commands/index.js.map +1 -1
  20. package/dist/auth/profile-continuity-inheritance.d.ts +1 -0
  21. package/dist/auth/profile-continuity-inheritance.d.ts.map +1 -1
  22. package/dist/auth/profile-continuity-inheritance.js +10 -6
  23. package/dist/auth/profile-continuity-inheritance.js.map +1 -1
  24. package/dist/auth/profile-detector.d.ts +9 -1
  25. package/dist/auth/profile-detector.d.ts.map +1 -1
  26. package/dist/auth/profile-detector.js +35 -0
  27. package/dist/auth/profile-detector.js.map +1 -1
  28. package/dist/auth/resume-lane-diagnostics.d.ts +21 -0
  29. package/dist/auth/resume-lane-diagnostics.d.ts.map +1 -0
  30. package/dist/auth/resume-lane-diagnostics.js +146 -0
  31. package/dist/auth/resume-lane-diagnostics.js.map +1 -0
  32. package/dist/auth/resume-lane-warning.d.ts +9 -0
  33. package/dist/auth/resume-lane-warning.d.ts.map +1 -0
  34. package/dist/auth/resume-lane-warning.js +60 -0
  35. package/dist/auth/resume-lane-warning.js.map +1 -0
  36. package/dist/ccs.js +79 -7
  37. package/dist/ccs.js.map +1 -1
  38. package/dist/cliproxy/executor/env-resolver.d.ts +3 -0
  39. package/dist/cliproxy/executor/env-resolver.d.ts.map +1 -1
  40. package/dist/cliproxy/executor/env-resolver.js +19 -1
  41. package/dist/cliproxy/executor/env-resolver.js.map +1 -1
  42. package/dist/cliproxy/executor/index.d.ts.map +1 -1
  43. package/dist/cliproxy/executor/index.js +24 -5
  44. package/dist/cliproxy/executor/index.js.map +1 -1
  45. package/dist/cliproxy/gemini-cli-quota-normalizer.d.ts +10 -0
  46. package/dist/cliproxy/gemini-cli-quota-normalizer.d.ts.map +1 -0
  47. package/dist/cliproxy/gemini-cli-quota-normalizer.js +122 -0
  48. package/dist/cliproxy/gemini-cli-quota-normalizer.js.map +1 -0
  49. package/dist/cliproxy/quota-fetcher-gemini-cli.d.ts.map +1 -1
  50. package/dist/cliproxy/quota-fetcher-gemini-cli.js +133 -92
  51. package/dist/cliproxy/quota-fetcher-gemini-cli.js.map +1 -1
  52. package/dist/cliproxy/quota-types.d.ts +8 -0
  53. package/dist/cliproxy/quota-types.d.ts.map +1 -1
  54. package/dist/cliproxy/services/variant-settings.d.ts.map +1 -1
  55. package/dist/cliproxy/services/variant-settings.js +11 -0
  56. package/dist/cliproxy/services/variant-settings.js.map +1 -1
  57. package/dist/commands/cliproxy/quota-subcommand.d.ts.map +1 -1
  58. package/dist/commands/cliproxy/quota-subcommand.js +10 -1
  59. package/dist/commands/cliproxy/quota-subcommand.js.map +1 -1
  60. package/dist/commands/command-catalog.d.ts +39 -0
  61. package/dist/commands/command-catalog.d.ts.map +1 -0
  62. package/dist/commands/command-catalog.js +298 -0
  63. package/dist/commands/command-catalog.js.map +1 -0
  64. package/dist/commands/completion-backend.d.ts +14 -0
  65. package/dist/commands/completion-backend.d.ts.map +1 -0
  66. package/dist/commands/completion-backend.js +208 -0
  67. package/dist/commands/completion-backend.js.map +1 -0
  68. package/dist/commands/cursor-command-display.d.ts.map +1 -1
  69. package/dist/commands/cursor-command-display.js +25 -5
  70. package/dist/commands/cursor-command-display.js.map +1 -1
  71. package/dist/commands/cursor-command.d.ts +1 -3
  72. package/dist/commands/cursor-command.d.ts.map +1 -1
  73. package/dist/commands/cursor-command.js +3 -15
  74. package/dist/commands/cursor-command.js.map +1 -1
  75. package/dist/commands/help-command.d.ts +4 -3
  76. package/dist/commands/help-command.d.ts.map +1 -1
  77. package/dist/commands/help-command.js +155 -507
  78. package/dist/commands/help-command.js.map +1 -1
  79. package/dist/commands/install-command.d.ts.map +1 -1
  80. package/dist/commands/install-command.js +16 -3
  81. package/dist/commands/install-command.js.map +1 -1
  82. package/dist/commands/root-command-router.d.ts +2 -0
  83. package/dist/commands/root-command-router.d.ts.map +1 -1
  84. package/dist/commands/root-command-router.js +13 -13
  85. package/dist/commands/root-command-router.js.map +1 -1
  86. package/dist/commands/shell-completion-command.d.ts +1 -0
  87. package/dist/commands/shell-completion-command.d.ts.map +1 -1
  88. package/dist/commands/shell-completion-command.js +27 -11
  89. package/dist/commands/shell-completion-command.js.map +1 -1
  90. package/dist/copilot/copilot-executor.d.ts +2 -0
  91. package/dist/copilot/copilot-executor.d.ts.map +1 -1
  92. package/dist/copilot/copilot-executor.js +36 -4
  93. package/dist/copilot/copilot-executor.js.map +1 -1
  94. package/dist/cursor/constants.d.ts +3 -0
  95. package/dist/cursor/constants.d.ts.map +1 -0
  96. package/dist/cursor/constants.js +20 -0
  97. package/dist/cursor/constants.js.map +1 -0
  98. package/dist/cursor/cursor-models.d.ts.map +1 -1
  99. package/dist/cursor/cursor-models.js +2 -0
  100. package/dist/cursor/cursor-models.js.map +1 -1
  101. package/dist/cursor/cursor-profile-executor.d.ts +10 -0
  102. package/dist/cursor/cursor-profile-executor.d.ts.map +1 -0
  103. package/dist/cursor/cursor-profile-executor.js +158 -0
  104. package/dist/cursor/cursor-profile-executor.js.map +1 -0
  105. package/dist/cursor/cursor-translator.d.ts +22 -11
  106. package/dist/cursor/cursor-translator.d.ts.map +1 -1
  107. package/dist/cursor/cursor-translator.js +254 -75
  108. package/dist/cursor/cursor-translator.js.map +1 -1
  109. package/dist/cursor/index.d.ts +1 -0
  110. package/dist/cursor/index.d.ts.map +1 -1
  111. package/dist/cursor/index.js +4 -1
  112. package/dist/cursor/index.js.map +1 -1
  113. package/dist/delegation/headless-executor.d.ts.map +1 -1
  114. package/dist/delegation/headless-executor.js +79 -2
  115. package/dist/delegation/headless-executor.js.map +1 -1
  116. package/dist/management/checks/image-analysis-check.d.ts.map +1 -1
  117. package/dist/management/checks/image-analysis-check.js +4 -5
  118. package/dist/management/checks/image-analysis-check.js.map +1 -1
  119. package/dist/management/instance-manager.js +1 -1
  120. package/dist/management/instance-manager.js.map +1 -1
  121. package/dist/shared/claude-extension-setup.d.ts.map +1 -1
  122. package/dist/shared/claude-extension-setup.js +36 -16
  123. package/dist/shared/claude-extension-setup.js.map +1 -1
  124. package/dist/targets/target-runtime-compatibility.d.ts.map +1 -1
  125. package/dist/targets/target-runtime-compatibility.js +6 -0
  126. package/dist/targets/target-runtime-compatibility.js.map +1 -1
  127. package/dist/types/profile.d.ts +1 -1
  128. package/dist/types/profile.d.ts.map +1 -1
  129. package/dist/ui/assets/accounts-BjfPKR8m.js +1 -0
  130. package/dist/ui/assets/{alert-dialog-D0EFRcfB.js → alert-dialog-Dh2NUFdm.js} +1 -1
  131. package/dist/ui/assets/{api-DhM3BYXr.js → api-C-3mQCFf.js} +1 -1
  132. package/dist/ui/assets/{auth-section-DVp8FQGm.js → auth-section-Dp10_YyD.js} +1 -1
  133. package/dist/ui/assets/{backups-section-CRo0NZkA.js → backups-section-C0jF8MP1.js} +1 -1
  134. package/dist/ui/assets/{channels-uZ_9CBqO.js → channels-CkXuK5i7.js} +1 -1
  135. package/dist/ui/assets/{checkbox-32DNqW_Q.js → checkbox-tA5FH8Ol.js} +1 -1
  136. package/dist/ui/assets/{claude-extension-BfXlz5gV.js → claude-extension-Bg2ZkzMz.js} +1 -1
  137. package/dist/ui/assets/{cliproxy-DjNY9H-U.js → cliproxy-1qRVSbVC.js} +2 -2
  138. package/dist/ui/assets/{cliproxy-ai-providers-5SHLMHiy.js → cliproxy-ai-providers-DBSXTTyw.js} +1 -1
  139. package/dist/ui/assets/cliproxy-control-panel-Da-sGGyI.js +1 -0
  140. package/dist/ui/assets/{codex-CRUSpjsu.js → codex-ooWKOPa2.js} +1 -1
  141. package/dist/ui/assets/{confirm-dialog-DVf5ZmCZ.js → confirm-dialog-CKjwhn9j.js} +1 -1
  142. package/dist/ui/assets/{copilot-BZrihl_Z.js → copilot-GA7EPiK1.js} +1 -1
  143. package/dist/ui/assets/{cursor-BP4nbEk_.js → cursor-B6c8CyHG.js} +1 -1
  144. package/dist/ui/assets/{droid-BG92rdM2.js → droid-CPRHOIX2.js} +1 -1
  145. package/dist/ui/assets/{globalenv-section-Cf6dKgSf.js → globalenv-section-Czgnw_GV.js} +1 -1
  146. package/dist/ui/assets/{health-BTy1UZs3.js → health-CXLOMk8n.js} +1 -1
  147. package/dist/ui/assets/icons-B9oTjo-t.js +1 -0
  148. package/dist/ui/assets/index-BMHPMj0j.js +69 -0
  149. package/dist/ui/assets/{index-BVeN0dIB.js → index-BceMcbCR.js} +1 -1
  150. package/dist/ui/assets/{index-N2ZSJurX.js → index-Boa5e-GY.js} +1 -1
  151. package/dist/ui/assets/index-CknHGRYp.css +1 -0
  152. package/dist/ui/assets/{index-wg7UtkFv.js → index-CvfzKRSH.js} +1 -1
  153. package/dist/ui/assets/{index-DuRYaONg.js → index-D2v_-6AW.js} +1 -1
  154. package/dist/ui/assets/{index-DHrTq-0n.js → index-v-DY6Zby.js} +1 -1
  155. package/dist/ui/assets/{masked-input-DX9bedLy.js → masked-input-B1_asiUI.js} +1 -1
  156. package/dist/ui/assets/{proxy-status-widget-DVDMuZK5.js → proxy-status-widget-Ci1JpStj.js} +1 -1
  157. package/dist/ui/assets/{radix-ui-C98W0NRG.js → radix-ui-Zb8sVEtn.js} +1 -1
  158. package/dist/ui/assets/{raw-json-settings-editor-panel-Dkt5E6Z_.js → raw-json-settings-editor-panel-DMbTkxWw.js} +1 -1
  159. package/dist/ui/assets/{searchable-select-BP3Q1-Yn.js → searchable-select-HbP2PXl3.js} +1 -1
  160. package/dist/ui/assets/{separator-BLGGUlh9.js → separator-CdaalG0K.js} +1 -1
  161. package/dist/ui/assets/{shared-G0XRyLig.js → shared-BiFB-et0.js} +1 -1
  162. package/dist/ui/assets/{table-B4lRrWC-.js → table-BPwgFXLQ.js} +1 -1
  163. package/dist/ui/assets/{tanstack-CfKik0yL.js → tanstack-DWm6aJ-G.js} +1 -1
  164. package/dist/ui/assets/{updates--A2Sdo7N.js → updates-l8Co9uve.js} +1 -1
  165. package/dist/ui/index.html +5 -5
  166. package/dist/utils/config-manager.d.ts +5 -0
  167. package/dist/utils/config-manager.d.ts.map +1 -1
  168. package/dist/utils/config-manager.js +10 -1
  169. package/dist/utils/config-manager.js.map +1 -1
  170. package/dist/utils/hooks/get-image-analysis-hook-env.d.ts +26 -0
  171. package/dist/utils/hooks/get-image-analysis-hook-env.d.ts.map +1 -1
  172. package/dist/utils/hooks/get-image-analysis-hook-env.js +79 -1
  173. package/dist/utils/hooks/get-image-analysis-hook-env.js.map +1 -1
  174. package/dist/utils/hooks/image-analysis-backend-resolver.d.ts.map +1 -1
  175. package/dist/utils/hooks/image-analysis-backend-resolver.js +13 -5
  176. package/dist/utils/hooks/image-analysis-backend-resolver.js.map +1 -1
  177. package/dist/utils/hooks/image-analysis-runtime-status.d.ts +2 -0
  178. package/dist/utils/hooks/image-analysis-runtime-status.d.ts.map +1 -1
  179. package/dist/utils/hooks/image-analysis-runtime-status.js +15 -11
  180. package/dist/utils/hooks/image-analysis-runtime-status.js.map +1 -1
  181. package/dist/utils/hooks/image-analyzer-hook-installer.d.ts.map +1 -1
  182. package/dist/utils/hooks/image-analyzer-hook-installer.js +60 -27
  183. package/dist/utils/hooks/image-analyzer-hook-installer.js.map +1 -1
  184. package/dist/utils/hooks/image-analyzer-profile-hook-injector.d.ts.map +1 -1
  185. package/dist/utils/hooks/image-analyzer-profile-hook-injector.js +3 -0
  186. package/dist/utils/hooks/image-analyzer-profile-hook-injector.js.map +1 -1
  187. package/dist/utils/hooks/index.d.ts +2 -1
  188. package/dist/utils/hooks/index.d.ts.map +1 -1
  189. package/dist/utils/hooks/index.js +14 -7
  190. package/dist/utils/hooks/index.js.map +1 -1
  191. package/dist/utils/image-analysis/claude-tool-args.d.ts +6 -0
  192. package/dist/utils/image-analysis/claude-tool-args.d.ts.map +1 -0
  193. package/dist/utils/image-analysis/claude-tool-args.js +65 -0
  194. package/dist/utils/image-analysis/claude-tool-args.js.map +1 -0
  195. package/dist/utils/image-analysis/index.d.ts +4 -0
  196. package/dist/utils/image-analysis/index.d.ts.map +1 -1
  197. package/dist/utils/image-analysis/index.js +20 -1
  198. package/dist/utils/image-analysis/index.js.map +1 -1
  199. package/dist/utils/image-analysis/mcp-installer.d.ts +18 -0
  200. package/dist/utils/image-analysis/mcp-installer.d.ts.map +1 -0
  201. package/dist/utils/image-analysis/mcp-installer.js +447 -0
  202. package/dist/utils/image-analysis/mcp-installer.js.map +1 -0
  203. package/dist/web-server/routes/account-routes.d.ts.map +1 -1
  204. package/dist/web-server/routes/account-routes.js +14 -2
  205. package/dist/web-server/routes/account-routes.js.map +1 -1
  206. package/dist/web-server/routes/cliproxy-auth-routes.d.ts.map +1 -1
  207. package/dist/web-server/routes/cliproxy-auth-routes.js +80 -6
  208. package/dist/web-server/routes/cliproxy-auth-routes.js.map +1 -1
  209. package/dist/web-server/routes/image-analysis-routes.d.ts.map +1 -1
  210. package/dist/web-server/routes/image-analysis-routes.js +30 -5
  211. package/dist/web-server/routes/image-analysis-routes.js.map +1 -1
  212. package/lib/hooks/image-analysis-runtime.cjs +469 -0
  213. package/lib/hooks/image-analyzer-transformer.cjs +27 -418
  214. package/lib/hooks/websearch-transformer.cjs +37 -2
  215. package/lib/mcp/ccs-image-analysis-server.cjs +440 -0
  216. package/package.json +1 -1
  217. package/scripts/completion/README.md +55 -131
  218. package/scripts/completion/ccs.bash +22 -190
  219. package/scripts/completion/ccs.fish +19 -245
  220. package/scripts/completion/ccs.ps1 +37 -427
  221. package/scripts/completion/ccs.zsh +27 -305
  222. package/dist/ui/assets/accounts-BHEYnq6b.js +0 -1
  223. package/dist/ui/assets/cliproxy-control-panel-Zax_m1AC.js +0 -1
  224. package/dist/ui/assets/icons-CeH5899d.js +0 -1
  225. package/dist/ui/assets/index-B6SrL1O-.css +0 -1
  226. package/dist/ui/assets/index-Corv1lSo.js +0 -69
@@ -24,7 +24,7 @@
24
24
 
25
25
  const fs = require('fs');
26
26
  const path = require('path');
27
- const http = require('http');
27
+ const { analyzeFile, isAnalyzableFile, parseProviderModels } = require('./image-analysis-runtime.cjs');
28
28
 
29
29
  // ============================================================================
30
30
  // PLATFORM DETECTION
@@ -36,19 +36,8 @@ const isWindows = process.platform === 'win32';
36
36
  // CONFIGURATION
37
37
  // ============================================================================
38
38
 
39
- const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.heic', '.bmp', '.tiff'];
40
- const PDF_EXTENSIONS = ['.pdf'];
41
-
42
39
  const DEFAULT_MODEL = 'gemini-2.5-flash';
43
40
  const DEFAULT_TIMEOUT_SEC = 60;
44
- const MAX_FILE_SIZE_MB = 10;
45
- const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
46
-
47
- const CLIPROXY_HOST = '127.0.0.1';
48
- const CLIPROXY_PORT = parseInt(process.env.CCS_CLIPROXY_PORT || '8317', 10);
49
- const CLIPROXY_PATH = '/v1/messages';
50
- // API key passed via env from cliproxy-executor, defaults to CCS internal key
51
- const CLIPROXY_API_KEY = process.env.CCS_CLIPROXY_API_KEY || 'ccs-internal-managed';
52
41
 
53
42
  // ============================================================================
54
43
  // ERROR CODES (for categorization)
@@ -65,23 +54,6 @@ const ERROR_CODES = {
65
54
  UNKNOWN: 'UNKNOWN',
66
55
  };
67
56
 
68
- // Default analysis prompt
69
- const DEFAULT_PROMPT = `Analyze this image/document thoroughly and provide a detailed description.
70
-
71
- Include:
72
- 1. Overall content and purpose
73
- 2. Text content (if any) - transcribe important text
74
- 3. Visual elements (diagrams, charts, UI components)
75
- 4. Layout and structure
76
- 5. Colors, styling, notable design elements
77
- 6. Any actionable information (buttons, links, code)
78
-
79
- Be comprehensive - this description replaces direct visual access.`;
80
-
81
- // ============================================================================
82
- // HELPER FUNCTIONS
83
- // ============================================================================
84
-
85
57
  /**
86
58
  * Output debug information to stderr
87
59
  * Only outputs when CCS_DEBUG=1
@@ -105,19 +77,19 @@ function debugLog(message, data = {}) {
105
77
  */
106
78
  function getDebugContext(filePath, stats) {
107
79
  const currentProvider = process.env.CCS_CURRENT_PROVIDER || 'unknown';
108
- const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
109
- const model = providerModels[currentProvider] || DEFAULT_MODEL;
80
+ const model =
81
+ process.env.CCS_IMAGE_ANALYSIS_MODEL ||
82
+ parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS)[currentProvider] ||
83
+ DEFAULT_MODEL;
110
84
  const timeout = parseInt(process.env.CCS_IMAGE_ANALYSIS_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
111
- const modelsToTry = getModelsToTry();
112
85
 
113
86
  return {
114
87
  file: path.basename(filePath),
115
88
  size: stats ? `${(stats.size / 1024).toFixed(1)} KB` : 'unknown',
116
89
  provider: currentProvider,
117
90
  model: model,
118
- modelsToTry: modelsToTry.length > 1 ? modelsToTry.join(' -> ') : model,
119
91
  timeout: `${timeout}s`,
120
- endpoint: `http://${CLIPROXY_HOST}:${CLIPROXY_PORT}${CLIPROXY_PATH}`,
92
+ endpoint: process.env.CCS_IMAGE_ANALYSIS_RUNTIME_BASE_URL || '(runtime fallback)',
121
93
  };
122
94
  }
123
95
 
@@ -126,319 +98,12 @@ function getDebugContext(filePath, stats) {
126
98
  */
127
99
  function getProviderContext() {
128
100
  const provider = process.env.CCS_CURRENT_PROVIDER || 'unknown';
129
- const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
130
- const model = providerModels[provider] || DEFAULT_MODEL;
101
+ const model =
102
+ process.env.CCS_IMAGE_ANALYSIS_MODEL ||
103
+ parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS)[provider] ||
104
+ DEFAULT_MODEL;
131
105
  return { provider, model };
132
106
  }
133
-
134
- /**
135
- * Parse provider_models env var to object
136
- * Format: provider:model,provider:model
137
- */
138
- function parseProviderModels(envValue) {
139
- if (!envValue) return {};
140
- const result = {};
141
- envValue.split(',').forEach((pair) => {
142
- const [provider, model] = pair.split(':');
143
- if (provider && model && model.trim()) {
144
- result[provider.trim()] = model.trim();
145
- }
146
- });
147
- return result;
148
- }
149
-
150
- /**
151
- * Extract concatenated text content from CLIProxy response blocks.
152
- * Skips thinking and other non-text blocks.
153
- */
154
- function extractTextContent(response) {
155
- if (!response || !Array.isArray(response.content)) {
156
- return null;
157
- }
158
-
159
- const textBlocks = response.content
160
- .filter((block) => block && block.type === 'text' && typeof block.text === 'string')
161
- .map((block) => block.text)
162
- .filter((text) => text.trim());
163
-
164
- if (textBlocks.length === 0) {
165
- return null;
166
- }
167
-
168
- return textBlocks.join('\n\n');
169
- }
170
-
171
- /**
172
- * Parse raw CLIProxy response body and extract text content.
173
- */
174
- function parseCliProxyResponse(data) {
175
- let response;
176
- try {
177
- response = JSON.parse(data);
178
- } catch (err) {
179
- throw new Error(`Failed to parse response: ${err.message}`);
180
- }
181
-
182
- const text = extractTextContent(response);
183
- if (!text) {
184
- throw new Error('No text content in response');
185
- }
186
-
187
- return text;
188
- }
189
-
190
- /**
191
- * Get model for current provider from provider_models mapping
192
- * Returns primary model only (for display/logging)
193
- */
194
- function getModelForProvider() {
195
- const currentProvider = process.env.CCS_CURRENT_PROVIDER || '';
196
- const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
197
- return providerModels[currentProvider] || DEFAULT_MODEL;
198
- }
199
-
200
- /**
201
- * Get list of models to try in order:
202
- * 1. provider_models[current_provider] (if exists)
203
- * 2. DEFAULT_MODEL when no provider-specific vision model is configured
204
- *
205
- * ANTHROPIC_MODEL is intentionally ignored here because the chat model may
206
- * not be vision-capable on the current provider route.
207
- */
208
- function getModelsToTry() {
209
- const currentProvider = process.env.CCS_CURRENT_PROVIDER || '';
210
- const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
211
-
212
- const models = [];
213
- const seen = new Set();
214
-
215
- // 1. Provider-specific model
216
- if (providerModels[currentProvider]) {
217
- models.push(providerModels[currentProvider]);
218
- seen.add(providerModels[currentProvider]);
219
- }
220
-
221
- // 2. Default model — only when no provider-specific model is configured,
222
- // since DEFAULT_MODEL (gemini-2.5-flash) may not be routable on the
223
- // current provider's CLIProxy endpoint (e.g. codex, claude)
224
- if (models.length === 0 && !seen.has(DEFAULT_MODEL)) {
225
- models.push(DEFAULT_MODEL);
226
- seen.add(DEFAULT_MODEL);
227
- }
228
-
229
- return models;
230
- }
231
-
232
- /**
233
- * Analyze with retry logic - tries models in order until one succeeds
234
- */
235
- async function analyzeWithRetry(base64Data, mediaType, timeoutMs) {
236
- const models = getModelsToTry();
237
- // Defensive check - should never happen but provides clear error
238
- if (models.length === 0) {
239
- throw new Error('No models configured for image analysis');
240
- }
241
- let lastError = null;
242
-
243
- for (let i = 0; i < models.length; i++) {
244
- const model = models[i];
245
- try {
246
- debugLog(`Trying model ${i + 1}/${models.length}`, { model });
247
- const result = await analyzeViaCliProxy(base64Data, mediaType, model, timeoutMs);
248
- if (i > 0) {
249
- debugLog('Retry succeeded', { model, attempt: i + 1 });
250
- }
251
- return { description: result, model };
252
- } catch (err) {
253
- lastError = err;
254
- const isLastModel = i === models.length - 1;
255
-
256
- // Don't retry on certain errors (auth, rate limit, timeout, file access, network)
257
- const errMsg = err.message || '';
258
- const noRetryPatterns = [
259
- 'AUTH_ERROR', 'RATE_LIMIT', 'TIMEOUT',
260
- 'EACCES', 'EPERM', 'ECONNREFUSED',
261
- 'ENOTFOUND', 'ENETUNREACH', 'EAI_AGAIN' // Network errors - no point retrying
262
- ];
263
- const shouldNotRetry = noRetryPatterns.some(p => errMsg.includes(p));
264
-
265
- if (shouldNotRetry || isLastModel) {
266
- debugLog('Analysis failed, no more retries', {
267
- model,
268
- error: errMsg,
269
- reason: shouldNotRetry ? 'non-retryable error' : 'last model'
270
- });
271
- throw err;
272
- }
273
-
274
- debugLog('Model failed, trying next', {
275
- model,
276
- error: errMsg.substring(0, 100),
277
- nextModel: models[i + 1]
278
- });
279
- }
280
- }
281
-
282
- throw lastError || new Error('No models available');
283
- }
284
-
285
- /**
286
- * Check if file is an analyzable image or PDF
287
- */
288
- function isAnalyzableFile(filePath) {
289
- const ext = path.extname(filePath).toLowerCase();
290
- return IMAGE_EXTENSIONS.includes(ext) || PDF_EXTENSIONS.includes(ext);
291
- }
292
-
293
- /**
294
- * Get MIME type from file extension
295
- */
296
- function getMediaType(filePath) {
297
- const ext = path.extname(filePath).toLowerCase();
298
- const mimeTypes = {
299
- '.jpg': 'image/jpeg',
300
- '.jpeg': 'image/jpeg',
301
- '.png': 'image/png',
302
- '.gif': 'image/gif',
303
- '.webp': 'image/webp',
304
- '.heic': 'image/heic',
305
- '.bmp': 'image/bmp',
306
- '.tiff': 'image/tiff',
307
- '.pdf': 'application/pdf',
308
- };
309
- return mimeTypes[ext] || 'application/octet-stream';
310
- }
311
-
312
- /**
313
- * Encode file to base64
314
- */
315
- function encodeFileToBase64(filePath) {
316
- const content = fs.readFileSync(filePath);
317
- return content.toString('base64');
318
- }
319
-
320
- /**
321
- * Check if CLIProxy is available
322
- */
323
- function isCliProxyAvailable() {
324
- return new Promise((resolve) => {
325
- const req = http.request(
326
- {
327
- hostname: CLIPROXY_HOST,
328
- port: CLIPROXY_PORT,
329
- path: '/',
330
- method: 'GET',
331
- timeout: 2000,
332
- },
333
- (res) => {
334
- resolve(res.statusCode >= 200 && res.statusCode < 500);
335
- }
336
- );
337
-
338
- req.on('error', () => resolve(false));
339
- req.on('timeout', () => {
340
- req.destroy();
341
- resolve(false);
342
- });
343
-
344
- req.end();
345
- });
346
- }
347
-
348
- /**
349
- * Analyze file via CLIProxy vision API
350
- */
351
- function analyzeViaCliProxy(base64Data, mediaType, model, timeoutMs) {
352
- return new Promise((resolve, reject) => {
353
- const requestBody = JSON.stringify({
354
- model: model,
355
- max_tokens: 4096,
356
- messages: [
357
- {
358
- role: 'user',
359
- content: [
360
- { type: 'text', text: DEFAULT_PROMPT },
361
- {
362
- type: 'image',
363
- source: {
364
- type: 'base64',
365
- media_type: mediaType,
366
- data: base64Data,
367
- },
368
- },
369
- ],
370
- },
371
- ],
372
- });
373
-
374
- const req = http.request(
375
- {
376
- hostname: CLIPROXY_HOST,
377
- port: CLIPROXY_PORT,
378
- path: CLIPROXY_PATH,
379
- method: 'POST',
380
- headers: {
381
- 'Content-Type': 'application/json',
382
- 'Content-Length': Buffer.byteLength(requestBody),
383
- 'x-api-key': CLIPROXY_API_KEY,
384
- },
385
- timeout: timeoutMs,
386
- },
387
- (res) => {
388
- let data = '';
389
-
390
- res.on('data', (chunk) => {
391
- data += chunk;
392
- });
393
-
394
- res.on('error', (err) => {
395
- reject(err);
396
- });
397
-
398
- res.on('end', () => {
399
- // Categorize by status code
400
- if (res.statusCode === 401 || res.statusCode === 403) {
401
- reject(new Error(`AUTH_ERROR:${res.statusCode}`));
402
- return;
403
- }
404
-
405
- if (res.statusCode === 429) {
406
- const retryAfter = res.headers['retry-after'];
407
- reject(new Error(`RATE_LIMIT:${retryAfter || ''}`));
408
- return;
409
- }
410
-
411
- if (res.statusCode !== 200) {
412
- reject(new Error(`API_ERROR:${res.statusCode}:${data}`));
413
- return;
414
- }
415
-
416
- if (!data || !data.trim()) {
417
- reject(new Error('Empty response from CLIProxy'));
418
- return;
419
- }
420
-
421
- try {
422
- const text = parseCliProxyResponse(data);
423
- resolve(text);
424
- } catch (err) {
425
- reject(err);
426
- }
427
- });
428
- }
429
- );
430
-
431
- req.on('error', (err) => reject(err));
432
- req.on('timeout', () => {
433
- req.destroy();
434
- reject(new Error('TIMEOUT'));
435
- });
436
-
437
- req.write(requestBody);
438
- req.end();
439
- });
440
- }
441
-
442
107
  /**
443
108
  * Format analysis description for Claude (matches websearch format)
444
109
  */
@@ -520,29 +185,6 @@ function outputFileTooLargeError(filePath, actualSizeMB, maxSizeMB) {
520
185
  process.exit(2);
521
186
  }
522
187
 
523
- /**
524
- * CLIProxy unavailable error
525
- */
526
- function outputCliProxyUnavailableError(filePath, endpoint) {
527
- const output = formatErrorOutput(
528
- filePath,
529
- ERROR_CODES.CLIPROXY_UNAVAILABLE,
530
- `CLIProxy not available at ${endpoint}`,
531
- [
532
- 'CLIProxy service may not be running',
533
- 'Start with: ccs config (opens dashboard, starts CLIProxy)',
534
- 'Or manually: ccs cliproxy start',
535
- `Verify: curl ${endpoint}`,
536
- 'Check status: ccs doctor',
537
- ]
538
- );
539
- console.log(JSON.stringify(output));
540
- process.exit(2);
541
- }
542
-
543
- /**
544
- * Authentication error
545
- */
546
188
  function outputAuthError(filePath, statusCode) {
547
189
  const { provider } = getProviderContext();
548
190
  const output = formatErrorOutput(
@@ -758,10 +400,11 @@ function shouldSkipHook() {
758
400
  }
759
401
 
760
402
  // Check if current provider has a vision model configured
403
+ const explicitModel = process.env.CCS_IMAGE_ANALYSIS_MODEL;
761
404
  const currentProvider = process.env.CCS_CURRENT_PROVIDER || '';
762
405
  const providerModels = parseProviderModels(process.env.CCS_IMAGE_ANALYSIS_PROVIDER_MODELS);
763
406
 
764
- if (!providerModels[currentProvider]) {
407
+ if (!explicitModel?.trim() && !providerModels[currentProvider]) {
765
408
  debugLog(`Skipping: provider "${currentProvider}" not in provider_models`, {
766
409
  configured_providers: Object.keys(providerModels).join(', ') || 'none',
767
410
  });
@@ -832,57 +475,15 @@ async function processHook() {
832
475
  process.exit(0);
833
476
  }
834
477
 
835
- // Check if file exists
836
478
  if (!fs.existsSync(filePath)) {
837
- // Let native Read handle the error
838
479
  process.exit(0);
839
480
  }
840
481
 
841
- // Check file size
842
- const stats = fs.statSync(filePath);
843
- if (stats.size >= MAX_FILE_SIZE_BYTES) {
844
- outputFileTooLargeError(filePath, stats.size / 1024 / 1024, MAX_FILE_SIZE_MB);
845
- return;
846
- }
847
-
848
- // Check CLIProxy availability
849
- const cliProxyAvailable = await isCliProxyAvailable();
850
- if (!cliProxyAvailable) {
851
- debugLog('Blocking: CLIProxy not available', {
852
- endpoint: `http://${CLIPROXY_HOST}:${CLIPROXY_PORT}`,
853
- action: 'blocking to prevent context overflow',
854
- });
855
- outputCliProxyUnavailableFallback(filePath);
856
- return;
857
- }
858
-
859
- const model = getModelForProvider();
860
- const timeout = parseInt(process.env.CCS_IMAGE_ANALYSIS_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
861
- const timeoutMs = Math.max(1, Math.min(600, timeout)) * 1000;
862
-
863
- // Get debug context before analysis
864
- const debugContext = getDebugContext(filePath, stats);
865
- debugLog('Starting image analysis', debugContext);
482
+ const debugContext = getDebugContext(filePath, null);
483
+ debugLog('Image analysis runtime prepared', debugContext);
866
484
 
867
- // Encode file to base64
868
- const base64Data = encodeFileToBase64(filePath);
869
- const mediaType = getMediaType(filePath);
870
-
871
- debugLog('File encoded', {
872
- mediaType: mediaType,
873
- base64Length: `${(base64Data.length / 1024).toFixed(1)}KB`,
874
- });
875
-
876
- // Analyze via CLIProxy with retry logic
877
- const { description, model: usedModel } = await analyzeWithRetry(base64Data, mediaType, timeoutMs);
878
-
879
- debugLog('Analysis complete', {
880
- responseLength: `${description.length} chars`,
881
- model: usedModel,
882
- });
883
-
884
- // Output success
885
- outputSuccess(filePath, description, usedModel, stats.size);
485
+ const result = await analyzeFile(filePath);
486
+ outputSuccess(filePath, result.description, result.model, result.fileSize);
886
487
  } catch (err) {
887
488
  if (process.env.CCS_DEBUG) {
888
489
  console.error('[CCS Hook] Error:', err.message);
@@ -893,7 +494,10 @@ async function processHook() {
893
494
  // Categorize error by message pattern
894
495
  const errMsg = err.message || '';
895
496
 
896
- if (errMsg.startsWith('AUTH_ERROR:')) {
497
+ if (errMsg.startsWith('FILE_TOO_LARGE:')) {
498
+ const fileSizeMb = Number.parseInt(errMsg.split(':')[1], 10) / 1024 / 1024;
499
+ outputFileTooLargeError(filePath, fileSizeMb, 10);
500
+ } else if (errMsg.startsWith('AUTH_ERROR:')) {
897
501
  const statusCode = parseInt(errMsg.split(':')[1], 10);
898
502
  outputAuthError(filePath, statusCode);
899
503
  } else if (errMsg.startsWith('RATE_LIMIT:')) {
@@ -907,8 +511,13 @@ async function processHook() {
907
511
  } else if (errMsg === 'TIMEOUT' || errMsg.includes('timed out') || errMsg.includes('timeout')) {
908
512
  const timeout = parseInt(process.env.CCS_IMAGE_ANALYSIS_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
909
513
  outputTimeoutError(filePath, timeout);
910
- } else if (errMsg.includes('ECONNREFUSED') || errMsg.includes('ENOTFOUND')) {
911
- outputCliProxyUnavailableError(filePath, `http://${CLIPROXY_HOST}:${CLIPROXY_PORT}`);
514
+ } else if (
515
+ errMsg.includes('ECONNREFUSED') ||
516
+ errMsg.includes('ENOTFOUND') ||
517
+ errMsg.includes('ENETUNREACH') ||
518
+ errMsg.includes('EAI_AGAIN')
519
+ ) {
520
+ outputCliProxyUnavailableFallback(filePath);
912
521
  } else if (errMsg.includes('EACCES') || errMsg.includes('EPERM')) {
913
522
  outputFileAccessError(filePath, errMsg);
914
523
  } else {
@@ -67,6 +67,8 @@ const PROVIDER_CONFIG = {
67
67
 
68
68
  const ddgLinkRe = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
69
69
  const ddgSnippetRe = /<a class="result__snippet[^"]*".*?>([\s\S]*?)<\/a>/g;
70
+ const ddgNoResultsRe = /class=['"][^'"]*no-results(?:__message)?[^'"]*['"]/i;
71
+ const ddgNoResultsHeadingRe = /No results found for/i;
70
72
  const htmlTagRe = /<[^>]+>/g;
71
73
 
72
74
  function debug(message) {
@@ -427,6 +429,30 @@ function extractDuckDuckGoResults(html, count) {
427
429
  });
428
430
  }
429
431
 
432
+ function classifyDuckDuckGoHtml(html, count) {
433
+ const responseHtml = String(html || '');
434
+ const results = extractDuckDuckGoResults(responseHtml, count);
435
+ if (results.length > 0) {
436
+ return {
437
+ kind: 'results',
438
+ results,
439
+ };
440
+ }
441
+
442
+ if (ddgNoResultsRe.test(responseHtml) || ddgNoResultsHeadingRe.test(responseHtml)) {
443
+ return {
444
+ kind: 'no_results',
445
+ results: [],
446
+ };
447
+ }
448
+
449
+ return {
450
+ kind: 'non_result_html',
451
+ results: [],
452
+ error: 'DuckDuckGo returned non-result HTML response (possible anti-bot/challenge page)',
453
+ };
454
+ }
455
+
430
456
  function formatStructuredSearchResults(query, providerName, results) {
431
457
  const lines = [
432
458
  'CCS local WebSearch evidence',
@@ -680,10 +706,18 @@ async function tryDuckDuckGoSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
680
706
  }
681
707
 
682
708
  const html = await response.text();
683
- const results = extractDuckDuckGoResults(html, getResultCount('duckduckgo'));
709
+ const parsed = classifyDuckDuckGoHtml(html, getResultCount('duckduckgo'));
710
+ if (parsed.kind === 'non_result_html') {
711
+ return {
712
+ success: false,
713
+ error: `${parsed.error} (status ${response.status})`,
714
+ statusCode: response.status,
715
+ };
716
+ }
717
+
684
718
  return {
685
719
  success: true,
686
- content: formatStructuredSearchResults(query, 'DuckDuckGo', results),
720
+ content: formatStructuredSearchResults(query, 'DuckDuckGo', parsed.results),
687
721
  };
688
722
  } catch (error) {
689
723
  return {
@@ -1229,6 +1263,7 @@ if (require.main === module) {
1229
1263
  module.exports = {
1230
1264
  buildFailureHookOutput,
1231
1265
  buildSuccessHookOutput,
1266
+ classifyDuckDuckGoHtml,
1232
1267
  extractDuckDuckGoResults,
1233
1268
  formatStructuredSearchResults,
1234
1269
  getActiveProviders,