@vinaes/succ 1.3.22 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/README.md +6 -2
  2. package/agents/succ-code-reviewer.md +1 -1
  3. package/agents/succ-diff-reviewer.md +1 -1
  4. package/agents/succ-general.md +1 -1
  5. package/dist/cli.js +13 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/analyze-agents.d.ts.map +1 -1
  8. package/dist/commands/analyze-agents.js +10 -11
  9. package/dist/commands/analyze-agents.js.map +1 -1
  10. package/dist/commands/analyze-recursive.d.ts.map +1 -1
  11. package/dist/commands/analyze-recursive.js +2 -1
  12. package/dist/commands/analyze-recursive.js.map +1 -1
  13. package/dist/commands/analyze-utils.d.ts +1 -1
  14. package/dist/commands/analyze-utils.d.ts.map +1 -1
  15. package/dist/commands/analyze-utils.js +1 -2
  16. package/dist/commands/analyze-utils.js.map +1 -1
  17. package/dist/commands/init-templates.js +2 -2
  18. package/dist/commands/soul.d.ts.map +1 -1
  19. package/dist/commands/soul.js +4 -44
  20. package/dist/commands/soul.js.map +1 -1
  21. package/dist/daemon/analyzer.js +2 -2
  22. package/dist/daemon/analyzer.js.map +1 -1
  23. package/dist/daemon/service.d.ts.map +1 -1
  24. package/dist/daemon/service.js +37 -3
  25. package/dist/daemon/service.js.map +1 -1
  26. package/dist/daemon/session-processor.d.ts.map +1 -1
  27. package/dist/daemon/session-processor.js +10 -184
  28. package/dist/daemon/session-processor.js.map +1 -1
  29. package/dist/lib/compact-briefing.d.ts.map +1 -1
  30. package/dist/lib/compact-briefing.js +8 -4
  31. package/dist/lib/compact-briefing.js.map +1 -1
  32. package/dist/lib/config-types.d.ts +9 -0
  33. package/dist/lib/config-types.d.ts.map +1 -1
  34. package/dist/lib/config-validation.d.ts.map +1 -1
  35. package/dist/lib/config-validation.js +1 -0
  36. package/dist/lib/config-validation.js.map +1 -1
  37. package/dist/lib/config.d.ts +14 -0
  38. package/dist/lib/config.d.ts.map +1 -1
  39. package/dist/lib/config.js +37 -0
  40. package/dist/lib/config.js.map +1 -1
  41. package/dist/lib/consolidate.d.ts.map +1 -1
  42. package/dist/lib/consolidate.js +7 -2
  43. package/dist/lib/consolidate.js.map +1 -1
  44. package/dist/lib/db/connection.d.ts.map +1 -1
  45. package/dist/lib/db/connection.js +8 -1
  46. package/dist/lib/db/connection.js.map +1 -1
  47. package/dist/lib/db/index.d.ts +1 -1
  48. package/dist/lib/db/index.d.ts.map +1 -1
  49. package/dist/lib/db/index.js +1 -1
  50. package/dist/lib/db/index.js.map +1 -1
  51. package/dist/lib/db/memories.d.ts +6 -0
  52. package/dist/lib/db/memories.d.ts.map +1 -1
  53. package/dist/lib/db/memories.js +34 -0
  54. package/dist/lib/db/memories.js.map +1 -1
  55. package/dist/lib/graph/llm-relations.d.ts.map +1 -1
  56. package/dist/lib/graph/llm-relations.js +7 -40
  57. package/dist/lib/graph/llm-relations.js.map +1 -1
  58. package/dist/lib/graph-export.js +1 -2
  59. package/dist/lib/graph-export.js.map +1 -1
  60. package/dist/lib/hook-rules.d.ts +31 -0
  61. package/dist/lib/hook-rules.d.ts.map +1 -0
  62. package/dist/lib/hook-rules.js +102 -0
  63. package/dist/lib/hook-rules.js.map +1 -0
  64. package/dist/lib/llm.d.ts +14 -0
  65. package/dist/lib/llm.d.ts.map +1 -1
  66. package/dist/lib/llm.js +30 -9
  67. package/dist/lib/llm.js.map +1 -1
  68. package/dist/lib/ort-session.d.ts.map +1 -1
  69. package/dist/lib/ort-session.js +0 -1
  70. package/dist/lib/ort-session.js.map +1 -1
  71. package/dist/lib/prd/generate.d.ts.map +1 -1
  72. package/dist/lib/prd/generate.js +2 -1
  73. package/dist/lib/prd/generate.js.map +1 -1
  74. package/dist/lib/prd/parse.d.ts.map +1 -1
  75. package/dist/lib/prd/parse.js +2 -1
  76. package/dist/lib/prd/parse.js.map +1 -1
  77. package/dist/lib/prd/prompt-builder.d.ts +9 -2
  78. package/dist/lib/prd/prompt-builder.d.ts.map +1 -1
  79. package/dist/lib/prd/prompt-builder.js +7 -8
  80. package/dist/lib/prd/prompt-builder.js.map +1 -1
  81. package/dist/lib/prd/runner.js +2 -1
  82. package/dist/lib/prd/runner.js.map +1 -1
  83. package/dist/lib/prd/team-runner.js +2 -1
  84. package/dist/lib/prd/team-runner.js.map +1 -1
  85. package/dist/lib/precompute-context.d.ts.map +1 -1
  86. package/dist/lib/precompute-context.js +7 -2
  87. package/dist/lib/precompute-context.js.map +1 -1
  88. package/dist/lib/public-api.d.ts +14 -4
  89. package/dist/lib/public-api.d.ts.map +1 -1
  90. package/dist/lib/public-api.js +14 -2
  91. package/dist/lib/public-api.js.map +1 -1
  92. package/dist/lib/query-expansion.d.ts.map +1 -1
  93. package/dist/lib/query-expansion.js +2 -12
  94. package/dist/lib/query-expansion.js.map +1 -1
  95. package/dist/lib/reflection-synthesizer.d.ts.map +1 -1
  96. package/dist/lib/reflection-synthesizer.js +2 -16
  97. package/dist/lib/reflection-synthesizer.js.map +1 -1
  98. package/dist/lib/session-summary.d.ts +1 -0
  99. package/dist/lib/session-summary.d.ts.map +1 -1
  100. package/dist/lib/session-summary.js +10 -2
  101. package/dist/lib/session-summary.js.map +1 -1
  102. package/dist/lib/skills.d.ts.map +1 -1
  103. package/dist/lib/skills.js +5 -5
  104. package/dist/lib/skills.js.map +1 -1
  105. package/dist/lib/storage/backends/postgresql.d.ts +1 -0
  106. package/dist/lib/storage/backends/postgresql.d.ts.map +1 -1
  107. package/dist/lib/storage/backends/postgresql.js +33 -0
  108. package/dist/lib/storage/backends/postgresql.js.map +1 -1
  109. package/dist/lib/storage/dispatcher.d.ts +1 -0
  110. package/dist/lib/storage/dispatcher.d.ts.map +1 -1
  111. package/dist/lib/storage/dispatcher.js +6 -0
  112. package/dist/lib/storage/dispatcher.js.map +1 -1
  113. package/dist/lib/storage/index.d.ts +1 -0
  114. package/dist/lib/storage/index.d.ts.map +1 -1
  115. package/dist/lib/storage/index.js +4 -0
  116. package/dist/lib/storage/index.js.map +1 -1
  117. package/dist/lib/supersession.d.ts.map +1 -1
  118. package/dist/lib/supersession.js +2 -15
  119. package/dist/lib/supersession.js.map +1 -1
  120. package/dist/mcp/helpers.d.ts +11 -0
  121. package/dist/mcp/helpers.d.ts.map +1 -1
  122. package/dist/mcp/helpers.js +35 -0
  123. package/dist/mcp/helpers.js.map +1 -1
  124. package/dist/mcp/profile.d.ts +16 -0
  125. package/dist/mcp/profile.d.ts.map +1 -0
  126. package/dist/mcp/profile.js +64 -0
  127. package/dist/mcp/profile.js.map +1 -0
  128. package/dist/mcp/server.d.ts +16 -15
  129. package/dist/mcp/server.d.ts.map +1 -1
  130. package/dist/mcp/server.js +119 -18
  131. package/dist/mcp/server.js.map +1 -1
  132. package/dist/mcp/tools/config.d.ts +1 -5
  133. package/dist/mcp/tools/config.d.ts.map +1 -1
  134. package/dist/mcp/tools/config.js +224 -180
  135. package/dist/mcp/tools/config.js.map +1 -1
  136. package/dist/mcp/tools/dead-end.d.ts.map +1 -1
  137. package/dist/mcp/tools/dead-end.js +20 -11
  138. package/dist/mcp/tools/dead-end.js.map +1 -1
  139. package/dist/mcp/tools/debug.d.ts.map +1 -1
  140. package/dist/mcp/tools/debug.js +76 -67
  141. package/dist/mcp/tools/debug.js.map +1 -1
  142. package/dist/mcp/tools/graph.d.ts +0 -1
  143. package/dist/mcp/tools/graph.d.ts.map +1 -1
  144. package/dist/mcp/tools/graph.js +91 -87
  145. package/dist/mcp/tools/graph.js.map +1 -1
  146. package/dist/mcp/tools/indexing.d.ts +1 -7
  147. package/dist/mcp/tools/indexing.d.ts.map +1 -1
  148. package/dist/mcp/tools/indexing.js +274 -261
  149. package/dist/mcp/tools/indexing.js.map +1 -1
  150. package/dist/mcp/tools/memory.d.ts.map +1 -1
  151. package/dist/mcp/tools/memory.js +124 -63
  152. package/dist/mcp/tools/memory.js.map +1 -1
  153. package/dist/mcp/tools/prd.d.ts +1 -7
  154. package/dist/mcp/tools/prd.d.ts.map +1 -1
  155. package/dist/mcp/tools/prd.js +240 -246
  156. package/dist/mcp/tools/prd.js.map +1 -1
  157. package/dist/mcp/tools/search.d.ts.map +1 -1
  158. package/dist/mcp/tools/search.js +91 -41
  159. package/dist/mcp/tools/search.js.map +1 -1
  160. package/dist/mcp/tools/status.d.ts +1 -5
  161. package/dist/mcp/tools/status.d.ts.map +1 -1
  162. package/dist/mcp/tools/status.js +310 -272
  163. package/dist/mcp/tools/status.js.map +1 -1
  164. package/dist/mcp/tools/web-fetch.d.ts +2 -1
  165. package/dist/mcp/tools/web-fetch.d.ts.map +1 -1
  166. package/dist/mcp/tools/web-fetch.js +68 -43
  167. package/dist/mcp/tools/web-fetch.js.map +1 -1
  168. package/dist/mcp/tools/web-search.d.ts +1 -6
  169. package/dist/mcp/tools/web-search.d.ts.map +1 -1
  170. package/dist/mcp/tools/web-search.js +266 -265
  171. package/dist/mcp/tools/web-search.js.map +1 -1
  172. package/dist/prompts/briefing.d.ts +10 -4
  173. package/dist/prompts/briefing.d.ts.map +1 -1
  174. package/dist/prompts/briefing.js +33 -35
  175. package/dist/prompts/briefing.js.map +1 -1
  176. package/dist/prompts/daemon.d.ts +5 -10
  177. package/dist/prompts/daemon.d.ts.map +1 -1
  178. package/dist/prompts/daemon.js +9 -18
  179. package/dist/prompts/daemon.js.map +1 -1
  180. package/dist/prompts/extraction.d.ts +19 -3
  181. package/dist/prompts/extraction.d.ts.map +1 -1
  182. package/dist/prompts/extraction.js +36 -45
  183. package/dist/prompts/extraction.js.map +1 -1
  184. package/dist/prompts/graph.d.ts +9 -0
  185. package/dist/prompts/graph.d.ts.map +1 -0
  186. package/dist/prompts/graph.js +27 -0
  187. package/dist/prompts/graph.js.map +1 -0
  188. package/dist/prompts/index.d.ts +16 -7
  189. package/dist/prompts/index.d.ts.map +1 -1
  190. package/dist/prompts/index.js +21 -7
  191. package/dist/prompts/index.js.map +1 -1
  192. package/dist/prompts/memory.d.ts +6 -3
  193. package/dist/prompts/memory.d.ts.map +1 -1
  194. package/dist/prompts/memory.js +9 -8
  195. package/dist/prompts/memory.js.map +1 -1
  196. package/dist/prompts/onboarding.d.ts +1 -1
  197. package/dist/prompts/onboarding.d.ts.map +1 -1
  198. package/dist/prompts/onboarding.js +2 -3
  199. package/dist/prompts/onboarding.js.map +1 -1
  200. package/dist/prompts/prd.d.ts +15 -6
  201. package/dist/prompts/prd.d.ts.map +1 -1
  202. package/dist/prompts/prd.js +44 -38
  203. package/dist/prompts/prd.js.map +1 -1
  204. package/dist/prompts/query-expansion.d.ts +8 -0
  205. package/dist/prompts/query-expansion.d.ts.map +1 -0
  206. package/dist/prompts/query-expansion.js +17 -0
  207. package/dist/prompts/query-expansion.js.map +1 -0
  208. package/dist/prompts/skills.d.ts +5 -10
  209. package/dist/prompts/skills.d.ts.map +1 -1
  210. package/dist/prompts/skills.js +9 -17
  211. package/dist/prompts/skills.js.map +1 -1
  212. package/dist/prompts/soul.d.ts +9 -0
  213. package/dist/prompts/soul.d.ts.map +1 -0
  214. package/dist/prompts/soul.js +47 -0
  215. package/dist/prompts/soul.js.map +1 -0
  216. package/dist/prompts/supersession.d.ts +9 -0
  217. package/dist/prompts/supersession.d.ts.map +1 -0
  218. package/dist/prompts/supersession.js +21 -0
  219. package/dist/prompts/supersession.js.map +1 -0
  220. package/dist/prompts/synthesis.d.ts +9 -0
  221. package/dist/prompts/synthesis.d.ts.map +1 -0
  222. package/dist/prompts/synthesis.js +22 -0
  223. package/dist/prompts/synthesis.js.map +1 -0
  224. package/hooks/succ-pre-tool.cjs +304 -76
  225. package/hooks/succ-session-start.cjs +58 -27
  226. package/package.json +5 -4
@@ -1,10 +1,5 @@
1
1
  /**
2
- * MCP Web Search tools via OpenRouter (Perplexity, Grok, and other search-capable models)
3
- *
4
- * - succ_quick_search: Fast, cheap search (default: Perplexity Sonar)
5
- * - succ_web_search: Quality search (default: Perplexity Sonar Pro)
6
- * - succ_deep_research: Multi-step deep research (default: Perplexity Sonar Deep Research)
7
- * - succ_web_search_history: Browse and filter past web search history
2
+ * MCP Web Search tool succ_web with actions: quick, search, deep, history
8
3
  *
9
4
  * Models are configurable via web_search.* config keys. Any OpenRouter model
10
5
  * with :online suffix supports web search (e.g., x-ai/grok-3:online).
@@ -13,6 +8,7 @@ import { z } from 'zod';
13
8
  import { getWebSearchConfig } from '../../lib/config.js';
14
9
  import { isApiConfigured, callOpenRouterSearch, } from '../../lib/llm.js';
15
10
  import { projectPathParam, applyProjectPath } from '../helpers.js';
11
+ import { gateAction } from '../profile.js';
16
12
  import { recordWebSearch, getTodayWebSearchSpend, getWebSearchHistory, getWebSearchSummary, } from '../../lib/storage/index.js';
17
13
  import { logWarn } from '../../lib/fault-logger.js';
18
14
  // Approximate pricing per 1M tokens (USD) — OpenRouter models with web search
@@ -77,7 +73,7 @@ async function checkBudget(dailyBudget) {
77
73
  try {
78
74
  const spent = await getTodayWebSearchSpend();
79
75
  if (spent >= dailyBudget) {
80
- return `Daily web search budget exceeded ($${spent.toFixed(4)} / $${dailyBudget}). Reset tomorrow or increase with: succ_config_set key="web_search.daily_budget_usd" value="..."`;
76
+ return `Daily web search budget exceeded ($${spent.toFixed(4)} / $${dailyBudget}). Reset tomorrow or increase with: succ_config(action="set", key="web_search.daily_budget_usd", value="...")`;
81
77
  }
82
78
  }
83
79
  catch (err) {
@@ -146,281 +142,286 @@ async function saveResultToMemory(query, content, citations, toolName, type) {
146
142
  }
147
143
  }
148
144
  export function registerWebSearchTools(server) {
149
- // succ_quick_search — cheapest, fastest web search
150
- server.tool('succ_quick_search', 'Quick, cheap web search via OpenRouter (default: Perplexity Sonar, ~$1/MTok). Best for simple factual queries: version numbers, release dates, quick lookups. Configure model with web_search.quick_search_model. Requires OPENROUTER_API_KEY.', {
151
- query: z
152
- .string()
153
- .describe('Simple factual query (e.g., "latest Node.js LTS version", "TypeScript 5.8 release date")'),
154
- system_prompt: z
155
- .string()
156
- .optional()
157
- .describe('Optional system prompt to guide response format'),
158
- max_tokens: z.number().optional().describe('Max response tokens (default: 2000)'),
159
- save_to_memory: z
160
- .boolean()
161
- .optional()
162
- .describe('Save result to succ memory (default: from config)'),
163
- project_path: projectPathParam,
164
- }, async ({ query, system_prompt, max_tokens, save_to_memory, project_path }) => {
165
- await applyProjectPath(project_path);
166
- if (!isApiConfigured()) {
167
- return {
168
- content: [
169
- {
170
- type: 'text',
171
- text: 'API key not configured. Set OPENROUTER_API_KEY environment variable or run:\nsucc_config_set key="llm.api_key" value="sk-or-..."',
172
- },
173
- ],
174
- isError: true,
175
- };
176
- }
177
- const wsConfig = getWebSearchConfig();
178
- if (!wsConfig.enabled) {
179
- return {
180
- content: [
181
- {
182
- type: 'text',
183
- text: 'Web search is disabled. Enable with: succ_config_set key="web_search.enabled" value="true"',
184
- },
185
- ],
186
- isError: true,
187
- };
188
- }
189
- const budgetError = await checkBudget(wsConfig.daily_budget_usd);
190
- if (budgetError)
191
- return { content: [{ type: 'text', text: budgetError }], isError: true };
192
- try {
193
- const messages = [];
194
- if (system_prompt) {
195
- messages.push({ role: 'system', content: system_prompt });
196
- }
197
- messages.push({ role: 'user', content: query });
198
- const effectiveModel = wsConfig.quick_search_model;
199
- const result = await callOpenRouterSearch(messages, effectiveModel, wsConfig.quick_search_timeout_ms, max_tokens || wsConfig.quick_search_max_tokens, wsConfig.temperature);
200
- const cost = estimateCost(result.usage, effectiveModel);
201
- await recordSearchToDb('succ_quick_search', effectiveModel, query, result.usage, cost, result.citations, false, result.content.length);
202
- const todaySpent = wsConfig.daily_budget_usd > 0 ? await getTodayWebSearchSpend() : 0;
203
- let text = result.content;
204
- text += formatCitations(result.citations, result.search_results);
205
- text += formatUsage(result.usage, cost, wsConfig.daily_budget_usd, todaySpent);
206
- const shouldSave = save_to_memory ?? wsConfig.save_to_memory;
207
- if (shouldSave) {
208
- text += await saveResultToMemory(query, result.content, result.citations, 'succ_quick_search', 'observation');
209
- }
210
- return { content: [{ type: 'text', text }] };
211
- }
212
- catch (error) {
213
- return {
214
- content: [{ type: 'text', text: `Quick search failed: ${error.message}` }],
215
- isError: true,
216
- };
217
- }
218
- });
219
- // succ_web_search — fast real-time web search
220
- server.tool('succ_web_search', 'Web search via OpenRouter (default: Perplexity Sonar Pro). Higher quality than succ_quick_search. Use for complex queries, documentation lookups, multi-faceted questions. Returns answers with citations. Alternatives: x-ai/grok-3:online, google/gemini-2.0-flash-001:online, or any model with :online suffix. Requires OPENROUTER_API_KEY.', {
221
- query: z
222
- .string()
223
- .describe('The search query (e.g., "latest React 19 features", "how to configure nginx reverse proxy")'),
224
- model: z
225
- .string()
226
- .optional()
227
- .describe('Override search model. Default from config (perplexity/sonar-pro). Perplexity: sonar, sonar-pro, sonar-reasoning-pro. Grok: x-ai/grok-3:online, x-ai/grok-3-mini:online. Any OpenRouter model with :online suffix supports web search.'),
228
- system_prompt: z
229
- .string()
230
- .optional()
231
- .describe('Optional system prompt to guide the response format or focus'),
232
- max_tokens: z.number().optional().describe('Max response tokens (default: 4000)'),
233
- save_to_memory: z
234
- .boolean()
235
- .optional()
236
- .describe('Save result to succ memory (default: from config)'),
237
- project_path: projectPathParam,
238
- }, async ({ query, model, system_prompt, max_tokens, save_to_memory, project_path }) => {
239
- await applyProjectPath(project_path);
240
- if (!isApiConfigured()) {
241
- return {
242
- content: [
243
- {
244
- type: 'text',
245
- text: 'API key not configured. Set OPENROUTER_API_KEY environment variable or run:\nsucc_config_set key="llm.api_key" value="sk-or-..."',
246
- },
247
- ],
248
- isError: true,
249
- };
250
- }
251
- const wsConfig = getWebSearchConfig();
252
- if (!wsConfig.enabled) {
253
- return {
254
- content: [
255
- {
256
- type: 'text',
257
- text: 'Web search is disabled. Enable with: succ_config_set key="web_search.enabled" value="true"',
258
- },
259
- ],
260
- isError: true,
261
- };
262
- }
263
- const budgetError = await checkBudget(wsConfig.daily_budget_usd);
264
- if (budgetError)
265
- return { content: [{ type: 'text', text: budgetError }], isError: true };
266
- const effectiveModel = model || wsConfig.model;
267
- try {
268
- const messages = [];
269
- if (system_prompt) {
270
- messages.push({ role: 'system', content: system_prompt });
271
- }
272
- messages.push({ role: 'user', content: query });
273
- const result = await callOpenRouterSearch(messages, effectiveModel, wsConfig.timeout_ms, max_tokens || wsConfig.max_tokens, wsConfig.temperature);
274
- const cost = estimateCost(result.usage, effectiveModel);
275
- await recordSearchToDb('succ_web_search', effectiveModel, query, result.usage, cost, result.citations, false, result.content.length);
276
- const todaySpent = wsConfig.daily_budget_usd > 0 ? await getTodayWebSearchSpend() : 0;
277
- let text = result.content;
278
- text += formatCitations(result.citations, result.search_results);
279
- text += formatUsage(result.usage, cost, wsConfig.daily_budget_usd, todaySpent);
280
- const shouldSave = save_to_memory ?? wsConfig.save_to_memory;
281
- if (shouldSave) {
282
- text += await saveResultToMemory(query, result.content, result.citations, 'succ_web_search', 'observation');
283
- }
284
- return { content: [{ type: 'text', text }] };
285
- }
286
- catch (error) {
287
- return {
288
- content: [{ type: 'text', text: `Web search failed: ${error.message}` }],
289
- isError: true,
290
- };
291
- }
292
- });
293
- // succ_deep_research — expensive multi-step research
294
- server.tool('succ_deep_research', 'Deep multi-step web research via OpenRouter (default: Perplexity Sonar Deep Research). Autonomously searches, reads, and synthesizes multiple sources. WARNING: Significantly more expensive and slower than succ_web_search (30-120s, runs 30+ searches). Configure model with web_search.deep_research_model. Requires OPENROUTER_API_KEY.', {
295
- query: z
296
- .string()
297
- .describe('The research question (e.g., "Compare React Server Components vs Astro Islands for e-commerce")'),
298
- system_prompt: z
299
- .string()
300
- .optional()
301
- .describe('Optional system prompt to guide research focus or output format'),
302
- max_tokens: z.number().optional().describe('Max response tokens (default: 8000)'),
303
- include_reasoning: z
304
- .boolean()
305
- .optional()
306
- .describe("Include the model's internal reasoning process (default: false)"),
307
- save_to_memory: z
308
- .boolean()
309
- .optional()
310
- .describe('Save result to succ memory (default: from config)'),
311
- project_path: projectPathParam,
312
- }, async ({ query, system_prompt, max_tokens, include_reasoning, save_to_memory, project_path, }) => {
145
+ server.registerTool('succ_web', {
146
+ description: 'Web search via OpenRouter (Perplexity, Grok, Gemini). Supports quick lookups, quality search, deep multi-step research, and search history.\n\nExamples:\n- Quick: succ_web(action="quick", query="Node.js 22 LTS release date")\n- Search: succ_web(query="React 19 best practices")\n- Deep: succ_web(action="deep", query="Compare React vs Astro for e-commerce")\n- History: succ_web(action="history", limit=5)',
147
+ inputSchema: {
148
+ action: z
149
+ .enum(['quick', 'search', 'deep', 'history'])
150
+ .optional()
151
+ .default('search')
152
+ .describe('quick = fast factual lookup (~$1/MTok), search = quality search (Sonar Pro), deep = multi-step research (30-120s, expensive), history = past searches'),
153
+ query: z.string().optional().describe('Search query (required for quick, search, deep)'),
154
+ system_prompt: z.string().optional().describe('Optional system prompt to guide response'),
155
+ max_tokens: z.number().optional().describe('Max response tokens'),
156
+ save_to_memory: z
157
+ .boolean()
158
+ .optional()
159
+ .describe('Save result to succ memory (default: from config)'),
160
+ model: z
161
+ .string()
162
+ .optional()
163
+ .describe('Override search model (for search) or filter by model (for history)'),
164
+ include_reasoning: z
165
+ .boolean()
166
+ .optional()
167
+ .describe("Include model's reasoning process (for deep)"),
168
+ // history filters
169
+ tool_name: z
170
+ .enum(['succ_quick_search', 'succ_web_search', 'succ_deep_research'])
171
+ .optional()
172
+ .describe('Filter by original tool name (for history, uses DB column values)'),
173
+ query_text: z.string().optional().describe('Filter by query substring (for history)'),
174
+ date_from: z.string().optional().describe('Start date ISO (for history)'),
175
+ date_to: z.string().optional().describe('End date ISO (for history)'),
176
+ limit: z.number().optional().describe('Max records (for history, default: 20)'),
177
+ project_path: projectPathParam,
178
+ },
179
+ annotations: {
180
+ readOnlyHint: false,
181
+ destructiveHint: false,
182
+ idempotentHint: false,
183
+ openWorldHint: true,
184
+ },
185
+ }, async ({ action = 'search', query, system_prompt, max_tokens, save_to_memory, model, include_reasoning, tool_name, query_text, date_from, date_to, limit, project_path, }) => {
313
186
  await applyProjectPath(project_path);
314
- if (!isApiConfigured()) {
315
- return {
316
- content: [
317
- {
318
- type: 'text',
319
- text: 'API key not configured. Set OPENROUTER_API_KEY environment variable or run:\nsucc_config_set key="llm.api_key" value="sk-or-..."',
320
- },
321
- ],
322
- isError: true,
323
- };
324
- }
325
- const wsConfig = getWebSearchConfig();
326
- if (!wsConfig.enabled) {
187
+ const gated = gateAction('succ_web', action);
188
+ if (gated)
189
+ return gated;
190
+ // Shared validation for search actions
191
+ if (['quick', 'search', 'deep'].includes(action) && !query) {
327
192
  return {
328
193
  content: [
329
194
  {
330
195
  type: 'text',
331
- text: 'Web search is disabled. Enable with: succ_config_set key="web_search.enabled" value="true"',
196
+ text: `"query" is required for action="${action}"`,
332
197
  },
333
198
  ],
334
199
  isError: true,
335
200
  };
336
201
  }
337
- const budgetError = await checkBudget(wsConfig.daily_budget_usd);
338
- if (budgetError)
339
- return { content: [{ type: 'text', text: budgetError }], isError: true };
340
- try {
341
- const messages = [];
342
- if (system_prompt) {
343
- messages.push({ role: 'system', content: system_prompt });
344
- }
345
- messages.push({ role: 'user', content: query });
346
- const result = await callOpenRouterSearch(messages, wsConfig.deep_research_model, wsConfig.deep_research_timeout_ms, max_tokens || wsConfig.deep_research_max_tokens, wsConfig.temperature);
347
- const cost = estimateCost(result.usage, wsConfig.deep_research_model);
348
- await recordSearchToDb('succ_deep_research', wsConfig.deep_research_model, query, result.usage, cost, result.citations, !!result.reasoning, result.content.length);
349
- const todaySpent = wsConfig.daily_budget_usd > 0 ? await getTodayWebSearchSpend() : 0;
350
- let text = '';
351
- if (include_reasoning && result.reasoning) {
352
- text += `**Reasoning Process:**\n${result.reasoning}\n\n---\n\n`;
202
+ switch (action) {
203
+ case 'quick': {
204
+ if (!isApiConfigured()) {
205
+ return {
206
+ content: [
207
+ {
208
+ type: 'text',
209
+ text: 'API key not configured. Set OPENROUTER_API_KEY environment variable or run:\nsucc_config(action="set", key="llm.api_key", value="sk-or-...")',
210
+ },
211
+ ],
212
+ isError: true,
213
+ };
214
+ }
215
+ const wsConfig = getWebSearchConfig();
216
+ if (!wsConfig.enabled) {
217
+ return {
218
+ content: [
219
+ {
220
+ type: 'text',
221
+ text: 'Web search is disabled. Enable with: succ_config(action="set", key="web_search.enabled", value="true")',
222
+ },
223
+ ],
224
+ isError: true,
225
+ };
226
+ }
227
+ const budgetError = await checkBudget(wsConfig.daily_budget_usd);
228
+ if (budgetError)
229
+ return { content: [{ type: 'text', text: budgetError }], isError: true };
230
+ try {
231
+ const messages = [];
232
+ if (system_prompt) {
233
+ messages.push({ role: 'system', content: system_prompt });
234
+ }
235
+ messages.push({ role: 'user', content: query });
236
+ const effectiveModel = wsConfig.quick_search_model;
237
+ const result = await callOpenRouterSearch(messages, effectiveModel, wsConfig.quick_search_timeout_ms, max_tokens || wsConfig.quick_search_max_tokens, wsConfig.temperature);
238
+ const cost = estimateCost(result.usage, effectiveModel);
239
+ await recordSearchToDb('succ_quick_search', effectiveModel, query, result.usage, cost, result.citations, false, result.content.length);
240
+ const todaySpent = wsConfig.daily_budget_usd > 0 ? await getTodayWebSearchSpend() : 0;
241
+ let text = result.content;
242
+ text += formatCitations(result.citations, result.search_results);
243
+ text += formatUsage(result.usage, cost, wsConfig.daily_budget_usd, todaySpent);
244
+ const shouldSave = save_to_memory ?? wsConfig.save_to_memory;
245
+ if (shouldSave) {
246
+ text += await saveResultToMemory(query, result.content, result.citations, 'succ_quick_search', 'observation');
247
+ }
248
+ return { content: [{ type: 'text', text }] };
249
+ }
250
+ catch (error) {
251
+ return {
252
+ content: [{ type: 'text', text: `Quick search failed: ${error.message}` }],
253
+ isError: true,
254
+ };
255
+ }
353
256
  }
354
- text += result.content;
355
- text += formatCitations(result.citations, result.search_results);
356
- text += formatUsage(result.usage, cost, wsConfig.daily_budget_usd, todaySpent);
357
- const shouldSave = save_to_memory ?? wsConfig.save_to_memory;
358
- if (shouldSave) {
359
- text += await saveResultToMemory(query, result.content, result.citations, 'succ_deep_research', 'learning');
257
+ case 'search': {
258
+ if (!isApiConfigured()) {
259
+ return {
260
+ content: [
261
+ {
262
+ type: 'text',
263
+ text: 'API key not configured. Set OPENROUTER_API_KEY environment variable or run:\nsucc_config(action="set", key="llm.api_key", value="sk-or-...")',
264
+ },
265
+ ],
266
+ isError: true,
267
+ };
268
+ }
269
+ const wsConfig = getWebSearchConfig();
270
+ if (!wsConfig.enabled) {
271
+ return {
272
+ content: [
273
+ {
274
+ type: 'text',
275
+ text: 'Web search is disabled. Enable with: succ_config(action="set", key="web_search.enabled", value="true")',
276
+ },
277
+ ],
278
+ isError: true,
279
+ };
280
+ }
281
+ const budgetError = await checkBudget(wsConfig.daily_budget_usd);
282
+ if (budgetError)
283
+ return { content: [{ type: 'text', text: budgetError }], isError: true };
284
+ const effectiveModel = model || wsConfig.model;
285
+ try {
286
+ const messages = [];
287
+ if (system_prompt) {
288
+ messages.push({ role: 'system', content: system_prompt });
289
+ }
290
+ messages.push({ role: 'user', content: query });
291
+ const result = await callOpenRouterSearch(messages, effectiveModel, wsConfig.timeout_ms, max_tokens || wsConfig.max_tokens, wsConfig.temperature);
292
+ const cost = estimateCost(result.usage, effectiveModel);
293
+ await recordSearchToDb('succ_web_search', effectiveModel, query, result.usage, cost, result.citations, false, result.content.length);
294
+ const todaySpent = wsConfig.daily_budget_usd > 0 ? await getTodayWebSearchSpend() : 0;
295
+ let text = result.content;
296
+ text += formatCitations(result.citations, result.search_results);
297
+ text += formatUsage(result.usage, cost, wsConfig.daily_budget_usd, todaySpent);
298
+ const shouldSave = save_to_memory ?? wsConfig.save_to_memory;
299
+ if (shouldSave) {
300
+ text += await saveResultToMemory(query, result.content, result.citations, 'succ_web_search', 'observation');
301
+ }
302
+ return { content: [{ type: 'text', text }] };
303
+ }
304
+ catch (error) {
305
+ return {
306
+ content: [{ type: 'text', text: `Web search failed: ${error.message}` }],
307
+ isError: true,
308
+ };
309
+ }
360
310
  }
361
- return { content: [{ type: 'text', text }] };
362
- }
363
- catch (error) {
364
- return {
365
- content: [{ type: 'text', text: `Deep research failed: ${error.message}` }],
366
- isError: true,
367
- };
368
- }
369
- });
370
- // succ_web_search_history — view past searches
371
- server.tool('succ_web_search_history', 'View web search history with filtering. Shows past searches, costs, and usage statistics. Useful for tracking spend, reviewing past queries, and auditing search usage.', {
372
- tool_name: z
373
- .enum(['succ_quick_search', 'succ_web_search', 'succ_deep_research'])
374
- .optional()
375
- .describe('Filter by tool'),
376
- model: z.string().optional().describe('Filter by model (e.g., "perplexity/sonar-pro")'),
377
- query_text: z.string().optional().describe('Filter by query substring'),
378
- date_from: z.string().optional().describe('Start date (ISO, e.g., "2025-01-01")'),
379
- date_to: z.string().optional().describe('End date (ISO, e.g., "2025-12-31")'),
380
- limit: z.number().optional().describe('Max records to return (default: 20)'),
381
- project_path: projectPathParam,
382
- }, async ({ tool_name, model, query_text, date_from, date_to, limit, project_path }) => {
383
- await applyProjectPath(project_path);
384
- try {
385
- const [records, summary] = await Promise.all([
386
- getWebSearchHistory({ tool_name, model, query_text, date_from, date_to, limit }),
387
- getWebSearchSummary(),
388
- ]);
389
- const lines = [];
390
- // Summary section
391
- lines.push('## Web Search Summary');
392
- lines.push(`Total: ${summary.total_searches} searches, $${summary.total_cost_usd.toFixed(4)}`);
393
- lines.push(`Today: ${summary.today_searches} searches, $${summary.today_cost_usd.toFixed(4)}`);
394
- if (Object.keys(summary.by_tool).length > 0) {
395
- lines.push('');
396
- lines.push('**By tool:**');
397
- for (const [tool, stats] of Object.entries(summary.by_tool)) {
398
- lines.push(` ${tool}: ${stats.count} queries, $${stats.cost.toFixed(4)}`);
311
+ case 'deep': {
312
+ if (!isApiConfigured()) {
313
+ return {
314
+ content: [
315
+ {
316
+ type: 'text',
317
+ text: 'API key not configured. Set OPENROUTER_API_KEY environment variable or run:\nsucc_config(action="set", key="llm.api_key", value="sk-or-...")',
318
+ },
319
+ ],
320
+ isError: true,
321
+ };
322
+ }
323
+ const wsConfig = getWebSearchConfig();
324
+ if (!wsConfig.enabled) {
325
+ return {
326
+ content: [
327
+ {
328
+ type: 'text',
329
+ text: 'Web search is disabled. Enable with: succ_config(action="set", key="web_search.enabled", value="true")',
330
+ },
331
+ ],
332
+ isError: true,
333
+ };
334
+ }
335
+ const budgetError = await checkBudget(wsConfig.daily_budget_usd);
336
+ if (budgetError)
337
+ return { content: [{ type: 'text', text: budgetError }], isError: true };
338
+ try {
339
+ const messages = [];
340
+ if (system_prompt) {
341
+ messages.push({ role: 'system', content: system_prompt });
342
+ }
343
+ messages.push({ role: 'user', content: query });
344
+ const result = await callOpenRouterSearch(messages, wsConfig.deep_research_model, wsConfig.deep_research_timeout_ms, max_tokens || wsConfig.deep_research_max_tokens, wsConfig.temperature);
345
+ const cost = estimateCost(result.usage, wsConfig.deep_research_model);
346
+ await recordSearchToDb('succ_deep_research', wsConfig.deep_research_model, query, result.usage, cost, result.citations, !!result.reasoning, result.content.length);
347
+ const todaySpent = wsConfig.daily_budget_usd > 0 ? await getTodayWebSearchSpend() : 0;
348
+ let text = '';
349
+ if (include_reasoning && result.reasoning) {
350
+ text += `**Reasoning Process:**\n${result.reasoning}\n\n---\n\n`;
351
+ }
352
+ text += result.content;
353
+ text += formatCitations(result.citations, result.search_results);
354
+ text += formatUsage(result.usage, cost, wsConfig.daily_budget_usd, todaySpent);
355
+ const shouldSave = save_to_memory ?? wsConfig.save_to_memory;
356
+ if (shouldSave) {
357
+ text += await saveResultToMemory(query, result.content, result.citations, 'succ_deep_research', 'learning');
358
+ }
359
+ return { content: [{ type: 'text', text }] };
360
+ }
361
+ catch (error) {
362
+ return {
363
+ content: [{ type: 'text', text: `Deep research failed: ${error.message}` }],
364
+ isError: true,
365
+ };
399
366
  }
400
367
  }
401
- // Records section
402
- if (records.length > 0) {
403
- lines.push('');
404
- lines.push(`## Recent Searches (${records.length})`);
405
- for (const r of records) {
406
- const date = r.created_at.slice(0, 16).replace('T', ' ');
407
- const tokens = r.prompt_tokens + r.completion_tokens;
408
- lines.push(`- **[${date}]** \`${r.tool_name}\` — "${r.query.slice(0, 80)}${r.query.length > 80 ? '...' : ''}"`);
409
- lines.push(` Model: ${r.model} | Tokens: ${tokens.toLocaleString()} | Cost: $${r.estimated_cost_usd.toFixed(4)}${r.citations_count > 0 ? ` | Citations: ${r.citations_count}` : ''}`);
368
+ case 'history': {
369
+ try {
370
+ const [records, summary] = await Promise.all([
371
+ getWebSearchHistory({ tool_name, model, query_text, date_from, date_to, limit }),
372
+ getWebSearchSummary(),
373
+ ]);
374
+ const lines = [];
375
+ // Summary section
376
+ lines.push('## Web Search Summary');
377
+ lines.push(`Total: ${summary.total_searches} searches, $${summary.total_cost_usd.toFixed(4)}`);
378
+ lines.push(`Today: ${summary.today_searches} searches, $${summary.today_cost_usd.toFixed(4)}`);
379
+ if (Object.keys(summary.by_tool).length > 0) {
380
+ lines.push('');
381
+ lines.push('**By tool:**');
382
+ for (const [tool, stats] of Object.entries(summary.by_tool)) {
383
+ lines.push(` ${tool}: ${stats.count} queries, $${stats.cost.toFixed(4)}`);
384
+ }
385
+ }
386
+ // Records section
387
+ if (records.length > 0) {
388
+ lines.push('');
389
+ lines.push(`## Recent Searches (${records.length})`);
390
+ for (const r of records) {
391
+ const date = r.created_at.slice(0, 16).replace('T', ' ');
392
+ const tokens = r.prompt_tokens + r.completion_tokens;
393
+ lines.push(`- **[${date}]** \`${r.tool_name}\` — "${r.query.slice(0, 80)}${r.query.length > 80 ? '...' : ''}"`);
394
+ lines.push(` Model: ${r.model} | Tokens: ${tokens.toLocaleString()} | Cost: $${r.estimated_cost_usd.toFixed(4)}${r.citations_count > 0 ? ` | Citations: ${r.citations_count}` : ''}`);
395
+ }
396
+ }
397
+ else {
398
+ lines.push('\n_No search records found matching filters._');
399
+ }
400
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
401
+ }
402
+ catch (error) {
403
+ return {
404
+ content: [
405
+ {
406
+ type: 'text',
407
+ text: `Failed to retrieve search history: ${error.message}`,
408
+ },
409
+ ],
410
+ isError: true,
411
+ };
410
412
  }
411
413
  }
412
- else {
413
- lines.push('\n_No search records found matching filters._');
414
+ default: {
415
+ return {
416
+ content: [
417
+ {
418
+ type: 'text',
419
+ text: `Unknown action "${action}". Valid actions: quick, search, deep, history`,
420
+ },
421
+ ],
422
+ isError: true,
423
+ };
414
424
  }
415
- return { content: [{ type: 'text', text: lines.join('\n') }] };
416
- }
417
- catch (error) {
418
- return {
419
- content: [
420
- { type: 'text', text: `Failed to retrieve search history: ${error.message}` },
421
- ],
422
- isError: true,
423
- };
424
425
  }
425
426
  });
426
427
  }