@kaitranntt/ccs 7.63.0 → 7.63.1-dev.2
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 +10 -3
- package/dist/api/services/profile-lifecycle-service.js +4 -4
- 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 -5
- package/dist/api/services/profile-writer.js.map +1 -1
- package/dist/ccs.js +21 -9
- package/dist/ccs.js.map +1 -1
- package/dist/cliproxy/accounts/email-account-identity.d.ts +12 -0
- package/dist/cliproxy/accounts/email-account-identity.d.ts.map +1 -0
- package/dist/cliproxy/accounts/email-account-identity.js +124 -0
- package/dist/cliproxy/accounts/email-account-identity.js.map +1 -0
- package/dist/cliproxy/accounts/query.d.ts.map +1 -1
- package/dist/cliproxy/accounts/query.js +15 -8
- package/dist/cliproxy/accounts/query.js.map +1 -1
- package/dist/cliproxy/accounts/registry.d.ts +6 -0
- package/dist/cliproxy/accounts/registry.d.ts.map +1 -1
- package/dist/cliproxy/accounts/registry.js +136 -42
- package/dist/cliproxy/accounts/registry.js.map +1 -1
- package/dist/cliproxy/auth/token-manager.d.ts.map +1 -1
- package/dist/cliproxy/auth/token-manager.js +45 -11
- package/dist/cliproxy/auth/token-manager.js.map +1 -1
- package/dist/cliproxy/executor/index.d.ts.map +1 -1
- package/dist/cliproxy/executor/index.js +25 -12
- package/dist/cliproxy/executor/index.js.map +1 -1
- package/dist/cliproxy/quota-fetcher-codex.d.ts +0 -3
- package/dist/cliproxy/quota-fetcher-codex.d.ts.map +1 -1
- package/dist/cliproxy/quota-fetcher-codex.js +46 -17
- package/dist/cliproxy/quota-fetcher-codex.js.map +1 -1
- package/dist/cliproxy/services/variant-settings.d.ts.map +1 -1
- package/dist/cliproxy/services/variant-settings.js +4 -6
- package/dist/cliproxy/services/variant-settings.js.map +1 -1
- package/dist/cliproxy/stats-transformer.d.ts.map +1 -1
- package/dist/cliproxy/stats-transformer.js +26 -3
- package/dist/cliproxy/stats-transformer.js.map +1 -1
- package/dist/commands/cliproxy/quota-subcommand.d.ts.map +1 -1
- package/dist/commands/cliproxy/quota-subcommand.js +25 -22
- package/dist/commands/cliproxy/quota-subcommand.js.map +1 -1
- package/dist/commands/cliproxy/variant-subcommand.d.ts.map +1 -1
- package/dist/commands/cliproxy/variant-subcommand.js +14 -6
- package/dist/commands/cliproxy/variant-subcommand.js.map +1 -1
- package/dist/commands/install-command.d.ts.map +1 -1
- package/dist/commands/install-command.js +8 -2
- package/dist/commands/install-command.js.map +1 -1
- package/dist/copilot/copilot-executor.d.ts.map +1 -1
- package/dist/copilot/copilot-executor.js +11 -2
- package/dist/copilot/copilot-executor.js.map +1 -1
- package/dist/delegation/executor/result-aggregator.d.ts +2 -1
- package/dist/delegation/executor/result-aggregator.d.ts.map +1 -1
- package/dist/delegation/executor/result-aggregator.js +21 -1
- package/dist/delegation/executor/result-aggregator.js.map +1 -1
- package/dist/delegation/executor/types.d.ts +6 -0
- package/dist/delegation/executor/types.d.ts.map +1 -1
- package/dist/delegation/headless-executor.d.ts.map +1 -1
- package/dist/delegation/headless-executor.js +69 -4
- package/dist/delegation/headless-executor.js.map +1 -1
- package/dist/management/instance-manager.d.ts +1 -1
- package/dist/management/instance-manager.d.ts.map +1 -1
- package/dist/management/instance-manager.js +10 -2
- package/dist/management/instance-manager.js.map +1 -1
- package/dist/shared/compatible-cli-contracts.d.ts +4 -0
- package/dist/shared/compatible-cli-contracts.d.ts.map +1 -1
- package/dist/targets/codex-adapter.d.ts.map +1 -1
- package/dist/targets/codex-adapter.js +78 -3
- package/dist/targets/codex-adapter.js.map +1 -1
- package/dist/targets/codex-detector.d.ts.map +1 -1
- package/dist/targets/codex-detector.js +28 -7
- package/dist/targets/codex-detector.js.map +1 -1
- package/dist/ui/assets/{accounts-DkxZnPJE.js → accounts-CUUJb7DN.js} +1 -1
- package/dist/ui/assets/{alert-dialog-CiYMglgR.js → alert-dialog-B3irFOWw.js} +1 -1
- package/dist/ui/assets/{api-DaOtMRT4.js → api-D-Qqy1yz.js} +2 -2
- package/dist/ui/assets/{auth-section-BMaKBRA_.js → auth-section-DIo3G68G.js} +1 -1
- package/dist/ui/assets/{backups-section-DOpSADoH.js → backups-section-AFO82gIq.js} +1 -1
- package/dist/ui/assets/{channels-zDFV-BlC.js → channels-BCTKZ7gt.js} +1 -1
- package/dist/ui/assets/{checkbox-Cb5AZBZL.js → checkbox-jPTeVbcz.js} +1 -1
- package/dist/ui/assets/{claude-extension-B5RngGem.js → claude-extension-BNiAud6L.js} +1 -1
- package/dist/ui/assets/cliproxy-CoYvAxcj.js +3 -0
- package/dist/ui/assets/{cliproxy-ai-providers-DVaaS-CT.js → cliproxy-ai-providers-C9jXuZcV.js} +1 -1
- package/dist/ui/assets/cliproxy-control-panel-DRSjNcrR.js +1 -0
- package/dist/ui/assets/codex-DSAAz_k8.js +27 -0
- package/dist/ui/assets/{confirm-dialog-B9vRgowr.js → confirm-dialog-Dn7GsgSZ.js} +1 -1
- package/dist/ui/assets/{copilot-HvsOp6hu.js → copilot-Ddvc59py.js} +2 -2
- package/dist/ui/assets/{cursor-C1XOjAWS.js → cursor-CPLxNsYY.js} +1 -1
- package/dist/ui/assets/{droid-DshEfT1H.js → droid-C5ArrKfL.js} +1 -1
- package/dist/ui/assets/{globalenv-section-CmcMkb6z.js → globalenv-section-CddBw-9L.js} +1 -1
- package/dist/ui/assets/{health-CE0VQs6K.js → health-7Wp3Ar7n.js} +1 -1
- package/dist/ui/assets/icons-C7Np5k44.js +1 -0
- package/dist/ui/assets/{index-CmKclBR1.js → index-1GqjYVWP.js} +1 -1
- package/dist/ui/assets/{index-CmtSgCxo.js → index-BPIA8mAF.js} +1 -1
- package/dist/ui/assets/index-BsTZB-Im.css +1 -0
- package/dist/ui/assets/{index-DAtuJuGe.js → index-C7OpByHy.js} +36 -36
- package/dist/ui/assets/index-CBwOAp7P.js +1 -0
- package/dist/ui/assets/{index-CesVGA6m.js → index-DSgKWnG8.js} +1 -1
- package/dist/ui/assets/{masked-input-B2tcbvAj.js → masked-input-BlP9jFt5.js} +1 -1
- package/dist/ui/assets/{proxy-status-widget-BnJD49TF.js → proxy-status-widget-CFyrt5Yw.js} +1 -1
- package/dist/ui/assets/{raw-json-settings-editor-panel-DnUbq1__.js → raw-json-settings-editor-panel-CfcTOhs4.js} +1 -1
- package/dist/ui/assets/{searchable-select-ULayr5K1.js → searchable-select-CcCH2Ug3.js} +1 -1
- package/dist/ui/assets/{separator--ZH5ZM-3.js → separator-DRULo1eG.js} +1 -1
- package/dist/ui/assets/shared-ByRk7nxS.js +8 -0
- package/dist/ui/assets/{switch-DmDIWykO.js → switch-BO_9wSB8.js} +1 -1
- package/dist/ui/assets/{table-E5IxHhrW.js → table-84HxfWMM.js} +1 -1
- package/dist/ui/assets/tanstack-CkjseTWE.js +4 -0
- package/dist/ui/assets/updates-BgPdUQoA.js +1 -0
- package/dist/ui/index.html +4 -4
- package/dist/utils/claude-config-path.d.ts +2 -0
- package/dist/utils/claude-config-path.d.ts.map +1 -1
- package/dist/utils/claude-config-path.js +6 -1
- package/dist/utils/claude-config-path.js.map +1 -1
- package/dist/utils/websearch/claude-tool-args.d.ts +5 -0
- package/dist/utils/websearch/claude-tool-args.d.ts.map +1 -0
- package/dist/utils/websearch/claude-tool-args.js +125 -0
- package/dist/utils/websearch/claude-tool-args.js.map +1 -0
- package/dist/utils/websearch/hook-env.d.ts.map +1 -1
- package/dist/utils/websearch/hook-env.js +8 -0
- package/dist/utils/websearch/hook-env.js.map +1 -1
- package/dist/utils/websearch/hook-installer.d.ts +3 -2
- package/dist/utils/websearch/hook-installer.d.ts.map +1 -1
- package/dist/utils/websearch/hook-installer.js +3 -2
- package/dist/utils/websearch/hook-installer.js.map +1 -1
- package/dist/utils/websearch/index.d.ts +3 -0
- package/dist/utils/websearch/index.d.ts.map +1 -1
- package/dist/utils/websearch/index.js +23 -2
- package/dist/utils/websearch/index.js.map +1 -1
- package/dist/utils/websearch/mcp-installer.d.ts +14 -0
- package/dist/utils/websearch/mcp-installer.d.ts.map +1 -0
- package/dist/utils/websearch/mcp-installer.js +351 -0
- package/dist/utils/websearch/mcp-installer.js.map +1 -0
- package/dist/utils/websearch/profile-hook-injector.d.ts +5 -3
- package/dist/utils/websearch/profile-hook-injector.d.ts.map +1 -1
- package/dist/utils/websearch/profile-hook-injector.js +5 -3
- package/dist/utils/websearch/profile-hook-injector.js.map +1 -1
- package/dist/utils/websearch/status.d.ts.map +1 -1
- package/dist/utils/websearch/status.js +67 -1
- package/dist/utils/websearch/status.js.map +1 -1
- package/dist/utils/websearch/trace.d.ts +23 -0
- package/dist/utils/websearch/trace.d.ts.map +1 -0
- package/dist/utils/websearch/trace.js +206 -0
- package/dist/utils/websearch/trace.js.map +1 -0
- package/dist/utils/websearch-manager.d.ts +11 -11
- package/dist/utils/websearch-manager.d.ts.map +1 -1
- package/dist/utils/websearch-manager.js +32 -17
- package/dist/utils/websearch-manager.js.map +1 -1
- package/dist/web-server/index.d.ts.map +1 -1
- package/dist/web-server/index.js +3 -0
- package/dist/web-server/index.js.map +1 -1
- package/dist/web-server/routes/account-routes.d.ts.map +1 -1
- package/dist/web-server/routes/account-routes.js +2 -1
- 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 +1 -1
- package/dist/web-server/routes/cliproxy-auth-routes.js.map +1 -1
- package/dist/web-server/routes/cliproxy-local-proxy.d.ts +20 -0
- package/dist/web-server/routes/cliproxy-local-proxy.d.ts.map +1 -0
- package/dist/web-server/routes/cliproxy-local-proxy.js +117 -0
- package/dist/web-server/routes/cliproxy-local-proxy.js.map +1 -0
- package/dist/web-server/services/codex-dashboard-service.d.ts.map +1 -1
- package/dist/web-server/services/codex-dashboard-service.js +27 -8
- package/dist/web-server/services/codex-dashboard-service.js.map +1 -1
- package/lib/hooks/websearch-transformer.cjs +660 -96
- package/lib/mcp/ccs-websearch-server.cjs +339 -0
- package/package.json +2 -1
- package/scripts/github/normalize-ai-review-output.mjs +328 -0
- package/dist/ui/assets/cliproxy-VYe0Qov1.js +0 -3
- package/dist/ui/assets/cliproxy-control-panel-FVIQcFti.js +0 -1
- package/dist/ui/assets/codex-D2yIwOs4.js +0 -27
- package/dist/ui/assets/icons-EMBHZkGo.js +0 -1
- package/dist/ui/assets/index-6dNBcNC3.js +0 -1
- package/dist/ui/assets/index-BAuT6yuc.css +0 -1
- package/dist/ui/assets/shared-qizFb9Ye.js +0 -8
- package/dist/ui/assets/tanstack-B8i0evp-.js +0 -4
- package/dist/ui/assets/updates-2Uu4Mgtg.js +0 -1
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const { spawnSync } = require('child_process');
|
|
18
|
+
const { createHash } = require('crypto');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
18
22
|
|
|
19
23
|
const isWindows = process.platform === 'win32';
|
|
20
24
|
const DEFAULT_TIMEOUT_SEC = 55;
|
|
@@ -26,6 +30,13 @@ const DDG_URL = 'https://html.duckduckgo.com/html/';
|
|
|
26
30
|
const BRAVE_URL = 'https://api.search.brave.com/res/v1/web/search';
|
|
27
31
|
const USER_AGENT =
|
|
28
32
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
33
|
+
const PROVIDER_STATE_FILE = 'websearch-provider-state.json';
|
|
34
|
+
const SHORT_RETRY_AFTER_MAX_SEC = 3;
|
|
35
|
+
const TRANSIENT_RETRY_DELAY_MS = 750;
|
|
36
|
+
const TRANSIENT_RETRY_ATTEMPTS = 1;
|
|
37
|
+
const DEFAULT_RATE_LIMIT_COOLDOWN_SEC = 120;
|
|
38
|
+
const DEFAULT_QUOTA_COOLDOWN_SEC = 900;
|
|
39
|
+
const MAX_PROVIDER_COOLDOWN_SEC = 60 * 60;
|
|
29
40
|
|
|
30
41
|
const SHARED_INSTRUCTIONS = `Instructions:
|
|
31
42
|
1. Search the web for current, up-to-date information
|
|
@@ -64,12 +75,246 @@ function debug(message) {
|
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
77
|
|
|
67
|
-
function
|
|
68
|
-
if (process.env.
|
|
78
|
+
function getCcsDirPath() {
|
|
79
|
+
if ((process.env.CCS_DIR || '').trim()) {
|
|
80
|
+
return path.resolve(process.env.CCS_DIR.trim());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if ((process.env.CCS_HOME || '').trim()) {
|
|
84
|
+
return path.join(path.resolve(process.env.CCS_HOME.trim()), '.ccs');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const home = (process.env.HOME || process.env.USERPROFILE || '').trim();
|
|
88
|
+
if (home) {
|
|
89
|
+
return path.join(home, '.ccs');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return path.join(process.cwd(), '.ccs');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isTraceEnabled() {
|
|
96
|
+
return process.env.CCS_WEBSEARCH_TRACE === '1' || process.env.CCS_DEBUG === '1';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeSafePrefix(inputPath) {
|
|
100
|
+
return `${path.resolve(inputPath)}${path.sep}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getSafeTracePrefixes() {
|
|
104
|
+
return [
|
|
105
|
+
normalizeSafePrefix(path.join(getCcsDirPath(), 'logs')),
|
|
106
|
+
normalizeSafePrefix(os.tmpdir()),
|
|
107
|
+
normalizeSafePrefix('/var/log'),
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getProviderStatePath() {
|
|
112
|
+
return path.join(getCcsDirPath(), 'cache', PROVIDER_STATE_FILE);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readProviderState() {
|
|
116
|
+
try {
|
|
117
|
+
const statePath = getProviderStatePath();
|
|
118
|
+
if (!fs.existsSync(statePath)) {
|
|
119
|
+
return { cooldowns: {} };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const parsed = JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
123
|
+
const cooldowns =
|
|
124
|
+
parsed && typeof parsed === 'object' && parsed.cooldowns && typeof parsed.cooldowns === 'object'
|
|
125
|
+
? parsed.cooldowns
|
|
126
|
+
: {};
|
|
127
|
+
return { cooldowns };
|
|
128
|
+
} catch {
|
|
129
|
+
return { cooldowns: {} };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function writeProviderState(state) {
|
|
134
|
+
try {
|
|
135
|
+
const statePath = getProviderStatePath();
|
|
136
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
137
|
+
const tempPath = `${statePath}.${process.pid}.${Date.now()}.tmp`;
|
|
138
|
+
fs.writeFileSync(tempPath, JSON.stringify(state, null, 2) + '\n', 'utf8');
|
|
139
|
+
fs.renameSync(tempPath, statePath);
|
|
140
|
+
} catch {
|
|
141
|
+
// Best-effort only.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sanitizeProviderState(state) {
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
const nextCooldowns = {};
|
|
148
|
+
let changed = false;
|
|
149
|
+
|
|
150
|
+
for (const [providerId, entry] of Object.entries(state.cooldowns || {})) {
|
|
151
|
+
if (!entry || typeof entry !== 'object') {
|
|
152
|
+
changed = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const until = Number.parseInt(String(entry.until || ''), 10);
|
|
157
|
+
if (!Number.isFinite(until) || until <= now) {
|
|
158
|
+
changed = true;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
nextCooldowns[providerId] = {
|
|
163
|
+
until,
|
|
164
|
+
reason: typeof entry.reason === 'string' ? entry.reason : 'rate_limited',
|
|
165
|
+
updatedAt: Number.parseInt(String(entry.updatedAt || ''), 10) || now,
|
|
166
|
+
sourceError: typeof entry.sourceError === 'string' ? entry.sourceError : '',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
state: { cooldowns: nextCooldowns },
|
|
172
|
+
changed,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getProviderCooldown(providerId) {
|
|
177
|
+
const { state, changed } = sanitizeProviderState(readProviderState());
|
|
178
|
+
if (changed) {
|
|
179
|
+
writeProviderState(state);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return state.cooldowns[providerId] || null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function clearProviderCooldown(providerId) {
|
|
186
|
+
const { state } = sanitizeProviderState(readProviderState());
|
|
187
|
+
if (!(providerId in state.cooldowns)) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
delete state.cooldowns[providerId];
|
|
192
|
+
writeProviderState(state);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function applyProviderCooldown(providerId, cooldownSec, reason, sourceError) {
|
|
196
|
+
const clampedCooldownSec = Math.max(
|
|
197
|
+
1,
|
|
198
|
+
Math.min(MAX_PROVIDER_COOLDOWN_SEC, Math.floor(cooldownSec))
|
|
199
|
+
);
|
|
200
|
+
const { state } = sanitizeProviderState(readProviderState());
|
|
201
|
+
const until = Date.now() + clampedCooldownSec * 1000;
|
|
202
|
+
state.cooldowns[providerId] = {
|
|
203
|
+
until,
|
|
204
|
+
reason,
|
|
205
|
+
updatedAt: Date.now(),
|
|
206
|
+
sourceError: sourceError || '',
|
|
207
|
+
};
|
|
208
|
+
writeProviderState(state);
|
|
209
|
+
return until;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function sleep(ms) {
|
|
213
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getAllowedTraceFileOverride() {
|
|
217
|
+
const configured = (process.env.CCS_WEBSEARCH_TRACE_FILE || '').trim();
|
|
218
|
+
if (!configured) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const resolved = path.resolve(configured);
|
|
223
|
+
if (getSafeTracePrefixes().some((prefix) => resolved.startsWith(prefix))) {
|
|
224
|
+
return resolved;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getTraceFilePath() {
|
|
231
|
+
const fallback = path.join(getCcsDirPath(), 'logs', 'websearch-trace.jsonl');
|
|
232
|
+
return getAllowedTraceFileOverride() || fallback;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function traceWebSearchEvent(event, payload = {}) {
|
|
236
|
+
if (!isTraceEnabled()) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const traceFilePath = getTraceFilePath();
|
|
242
|
+
fs.mkdirSync(path.dirname(traceFilePath), { recursive: true });
|
|
243
|
+
fs.appendFileSync(
|
|
244
|
+
traceFilePath,
|
|
245
|
+
JSON.stringify({
|
|
246
|
+
at: new Date().toISOString(),
|
|
247
|
+
event,
|
|
248
|
+
launchId: process.env.CCS_WEBSEARCH_TRACE_LAUNCH_ID || null,
|
|
249
|
+
launcher: process.env.CCS_WEBSEARCH_TRACE_LAUNCHER || null,
|
|
250
|
+
profileType: process.env.CCS_PROFILE_TYPE || null,
|
|
251
|
+
pid: process.pid,
|
|
252
|
+
...payload,
|
|
253
|
+
}) + '\n',
|
|
254
|
+
'utf8'
|
|
255
|
+
);
|
|
256
|
+
} catch {
|
|
257
|
+
// Best-effort only.
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function readHeaderValue(headers, headerName) {
|
|
262
|
+
if (!headers) {
|
|
263
|
+
return '';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (typeof headers.get === 'function') {
|
|
267
|
+
return headers.get(headerName) || '';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const direct = headers[headerName] ?? headers[String(headerName).toLowerCase()];
|
|
271
|
+
if (Array.isArray(direct)) {
|
|
272
|
+
return direct[0] || '';
|
|
273
|
+
}
|
|
274
|
+
return typeof direct === 'string' ? direct : '';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseRetryAfterSeconds(rawValue) {
|
|
278
|
+
const value = String(rawValue || '').trim();
|
|
279
|
+
if (!value) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const asSeconds = Number.parseInt(value, 10);
|
|
284
|
+
if (Number.isFinite(asSeconds) && asSeconds > 0) {
|
|
285
|
+
return asSeconds;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const asDate = Date.parse(value);
|
|
289
|
+
if (Number.isFinite(asDate)) {
|
|
290
|
+
const deltaSec = Math.ceil((asDate - Date.now()) / 1000);
|
|
291
|
+
return deltaSec > 0 ? deltaSec : null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getQueryFingerprint(query) {
|
|
298
|
+
const normalizedQuery = typeof query === 'string' ? query.trim() : '';
|
|
299
|
+
return {
|
|
300
|
+
queryHash: normalizedQuery
|
|
301
|
+
? createHash('sha256').update(normalizedQuery).digest('hex').slice(0, 16)
|
|
302
|
+
: null,
|
|
303
|
+
queryLength: normalizedQuery.length,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function getSkipReason() {
|
|
308
|
+
if (process.env.CCS_WEBSEARCH_SKIP === '1') return 'skip_flag';
|
|
69
309
|
const profileType = process.env.CCS_PROFILE_TYPE;
|
|
70
|
-
if (profileType === 'account'
|
|
71
|
-
if (
|
|
72
|
-
return
|
|
310
|
+
if (profileType === 'account') return 'native_account_profile';
|
|
311
|
+
if (profileType === 'default') return 'native_default_profile';
|
|
312
|
+
if (process.env.CCS_WEBSEARCH_ENABLED === '0') return 'disabled';
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function shouldSkipHook() {
|
|
317
|
+
return getSkipReason() !== null;
|
|
73
318
|
}
|
|
74
319
|
|
|
75
320
|
function isCliAvailable(cmd) {
|
|
@@ -183,23 +428,58 @@ function extractDuckDuckGoResults(html, count) {
|
|
|
183
428
|
}
|
|
184
429
|
|
|
185
430
|
function formatStructuredSearchResults(query, providerName, results) {
|
|
431
|
+
const lines = [
|
|
432
|
+
'CCS local WebSearch evidence',
|
|
433
|
+
`Provider: ${providerName}`,
|
|
434
|
+
`Query: "${query}"`,
|
|
435
|
+
`Result count: ${results.length}`,
|
|
436
|
+
'',
|
|
437
|
+
];
|
|
438
|
+
|
|
186
439
|
if (!results.length) {
|
|
187
|
-
|
|
440
|
+
lines.push('No results found.');
|
|
441
|
+
return lines.join('\n');
|
|
188
442
|
}
|
|
189
443
|
|
|
190
|
-
const lines = [`Search results for "${query}" via ${providerName}:`, ''];
|
|
191
444
|
for (const [index, result] of results.entries()) {
|
|
192
445
|
lines.push(`${index + 1}. ${result.title}`);
|
|
193
|
-
lines.push(` ${result.url}`);
|
|
446
|
+
lines.push(` URL: ${result.url}`);
|
|
194
447
|
if (result.description) {
|
|
195
|
-
lines.push(` ${result.description}`);
|
|
448
|
+
lines.push(` Snippet: ${result.description}`);
|
|
196
449
|
}
|
|
197
450
|
lines.push('');
|
|
198
451
|
}
|
|
199
|
-
lines.push('Use these results to answer the user directly.');
|
|
200
452
|
return lines.join('\n');
|
|
201
453
|
}
|
|
202
454
|
|
|
455
|
+
function buildSuccessHookOutput(query, providerName, content) {
|
|
456
|
+
return {
|
|
457
|
+
hookSpecificOutput: {
|
|
458
|
+
hookEventName: 'PreToolUse',
|
|
459
|
+
permissionDecision: 'deny',
|
|
460
|
+
permissionDecisionReason: `CCS already retrieved WebSearch results locally via ${providerName}. Use the provided context instead of calling native WebSearch for "${query}".`,
|
|
461
|
+
additionalContext: content,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function buildFailureHookOutput(query, errors) {
|
|
467
|
+
const detail = errors.map((entry) => `${entry.provider}: ${entry.error}`).join(' | ');
|
|
468
|
+
return {
|
|
469
|
+
hookSpecificOutput: {
|
|
470
|
+
hookEventName: 'PreToolUse',
|
|
471
|
+
permissionDecision: 'deny',
|
|
472
|
+
permissionDecisionReason: `CCS could not complete local WebSearch for "${query}". Native WebSearch is unavailable for this profile.`,
|
|
473
|
+
additionalContext: `CCS local WebSearch failed for "${query}". Attempted providers: ${detail}`,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function emitHookOutput(output) {
|
|
479
|
+
console.log(JSON.stringify(output));
|
|
480
|
+
process.exit(0);
|
|
481
|
+
}
|
|
482
|
+
|
|
203
483
|
async function fetchWithTimeout(url, options, timeoutMs) {
|
|
204
484
|
const controller = new AbortController();
|
|
205
485
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -239,6 +519,8 @@ async function tryBraveSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
|
239
519
|
return {
|
|
240
520
|
success: false,
|
|
241
521
|
error: `Brave Search returned ${response.status}: ${body.slice(0, 160)}`,
|
|
522
|
+
statusCode: response.status,
|
|
523
|
+
retryAfterSec: parseRetryAfterSeconds(readHeaderValue(response.headers, 'retry-after')),
|
|
242
524
|
};
|
|
243
525
|
}
|
|
244
526
|
|
|
@@ -290,7 +572,12 @@ async function tryExaSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
|
290
572
|
|
|
291
573
|
if (!response.ok) {
|
|
292
574
|
const body = await response.text();
|
|
293
|
-
return {
|
|
575
|
+
return {
|
|
576
|
+
success: false,
|
|
577
|
+
error: `Exa returned ${response.status}: ${body.slice(0, 160)}`,
|
|
578
|
+
statusCode: response.status,
|
|
579
|
+
retryAfterSec: parseRetryAfterSeconds(readHeaderValue(response.headers, 'retry-after')),
|
|
580
|
+
};
|
|
294
581
|
}
|
|
295
582
|
|
|
296
583
|
const body = await response.json();
|
|
@@ -342,7 +629,12 @@ async function tryTavilySearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
|
342
629
|
|
|
343
630
|
if (!response.ok) {
|
|
344
631
|
const body = await response.text();
|
|
345
|
-
return {
|
|
632
|
+
return {
|
|
633
|
+
success: false,
|
|
634
|
+
error: `Tavily returned ${response.status}: ${body.slice(0, 160)}`,
|
|
635
|
+
statusCode: response.status,
|
|
636
|
+
retryAfterSec: parseRetryAfterSeconds(readHeaderValue(response.headers, 'retry-after')),
|
|
637
|
+
};
|
|
346
638
|
}
|
|
347
639
|
|
|
348
640
|
const body = await response.json();
|
|
@@ -379,7 +671,12 @@ async function tryDuckDuckGoSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
|
379
671
|
);
|
|
380
672
|
|
|
381
673
|
if (!response.ok) {
|
|
382
|
-
return {
|
|
674
|
+
return {
|
|
675
|
+
success: false,
|
|
676
|
+
error: `DuckDuckGo returned ${response.status}`,
|
|
677
|
+
statusCode: response.status,
|
|
678
|
+
retryAfterSec: parseRetryAfterSeconds(readHeaderValue(response.headers, 'retry-after')),
|
|
679
|
+
};
|
|
383
680
|
}
|
|
384
681
|
|
|
385
682
|
const html = await response.text();
|
|
@@ -534,39 +831,325 @@ function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
|
534
831
|
}
|
|
535
832
|
|
|
536
833
|
function outputSuccess(query, content, providerName) {
|
|
537
|
-
|
|
538
|
-
decision: 'block',
|
|
539
|
-
reason: `WebSearch handled via ${providerName}`,
|
|
540
|
-
hookSpecificOutput: {
|
|
541
|
-
hookEventName: 'PreToolUse',
|
|
542
|
-
permissionDecision: 'deny',
|
|
543
|
-
permissionDecisionReason: `[WebSearch Result via ${providerName}]\n\nQuery: "${query}"\n\n${content}`,
|
|
544
|
-
},
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
console.log(JSON.stringify(output));
|
|
548
|
-
process.exit(2);
|
|
834
|
+
emitHookOutput(buildSuccessHookOutput(query, providerName, content));
|
|
549
835
|
}
|
|
550
836
|
|
|
551
837
|
function outputAllFailedMessage(query, errors) {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
838
|
+
emitHookOutput(buildFailureHookOutput(query, errors));
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function getConfiguredProviders() {
|
|
842
|
+
return [
|
|
843
|
+
{
|
|
844
|
+
name: 'Exa',
|
|
845
|
+
id: 'exa',
|
|
846
|
+
available: () => isProviderEnabled('exa') && Boolean(getProviderApiKey('exa')),
|
|
847
|
+
fn: tryExaSearch,
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
name: 'Tavily',
|
|
851
|
+
id: 'tavily',
|
|
852
|
+
available: () => isProviderEnabled('tavily') && Boolean(getProviderApiKey('tavily')),
|
|
853
|
+
fn: tryTavilySearch,
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
name: 'Brave Search',
|
|
857
|
+
id: 'brave',
|
|
858
|
+
available: () => isProviderEnabled('brave') && Boolean(getProviderApiKey('brave')),
|
|
859
|
+
fn: tryBraveSearch,
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
name: 'DuckDuckGo',
|
|
863
|
+
id: 'duckduckgo',
|
|
864
|
+
available: () => isProviderEnabled('duckduckgo'),
|
|
865
|
+
fn: tryDuckDuckGoSearch,
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
name: 'Gemini CLI',
|
|
869
|
+
id: 'gemini',
|
|
870
|
+
available: () => isProviderEnabled('gemini') && isCliAvailable('gemini'),
|
|
871
|
+
fn: tryGeminiSearch,
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
name: 'OpenCode',
|
|
875
|
+
id: 'opencode',
|
|
876
|
+
available: () => isProviderEnabled('opencode') && isCliAvailable('opencode'),
|
|
877
|
+
fn: tryOpenCodeSearch,
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
name: 'Grok CLI',
|
|
881
|
+
id: 'grok',
|
|
882
|
+
available: () => isProviderEnabled('grok') && isCliAvailable('grok'),
|
|
883
|
+
fn: tryGrokSearch,
|
|
560
884
|
},
|
|
885
|
+
];
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function looksLikeQuotaExhaustion(errorMessage) {
|
|
889
|
+
const lower = String(errorMessage || '').toLowerCase();
|
|
890
|
+
return (
|
|
891
|
+
(lower.includes('quota') &&
|
|
892
|
+
(lower.includes('exceed') ||
|
|
893
|
+
lower.includes('exhaust') ||
|
|
894
|
+
lower.includes('deplet') ||
|
|
895
|
+
lower.includes('limit') ||
|
|
896
|
+
lower.includes('used up'))) ||
|
|
897
|
+
lower.includes('insufficient credits') ||
|
|
898
|
+
lower.includes('credit balance') ||
|
|
899
|
+
lower.includes('out of credits') ||
|
|
900
|
+
lower.includes('billing hard limit') ||
|
|
901
|
+
lower.includes('monthly usage cap')
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function looksLikeTransientFailure(errorMessage) {
|
|
906
|
+
const lower = String(errorMessage || '').toLowerCase();
|
|
907
|
+
return (
|
|
908
|
+
lower.includes('timed out') ||
|
|
909
|
+
lower.includes('timeout') ||
|
|
910
|
+
lower.includes('temporarily unavailable') ||
|
|
911
|
+
lower.includes('service unavailable') ||
|
|
912
|
+
lower.includes('bad gateway') ||
|
|
913
|
+
lower.includes('gateway timeout') ||
|
|
914
|
+
lower.includes('socket hang up') ||
|
|
915
|
+
lower.includes('econnreset') ||
|
|
916
|
+
lower.includes('fetch failed') ||
|
|
917
|
+
lower.includes('network')
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function classifyProviderFailure(result) {
|
|
922
|
+
const errorMessage = String(result.error || '');
|
|
923
|
+
const statusCode =
|
|
924
|
+
Number.isFinite(result.statusCode) && result.statusCode > 0 ? result.statusCode : null;
|
|
925
|
+
const retryAfterSec = Number.isFinite(result.retryAfterSec) ? result.retryAfterSec : null;
|
|
926
|
+
|
|
927
|
+
if (looksLikeQuotaExhaustion(errorMessage)) {
|
|
928
|
+
return {
|
|
929
|
+
kind: 'cooldown',
|
|
930
|
+
reason: 'quota_exhausted',
|
|
931
|
+
cooldownSec: retryAfterSec || DEFAULT_QUOTA_COOLDOWN_SEC,
|
|
932
|
+
retryAfterSec,
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (statusCode === 429 || /too many requests|rate limit/i.test(errorMessage)) {
|
|
937
|
+
if (retryAfterSec && retryAfterSec <= SHORT_RETRY_AFTER_MAX_SEC) {
|
|
938
|
+
return {
|
|
939
|
+
kind: 'retry',
|
|
940
|
+
delayMs: retryAfterSec * 1000,
|
|
941
|
+
reason: 'rate_limited_short_backoff',
|
|
942
|
+
retryAfterSec,
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
return {
|
|
947
|
+
kind: 'cooldown',
|
|
948
|
+
reason: 'rate_limited',
|
|
949
|
+
cooldownSec: retryAfterSec || DEFAULT_RATE_LIMIT_COOLDOWN_SEC,
|
|
950
|
+
retryAfterSec,
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (
|
|
955
|
+
(statusCode && [502, 503, 504].includes(statusCode)) ||
|
|
956
|
+
looksLikeTransientFailure(errorMessage)
|
|
957
|
+
) {
|
|
958
|
+
return {
|
|
959
|
+
kind: 'retry',
|
|
960
|
+
delayMs: TRANSIENT_RETRY_DELAY_MS,
|
|
961
|
+
reason: 'transient_failure',
|
|
962
|
+
retryAfterSec,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return {
|
|
967
|
+
kind: 'fail',
|
|
968
|
+
reason: 'non_retryable',
|
|
969
|
+
retryAfterSec,
|
|
561
970
|
};
|
|
971
|
+
}
|
|
562
972
|
|
|
563
|
-
|
|
564
|
-
|
|
973
|
+
async function runProviderWithPolicy(provider, query, timeoutSec, fingerprint) {
|
|
974
|
+
for (let attempt = 0; attempt <= TRANSIENT_RETRY_ATTEMPTS; attempt += 1) {
|
|
975
|
+
traceWebSearchEvent('websearch_provider_attempt', {
|
|
976
|
+
source: 'provider',
|
|
977
|
+
providerId: provider.id,
|
|
978
|
+
providerName: provider.name,
|
|
979
|
+
attempt: attempt + 1,
|
|
980
|
+
...fingerprint,
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
const result = await provider.fn(query, timeoutSec);
|
|
984
|
+
if (result.success) {
|
|
985
|
+
clearProviderCooldown(provider.id);
|
|
986
|
+
return result;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const policy = classifyProviderFailure(result);
|
|
990
|
+
if (policy.kind === 'retry' && attempt < TRANSIENT_RETRY_ATTEMPTS) {
|
|
991
|
+
traceWebSearchEvent('websearch_provider_retry_scheduled', {
|
|
992
|
+
source: 'provider',
|
|
993
|
+
providerId: provider.id,
|
|
994
|
+
providerName: provider.name,
|
|
995
|
+
attempt: attempt + 1,
|
|
996
|
+
delayMs: policy.delayMs,
|
|
997
|
+
reason: policy.reason,
|
|
998
|
+
retryAfterSec: policy.retryAfterSec,
|
|
999
|
+
...fingerprint,
|
|
1000
|
+
});
|
|
1001
|
+
await sleep(policy.delayMs);
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (policy.kind === 'retry' && policy.reason === 'rate_limited_short_backoff') {
|
|
1006
|
+
const cooldownSec = policy.retryAfterSec || DEFAULT_RATE_LIMIT_COOLDOWN_SEC;
|
|
1007
|
+
const until = applyProviderCooldown(provider.id, cooldownSec, 'rate_limited', result.error);
|
|
1008
|
+
traceWebSearchEvent('websearch_provider_cooldown_applied', {
|
|
1009
|
+
source: 'provider',
|
|
1010
|
+
providerId: provider.id,
|
|
1011
|
+
providerName: provider.name,
|
|
1012
|
+
cooldownUntil: until,
|
|
1013
|
+
cooldownSec,
|
|
1014
|
+
reason: 'rate_limited',
|
|
1015
|
+
retryAfterSec: policy.retryAfterSec,
|
|
1016
|
+
afterRetryExhausted: true,
|
|
1017
|
+
...fingerprint,
|
|
1018
|
+
});
|
|
1019
|
+
return {
|
|
1020
|
+
...result,
|
|
1021
|
+
error: `${result.error} (cooldown ${cooldownSec}s)`,
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (policy.kind === 'cooldown') {
|
|
1026
|
+
const until = applyProviderCooldown(
|
|
1027
|
+
provider.id,
|
|
1028
|
+
policy.cooldownSec,
|
|
1029
|
+
policy.reason,
|
|
1030
|
+
result.error
|
|
1031
|
+
);
|
|
1032
|
+
traceWebSearchEvent('websearch_provider_cooldown_applied', {
|
|
1033
|
+
source: 'provider',
|
|
1034
|
+
providerId: provider.id,
|
|
1035
|
+
providerName: provider.name,
|
|
1036
|
+
cooldownUntil: until,
|
|
1037
|
+
cooldownSec: policy.cooldownSec,
|
|
1038
|
+
reason: policy.reason,
|
|
1039
|
+
retryAfterSec: policy.retryAfterSec,
|
|
1040
|
+
...fingerprint,
|
|
1041
|
+
});
|
|
1042
|
+
return {
|
|
1043
|
+
...result,
|
|
1044
|
+
error: `${result.error} (cooldown ${policy.cooldownSec}s)`,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return result;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
return { success: false, error: 'Provider retry policy exhausted' };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function getActiveProviders() {
|
|
1055
|
+
return getConfiguredProviders().filter((provider) => !getProviderCooldown(provider.id) && provider.available());
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function getActiveProviderIds() {
|
|
1059
|
+
return getActiveProviders().map((provider) => provider.id);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function hasAnyActiveProviders() {
|
|
1063
|
+
return getActiveProviders().length > 0;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
async function runLocalWebSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
1067
|
+
const fingerprint = getQueryFingerprint(query);
|
|
1068
|
+
const configuredProviders = getConfiguredProviders();
|
|
1069
|
+
const activeProviders = [];
|
|
1070
|
+
|
|
1071
|
+
for (const provider of configuredProviders) {
|
|
1072
|
+
const cooldown = getProviderCooldown(provider.id);
|
|
1073
|
+
if (cooldown) {
|
|
1074
|
+
traceWebSearchEvent('websearch_provider_cooldown_skip', {
|
|
1075
|
+
source: 'provider',
|
|
1076
|
+
providerId: provider.id,
|
|
1077
|
+
providerName: provider.name,
|
|
1078
|
+
cooldownUntil: cooldown.until,
|
|
1079
|
+
cooldownReason: cooldown.reason,
|
|
1080
|
+
remainingMs: Math.max(0, cooldown.until - Date.now()),
|
|
1081
|
+
...fingerprint,
|
|
1082
|
+
});
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (provider.available()) {
|
|
1087
|
+
activeProviders.push(provider);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
debug(
|
|
1092
|
+
`Enabled providers: ${activeProviders.map((provider) => provider.name).join(', ') || 'none'}`
|
|
1093
|
+
);
|
|
1094
|
+
traceWebSearchEvent('websearch_provider_run_started', {
|
|
1095
|
+
source: 'provider',
|
|
1096
|
+
activeProviderIds: activeProviders.map((provider) => provider.id),
|
|
1097
|
+
...fingerprint,
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
if (activeProviders.length === 0) {
|
|
1101
|
+
traceWebSearchEvent('websearch_provider_run_unavailable', {
|
|
1102
|
+
source: 'provider',
|
|
1103
|
+
activeProviderIds: [],
|
|
1104
|
+
...fingerprint,
|
|
1105
|
+
});
|
|
1106
|
+
return { success: false, noActiveProviders: true, errors: [] };
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const errors = [];
|
|
1110
|
+
for (const provider of activeProviders) {
|
|
1111
|
+
debug(`Trying ${provider.name}`);
|
|
1112
|
+
const result = await runProviderWithPolicy(provider, query, timeoutSec, fingerprint);
|
|
1113
|
+
if (result.success) {
|
|
1114
|
+
traceWebSearchEvent('websearch_provider_success', {
|
|
1115
|
+
source: 'provider',
|
|
1116
|
+
providerId: provider.id,
|
|
1117
|
+
providerName: provider.name,
|
|
1118
|
+
...fingerprint,
|
|
1119
|
+
});
|
|
1120
|
+
return {
|
|
1121
|
+
success: true,
|
|
1122
|
+
providerId: provider.id,
|
|
1123
|
+
providerName: provider.name,
|
|
1124
|
+
content: result.content,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
traceWebSearchEvent('websearch_provider_failure', {
|
|
1128
|
+
source: 'provider',
|
|
1129
|
+
providerId: provider.id,
|
|
1130
|
+
providerName: provider.name,
|
|
1131
|
+
error: result.error,
|
|
1132
|
+
...fingerprint,
|
|
1133
|
+
});
|
|
1134
|
+
errors.push({ provider: provider.name, error: result.error });
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
traceWebSearchEvent('websearch_provider_run_failed', {
|
|
1138
|
+
source: 'provider',
|
|
1139
|
+
errorCount: errors.length,
|
|
1140
|
+
activeProviderIds: activeProviders.map((provider) => provider.id),
|
|
1141
|
+
...fingerprint,
|
|
1142
|
+
});
|
|
1143
|
+
return { success: false, noActiveProviders: false, errors };
|
|
565
1144
|
}
|
|
566
1145
|
|
|
567
1146
|
async function processHook(input) {
|
|
568
1147
|
try {
|
|
569
1148
|
if (shouldSkipHook()) {
|
|
1149
|
+
traceWebSearchEvent('websearch_hook_skipped', {
|
|
1150
|
+
source: 'hook',
|
|
1151
|
+
reason: getSkipReason(),
|
|
1152
|
+
});
|
|
570
1153
|
process.exit(0);
|
|
571
1154
|
}
|
|
572
1155
|
|
|
@@ -580,78 +1163,47 @@ async function processHook(input) {
|
|
|
580
1163
|
process.exit(0);
|
|
581
1164
|
}
|
|
582
1165
|
|
|
1166
|
+
traceWebSearchEvent('websearch_hook_invoked', {
|
|
1167
|
+
source: 'hook',
|
|
1168
|
+
...getQueryFingerprint(query),
|
|
1169
|
+
});
|
|
1170
|
+
|
|
583
1171
|
const timeout = Number.parseInt(
|
|
584
1172
|
process.env.CCS_WEBSEARCH_TIMEOUT || `${DEFAULT_TIMEOUT_SEC}`,
|
|
585
1173
|
10
|
|
586
1174
|
);
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
name: 'Tavily',
|
|
596
|
-
id: 'tavily',
|
|
597
|
-
available: () => isProviderEnabled('tavily') && Boolean(getProviderApiKey('tavily')),
|
|
598
|
-
fn: tryTavilySearch,
|
|
599
|
-
},
|
|
600
|
-
{
|
|
601
|
-
name: 'Brave Search',
|
|
602
|
-
id: 'brave',
|
|
603
|
-
available: () => isProviderEnabled('brave') && Boolean(getProviderApiKey('brave')),
|
|
604
|
-
fn: tryBraveSearch,
|
|
605
|
-
},
|
|
606
|
-
{
|
|
607
|
-
name: 'DuckDuckGo',
|
|
608
|
-
id: 'duckduckgo',
|
|
609
|
-
available: () => isProviderEnabled('duckduckgo'),
|
|
610
|
-
fn: tryDuckDuckGoSearch,
|
|
611
|
-
},
|
|
612
|
-
{
|
|
613
|
-
name: 'Gemini CLI',
|
|
614
|
-
id: 'gemini',
|
|
615
|
-
available: () => isProviderEnabled('gemini') && isCliAvailable('gemini'),
|
|
616
|
-
fn: tryGeminiSearch,
|
|
617
|
-
},
|
|
618
|
-
{
|
|
619
|
-
name: 'OpenCode',
|
|
620
|
-
id: 'opencode',
|
|
621
|
-
available: () => isProviderEnabled('opencode') && isCliAvailable('opencode'),
|
|
622
|
-
fn: tryOpenCodeSearch,
|
|
623
|
-
},
|
|
624
|
-
{
|
|
625
|
-
name: 'Grok CLI',
|
|
626
|
-
id: 'grok',
|
|
627
|
-
available: () => isProviderEnabled('grok') && isCliAvailable('grok'),
|
|
628
|
-
fn: tryGrokSearch,
|
|
629
|
-
},
|
|
630
|
-
];
|
|
631
|
-
|
|
632
|
-
const activeProviders = providers.filter((provider) => provider.available());
|
|
633
|
-
debug(
|
|
634
|
-
`Enabled providers: ${activeProviders.map((provider) => provider.name).join(', ') || 'none'}`
|
|
635
|
-
);
|
|
636
|
-
|
|
637
|
-
if (activeProviders.length === 0) {
|
|
1175
|
+
const result = await runLocalWebSearch(query, timeout);
|
|
1176
|
+
if (result.noActiveProviders) {
|
|
1177
|
+
traceWebSearchEvent('websearch_hook_no_active_providers', {
|
|
1178
|
+
source: 'hook',
|
|
1179
|
+
...getQueryFingerprint(query),
|
|
1180
|
+
});
|
|
638
1181
|
process.exit(0);
|
|
639
1182
|
}
|
|
640
1183
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1184
|
+
if (result.success) {
|
|
1185
|
+
traceWebSearchEvent('websearch_hook_success', {
|
|
1186
|
+
source: 'hook',
|
|
1187
|
+
providerId: result.providerId,
|
|
1188
|
+
providerName: result.providerName,
|
|
1189
|
+
...getQueryFingerprint(query),
|
|
1190
|
+
});
|
|
1191
|
+
outputSuccess(query, result.content, result.providerName);
|
|
1192
|
+
return;
|
|
650
1193
|
}
|
|
651
1194
|
|
|
652
|
-
|
|
1195
|
+
traceWebSearchEvent('websearch_hook_failure', {
|
|
1196
|
+
source: 'hook',
|
|
1197
|
+
errorCount: result.errors.length,
|
|
1198
|
+
...getQueryFingerprint(query),
|
|
1199
|
+
});
|
|
1200
|
+
outputAllFailedMessage(query, result.errors);
|
|
653
1201
|
} catch (error) {
|
|
654
1202
|
debug(`Hook error: ${error.message}`);
|
|
1203
|
+
traceWebSearchEvent('websearch_hook_error', {
|
|
1204
|
+
source: 'hook',
|
|
1205
|
+
error: error.message,
|
|
1206
|
+
});
|
|
655
1207
|
process.exit(0);
|
|
656
1208
|
}
|
|
657
1209
|
}
|
|
@@ -675,8 +1227,20 @@ if (require.main === module) {
|
|
|
675
1227
|
}
|
|
676
1228
|
|
|
677
1229
|
module.exports = {
|
|
1230
|
+
buildFailureHookOutput,
|
|
1231
|
+
buildSuccessHookOutput,
|
|
678
1232
|
extractDuckDuckGoResults,
|
|
679
1233
|
formatStructuredSearchResults,
|
|
1234
|
+
getActiveProviders,
|
|
1235
|
+
hasAnyActiveProviders,
|
|
1236
|
+
runLocalWebSearch,
|
|
1237
|
+
shouldSkipHook,
|
|
1238
|
+
getActiveProviderIds,
|
|
1239
|
+
classifyProviderFailure,
|
|
1240
|
+
getQueryFingerprint,
|
|
1241
|
+
getSkipReason,
|
|
1242
|
+
parseRetryAfterSeconds,
|
|
1243
|
+
traceWebSearchEvent,
|
|
680
1244
|
tryExaSearch,
|
|
681
1245
|
tryTavilySearch,
|
|
682
1246
|
tryDuckDuckGoSearch,
|