@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/tools/vault.js
DELETED
|
@@ -1,1020 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VAULT_TOOLS = void 0;
|
|
4
|
-
exports.buildVaultList = buildVaultList;
|
|
5
|
-
exports.buildVaultSearch = buildVaultSearch;
|
|
6
|
-
exports.handleVaultTool = handleVaultTool;
|
|
7
|
-
const zod_1 = require("zod");
|
|
8
|
-
const convex_js_1 = require("../convex.js");
|
|
9
|
-
const memory_js_1 = require("./memory.js");
|
|
10
|
-
const local_vault_js_1 = require("../local-vault.js");
|
|
11
|
-
const local_memory_js_1 = require("../local-memory.js");
|
|
12
|
-
const project_js_1 = require("../project.js");
|
|
13
|
-
const VAULT_TYPES = ["research", "execution", "workflow", "prompt", "file", "memory", "code", "credential"];
|
|
14
|
-
exports.VAULT_TOOLS = [
|
|
15
|
-
{
|
|
16
|
-
name: "vault_save",
|
|
17
|
-
description: "Save or update a versioned artifact in Finch Vault. Same key = update (git-style: prior version snapshotted, patched to v+1). " +
|
|
18
|
-
"Types: research | execution | workflow | prompt | file | memory | code. " +
|
|
19
|
-
"Entries up to 10MB - content over 600KB auto-offloads to blob storage. " +
|
|
20
|
-
"For quick unstructured notes, use memory_add instead. For coding sessions specifically, " +
|
|
21
|
-
"prefer code_session_save - same versioning, but a structured template and auto-linking built in.",
|
|
22
|
-
inputSchema: {
|
|
23
|
-
type: "object",
|
|
24
|
-
properties: {
|
|
25
|
-
type: { type: "string", enum: [...VAULT_TYPES], description: "Entry type" },
|
|
26
|
-
title: { type: "string", description: "Human-readable title (auto-generated from content if omitted)" },
|
|
27
|
-
content: { type: "string", description: "Main content - markdown, JSON, code, or plain text" },
|
|
28
|
-
key: { type: "string", description: "Optional slug key e.g. 'research/btc-dominance-analysis'. Auto-generated if omitted." },
|
|
29
|
-
contentType: { type: "string", enum: ["markdown", "json", "text", "code"], description: "Content format hint" },
|
|
30
|
-
agentId: { type: "string", description: "Agent ID writing this entry" },
|
|
31
|
-
tags: { type: "array", items: { type: "string" }, description: "Tags for filtering and search" },
|
|
32
|
-
commitMsg: { type: "string", description: "Commit message for this version, e.g. 'initial research', 'refined with on-chain data'" },
|
|
33
|
-
metadata: { type: "string", description: "Optional JSON string for extra structured fields" },
|
|
34
|
-
workspaceProject: {
|
|
35
|
-
type: "string",
|
|
36
|
-
description: "Optional: file this entry into a named Finch workspace project (the same Projects a user " +
|
|
37
|
-
"organizes their Agents/vault into on the Agents page). Matched case-insensitively by name; " +
|
|
38
|
-
"created automatically if it doesn't exist yet. Not the same thing as a `key` path segment - " +
|
|
39
|
-
"this tags the entry in Finch's own project system. Hosted vault only (no effect in local-vault mode).",
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
required: ["type", "content"],
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: "code_session_save",
|
|
47
|
-
description: "Persist a coding/debugging session as a versioned Markdown snapshot in Finch Vault, keyed by " +
|
|
48
|
-
"project (`code/<project>`) - so the next session (yours, or another agent's) has real context " +
|
|
49
|
-
"instead of starting cold. Same project = new version, full history kept (git-style, like vault_save). " +
|
|
50
|
-
"Auto-links to related past code and research entries. " +
|
|
51
|
-
"Call this at the end of a substantive coding task - not for every single file read or trivial edit.",
|
|
52
|
-
inputSchema: {
|
|
53
|
-
type: "object",
|
|
54
|
-
properties: {
|
|
55
|
-
project: { type: "string", description: "Project or repo slug, e.g. 'finch-webapp', 'mcp-server'. Becomes the vault key: code/<project>." },
|
|
56
|
-
summary: { type: "string", description: "What was done this session - the task, the approach, the outcome." },
|
|
57
|
-
filesChanged: { type: "array", items: { type: "string" }, description: "Files touched, e.g. ['app/convex/vault.ts', 'app/src/App.tsx']" },
|
|
58
|
-
decisions: { type: "string", description: "Notable decisions or tradeoffs made and why - the part a future session can't re-derive from a diff alone." },
|
|
59
|
-
nextSteps: { type: "string", description: "What's left, or what to pick up next session." },
|
|
60
|
-
tags: { type: "array", items: { type: "string" }, description: "Extra tags for search, e.g. ['bugfix', 'refactor']" },
|
|
61
|
-
},
|
|
62
|
-
required: ["project", "summary"],
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
name: "vault_read",
|
|
67
|
-
description: "Read a Finch Vault entry by its key. Returns full content, version, tags, and any linked entries.",
|
|
68
|
-
inputSchema: {
|
|
69
|
-
type: "object",
|
|
70
|
-
properties: {
|
|
71
|
-
key: { type: "string", description: "Entry key e.g. 'research/btc-dominance-analysis'" },
|
|
72
|
-
},
|
|
73
|
-
required: ["key"],
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: "vault_list",
|
|
78
|
-
description: "List Finch Vault entries. Filter by type, agent, or pinned status. Returns previews, not full content.",
|
|
79
|
-
inputSchema: {
|
|
80
|
-
type: "object",
|
|
81
|
-
properties: {
|
|
82
|
-
type: { type: "string", enum: [...VAULT_TYPES], description: "Filter by type" },
|
|
83
|
-
agentId: { type: "string", description: "Filter by agent that wrote the entries" },
|
|
84
|
-
pinned: { type: "boolean", description: "Show only pinned entries" },
|
|
85
|
-
limit: { type: "number", description: "Max entries to return (default 50)" },
|
|
86
|
-
},
|
|
87
|
-
required: [],
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
name: "vault_search",
|
|
92
|
-
description: "Search Finch Vault using full-text (keyword) search over titles, content, and tags. " +
|
|
93
|
-
"This is lexical matching, not embeddings - exact and near words in your query rank " +
|
|
94
|
-
"highest, so short specific phrases work better than long abstract descriptions. " +
|
|
95
|
-
"Optionally filter by type. Returns ranked results with previews.",
|
|
96
|
-
inputSchema: {
|
|
97
|
-
type: "object",
|
|
98
|
-
properties: {
|
|
99
|
-
query: { type: "string", description: "Search query - specific keywords work better than abstract phrasing (this is full-text search, not semantic)" },
|
|
100
|
-
type: { type: "string", enum: [...VAULT_TYPES], description: "Narrow search to a specific type" },
|
|
101
|
-
limit: { type: "number", description: "Max results (default 20)" },
|
|
102
|
-
},
|
|
103
|
-
required: ["query"],
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: "vault_history",
|
|
108
|
-
description: "Get the full version history of a Finch Vault entry - like git log. " +
|
|
109
|
-
"Shows each version with its commit message, author agent, size, and timestamp.",
|
|
110
|
-
inputSchema: {
|
|
111
|
-
type: "object",
|
|
112
|
-
properties: {
|
|
113
|
-
key: { type: "string", description: "Entry key" },
|
|
114
|
-
},
|
|
115
|
-
required: ["key"],
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
name: "vault_diff",
|
|
120
|
-
description: "Compare two versions of a Finch Vault entry - like git diff. " +
|
|
121
|
-
"Shows lines added (+) and removed (-) between fromVersion and toVersion.",
|
|
122
|
-
inputSchema: {
|
|
123
|
-
type: "object",
|
|
124
|
-
properties: {
|
|
125
|
-
key: { type: "string", description: "Entry key" },
|
|
126
|
-
fromVersion: { type: "number", description: "Older version number" },
|
|
127
|
-
toVersion: { type: "number", description: "Newer version number" },
|
|
128
|
-
},
|
|
129
|
-
required: ["key", "fromVersion", "toVersion"],
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
name: "vault_export",
|
|
134
|
-
description: "Export your entire Finch Vault or a specific type as a structured bundle. " +
|
|
135
|
-
"Useful for archiving, syncing to GitHub, or passing context to another agent.",
|
|
136
|
-
inputSchema: {
|
|
137
|
-
type: "object",
|
|
138
|
-
properties: {
|
|
139
|
-
type: { type: "string", enum: [...VAULT_TYPES], description: "Export only this type (omit for full export)" },
|
|
140
|
-
},
|
|
141
|
-
required: [],
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
name: "vault_store_credential",
|
|
146
|
-
description: "Securely store an API key, token, or secret in your vault. " +
|
|
147
|
-
"Credentials are stored under type=credential and are excluded from normal search and export. " +
|
|
148
|
-
"Use this to keep API keys organized and accessible across agent sessions.",
|
|
149
|
-
inputSchema: {
|
|
150
|
-
type: "object",
|
|
151
|
-
properties: {
|
|
152
|
-
name: { type: "string", description: "Credential name, e.g. 'ALCHEMY_API_KEY', 'TELEGRAM_BOT_TOKEN'" },
|
|
153
|
-
value: { type: "string", description: "The secret value to store" },
|
|
154
|
-
description: { type: "string", description: "Optional note about this credential - what it's for, expiry, etc." },
|
|
155
|
-
},
|
|
156
|
-
required: ["name", "value"],
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
name: "vault_get_credential",
|
|
161
|
-
description: "Retrieve a stored credential from the vault by name. " +
|
|
162
|
-
"Only returns credentials owned by the authenticated user.",
|
|
163
|
-
inputSchema: {
|
|
164
|
-
type: "object",
|
|
165
|
-
properties: {
|
|
166
|
-
name: { type: "string", description: "Credential name as used in vault_store_credential" },
|
|
167
|
-
},
|
|
168
|
-
required: ["name"],
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
name: "vault_pin",
|
|
173
|
-
description: "Pin or unpin a Finch Vault entry. Pinned entries always appear first in vault_list and are " +
|
|
174
|
-
"prioritized in memory_context and search results. Use for your most important research, key prompts, or canonical references.",
|
|
175
|
-
inputSchema: {
|
|
176
|
-
type: "object",
|
|
177
|
-
properties: {
|
|
178
|
-
key: { type: "string", description: "Entry key to pin or unpin" },
|
|
179
|
-
pinned: { type: "boolean", description: "true to pin, false to unpin (default: true)" },
|
|
180
|
-
},
|
|
181
|
-
required: ["key"],
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
name: "vault_unpublish",
|
|
186
|
-
description: "Make a previously shared Finch Vault entry private again, removing it from the public community " +
|
|
187
|
-
"listing. Use this to reverse vault publishing or packet_share. Note that anyone who already " +
|
|
188
|
-
"copied the content while it was public still has it — unpublishing stops future discovery, " +
|
|
189
|
-
"it does not retract what was taken.",
|
|
190
|
-
inputSchema: {
|
|
191
|
-
type: "object",
|
|
192
|
-
properties: {
|
|
193
|
-
key: { type: "string", description: "Entry key to make private again, e.g. 'packets/daily-research'" },
|
|
194
|
-
},
|
|
195
|
-
required: ["key"],
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
name: "vault_delete",
|
|
200
|
-
description: "PERMANENT. Delete a Finch Vault entry and ALL of its version history — this cannot be undone. " +
|
|
201
|
-
"Requires confirm: true. Use vault_list to browse first, and show the user the exact entry " +
|
|
202
|
-
"(key + title) you are about to destroy before confirming.",
|
|
203
|
-
inputSchema: {
|
|
204
|
-
type: "object",
|
|
205
|
-
properties: {
|
|
206
|
-
key: { type: "string", description: "Entry key to delete permanently" },
|
|
207
|
-
confirm: { type: "boolean", description: "Must be true to delete. Guards against irreversible loss." },
|
|
208
|
-
},
|
|
209
|
-
required: ["key", "confirm"],
|
|
210
|
-
},
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
name: "vault_tag",
|
|
214
|
-
description: "Add or replace tags on an existing Finch Vault entry without modifying its content. " +
|
|
215
|
-
"Useful for organizing entries retroactively. Set replace=true to overwrite all existing tags.",
|
|
216
|
-
inputSchema: {
|
|
217
|
-
type: "object",
|
|
218
|
-
properties: {
|
|
219
|
-
key: { type: "string", description: "Entry key to update tags on" },
|
|
220
|
-
tags: { type: "array", items: { type: "string" }, description: "Tags to add (or replace if replace=true)" },
|
|
221
|
-
replace: { type: "boolean", description: "If true, replaces all existing tags. If false (default), merges with existing." },
|
|
222
|
-
},
|
|
223
|
-
required: ["key", "tags"],
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
name: "vault_link",
|
|
228
|
-
description: "Create a semantic relationship between two Finch Vault entries - building a knowledge graph. " +
|
|
229
|
-
"Relations: references | derived_from | supersedes | related | continues. " +
|
|
230
|
-
"Example: link a synthesis entry as 'derived_from' several research entries, or mark a newer analysis as 'supersedes' an older one. " +
|
|
231
|
-
"Duplicate links are updated in-place.",
|
|
232
|
-
inputSchema: {
|
|
233
|
-
type: "object",
|
|
234
|
-
properties: {
|
|
235
|
-
fromKey: { type: "string", description: "Source entry key" },
|
|
236
|
-
toKey: { type: "string", description: "Target entry key" },
|
|
237
|
-
relation: {
|
|
238
|
-
type: "string",
|
|
239
|
-
enum: ["references", "derived_from", "supersedes", "related", "continues"],
|
|
240
|
-
description: "How fromKey relates to toKey",
|
|
241
|
-
},
|
|
242
|
-
},
|
|
243
|
-
required: ["fromKey", "toKey", "relation"],
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
name: "vault_related",
|
|
248
|
-
description: "Traverse the Finch Vault knowledge graph - get all entries linked to a given entry. " +
|
|
249
|
-
"Returns both outbound links (entries this entry references) and inbound links (entries that reference this one). " +
|
|
250
|
-
"Filter by relation type to find only derived entries, superseded versions, continuations, etc.",
|
|
251
|
-
inputSchema: {
|
|
252
|
-
type: "object",
|
|
253
|
-
properties: {
|
|
254
|
-
key: { type: "string", description: "Entry key to find related entries for" },
|
|
255
|
-
relation: {
|
|
256
|
-
type: "string",
|
|
257
|
-
enum: ["references", "derived_from", "supersedes", "related", "continues"],
|
|
258
|
-
description: "Filter to only this relation type (omit for all relations)",
|
|
259
|
-
},
|
|
260
|
-
},
|
|
261
|
-
required: ["key"],
|
|
262
|
-
},
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
name: "list_projects",
|
|
266
|
-
description: "List your Finch workspace projects - the same Projects used to organize Agents and vault content on the " +
|
|
267
|
-
"webapp Agents page. Read-only. Check this before passing `workspaceProject` to vault_save/agent_spawn if " +
|
|
268
|
-
"you want to reuse an existing project rather than relying on the automatic case-insensitive name match. " +
|
|
269
|
-
"No effect / nothing to list in local-vault mode.",
|
|
270
|
-
inputSchema: { type: "object", properties: {}, required: [] },
|
|
271
|
-
},
|
|
272
|
-
];
|
|
273
|
-
// ─── Zod schemas ─────────────────────────────────────────────────────────────
|
|
274
|
-
const SaveSchema = zod_1.z.object({
|
|
275
|
-
type: zod_1.z.enum(VAULT_TYPES),
|
|
276
|
-
title: zod_1.z.string().optional(),
|
|
277
|
-
content: zod_1.z.string().min(1),
|
|
278
|
-
key: zod_1.z.string().optional(),
|
|
279
|
-
contentType: zod_1.z.enum(["markdown", "json", "text", "code"]).optional(),
|
|
280
|
-
agentId: zod_1.z.string().optional(),
|
|
281
|
-
tags: zod_1.z.array(zod_1.z.string()).optional(),
|
|
282
|
-
commitMsg: zod_1.z.string().optional(),
|
|
283
|
-
metadata: zod_1.z.string().optional(),
|
|
284
|
-
workspaceProject: zod_1.z.string().optional(),
|
|
285
|
-
});
|
|
286
|
-
const CodeSessionSchema = zod_1.z.object({
|
|
287
|
-
project: zod_1.z.string().min(1).max(80),
|
|
288
|
-
summary: zod_1.z.string().min(1),
|
|
289
|
-
filesChanged: zod_1.z.array(zod_1.z.string()).max(100).optional(),
|
|
290
|
-
decisions: zod_1.z.string().optional(),
|
|
291
|
-
nextSteps: zod_1.z.string().optional(),
|
|
292
|
-
tags: zod_1.z.array(zod_1.z.string()).optional(),
|
|
293
|
-
});
|
|
294
|
-
const ReadSchema = zod_1.z.object({ key: zod_1.z.string().min(1) });
|
|
295
|
-
const ListSchema = zod_1.z.object({
|
|
296
|
-
type: zod_1.z.enum(VAULT_TYPES).optional(),
|
|
297
|
-
agentId: zod_1.z.string().optional(),
|
|
298
|
-
pinned: zod_1.z.boolean().optional(),
|
|
299
|
-
limit: zod_1.z.number().optional(),
|
|
300
|
-
});
|
|
301
|
-
const SearchSchema = zod_1.z.object({
|
|
302
|
-
query: zod_1.z.string().min(1),
|
|
303
|
-
type: zod_1.z.enum(VAULT_TYPES).optional(),
|
|
304
|
-
limit: zod_1.z.number().optional(),
|
|
305
|
-
});
|
|
306
|
-
const HistorySchema = zod_1.z.object({ key: zod_1.z.string().min(1) });
|
|
307
|
-
const DiffSchema = zod_1.z.object({ key: zod_1.z.string().min(1), fromVersion: zod_1.z.number(), toVersion: zod_1.z.number() });
|
|
308
|
-
const ExportSchema = zod_1.z.object({ type: zod_1.z.enum(VAULT_TYPES).optional() });
|
|
309
|
-
const StoreCredentialSchema = zod_1.z.object({ name: zod_1.z.string().min(1), value: zod_1.z.string().min(1), description: zod_1.z.string().optional() });
|
|
310
|
-
const GetCredentialSchema = zod_1.z.object({ name: zod_1.z.string().min(1) });
|
|
311
|
-
const PinSchema = zod_1.z.object({ key: zod_1.z.string().min(1), pinned: zod_1.z.boolean().optional() });
|
|
312
|
-
const DeleteSchema = zod_1.z.object({ key: zod_1.z.string().min(1) });
|
|
313
|
-
const UnpublishSchema = zod_1.z.object({ key: zod_1.z.string().min(1) });
|
|
314
|
-
const TagSchema = zod_1.z.object({ key: zod_1.z.string().min(1), tags: zod_1.z.array(zod_1.z.string()).min(1), replace: zod_1.z.boolean().optional() });
|
|
315
|
-
const VAULT_RELATIONS = ["references", "derived_from", "supersedes", "related", "continues"];
|
|
316
|
-
const LinkSchema = zod_1.z.object({ fromKey: zod_1.z.string().min(1), toKey: zod_1.z.string().min(1), relation: zod_1.z.enum(VAULT_RELATIONS) });
|
|
317
|
-
const RelatedSchema = zod_1.z.object({ key: zod_1.z.string().min(1), relation: zod_1.z.enum(VAULT_RELATIONS).optional() });
|
|
318
|
-
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
319
|
-
function formatBytes(n) {
|
|
320
|
-
if (!n)
|
|
321
|
-
return "-";
|
|
322
|
-
if (n < 1024)
|
|
323
|
-
return `${n}B`;
|
|
324
|
-
if (n < 1024 * 1024)
|
|
325
|
-
return `${(n / 1024).toFixed(1)}KB`;
|
|
326
|
-
return `${(n / 1024 / 1024).toFixed(2)}MB`;
|
|
327
|
-
}
|
|
328
|
-
function formatDate(ts) {
|
|
329
|
-
return new Date(ts).toUTCString();
|
|
330
|
-
}
|
|
331
|
-
// ── Structured output builders (schemas in output-schemas.ts) ───────────────
|
|
332
|
-
function buildVaultList(entries, type) {
|
|
333
|
-
return {
|
|
334
|
-
type: type ?? null,
|
|
335
|
-
count: entries.length,
|
|
336
|
-
entries: entries.map((e) => ({
|
|
337
|
-
key: e.key,
|
|
338
|
-
title: e.title ?? null,
|
|
339
|
-
type: e.type ?? null,
|
|
340
|
-
version: e.version ?? null,
|
|
341
|
-
size: e.size ?? null,
|
|
342
|
-
updatedAt: e.updatedAt ?? null,
|
|
343
|
-
isPinned: !!e.isPinned,
|
|
344
|
-
})),
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
function buildVaultSearch(query, results) {
|
|
348
|
-
return {
|
|
349
|
-
query,
|
|
350
|
-
count: results.length,
|
|
351
|
-
results: results.map((r) => ({
|
|
352
|
-
key: r.key,
|
|
353
|
-
title: r.title ?? null,
|
|
354
|
-
type: r.type ?? null,
|
|
355
|
-
score: r.score ?? null,
|
|
356
|
-
preview: r.preview ?? null,
|
|
357
|
-
})),
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
// ─── Handler ─────────────────────────────────────────────────────────────────
|
|
361
|
-
async function handleVaultTool(name, args) {
|
|
362
|
-
// When the user has opted into a fully-local, user-owned vault
|
|
363
|
-
// (`vaultBackend: "local"`), every operation runs against ~/.finch/vault/
|
|
364
|
-
// on their own disk - no Convex, no account. Falls through to the hosted
|
|
365
|
-
// path when local isn't enabled. Same two-tier shape as local memory.
|
|
366
|
-
const localVault = (0, local_vault_js_1.getLocalVaultConfig)();
|
|
367
|
-
switch (name) {
|
|
368
|
-
case "vault_save": {
|
|
369
|
-
const parsed = SaveSchema.safeParse(args);
|
|
370
|
-
if (!parsed.success)
|
|
371
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
372
|
-
if (parsed.data.type === "credential") {
|
|
373
|
-
// vault_save writes plaintext to disk/DB - "credential" is only a
|
|
374
|
-
// valid FILTER value for vault_list/search/export (which correctly
|
|
375
|
-
// exclude it), never a valid type to actually SAVE through here.
|
|
376
|
-
// vault_store_credential is the only path that encrypts at rest.
|
|
377
|
-
return {
|
|
378
|
-
content: [{
|
|
379
|
-
type: "text",
|
|
380
|
-
text: "Use `vault_store_credential` to save a secret - it encrypts at rest (AES-256-GCM). " +
|
|
381
|
-
"`vault_save` writes plaintext, so `type: \"credential\"` is refused here.",
|
|
382
|
-
}],
|
|
383
|
-
isError: true,
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
// Auto-generate title from content if not provided
|
|
387
|
-
const firstLine = parsed.data.content.split("\n")[0].replace(/^#+\s*/, "").slice(0, 80);
|
|
388
|
-
const autoTitle = parsed.data.title ?? (firstLine || `${parsed.data.type} - ${new Date().toISOString().slice(0, 10)}`);
|
|
389
|
-
const { workspaceProject, ...rest } = parsed.data;
|
|
390
|
-
const savePayload = { ...rest, title: autoTitle };
|
|
391
|
-
// Resolve the project name -> id server-side (auto-creates on first use).
|
|
392
|
-
// Local vault has no project concept at all - workspaceProject is
|
|
393
|
-
// silently a no-op there rather than a confusing network error.
|
|
394
|
-
let resolvedProjectName = null;
|
|
395
|
-
if (workspaceProject && !localVault) {
|
|
396
|
-
const resolved = await (0, project_js_1.resolveProjectId)(workspaceProject);
|
|
397
|
-
if (resolved) {
|
|
398
|
-
savePayload.projectId = resolved.projectId;
|
|
399
|
-
resolvedProjectName = resolved.name;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
const data = localVault
|
|
403
|
-
? (0, local_vault_js_1.localVaultSave)(localVault, savePayload)
|
|
404
|
-
: await (0, convex_js_1.callConvex)("/vault/save", "POST", savePayload, "vault_save");
|
|
405
|
-
if (data.error)
|
|
406
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
407
|
-
const { key, version, changed } = data;
|
|
408
|
-
// Mirror to searchable memory (fire-and-forget). Skip when running a local
|
|
409
|
-
// vault WITHOUT local memory - otherwise the mirror would phone home to
|
|
410
|
-
// the hosted proxy (needs auth the local-only user doesn't have, and
|
|
411
|
-
// ships their data off-machine, defeating the whole point). vault_search
|
|
412
|
-
// already full-text-searches the local vault directly in that mode.
|
|
413
|
-
const mirrorToMemory = savePayload.type !== "credential" && (!localVault || !!(0, local_memory_js_1.getLocalMemoryConfig)());
|
|
414
|
-
if (mirrorToMemory) {
|
|
415
|
-
(0, memory_js_1.syncToSupermemory)(savePayload.content, {
|
|
416
|
-
vaultKey: key, title: autoTitle, type: savePayload.type,
|
|
417
|
-
tags: savePayload.tags, version, source: "vault_save",
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
// Surface inline-syntax results so the user sees [[wikilinks]] and
|
|
421
|
-
// #tags doing their work. Backend reports inlineLinksDetected,
|
|
422
|
-
// linksCreated, linksMissing[], inlineTagsExtracted.
|
|
423
|
-
const linkSummary = [];
|
|
424
|
-
if (typeof data.linksCreated === "number" && data.linksCreated > 0) {
|
|
425
|
-
linkSummary.push(`🔗 ${data.linksCreated} wikilink edge(s) created from \`[[...]]\``);
|
|
426
|
-
}
|
|
427
|
-
if (Array.isArray(data.linksMissing) && data.linksMissing.length > 0) {
|
|
428
|
-
linkSummary.push(`⚠️ Missing target(s): ${data.linksMissing.map((k) => `\`${k}\``).join(", ")} - save them later to backlink`);
|
|
429
|
-
}
|
|
430
|
-
if (typeof data.inlineTagsExtracted === "number" && data.inlineTagsExtracted > 0) {
|
|
431
|
-
linkSummary.push(`🏷️ ${data.inlineTagsExtracted} \`#tag\`(s) auto-extracted`);
|
|
432
|
-
}
|
|
433
|
-
if (data.blobStored) {
|
|
434
|
-
linkSummary.push(`📁 Large content (${Math.round((data.originalSize ?? 0) / 1024)}KB) stored as blob; chunked indexing in progress`);
|
|
435
|
-
}
|
|
436
|
-
const lines = [
|
|
437
|
-
`📦 **Vault ${changed ? (version === 1 ? "Created" : "Updated") : "Unchanged"}**`,
|
|
438
|
-
`Key: \`${key}\``,
|
|
439
|
-
`Version: v${version}`,
|
|
440
|
-
changed && version > 1 ? `Previous version auto-snapshotted.` : "",
|
|
441
|
-
resolvedProjectName ? `📁 Project: ${resolvedProjectName}` : "",
|
|
442
|
-
mirrorToMemory ? `🧠 Synced to searchable memory` : (localVault ? `💾 Stored locally at ~/.finch/vault` : ""),
|
|
443
|
-
...linkSummary,
|
|
444
|
-
``,
|
|
445
|
-
`Use \`vault_read\` to retrieve, \`vault_history\` to see all versions.`,
|
|
446
|
-
].filter(Boolean);
|
|
447
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
448
|
-
}
|
|
449
|
-
case "code_session_save": {
|
|
450
|
-
const parsed = CodeSessionSchema.safeParse(args);
|
|
451
|
-
if (!parsed.success)
|
|
452
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
453
|
-
const { project, summary, filesChanged, decisions, nextSteps, tags } = parsed.data;
|
|
454
|
-
const projectSlug = project.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "session";
|
|
455
|
-
const key = `code/${projectSlug}`;
|
|
456
|
-
const content = [
|
|
457
|
-
`# Code session: ${project}`,
|
|
458
|
-
``,
|
|
459
|
-
`_${new Date().toISOString()}_`,
|
|
460
|
-
``,
|
|
461
|
-
`## Summary`,
|
|
462
|
-
summary,
|
|
463
|
-
filesChanged?.length ? `\n## Files changed\n${filesChanged.map((f) => `- \`${f}\``).join("\n")}` : "",
|
|
464
|
-
decisions ? `\n## Decisions\n${decisions}` : "",
|
|
465
|
-
nextSteps ? `\n## Next steps\n${nextSteps}` : "",
|
|
466
|
-
].filter(Boolean).join("\n");
|
|
467
|
-
const savePayload = {
|
|
468
|
-
type: "code",
|
|
469
|
-
key,
|
|
470
|
-
title: `Code: ${project}`,
|
|
471
|
-
content,
|
|
472
|
-
contentType: "markdown",
|
|
473
|
-
agentId: "code-session",
|
|
474
|
-
tags: ["code-session", ...(tags ?? [])],
|
|
475
|
-
commitMsg: summary.slice(0, 80),
|
|
476
|
-
};
|
|
477
|
-
const data = localVault
|
|
478
|
-
? (0, local_vault_js_1.localVaultSave)(localVault, savePayload)
|
|
479
|
-
: await (0, convex_js_1.callConvex)("/vault/save", "POST", savePayload, "vault_save");
|
|
480
|
-
if (data.error)
|
|
481
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
482
|
-
const { version, changed } = data;
|
|
483
|
-
// Same mirror-to-memory rule as vault_save: skip only when local vault
|
|
484
|
-
// is on WITHOUT local memory, so query terms never phone home for a
|
|
485
|
-
// fully offline setup.
|
|
486
|
-
const mirrorToMemory = !localVault || !!(0, local_memory_js_1.getLocalMemoryConfig)();
|
|
487
|
-
if (mirrorToMemory) {
|
|
488
|
-
(0, memory_js_1.syncToSupermemory)(content, {
|
|
489
|
-
vaultKey: key, title: savePayload.title, type: "code",
|
|
490
|
-
tags: savePayload.tags, version, source: "code_session_save",
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
// Auto-link to related past code/research entries - same idea as
|
|
494
|
-
// deep_research's auto-linking, so a project's session history and any
|
|
495
|
-
// research that informed it stay connected instead of sitting as
|
|
496
|
-
// disconnected entries. Purely additive - never blocks the save.
|
|
497
|
-
const linked = [];
|
|
498
|
-
try {
|
|
499
|
-
const searchQuery = `${project} ${summary}`.slice(0, 200);
|
|
500
|
-
let hits = [];
|
|
501
|
-
if (localVault) {
|
|
502
|
-
const searchResult = (0, local_vault_js_1.localVaultSearch)(localVault, searchQuery, { limit: 6 });
|
|
503
|
-
hits = (searchResult.results ?? [])
|
|
504
|
-
.filter((r) => !!r.key && r.key !== key)
|
|
505
|
-
.map((r) => ({ key: r.key, title: r.title ?? "(untitled)" }));
|
|
506
|
-
}
|
|
507
|
-
else {
|
|
508
|
-
const searchResult = (await (0, convex_js_1.callConvex)("/vault/search", "POST", { q: searchQuery, n: 8 }, "vault_search"));
|
|
509
|
-
hits = (searchResult?.results ?? [])
|
|
510
|
-
.map((r) => {
|
|
511
|
-
const hitKey = r.metadata?.vaultKey ?? r.metadata?.key ?? null;
|
|
512
|
-
return hitKey ? { key: hitKey, title: r.metadata?.title ?? "(untitled)" } : null;
|
|
513
|
-
})
|
|
514
|
-
.filter((h) => !!h && h.key !== key);
|
|
515
|
-
}
|
|
516
|
-
for (const hit of hits.slice(0, 3)) {
|
|
517
|
-
try {
|
|
518
|
-
if (localVault)
|
|
519
|
-
(0, local_vault_js_1.localVaultLink)(localVault, key, hit.key, "related");
|
|
520
|
-
else
|
|
521
|
-
await (0, convex_js_1.callConvex)("/vault/link", "POST", { fromKey: key, toKey: hit.key, relation: "related" }, "vault_link");
|
|
522
|
-
linked.push(hit.key);
|
|
523
|
-
}
|
|
524
|
-
catch { /* skip individual link failures */ }
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
catch { /* auto-link is purely additive */ }
|
|
528
|
-
const lines = [
|
|
529
|
-
`📦 **Code session ${changed ? (version === 1 ? "saved" : "updated") : "unchanged"}** - \`${key}\` (v${version})`,
|
|
530
|
-
filesChanged?.length ? `Files: ${filesChanged.length}` : "",
|
|
531
|
-
linked.length ? `🔗 Linked to ${linked.length} related entr${linked.length === 1 ? "y" : "ies"}: ${linked.map((k) => `\`${k}\``).join(", ")}` : "",
|
|
532
|
-
mirrorToMemory ? `🧠 Synced to searchable memory` : "",
|
|
533
|
-
``,
|
|
534
|
-
`Next session: \`vault_read key="${key}"\` for the latest state, or \`vault_history key="${key}"\` for the full timeline.`,
|
|
535
|
-
].filter(Boolean);
|
|
536
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
537
|
-
}
|
|
538
|
-
case "vault_read": {
|
|
539
|
-
const parsed = ReadSchema.safeParse(args);
|
|
540
|
-
if (!parsed.success)
|
|
541
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
542
|
-
const vaultReadKey = parsed.data.key;
|
|
543
|
-
let data;
|
|
544
|
-
try {
|
|
545
|
-
data = localVault
|
|
546
|
-
? (0, local_vault_js_1.localVaultRead)(localVault, vaultReadKey)
|
|
547
|
-
: await (0, convex_js_1.callConvex)(`/vault/entry?key=${encodeURIComponent(vaultReadKey)}`, "GET", undefined, "vault_read");
|
|
548
|
-
}
|
|
549
|
-
catch (e) {
|
|
550
|
-
const msg = String(e?.message ?? e).toLowerCase();
|
|
551
|
-
const searchTerms = vaultReadKey.split("/").pop()?.replace(/-/g, " ") ?? vaultReadKey;
|
|
552
|
-
if (msg.includes("404") || msg.includes("not found")) {
|
|
553
|
-
return { content: [{ type: "text", text: [
|
|
554
|
-
`vault_read: entry \`${vaultReadKey}\` not found.`,
|
|
555
|
-
``,
|
|
556
|
-
`Try searching for it:`,
|
|
557
|
-
`- \`vault_search query="${searchTerms}"\` — full-text search across all entries`,
|
|
558
|
-
`- \`vault_list\` — browse all entries`,
|
|
559
|
-
`- \`vault_search query="${vaultReadKey.split("/")[0]}"\` — search by type prefix`,
|
|
560
|
-
].join("\n") }], isError: true };
|
|
561
|
-
}
|
|
562
|
-
if (msg.includes("401") || msg.includes("auth") || msg.includes("unauthorized")) {
|
|
563
|
-
return { content: [{ type: "text", text: `vault_read: not authenticated. Run \`finch login\` to sign in.` }], isError: true };
|
|
564
|
-
}
|
|
565
|
-
return { content: [{ type: "text", text: `vault_read failed: ${e?.message ?? e}` }], isError: true };
|
|
566
|
-
}
|
|
567
|
-
if (data?.error) {
|
|
568
|
-
const err = String(data.error).toLowerCase();
|
|
569
|
-
if (err.includes("not found") || err.includes("404") || err.includes("no entry")) {
|
|
570
|
-
const searchTerms = vaultReadKey.split("/").pop()?.replace(/-/g, " ") ?? vaultReadKey;
|
|
571
|
-
return { content: [{ type: "text", text: [
|
|
572
|
-
`vault_read: entry \`${vaultReadKey}\` not found.`,
|
|
573
|
-
`Try: vault_search query="${searchTerms}"`,
|
|
574
|
-
`Or: vault_list`,
|
|
575
|
-
].join("\n") }], isError: true };
|
|
576
|
-
}
|
|
577
|
-
return { content: [{ type: "text", text: `vault_read error: ${data.error}` }], isError: true };
|
|
578
|
-
}
|
|
579
|
-
// NOTE: there is no blob-storage tier on the backend (see
|
|
580
|
-
// app/convex/vault.ts MAX_CONTENT_BYTES comment) - oversized content is
|
|
581
|
-
// rejected at save time, not offloaded to file storage, so `contentFileId`
|
|
582
|
-
// never comes back on a vault entry. A `/vault/blob` fallback used to live
|
|
583
|
-
// here but the route was never registered in http.ts either, so it was
|
|
584
|
-
// dead in both directions - removed rather than fixed against a storage
|
|
585
|
-
// tier that doesn't exist. Re-add only alongside building that tier.
|
|
586
|
-
const fullContent = data.content ?? "";
|
|
587
|
-
const sizeLabel = data.originalSize ? formatBytes(data.originalSize) : formatBytes(data.size);
|
|
588
|
-
const backlinksBlock = Array.isArray(data.backlinks) && data.backlinks.length > 0
|
|
589
|
-
? `\n🔙 Linked from (${data.backlinks.length}):\n${data.backlinks.map((b) => ` ← \`${b.key}\`${b.title ? ` - ${b.title}` : ""}`).join("\n")}`
|
|
590
|
-
: "";
|
|
591
|
-
// A raw projectId means nothing to a reader - resolve it to a name.
|
|
592
|
-
// One extra call only when the entry is actually tagged; skipped
|
|
593
|
-
// entirely for the common case (unassigned) and in local-vault mode.
|
|
594
|
-
let projectLabel = "";
|
|
595
|
-
if (data.projectId && !localVault) {
|
|
596
|
-
try {
|
|
597
|
-
const projData = await (0, convex_js_1.callConvex)("/projects/list", "GET", undefined, "list_projects");
|
|
598
|
-
const match = projData?.projects?.find((p) => p.id === data.projectId);
|
|
599
|
-
if (match)
|
|
600
|
-
projectLabel = `📁 Project: ${match.name}`;
|
|
601
|
-
}
|
|
602
|
-
catch { /* best-effort - never block the read over this */ }
|
|
603
|
-
}
|
|
604
|
-
const lines = [
|
|
605
|
-
`📂 **${data.title}**`,
|
|
606
|
-
`Key: \`${data.key}\` · Type: ${data.type} · v${data.version} · ${sizeLabel}${data.contentFileId ? " · blob" : ""}`,
|
|
607
|
-
data.tags?.length ? `Tags: ${data.tags.join(", ")}` : "",
|
|
608
|
-
data.isPinned ? "📌 Pinned" : "",
|
|
609
|
-
data.agentId ? `Agent: ${data.agentId}` : "",
|
|
610
|
-
projectLabel,
|
|
611
|
-
`Updated: ${formatDate(data.updatedAt)}`,
|
|
612
|
-
data.linkedKeys?.length ? `\nLinks out:\n${data.linkedKeys.map((l) => ` → ${l}`).join("\n")}` : "",
|
|
613
|
-
backlinksBlock,
|
|
614
|
-
``,
|
|
615
|
-
`---`,
|
|
616
|
-
``,
|
|
617
|
-
fullContent,
|
|
618
|
-
].filter((l) => l !== "");
|
|
619
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
620
|
-
}
|
|
621
|
-
case "vault_list": {
|
|
622
|
-
const parsed = ListSchema.safeParse(args ?? {});
|
|
623
|
-
if (!parsed.success)
|
|
624
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
625
|
-
const params = new URLSearchParams();
|
|
626
|
-
if (parsed.data.type)
|
|
627
|
-
params.set("type", parsed.data.type);
|
|
628
|
-
if (parsed.data.agentId)
|
|
629
|
-
params.set("agentId", parsed.data.agentId);
|
|
630
|
-
if (parsed.data.pinned !== undefined)
|
|
631
|
-
params.set("pinned", String(parsed.data.pinned));
|
|
632
|
-
if (parsed.data.limit)
|
|
633
|
-
params.set("limit", String(parsed.data.limit));
|
|
634
|
-
const data = localVault
|
|
635
|
-
? (0, local_vault_js_1.localVaultList)(localVault, parsed.data)
|
|
636
|
-
: await (0, convex_js_1.callConvex)(`/vault/list?${params}`, "GET", undefined, "vault_list");
|
|
637
|
-
if (data.error)
|
|
638
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
639
|
-
const entries = data.entries ?? [];
|
|
640
|
-
if (!entries.length) {
|
|
641
|
-
return {
|
|
642
|
-
content: [{ type: "text", text: `No vault entries found${parsed.data.type ? ` of type '${parsed.data.type}'` : ""}.` }],
|
|
643
|
-
structuredContent: buildVaultList([], parsed.data.type),
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
const header = `📚 **Finch Vault** (${entries.length} entries)`;
|
|
647
|
-
const rows = entries.map((e) => `${e.isPinned ? "📌 " : ""}[\`${e.key}\`] ${e.title} - v${e.version} · ${e.type} · ${formatBytes(e.size)} · ${formatDate(e.updatedAt)}`);
|
|
648
|
-
return {
|
|
649
|
-
content: [{ type: "text", text: [header, "", ...rows].join("\n") }],
|
|
650
|
-
structuredContent: buildVaultList(entries, parsed.data.type),
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
case "vault_search": {
|
|
654
|
-
const parsed = SearchSchema.safeParse(args);
|
|
655
|
-
if (!parsed.success)
|
|
656
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
657
|
-
// Full-text search, proxied through Convex (searchSupermemory is a
|
|
658
|
-
// legacy name - it calls Finch's own /memory/search endpoint, not a
|
|
659
|
-
// third-party semantic service; there is no embedding step here). Large vault
|
|
660
|
-
// entries are indexed as multiple chunks tagged with isVaultChunk +
|
|
661
|
-
// vaultKey - group chunks back to their parent entry so the result
|
|
662
|
-
// list shows one row per entry, not one row per chunk.
|
|
663
|
-
//
|
|
664
|
-
// Skipped entirely when vaultBackend is local - a local vault's whole
|
|
665
|
-
// point is "no network," so the query string must never leave the
|
|
666
|
-
// machine, not even to check for results before falling back to the
|
|
667
|
-
// (always-local) full-text branch below.
|
|
668
|
-
if (!localVault) {
|
|
669
|
-
const limit = parsed.data.limit ?? 20;
|
|
670
|
-
// Over-fetch so that after chunk dedup we still have ~limit rows.
|
|
671
|
-
const smResults = await (0, memory_js_1.searchSupermemory)(parsed.data.query, Math.min(50, limit * 3));
|
|
672
|
-
if (smResults.length > 0) {
|
|
673
|
-
const filtered = parsed.data.type
|
|
674
|
-
? smResults.filter(r => r.metadata?.type === parsed.data.type)
|
|
675
|
-
: smResults;
|
|
676
|
-
if (filtered.length > 0) {
|
|
677
|
-
const groups = new Map();
|
|
678
|
-
for (const r of filtered) {
|
|
679
|
-
const isChunk = r.metadata?.isVaultChunk === true;
|
|
680
|
-
const groupKey = isChunk ? (r.metadata?.vaultKey ?? r.id) : r.id;
|
|
681
|
-
const existing = groups.get(groupKey);
|
|
682
|
-
const score = r.score ?? 0;
|
|
683
|
-
const preview = (r.content ?? "").slice(0, 200).replace(/\n/g, " ");
|
|
684
|
-
const title = r.metadata?.title ?? r.content.slice(0, 60);
|
|
685
|
-
const type = r.metadata?.type ?? (isChunk ? "vault" : "memory");
|
|
686
|
-
if (!existing) {
|
|
687
|
-
groups.set(groupKey, {
|
|
688
|
-
key: groupKey,
|
|
689
|
-
title,
|
|
690
|
-
type,
|
|
691
|
-
bestScore: score,
|
|
692
|
-
bestPreview: preview,
|
|
693
|
-
chunkHits: 1,
|
|
694
|
-
isVaultChunk: isChunk,
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
existing.chunkHits += 1;
|
|
699
|
-
if (score > existing.bestScore) {
|
|
700
|
-
existing.bestScore = score;
|
|
701
|
-
existing.bestPreview = preview;
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
const grouped = Array.from(groups.values())
|
|
706
|
-
.sort((a, b) => b.bestScore - a.bestScore)
|
|
707
|
-
.slice(0, limit);
|
|
708
|
-
const header = `🔍 **Vault Search**: "${parsed.data.query}" - ${grouped.length} entry/entries`;
|
|
709
|
-
const rows = grouped.map((g, i) => {
|
|
710
|
-
const score = g.bestScore ? ` ${(g.bestScore * 100).toFixed(0)}%` : "";
|
|
711
|
-
const chunkBadge = g.isVaultChunk && g.chunkHits > 1
|
|
712
|
-
? ` · ${g.chunkHits} chunk hits`
|
|
713
|
-
: "";
|
|
714
|
-
return [
|
|
715
|
-
`${i + 1}.${score} [\`${g.key}\`] **${g.title}** (${g.type}${chunkBadge})`,
|
|
716
|
-
` ${g.bestPreview}${g.bestPreview.length >= 200 ? "…" : ""}`,
|
|
717
|
-
].join("\n");
|
|
718
|
-
});
|
|
719
|
-
return {
|
|
720
|
-
content: [{ type: "text", text: [header, "", ...rows].join("\n") }],
|
|
721
|
-
structuredContent: buildVaultSearch(parsed.data.query, grouped.map((g) => ({ key: g.key, title: g.title, type: g.type, score: g.bestScore, preview: g.bestPreview }))),
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
// Fallback: local full-text (user-owned vault) or Convex full-text.
|
|
727
|
-
const params = new URLSearchParams({ q: parsed.data.query });
|
|
728
|
-
if (parsed.data.type)
|
|
729
|
-
params.set("type", parsed.data.type);
|
|
730
|
-
if (parsed.data.limit)
|
|
731
|
-
params.set("limit", String(parsed.data.limit));
|
|
732
|
-
const data = localVault
|
|
733
|
-
? (0, local_vault_js_1.localVaultSearch)(localVault, parsed.data.query, { type: parsed.data.type, limit: parsed.data.limit })
|
|
734
|
-
: await (0, convex_js_1.callConvex)(`/vault/search?${params}`, "GET", undefined, "vault_search");
|
|
735
|
-
if (data.error)
|
|
736
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
737
|
-
const results = data.results ?? [];
|
|
738
|
-
if (!results.length) {
|
|
739
|
-
return {
|
|
740
|
-
content: [{ type: "text", text: `No vault entries found for: "${parsed.data.query}"` }],
|
|
741
|
-
structuredContent: buildVaultSearch(parsed.data.query, []),
|
|
742
|
-
};
|
|
743
|
-
}
|
|
744
|
-
const header = `🔍 **Vault Search**: "${parsed.data.query}" - ${results.length} result(s)`;
|
|
745
|
-
const rows = results.map((r, i) => [
|
|
746
|
-
`${i + 1}. [\`${r.key}\`] **${r.title}** (${r.type} · v${r.version})`,
|
|
747
|
-
` ${r.preview}`,
|
|
748
|
-
].join("\n"));
|
|
749
|
-
return {
|
|
750
|
-
content: [{ type: "text", text: [header, "", ...rows].join("\n") }],
|
|
751
|
-
structuredContent: buildVaultSearch(parsed.data.query, results),
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
case "vault_history": {
|
|
755
|
-
const parsed = HistorySchema.safeParse(args);
|
|
756
|
-
if (!parsed.success)
|
|
757
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
758
|
-
const histKey = parsed.data.key;
|
|
759
|
-
let data;
|
|
760
|
-
try {
|
|
761
|
-
data = localVault
|
|
762
|
-
? (0, local_vault_js_1.localVaultHistory)(localVault, histKey)
|
|
763
|
-
: await (0, convex_js_1.callConvex)(`/vault/history?key=${encodeURIComponent(histKey)}`, "GET", undefined, "vault_history");
|
|
764
|
-
}
|
|
765
|
-
catch (e) {
|
|
766
|
-
const msg = String(e?.message ?? e).toLowerCase();
|
|
767
|
-
const searchTerms = histKey.split("/").pop()?.replace(/-/g, " ") ?? histKey;
|
|
768
|
-
if (msg.includes("404") || msg.includes("not found")) {
|
|
769
|
-
return { content: [{ type: "text", text: [
|
|
770
|
-
`vault_history: entry \`${histKey}\` not found.`,
|
|
771
|
-
`Try: vault_search query="${searchTerms}"`,
|
|
772
|
-
`Or: vault_list`,
|
|
773
|
-
].join("\n") }], isError: true };
|
|
774
|
-
}
|
|
775
|
-
if (msg.includes("401") || msg.includes("unauthorized")) {
|
|
776
|
-
return { content: [{ type: "text", text: `vault_history: not authenticated. Run \`finch login\`.` }], isError: true };
|
|
777
|
-
}
|
|
778
|
-
return { content: [{ type: "text", text: `vault_history failed: ${e?.message ?? e}` }], isError: true };
|
|
779
|
-
}
|
|
780
|
-
if (data?.error) {
|
|
781
|
-
return { content: [{ type: "text", text: `vault_history failed: ${data.error}` }], isError: true };
|
|
782
|
-
}
|
|
783
|
-
const { key, title, currentVersion, history } = data;
|
|
784
|
-
const header = [
|
|
785
|
-
`📜 **History**: ${title}`,
|
|
786
|
-
`Key: \`${key}\` · Current: v${currentVersion}`,
|
|
787
|
-
``,
|
|
788
|
-
`| Version | Commit | Agent | Size | Date |`,
|
|
789
|
-
`|---------|--------|-------|------|------|`,
|
|
790
|
-
];
|
|
791
|
-
const rows = history.map((v) => `| v${v.version} | ${v.commitMsg ?? "-"} | ${v.agentId ?? "-"} | ${formatBytes(v.size)} | ${formatDate(v.createdAt)} |`);
|
|
792
|
-
return { content: [{ type: "text", text: [...header, ...rows].join("\n") }] };
|
|
793
|
-
}
|
|
794
|
-
case "vault_diff": {
|
|
795
|
-
const parsed = DiffSchema.safeParse(args);
|
|
796
|
-
if (!parsed.success)
|
|
797
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
798
|
-
const { key, fromVersion, toVersion } = parsed.data;
|
|
799
|
-
let data;
|
|
800
|
-
try {
|
|
801
|
-
data = localVault
|
|
802
|
-
? (0, local_vault_js_1.localVaultDiff)(localVault, key, fromVersion, toVersion)
|
|
803
|
-
: await (0, convex_js_1.callConvex)(`/vault/diff?key=${encodeURIComponent(key)}&from=${fromVersion}&to=${toVersion}`, "GET", undefined, "vault_diff");
|
|
804
|
-
}
|
|
805
|
-
catch (e) {
|
|
806
|
-
const msg = String(e?.message ?? e).toLowerCase();
|
|
807
|
-
const searchTerms = key.split("/").pop()?.replace(/-/g, " ") ?? key;
|
|
808
|
-
if (msg.includes("404") || msg.includes("not found")) {
|
|
809
|
-
return { content: [{ type: "text", text: [
|
|
810
|
-
`vault_diff: entry \`${key}\` not found.`,
|
|
811
|
-
`Try: vault_search query="${searchTerms}"`,
|
|
812
|
-
`Or: vault_history key="${key}" to see available versions`,
|
|
813
|
-
].join("\n") }], isError: true };
|
|
814
|
-
}
|
|
815
|
-
if (msg.includes("version") || msg.includes("range")) {
|
|
816
|
-
return { content: [{ type: "text", text: [
|
|
817
|
-
`vault_diff: version range v${fromVersion}→v${toVersion} invalid for \`${key}\`.`,
|
|
818
|
-
`Check available versions: vault_history key="${key}"`,
|
|
819
|
-
].join("\n") }], isError: true };
|
|
820
|
-
}
|
|
821
|
-
if (msg.includes("401") || msg.includes("unauthorized")) {
|
|
822
|
-
return { content: [{ type: "text", text: `vault_diff: not authenticated. Run \`finch login\`.` }], isError: true };
|
|
823
|
-
}
|
|
824
|
-
return { content: [{ type: "text", text: `vault_diff failed: ${e?.message ?? e}` }], isError: true };
|
|
825
|
-
}
|
|
826
|
-
if (data?.error) {
|
|
827
|
-
return { content: [{ type: "text", text: `vault_diff failed: ${data.error}` }], isError: true };
|
|
828
|
-
}
|
|
829
|
-
const lines = [
|
|
830
|
-
`📝 **Diff**: \`${data.key}\` - v${fromVersion} → v${toVersion}`,
|
|
831
|
-
``,
|
|
832
|
-
"```diff",
|
|
833
|
-
data.diff,
|
|
834
|
-
"```",
|
|
835
|
-
];
|
|
836
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
837
|
-
}
|
|
838
|
-
case "vault_export": {
|
|
839
|
-
const parsed = ExportSchema.safeParse(args ?? {});
|
|
840
|
-
if (!parsed.success)
|
|
841
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
842
|
-
const params = parsed.data.type ? `?type=${parsed.data.type}` : "";
|
|
843
|
-
const data = localVault
|
|
844
|
-
? (0, local_vault_js_1.localVaultExport)(localVault, parsed.data.type)
|
|
845
|
-
: await (0, convex_js_1.callConvex)(`/vault/export${params}`, "GET", undefined, "vault_export");
|
|
846
|
-
if (data.error)
|
|
847
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
848
|
-
const { exportedAt, totalEntries, entries } = data;
|
|
849
|
-
const header = [
|
|
850
|
-
`📤 **Vault Export**`,
|
|
851
|
-
`Exported: ${formatDate(exportedAt)} · ${totalEntries} entries${parsed.data.type ? ` (type: ${parsed.data.type})` : ""}`,
|
|
852
|
-
``,
|
|
853
|
-
];
|
|
854
|
-
const rows = entries.map((e) => `**[\`${e.key}\`]** ${e.title} (${e.type} · v${e.version})\n${e.content.slice(0, 500)}${e.content.length > 500 ? "\n…" : ""}`);
|
|
855
|
-
return { content: [{ type: "text", text: [...header, ...rows.join("\n\n---\n\n").split("\n")].join("\n") }] };
|
|
856
|
-
}
|
|
857
|
-
case "vault_store_credential": {
|
|
858
|
-
const parsed = StoreCredentialSchema.safeParse(args);
|
|
859
|
-
if (!parsed.success)
|
|
860
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
861
|
-
const data = localVault
|
|
862
|
-
? (0, local_vault_js_1.localVaultStoreCredential)(localVault, parsed.data.name, parsed.data.value, parsed.data.description)
|
|
863
|
-
: await (0, convex_js_1.callConvex)("/vault/credential/store", "POST", parsed.data, "vault_store_credential");
|
|
864
|
-
if (data.error)
|
|
865
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
866
|
-
return { content: [{ type: "text", text: `🔐 Credential stored: \`${data.name}\`\nKey: \`${data.key}\`\nRetrieve with: \`vault_get_credential name: "${data.name}"\`` }] };
|
|
867
|
-
}
|
|
868
|
-
case "vault_get_credential": {
|
|
869
|
-
const parsed = GetCredentialSchema.safeParse(args);
|
|
870
|
-
if (!parsed.success)
|
|
871
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
872
|
-
const params = new URLSearchParams({ name: parsed.data.name });
|
|
873
|
-
let data;
|
|
874
|
-
try {
|
|
875
|
-
data = localVault
|
|
876
|
-
? (0, local_vault_js_1.localVaultGetCredential)(localVault, parsed.data.name)
|
|
877
|
-
: await (0, convex_js_1.callConvex)(`/vault/credential?${params}`, "GET", undefined, "vault_get_credential");
|
|
878
|
-
}
|
|
879
|
-
catch {
|
|
880
|
-
return { content: [{ type: "text", text: `vault_get_credential: no credential named \`${parsed.data.name}\`.` }], isError: true };
|
|
881
|
-
}
|
|
882
|
-
if (data.error)
|
|
883
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
884
|
-
const lines = [`🔐 **${data.name}**`, `Value: \`${data.value}\``];
|
|
885
|
-
if (data.description)
|
|
886
|
-
lines.push(`Note: ${data.description}`);
|
|
887
|
-
if (data.storedAt)
|
|
888
|
-
lines.push(`Stored: ${data.storedAt}`);
|
|
889
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
890
|
-
}
|
|
891
|
-
case "vault_pin": {
|
|
892
|
-
const parsed = PinSchema.safeParse(args);
|
|
893
|
-
if (!parsed.success)
|
|
894
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
895
|
-
const { key, pinned = true } = parsed.data;
|
|
896
|
-
const data = localVault
|
|
897
|
-
? (0, local_vault_js_1.localVaultPin)(localVault, key, pinned)
|
|
898
|
-
: await (0, convex_js_1.callConvex)("/vault/pin", "POST", { key, pinned }, "vault_pin");
|
|
899
|
-
if (data.error)
|
|
900
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
901
|
-
return { content: [{ type: "text", text: pinned ? `📌 Pinned: \`${key}\`` : `📌 Unpinned: \`${key}\`` }] };
|
|
902
|
-
}
|
|
903
|
-
case "vault_unpublish": {
|
|
904
|
-
const parsed = UnpublishSchema.safeParse(args);
|
|
905
|
-
if (!parsed.success)
|
|
906
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
907
|
-
// Publishing is a hosted/marketplace concept - a local vault is private
|
|
908
|
-
// by construction, so there is nothing to retract.
|
|
909
|
-
if (localVault) {
|
|
910
|
-
return { content: [{ type: "text", text: `🔒 \`${parsed.data.key}\` is in your local vault — already private, nothing to unpublish. (Publishing only applies to the hosted vault.)` }] };
|
|
911
|
-
}
|
|
912
|
-
const data = await (0, convex_js_1.callConvex)("/vault/unpublish", "POST", { key: parsed.data.key }, "vault_unpublish");
|
|
913
|
-
if (data.error)
|
|
914
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
915
|
-
return {
|
|
916
|
-
content: [{
|
|
917
|
-
type: "text",
|
|
918
|
-
text: `🔒 **Private again:** \`${parsed.data.key}\`\n\n` +
|
|
919
|
-
`Removed from the public community listing. Anyone who copied it while it was public ` +
|
|
920
|
-
`still has that copy — this stops discovery, not distribution.`,
|
|
921
|
-
}],
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
case "vault_delete": {
|
|
925
|
-
const parsed = DeleteSchema.safeParse(args);
|
|
926
|
-
if (!parsed.success)
|
|
927
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
928
|
-
if (args?.confirm !== true) {
|
|
929
|
-
return {
|
|
930
|
-
content: [{
|
|
931
|
-
type: "text",
|
|
932
|
-
text: "Refusing to delete: this permanently removes the entry **and its entire version " +
|
|
933
|
-
"history**, and cannot be undone. Show the user the exact entry, then pass `confirm: true`.",
|
|
934
|
-
}],
|
|
935
|
-
isError: true,
|
|
936
|
-
};
|
|
937
|
-
}
|
|
938
|
-
const data = localVault
|
|
939
|
-
? (0, local_vault_js_1.localVaultDelete)(localVault, parsed.data.key)
|
|
940
|
-
: await (0, convex_js_1.callConvex)("/vault/delete", "POST", { key: parsed.data.key }, "vault_delete");
|
|
941
|
-
if (data.error)
|
|
942
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
943
|
-
// vault_save mirrors non-credential entries into memory for search - a
|
|
944
|
-
// "PERMANENT... cannot be undone" delete that leaves that mirror intact
|
|
945
|
-
// is not actually permanent. Only relevant for the local memory-file
|
|
946
|
-
// backend (the hosted Convex path cleans its own memories table inside
|
|
947
|
-
// the /vault/delete mutation itself, same request, no separate call).
|
|
948
|
-
let memoriesRemoved = 0;
|
|
949
|
-
const localMem = (0, local_memory_js_1.getLocalMemoryConfig)();
|
|
950
|
-
if (localMem) {
|
|
951
|
-
memoriesRemoved = (0, local_memory_js_1.localMemoryDeleteByVaultKey)(localMem, parsed.data.key);
|
|
952
|
-
}
|
|
953
|
-
else if (typeof data.memoriesRemoved === "number") {
|
|
954
|
-
memoriesRemoved = data.memoriesRemoved;
|
|
955
|
-
}
|
|
956
|
-
const memoryNote = memoriesRemoved > 0 ? ` + ${memoriesRemoved} memory mirror${memoriesRemoved === 1 ? "" : "s"} removed` : "";
|
|
957
|
-
return { content: [{ type: "text", text: `🗑️ Deleted: \`${parsed.data.key}\` (${data.versionsRemoved ?? 0} versions removed${memoryNote})` }] };
|
|
958
|
-
}
|
|
959
|
-
case "vault_tag": {
|
|
960
|
-
const parsed = TagSchema.safeParse(args);
|
|
961
|
-
if (!parsed.success)
|
|
962
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
963
|
-
const { key, tags, replace = false } = parsed.data;
|
|
964
|
-
const data = localVault
|
|
965
|
-
? (0, local_vault_js_1.localVaultTag)(localVault, key, tags, replace)
|
|
966
|
-
: await (0, convex_js_1.callConvex)("/vault/tag", "POST", { key, tags, replace }, "vault_tag");
|
|
967
|
-
if (data.error)
|
|
968
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
969
|
-
return { content: [{ type: "text", text: `🏷️ Tags ${replace ? "set" : "updated"} on \`${key}\`: ${(data.tags ?? tags).join(", ")}` }] };
|
|
970
|
-
}
|
|
971
|
-
case "vault_link": {
|
|
972
|
-
const parsed = LinkSchema.safeParse(args);
|
|
973
|
-
if (!parsed.success)
|
|
974
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
975
|
-
const { fromKey, toKey, relation } = parsed.data;
|
|
976
|
-
const data = localVault
|
|
977
|
-
? (0, local_vault_js_1.localVaultLink)(localVault, fromKey, toKey, relation)
|
|
978
|
-
: await (0, convex_js_1.callConvex)("/vault/link", "POST", { fromKey, toKey, relation }, "vault_link");
|
|
979
|
-
if (data.error)
|
|
980
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
981
|
-
const action = data.updated ? "Updated link" : "Linked";
|
|
982
|
-
return { content: [{ type: "text", text: `🔗 ${action}: \`${fromKey}\` -[${relation}]→ \`${toKey}\`` }] };
|
|
983
|
-
}
|
|
984
|
-
case "vault_related": {
|
|
985
|
-
const parsed = RelatedSchema.safeParse(args);
|
|
986
|
-
if (!parsed.success)
|
|
987
|
-
return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
|
|
988
|
-
const { key, relation } = parsed.data;
|
|
989
|
-
const params = new URLSearchParams({ key });
|
|
990
|
-
if (relation)
|
|
991
|
-
params.set("relation", relation);
|
|
992
|
-
const data = (localVault
|
|
993
|
-
? (0, local_vault_js_1.localVaultRelated)(localVault, key, relation)
|
|
994
|
-
: await (0, convex_js_1.callConvex)(`/vault/related?${params}`, "GET", undefined, "vault_related"));
|
|
995
|
-
if (data.error)
|
|
996
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
997
|
-
const items = data.related ?? [];
|
|
998
|
-
if (!items.length)
|
|
999
|
-
return { content: [{ type: "text", text: `No related entries found for \`${key}\`${relation ? ` (relation: ${relation})` : ""}.` }] };
|
|
1000
|
-
const lines = items.map(r => `- **${r.title}** (\`${r.key}\`) [${r.type}] - ${r.direction} \`${r.relation}\``);
|
|
1001
|
-
return { content: [{ type: "text", text: `## Related entries for \`${key}\` (${items.length})\n\n${lines.join("\n")}` }] };
|
|
1002
|
-
}
|
|
1003
|
-
case "list_projects": {
|
|
1004
|
-
if (localVault) {
|
|
1005
|
-
return { content: [{ type: "text", text: "Local vault mode has no project concept - `workspaceProject` has no effect, and there's nothing to list here." }] };
|
|
1006
|
-
}
|
|
1007
|
-
const data = await (0, convex_js_1.callConvex)("/projects/list", "GET", undefined, "list_projects");
|
|
1008
|
-
if (data.error)
|
|
1009
|
-
return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
|
|
1010
|
-
const projects = data.projects ?? [];
|
|
1011
|
-
if (!projects.length) {
|
|
1012
|
-
return { content: [{ type: "text", text: "No projects yet. Pass `workspaceProject: \"name\"` to `vault_save` or `agent_spawn` and one is created automatically." }] };
|
|
1013
|
-
}
|
|
1014
|
-
const lines = projects.map((p) => `- **${p.name}** (\`${p.slug}\`)${p.description ? ` - ${p.description}` : ""}`);
|
|
1015
|
-
return { content: [{ type: "text", text: `📁 **Your projects** (${projects.length})\n\n${lines.join("\n")}` }] };
|
|
1016
|
-
}
|
|
1017
|
-
default:
|
|
1018
|
-
return null;
|
|
1019
|
-
}
|
|
1020
|
-
}
|