@kaitranntt/ccs 5.20.0 → 6.0.0-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.
Files changed (76) hide show
  1. package/README.md +39 -0
  2. package/VERSION +1 -1
  3. package/dist/ccs.js +9 -1
  4. package/dist/ccs.js.map +1 -1
  5. package/dist/cliproxy/account-manager.d.ts.map +1 -1
  6. package/dist/cliproxy/account-manager.js +3 -1
  7. package/dist/cliproxy/account-manager.js.map +1 -1
  8. package/dist/cliproxy/cliproxy-executor.d.ts.map +1 -1
  9. package/dist/cliproxy/cliproxy-executor.js +145 -76
  10. package/dist/cliproxy/cliproxy-executor.js.map +1 -1
  11. package/dist/cliproxy/session-tracker.d.ts +54 -0
  12. package/dist/cliproxy/session-tracker.d.ts.map +1 -0
  13. package/dist/cliproxy/session-tracker.js +228 -0
  14. package/dist/cliproxy/session-tracker.js.map +1 -0
  15. package/dist/cliproxy/stats-fetcher.d.ts +19 -0
  16. package/dist/cliproxy/stats-fetcher.d.ts.map +1 -1
  17. package/dist/cliproxy/stats-fetcher.js +41 -3
  18. package/dist/cliproxy/stats-fetcher.js.map +1 -1
  19. package/dist/config/unified-config-loader.d.ts +33 -0
  20. package/dist/config/unified-config-loader.d.ts.map +1 -1
  21. package/dist/config/unified-config-loader.js +105 -3
  22. package/dist/config/unified-config-loader.js.map +1 -1
  23. package/dist/config/unified-config-types.d.ts +75 -1
  24. package/dist/config/unified-config-types.d.ts.map +1 -1
  25. package/dist/config/unified-config-types.js +21 -1
  26. package/dist/config/unified-config-types.js.map +1 -1
  27. package/dist/ui/assets/{accounts-achdtDUJ.js → accounts-BfRSUzvC.js} +1 -1
  28. package/dist/ui/assets/{analytics-BVlC8Y7-.js → analytics-ClDxRP9x.js} +34 -34
  29. package/dist/ui/assets/api-DyJHhCv0.js +1 -0
  30. package/dist/ui/assets/card-CrGAcUkx.js +1 -0
  31. package/dist/ui/assets/cliproxy-Zr4pFy6O.js +1 -0
  32. package/dist/ui/assets/cliproxy-control-panel-Cu7DgEPx.js +1 -0
  33. package/dist/ui/assets/code-editor-D0YjMRQO.js +2 -0
  34. package/dist/ui/assets/{form-utils-DKkU3nz7.js → form-utils-CKETQmt9.js} +1 -1
  35. package/dist/ui/assets/{health-Xbq8eUat.js → health-Rretog4H.js} +1 -1
  36. package/dist/ui/assets/icons-D6DaLbNh.js +1 -0
  37. package/dist/ui/assets/index-B2PFll-u.js +46 -0
  38. package/dist/ui/assets/index-DTVwRqgB.css +1 -0
  39. package/dist/ui/assets/providers/iflow.png +0 -0
  40. package/dist/ui/assets/{radix-ui-OFtPgiRV.js → radix-ui-Bhyw9pC9.js} +2 -2
  41. package/dist/ui/assets/{react-vendor-CjrBBxxX.js → react-vendor-DadlvKzT.js} +1 -1
  42. package/dist/ui/assets/settings-B7OFU9lZ.js +1 -0
  43. package/dist/ui/assets/shared-qGCa7mu2.js +1 -0
  44. package/dist/ui/assets/{tanstack-DMWkeNzM.js → tanstack-DZ3a6J0N.js} +1 -1
  45. package/dist/ui/index.html +7 -7
  46. package/dist/utils/shell-executor.d.ts.map +1 -1
  47. package/dist/utils/shell-executor.js +6 -1
  48. package/dist/utils/shell-executor.js.map +1 -1
  49. package/dist/utils/websearch-manager.d.ts +197 -0
  50. package/dist/utils/websearch-manager.d.ts.map +1 -0
  51. package/dist/utils/websearch-manager.js +685 -0
  52. package/dist/utils/websearch-manager.js.map +1 -0
  53. package/dist/web-server/health-service.d.ts.map +1 -1
  54. package/dist/web-server/health-service.js +50 -0
  55. package/dist/web-server/health-service.js.map +1 -1
  56. package/dist/web-server/routes.d.ts.map +1 -1
  57. package/dist/web-server/routes.js +169 -1
  58. package/dist/web-server/routes.js.map +1 -1
  59. package/dist/web-server/shutdown.d.ts +4 -4
  60. package/dist/web-server/shutdown.d.ts.map +1 -1
  61. package/dist/web-server/shutdown.js +9 -17
  62. package/dist/web-server/shutdown.js.map +1 -1
  63. package/lib/hooks/block-websearch.cjs +75 -0
  64. package/lib/hooks/websearch-transformer.cjs +600 -0
  65. package/package.json +2 -1
  66. package/scripts/dev-install.sh +57 -5
  67. package/scripts/preinstall.js +59 -0
  68. package/dist/ui/assets/api-BwWsFLoC.js +0 -1
  69. package/dist/ui/assets/cliproxy-DuaxYcVk.js +0 -1
  70. package/dist/ui/assets/cliproxy-control-panel-DTiBzA5u.js +0 -1
  71. package/dist/ui/assets/code-editor-D7CYoILm.js +0 -36
  72. package/dist/ui/assets/icons-Alnq4BWm.js +0 -1
  73. package/dist/ui/assets/index-B1gvMo-b.css +0 -1
  74. package/dist/ui/assets/index-C2RMogI8.js +0 -12
  75. package/dist/ui/assets/settings-95g3F5Gk.js +0 -1
  76. 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": "5.20.0",
3
+ "version": "6.0.0-dev.2",
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",
@@ -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 npm (handles bin linking correctly)
47
- echo "[i] Installing globally with npm..."
48
- npm install -g "$TARBALL"
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..."