@kaitranntt/ccs 5.20.0 → 6.0.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -0
- package/VERSION +1 -1
- package/dist/ccs.js +9 -1
- package/dist/ccs.js.map +1 -1
- package/dist/cliproxy/account-manager.d.ts.map +1 -1
- package/dist/cliproxy/account-manager.js +3 -1
- package/dist/cliproxy/account-manager.js.map +1 -1
- package/dist/cliproxy/cliproxy-executor.d.ts.map +1 -1
- package/dist/cliproxy/cliproxy-executor.js +145 -76
- package/dist/cliproxy/cliproxy-executor.js.map +1 -1
- package/dist/cliproxy/session-tracker.d.ts +54 -0
- package/dist/cliproxy/session-tracker.d.ts.map +1 -0
- package/dist/cliproxy/session-tracker.js +228 -0
- package/dist/cliproxy/session-tracker.js.map +1 -0
- package/dist/cliproxy/stats-fetcher.d.ts +19 -0
- package/dist/cliproxy/stats-fetcher.d.ts.map +1 -1
- package/dist/cliproxy/stats-fetcher.js +41 -3
- package/dist/cliproxy/stats-fetcher.js.map +1 -1
- package/dist/config/unified-config-loader.d.ts +33 -0
- package/dist/config/unified-config-loader.d.ts.map +1 -1
- package/dist/config/unified-config-loader.js +105 -3
- package/dist/config/unified-config-loader.js.map +1 -1
- package/dist/config/unified-config-types.d.ts +75 -1
- package/dist/config/unified-config-types.d.ts.map +1 -1
- package/dist/config/unified-config-types.js +21 -1
- package/dist/config/unified-config-types.js.map +1 -1
- package/dist/ui/assets/{accounts-achdtDUJ.js → accounts-CZmhPg-s.js} +1 -1
- package/dist/ui/assets/{analytics-BVlC8Y7-.js → analytics-BeHvdYDb.js} +34 -34
- package/dist/ui/assets/api-BJE0UHIr.js +1 -0
- package/dist/ui/assets/card-CB1ifO1P.js +1 -0
- package/dist/ui/assets/cliproxy-B7L-XME_.js +1 -0
- package/dist/ui/assets/cliproxy-control-panel-D4MuMEOJ.js +1 -0
- package/dist/ui/assets/code-editor-DxoT483F.js +2 -0
- package/dist/ui/assets/{form-utils-DKkU3nz7.js → form-utils-CKETQmt9.js} +1 -1
- package/dist/ui/assets/{health-Xbq8eUat.js → health-Dl8j1bVN.js} +1 -1
- package/dist/ui/assets/icons-Dzu62U5U.js +1 -0
- package/dist/ui/assets/index-Arbf-zPY.js +46 -0
- package/dist/ui/assets/index-B2VhinkK.css +1 -0
- package/dist/ui/assets/{radix-ui-OFtPgiRV.js → radix-ui-Bhyw9pC9.js} +2 -2
- package/dist/ui/assets/{react-vendor-CjrBBxxX.js → react-vendor-DadlvKzT.js} +1 -1
- package/dist/ui/assets/settings-D3HNz4K7.js +1 -0
- package/dist/ui/assets/shared-DiXNS0L2.js +1 -0
- package/dist/ui/assets/{tanstack-DMWkeNzM.js → tanstack-DZ3a6J0N.js} +1 -1
- package/dist/ui/index.html +7 -7
- package/dist/utils/shell-executor.d.ts.map +1 -1
- package/dist/utils/shell-executor.js +6 -1
- package/dist/utils/shell-executor.js.map +1 -1
- package/dist/utils/websearch-manager.d.ts +197 -0
- package/dist/utils/websearch-manager.d.ts.map +1 -0
- package/dist/utils/websearch-manager.js +685 -0
- package/dist/utils/websearch-manager.js.map +1 -0
- package/dist/web-server/health-service.d.ts.map +1 -1
- package/dist/web-server/health-service.js +50 -0
- package/dist/web-server/health-service.js.map +1 -1
- package/dist/web-server/routes.d.ts.map +1 -1
- package/dist/web-server/routes.js +169 -1
- package/dist/web-server/routes.js.map +1 -1
- package/dist/web-server/shutdown.d.ts +4 -4
- package/dist/web-server/shutdown.d.ts.map +1 -1
- package/dist/web-server/shutdown.js +9 -17
- package/dist/web-server/shutdown.js.map +1 -1
- package/lib/hooks/block-websearch.cjs +75 -0
- package/lib/hooks/websearch-transformer.cjs +600 -0
- package/package.json +2 -1
- package/scripts/dev-install.sh +57 -5
- package/scripts/preinstall.js +59 -0
- package/dist/ui/assets/api-BwWsFLoC.js +0 -1
- package/dist/ui/assets/cliproxy-DuaxYcVk.js +0 -1
- package/dist/ui/assets/cliproxy-control-panel-DTiBzA5u.js +0 -1
- package/dist/ui/assets/code-editor-D7CYoILm.js +0 -36
- package/dist/ui/assets/icons-Alnq4BWm.js +0 -1
- package/dist/ui/assets/index-B1gvMo-b.css +0 -1
- package/dist/ui/assets/index-C2RMogI8.js +0 -12
- package/dist/ui/assets/settings-95g3F5Gk.js +0 -1
- package/dist/ui/assets/shared-Cg5XjdQM.js +0 -1
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CCS WebSearch Hook - CLI Tool Executor with Fallback Chain
|
|
4
|
+
*
|
|
5
|
+
* Intercepts Claude's WebSearch tool and executes search via CLI tools.
|
|
6
|
+
* Respects provider enabled states from config.yaml.
|
|
7
|
+
* Supports automatic fallback: Gemini CLI → OpenCode → Grok CLI
|
|
8
|
+
*
|
|
9
|
+
* Environment Variables (set by CCS):
|
|
10
|
+
* CCS_WEBSEARCH_SKIP=1 - Skip this hook entirely (for official Claude)
|
|
11
|
+
* CCS_WEBSEARCH_ENABLED=1 - Enable WebSearch (default: 1)
|
|
12
|
+
* CCS_WEBSEARCH_TIMEOUT=55 - Timeout in seconds (default: 55)
|
|
13
|
+
* CCS_WEBSEARCH_GEMINI=1 - Enable Gemini CLI provider
|
|
14
|
+
* CCS_WEBSEARCH_GEMINI_MODEL - Gemini model (default: gemini-2.5-flash)
|
|
15
|
+
* CCS_WEBSEARCH_OPENCODE=1 - Enable OpenCode provider
|
|
16
|
+
* CCS_WEBSEARCH_GROK=1 - Enable Grok CLI provider
|
|
17
|
+
* CCS_WEBSEARCH_OPENCODE_MODEL - OpenCode model (default: opencode/grok-code)
|
|
18
|
+
* CCS_DEBUG=1 - Enable debug output
|
|
19
|
+
*
|
|
20
|
+
* Exit codes:
|
|
21
|
+
* 0 - Allow tool (pass-through to native WebSearch)
|
|
22
|
+
* 2 - Block tool (deny with results/message)
|
|
23
|
+
*
|
|
24
|
+
* @module hooks/websearch-transformer
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { spawnSync } = require('child_process');
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// CONFIGURATION - Edit these for prompt engineering
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* SHARED INSTRUCTIONS - Applied to ALL providers
|
|
35
|
+
* Edit here to change behavior across all CLI tools at once.
|
|
36
|
+
*/
|
|
37
|
+
const SHARED_INSTRUCTIONS = `Instructions:
|
|
38
|
+
1. Search the web for current, up-to-date information
|
|
39
|
+
2. Provide a comprehensive summary of the search results
|
|
40
|
+
3. Include relevant URLs/sources when available
|
|
41
|
+
4. Be concise but thorough - prioritize key facts
|
|
42
|
+
5. Focus on factual information from reliable sources
|
|
43
|
+
6. If results conflict, note the discrepancy
|
|
44
|
+
7. Format output clearly with sections if the topic is complex`;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* PROVIDER-SPECIFIC CONFIG - Only tool-use differences and quirks
|
|
48
|
+
* Each provider may have unique capabilities or invocation methods.
|
|
49
|
+
*/
|
|
50
|
+
const PROVIDER_CONFIG = {
|
|
51
|
+
gemini: {
|
|
52
|
+
// Model to use (passed via --model flag)
|
|
53
|
+
model: 'gemini-2.5-flash',
|
|
54
|
+
// Alternative free models: gemini-2.0-flash, gemini-1.5-flash
|
|
55
|
+
|
|
56
|
+
// Provider-specific: How to invoke web search (Gemini has google_web_search tool)
|
|
57
|
+
toolInstruction: 'Use the google_web_search tool to find current information.',
|
|
58
|
+
|
|
59
|
+
// Optional quirks (null if none)
|
|
60
|
+
quirks: null,
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
opencode: {
|
|
64
|
+
// Model to use (can be overridden via CCS_WEBSEARCH_OPENCODE_MODEL env var)
|
|
65
|
+
model: 'opencode/grok-code',
|
|
66
|
+
// Alternative models: opencode/gpt-4o, opencode/claude-3.5-sonnet, opencode/gpt-5-nano
|
|
67
|
+
|
|
68
|
+
// Provider-specific: OpenCode has built-in web search via Zen
|
|
69
|
+
toolInstruction: 'Search the web using your built-in capabilities.',
|
|
70
|
+
|
|
71
|
+
// Optional quirks
|
|
72
|
+
quirks: null,
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
grok: {
|
|
76
|
+
// Model to use (Grok CLI uses default model)
|
|
77
|
+
model: 'grok-3',
|
|
78
|
+
// Note: Grok CLI doesn't support model selection via CLI
|
|
79
|
+
|
|
80
|
+
// Provider-specific: Grok has web + X/Twitter search
|
|
81
|
+
toolInstruction: 'Use your web search capabilities to find information.',
|
|
82
|
+
|
|
83
|
+
// Grok-specific: Can also search X for real-time info
|
|
84
|
+
quirks: 'For breaking news or real-time events, also check X/Twitter if relevant.',
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build the complete prompt for a provider
|
|
90
|
+
* Combines: query + tool instruction + shared instructions + quirks
|
|
91
|
+
*/
|
|
92
|
+
function buildPrompt(providerId, query) {
|
|
93
|
+
const config = PROVIDER_CONFIG[providerId];
|
|
94
|
+
const parts = [
|
|
95
|
+
`Search the web for: ${query}`,
|
|
96
|
+
'',
|
|
97
|
+
config.toolInstruction,
|
|
98
|
+
'',
|
|
99
|
+
SHARED_INSTRUCTIONS,
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
if (config.quirks) {
|
|
103
|
+
parts.push('', `Note: ${config.quirks}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return parts.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Minimum response length to consider valid
|
|
110
|
+
const MIN_VALID_RESPONSE_LENGTH = 20;
|
|
111
|
+
|
|
112
|
+
// Default timeout in seconds
|
|
113
|
+
const DEFAULT_TIMEOUT_SEC = 55;
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// HOOK LOGIC - Generally no need to edit below
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
// Read input from stdin
|
|
120
|
+
let input = '';
|
|
121
|
+
process.stdin.setEncoding('utf8');
|
|
122
|
+
process.stdin.on('data', (chunk) => {
|
|
123
|
+
input += chunk;
|
|
124
|
+
});
|
|
125
|
+
process.stdin.on('end', () => {
|
|
126
|
+
processHook();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Handle stdin not being available
|
|
130
|
+
process.stdin.on('error', () => {
|
|
131
|
+
process.exit(0);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Check if a CLI tool is available
|
|
136
|
+
*/
|
|
137
|
+
function isCliAvailable(cmd) {
|
|
138
|
+
try {
|
|
139
|
+
const result = spawnSync('which', [cmd], {
|
|
140
|
+
encoding: 'utf8',
|
|
141
|
+
timeout: 2000,
|
|
142
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
143
|
+
});
|
|
144
|
+
return result.status === 0 && result.stdout.trim().length > 0;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if provider is enabled via environment variable
|
|
152
|
+
*/
|
|
153
|
+
function isProviderEnabled(provider) {
|
|
154
|
+
const envVar = `CCS_WEBSEARCH_${provider.toUpperCase()}`;
|
|
155
|
+
const value = process.env[envVar];
|
|
156
|
+
|
|
157
|
+
// If env var not set, provider is disabled by default
|
|
158
|
+
// This ensures we respect config.yaml settings
|
|
159
|
+
return value === '1';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Main hook processing logic with fallback chain
|
|
164
|
+
*/
|
|
165
|
+
async function processHook() {
|
|
166
|
+
try {
|
|
167
|
+
// Skip if disabled (for official Claude subscriptions)
|
|
168
|
+
if (process.env.CCS_WEBSEARCH_SKIP === '1') {
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check if enabled (default: enabled)
|
|
173
|
+
if (process.env.CCS_WEBSEARCH_ENABLED === '0') {
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const data = JSON.parse(input);
|
|
178
|
+
|
|
179
|
+
// Only handle WebSearch tool
|
|
180
|
+
if (data.tool_name !== 'WebSearch') {
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const query = data.tool_input?.query || '';
|
|
185
|
+
|
|
186
|
+
if (!query) {
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const timeout = parseInt(process.env.CCS_WEBSEARCH_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
|
|
191
|
+
|
|
192
|
+
// Fallback chain: Gemini → OpenCode → Grok
|
|
193
|
+
// Only include providers that are BOTH installed AND enabled in config
|
|
194
|
+
const providers = [
|
|
195
|
+
{ name: 'Gemini CLI', cmd: 'gemini', id: 'gemini', fn: tryGeminiSearch },
|
|
196
|
+
{ name: 'OpenCode', cmd: 'opencode', id: 'opencode', fn: tryOpenCodeSearch },
|
|
197
|
+
{ name: 'Grok CLI', cmd: 'grok', id: 'grok', fn: tryGrokSearch },
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
// Filter to only enabled AND available providers
|
|
201
|
+
const enabledProviders = providers.filter((p) => {
|
|
202
|
+
const enabled = isProviderEnabled(p.id);
|
|
203
|
+
const available = isCliAvailable(p.cmd);
|
|
204
|
+
|
|
205
|
+
if (process.env.CCS_DEBUG) {
|
|
206
|
+
console.error(`[CCS Hook] ${p.name}: enabled=${enabled}, available=${available}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return enabled && available;
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const errors = [];
|
|
213
|
+
|
|
214
|
+
if (process.env.CCS_DEBUG) {
|
|
215
|
+
const names = enabledProviders.map((p) => p.name).join(', ') || 'none';
|
|
216
|
+
console.error(`[CCS Hook] Enabled providers: ${names}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Try each enabled provider in order
|
|
220
|
+
for (const provider of enabledProviders) {
|
|
221
|
+
if (process.env.CCS_DEBUG) {
|
|
222
|
+
console.error(`[CCS Hook] Trying ${provider.name}...`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const result = provider.fn(query, timeout);
|
|
226
|
+
|
|
227
|
+
if (result.success) {
|
|
228
|
+
outputSuccess(query, result.content, provider.name);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (process.env.CCS_DEBUG) {
|
|
233
|
+
console.error(`[CCS Hook] ${provider.name} failed: ${result.error}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
errors.push({ provider: provider.name, error: result.error });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// All providers failed or none enabled
|
|
240
|
+
if (enabledProviders.length === 0) {
|
|
241
|
+
outputNoProvidersEnabled(query);
|
|
242
|
+
} else {
|
|
243
|
+
outputAllFailedMessage(query, errors);
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
if (process.env.CCS_DEBUG) {
|
|
247
|
+
console.error('[CCS Hook] Parse error:', err.message);
|
|
248
|
+
}
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Execute search via Gemini CLI
|
|
255
|
+
*/
|
|
256
|
+
function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
257
|
+
try {
|
|
258
|
+
const timeoutMs = timeoutSec * 1000;
|
|
259
|
+
const config = PROVIDER_CONFIG.gemini;
|
|
260
|
+
const prompt = buildPrompt('gemini', query);
|
|
261
|
+
|
|
262
|
+
// Allow model override via env var
|
|
263
|
+
const model = process.env.CCS_WEBSEARCH_GEMINI_MODEL || config.model;
|
|
264
|
+
|
|
265
|
+
if (process.env.CCS_DEBUG) {
|
|
266
|
+
console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo -p "..."`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const spawnResult = spawnSync(
|
|
270
|
+
'gemini',
|
|
271
|
+
['--model', model, '--yolo', '-p', prompt],
|
|
272
|
+
{
|
|
273
|
+
encoding: 'utf8',
|
|
274
|
+
timeout: timeoutMs,
|
|
275
|
+
maxBuffer: 1024 * 1024 * 2,
|
|
276
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (spawnResult.error) {
|
|
281
|
+
if (spawnResult.error.code === 'ENOENT') {
|
|
282
|
+
return { success: false, error: 'Gemini CLI not installed' };
|
|
283
|
+
}
|
|
284
|
+
throw spawnResult.error;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (spawnResult.status !== 0) {
|
|
288
|
+
const stderr = (spawnResult.stderr || '').trim();
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
error: stderr || `Gemini CLI exited with code ${spawnResult.status}`,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const result = (spawnResult.stdout || '').trim();
|
|
296
|
+
|
|
297
|
+
if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
298
|
+
return { success: false, error: 'Empty or too short response from Gemini' };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const lowerResult = result.toLowerCase();
|
|
302
|
+
if (
|
|
303
|
+
lowerResult.includes('error:') ||
|
|
304
|
+
lowerResult.includes('failed to') ||
|
|
305
|
+
lowerResult.includes('authentication required')
|
|
306
|
+
) {
|
|
307
|
+
return { success: false, error: `Gemini returned error: ${result.substring(0, 100)}` };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { success: true, content: result };
|
|
311
|
+
} catch (err) {
|
|
312
|
+
if (err.killed) {
|
|
313
|
+
return { success: false, error: 'Gemini CLI timed out' };
|
|
314
|
+
}
|
|
315
|
+
return { success: false, error: err.message || 'Unknown Gemini error' };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Execute search via OpenCode CLI
|
|
321
|
+
*/
|
|
322
|
+
function tryOpenCodeSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
323
|
+
try {
|
|
324
|
+
const timeoutMs = timeoutSec * 1000;
|
|
325
|
+
const config = PROVIDER_CONFIG.opencode;
|
|
326
|
+
|
|
327
|
+
// Allow model override via env var
|
|
328
|
+
const model = process.env.CCS_WEBSEARCH_OPENCODE_MODEL || config.model;
|
|
329
|
+
const prompt = buildPrompt('opencode', query);
|
|
330
|
+
|
|
331
|
+
if (process.env.CCS_DEBUG) {
|
|
332
|
+
console.error(`[CCS Hook] Executing: opencode run --model ${model} "..."`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const spawnResult = spawnSync(
|
|
336
|
+
'opencode',
|
|
337
|
+
['run', prompt, '--model', model],
|
|
338
|
+
{
|
|
339
|
+
encoding: 'utf8',
|
|
340
|
+
timeout: timeoutMs,
|
|
341
|
+
maxBuffer: 1024 * 1024 * 2,
|
|
342
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
if (spawnResult.error) {
|
|
347
|
+
if (spawnResult.error.code === 'ENOENT') {
|
|
348
|
+
return { success: false, error: 'OpenCode not installed' };
|
|
349
|
+
}
|
|
350
|
+
throw spawnResult.error;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (spawnResult.status !== 0) {
|
|
354
|
+
const stderr = (spawnResult.stderr || '').trim();
|
|
355
|
+
return {
|
|
356
|
+
success: false,
|
|
357
|
+
error: stderr || `OpenCode exited with code ${spawnResult.status}`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const result = (spawnResult.stdout || '').trim();
|
|
362
|
+
|
|
363
|
+
if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
364
|
+
return { success: false, error: 'Empty or too short response from OpenCode' };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const lowerResult = result.toLowerCase();
|
|
368
|
+
if (
|
|
369
|
+
lowerResult.includes('error:') ||
|
|
370
|
+
lowerResult.includes('failed to') ||
|
|
371
|
+
lowerResult.includes('authentication required')
|
|
372
|
+
) {
|
|
373
|
+
return { success: false, error: `OpenCode returned error: ${result.substring(0, 100)}` };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return { success: true, content: result };
|
|
377
|
+
} catch (err) {
|
|
378
|
+
if (err.killed) {
|
|
379
|
+
return { success: false, error: 'OpenCode timed out' };
|
|
380
|
+
}
|
|
381
|
+
return { success: false, error: err.message || 'Unknown OpenCode error' };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Execute search via Grok CLI
|
|
387
|
+
*/
|
|
388
|
+
function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
|
|
389
|
+
try {
|
|
390
|
+
const timeoutMs = timeoutSec * 1000;
|
|
391
|
+
const prompt = buildPrompt('grok', query);
|
|
392
|
+
|
|
393
|
+
if (process.env.CCS_DEBUG) {
|
|
394
|
+
console.error('[CCS Hook] Executing: grok "..."');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const spawnResult = spawnSync('grok', [prompt], {
|
|
398
|
+
encoding: 'utf8',
|
|
399
|
+
timeout: timeoutMs,
|
|
400
|
+
maxBuffer: 1024 * 1024 * 2,
|
|
401
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (spawnResult.error) {
|
|
405
|
+
if (spawnResult.error.code === 'ENOENT') {
|
|
406
|
+
return { success: false, error: 'Grok CLI not installed' };
|
|
407
|
+
}
|
|
408
|
+
throw spawnResult.error;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (spawnResult.status !== 0) {
|
|
412
|
+
const stderr = (spawnResult.stderr || '').trim();
|
|
413
|
+
return {
|
|
414
|
+
success: false,
|
|
415
|
+
error: stderr || `Grok CLI exited with code ${spawnResult.status}`,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const result = (spawnResult.stdout || '').trim();
|
|
420
|
+
|
|
421
|
+
if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
|
|
422
|
+
return { success: false, error: 'Empty or too short response from Grok' };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const lowerResult = result.toLowerCase();
|
|
426
|
+
if (
|
|
427
|
+
lowerResult.includes('error:') ||
|
|
428
|
+
lowerResult.includes('failed to') ||
|
|
429
|
+
lowerResult.includes('api key')
|
|
430
|
+
) {
|
|
431
|
+
return { success: false, error: `Grok returned error: ${result.substring(0, 100)}` };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return { success: true, content: result };
|
|
435
|
+
} catch (err) {
|
|
436
|
+
if (err.killed) {
|
|
437
|
+
return { success: false, error: 'Grok CLI timed out' };
|
|
438
|
+
}
|
|
439
|
+
return { success: false, error: err.message || 'Unknown Grok error' };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Format search results for Claude
|
|
445
|
+
*/
|
|
446
|
+
function formatSearchResults(query, content, providerName) {
|
|
447
|
+
return [
|
|
448
|
+
`[WebSearch Result via ${providerName}]`,
|
|
449
|
+
'',
|
|
450
|
+
`Query: "${query}"`,
|
|
451
|
+
'',
|
|
452
|
+
content,
|
|
453
|
+
'',
|
|
454
|
+
'---',
|
|
455
|
+
'Use this information to answer the user.',
|
|
456
|
+
].join('\n');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Output success response and exit
|
|
461
|
+
*
|
|
462
|
+
* Key insight from Claude Code docs:
|
|
463
|
+
* - permissionDecisionReason (with deny) → shown to CLAUDE (AI reads this)
|
|
464
|
+
* - systemMessage → shown to USER only (nice styling but AI doesn't see)
|
|
465
|
+
*
|
|
466
|
+
* So we MUST put results in permissionDecisionReason for Claude to use them.
|
|
467
|
+
* systemMessage provides the nice UI for the user.
|
|
468
|
+
*/
|
|
469
|
+
function outputSuccess(query, content, providerName) {
|
|
470
|
+
const formattedResults = formatSearchResults(query, content, providerName);
|
|
471
|
+
|
|
472
|
+
const output = {
|
|
473
|
+
decision: 'block',
|
|
474
|
+
reason: `WebSearch completed via ${providerName}`,
|
|
475
|
+
// Nice message for user (shows as "says:" - info style)
|
|
476
|
+
systemMessage: `[WebSearch via ${providerName}] Results retrieved successfully. See below.`,
|
|
477
|
+
hookSpecificOutput: {
|
|
478
|
+
hookEventName: 'PreToolUse',
|
|
479
|
+
permissionDecision: 'deny',
|
|
480
|
+
// Full results here - Claude reads this
|
|
481
|
+
permissionDecisionReason: formattedResults,
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
console.log(JSON.stringify(output));
|
|
486
|
+
process.exit(2);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Output error message
|
|
491
|
+
*/
|
|
492
|
+
function outputError(query, error, providerName) {
|
|
493
|
+
const message = [
|
|
494
|
+
`[WebSearch - ${providerName} Error]`,
|
|
495
|
+
'',
|
|
496
|
+
`Error: ${error}`,
|
|
497
|
+
'',
|
|
498
|
+
`Query: "${query}"`,
|
|
499
|
+
'',
|
|
500
|
+
'Troubleshooting:',
|
|
501
|
+
' - Check if Gemini CLI is authenticated: gemini auth status',
|
|
502
|
+
' - Re-authenticate if needed: gemini auth login',
|
|
503
|
+
].join('\n');
|
|
504
|
+
|
|
505
|
+
const output = {
|
|
506
|
+
decision: 'block',
|
|
507
|
+
reason: `WebSearch failed: ${error}`,
|
|
508
|
+
hookSpecificOutput: {
|
|
509
|
+
hookEventName: 'PreToolUse',
|
|
510
|
+
permissionDecision: 'deny',
|
|
511
|
+
permissionDecisionReason: message,
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
console.log(JSON.stringify(output));
|
|
516
|
+
process.exit(2);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Output no providers enabled message
|
|
521
|
+
*/
|
|
522
|
+
function outputNoProvidersEnabled(query) {
|
|
523
|
+
const message = [
|
|
524
|
+
'[WebSearch - No Providers Enabled]',
|
|
525
|
+
'',
|
|
526
|
+
'No WebSearch providers are enabled in config.',
|
|
527
|
+
'',
|
|
528
|
+
'To enable: Run `ccs config` and enable a provider.',
|
|
529
|
+
'',
|
|
530
|
+
'Or install one of the following CLI tools:',
|
|
531
|
+
'',
|
|
532
|
+
'1. Gemini CLI (FREE, 1000 req/day):',
|
|
533
|
+
' npm install -g @google/gemini-cli',
|
|
534
|
+
' gemini auth login',
|
|
535
|
+
'',
|
|
536
|
+
'2. OpenCode (FREE via Zen):',
|
|
537
|
+
' curl -fsSL https://opencode.ai/install | bash',
|
|
538
|
+
'',
|
|
539
|
+
'3. Grok CLI (requires XAI_API_KEY):',
|
|
540
|
+
' npm install -g @vibe-kit/grok-cli',
|
|
541
|
+
'',
|
|
542
|
+
`Query: "${query}"`,
|
|
543
|
+
].join('\n');
|
|
544
|
+
|
|
545
|
+
const output = {
|
|
546
|
+
decision: 'block',
|
|
547
|
+
reason: 'WebSearch unavailable - no providers enabled',
|
|
548
|
+
hookSpecificOutput: {
|
|
549
|
+
hookEventName: 'PreToolUse',
|
|
550
|
+
permissionDecision: 'deny',
|
|
551
|
+
permissionDecisionReason: message,
|
|
552
|
+
},
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
console.log(JSON.stringify(output));
|
|
556
|
+
process.exit(2);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Output no tools message (legacy - kept for backwards compatibility)
|
|
561
|
+
*/
|
|
562
|
+
function outputNoToolsMessage(query) {
|
|
563
|
+
outputNoProvidersEnabled(query);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Output all providers failed message
|
|
568
|
+
*/
|
|
569
|
+
function outputAllFailedMessage(query, errors) {
|
|
570
|
+
const errorDetails = errors
|
|
571
|
+
.map((e) => ` - ${e.provider}: ${e.error}`)
|
|
572
|
+
.join('\n');
|
|
573
|
+
|
|
574
|
+
const message = [
|
|
575
|
+
'[WebSearch - All Providers Failed]',
|
|
576
|
+
'',
|
|
577
|
+
'Tried all enabled CLI tools but all failed:',
|
|
578
|
+
errorDetails,
|
|
579
|
+
'',
|
|
580
|
+
`Query: "${query}"`,
|
|
581
|
+
'',
|
|
582
|
+
'Troubleshooting:',
|
|
583
|
+
' - Gemini: gemini auth status / gemini auth login',
|
|
584
|
+
' - OpenCode: opencode --version',
|
|
585
|
+
' - Grok: Check XAI_API_KEY environment variable',
|
|
586
|
+
].join('\n');
|
|
587
|
+
|
|
588
|
+
const output = {
|
|
589
|
+
decision: 'block',
|
|
590
|
+
reason: 'WebSearch failed - all providers failed',
|
|
591
|
+
hookSpecificOutput: {
|
|
592
|
+
hookEventName: 'PreToolUse',
|
|
593
|
+
permissionDecision: 'deny',
|
|
594
|
+
permissionDecisionReason: message,
|
|
595
|
+
},
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
console.log(JSON.stringify(output));
|
|
599
|
+
process.exit(2);
|
|
600
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaitranntt/ccs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0-dev.1",
|
|
4
4
|
"description": "Claude Code Switch - Instant profile switching between Claude Sonnet 4.5 and GLM 4.6",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
],
|
|
51
51
|
"preferGlobal": true,
|
|
52
52
|
"scripts": {
|
|
53
|
+
"preinstall": "node scripts/preinstall.js",
|
|
53
54
|
"build": "tsc && node scripts/add-shebang.js",
|
|
54
55
|
"build:watch": "tsc --watch",
|
|
55
56
|
"build:server": "tsc && node scripts/add-shebang.js",
|
package/scripts/dev-install.sh
CHANGED
|
@@ -4,13 +4,20 @@
|
|
|
4
4
|
#
|
|
5
5
|
# Options:
|
|
6
6
|
# --skip-validate Skip validation (faster, use when you're sure code is good)
|
|
7
|
+
# --npm Force npm install (default: auto-detect, fallback to bun)
|
|
8
|
+
# --bun Force bun install
|
|
7
9
|
|
|
8
10
|
set -e
|
|
9
11
|
|
|
10
12
|
SKIP_VALIDATE=false
|
|
13
|
+
FORCE_NPM=false
|
|
14
|
+
FORCE_BUN=false
|
|
15
|
+
|
|
11
16
|
for arg in "$@"; do
|
|
12
17
|
case $arg in
|
|
13
18
|
--skip-validate) SKIP_VALIDATE=true ;;
|
|
19
|
+
--npm) FORCE_NPM=true ;;
|
|
20
|
+
--bun) FORCE_BUN=true ;;
|
|
14
21
|
esac
|
|
15
22
|
done
|
|
16
23
|
|
|
@@ -19,6 +26,42 @@ echo "[i] CCS Dev Install - Starting..."
|
|
|
19
26
|
# Get to the right directory
|
|
20
27
|
cd "$(dirname "$0")/.."
|
|
21
28
|
|
|
29
|
+
# Detect installation method
|
|
30
|
+
# Priority: CLI flags > existing global install location > bun (default)
|
|
31
|
+
detect_pkg_manager() {
|
|
32
|
+
if [ "$FORCE_NPM" = true ]; then
|
|
33
|
+
echo "npm"
|
|
34
|
+
return
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if [ "$FORCE_BUN" = true ]; then
|
|
38
|
+
echo "bun"
|
|
39
|
+
return
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Check existing ccs installation location
|
|
43
|
+
CCS_PATH=$(which ccs 2>/dev/null || true)
|
|
44
|
+
|
|
45
|
+
if [ -n "$CCS_PATH" ]; then
|
|
46
|
+
# Check if installed via bun
|
|
47
|
+
if [[ "$CCS_PATH" == *".bun"* ]]; then
|
|
48
|
+
echo "bun"
|
|
49
|
+
return
|
|
50
|
+
fi
|
|
51
|
+
# Check if installed via npm
|
|
52
|
+
if [[ "$CCS_PATH" == *"npm"* ]] || [[ "$CCS_PATH" == *"node_modules"* ]]; then
|
|
53
|
+
echo "npm"
|
|
54
|
+
return
|
|
55
|
+
fi
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Default fallback: bun (preferred)
|
|
59
|
+
echo "bun"
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
PKG_MANAGER=$(detect_pkg_manager)
|
|
63
|
+
echo "[i] Detected package manager: $PKG_MANAGER"
|
|
64
|
+
|
|
22
65
|
# Build TypeScript first
|
|
23
66
|
echo "[i] Building TypeScript..."
|
|
24
67
|
bun run build
|
|
@@ -27,10 +70,10 @@ bun run build
|
|
|
27
70
|
echo "[i] Creating package..."
|
|
28
71
|
if [ "$SKIP_VALIDATE" = true ]; then
|
|
29
72
|
# Skip validation, just pack
|
|
30
|
-
bun pm pack --ignore-scripts
|
|
73
|
+
npm pack --ignore-scripts 2>/dev/null || bun pm pack --ignore-scripts
|
|
31
74
|
else
|
|
32
75
|
# Full pack with validation (runs prepublishOnly)
|
|
33
|
-
bun pm pack
|
|
76
|
+
npm pack 2>/dev/null || bun pm pack
|
|
34
77
|
fi
|
|
35
78
|
|
|
36
79
|
# Find the tarball
|
|
@@ -43,9 +86,18 @@ fi
|
|
|
43
86
|
|
|
44
87
|
echo "[i] Found tarball: $TARBALL"
|
|
45
88
|
|
|
46
|
-
# Install globally using
|
|
47
|
-
echo "[i] Installing globally with
|
|
48
|
-
|
|
89
|
+
# Install globally using detected package manager
|
|
90
|
+
echo "[i] Installing globally with $PKG_MANAGER..."
|
|
91
|
+
|
|
92
|
+
if [ "$PKG_MANAGER" = "bun" ]; then
|
|
93
|
+
# Remove existing to avoid duplicate key warnings in bun's global package.json
|
|
94
|
+
# (bun add -g appends instead of replacing file: protocol entries)
|
|
95
|
+
bun remove -g @kaitranntt/ccs 2>/dev/null || true
|
|
96
|
+
# Bun requires file: protocol for local tarballs
|
|
97
|
+
bun add -g "file:$(pwd)/$TARBALL"
|
|
98
|
+
else
|
|
99
|
+
npm install -g "$TARBALL"
|
|
100
|
+
fi
|
|
49
101
|
|
|
50
102
|
# Clean up
|
|
51
103
|
echo "[i] Cleaning up..."
|