@finchagentic/mcp 4.6.1 → 4.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -6
- package/dist/_http-cache.js +0 -96
- package/dist/_text-search.js +0 -39
- package/dist/agent-loop.js +0 -301
- package/dist/annotations.js +0 -122
- package/dist/cli.js +0 -1391
- package/dist/clink-input.js +0 -15
- package/dist/config.js +0 -132
- package/dist/convex.js +0 -175
- package/dist/dex-pair.js +0 -54
- package/dist/enrichment-router.js +0 -315
- package/dist/index.js +0 -258
- package/dist/llm.js +0 -298
- package/dist/local-memory-file.js +0 -150
- package/dist/local-memory.js +0 -135
- package/dist/local-vault.js +0 -456
- package/dist/output-schemas.js +0 -605
- package/dist/project.js +0 -36
- package/dist/prompts.js +0 -111
- package/dist/public-url.js +0 -107
- package/dist/resources.js +0 -111
- package/dist/server.js +0 -322
- package/dist/signal-gate.js +0 -57
- package/dist/token-decimals.js +0 -26
- package/dist/token-gate.js +0 -88
- package/dist/tool-filter.js +0 -53
- package/dist/tools/_solidity-scan.js +0 -313
- package/dist/tools/agents.js +0 -441
- package/dist/tools/automation.js +0 -354
- package/dist/tools/base-mcp.js +0 -466
- package/dist/tools/base.js +0 -283
- package/dist/tools/chronicle.js +0 -268
- package/dist/tools/coder.js +0 -94
- package/dist/tools/deep-research.js +0 -1421
- package/dist/tools/defi.js +0 -292
- package/dist/tools/equity.js +0 -372
- package/dist/tools/events.js +0 -182
- package/dist/tools/github.js +0 -564
- package/dist/tools/insider.js +0 -264
- package/dist/tools/insight.js +0 -630
- package/dist/tools/market.js +0 -555
- package/dist/tools/memory.js +0 -1059
- package/dist/tools/miroshark.js +0 -350
- package/dist/tools/monitor.js +0 -319
- package/dist/tools/os.js +0 -236
- package/dist/tools/packets.js +0 -296
- package/dist/tools/research-chain.js +0 -226
- package/dist/tools/research-compare.js +0 -280
- package/dist/tools/research.js +0 -188
- package/dist/tools/rh-bridge.js +0 -148
- package/dist/tools/rh-mcp.js +0 -1448
- package/dist/tools/rh-orders.js +0 -556
- package/dist/tools/scanner.js +0 -564
- package/dist/tools/stake.js +0 -369
- package/dist/tools/vault.js +0 -1020
- package/dist/tools/wallet.js +0 -200
- package/dist/types.js +0 -2
- package/dist/wallet.js +0 -372
package/dist/llm.js
DELETED
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.hasDirectLLMKey = hasDirectLLMKey;
|
|
4
|
-
exports.grokLiveSearchHits = grokLiveSearchHits;
|
|
5
|
-
exports.isGrokActive = isGrokActive;
|
|
6
|
-
exports.callLLM = callLLM;
|
|
7
|
-
const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
|
|
8
|
-
const BANKR_URL = "https://llm.bankr.bot/v1/chat/completions";
|
|
9
|
-
const GROK_URL = "https://api.x.ai/v1/chat/completions";
|
|
10
|
-
const OPENAI_URL = "https://api.openai.com/v1/chat/completions";
|
|
11
|
-
/**
|
|
12
|
-
* Returns true if Grok is the currently active LLM provider, based on env.
|
|
13
|
-
* Useful for tools that want to conditionally enable Grok-specific features
|
|
14
|
-
* like Live Search.
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* True if any BYOK provider key (Bankr/Anthropic/OpenAI/Grok) is set. Callers
|
|
18
|
-
* that must never fall through to callViaConvex() - e.g. local-memory mode,
|
|
19
|
-
* which promises zero Convex involvement - should check this first and fail
|
|
20
|
-
* clearly instead of letting callLLM() silently proxy through Finch.
|
|
21
|
-
*/
|
|
22
|
-
function hasDirectLLMKey() {
|
|
23
|
-
return !!(process.env.BANKR_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GROK_API_KEY);
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Grok Live Search used purely as a RETRIEVAL layer.
|
|
27
|
-
*
|
|
28
|
-
* X/news content is the one thing an MCP client genuinely cannot reach on its
|
|
29
|
-
* own, which makes fetching it real tool work. Interpreting it is not — so this
|
|
30
|
-
* asks Grok to return the found items verbatim and hands them upward as sources.
|
|
31
|
-
* The caller's model decides what any of it means.
|
|
32
|
-
*
|
|
33
|
-
* Returns [] on any failure: live search is an augmentation, never a hard
|
|
34
|
-
* dependency, and research must still work without a Grok key.
|
|
35
|
-
*/
|
|
36
|
-
async function grokLiveSearchHits(query, sources = ["x", "news", "web"], days, maxResults = 10) {
|
|
37
|
-
const apiKey = process.env.GROK_API_KEY;
|
|
38
|
-
if (!apiKey)
|
|
39
|
-
return [];
|
|
40
|
-
const body = {
|
|
41
|
-
model: process.env.FINCH_GROK_MODEL ?? "grok-4.3",
|
|
42
|
-
max_tokens: 2000,
|
|
43
|
-
messages: [
|
|
44
|
-
{
|
|
45
|
-
role: "system",
|
|
46
|
-
content: "You are a retrieval tool, not an analyst. Return what you find verbatim. " +
|
|
47
|
-
"Never summarise, interpret, rank by opinion, or add commentary.",
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
role: "user",
|
|
51
|
-
content: `Search for: ${query}\n\n` +
|
|
52
|
-
`Return up to ${maxResults} of the most relevant recent items, one per line, ` +
|
|
53
|
-
`in exactly this format:\n\n` +
|
|
54
|
-
`URL :: verbatim quote or post text (max 400 chars)\n\n` +
|
|
55
|
-
`Rules: quote the source's own words. Do not paraphrase. Do not add analysis, ` +
|
|
56
|
-
`conclusions, or your own framing. If an item has no URL, use "-" for the URL.`,
|
|
57
|
-
},
|
|
58
|
-
],
|
|
59
|
-
search_parameters: {
|
|
60
|
-
mode: "on",
|
|
61
|
-
sources: sources.map((type) => ({ type })),
|
|
62
|
-
max_search_results: maxResults,
|
|
63
|
-
...(days ? { from_date: new Date(Date.now() - days * 86400000).toISOString().slice(0, 10) } : {}),
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
try {
|
|
67
|
-
const res = await fetch(GROK_URL, {
|
|
68
|
-
method: "POST",
|
|
69
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
70
|
-
body: JSON.stringify(body),
|
|
71
|
-
signal: AbortSignal.timeout(45000),
|
|
72
|
-
});
|
|
73
|
-
if (!res.ok)
|
|
74
|
-
return [];
|
|
75
|
-
const data = (await res.json());
|
|
76
|
-
const content = data.choices?.[0]?.message?.content ?? "";
|
|
77
|
-
const hits = [];
|
|
78
|
-
for (const line of content.split("\n")) {
|
|
79
|
-
const idx = line.indexOf("::");
|
|
80
|
-
if (idx === -1)
|
|
81
|
-
continue;
|
|
82
|
-
const url = line.slice(0, idx).trim().replace(/^[-*\d.\s]+/, "");
|
|
83
|
-
const excerpt = line.slice(idx + 2).trim();
|
|
84
|
-
if (excerpt.length < 20)
|
|
85
|
-
continue;
|
|
86
|
-
hits.push({ url: /^https?:\/\//.test(url) ? url : "", excerpt });
|
|
87
|
-
}
|
|
88
|
-
// If the model ignored the format, fall back to bare citations so the
|
|
89
|
-
// caller still receives the URLs rather than nothing.
|
|
90
|
-
if (hits.length === 0 && data.citations?.length) {
|
|
91
|
-
return data.citations.slice(0, maxResults).map((url) => ({ url, excerpt: "" }));
|
|
92
|
-
}
|
|
93
|
-
return hits.slice(0, maxResults);
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
return [];
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function isGrokActive() {
|
|
100
|
-
const provider = process.env.FINCH_PROVIDER?.toLowerCase().trim();
|
|
101
|
-
if (provider === "grok")
|
|
102
|
-
return !!process.env.GROK_API_KEY;
|
|
103
|
-
if (provider === "bankr" || provider === "anthropic" || provider === "openai")
|
|
104
|
-
return false;
|
|
105
|
-
// Auto-priority - Grok is only active if it's the only key present
|
|
106
|
-
if (process.env.BANKR_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY)
|
|
107
|
-
return false;
|
|
108
|
-
return !!process.env.GROK_API_KEY;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Call the best available LLM.
|
|
112
|
-
*
|
|
113
|
-
* Provider auto-priority (when FINCH_PROVIDER unset):
|
|
114
|
-
* BANKR_API_KEY → ANTHROPIC_API_KEY → OPENAI_API_KEY → GROK_API_KEY → Convex backend
|
|
115
|
-
*
|
|
116
|
-
* Force a provider via FINCH_PROVIDER: "bankr" | "anthropic" | "openai" | "grok"
|
|
117
|
-
*
|
|
118
|
-
* Model selection (first wins):
|
|
119
|
-
* FINCH_MODEL → {provider}_MODEL → provider default
|
|
120
|
-
*/
|
|
121
|
-
async function callLLM(systemPrompt, userPrompt, maxTokens = 1024, history = [], timeoutMs = 60000, options = {}) {
|
|
122
|
-
const provider = process.env.FINCH_PROVIDER?.toLowerCase().trim();
|
|
123
|
-
const bankrKey = process.env.BANKR_API_KEY;
|
|
124
|
-
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
125
|
-
const openaiKey = process.env.OPENAI_API_KEY;
|
|
126
|
-
const grokKey = process.env.GROK_API_KEY;
|
|
127
|
-
// Explicit provider override - user picked one
|
|
128
|
-
if (provider === "grok" && grokKey)
|
|
129
|
-
return callGrok(grokKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.liveSearch, options.model);
|
|
130
|
-
if (provider === "anthropic" && anthropicKey)
|
|
131
|
-
return callAnthropic(anthropicKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
132
|
-
if (provider === "openai" && openaiKey)
|
|
133
|
-
return callOpenAI(openaiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
134
|
-
if (provider === "bankr" && bankrKey)
|
|
135
|
-
return callBankr(bankrKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
136
|
-
// Auto priority
|
|
137
|
-
if (bankrKey)
|
|
138
|
-
return callBankr(bankrKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
139
|
-
if (anthropicKey)
|
|
140
|
-
return callAnthropic(anthropicKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
141
|
-
if (openaiKey)
|
|
142
|
-
return callOpenAI(openaiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
143
|
-
if (grokKey)
|
|
144
|
-
return callGrok(grokKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.liveSearch, options.model);
|
|
145
|
-
// No provider key configured - BYOK is required. This tool needs its own
|
|
146
|
-
// multi-step reasoning (planning, synthesis, critique) that an MCP host's
|
|
147
|
-
// model cannot do on the tool's behalf, so there is no free path: bring
|
|
148
|
-
// your own key or don't use tools that need one (ask_finch, deep_research,
|
|
149
|
-
// market_thesis, trade_plan, scheduled agent runs). Fails clearly instead
|
|
150
|
-
// of silently billing the Finch deployment owner for every anonymous
|
|
151
|
-
// install of this package.
|
|
152
|
-
throw new Error("No LLM provider configured. This tool needs its own key to do multi-step " +
|
|
153
|
-
"reasoning server-side - set one of BANKR_API_KEY, ANTHROPIC_API_KEY, " +
|
|
154
|
-
"OPENAI_API_KEY, or GROK_API_KEY as an environment variable (see the " +
|
|
155
|
-
"Configuration section of the README), then retry. Most other Finch tools " +
|
|
156
|
-
"don't need this - only ones that do their own internal LLM reasoning do.");
|
|
157
|
-
}
|
|
158
|
-
async function callAnthropic(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, modelOverride) {
|
|
159
|
-
const messages = [...history, { role: "user", content: userPrompt }];
|
|
160
|
-
const model = modelOverride ?? process.env.FINCH_MODEL ?? process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
|
|
161
|
-
const res = await fetch(ANTHROPIC_URL, {
|
|
162
|
-
method: "POST",
|
|
163
|
-
headers: {
|
|
164
|
-
"Content-Type": "application/json",
|
|
165
|
-
"x-api-key": apiKey,
|
|
166
|
-
"anthropic-version": "2023-06-01",
|
|
167
|
-
},
|
|
168
|
-
body: JSON.stringify({ model, max_tokens: maxTokens, system: systemPrompt, messages }),
|
|
169
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
170
|
-
});
|
|
171
|
-
if (!res.ok) {
|
|
172
|
-
const body = await res.text().catch(() => "");
|
|
173
|
-
throw new Error(`Anthropic error ${res.status}: ${body.slice(0, 200)}`);
|
|
174
|
-
}
|
|
175
|
-
const data = await res.json();
|
|
176
|
-
return data.content?.find(b => b.type === "text")?.text ?? "";
|
|
177
|
-
}
|
|
178
|
-
async function callBankr(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, modelOverride) {
|
|
179
|
-
const model = modelOverride ?? process.env.FINCH_MODEL ?? process.env.BANKR_MODEL ?? "claude-haiku-4-5-20251001";
|
|
180
|
-
const res = await fetch(BANKR_URL, {
|
|
181
|
-
method: "POST",
|
|
182
|
-
headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
|
|
183
|
-
body: JSON.stringify({
|
|
184
|
-
model,
|
|
185
|
-
messages: [
|
|
186
|
-
{ role: "system", content: systemPrompt },
|
|
187
|
-
...history,
|
|
188
|
-
{ role: "user", content: userPrompt },
|
|
189
|
-
],
|
|
190
|
-
max_tokens: maxTokens,
|
|
191
|
-
}),
|
|
192
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
193
|
-
});
|
|
194
|
-
if (!res.ok) {
|
|
195
|
-
const body = await res.text().catch(() => "");
|
|
196
|
-
throw new Error(`Bankr error ${res.status}: ${body.slice(0, 200)}`);
|
|
197
|
-
}
|
|
198
|
-
const data = await res.json();
|
|
199
|
-
return data.choices?.[0]?.message?.content ?? "";
|
|
200
|
-
}
|
|
201
|
-
// OPENAI_BASE_URL lets this point at any OpenAI Chat Completions-compatible
|
|
202
|
-
// endpoint instead of api.openai.com - self-hosted gateways (LiteLLM, vLLM,
|
|
203
|
-
// Ollama, LocalAI) and aggregators (OpenRouter) all speak this same API
|
|
204
|
-
// shape, so no separate provider code is needed for them.
|
|
205
|
-
function openAiChatUrl() {
|
|
206
|
-
const base = process.env.OPENAI_BASE_URL?.replace(/\/+$/, "");
|
|
207
|
-
return base ? `${base}/chat/completions` : OPENAI_URL;
|
|
208
|
-
}
|
|
209
|
-
async function callOpenAI(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, modelOverride) {
|
|
210
|
-
const model = modelOverride ?? process.env.FINCH_MODEL ?? process.env.OPENAI_MODEL ?? "gpt-4o-mini";
|
|
211
|
-
const res = await fetch(openAiChatUrl(), {
|
|
212
|
-
method: "POST",
|
|
213
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
214
|
-
body: JSON.stringify({
|
|
215
|
-
model,
|
|
216
|
-
messages: [
|
|
217
|
-
{ role: "system", content: systemPrompt },
|
|
218
|
-
...history,
|
|
219
|
-
{ role: "user", content: userPrompt },
|
|
220
|
-
],
|
|
221
|
-
max_tokens: maxTokens,
|
|
222
|
-
}),
|
|
223
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
224
|
-
});
|
|
225
|
-
if (!res.ok) {
|
|
226
|
-
const body = await res.text().catch(() => "");
|
|
227
|
-
throw new Error(`OpenAI error ${res.status}: ${body.slice(0, 200)}`);
|
|
228
|
-
}
|
|
229
|
-
const data = await res.json();
|
|
230
|
-
// Most OpenAI-compatible gateways put `choices` at the top level, but
|
|
231
|
-
// OPENAI_BASE_URL can point at anything that speaks this API shape - at
|
|
232
|
-
// least one (9Router) wraps the whole payload in `{ data: {...}, success:
|
|
233
|
-
// true }` for some models (confirmed live: routers9/glm5.2 returns this
|
|
234
|
-
// wrapper while routers9/tencent/hy3 on the SAME endpoint/key returns
|
|
235
|
-
// top-level choices). Without this fallback the call silently returns ""
|
|
236
|
-
// instead of throwing - worse than an error, since every caller of
|
|
237
|
-
// callLLM() just gets an empty synthesis with no indication anything failed.
|
|
238
|
-
const content = data.choices?.[0]?.message?.content ?? data.data?.choices?.[0]?.message?.content;
|
|
239
|
-
if (!content)
|
|
240
|
-
throw new Error(`OpenAI-compatible endpoint returned no content (model: ${model})`);
|
|
241
|
-
return content;
|
|
242
|
-
}
|
|
243
|
-
async function callGrok(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, liveSearch, modelOverride) {
|
|
244
|
-
const model = modelOverride ?? process.env.FINCH_MODEL ?? process.env.GROK_MODEL ?? "grok-4-fast-reasoning";
|
|
245
|
-
const body = {
|
|
246
|
-
model,
|
|
247
|
-
messages: [
|
|
248
|
-
{ role: "system", content: systemPrompt },
|
|
249
|
-
...history,
|
|
250
|
-
{ role: "user", content: userPrompt },
|
|
251
|
-
],
|
|
252
|
-
max_tokens: maxTokens,
|
|
253
|
-
stream: false,
|
|
254
|
-
};
|
|
255
|
-
// xAI Live Search - pulls real-time results from web/X/news/RSS during inference.
|
|
256
|
-
// Docs: https://docs.x.ai/docs/guides/live-search
|
|
257
|
-
// Note: as of late 2026 xAI deprecated this in favor of Agent Tools API
|
|
258
|
-
// (returns 410 Gone). We detect that and retry without search_parameters
|
|
259
|
-
// so synthesis still succeeds - just without the real-time augmentation.
|
|
260
|
-
if (liveSearch) {
|
|
261
|
-
body.search_parameters = {
|
|
262
|
-
mode: liveSearch.mode,
|
|
263
|
-
sources: liveSearch.sources.map((type) => ({ type })),
|
|
264
|
-
max_search_results: liveSearch.maxResults ?? 10,
|
|
265
|
-
...(liveSearch.fromDate ? { from_date: liveSearch.fromDate } : {}),
|
|
266
|
-
...(liveSearch.toDate ? { to_date: liveSearch.toDate } : {}),
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
let res = await fetch(GROK_URL, {
|
|
270
|
-
method: "POST",
|
|
271
|
-
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
|
|
272
|
-
body: JSON.stringify(body),
|
|
273
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
274
|
-
});
|
|
275
|
-
// Live Search deprecated → strip search_parameters and retry
|
|
276
|
-
if (res.status === 410 && liveSearch) {
|
|
277
|
-
delete body.search_parameters;
|
|
278
|
-
res = await fetch(GROK_URL, {
|
|
279
|
-
method: "POST",
|
|
280
|
-
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
|
|
281
|
-
body: JSON.stringify(body),
|
|
282
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
if (!res.ok) {
|
|
286
|
-
const errBody = await res.text().catch(() => "");
|
|
287
|
-
throw new Error(`Grok error ${res.status}: ${errBody.slice(0, 200)}`);
|
|
288
|
-
}
|
|
289
|
-
const data = await res.json();
|
|
290
|
-
const content = data.choices?.[0]?.message?.content ?? "";
|
|
291
|
-
// When Live Search ran (and was not deprecated), append the citations as a
|
|
292
|
-
// parsable block at the bottom - downstream consumers (deep_research) can
|
|
293
|
-
// read these and merge into the final source list.
|
|
294
|
-
if (liveSearch && data.citations && data.citations.length > 0) {
|
|
295
|
-
return `${content}\n\n<!--GROK_LIVE_CITATIONS\n${data.citations.join("\n")}\nGROK_LIVE_CITATIONS-->`;
|
|
296
|
-
}
|
|
297
|
-
return content;
|
|
298
|
-
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.getLocalMemoryFileConfig = getLocalMemoryFileConfig;
|
|
37
|
-
exports.fileMemoryAdd = fileMemoryAdd;
|
|
38
|
-
exports.fileMemoryDeleteByVaultKey = fileMemoryDeleteByVaultKey;
|
|
39
|
-
exports.fileMemorySearch = fileMemorySearch;
|
|
40
|
-
exports.fileMemoryList = fileMemoryList;
|
|
41
|
-
exports.fileMemoryDelete = fileMemoryDelete;
|
|
42
|
-
exports.fileMemoryProfile = fileMemoryProfile;
|
|
43
|
-
const fs = __importStar(require("fs"));
|
|
44
|
-
const os = __importStar(require("os"));
|
|
45
|
-
const path = __importStar(require("path"));
|
|
46
|
-
const crypto = __importStar(require("crypto"));
|
|
47
|
-
const _text_search_js_1 = require("./_text-search.js");
|
|
48
|
-
function getLocalMemoryFileConfig() {
|
|
49
|
-
return { dir: path.join(os.homedir(), ".finch", "memory") };
|
|
50
|
-
}
|
|
51
|
-
function indexPath(cfg) {
|
|
52
|
-
return path.join(cfg.dir, "index.json");
|
|
53
|
-
}
|
|
54
|
-
function readIndex(cfg) {
|
|
55
|
-
try {
|
|
56
|
-
const raw = fs.readFileSync(indexPath(cfg), "utf8");
|
|
57
|
-
const parsed = JSON.parse(raw);
|
|
58
|
-
return { memories: parsed.memories ?? [] };
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return { memories: [] };
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
// Atomic write: temp file + rename, matching local-vault.ts's pattern so a
|
|
65
|
-
// crash mid-write can't corrupt the index every memory depends on.
|
|
66
|
-
function writeIndex(cfg, idx) {
|
|
67
|
-
fs.mkdirSync(cfg.dir, { recursive: true });
|
|
68
|
-
const tmp = indexPath(cfg) + `.tmp-${process.pid}`;
|
|
69
|
-
fs.writeFileSync(tmp, JSON.stringify(idx, null, 2), "utf8");
|
|
70
|
-
fs.renameSync(tmp, indexPath(cfg));
|
|
71
|
-
}
|
|
72
|
-
function toResult(m) {
|
|
73
|
-
return {
|
|
74
|
-
id: m.id,
|
|
75
|
-
content: m.content,
|
|
76
|
-
metadata: { title: m.title, tags: m.tags, source: m.source, sourceUrl: m.sourceUrl, contentHash: m.contentHash, pinned: m.pinned, addedAt: m.addedAt, vaultKey: m.vaultKey },
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
function fileMemoryAdd(cfg, content, metadata, sourceUrl) {
|
|
80
|
-
const idx = readIndex(cfg);
|
|
81
|
-
const id = crypto.randomBytes(8).toString("hex");
|
|
82
|
-
const row = {
|
|
83
|
-
id,
|
|
84
|
-
content,
|
|
85
|
-
title: metadata.title,
|
|
86
|
-
tags: metadata.tags,
|
|
87
|
-
source: metadata.source,
|
|
88
|
-
sourceUrl,
|
|
89
|
-
contentHash: metadata.contentHash ?? "",
|
|
90
|
-
pinned: metadata.pinned,
|
|
91
|
-
addedAt: metadata.addedAt ?? Date.now(),
|
|
92
|
-
vaultKey: metadata.vaultKey,
|
|
93
|
-
};
|
|
94
|
-
idx.memories.push(row);
|
|
95
|
-
writeIndex(cfg, idx);
|
|
96
|
-
return { id };
|
|
97
|
-
}
|
|
98
|
-
/** Remove every memory row mirroring a given vault entry - called by
|
|
99
|
-
* vault_delete so its "PERMANENT... cannot be undone" claim is actually true,
|
|
100
|
-
* instead of leaving the content fully recoverable via memory_search. */
|
|
101
|
-
function fileMemoryDeleteByVaultKey(cfg, vaultKey) {
|
|
102
|
-
const idx = readIndex(cfg);
|
|
103
|
-
const next = idx.memories.filter((m) => m.vaultKey !== vaultKey);
|
|
104
|
-
const removed = idx.memories.length - next.length;
|
|
105
|
-
if (removed > 0)
|
|
106
|
-
writeIndex(cfg, { memories: next });
|
|
107
|
-
return removed;
|
|
108
|
-
}
|
|
109
|
-
function fileMemorySearch(cfg, query, limit) {
|
|
110
|
-
const idx = readIndex(cfg);
|
|
111
|
-
const terms = (0, _text_search_js_1.meaningfulTerms)(query);
|
|
112
|
-
if (terms.length === 0)
|
|
113
|
-
return [];
|
|
114
|
-
const scored = [];
|
|
115
|
-
for (const m of idx.memories) {
|
|
116
|
-
const hay = `${m.title ?? ""}\n${(m.tags ?? []).join(" ")}\n${m.content}`.toLowerCase();
|
|
117
|
-
let score = 0;
|
|
118
|
-
for (const t of terms) {
|
|
119
|
-
if ((m.title ?? "").toLowerCase().includes(t))
|
|
120
|
-
score += 3;
|
|
121
|
-
if ((m.tags ?? []).some((tag) => tag.toLowerCase().includes(t)))
|
|
122
|
-
score += 2;
|
|
123
|
-
score += Math.min((0, _text_search_js_1.wordOccurrences)(hay, t), 5);
|
|
124
|
-
}
|
|
125
|
-
if (score > 0)
|
|
126
|
-
scored.push({ m, score });
|
|
127
|
-
}
|
|
128
|
-
scored.sort((a, b) => b.score - a.score);
|
|
129
|
-
const top = scored.slice(0, limit);
|
|
130
|
-
const maxScore = top[0]?.score || 1;
|
|
131
|
-
return top.map(({ m, score }) => ({ ...toResult(m), score: score / maxScore }));
|
|
132
|
-
}
|
|
133
|
-
function fileMemoryList(cfg, limit, tag) {
|
|
134
|
-
const idx = readIndex(cfg);
|
|
135
|
-
let rows = [...idx.memories].sort((a, b) => b.addedAt - a.addedAt);
|
|
136
|
-
if (tag)
|
|
137
|
-
rows = rows.filter((m) => (m.tags ?? []).includes(tag));
|
|
138
|
-
return rows.slice(0, limit).map(toResult);
|
|
139
|
-
}
|
|
140
|
-
function fileMemoryDelete(cfg, id) {
|
|
141
|
-
const idx = readIndex(cfg);
|
|
142
|
-
const next = idx.memories.filter((m) => m.id !== id);
|
|
143
|
-
if (next.length === idx.memories.length)
|
|
144
|
-
throw new Error(`Memory not found: ${id}`);
|
|
145
|
-
writeIndex(cfg, { memories: next });
|
|
146
|
-
}
|
|
147
|
-
function fileMemoryProfile(cfg) {
|
|
148
|
-
const idx = readIndex(cfg);
|
|
149
|
-
return { total: idx.memories.length, status: "ok", space: "local" };
|
|
150
|
-
}
|
package/dist/local-memory.js
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getLocalMemoryConfig = getLocalMemoryConfig;
|
|
4
|
-
exports.isLocalMemoryReachable = isLocalMemoryReachable;
|
|
5
|
-
exports.localMemoryAdd = localMemoryAdd;
|
|
6
|
-
exports.localMemorySearch = localMemorySearch;
|
|
7
|
-
exports.localMemoryList = localMemoryList;
|
|
8
|
-
exports.localMemoryDelete = localMemoryDelete;
|
|
9
|
-
exports.localMemoryDeleteByVaultKey = localMemoryDeleteByVaultKey;
|
|
10
|
-
exports.localMemoryProfile = localMemoryProfile;
|
|
11
|
-
const config_js_1 = require("./config.js");
|
|
12
|
-
const local_memory_file_js_1 = require("./local-memory-file.js");
|
|
13
|
-
const DEFAULT_URL = "http://localhost:6767";
|
|
14
|
-
const REACHABILITY_TIMEOUT_MS = 1500;
|
|
15
|
-
const CALL_TIMEOUT_MS = 15000;
|
|
16
|
-
// Returns config only when the user has explicitly opted into a local
|
|
17
|
-
// backend. Callers should still treat a null return as "use Convex" - this
|
|
18
|
-
// never throws.
|
|
19
|
-
function getLocalMemoryConfig() {
|
|
20
|
-
try {
|
|
21
|
-
const cfg = (0, config_js_1.readConfig)();
|
|
22
|
-
if (cfg.memoryBackend === "local-file")
|
|
23
|
-
return { kind: "file", ...(0, local_memory_file_js_1.getLocalMemoryFileConfig)() };
|
|
24
|
-
if (cfg.memoryBackend === "local" && cfg.supermemoryApiKey) {
|
|
25
|
-
return { kind: "supermemory", url: cfg.supermemoryUrl ?? DEFAULT_URL, apiKey: cfg.supermemoryApiKey };
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
// Requires a genuine 2xx, not just "something answered" - a stale/wrong
|
|
34
|
-
// supermemoryApiKey returns 401 (server is up, but every real memory call
|
|
35
|
-
// will fail the same way), and that must show as unhealthy, not healthy.
|
|
36
|
-
// The file backend is always "reachable" - it's a directory, not a server.
|
|
37
|
-
async function isLocalMemoryReachable(cfg) {
|
|
38
|
-
if (cfg.kind === "file")
|
|
39
|
-
return true;
|
|
40
|
-
try {
|
|
41
|
-
const res = await fetch(`${cfg.url}/v3/search`, {
|
|
42
|
-
method: "POST",
|
|
43
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
44
|
-
body: JSON.stringify({ q: "__finch_reachability_check__", limit: 1 }),
|
|
45
|
-
signal: AbortSignal.timeout(REACHABILITY_TIMEOUT_MS),
|
|
46
|
-
});
|
|
47
|
-
return res.ok;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
async function localMemoryAdd(cfg, content, metadata, sourceUrl) {
|
|
54
|
-
if (cfg.kind === "file")
|
|
55
|
-
return (0, local_memory_file_js_1.fileMemoryAdd)(cfg, content, metadata, sourceUrl);
|
|
56
|
-
const res = await fetch(`${cfg.url}/v3/documents`, {
|
|
57
|
-
method: "POST",
|
|
58
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
59
|
-
body: JSON.stringify({ content, metadata, ...(sourceUrl ? { sourceUrl } : {}) }),
|
|
60
|
-
signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
|
|
61
|
-
});
|
|
62
|
-
if (!res.ok)
|
|
63
|
-
throw new Error(`local supermemory add failed: HTTP ${res.status}`);
|
|
64
|
-
const data = await res.json();
|
|
65
|
-
return { id: data.id ?? data.documentId ?? "saved" };
|
|
66
|
-
}
|
|
67
|
-
async function localMemorySearch(cfg, query, limit) {
|
|
68
|
-
if (cfg.kind === "file")
|
|
69
|
-
return (0, local_memory_file_js_1.fileMemorySearch)(cfg, query, limit);
|
|
70
|
-
const res = await fetch(`${cfg.url}/v3/search`, {
|
|
71
|
-
method: "POST",
|
|
72
|
-
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
73
|
-
body: JSON.stringify({ q: query, limit }),
|
|
74
|
-
signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
|
|
75
|
-
});
|
|
76
|
-
if (!res.ok)
|
|
77
|
-
throw new Error(`local supermemory search failed: HTTP ${res.status}`);
|
|
78
|
-
const data = await res.json();
|
|
79
|
-
return (data.results ?? []).map((r) => ({
|
|
80
|
-
id: r.id ?? r.documentId ?? "",
|
|
81
|
-
content: r.content ?? r.chunks?.map((c) => c.content ?? "").join("\n") ?? "",
|
|
82
|
-
metadata: r.metadata ?? {},
|
|
83
|
-
score: r.score,
|
|
84
|
-
}));
|
|
85
|
-
}
|
|
86
|
-
// supermemory's search endpoint doubles as "list": a wildcard query returns
|
|
87
|
-
// recent documents. No dedicated /list endpoint is documented for the local
|
|
88
|
-
// server, so callers pass "*" and post-filter by tag client-side, matching
|
|
89
|
-
// how the Convex-side /memory/list already does its own tag post-filter.
|
|
90
|
-
async function localMemoryList(cfg, limit, tag) {
|
|
91
|
-
if (cfg.kind === "file")
|
|
92
|
-
return (0, local_memory_file_js_1.fileMemoryList)(cfg, limit, tag);
|
|
93
|
-
const rows = await localMemorySearch(cfg, "*", Math.max(limit * (tag ? 3 : 1), limit));
|
|
94
|
-
const filtered = tag ? rows.filter((r) => Array.isArray(r.metadata?.tags) && r.metadata.tags.includes(tag)) : rows;
|
|
95
|
-
return filtered.slice(0, limit);
|
|
96
|
-
}
|
|
97
|
-
// Delete endpoint isn't confirmed in the public self-hosting docs at the
|
|
98
|
-
// time this was written - throws a distinct error so callers can surface a
|
|
99
|
-
// clear "not supported locally yet" message instead of a generic failure.
|
|
100
|
-
async function localMemoryDelete(cfg, id) {
|
|
101
|
-
if (cfg.kind === "file") {
|
|
102
|
-
(0, local_memory_file_js_1.fileMemoryDelete)(cfg, id);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
const res = await fetch(`${cfg.url}/v3/documents/${encodeURIComponent(id)}`, {
|
|
106
|
-
method: "DELETE",
|
|
107
|
-
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
108
|
-
signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
|
|
109
|
-
});
|
|
110
|
-
if (!res.ok)
|
|
111
|
-
throw new Error(`local supermemory delete failed: HTTP ${res.status}`);
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Remove memory rows mirroring a deleted vault entry - called by vault_delete.
|
|
115
|
-
* "file" backend: direct filter+delete, exact. Self-hosted supermemory server:
|
|
116
|
-
* no documented "delete by metadata filter" endpoint, so this is a no-op that
|
|
117
|
-
* returns 0 rather than guessing at an unconfirmed API - vault_delete surfaces
|
|
118
|
-
* that count so a 0 on that backend doesn't get silently mistaken for success.
|
|
119
|
-
*/
|
|
120
|
-
function localMemoryDeleteByVaultKey(cfg, vaultKey) {
|
|
121
|
-
if (cfg.kind === "file")
|
|
122
|
-
return (0, local_memory_file_js_1.fileMemoryDeleteByVaultKey)(cfg, vaultKey);
|
|
123
|
-
return 0;
|
|
124
|
-
}
|
|
125
|
-
// No dedicated count endpoint is documented for the local supermemory server,
|
|
126
|
-
// so that path approximates via a capped wildcard search - accurate up to
|
|
127
|
-
// `PROFILE_SAMPLE_LIMIT`, reported as a floor ("200+") beyond that rather
|
|
128
|
-
// than a false exact count. The file backend counts exactly.
|
|
129
|
-
const PROFILE_SAMPLE_LIMIT = 200;
|
|
130
|
-
async function localMemoryProfile(cfg) {
|
|
131
|
-
if (cfg.kind === "file")
|
|
132
|
-
return { ...(0, local_memory_file_js_1.fileMemoryProfile)(cfg), approximate: false };
|
|
133
|
-
const rows = await localMemorySearch(cfg, "*", PROFILE_SAMPLE_LIMIT);
|
|
134
|
-
return { total: rows.length, status: "ok", space: "local", approximate: rows.length >= PROFILE_SAMPLE_LIMIT };
|
|
135
|
-
}
|