@finchagentic/mcp 4.7.2 → 4.7.4

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 (50) hide show
  1. package/README.md +74 -39
  2. package/dist/_zod-helpers.js +10 -0
  3. package/dist/cli-doctor.js +300 -0
  4. package/dist/cli-env.js +91 -0
  5. package/dist/cli-install.js +236 -0
  6. package/dist/cli-login.js +101 -0
  7. package/dist/cli-orders.js +175 -0
  8. package/dist/cli-setup.js +270 -0
  9. package/dist/cli-ui.js +168 -0
  10. package/dist/cli-vault.js +75 -0
  11. package/dist/cli.js +61 -1152
  12. package/dist/convex.js +16 -5
  13. package/dist/enrichment-router.js +5 -18
  14. package/dist/finch-output.js +30 -29
  15. package/dist/finch-status.js +126 -44
  16. package/dist/local-vault.js +5 -12
  17. package/dist/output-schemas.js +3 -12
  18. package/dist/project.js +4 -13
  19. package/dist/server.js +13 -35
  20. package/dist/tool-filter.js +4 -13
  21. package/dist/tools/_solidity-scan.js +5 -14
  22. package/dist/tools/agents.js +14 -26
  23. package/dist/tools/deep-research-constants.js +33 -0
  24. package/dist/tools/deep-research-firecrawl.js +88 -0
  25. package/dist/tools/deep-research-planning.js +249 -0
  26. package/dist/tools/deep-research-synthesis.js +343 -0
  27. package/dist/tools/deep-research-text.js +158 -0
  28. package/dist/tools/deep-research-tools.js +90 -0
  29. package/dist/tools/deep-research.js +47 -938
  30. package/dist/tools/defi.js +4 -3
  31. package/dist/tools/insider.js +4 -14
  32. package/dist/tools/insight.js +7 -6
  33. package/dist/tools/market.js +16 -15
  34. package/dist/tools/memory.js +43 -61
  35. package/dist/tools/monitor.js +7 -6
  36. package/dist/tools/os.js +7 -85
  37. package/dist/tools/research.js +7 -6
  38. package/dist/tools/rh-mcp-constants.js +65 -0
  39. package/dist/tools/rh-mcp-dex.js +107 -0
  40. package/dist/tools/rh-mcp-provider.js +127 -0
  41. package/dist/tools/rh-mcp-resolve.js +137 -0
  42. package/dist/tools/rh-mcp-risk.js +230 -0
  43. package/dist/tools/rh-mcp-safety.js +234 -0
  44. package/dist/tools/rh-mcp-swap.js +181 -0
  45. package/dist/tools/rh-mcp-tools.js +137 -0
  46. package/dist/tools/rh-mcp.js +75 -1191
  47. package/dist/tools/scanner.js +10 -9
  48. package/dist/tools/stake.js +7 -20
  49. package/dist/tools/vault.js +52 -59
  50. package/package.json +13 -10
@@ -9,6 +9,7 @@ const llm_js_1 = require("../llm.js");
9
9
  const local_vault_js_1 = require("../local-vault.js");
10
10
  const memory_js_1 = require("./memory.js");
11
11
  const project_js_1 = require("../project.js");
12
+ const _zod_helpers_js_1 = require("../_zod-helpers.js");
12
13
  // ─── Agent Learning Memory (v3.25) ──────────────────────────────────────────
13
14
  // After every agent_update, an LLM reviews the new progress in context of the
14
15
  // agent's goal + prior learnings to extract a single repeatable insight. The
@@ -104,23 +105,10 @@ function withAgentLock(agentName, fn) {
104
105
  });
105
106
  return next;
106
107
  }
107
- // list_agents/hire_agent were removed (not disabled - deleted) because their
108
- // backend routes, /agents/list and /agents/hire, were never actually
109
- // registered in app/convex/http.ts - only dangling section-header comments
110
- // exist there, no matching http.route(...) call. Every call to either tool
111
- // always 404'd. Re-add them (and the matching handler blocks below) only
112
- // alongside building those two routes for real.
113
- //
114
- // Same story, same fix, applied again here: agent_identity, agent_schedule,
115
- // agent_unschedule, agent_pause, agent_resume and agent_runs called
116
- // /agents/identity, /agents/schedule, /agents/unschedule, /agents/pause and
117
- // /agents/runs - none of which exist in http.ts, and none of which have any
118
- // backing data model in schema.ts (no agent-identity table, no scheduler,
119
- // no run-history table; there isn't even a cron for it in crons.ts). This
120
- // isn't a missing route, it's an unbuilt subsystem, so the tools were
121
- // removed rather than stubbed. Re-add only alongside building that
122
- // autonomous-scheduling backend for real: identity/schedule storage, a cron
123
- // that actually wakes agents up, and run-history writes.
108
+ // list_agents/hire_agent/agent_identity/agent_schedule/agent_unschedule/
109
+ // agent_pause/agent_resume/agent_runs deleted - their backend routes never
110
+ // existed (dangling comments in http.ts, no actual route/data model). Not a
111
+ // missing route, an unbuilt subsystem. Re-add only alongside building it.
124
112
  exports.AGENT_TOOLS = [
125
113
  {
126
114
  name: "agent_spawn",
@@ -226,9 +214,9 @@ function buildAgentLedger(name, versions) {
226
214
  }
227
215
  async function handleAgentTool(name, args) {
228
216
  if (name === "agent_spawn") {
229
- const parsed = SpawnAgentSchema.safeParse(args);
230
- if (!parsed.success)
231
- return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
217
+ const parsed = (0, _zod_helpers_js_1.parseOrError)(SpawnAgentSchema, args);
218
+ if (!parsed.ok)
219
+ return parsed.error;
232
220
  const { name: agentName, goal, context, workspaceProject } = parsed.data;
233
221
  const content = JSON.stringify({
234
222
  goal,
@@ -269,9 +257,9 @@ async function handleAgentTool(name, args) {
269
257
  };
270
258
  }
271
259
  if (name === "agent_recall") {
272
- const parsed = RecallAgentSchema.safeParse(args);
273
- if (!parsed.success)
274
- return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
260
+ const parsed = (0, _zod_helpers_js_1.parseOrError)(RecallAgentSchema, args);
261
+ if (!parsed.ok)
262
+ return parsed.error;
275
263
  const localVault = (0, local_vault_js_1.getLocalVaultConfig)();
276
264
  let data;
277
265
  try {
@@ -334,9 +322,9 @@ async function handleAgentTool(name, args) {
334
322
  return { content: [{ type: "text", text: lines.join("\n") }] };
335
323
  }
336
324
  if (name === "agent_update") {
337
- const parsed = UpdateAgentSchema.safeParse(args);
338
- if (!parsed.success)
339
- return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
325
+ const parsed = (0, _zod_helpers_js_1.parseOrError)(UpdateAgentSchema, args);
326
+ if (!parsed.ok)
327
+ return parsed.error;
340
328
  const { name: agentName, progress, findings, status = "active", nextStep } = parsed.data;
341
329
  return withAgentLock(agentName, async () => {
342
330
  const localVault = (0, local_vault_js_1.getLocalVaultConfig)();
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ // Shared constants for the deep_research pipeline (deep-research-*.ts).
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.STOPWORDS = exports.FRESH_TRIGGER_RE = exports.NEWS_DOMAIN_BOOST_RE = exports.DOMAIN_TIER_BONUS = exports.EXCERPT_TOP_CHUNKS = exports.EXCERPT_CHARS_PER_CHUNK = exports.MAX_PER_DOMAIN = exports.FC_BASE = void 0;
5
+ exports.FC_BASE = "https://api.firecrawl.dev/v1";
6
+ exports.MAX_PER_DOMAIN = 2; // source diversity - cap hits per domain
7
+ exports.EXCERPT_CHARS_PER_CHUNK = 600;
8
+ exports.EXCERPT_TOP_CHUNKS = 4; // pick top N relevant chunks per source
9
+ // Quality scoring - higher = more trustworthy primary source
10
+ exports.DOMAIN_TIER_BONUS = [
11
+ [/\.gov(\b|\/|$)/i, 4],
12
+ [/\.edu(\b|\/|$)/i, 3],
13
+ [/(?:nature|science|nih|arxiv|acm|ieee|sciencedirect)\.(?:org|com)/i, 3],
14
+ // Crypto primary-source boost - these are the authoritative data sources
15
+ // for protocol TVL, yields, prices, and on-chain analytics. Rank above
16
+ // generic news for crypto queries.
17
+ [/(?:defillama|tokenterminal|coingecko|coinmarketcap|dune|messari|artemis)\.(?:com|fi)/i, 3],
18
+ [/(?:etherscan|basescan|arbiscan|solscan|polygonscan|optimistic\.etherscan)\.(?:io|com)/i, 2],
19
+ [/(?:reuters|apnews|bbc|economist|ft|wsj|bloomberg)\.com/i, 2],
20
+ [/(?:coindesk|theblock|cointelegraph|decrypt|theinformation)\.(?:co|com|io)/i, 1],
21
+ [/(?:wikipedia|github|stackoverflow)\.(?:org|com)/i, 1],
22
+ [/(?:medium|substack|reddit|twitter|x)\.com/i, -1],
23
+ ];
24
+ // News-domain bonus - applied additionally in fresh mode
25
+ exports.NEWS_DOMAIN_BOOST_RE = /(?:reuters|apnews|bbc|economist|ft|wsj|bloomberg|cnbc|theverge|techcrunch|axios|coindesk|theinformation|nytimes|guardian|aljazeera)\.com/i;
26
+ // Keywords that signal a time-sensitive query - trigger fresh mode auto.
27
+ exports.FRESH_TRIGGER_RE = /\b(today|tonight|tomorrow|yesterday|this week|last week|past week|latest|breaking|just now|recent|currently|now|live|happening|this month|last month|past month|past \d+ days?|last \d+ days?|q[1-4]|h[12]|202[6-9])\b/i;
28
+ exports.STOPWORDS = new Set([
29
+ "the", "and", "for", "with", "that", "this", "what", "when", "where", "how", "why",
30
+ "have", "has", "had", "are", "was", "were", "will", "would", "could", "should", "does",
31
+ "did", "being", "been", "from", "into", "over", "under", "about", "into", "than", "then",
32
+ "your", "yours", "their", "they", "them", "there", "here", "just", "also", "more", "most",
33
+ ]);
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ // Firecrawl search/scrape - BYOK direct call with a Finch backend-proxy
3
+ // fallback for signed-in users without their own key.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.normalizeSearchHits = normalizeSearchHits;
6
+ exports.fcSearch = fcSearch;
7
+ exports.fcScrape = fcScrape;
8
+ const convex_js_1 = require("../convex.js");
9
+ const deep_research_constants_js_1 = require("./deep-research-constants.js");
10
+ /**
11
+ * Firecrawl returns either a flat array or a keyed object of result groups
12
+ * (`{ web: [...], news: [...] }`). The proxy path assumed the flat shape and
13
+ * handed an object to `.forEach`, which threw — invisible until now because
14
+ * without an API key the run always died at the planner first.
15
+ */
16
+ function normalizeSearchHits(raw) {
17
+ if (Array.isArray(raw))
18
+ return raw;
19
+ if (raw && typeof raw === "object") {
20
+ const out = [];
21
+ for (const group of Object.values(raw)) {
22
+ if (Array.isArray(group))
23
+ out.push(...group);
24
+ }
25
+ return out;
26
+ }
27
+ return [];
28
+ }
29
+ async function fcSearch(query, limit) {
30
+ // BYOK path - direct call to Firecrawl with user's key. Fastest, no proxy hop.
31
+ const key = process.env.FIRECRAWL_API_KEY;
32
+ if (key) {
33
+ try {
34
+ const res = await fetch(`${deep_research_constants_js_1.FC_BASE}/search`, {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
37
+ body: JSON.stringify({ query, limit }),
38
+ signal: AbortSignal.timeout(15000),
39
+ });
40
+ if (res.ok) {
41
+ const data = (await res.json());
42
+ return normalizeSearchHits(data.data);
43
+ }
44
+ }
45
+ catch { /* fall through */ }
46
+ }
47
+ // Backend-proxy path - session-authed; Finch covers Firecrawl cost.
48
+ try {
49
+ const data = await (0, convex_js_1.callConvex)("/research/firecrawl-search", "POST", { query, limit }, "web_search", 20000);
50
+ return normalizeSearchHits(data?.results);
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ }
56
+ async function fcScrape(url) {
57
+ const key = process.env.FIRECRAWL_API_KEY;
58
+ if (key) {
59
+ try {
60
+ const res = await fetch(`${deep_research_constants_js_1.FC_BASE}/scrape`, {
61
+ method: "POST",
62
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${key}` },
63
+ body: JSON.stringify({ url, formats: ["markdown"], onlyMainContent: true }),
64
+ signal: AbortSignal.timeout(20000),
65
+ });
66
+ if (res.ok) {
67
+ const data = (await res.json());
68
+ const md = data.data?.markdown;
69
+ if (md) {
70
+ const meta = data.data?.metadata;
71
+ const publishedAt = meta?.publishedAt ?? meta?.ogPublishedTime ?? meta?.["article:published_time"];
72
+ return { markdown: md, publishedAt };
73
+ }
74
+ }
75
+ }
76
+ catch { /* fall through */ }
77
+ }
78
+ // Backend-proxy path. Returns markdown only - no metadata extraction yet,
79
+ // which means continueFrom can't auto-date proxied scrapes. Acceptable
80
+ // tradeoff for now; markdown is the primary signal.
81
+ try {
82
+ const data = await (0, convex_js_1.callConvex)("/research/firecrawl-scrape", "POST", { url }, "web_scrape", 25000);
83
+ if (data?.markdown)
84
+ return { markdown: data.markdown };
85
+ }
86
+ catch { /* swallow */ }
87
+ return null;
88
+ }
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ // Query planning: sub-query derivation, LLM planners (flat + multi-angle),
3
+ // the deep-mode adversarial critic, and gap-finding reflection.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.deriveSubQueries = deriveSubQueries;
6
+ exports.planQueries = planQueries;
7
+ exports.planAngles = planAngles;
8
+ exports.runCritic = runCritic;
9
+ exports.reflectAndExtend = reflectAndExtend;
10
+ const llm_js_1 = require("../llm.js");
11
+ const deep_research_text_js_1 = require("./deep-research-text.js");
12
+ /**
13
+ * Sub-queries without an LLM.
14
+ *
15
+ * The caller should normally pass `queries` — it is a model and plans better
16
+ * than any heuristic. This exists so the evidence-pack path still works with
17
+ * zero configuration, rather than failing when no API key is set.
18
+ */
19
+ function deriveSubQueries(query, n, focus, freshMode) {
20
+ const base = query.trim().replace(/\?+$/, "");
21
+ const recency = freshMode ? ` ${new Date().getFullYear()} latest` : "";
22
+ const lenses = focus
23
+ ? [focus, `${focus} risks`, `${focus} data`]
24
+ : ["overview", "latest developments", "risks and criticism", "data and numbers", "expert analysis"];
25
+ const out = [`${base}${recency}`];
26
+ for (const lens of lenses) {
27
+ if (out.length >= Math.max(2, n))
28
+ break;
29
+ out.push(`${base} ${lens}${recency}`);
30
+ }
31
+ return out;
32
+ }
33
+ async function planQueries(query, n, focus, priorContext, freshMode) {
34
+ const focusNote = focus ? ` Focus angle: ${focus}.` : "";
35
+ const sys = "You are a research planner. Output strict JSON only - no preamble, no markdown.";
36
+ const freshNote = freshMode
37
+ ? `
38
+
39
+ ⏱ FRESH MODE - last ${freshMode.days} days:
40
+ - Add a recency hint to most sub-questions: "in ${(0, deep_research_text_js_1.currentYearMonth)()}", "last ${freshMode.days} days", "this week", "as of ${(0, deep_research_text_js_1.todayISO)()}", etc.
41
+ - Prefer queries that surface news / press releases / X posts over evergreen background.
42
+ - Skip generic background - the user wants what's CURRENT, not historical.
43
+ - At least 70% of sub-questions must include a date or recency token.`
44
+ : "";
45
+ const continuationNote = priorContext
46
+ ? `
47
+
48
+ ⚠️ CONTINUATION MODE - there is a PRIOR research report on this topic:
49
+
50
+ """
51
+ ${priorContext.slice(0, 2500)}
52
+ """
53
+
54
+ Your sub-questions must focus on:
55
+ 1. UPDATES - what has changed since the prior report (new releases, news, data revisions)
56
+ 2. GAPS - angles the prior report explicitly listed as open questions or follow-ups
57
+ 3. NEW developments - entities/events the prior report doesn't mention
58
+ 4. VERIFICATION - claims the prior report flagged as low-confidence or single-source
59
+
60
+ DO NOT re-tread material already well-covered in the prior report. The user already has those answers.`
61
+ : "";
62
+ const user = `Decompose this research question into ${n} sub-questions that together cover the topic from different angles.${focusNote}${continuationNote}${freshNote}
63
+
64
+ Rules:
65
+ - Each sub-question must be a standalone web search query, under 90 chars.
66
+ - Cover different facets: definition, current state, key actors, comparisons, counterarguments, recent news, forward outlook.
67
+ - No duplicates, no near-paraphrases.
68
+
69
+ ENTITY-HUNTING - at least HALF of your sub-questions must target queries likely to surface:
70
+ - Specific company / product / framework names (e.g., "LangGraph adoption stats", "Manus orchestration funding")
71
+ - Dollar amounts (acquisitions, funding rounds, revenue, ARR, market size)
72
+ - Benchmark numbers (% adoption, latency ms, accuracy scores, MMLU/HumanEval/SWE-bench results)
73
+ - Specific dates and timeline events (when X launched, when Y reached scale)
74
+ - Named studies / surveys / reports (e.g., "Anthropic Economic Index 2026", "a16z AI infrastructure report")
75
+
76
+ Bad: "what is X" → too generic, returns Wikipedia
77
+ Good: "X adoption rate enterprise 2026 survey" → returns concrete stats
78
+
79
+ Question: "${query}"
80
+
81
+ Return: {"queries": ["...", "..."]} - exactly ${n} items.`;
82
+ let raw = "";
83
+ try {
84
+ raw = await (0, llm_js_1.callLLM)(sys, user, 500, [], 30000);
85
+ }
86
+ catch {
87
+ return [query];
88
+ }
89
+ const parsed = (0, deep_research_text_js_1.safeParseJson)(raw, {});
90
+ if (!parsed.queries || !Array.isArray(parsed.queries))
91
+ return [query];
92
+ const queries = parsed.queries
93
+ .filter((s) => typeof s === "string")
94
+ .map((s) => s.trim())
95
+ .filter((s) => s.length > 0 && s.length <= 200)
96
+ .slice(0, n);
97
+ return queries.length > 0 ? queries : [query];
98
+ }
99
+ async function planAngles(query, n, focus, priorContext, freshMode) {
100
+ const focusNote = focus ? ` Focus angle: ${focus}.` : "";
101
+ const freshNote = freshMode
102
+ ? `\n\n⏱ FRESH MODE - last ${freshMode.days} days: bias each angle's queries toward news/recent press/dated reports.`
103
+ : "";
104
+ const continuationNote = priorContext
105
+ ? `\n\n⚠️ CONTINUATION - prior report exists. Angles must focus on UPDATES, GAPS, NEW developments since:\n"""\n${priorContext.slice(0, 1500)}\n"""`
106
+ : "";
107
+ const sys = "You are a research planner that decomposes topics into specialist angles. Output strict JSON only.";
108
+ const user = `Break this research topic into ${n} DIFFERENT specialist angles. Each angle gets its own 2-3 search queries.${focusNote}${continuationNote}${freshNote}
109
+
110
+ Topic: "${query}"
111
+
112
+ Pick angles that are GENUINELY different - not paraphrases of the same question. Good angle diversity examples:
113
+ - data / quantitative metrics
114
+ - competitive landscape
115
+ - team / governance / actors
116
+ - recent news / catalysts
117
+ - counterarguments / risks / criticism
118
+ - forward outlook / projections
119
+ - historical context / origins
120
+ - regulatory / policy angle
121
+ - technical / mechanism
122
+ - ecosystem partners
123
+
124
+ Pick the ${n} angles that most fit THIS specific topic. Each angle should have a label (3-6 words) and rationale (1 sentence). Queries must be standalone, ≤90 chars, entity-rich (specific names, dollar amounts, dates, benchmark numbers - NOT generic background).
125
+
126
+ Return strict JSON:
127
+ {
128
+ "angles": [
129
+ {
130
+ "label": "...",
131
+ "rationale": "...",
132
+ "queries": ["...", "..."]
133
+ }
134
+ ]
135
+ }
136
+
137
+ Exactly ${n} angles. 2-3 queries per angle. No duplicate queries across angles.`;
138
+ let raw = "";
139
+ try {
140
+ raw = await (0, llm_js_1.callLLM)(sys, user, 1200, [], 30000);
141
+ }
142
+ catch {
143
+ return [];
144
+ }
145
+ const parsed = (0, deep_research_text_js_1.safeParseJson)(raw, {});
146
+ if (!parsed.angles || !Array.isArray(parsed.angles))
147
+ return [];
148
+ const angles = [];
149
+ for (const a of parsed.angles) {
150
+ if (typeof a !== "object" || a === null)
151
+ continue;
152
+ const o = a;
153
+ if (typeof o.label !== "string" || typeof o.rationale !== "string")
154
+ continue;
155
+ if (!Array.isArray(o.queries))
156
+ continue;
157
+ const queries = o.queries
158
+ .filter((q) => typeof q === "string")
159
+ .map((q) => q.trim())
160
+ .filter((q) => q.length > 0 && q.length <= 200)
161
+ .slice(0, 3);
162
+ if (queries.length === 0)
163
+ continue;
164
+ angles.push({
165
+ label: o.label.trim().slice(0, 80),
166
+ rationale: o.rationale.trim().slice(0, 200),
167
+ queries,
168
+ });
169
+ if (angles.length >= n)
170
+ break;
171
+ }
172
+ return angles;
173
+ }
174
+ // Adversarial critic - only runs for depth=deep. Reads the draft + sources
175
+ // already gathered and produces a structured challenge block: single-source
176
+ // claims, contradictions across angles, speculation framed as fact. The
177
+ // final synthesizer is told to incorporate or refute these challenges
178
+ // explicitly, lifting the quality floor.
179
+ async function runCritic(query, draft, angles, sourceCount) {
180
+ const sys = "You are an adversarial research critic. Be terse, specific, and unsparing. Output markdown.";
181
+ const angleLabels = angles.map((a, i) => `${i + 1}. ${a.label}`).join("\n");
182
+ const user = `Original question: "${query}"
183
+
184
+ Specialist angles investigated:
185
+ ${angleLabels}
186
+
187
+ Total sources gathered: ${sourceCount}
188
+
189
+ Draft report:
190
+ """
191
+ ${draft.slice(0, 5000)}
192
+ """
193
+
194
+ Audit the draft for quality problems. Be specific and quote spans where possible.
195
+
196
+ Identify:
197
+ 1. **Single-source claims** - major assertions resting on one [N] citation that aren't widely corroborated
198
+ 2. **Contradictions** - places where different sources disagree but the draft doesn't surface the disagreement
199
+ 3. **Speculation framed as fact** - confident statements about future/intent/cause without evidence
200
+ 4. **Coverage gaps** - angles from the list above that got shallow treatment
201
+ 5. **Stale or weak sources** - dated material treated as current, blog posts cited as primary data
202
+
203
+ Output format:
204
+ ## Critic notes
205
+ - **[type]**: specific issue + relevant quote or claim. (1-2 sentences each)
206
+ - Skip categories with no findings - don't pad.
207
+
208
+ End with one line: "Net recommendation: [accept|revise|reject]"
209
+
210
+ If the draft is solid, say so plainly - don't manufacture issues.`;
211
+ try {
212
+ const notes = await (0, llm_js_1.callLLM)(sys, user, 1500, [], 60000);
213
+ return notes.trim();
214
+ }
215
+ catch {
216
+ return ""; // critic failure shouldn't block final synth
217
+ }
218
+ }
219
+ async function reflectAndExtend(query, draft, existingQueries, n) {
220
+ const sys = "You are a research auditor. Find gaps in a draft report and propose follow-up search queries. Output strict JSON.";
221
+ const user = `Original question: "${query}"
222
+
223
+ Queries already run:
224
+ ${existingQueries.map((q, i) => `${i + 1}. ${q}`).join("\n")}
225
+
226
+ Draft report:
227
+ """
228
+ ${draft.slice(0, 4000)}
229
+ """
230
+
231
+ Identify ${n} GAPS in the draft - angles missing, claims that need verification, counter-perspectives not represented, or recent developments not covered. For each gap, give ONE web search query (≤90 chars) that would fill it.
232
+
233
+ Return: {"gap_queries": ["...", "..."]} - exactly ${n} items, no duplicates of existing queries.`;
234
+ let raw = "";
235
+ try {
236
+ raw = await (0, llm_js_1.callLLM)(sys, user, 500, [], 30000);
237
+ }
238
+ catch {
239
+ return [];
240
+ }
241
+ const parsed = (0, deep_research_text_js_1.safeParseJson)(raw, {});
242
+ if (!parsed.gap_queries || !Array.isArray(parsed.gap_queries))
243
+ return [];
244
+ return parsed.gap_queries
245
+ .filter((s) => typeof s === "string")
246
+ .map((s) => s.trim())
247
+ .filter((s) => s.length > 0 && s.length <= 200)
248
+ .slice(0, n);
249
+ }