@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.
- package/README.md +88 -750
- package/dist/api/services/profile-lifecycle-service.d.ts.map +1 -1
- package/dist/api/services/profile-lifecycle-service.js +4 -0
- package/dist/api/services/profile-lifecycle-service.js.map +1 -1
- package/dist/api/services/profile-writer.d.ts.map +1 -1
- package/dist/api/services/profile-writer.js +3 -0
- package/dist/api/services/profile-writer.js.map +1 -1
- package/dist/auth/auth-commands.d.ts +1 -0
- package/dist/auth/auth-commands.d.ts.map +1 -1
- package/dist/auth/auth-commands.js +11 -0
- package/dist/auth/auth-commands.js.map +1 -1
- package/dist/auth/commands/backup-command.d.ts +3 -0
- package/dist/auth/commands/backup-command.d.ts.map +1 -0
- package/dist/auth/commands/backup-command.js +126 -0
- package/dist/auth/commands/backup-command.js.map +1 -0
- package/dist/auth/commands/index.d.ts +1 -0
- package/dist/auth/commands/index.d.ts.map +1 -1
- package/dist/auth/commands/index.js +3 -1
- package/dist/auth/commands/index.js.map +1 -1
- package/dist/auth/profile-continuity-inheritance.d.ts +1 -0
- package/dist/auth/profile-continuity-inheritance.d.ts.map +1 -1
- package/dist/auth/profile-continuity-inheritance.js +10 -6
- package/dist/auth/profile-continuity-inheritance.js.map +1 -1
- package/dist/auth/profile-detector.d.ts +9 -1
- package/dist/auth/profile-detector.d.ts.map +1 -1
- package/dist/auth/profile-detector.js +35 -0
- package/dist/auth/profile-detector.js.map +1 -1
- package/dist/auth/resume-lane-diagnostics.d.ts +21 -0
- package/dist/auth/resume-lane-diagnostics.d.ts.map +1 -0
- package/dist/auth/resume-lane-diagnostics.js +146 -0
- package/dist/auth/resume-lane-diagnostics.js.map +1 -0
- package/dist/auth/resume-lane-warning.d.ts +9 -0
- package/dist/auth/resume-lane-warning.d.ts.map +1 -0
- package/dist/auth/resume-lane-warning.js +60 -0
- package/dist/auth/resume-lane-warning.js.map +1 -0
- package/dist/ccs.js +79 -7
- package/dist/ccs.js.map +1 -1
- package/dist/cliproxy/executor/env-resolver.d.ts +3 -0
- package/dist/cliproxy/executor/env-resolver.d.ts.map +1 -1
- package/dist/cliproxy/executor/env-resolver.js +19 -1
- package/dist/cliproxy/executor/env-resolver.js.map +1 -1
- package/dist/cliproxy/executor/index.d.ts.map +1 -1
- package/dist/cliproxy/executor/index.js +24 -5
- package/dist/cliproxy/executor/index.js.map +1 -1
- package/dist/cliproxy/gemini-cli-quota-normalizer.d.ts +10 -0
- package/dist/cliproxy/gemini-cli-quota-normalizer.d.ts.map +1 -0
- package/dist/cliproxy/gemini-cli-quota-normalizer.js +122 -0
- package/dist/cliproxy/gemini-cli-quota-normalizer.js.map +1 -0
- package/dist/cliproxy/quota-fetcher-gemini-cli.d.ts.map +1 -1
- package/dist/cliproxy/quota-fetcher-gemini-cli.js +133 -92
- package/dist/cliproxy/quota-fetcher-gemini-cli.js.map +1 -1
- package/dist/cliproxy/quota-types.d.ts +8 -0
- package/dist/cliproxy/quota-types.d.ts.map +1 -1
- package/dist/cliproxy/services/variant-settings.d.ts.map +1 -1
- package/dist/cliproxy/services/variant-settings.js +11 -0
- package/dist/cliproxy/services/variant-settings.js.map +1 -1
- package/dist/commands/cliproxy/quota-subcommand.d.ts.map +1 -1
- package/dist/commands/cliproxy/quota-subcommand.js +10 -1
- package/dist/commands/cliproxy/quota-subcommand.js.map +1 -1
- package/dist/commands/command-catalog.d.ts +39 -0
- package/dist/commands/command-catalog.d.ts.map +1 -0
- package/dist/commands/command-catalog.js +298 -0
- package/dist/commands/command-catalog.js.map +1 -0
- package/dist/commands/completion-backend.d.ts +14 -0
- package/dist/commands/completion-backend.d.ts.map +1 -0
- package/dist/commands/completion-backend.js +208 -0
- package/dist/commands/completion-backend.js.map +1 -0
- package/dist/commands/cursor-command-display.d.ts.map +1 -1
- package/dist/commands/cursor-command-display.js +25 -5
- package/dist/commands/cursor-command-display.js.map +1 -1
- package/dist/commands/cursor-command.d.ts +1 -3
- package/dist/commands/cursor-command.d.ts.map +1 -1
- package/dist/commands/cursor-command.js +3 -15
- package/dist/commands/cursor-command.js.map +1 -1
- package/dist/commands/help-command.d.ts +4 -3
- package/dist/commands/help-command.d.ts.map +1 -1
- package/dist/commands/help-command.js +155 -507
- package/dist/commands/help-command.js.map +1 -1
- package/dist/commands/install-command.d.ts.map +1 -1
- package/dist/commands/install-command.js +16 -3
- package/dist/commands/install-command.js.map +1 -1
- package/dist/commands/root-command-router.d.ts +2 -0
- package/dist/commands/root-command-router.d.ts.map +1 -1
- package/dist/commands/root-command-router.js +13 -13
- package/dist/commands/root-command-router.js.map +1 -1
- package/dist/commands/shell-completion-command.d.ts +1 -0
- package/dist/commands/shell-completion-command.d.ts.map +1 -1
- package/dist/commands/shell-completion-command.js +27 -11
- package/dist/commands/shell-completion-command.js.map +1 -1
- package/dist/copilot/copilot-executor.d.ts +2 -0
- package/dist/copilot/copilot-executor.d.ts.map +1 -1
- package/dist/copilot/copilot-executor.js +36 -4
- package/dist/copilot/copilot-executor.js.map +1 -1
- package/dist/cursor/constants.d.ts +3 -0
- package/dist/cursor/constants.d.ts.map +1 -0
- package/dist/cursor/constants.js +20 -0
- package/dist/cursor/constants.js.map +1 -0
- package/dist/cursor/cursor-models.d.ts.map +1 -1
- package/dist/cursor/cursor-models.js +2 -0
- package/dist/cursor/cursor-models.js.map +1 -1
- package/dist/cursor/cursor-profile-executor.d.ts +10 -0
- package/dist/cursor/cursor-profile-executor.d.ts.map +1 -0
- package/dist/cursor/cursor-profile-executor.js +158 -0
- package/dist/cursor/cursor-profile-executor.js.map +1 -0
- package/dist/cursor/cursor-translator.d.ts +22 -11
- package/dist/cursor/cursor-translator.d.ts.map +1 -1
- package/dist/cursor/cursor-translator.js +254 -75
- package/dist/cursor/cursor-translator.js.map +1 -1
- package/dist/cursor/index.d.ts +1 -0
- package/dist/cursor/index.d.ts.map +1 -1
- package/dist/cursor/index.js +4 -1
- package/dist/cursor/index.js.map +1 -1
- package/dist/delegation/headless-executor.d.ts.map +1 -1
- package/dist/delegation/headless-executor.js +79 -2
- package/dist/delegation/headless-executor.js.map +1 -1
- package/dist/management/checks/image-analysis-check.d.ts.map +1 -1
- package/dist/management/checks/image-analysis-check.js +4 -5
- package/dist/management/checks/image-analysis-check.js.map +1 -1
- package/dist/management/instance-manager.js +1 -1
- package/dist/management/instance-manager.js.map +1 -1
- package/dist/shared/claude-extension-setup.d.ts.map +1 -1
- package/dist/shared/claude-extension-setup.js +36 -16
- package/dist/shared/claude-extension-setup.js.map +1 -1
- package/dist/targets/target-runtime-compatibility.d.ts.map +1 -1
- package/dist/targets/target-runtime-compatibility.js +6 -0
- package/dist/targets/target-runtime-compatibility.js.map +1 -1
- package/dist/types/profile.d.ts +1 -1
- package/dist/types/profile.d.ts.map +1 -1
- package/dist/ui/assets/accounts-BjfPKR8m.js +1 -0
- package/dist/ui/assets/{alert-dialog-D0EFRcfB.js → alert-dialog-Dh2NUFdm.js} +1 -1
- package/dist/ui/assets/{api-DhM3BYXr.js → api-C-3mQCFf.js} +1 -1
- package/dist/ui/assets/{auth-section-DVp8FQGm.js → auth-section-Dp10_YyD.js} +1 -1
- package/dist/ui/assets/{backups-section-CRo0NZkA.js → backups-section-C0jF8MP1.js} +1 -1
- package/dist/ui/assets/{channels-uZ_9CBqO.js → channels-CkXuK5i7.js} +1 -1
- package/dist/ui/assets/{checkbox-32DNqW_Q.js → checkbox-tA5FH8Ol.js} +1 -1
- package/dist/ui/assets/{claude-extension-BfXlz5gV.js → claude-extension-Bg2ZkzMz.js} +1 -1
- package/dist/ui/assets/{cliproxy-DjNY9H-U.js → cliproxy-1qRVSbVC.js} +2 -2
- package/dist/ui/assets/{cliproxy-ai-providers-5SHLMHiy.js → cliproxy-ai-providers-DBSXTTyw.js} +1 -1
- package/dist/ui/assets/cliproxy-control-panel-Da-sGGyI.js +1 -0
- package/dist/ui/assets/{codex-CRUSpjsu.js → codex-ooWKOPa2.js} +1 -1
- package/dist/ui/assets/{confirm-dialog-DVf5ZmCZ.js → confirm-dialog-CKjwhn9j.js} +1 -1
- package/dist/ui/assets/{copilot-BZrihl_Z.js → copilot-GA7EPiK1.js} +1 -1
- package/dist/ui/assets/{cursor-BP4nbEk_.js → cursor-B6c8CyHG.js} +1 -1
- package/dist/ui/assets/{droid-BG92rdM2.js → droid-CPRHOIX2.js} +1 -1
- package/dist/ui/assets/{globalenv-section-Cf6dKgSf.js → globalenv-section-Czgnw_GV.js} +1 -1
- package/dist/ui/assets/{health-BTy1UZs3.js → health-CXLOMk8n.js} +1 -1
- package/dist/ui/assets/icons-B9oTjo-t.js +1 -0
- package/dist/ui/assets/index-BMHPMj0j.js +69 -0
- package/dist/ui/assets/{index-BVeN0dIB.js → index-BceMcbCR.js} +1 -1
- package/dist/ui/assets/{index-N2ZSJurX.js → index-Boa5e-GY.js} +1 -1
- package/dist/ui/assets/index-CknHGRYp.css +1 -0
- package/dist/ui/assets/{index-wg7UtkFv.js → index-CvfzKRSH.js} +1 -1
- package/dist/ui/assets/{index-DuRYaONg.js → index-D2v_-6AW.js} +1 -1
- package/dist/ui/assets/{index-DHrTq-0n.js → index-v-DY6Zby.js} +1 -1
- package/dist/ui/assets/{masked-input-DX9bedLy.js → masked-input-B1_asiUI.js} +1 -1
- package/dist/ui/assets/{proxy-status-widget-DVDMuZK5.js → proxy-status-widget-Ci1JpStj.js} +1 -1
- package/dist/ui/assets/{radix-ui-C98W0NRG.js → radix-ui-Zb8sVEtn.js} +1 -1
- package/dist/ui/assets/{raw-json-settings-editor-panel-Dkt5E6Z_.js → raw-json-settings-editor-panel-DMbTkxWw.js} +1 -1
- package/dist/ui/assets/{searchable-select-BP3Q1-Yn.js → searchable-select-HbP2PXl3.js} +1 -1
- package/dist/ui/assets/{separator-BLGGUlh9.js → separator-CdaalG0K.js} +1 -1
- package/dist/ui/assets/{shared-G0XRyLig.js → shared-BiFB-et0.js} +1 -1
- package/dist/ui/assets/{table-B4lRrWC-.js → table-BPwgFXLQ.js} +1 -1
- package/dist/ui/assets/{tanstack-CfKik0yL.js → tanstack-DWm6aJ-G.js} +1 -1
- package/dist/ui/assets/{updates--A2Sdo7N.js → updates-l8Co9uve.js} +1 -1
- package/dist/ui/index.html +5 -5
- package/dist/utils/config-manager.d.ts +5 -0
- package/dist/utils/config-manager.d.ts.map +1 -1
- package/dist/utils/config-manager.js +10 -1
- package/dist/utils/config-manager.js.map +1 -1
- package/dist/utils/hooks/get-image-analysis-hook-env.d.ts +26 -0
- package/dist/utils/hooks/get-image-analysis-hook-env.d.ts.map +1 -1
- package/dist/utils/hooks/get-image-analysis-hook-env.js +79 -1
- package/dist/utils/hooks/get-image-analysis-hook-env.js.map +1 -1
- package/dist/utils/hooks/image-analysis-backend-resolver.d.ts.map +1 -1
- package/dist/utils/hooks/image-analysis-backend-resolver.js +13 -5
- package/dist/utils/hooks/image-analysis-backend-resolver.js.map +1 -1
- package/dist/utils/hooks/image-analysis-runtime-status.d.ts +2 -0
- package/dist/utils/hooks/image-analysis-runtime-status.d.ts.map +1 -1
- package/dist/utils/hooks/image-analysis-runtime-status.js +15 -11
- package/dist/utils/hooks/image-analysis-runtime-status.js.map +1 -1
- package/dist/utils/hooks/image-analyzer-hook-installer.d.ts.map +1 -1
- package/dist/utils/hooks/image-analyzer-hook-installer.js +60 -27
- package/dist/utils/hooks/image-analyzer-hook-installer.js.map +1 -1
- package/dist/utils/hooks/image-analyzer-profile-hook-injector.d.ts.map +1 -1
- package/dist/utils/hooks/image-analyzer-profile-hook-injector.js +3 -0
- package/dist/utils/hooks/image-analyzer-profile-hook-injector.js.map +1 -1
- package/dist/utils/hooks/index.d.ts +2 -1
- package/dist/utils/hooks/index.d.ts.map +1 -1
- package/dist/utils/hooks/index.js +14 -7
- package/dist/utils/hooks/index.js.map +1 -1
- package/dist/utils/image-analysis/claude-tool-args.d.ts +6 -0
- package/dist/utils/image-analysis/claude-tool-args.d.ts.map +1 -0
- package/dist/utils/image-analysis/claude-tool-args.js +65 -0
- package/dist/utils/image-analysis/claude-tool-args.js.map +1 -0
- package/dist/utils/image-analysis/index.d.ts +4 -0
- package/dist/utils/image-analysis/index.d.ts.map +1 -1
- package/dist/utils/image-analysis/index.js +20 -1
- package/dist/utils/image-analysis/index.js.map +1 -1
- package/dist/utils/image-analysis/mcp-installer.d.ts +18 -0
- package/dist/utils/image-analysis/mcp-installer.d.ts.map +1 -0
- package/dist/utils/image-analysis/mcp-installer.js +447 -0
- package/dist/utils/image-analysis/mcp-installer.js.map +1 -0
- package/dist/web-server/routes/account-routes.d.ts.map +1 -1
- package/dist/web-server/routes/account-routes.js +14 -2
- package/dist/web-server/routes/account-routes.js.map +1 -1
- package/dist/web-server/routes/cliproxy-auth-routes.d.ts.map +1 -1
- package/dist/web-server/routes/cliproxy-auth-routes.js +80 -6
- package/dist/web-server/routes/cliproxy-auth-routes.js.map +1 -1
- package/dist/web-server/routes/image-analysis-routes.d.ts.map +1 -1
- package/dist/web-server/routes/image-analysis-routes.js +30 -5
- package/dist/web-server/routes/image-analysis-routes.js.map +1 -1
- package/lib/hooks/image-analysis-runtime.cjs +469 -0
- package/lib/hooks/image-analyzer-transformer.cjs +27 -418
- package/lib/hooks/websearch-transformer.cjs +37 -2
- package/lib/mcp/ccs-image-analysis-server.cjs +440 -0
- package/package.json +1 -1
- package/scripts/completion/README.md +55 -131
- package/scripts/completion/ccs.bash +22 -190
- package/scripts/completion/ccs.fish +19 -245
- package/scripts/completion/ccs.ps1 +37 -427
- package/scripts/completion/ccs.zsh +27 -305
- package/dist/ui/assets/accounts-BHEYnq6b.js +0 -1
- package/dist/ui/assets/cliproxy-control-panel-Zax_m1AC.js +0 -1
- package/dist/ui/assets/icons-CeH5899d.js +0 -1
- package/dist/ui/assets/index-B6SrL1O-.css +0 -1
- 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
|
|
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
|
|
109
|
-
|
|
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:
|
|
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
|
|
130
|
-
|
|
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
|
-
|
|
842
|
-
|
|
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
|
-
|
|
868
|
-
|
|
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('
|
|
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 (
|
|
911
|
-
|
|
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
|
|
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,
|