@jonnyhoo/ccs 1.1.1 → 1.1.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 (179) hide show
  1. package/.claude/skills/ccs-delegation/SKILL.md +7 -7
  2. package/.claude/skills/ccs-delegation/references/troubleshooting.md +20 -20
  3. package/config/base-codex.settings.json +3 -3
  4. package/dist/api/services/profile-writer.d.ts +1 -1
  5. package/dist/api/services/profile-writer.d.ts.map +1 -1
  6. package/dist/api/services/profile-writer.js +8 -9
  7. package/dist/api/services/profile-writer.js.map +1 -1
  8. package/dist/auth/profile-detector.d.ts +3 -1
  9. package/dist/auth/profile-detector.d.ts.map +1 -1
  10. package/dist/auth/profile-detector.js +16 -17
  11. package/dist/auth/profile-detector.js.map +1 -1
  12. package/dist/ccs.js +303 -44
  13. package/dist/ccs.js.map +1 -1
  14. package/dist/cliproxy/anthropic-to-openai-proxy.d.ts +15 -1
  15. package/dist/cliproxy/anthropic-to-openai-proxy.d.ts.map +1 -1
  16. package/dist/cliproxy/anthropic-to-openai-proxy.js +1100 -296
  17. package/dist/cliproxy/anthropic-to-openai-proxy.js.map +1 -1
  18. package/dist/cliproxy/cliproxy-executor.d.ts.map +1 -1
  19. package/dist/cliproxy/cliproxy-executor.js +58 -44
  20. package/dist/cliproxy/cliproxy-executor.js.map +1 -1
  21. package/dist/cliproxy/config-generator.d.ts.map +1 -1
  22. package/dist/cliproxy/config-generator.js +16 -2
  23. package/dist/cliproxy/config-generator.js.map +1 -1
  24. package/dist/cliproxy/model-catalog.d.ts.map +1 -1
  25. package/dist/cliproxy/model-catalog.js +12 -1
  26. package/dist/cliproxy/model-catalog.js.map +1 -1
  27. package/dist/cliproxy/services/variant-settings.d.ts.map +1 -1
  28. package/dist/cliproxy/services/variant-settings.js +0 -5
  29. package/dist/cliproxy/services/variant-settings.js.map +1 -1
  30. package/dist/cliproxy/tool-name-sanitizer.d.ts +17 -21
  31. package/dist/cliproxy/tool-name-sanitizer.d.ts.map +1 -1
  32. package/dist/cliproxy/tool-name-sanitizer.js +46 -33
  33. package/dist/cliproxy/tool-name-sanitizer.js.map +1 -1
  34. package/dist/cliproxy/tool-sanitization-proxy.d.ts.map +1 -1
  35. package/dist/cliproxy/tool-sanitization-proxy.js +18 -1
  36. package/dist/cliproxy/tool-sanitization-proxy.js.map +1 -1
  37. package/dist/commands/api-command.d.ts.map +1 -1
  38. package/dist/commands/api-command.js +21 -1
  39. package/dist/commands/api-command.js.map +1 -1
  40. package/dist/commands/help-command.d.ts.map +1 -1
  41. package/dist/commands/help-command.js +15 -107
  42. package/dist/commands/help-command.js.map +1 -1
  43. package/dist/commands/index.d.ts +0 -1
  44. package/dist/commands/index.d.ts.map +1 -1
  45. package/dist/commands/index.js +1 -3
  46. package/dist/commands/index.js.map +1 -1
  47. package/dist/commands/install-command.d.ts.map +1 -1
  48. package/dist/commands/install-command.js +2 -9
  49. package/dist/commands/install-command.js.map +1 -1
  50. package/dist/commands/router.js +1 -1
  51. package/dist/commands/router.js.map +1 -1
  52. package/dist/config/unified-config-loader.d.ts +0 -32
  53. package/dist/config/unified-config-loader.d.ts.map +1 -1
  54. package/dist/config/unified-config-loader.js +6 -95
  55. package/dist/config/unified-config-loader.js.map +1 -1
  56. package/dist/config/unified-config-types.d.ts +3 -75
  57. package/dist/config/unified-config-types.d.ts.map +1 -1
  58. package/dist/config/unified-config-types.js +0 -20
  59. package/dist/config/unified-config-types.js.map +1 -1
  60. package/dist/delegation/background-monitor.d.ts +80 -0
  61. package/dist/delegation/background-monitor.d.ts.map +1 -0
  62. package/dist/delegation/background-monitor.js +295 -0
  63. package/dist/delegation/background-monitor.js.map +1 -0
  64. package/dist/delegation/delegation-handler.d.ts +2 -1
  65. package/dist/delegation/delegation-handler.d.ts.map +1 -1
  66. package/dist/delegation/delegation-handler.js +48 -30
  67. package/dist/delegation/delegation-handler.js.map +1 -1
  68. package/dist/delegation/executor/types.d.ts +2 -0
  69. package/dist/delegation/executor/types.d.ts.map +1 -1
  70. package/dist/delegation/headless-executor.d.ts +3 -0
  71. package/dist/delegation/headless-executor.d.ts.map +1 -1
  72. package/dist/delegation/headless-executor.js +162 -14
  73. package/dist/delegation/headless-executor.js.map +1 -1
  74. package/dist/hello.d.ts +7 -0
  75. package/dist/hello.d.ts.map +1 -0
  76. package/dist/hello.js +13 -0
  77. package/dist/hello.js.map +1 -0
  78. package/dist/management/checks/image-analysis-check.d.ts.map +1 -1
  79. package/dist/management/checks/image-analysis-check.js.map +1 -1
  80. package/dist/router/index.d.ts +3 -3
  81. package/dist/router/index.d.ts.map +1 -1
  82. package/dist/router/index.js +1 -1
  83. package/dist/router/index.js.map +1 -1
  84. package/dist/router/scenario-router.d.ts +2 -8
  85. package/dist/router/scenario-router.d.ts.map +1 -1
  86. package/dist/router/scenario-router.js +3 -22
  87. package/dist/router/scenario-router.js.map +1 -1
  88. package/dist/router/scenario-routing-proxy.d.ts.map +1 -1
  89. package/dist/router/scenario-routing-proxy.js +19 -4
  90. package/dist/router/scenario-routing-proxy.js.map +1 -1
  91. package/dist/router/settings-routing-executor.d.ts.map +1 -1
  92. package/dist/router/settings-routing-executor.js.map +1 -1
  93. package/dist/router/types.d.ts +1 -1
  94. package/dist/router/types.d.ts.map +1 -1
  95. package/dist/utils/shell-executor.d.ts.map +1 -1
  96. package/dist/utils/shell-executor.js +1 -6
  97. package/dist/utils/shell-executor.js.map +1 -1
  98. package/dist/web-server/health/index.d.ts +0 -1
  99. package/dist/web-server/health/index.d.ts.map +1 -1
  100. package/dist/web-server/health/index.js +1 -4
  101. package/dist/web-server/health/index.js.map +1 -1
  102. package/dist/web-server/health-service.d.ts.map +1 -1
  103. package/dist/web-server/health-service.js +0 -4
  104. package/dist/web-server/health-service.js.map +1 -1
  105. package/dist/web-server/routes/index.d.ts.map +1 -1
  106. package/dist/web-server/routes/index.js +0 -3
  107. package/dist/web-server/routes/index.js.map +1 -1
  108. package/dist/web-server/routes/settings-routes.d.ts.map +1 -1
  109. package/dist/web-server/routes/settings-routes.js +0 -4
  110. package/dist/web-server/routes/settings-routes.js.map +1 -1
  111. package/package.json +1 -1
  112. package/scripts/background-redis-bridge.js +320 -0
  113. package/scripts/monitor-task.js +313 -0
  114. package/dist/commands/cliproxy-command.d.ts +0 -21
  115. package/dist/commands/cliproxy-command.d.ts.map +0 -1
  116. package/dist/commands/cliproxy-command.js +0 -1099
  117. package/dist/commands/cliproxy-command.js.map +0 -1
  118. package/dist/commands/cliproxy-sync-handler.d.ts +0 -19
  119. package/dist/commands/cliproxy-sync-handler.d.ts.map +0 -1
  120. package/dist/commands/cliproxy-sync-handler.js +0 -99
  121. package/dist/commands/cliproxy-sync-handler.js.map +0 -1
  122. package/dist/utils/websearch/gemini-cli.d.ts +0 -36
  123. package/dist/utils/websearch/gemini-cli.d.ts.map +0 -1
  124. package/dist/utils/websearch/gemini-cli.js +0 -132
  125. package/dist/utils/websearch/gemini-cli.js.map +0 -1
  126. package/dist/utils/websearch/grok-cli.d.ts +0 -26
  127. package/dist/utils/websearch/grok-cli.d.ts.map +0 -1
  128. package/dist/utils/websearch/grok-cli.js +0 -81
  129. package/dist/utils/websearch/grok-cli.js.map +0 -1
  130. package/dist/utils/websearch/hook-config.d.ts +0 -27
  131. package/dist/utils/websearch/hook-config.d.ts.map +0 -1
  132. package/dist/utils/websearch/hook-config.js +0 -280
  133. package/dist/utils/websearch/hook-config.js.map +0 -1
  134. package/dist/utils/websearch/hook-env.d.ts +0 -16
  135. package/dist/utils/websearch/hook-env.d.ts.map +0 -1
  136. package/dist/utils/websearch/hook-env.js +0 -62
  137. package/dist/utils/websearch/hook-env.js.map +0 -1
  138. package/dist/utils/websearch/hook-installer.d.ts +0 -30
  139. package/dist/utils/websearch/hook-installer.d.ts.map +0 -1
  140. package/dist/utils/websearch/hook-installer.js +0 -148
  141. package/dist/utils/websearch/hook-installer.js.map +0 -1
  142. package/dist/utils/websearch/hook-utils.d.ts +0 -18
  143. package/dist/utils/websearch/hook-utils.d.ts.map +0 -1
  144. package/dist/utils/websearch/hook-utils.js +0 -59
  145. package/dist/utils/websearch/hook-utils.js.map +0 -1
  146. package/dist/utils/websearch/index.d.ts +0 -17
  147. package/dist/utils/websearch/index.d.ts.map +0 -1
  148. package/dist/utils/websearch/index.js +0 -51
  149. package/dist/utils/websearch/index.js.map +0 -1
  150. package/dist/utils/websearch/opencode-cli.d.ts +0 -26
  151. package/dist/utils/websearch/opencode-cli.d.ts.map +0 -1
  152. package/dist/utils/websearch/opencode-cli.js +0 -81
  153. package/dist/utils/websearch/opencode-cli.js.map +0 -1
  154. package/dist/utils/websearch/profile-hook-injector.d.ts +0 -20
  155. package/dist/utils/websearch/profile-hook-injector.d.ts.map +0 -1
  156. package/dist/utils/websearch/profile-hook-injector.js +0 -250
  157. package/dist/utils/websearch/profile-hook-injector.js.map +0 -1
  158. package/dist/utils/websearch/status.d.ts +0 -36
  159. package/dist/utils/websearch/status.d.ts.map +0 -1
  160. package/dist/utils/websearch/status.js +0 -192
  161. package/dist/utils/websearch/status.js.map +0 -1
  162. package/dist/utils/websearch/types.d.ts +0 -85
  163. package/dist/utils/websearch/types.d.ts.map +0 -1
  164. package/dist/utils/websearch/types.js +0 -10
  165. package/dist/utils/websearch/types.js.map +0 -1
  166. package/dist/utils/websearch-manager.d.ts +0 -31
  167. package/dist/utils/websearch-manager.d.ts.map +0 -1
  168. package/dist/utils/websearch-manager.js +0 -72
  169. package/dist/utils/websearch-manager.js.map +0 -1
  170. package/dist/web-server/health/websearch-checks.d.ts +0 -11
  171. package/dist/web-server/health/websearch-checks.d.ts.map +0 -1
  172. package/dist/web-server/health/websearch-checks.js +0 -53
  173. package/dist/web-server/health/websearch-checks.js.map +0 -1
  174. package/dist/web-server/routes/websearch-routes.d.ts +0 -6
  175. package/dist/web-server/routes/websearch-routes.d.ts.map +0 -1
  176. package/dist/web-server/routes/websearch-routes.js +0 -130
  177. package/dist/web-server/routes/websearch-routes.js.map +0 -1
  178. package/lib/hooks/block-websearch.cjs +0 -75
  179. package/lib/hooks/websearch-transformer.cjs +0 -632
@@ -1,75 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * CCS WebSearch Blocking Hook
4
- *
5
- * Blocks Claude's native WebSearch tool and redirects to MCP alternative.
6
- * This is a PreToolUse hook that runs BEFORE the tool is executed.
7
- *
8
- * WebSearch is a server-side tool executed by Anthropic's API.
9
- * Third-party providers (gemini, agy, codex, qwen) don't have access.
10
- *
11
- * Usage:
12
- * Configured in ~/.claude/settings.json:
13
- * {
14
- * "hooks": {
15
- * "PreToolUse": [{
16
- * "matcher": "WebSearch",
17
- * "hooks": [{
18
- * "type": "command",
19
- * "command": "node ~/.ccs/hooks/block-websearch.cjs",
20
- * "timeout": 5
21
- * }]
22
- * }]
23
- * }
24
- * }
25
- *
26
- * Exit codes:
27
- * 0 - Allow tool (pass-through)
28
- * 2 - Block tool (deny with message)
29
- *
30
- * @module hooks/block-websearch
31
- */
32
-
33
- // Read input from stdin
34
- let input = '';
35
- process.stdin.setEncoding('utf8');
36
- process.stdin.on('data', (chunk) => {
37
- input += chunk;
38
- });
39
- process.stdin.on('end', () => {
40
- try {
41
- const data = JSON.parse(input);
42
-
43
- // Only block WebSearch tool
44
- if (data.tool_name === 'WebSearch') {
45
- const query = data.tool_input?.query || '';
46
-
47
- const output = {
48
- decision: 'block',
49
- reason: 'WebSearch unavailable with current provider',
50
- hookSpecificOutput: {
51
- hookEventName: 'PreToolUse',
52
- permissionDecision: 'deny',
53
- permissionDecisionReason: `WebSearch is not available with third-party providers. Use mcp__web-search-prime__webSearchPrime tool instead with the same query: "${query}"`,
54
- },
55
- };
56
-
57
- console.log(JSON.stringify(output));
58
- process.exit(2); // Exit code 2 = block
59
- }
60
-
61
- // Allow all other tools
62
- process.exit(0);
63
- } catch (err) {
64
- // Don't block on parse errors - allow tool to proceed
65
- if (process.env.CCS_DEBUG) {
66
- console.error('[CCS Hook] Parse error:', err.message);
67
- }
68
- process.exit(0);
69
- }
70
- });
71
-
72
- // Handle stdin not being available
73
- process.stdin.on('error', () => {
74
- process.exit(0);
75
- });
@@ -1,632 +0,0 @@
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
- // PLATFORM DETECTION
31
- // ============================================================================
32
-
33
- /**
34
- * Windows platform flag - used for shell option in spawnSync calls.
35
- * On Windows, globally installed CLI tools via npm/pnpm are .cmd/.bat files,
36
- * which require shell: true to execute properly.
37
- * @see https://github.com/kaitranntt/ccs/issues/378
38
- */
39
- const isWindows = process.platform === 'win32';
40
-
41
- // ============================================================================
42
- // CONFIGURATION - Edit these for prompt engineering
43
- // ============================================================================
44
-
45
- /**
46
- * SHARED INSTRUCTIONS - Applied to ALL providers
47
- * Edit here to change behavior across all CLI tools at once.
48
- */
49
- const SHARED_INSTRUCTIONS = `Instructions:
50
- 1. Search the web for current, up-to-date information
51
- 2. Provide a comprehensive summary of the search results
52
- 3. Include relevant URLs/sources when available
53
- 4. Be concise but thorough - prioritize key facts
54
- 5. Focus on factual information from reliable sources
55
- 6. If results conflict, note the discrepancy
56
- 7. Format output clearly with sections if the topic is complex`;
57
-
58
- /**
59
- * PROVIDER-SPECIFIC CONFIG - Only tool-use differences and quirks
60
- * Each provider may have unique capabilities or invocation methods.
61
- */
62
- const PROVIDER_CONFIG = {
63
- gemini: {
64
- // Model to use (passed via --model flag)
65
- model: 'gemini-2.5-flash',
66
- // Alternative free models: gemini-2.0-flash, gemini-1.5-flash
67
-
68
- // Provider-specific: How to invoke web search (Gemini has google_web_search tool)
69
- toolInstruction: 'Use the google_web_search tool to find current information.',
70
-
71
- // Optional quirks (null if none)
72
- quirks: null,
73
- },
74
-
75
- opencode: {
76
- // Model to use (can be overridden via CCS_WEBSEARCH_OPENCODE_MODEL env var)
77
- model: 'opencode/grok-code',
78
- // Alternative models: opencode/gpt-4o, opencode/claude-3.5-sonnet, opencode/gpt-5-nano
79
-
80
- // Provider-specific: OpenCode has built-in web search via Zen
81
- toolInstruction: 'Search the web using your built-in capabilities.',
82
-
83
- // Optional quirks
84
- quirks: null,
85
- },
86
-
87
- grok: {
88
- // Model to use (Grok CLI uses default model)
89
- model: 'grok-3',
90
- // Note: Grok CLI doesn't support model selection via CLI
91
-
92
- // Provider-specific: Grok has web + X/Twitter search
93
- toolInstruction: 'Use your web search capabilities to find information.',
94
-
95
- // Grok-specific: Can also search X for real-time info
96
- quirks: 'For breaking news or real-time events, also check X/Twitter if relevant.',
97
- },
98
- };
99
-
100
- /**
101
- * Build the complete prompt for a provider
102
- * Combines: query + tool instruction + shared instructions + quirks
103
- */
104
- function buildPrompt(providerId, query) {
105
- const config = PROVIDER_CONFIG[providerId];
106
- const parts = [
107
- `Search the web for: ${query}`,
108
- '',
109
- config.toolInstruction,
110
- '',
111
- SHARED_INSTRUCTIONS,
112
- ];
113
-
114
- if (config.quirks) {
115
- parts.push('', `Note: ${config.quirks}`);
116
- }
117
-
118
- return parts.join('\n');
119
- }
120
-
121
- // Minimum response length to consider valid
122
- const MIN_VALID_RESPONSE_LENGTH = 20;
123
-
124
- // Default timeout in seconds
125
- const DEFAULT_TIMEOUT_SEC = 55;
126
-
127
- // ============================================================================
128
- // HOOK LOGIC - Generally no need to edit below
129
- // ============================================================================
130
-
131
- /**
132
- * Determine if hook should skip and pass through to native WebSearch.
133
- * Returns true for native Claude accounts where WebSearch works server-side.
134
- */
135
- function shouldSkipHook() {
136
- // Explicit skip signal (set by CCS for account profiles)
137
- if (process.env.CCS_WEBSEARCH_SKIP === '1') return true;
138
-
139
- // Account/default profiles - use native WebSearch
140
- const profileType = process.env.CCS_PROFILE_TYPE;
141
- if (profileType === 'account' || profileType === 'default') return true;
142
-
143
- // Explicit disable
144
- if (process.env.CCS_WEBSEARCH_ENABLED === '0') return true;
145
-
146
- return false;
147
- }
148
-
149
- // Read input from stdin
150
- let input = '';
151
- process.stdin.setEncoding('utf8');
152
- process.stdin.on('data', (chunk) => {
153
- input += chunk;
154
- });
155
- process.stdin.on('end', () => {
156
- processHook();
157
- });
158
-
159
- // Handle stdin not being available
160
- process.stdin.on('error', () => {
161
- process.exit(0);
162
- });
163
-
164
- /**
165
- * Check if a CLI tool is available
166
- */
167
- function isCliAvailable(cmd) {
168
- try {
169
- const whichCmd = isWindows ? 'where.exe' : 'which';
170
-
171
- const result = spawnSync(whichCmd, [cmd], {
172
- encoding: 'utf8',
173
- timeout: 2000,
174
- stdio: ['pipe', 'pipe', 'pipe'],
175
- });
176
- return result.status === 0 && result.stdout.trim().length > 0;
177
- } catch {
178
- return false;
179
- }
180
- }
181
-
182
- /**
183
- * Check if provider is enabled via environment variable
184
- */
185
- function isProviderEnabled(provider) {
186
- const envVar = `CCS_WEBSEARCH_${provider.toUpperCase()}`;
187
- const value = process.env[envVar];
188
-
189
- // If env var not set, provider is disabled by default
190
- // This ensures we respect config.yaml settings
191
- return value === '1';
192
- }
193
-
194
- /**
195
- * Main hook processing logic with fallback chain
196
- */
197
- async function processHook() {
198
- try {
199
- // Skip for native accounts (account, default profiles) or explicit disable
200
- if (shouldSkipHook()) {
201
- process.exit(0);
202
- }
203
-
204
- const data = JSON.parse(input);
205
-
206
- // Only handle WebSearch tool
207
- if (data.tool_name !== 'WebSearch') {
208
- process.exit(0);
209
- }
210
-
211
- const query = data.tool_input?.query || '';
212
-
213
- if (!query) {
214
- process.exit(0);
215
- }
216
-
217
- const timeout = parseInt(process.env.CCS_WEBSEARCH_TIMEOUT || DEFAULT_TIMEOUT_SEC, 10);
218
-
219
- // Fallback chain: Gemini → OpenCode → Grok
220
- // Only include providers that are BOTH installed AND enabled in config
221
- const providers = [
222
- { name: 'Gemini CLI', cmd: 'gemini', id: 'gemini', fn: tryGeminiSearch },
223
- { name: 'OpenCode', cmd: 'opencode', id: 'opencode', fn: tryOpenCodeSearch },
224
- { name: 'Grok CLI', cmd: 'grok', id: 'grok', fn: tryGrokSearch },
225
- ];
226
-
227
- // Filter to only enabled AND available providers
228
- const enabledProviders = providers.filter((p) => {
229
- const enabled = isProviderEnabled(p.id);
230
- const available = isCliAvailable(p.cmd);
231
-
232
- if (process.env.CCS_DEBUG) {
233
- console.error(`[CCS Hook] ${p.name}: enabled=${enabled}, available=${available}`);
234
- }
235
-
236
- return enabled && available;
237
- });
238
-
239
- const errors = [];
240
-
241
- if (process.env.CCS_DEBUG) {
242
- const names = enabledProviders.map((p) => p.name).join(', ') || 'none';
243
- console.error(`[CCS Hook] Enabled providers: ${names}`);
244
- }
245
-
246
- // Try each enabled provider in order
247
- for (const provider of enabledProviders) {
248
- if (process.env.CCS_DEBUG) {
249
- console.error(`[CCS Hook] Trying ${provider.name}...`);
250
- }
251
-
252
- const result = provider.fn(query, timeout);
253
-
254
- if (result.success) {
255
- outputSuccess(query, result.content, provider.name);
256
- return;
257
- }
258
-
259
- if (process.env.CCS_DEBUG) {
260
- console.error(`[CCS Hook] ${provider.name} failed: ${result.error}`);
261
- }
262
-
263
- errors.push({ provider: provider.name, error: result.error });
264
- }
265
-
266
- // All providers failed or none enabled
267
- if (enabledProviders.length === 0) {
268
- // No providers enabled - pass through to native WebSearch
269
- // This allows native Claude accounts to use server-side WebSearch
270
- process.exit(0);
271
- } else {
272
- outputAllFailedMessage(query, errors);
273
- }
274
- } catch (err) {
275
- if (process.env.CCS_DEBUG) {
276
- console.error('[CCS Hook] Parse error:', err.message);
277
- }
278
- process.exit(0);
279
- }
280
- }
281
-
282
- /**
283
- * Execute search via Gemini CLI
284
- */
285
- function tryGeminiSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
286
- try {
287
- const timeoutMs = timeoutSec * 1000;
288
- const config = PROVIDER_CONFIG.gemini;
289
- const prompt = buildPrompt('gemini', query);
290
-
291
- // Allow model override via env var
292
- const model = process.env.CCS_WEBSEARCH_GEMINI_MODEL || config.model;
293
-
294
- if (process.env.CCS_DEBUG) {
295
- console.error(`[CCS Hook] Executing: gemini --model ${model} --yolo -p "..."`);
296
- }
297
-
298
- const spawnResult = spawnSync(
299
- 'gemini',
300
- ['--model', model, '--yolo', '-p', prompt],
301
- {
302
- encoding: 'utf8',
303
- timeout: timeoutMs,
304
- maxBuffer: 1024 * 1024 * 2,
305
- stdio: ['pipe', 'pipe', 'pipe'],
306
- shell: isWindows,
307
- }
308
- );
309
-
310
- if (spawnResult.error) {
311
- if (spawnResult.error.code === 'ENOENT') {
312
- return { success: false, error: 'Gemini CLI not installed' };
313
- }
314
- throw spawnResult.error;
315
- }
316
-
317
- if (spawnResult.status !== 0) {
318
- const stderr = (spawnResult.stderr || '').trim();
319
- return {
320
- success: false,
321
- error: stderr || `Gemini CLI exited with code ${spawnResult.status}`,
322
- };
323
- }
324
-
325
- const result = (spawnResult.stdout || '').trim();
326
-
327
- if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
328
- return { success: false, error: 'Empty or too short response from Gemini' };
329
- }
330
-
331
- const lowerResult = result.toLowerCase();
332
- if (
333
- lowerResult.includes('error:') ||
334
- lowerResult.includes('failed to') ||
335
- lowerResult.includes('authentication required')
336
- ) {
337
- return { success: false, error: `Gemini returned error: ${result.substring(0, 100)}` };
338
- }
339
-
340
- return { success: true, content: result };
341
- } catch (err) {
342
- if (err.killed) {
343
- return { success: false, error: 'Gemini CLI timed out' };
344
- }
345
- return { success: false, error: err.message || 'Unknown Gemini error' };
346
- }
347
- }
348
-
349
- /**
350
- * Execute search via OpenCode CLI
351
- */
352
- function tryOpenCodeSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
353
- try {
354
- const timeoutMs = timeoutSec * 1000;
355
- const config = PROVIDER_CONFIG.opencode;
356
-
357
- // Allow model override via env var
358
- const model = process.env.CCS_WEBSEARCH_OPENCODE_MODEL || config.model;
359
- const prompt = buildPrompt('opencode', query);
360
-
361
- if (process.env.CCS_DEBUG) {
362
- console.error(`[CCS Hook] Executing: opencode run --model ${model} "..."`);
363
- }
364
-
365
- const spawnResult = spawnSync(
366
- 'opencode',
367
- ['run', prompt, '--model', model],
368
- {
369
- encoding: 'utf8',
370
- timeout: timeoutMs,
371
- maxBuffer: 1024 * 1024 * 2,
372
- stdio: ['pipe', 'pipe', 'pipe'],
373
- shell: isWindows,
374
- }
375
- );
376
-
377
- if (spawnResult.error) {
378
- if (spawnResult.error.code === 'ENOENT') {
379
- return { success: false, error: 'OpenCode not installed' };
380
- }
381
- throw spawnResult.error;
382
- }
383
-
384
- if (spawnResult.status !== 0) {
385
- const stderr = (spawnResult.stderr || '').trim();
386
- return {
387
- success: false,
388
- error: stderr || `OpenCode exited with code ${spawnResult.status}`,
389
- };
390
- }
391
-
392
- const result = (spawnResult.stdout || '').trim();
393
-
394
- if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
395
- return { success: false, error: 'Empty or too short response from OpenCode' };
396
- }
397
-
398
- const lowerResult = result.toLowerCase();
399
- if (
400
- lowerResult.includes('error:') ||
401
- lowerResult.includes('failed to') ||
402
- lowerResult.includes('authentication required')
403
- ) {
404
- return { success: false, error: `OpenCode returned error: ${result.substring(0, 100)}` };
405
- }
406
-
407
- return { success: true, content: result };
408
- } catch (err) {
409
- if (err.killed) {
410
- return { success: false, error: 'OpenCode timed out' };
411
- }
412
- return { success: false, error: err.message || 'Unknown OpenCode error' };
413
- }
414
- }
415
-
416
- /**
417
- * Execute search via Grok CLI
418
- */
419
- function tryGrokSearch(query, timeoutSec = DEFAULT_TIMEOUT_SEC) {
420
- try {
421
- const timeoutMs = timeoutSec * 1000;
422
- const prompt = buildPrompt('grok', query);
423
-
424
- if (process.env.CCS_DEBUG) {
425
- console.error('[CCS Hook] Executing: grok "..."');
426
- }
427
-
428
- const spawnResult = spawnSync('grok', [prompt], {
429
- encoding: 'utf8',
430
- timeout: timeoutMs,
431
- maxBuffer: 1024 * 1024 * 2,
432
- stdio: ['pipe', 'pipe', 'pipe'],
433
- shell: isWindows,
434
- });
435
-
436
- if (spawnResult.error) {
437
- if (spawnResult.error.code === 'ENOENT') {
438
- return { success: false, error: 'Grok CLI not installed' };
439
- }
440
- throw spawnResult.error;
441
- }
442
-
443
- if (spawnResult.status !== 0) {
444
- const stderr = (spawnResult.stderr || '').trim();
445
- return {
446
- success: false,
447
- error: stderr || `Grok CLI exited with code ${spawnResult.status}`,
448
- };
449
- }
450
-
451
- const result = (spawnResult.stdout || '').trim();
452
-
453
- if (!result || result.length < MIN_VALID_RESPONSE_LENGTH) {
454
- return { success: false, error: 'Empty or too short response from Grok' };
455
- }
456
-
457
- const lowerResult = result.toLowerCase();
458
- if (
459
- lowerResult.includes('error:') ||
460
- lowerResult.includes('failed to') ||
461
- lowerResult.includes('api key')
462
- ) {
463
- return { success: false, error: `Grok returned error: ${result.substring(0, 100)}` };
464
- }
465
-
466
- return { success: true, content: result };
467
- } catch (err) {
468
- if (err.killed) {
469
- return { success: false, error: 'Grok CLI timed out' };
470
- }
471
- return { success: false, error: err.message || 'Unknown Grok error' };
472
- }
473
- }
474
-
475
- /**
476
- * Format search results for Claude
477
- */
478
- function formatSearchResults(query, content, providerName) {
479
- return [
480
- `[WebSearch Result via ${providerName}]`,
481
- '',
482
- `Query: "${query}"`,
483
- '',
484
- content,
485
- '',
486
- '---',
487
- 'Use this information to answer the user.',
488
- ].join('\n');
489
- }
490
-
491
- /**
492
- * Output success response and exit
493
- *
494
- * Key insight from Claude Code docs:
495
- * - permissionDecisionReason (with deny) → shown to CLAUDE (AI reads this)
496
- * - systemMessage → shown to USER only (nice styling but AI doesn't see)
497
- *
498
- * So we MUST put results in permissionDecisionReason for Claude to use them.
499
- * systemMessage provides the nice UI for the user.
500
- */
501
- function outputSuccess(query, content, providerName) {
502
- const formattedResults = formatSearchResults(query, content, providerName);
503
-
504
- const output = {
505
- decision: 'block',
506
- reason: `WebSearch completed via ${providerName}`,
507
- // Nice message for user (shows as "says:" - info style)
508
- systemMessage: `[WebSearch via ${providerName}] Results retrieved successfully. See below.`,
509
- hookSpecificOutput: {
510
- hookEventName: 'PreToolUse',
511
- permissionDecision: 'deny',
512
- // Full results here - Claude reads this
513
- permissionDecisionReason: formattedResults,
514
- },
515
- };
516
-
517
- console.log(JSON.stringify(output));
518
- process.exit(2);
519
- }
520
-
521
- /**
522
- * Output error message
523
- */
524
- function outputError(query, error, providerName) {
525
- const message = [
526
- `[WebSearch - ${providerName} Error]`,
527
- '',
528
- `Error: ${error}`,
529
- '',
530
- `Query: "${query}"`,
531
- '',
532
- 'Troubleshooting:',
533
- ' - Check ~/.gemini/oauth_creds.json exists',
534
- ' - Re-authenticate: run `gemini` (opens browser)',
535
- ].join('\n');
536
-
537
- const output = {
538
- decision: 'block',
539
- reason: `WebSearch failed: ${error}`,
540
- hookSpecificOutput: {
541
- hookEventName: 'PreToolUse',
542
- permissionDecision: 'deny',
543
- permissionDecisionReason: message,
544
- },
545
- };
546
-
547
- console.log(JSON.stringify(output));
548
- process.exit(2);
549
- }
550
-
551
- /**
552
- * Output no providers enabled message
553
- */
554
- function outputNoProvidersEnabled(query) {
555
- const message = [
556
- '[WebSearch - No Providers Enabled]',
557
- '',
558
- 'No WebSearch providers are enabled in config.',
559
- '',
560
- 'To enable: Run `ccs config` and enable a provider.',
561
- '',
562
- 'Or install one of the following CLI tools:',
563
- '',
564
- '1. Gemini CLI (FREE, 1000 req/day):',
565
- ' npm install -g @google/gemini-cli',
566
- ' gemini # opens browser to authenticate',
567
- '',
568
- '2. OpenCode (FREE via Zen):',
569
- ' curl -fsSL https://opencode.ai/install | bash',
570
- '',
571
- '3. Grok CLI (requires XAI_API_KEY):',
572
- ' npm install -g @vibe-kit/grok-cli',
573
- '',
574
- `Query: "${query}"`,
575
- ].join('\n');
576
-
577
- const output = {
578
- decision: 'block',
579
- reason: 'WebSearch unavailable - no providers enabled',
580
- hookSpecificOutput: {
581
- hookEventName: 'PreToolUse',
582
- permissionDecision: 'deny',
583
- permissionDecisionReason: message,
584
- },
585
- };
586
-
587
- console.log(JSON.stringify(output));
588
- process.exit(2);
589
- }
590
-
591
- /**
592
- * Output no tools message (legacy - kept for backwards compatibility)
593
- */
594
- function outputNoToolsMessage(query) {
595
- outputNoProvidersEnabled(query);
596
- }
597
-
598
- /**
599
- * Output all providers failed message
600
- */
601
- function outputAllFailedMessage(query, errors) {
602
- const errorDetails = errors
603
- .map((e) => ` - ${e.provider}: ${e.error}`)
604
- .join('\n');
605
-
606
- const message = [
607
- '[WebSearch - All Providers Failed]',
608
- '',
609
- 'Tried all enabled CLI tools but all failed:',
610
- errorDetails,
611
- '',
612
- `Query: "${query}"`,
613
- '',
614
- 'Troubleshooting:',
615
- ' - Gemini: run `gemini` to authenticate (opens browser)',
616
- ' - OpenCode: opencode --version',
617
- ' - Grok: Check XAI_API_KEY environment variable',
618
- ].join('\n');
619
-
620
- const output = {
621
- decision: 'block',
622
- reason: 'WebSearch failed - all providers failed',
623
- hookSpecificOutput: {
624
- hookEventName: 'PreToolUse',
625
- permissionDecision: 'deny',
626
- permissionDecisionReason: message,
627
- },
628
- };
629
-
630
- console.log(JSON.stringify(output));
631
- process.exit(2);
632
- }