@finchagentic/mcp 4.7.1 → 4.7.3

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 (51) 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/config.js +3 -2
  13. package/dist/enrichment-router.js +5 -18
  14. package/dist/finch-output.js +30 -29
  15. package/dist/finch-status.js +105 -40
  16. package/dist/index.js +0 -0
  17. package/dist/local-vault.js +5 -12
  18. package/dist/output-schemas.js +3 -12
  19. package/dist/project.js +4 -13
  20. package/dist/server.js +13 -35
  21. package/dist/tool-filter.js +4 -13
  22. package/dist/tools/_solidity-scan.js +5 -14
  23. package/dist/tools/agents.js +14 -26
  24. package/dist/tools/deep-research-constants.js +33 -0
  25. package/dist/tools/deep-research-firecrawl.js +88 -0
  26. package/dist/tools/deep-research-planning.js +249 -0
  27. package/dist/tools/deep-research-synthesis.js +343 -0
  28. package/dist/tools/deep-research-text.js +158 -0
  29. package/dist/tools/deep-research-tools.js +90 -0
  30. package/dist/tools/deep-research.js +47 -938
  31. package/dist/tools/defi.js +4 -3
  32. package/dist/tools/insider.js +4 -14
  33. package/dist/tools/insight.js +7 -6
  34. package/dist/tools/market.js +16 -15
  35. package/dist/tools/memory.js +57 -62
  36. package/dist/tools/monitor.js +7 -6
  37. package/dist/tools/os.js +7 -85
  38. package/dist/tools/research.js +7 -6
  39. package/dist/tools/rh-mcp-constants.js +65 -0
  40. package/dist/tools/rh-mcp-dex.js +107 -0
  41. package/dist/tools/rh-mcp-provider.js +127 -0
  42. package/dist/tools/rh-mcp-resolve.js +137 -0
  43. package/dist/tools/rh-mcp-risk.js +230 -0
  44. package/dist/tools/rh-mcp-safety.js +234 -0
  45. package/dist/tools/rh-mcp-swap.js +181 -0
  46. package/dist/tools/rh-mcp-tools.js +137 -0
  47. package/dist/tools/rh-mcp.js +75 -1191
  48. package/dist/tools/scanner.js +10 -9
  49. package/dist/tools/stake.js +7 -20
  50. package/dist/tools/vault.js +52 -59
  51. package/package.json +12 -9
@@ -0,0 +1,343 @@
1
+ "use strict";
2
+ // Report synthesis: source classification, the main LLM synthesis call,
3
+ // citation-density retry, and the structural/citation quality checks that
4
+ // gate whether a report ships as-is or gets one retry.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.classifySource = classifySource;
7
+ exports.synthesize = synthesize;
8
+ exports.extractLiveCitations = extractLiveCitations;
9
+ exports.measureCitationDensity = measureCitationDensity;
10
+ exports.validateReportStructure = validateReportStructure;
11
+ exports.formatLiveCitations = formatLiveCitations;
12
+ const llm_js_1 = require("../llm.js");
13
+ const convex_js_1 = require("../convex.js");
14
+ const enrichment_router_js_1 = require("../enrichment-router.js");
15
+ const memory_js_1 = require("./memory.js");
16
+ const deep_research_text_js_1 = require("./deep-research-text.js");
17
+ function classifySource(score) {
18
+ if (score >= 3)
19
+ return "primary";
20
+ if (score === 2)
21
+ return "expert";
22
+ if (score === 1)
23
+ return "secondary";
24
+ if (score < 0)
25
+ return "market";
26
+ return "unclassified";
27
+ }
28
+ async function synthesize(query, sources, isFinal, liveSearch, priorContext, freshMode, angles, criticNotes) {
29
+ const sourceBlocks = sources
30
+ .map((s) => {
31
+ const dateNote = s.publishedAt ? ` (published ${s.publishedAt.slice(0, 10)})` : "";
32
+ const classNote = s.class !== "unclassified" ? ` [${s.class}]` : "";
33
+ return `[${s.n}]${classNote} ${s.title} - ${s.domain}${dateNote}\nURL: ${s.url}\n\n${s.excerpt}`;
34
+ })
35
+ .join("\n\n---\n\n");
36
+ // Multi-domain enrichment - auto-detect topic (crypto/tech/academic/general)
37
+ // and pull live primary-source data in parallel. Crypto routes to DefiLlama
38
+ // + CoinGecko; tech to HackerNews + GitHub; academic to arXiv; general to
39
+ // Wikipedia. The combined block prepends to source blocks and is tagged
40
+ // AUTHORITATIVE LIVE DATA so the LLM treats those numbers as ground truth.
41
+ const enrichment = isFinal ? await (0, enrichment_router_js_1.enrichQuery)(query) : { context: "", hasData: false, domains: [] };
42
+ // Profile + memory auto-injection - only on final synthesis.
43
+ // Profile: persistent identity/business/state from vault.
44
+ // Memory: top relevant memories for this query (personalizes report framing).
45
+ // Both run in parallel; either can fail without blocking synthesis.
46
+ let profileContext = "";
47
+ let memoryContext = "";
48
+ if (isFinal) {
49
+ const [profileData, memHits] = await Promise.allSettled([
50
+ (0, convex_js_1.callConvex)("/vault/profile-context?maxChars=1200", "GET", undefined, "vault_read"),
51
+ (0, memory_js_1.searchSupermemory)(query, 4),
52
+ ]);
53
+ if (profileData.status === "fulfilled") {
54
+ profileContext = (profileData.value?.context ?? "").trim();
55
+ }
56
+ if (memHits.status === "fulfilled" && memHits.value.length > 0) {
57
+ memoryContext = memHits.value
58
+ .map((r) => {
59
+ const title = r.metadata?.title ? `[${r.metadata.title}] ` : "";
60
+ return `- ${title}${r.content.slice(0, 200).replace(/\n/g, " ")}`;
61
+ })
62
+ .join("\n");
63
+ }
64
+ }
65
+ const liveSearchNote = liveSearch
66
+ ? `\n\nIMPORTANT - Real-time augmentation:
67
+ You have Live Search enabled. In addition to the numbered sources above, you have access to **real-time results from ${liveSearch.sources.join(", ")}**. Use them to:
68
+ 1. Verify recent claims (last ${liveSearch.fromDate ? "from " + liveSearch.fromDate : "few weeks"})
69
+ 2. Add fresh data points the static sources may have missed
70
+ 3. Surface X (Twitter) posts when the topic is moving quickly
71
+ 4. Pull current numbers when the static sources are dated
72
+
73
+ CITATION RULE for Live Search content:
74
+ - Numbered sources [N] = static scraped sources at top of prompt
75
+ - Real-time content from Live Search: cite inline as **(X post)** or **(news, [outlet])** WITHOUT [N] numbering - they'll be appended to the Sources section automatically
76
+ - If a Live Search result contradicts a static source, FLAG it as "(real-time conflicts with [N])"`
77
+ : "";
78
+ const finalSections = isFinal
79
+ ? `## TL;DR
80
+ 2-3 sentences answering the question directly. No hedging unless evidence demands it.
81
+
82
+ ## At a Glance
83
+ A Markdown table summarizing 4-8 key metrics, dimensions, or status indicators from the sources. Format:
84
+ | Dimension | Value / Status | Source |
85
+ |---|---|---|
86
+ | (example) Production adoption | 57% have agents in production | [3] |
87
+
88
+ Use tables whenever you have:
89
+ - Comparison data (X vs Y vs Z)
90
+ - Status snapshots (multiple metrics at one point in time)
91
+ - Rankings or scorecards
92
+ - Dollar amounts, percentages, dates side-by-side
93
+
94
+ This section is REQUIRED whenever the sources contain quantitative data. Skip ONLY if the topic is purely qualitative.
95
+
96
+ ## Key Findings
97
+ - 6-10 substantive bullets, each citing source numbers like [1] or [2,4]
98
+ - Lead with specific named entities, dollar amounts, percentages, dates - not generalities
99
+ - Mix angles - definition, current state, comparisons, criticisms
100
+ - Tag each bullet with a confidence level at the end: \`(high)\` / \`(medium)\` / \`(low)\`
101
+ - "high" means primary sources or strong consensus; "low" means single source or contested
102
+
103
+ ## Analysis
104
+ 3-5 paragraphs synthesizing the sources. Connect findings, note tensions and gaps, distinguish correlation from causation. Use inline citations throughout - every numerical claim or named entity must carry a [N].
105
+
106
+ ## Counterevidence & Limitations
107
+ - 2-4 bullets listing what could change the conclusion: weak sources, missing data, conflicting findings, age of evidence
108
+ - This section is required - never skip it
109
+
110
+ ## Follow-up Questions
111
+ - 3-5 questions a curious reader would ask after reading this report
112
+ - Make them concrete and answerable, not philosophical`
113
+ : `## Draft Summary
114
+ Single paragraph synthesis covering the main findings from sources, with inline citations.`;
115
+ const profileBlock = profileContext
116
+ ? `\n\n<user_profile>\n${profileContext}\n</user_profile>\n`
117
+ : "";
118
+ const memoryBlock = memoryContext
119
+ ? `\n\n<user_memory>\nStored knowledge about this user — use to frame the report toward their known interests, not to fabricate facts:\n${memoryContext}\n</user_memory>\n`
120
+ : "";
121
+ const sys = `You are a senior analyst writing a structured research report from numbered web sources.
122
+
123
+ ${(0, enrichment_router_js_1.todayContext)()}${profileBlock}${memoryBlock}
124
+
125
+ OUTPUT FORMAT (strict - exact Markdown sections, in this order):
126
+
127
+ # {short concrete title - max 10 words}
128
+
129
+ ${finalSections}
130
+
131
+ SOURCE CLASS TAGGING (mandatory in Key Findings):
132
+ Each source in the list above is labeled [primary] / [expert] / [secondary] / [market].
133
+ - [primary]: official docs, .gov/.edu, protocol data (DefiLlama, CoinGecko, etherscan), peer-reviewed
134
+ - [expert]: named researchers, audit reports, tier-1 financial press (Reuters, FT, Bloomberg)
135
+ - [secondary]: crypto media, newsletters, Wikipedia, GitHub
136
+ - [market]: X/Twitter, Reddit, Substack, prediction markets - sentiment, not fact
137
+
138
+ TAG RULES:
139
+ - Every Key Findings bullet must end with the source class of its strongest citation, e.g. "[primary]" or "[market]"
140
+ - Example: "Aerodrome TVL reached $2.1B in June 2026 [3] [primary]"
141
+ - If a bullet's evidence mixes classes, use the LOWEST class: one Reddit citation drags the whole bullet to "[market]"
142
+
143
+ CONTRADICTION RULES (mandatory):
144
+ - When two sources disagree on a fact, do NOT average them. Surface the conflict explicitly:
145
+ "Source [N] says X; source [M] says not-X - reconciliation: ..."
146
+ - Put irreconcilable contradictions in Counterevidence & Limitations, not Key Findings
147
+ - A single uncontested source is flagged: "(single source [N])"
148
+
149
+ CITATION DENSITY RULES:
150
+ - Every numerical claim (percentage, dollar amount, count, date) MUST carry [N]
151
+ - Every named entity (company, product, framework, person) MUST carry [N] on first mention
152
+ - Target: at least 1 citation per 50 words in Key Findings and Analysis
153
+ - Note source dates when relevant - older sources may be stale
154
+
155
+ STYLE RULES:
156
+ - Be specific: numbers, names, dates over vague claims
157
+ - Lead with concrete entities, not abstract concepts
158
+ - Tables > bullets when comparing dimensions
159
+ - No filler ("it is important to note", "in conclusion", "in today's world", "navigate the landscape")
160
+ - No hedging when evidence is strong; no false confidence when it's weak
161
+ - Don't write a Sources section - that gets appended automatically`;
162
+ const freshBlock = freshMode && isFinal
163
+ ? `
164
+
165
+ ⏱ FRESH MODE active - today is ${(0, deep_research_text_js_1.todayISO)()}:
166
+ - Prioritize claims dated within the last 30 days. If a source is older, only cite it if it's primary evidence (data, official statement).
167
+ - In the At a Glance table, include a "Date" column showing the publish date for each metric.
168
+ - In Key Findings, prefix each bullet with the source's publish date in brackets: [2026-MM-DD] Finding text [N].
169
+ - In Counterevidence, flag any claim whose evidence is older than 60 days as "(potentially stale).".`
170
+ : "";
171
+ const continuationBlock = priorContext && isFinal
172
+ ? `
173
+
174
+ PRIOR REPORT (you are CONTINUING this research, not starting fresh):
175
+
176
+ \`\`\`
177
+ ${priorContext.content.slice(0, 3500)}
178
+ \`\`\`
179
+
180
+ CONTINUATION RULES:
181
+ - Start the TL;DR with: "Update to prior report \`${priorContext.key}\` -" followed by the new takeaway.
182
+ - The "At a Glance" table must include a column "Δ since prior" showing what changed.
183
+ - Key Findings must mark each bullet with: \`(NEW)\` for genuinely new info, \`(UPDATED)\` for changed numbers/positions, or \`(CONFIRMED)\` for points the new sources reinforce.
184
+ - Counterevidence section must explicitly say which prior claims are now weaker.
185
+ - Follow-up Questions must build on the prior report's open questions if they're still relevant.
186
+
187
+ Do not re-explain background already in the prior report. Assume the reader read it.`
188
+ : "";
189
+ const enrichmentBlock = enrichment.hasData
190
+ ? `\n\n${enrichment.context}\n\n`
191
+ : "";
192
+ // Specialist angles - when present, tell the synthesizer to mirror this
193
+ // structure in the report (Key Findings organized by angle, At a Glance
194
+ // table dimensions match the angles). Without this, the model blends
195
+ // every angle into a homogeneous narrative.
196
+ const anglesBlock = isFinal && angles && angles.length > 0
197
+ ? `\n\nSPECIALIST ANGLES INVESTIGATED:
198
+ ${angles.map((a, i) => `${i + 1}. **${a.label}** - ${a.rationale}`).join("\n")}
199
+
200
+ Mirror this structure: Key Findings should group bullets by angle (use the angle label as a sub-header), and the At a Glance table dimensions should match the angles.`
201
+ : "";
202
+ // Critic notes (deep mode only) - explicit weaknesses surfaced during
203
+ // the audit pass. The synthesizer must address each one rather than
204
+ // simply ignoring it. This is the strongest single quality lever.
205
+ const criticBlock = isFinal && criticNotes && criticNotes.length > 50
206
+ ? `\n\nCRITIC AUDIT - concrete weaknesses found in the draft. You MUST address each one in the final report (either by revising the claim, surfacing the uncertainty, or providing additional support):
207
+
208
+ ${criticNotes}
209
+
210
+ If a critic concern can't be resolved with the sources you have, surface it in Counterevidence & Limitations rather than burying it.`
211
+ : "";
212
+ const user = `RESEARCH QUESTION: ${query}
213
+ ${enrichmentBlock}
214
+ SOURCES:
215
+ ${sourceBlocks}${liveSearchNote}${continuationBlock}${freshBlock}${anglesBlock}${criticBlock}
216
+
217
+ Write the ${isFinal ? "final" : "draft"} report now. Markdown only - no preamble, no postamble.${enrichment.hasData
218
+ ? `\n\nIMPORTANT: The AUTHORITATIVE LIVE DATA block at the top contains current numbers from primary APIs (DefiLlama, CoinGecko). Lead with these numbers when they exist - they override any conflicting figures in the scraped sources below. Cite them as [DefiLlama] or [CoinGecko].`
219
+ : ""}`;
220
+ // Research synthesis uses FINCH_RESEARCH_MODEL when set. Default is
221
+ // `grok-4.3` - when Bankr is the active gateway this routes to Grok 4.3
222
+ // through Bankr (Grok handles fresh data better; Claude is the safer
223
+ // pick for reasoning, JSON, code). Override via env or pass the same
224
+ // model string to FINCH_MODEL to bypass.
225
+ const researchModel = process.env.FINCH_RESEARCH_MODEL ?? "grok-4.3";
226
+ // Deep mode (critic notes present, or the 5-angle planner ran) produces a
227
+ // longer report than the standard 6-section template - a fixed 4000-token
228
+ // budget was cutting deep reports off mid-sentence around risk #6-7 of 13+.
229
+ const isDeepMode = !!criticNotes || (angles?.length ?? 0) >= 5;
230
+ const finalTokens = isDeepMode ? 7000 : 4000;
231
+ const raw = await (0, llm_js_1.callLLM)(sys, user, isFinal ? finalTokens : 2000, [], 90000, { liveSearch, model: researchModel });
232
+ const { content: report, liveCitations } = extractLiveCitations(raw);
233
+ // Citation density check - only for final reports. If the report has many
234
+ // numerical claims but very few [N] citations, retry once with a stricter
235
+ // instruction. Cheap insurance against lazy synthesis.
236
+ if (!isFinal)
237
+ return { report, liveCitations };
238
+ const density = measureCitationDensity(report);
239
+ if (density.numericalClaims >= 5 && density.citations < Math.max(3, density.numericalClaims / 2)) {
240
+ const retryUser = `${user}
241
+
242
+ ⚠️ Your previous draft had ${density.numericalClaims} numerical claims but only ${density.citations} [N] citations. That ratio is too low. Rewrite with stricter citation density: every percentage, dollar amount, count, date, and named entity must carry [N]. Use the At a Glance table to anchor the key metrics.`;
243
+ try {
244
+ const rawRetry = await (0, llm_js_1.callLLM)(sys, retryUser, finalTokens, [], 90000, { liveSearch, model: researchModel });
245
+ const { content: retryReport, liveCitations: retryCitations } = extractLiveCitations(rawRetry);
246
+ return { report: retryReport, liveCitations: retryCitations.length > 0 ? retryCitations : liveCitations };
247
+ }
248
+ catch {
249
+ return { report, liveCitations };
250
+ }
251
+ }
252
+ return { report, liveCitations };
253
+ }
254
+ // Strip the GROK_LIVE_CITATIONS sentinel block (added by callGrok when Live
255
+ // Search ran) and return the citation URLs separately.
256
+ function extractLiveCitations(raw) {
257
+ const match = raw.match(/<!--GROK_LIVE_CITATIONS\n([\s\S]*?)\nGROK_LIVE_CITATIONS-->/);
258
+ if (!match)
259
+ return { content: raw, liveCitations: [] };
260
+ const urls = match[1].split("\n").map((u) => u.trim()).filter(Boolean);
261
+ return { content: raw.replace(match[0], "").trimEnd(), liveCitations: urls };
262
+ }
263
+ function measureCitationDensity(report) {
264
+ // Numerical claims: percentages, dollar amounts, large counts, years
265
+ const percentages = report.match(/\d+(?:\.\d+)?\s*%/g) ?? [];
266
+ const dollars = report.match(/\$\s*\d+(?:\.\d+)?\s*(?:[KkMmBbTt]|million|billion|trillion)?/g) ?? [];
267
+ const counts = report.match(/\b\d{1,3}(?:,\d{3})+\b/g) ?? [];
268
+ const years = report.match(/\b(?:19|20)\d{2}\b/g) ?? [];
269
+ const numericalClaims = percentages.length + dollars.length + counts.length + years.length;
270
+ // Inline citations
271
+ const citationsMatches = report.match(/\[\d+(?:\s*,\s*\d+)*\]/g) ?? [];
272
+ const citations = citationsMatches.length;
273
+ // Capitalized multi-word entities (proper nouns) - proxy for named entities
274
+ const namedEntities = (report.match(/\b[A-Z][a-z]+(?:[A-Z][a-z]+|\s+[A-Z][a-z]+)\b/g) ?? []).length;
275
+ return { numericalClaims, citations, namedEntities };
276
+ }
277
+ // Output structure validation - returns the names of any failed checks.
278
+ // Used to decide whether the synthesis output is worth retrying.
279
+ function validateReportStructure(report) {
280
+ const issues = [];
281
+ // 1. "At a Glance" section with a Markdown table
282
+ const atGlance = report.match(/##\s*At a Glance[\s\S]*?(?=\n##|\n#|$)/i);
283
+ if (!atGlance) {
284
+ issues.push("missing-at-a-glance");
285
+ }
286
+ else {
287
+ // Markdown table = at least 2 lines that start with `|`
288
+ const tableLines = (atGlance[0].match(/^\|.+\|.+$/gm) ?? []).length;
289
+ if (tableLines < 2)
290
+ issues.push("at-a-glance-no-table");
291
+ }
292
+ // 2. Counterevidence section must exist and be non-trivial
293
+ const counter = report.match(/##\s*Counterevidence[\s\S]*?(?=\n##|\n#|$)/i);
294
+ if (!counter) {
295
+ issues.push("missing-counterevidence");
296
+ }
297
+ else {
298
+ const counterText = counter[0].replace(/##.*$/m, "").trim();
299
+ if (counterText.length < 80)
300
+ issues.push("counterevidence-too-short");
301
+ }
302
+ // 3. Citation density - every 200 words should have at least 1 [N] citation
303
+ // in the Key Findings + Analysis sections
304
+ const findingsAndAnalysis = report
305
+ .replace(/^#.+$/m, "") // strip title
306
+ .replace(/##\s*(TL;DR|At a Glance|Sources|Follow-up Questions)[\s\S]*?(?=\n##|\n#|$)/gi, "")
307
+ .replace(/##\s*Counterevidence[\s\S]*?(?=\n##|\n#|$)/gi, "");
308
+ const wordCount = findingsAndAnalysis.split(/\s+/).filter(Boolean).length;
309
+ const citationCount = (findingsAndAnalysis.match(/\[\d+(?:\s*,\s*\d+)*\]/g) ?? []).length;
310
+ if (wordCount >= 200 && citationCount < Math.floor(wordCount / 200)) {
311
+ issues.push("low-citation-density");
312
+ }
313
+ // 4. Follow-up Questions section present
314
+ if (!report.match(/##\s*Follow-up Questions/i)) {
315
+ issues.push("missing-followups");
316
+ }
317
+ return issues;
318
+ }
319
+ // Group Live Search citations by source type (X, news, web) for readability.
320
+ function formatLiveCitations(urls) {
321
+ const groups = { "X (Twitter)": [], "News": [], "Web": [] };
322
+ for (const url of urls) {
323
+ if (/x\.com|twitter\.com/i.test(url))
324
+ groups["X (Twitter)"].push(url);
325
+ else if (/(reuters|apnews|bbc|cnbc|bloomberg|theverge|techcrunch|wsj|ft|nytimes|coindesk|axios|economist)\.com/i.test(url))
326
+ groups["News"].push(url);
327
+ else
328
+ groups["Web"].push(url);
329
+ }
330
+ const lines = [];
331
+ for (const [label, list] of Object.entries(groups)) {
332
+ if (list.length === 0)
333
+ continue;
334
+ lines.push(`**${label}** (${list.length}):`);
335
+ for (const url of list.slice(0, 8)) {
336
+ lines.push(`- ${url}`);
337
+ }
338
+ if (list.length > 8)
339
+ lines.push(`- _…and ${list.length - 8} more_`);
340
+ lines.push("");
341
+ }
342
+ return lines.join("\n").trimEnd();
343
+ }
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ // Text utilities: JSON extraction, domain scoring, excerpt selection, query
3
+ // terms, and source ranking/dedup - the parts of the pipeline with no LLM
4
+ // or network call.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.safeParseJson = safeParseJson;
7
+ exports.domainOf = domainOf;
8
+ exports.tierBonus = tierBonus;
9
+ exports.chunkMarkdown = chunkMarkdown;
10
+ exports.chunkRelevance = chunkRelevance;
11
+ exports.pickBestExcerpt = pickBestExcerpt;
12
+ exports.extractQueryTerms = extractQueryTerms;
13
+ exports.todayISO = todayISO;
14
+ exports.currentYearMonth = currentYearMonth;
15
+ exports.buildSearchTermsForLinking = buildSearchTermsForLinking;
16
+ exports.extractVaultKeyFromHit = extractVaultKeyFromHit;
17
+ exports.rankAndDedupe = rankAndDedupe;
18
+ const deep_research_constants_js_1 = require("./deep-research-constants.js");
19
+ function safeParseJson(raw, fallback) {
20
+ try {
21
+ return JSON.parse(raw);
22
+ }
23
+ catch { /* try the next parse strategy */ }
24
+ const stripped = raw.replace(/^```(?:json)?\n?/m, "").replace(/\n?```$/m, "").trim();
25
+ try {
26
+ return JSON.parse(stripped);
27
+ }
28
+ catch { /* try the next parse strategy */ }
29
+ const arrMatch = stripped.match(/\[[\s\S]*\]/);
30
+ if (arrMatch) {
31
+ try {
32
+ return JSON.parse(arrMatch[0]);
33
+ }
34
+ catch { /* try the next parse strategy */ }
35
+ }
36
+ const objMatch = stripped.match(/\{[\s\S]*\}/);
37
+ if (objMatch) {
38
+ try {
39
+ return JSON.parse(objMatch[0]);
40
+ }
41
+ catch { /* try the next parse strategy */ }
42
+ }
43
+ return fallback;
44
+ }
45
+ function domainOf(url) {
46
+ try {
47
+ return new URL(url).hostname.replace(/^www\./, "");
48
+ }
49
+ catch {
50
+ return "unknown";
51
+ }
52
+ }
53
+ function tierBonus(url) {
54
+ for (const [re, bonus] of deep_research_constants_js_1.DOMAIN_TIER_BONUS)
55
+ if (re.test(url))
56
+ return bonus;
57
+ return 0;
58
+ }
59
+ // Split markdown into ~600-char chunks at paragraph boundaries.
60
+ function chunkMarkdown(md, chunkSize = deep_research_constants_js_1.EXCERPT_CHARS_PER_CHUNK) {
61
+ const paragraphs = md.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
62
+ const chunks = [];
63
+ let current = "";
64
+ for (const p of paragraphs) {
65
+ if (current.length + p.length + 2 <= chunkSize) {
66
+ current = current ? `${current}\n\n${p}` : p;
67
+ }
68
+ else {
69
+ if (current)
70
+ chunks.push(current);
71
+ current = p.slice(0, chunkSize * 2); // very long single paragraph → cap
72
+ }
73
+ }
74
+ if (current)
75
+ chunks.push(current);
76
+ return chunks;
77
+ }
78
+ // Score a chunk against the query using term overlap. Cheap, no LLM call.
79
+ function chunkRelevance(chunk, queryTerms) {
80
+ const lower = chunk.toLowerCase();
81
+ let score = 0;
82
+ for (const term of queryTerms) {
83
+ const occurrences = lower.split(term).length - 1;
84
+ score += Math.min(occurrences, 5); // cap each term so a spammy page can't win
85
+ }
86
+ return score;
87
+ }
88
+ function pickBestExcerpt(md, queryTerms) {
89
+ const chunks = chunkMarkdown(md);
90
+ if (chunks.length === 0)
91
+ return md.slice(0, 1500);
92
+ const scored = chunks.map((c, i) => ({ c, i, score: chunkRelevance(c, queryTerms) }));
93
+ scored.sort((a, b) => b.score - a.score);
94
+ const top = scored.slice(0, deep_research_constants_js_1.EXCERPT_TOP_CHUNKS).sort((a, b) => a.i - b.i);
95
+ return top.map((t) => t.c).join("\n\n---\n\n");
96
+ }
97
+ function extractQueryTerms(query) {
98
+ return Array.from(new Set(query
99
+ .toLowerCase()
100
+ .replace(/[^a-z0-9\s]/g, " ")
101
+ .split(/\s+/)
102
+ .filter((t) => t.length >= 3 && !deep_research_constants_js_1.STOPWORDS.has(t)))).slice(0, 10);
103
+ }
104
+ function todayISO() {
105
+ return new Date().toISOString().slice(0, 10);
106
+ }
107
+ function currentYearMonth() {
108
+ const now = new Date();
109
+ const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
110
+ return `${months[now.getMonth()]} ${now.getFullYear()}`;
111
+ }
112
+ // Build a search query for finding related vault entries. Prefer high-signal
113
+ // terms from the user query, dropping common research filler words.
114
+ function buildSearchTermsForLinking(query) {
115
+ const terms = extractQueryTerms(query);
116
+ return terms.length > 0 ? terms.join(" ") : query;
117
+ }
118
+ // Extract a vault key from a search hit. The /vault/search endpoint returns
119
+ // semantic memory documents whose metadata may or may not contain a vault key.
120
+ function extractVaultKeyFromHit(hit) {
121
+ // Heuristic 1: metadata.vaultKey or metadata.key
122
+ if (hit.metadata?.vaultKey && typeof hit.metadata.vaultKey === "string")
123
+ return hit.metadata.vaultKey;
124
+ if (hit.metadata?.key && typeof hit.metadata.key === "string")
125
+ return hit.metadata.key;
126
+ // Heuristic 2: content first line matches the vault key path pattern
127
+ const firstLine = (hit.content ?? "").split("\n", 1)[0];
128
+ const m = firstLine.match(/^(research|memory|workflow|prompt|execution|file|credential)\/[a-z0-9\-/]+/i);
129
+ if (m)
130
+ return m[0];
131
+ return null;
132
+ }
133
+ function rankAndDedupe(candidates, freshMode) {
134
+ // Score: search-rank inverse + domain tier bonus. Lower queryRank = higher.
135
+ // In fresh mode, give news domains an extra +2 boost so recent reporting
136
+ // ranks above evergreen content.
137
+ const scored = candidates.map((c) => ({
138
+ ...c,
139
+ score: -c.queryRank + tierBonus(c.url) + (freshMode && deep_research_constants_js_1.NEWS_DOMAIN_BOOST_RE.test(c.url) ? 2 : 0),
140
+ domain: domainOf(c.url),
141
+ }));
142
+ scored.sort((a, b) => b.score - a.score);
143
+ // Domain diversity - cap MAX_PER_DOMAIN sources from same domain
144
+ const seenDomain = new Map();
145
+ const seenUrl = new Set();
146
+ const result = [];
147
+ for (const c of scored) {
148
+ if (seenUrl.has(c.url))
149
+ continue;
150
+ const count = seenDomain.get(c.domain) ?? 0;
151
+ if (count >= deep_research_constants_js_1.MAX_PER_DOMAIN)
152
+ continue;
153
+ seenUrl.add(c.url);
154
+ seenDomain.set(c.domain, count + 1);
155
+ result.push(c);
156
+ }
157
+ return result;
158
+ }
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ // Tool definition + input schema for deep_research. Pure data/validation,
3
+ // no execution logic.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.InputSchema = exports.DEEP_RESEARCH_TOOLS = void 0;
6
+ const zod_1 = require("zod");
7
+ exports.DEEP_RESEARCH_TOOLS = [
8
+ {
9
+ name: "deep_research",
10
+ description: "Web research engine: searches, scrapes, ranks and de-duplicates sources, then returns them " +
11
+ "as a numbered, citable evidence pack for YOU to synthesise. This is the default (mode='sources') " +
12
+ "and needs no API key. " +
13
+ "You are the analyst: pass your own sub-queries via `queries` for full control over the angles " +
14
+ "covered — otherwise they are derived from the topic. " +
15
+ "Set mode='report' only if you want the server to write the prose itself (requires an LLM key, " +
16
+ "and you cannot steer the result). " +
17
+ "Profile-aware, auto-saves to vault, auto-links to related past reports.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ query: {
22
+ type: "string",
23
+ description: "Research question. Be specific: 'state of Base chain TVL Q2 2026' beats 'Base chain'.",
24
+ },
25
+ queries: {
26
+ type: "array",
27
+ items: { type: "string" },
28
+ description: "YOUR sub-queries to search (recommended). You know the topic and the user's intent, so plan " +
29
+ "the angles yourself — 3-6 specific queries beat a generic decomposition. Omit to derive them.",
30
+ },
31
+ mode: {
32
+ type: "string",
33
+ enum: ["sources", "report"],
34
+ description: "'sources' (default) returns the ranked evidence pack for you to synthesise — no API key needed. " +
35
+ "'report' makes the server write the prose (needs an LLM key; you cannot steer it).",
36
+ },
37
+ depth: {
38
+ type: "string",
39
+ enum: ["fast", "standard", "deep"],
40
+ description: "fast=flat planner, 3 sub-Qs, ~10 sources (~45s). standard=3 specialist angles, ~14 sources, reflection round (~90s). deep=5 angles + adversarial critic + reflection, ~20 sources (~180s). Default standard.",
41
+ },
42
+ focus: {
43
+ type: "string",
44
+ description: "Optional angle hint - 'technical', 'investment', 'news', 'comparison'. Steers planning.",
45
+ },
46
+ continueFrom: {
47
+ type: "string",
48
+ description: "Vault key of a previous deep_research report to build on. When provided, the planner focuses on UPDATES, GAPS, and NEW developments since that report - not re-treading covered ground. The new report explicitly references and extends the prior findings. Format: 'research/...' (use vault_list type:research to find candidates). This is the multi-session research feature - Perplexity / ChatGPT Deep Research don't have an equivalent.",
49
+ },
50
+ freshMode: {
51
+ type: "boolean",
52
+ description: "Force time-sensitive research mode: planner appends recency hints to sub-queries, source ranking boosts news domains (Reuters, AP, Bloomberg, etc.), and the synthesizer is told to prioritize current/recent claims. Auto-enabled when the query contains time-sensitive keywords (today, latest, breaking, this week, etc.).",
53
+ },
54
+ freshDays: {
55
+ type: "number",
56
+ description: "When freshMode is on, restrict to results from the last N days. Default 14 days. Capped at 90.",
57
+ },
58
+ liveSearch: {
59
+ type: "boolean",
60
+ description: "Enable Grok Live Search - pulls real-time results from X (Twitter), news, web, RSS during synthesis. Only works when Grok is the active LLM provider. Adds ~5-15s per Grok call. Default: auto (on when Grok is active).",
61
+ },
62
+ liveSearchSources: {
63
+ type: "array",
64
+ items: { type: "string", enum: ["web", "x", "news", "rss"] },
65
+ description: "Which Live Search sources to pull from. Default: ['web', 'x', 'news']. Only respected when liveSearch is true and Grok is active.",
66
+ },
67
+ liveSearchDays: {
68
+ type: "number",
69
+ description: "Restrict Live Search to results from the last N days (max 365). Useful for time-sensitive queries. Default: no date filter.",
70
+ },
71
+ saveToVault: { type: "boolean", description: "Auto-save report to vault (default true)" },
72
+ },
73
+ required: ["query"],
74
+ },
75
+ },
76
+ ];
77
+ exports.InputSchema = zod_1.z.object({
78
+ query: zod_1.z.string().min(3).max(500),
79
+ queries: zod_1.z.array(zod_1.z.string().min(3).max(300)).max(12).optional(),
80
+ mode: zod_1.z.enum(["sources", "report"]).optional(),
81
+ depth: zod_1.z.enum(["fast", "standard", "deep"]).optional(),
82
+ focus: zod_1.z.string().max(80).optional(),
83
+ continueFrom: zod_1.z.string().max(200).optional(),
84
+ freshMode: zod_1.z.boolean().optional(),
85
+ freshDays: zod_1.z.number().int().min(1).max(90).optional(),
86
+ liveSearch: zod_1.z.boolean().optional(),
87
+ liveSearchSources: zod_1.z.array(zod_1.z.enum(["web", "x", "news", "rss"])).optional(),
88
+ liveSearchDays: zod_1.z.number().int().min(1).max(365).optional(),
89
+ saveToVault: zod_1.z.boolean().optional(),
90
+ });