pinpoint-bot 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/pinpoint-bot.js +9 -0
- package/index.js +2592 -0
- package/package.json +43 -0
- package/src/llm.js +254 -0
- package/src/skills.js +163 -0
- package/src/tools.js +2013 -0
- package/test/skills-paths.test.js +24 -0
package/src/tools.js
ADDED
|
@@ -0,0 +1,2013 @@
|
|
|
1
|
+
// tools.js — Tool declarations, routing, intent grouping, and summaries
|
|
2
|
+
// Extracted from bot/index.js (Seg 22C modularization)
|
|
3
|
+
// CommonJS module — no shared state dependencies
|
|
4
|
+
|
|
5
|
+
const pathModule = require("path");
|
|
6
|
+
const { existsSync, statSync } = require("fs");
|
|
7
|
+
|
|
8
|
+
// --- Intent detection keywords → categories ---
|
|
9
|
+
const INTENT_KEYWORDS = {
|
|
10
|
+
image:
|
|
11
|
+
/photo|image|picture|jpg|png|face|person|selfie|detect|object|bounding|visual|heic|camera|screenshot|exif|metadata|gps|lens|aperture|iso|when.*taken|shot.*with|cull|score.*photo|rate.*photo|best.*photo|reject|keeper|group.*photo|segregat|categoriz|classify/i,
|
|
12
|
+
search: /find|search|where|which|document|file.*contain|look.*for|indexed/i,
|
|
13
|
+
data: /excel|csv|spreadsheet|column|row|data|analyze|chart|graph|pandas|filter|sort/i,
|
|
14
|
+
files: /move|copy|rename|delete|duplicate|folder|list|organize|clean.*up|batch|zip|unzip|compress|extract|archive/i,
|
|
15
|
+
write: /write|create|save|put.*file|store|\.txt|\.csv|\.json|\.md|pdf|merge|split|combine|generate/i,
|
|
16
|
+
media: /video|mp4|clip|frame|scene|audio|mp3|wav|voice|transcri|podcast|recording|speech|listen/i,
|
|
17
|
+
web: /download|url|web|search.*online|internet|website|news|weather|score|playing|match|price|stock|latest|current|today|tomorrow|yesterday|live|trending|release/i,
|
|
18
|
+
memory: /remember|forget|memory|preference/i,
|
|
19
|
+
code: /python|code|script|run|execute|program/i,
|
|
20
|
+
google: /email|mail|gmail|send.*mail|inbox|calendar|event|meeting|schedule|appointment|drive|upload.*drive|google/i,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// --- Skill file categories (maps intent → skill .md files) ---
|
|
24
|
+
const SKILL_CATEGORIES = {
|
|
25
|
+
image: [
|
|
26
|
+
"face-analysis.md",
|
|
27
|
+
"image-analysis.md",
|
|
28
|
+
"image-tools.md",
|
|
29
|
+
"visual-search.md",
|
|
30
|
+
"photo-cull.md",
|
|
31
|
+
"photo-group.md",
|
|
32
|
+
],
|
|
33
|
+
search: ["search.md"],
|
|
34
|
+
data: ["data-analysis.md"],
|
|
35
|
+
files: ["file-tools.md", "smart-ops.md", "archive-tools.md"],
|
|
36
|
+
write: ["write-create.md", "pdf-tools.md"],
|
|
37
|
+
media: ["video-search.md", "audio-search.md"],
|
|
38
|
+
web: ["web-search.md", "download.md"],
|
|
39
|
+
memory: ["memory.md"],
|
|
40
|
+
code: ["python.md"],
|
|
41
|
+
google: ["google-workspace.md"],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// --- Tool grouping: map each tool to intent categories (mirrors SKILL_CATEGORIES) ---
|
|
45
|
+
// Core tools are always included. Category tools added based on user message intent.
|
|
46
|
+
const CORE_TOOLS = new Set([
|
|
47
|
+
"search_documents",
|
|
48
|
+
"search_facts",
|
|
49
|
+
"read_document",
|
|
50
|
+
"read_file",
|
|
51
|
+
"list_files",
|
|
52
|
+
"find_file",
|
|
53
|
+
"send_file",
|
|
54
|
+
"get_status",
|
|
55
|
+
"calculate",
|
|
56
|
+
]);
|
|
57
|
+
const TOOL_GROUPS = {
|
|
58
|
+
search: ["search_history", "grep_files", "index_file", "find_file", "search_generated_files"],
|
|
59
|
+
image: [
|
|
60
|
+
"detect_faces",
|
|
61
|
+
"crop_face",
|
|
62
|
+
"find_person",
|
|
63
|
+
"find_person_by_face",
|
|
64
|
+
"count_faces",
|
|
65
|
+
"compare_faces",
|
|
66
|
+
"remember_face",
|
|
67
|
+
"forget_face",
|
|
68
|
+
"search_images_visual",
|
|
69
|
+
"ocr",
|
|
70
|
+
"image_metadata",
|
|
71
|
+
"score_photo",
|
|
72
|
+
"cull_photos",
|
|
73
|
+
"cull_status",
|
|
74
|
+
"suggest_categories",
|
|
75
|
+
"group_photos",
|
|
76
|
+
"group_status",
|
|
77
|
+
],
|
|
78
|
+
data: ["analyze_data", "read_excel", "generate_chart", "extract_tables", "pdf_to_excel"],
|
|
79
|
+
files: [
|
|
80
|
+
"file_info",
|
|
81
|
+
"move_file",
|
|
82
|
+
"copy_file",
|
|
83
|
+
"batch_move",
|
|
84
|
+
"create_folder",
|
|
85
|
+
"delete_file",
|
|
86
|
+
"find_duplicates",
|
|
87
|
+
"batch_rename",
|
|
88
|
+
"compress_files",
|
|
89
|
+
"extract_archive",
|
|
90
|
+
"search_generated_files",
|
|
91
|
+
"find_file",
|
|
92
|
+
],
|
|
93
|
+
write: [
|
|
94
|
+
"write_file",
|
|
95
|
+
"generate_excel",
|
|
96
|
+
"merge_pdf",
|
|
97
|
+
"split_pdf",
|
|
98
|
+
"pdf_to_images",
|
|
99
|
+
"images_to_pdf",
|
|
100
|
+
"compress_pdf",
|
|
101
|
+
"add_page_numbers",
|
|
102
|
+
"pdf_to_word",
|
|
103
|
+
"organize_pdf",
|
|
104
|
+
"pdf_to_excel",
|
|
105
|
+
"resize_image",
|
|
106
|
+
"convert_image",
|
|
107
|
+
"crop_image",
|
|
108
|
+
],
|
|
109
|
+
media: ["search_video", "extract_frame", "transcribe_audio", "search_audio"],
|
|
110
|
+
web: ["web_search", "download_url"],
|
|
111
|
+
memory: ["memory_save", "memory_search", "memory_delete", "memory_forget"],
|
|
112
|
+
code: ["run_python"],
|
|
113
|
+
archive: ["compress_files", "extract_archive"],
|
|
114
|
+
google: ["gmail_send", "gmail_search", "gmail_triage", "calendar_events", "calendar_create", "drive_list", "drive_upload"],
|
|
115
|
+
automation: ["set_reminder", "list_reminders", "cancel_reminder", "watch_folder", "unwatch_folder", "list_watched"],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Detect user intent → return relevant skill categories
|
|
119
|
+
function detectIntentCategories(message) {
|
|
120
|
+
const cats = new Set();
|
|
121
|
+
for (const [cat, regex] of Object.entries(INTENT_KEYWORDS)) {
|
|
122
|
+
if (regex.test(message)) cats.add(cat);
|
|
123
|
+
}
|
|
124
|
+
// Always include search (core functionality)
|
|
125
|
+
if (cats.size === 0) cats.add("search");
|
|
126
|
+
return cats;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Per-chat intent memory: carry forward intent for short follow-ups (Claude Code pattern)
|
|
130
|
+
const lastIntentCats = {}; // chatJid → Set of categories from last substantive message
|
|
131
|
+
|
|
132
|
+
// Build filtered tools array based on user message intent
|
|
133
|
+
// Returns [{ functionDeclarations: [...] }] for Gemini
|
|
134
|
+
function getToolsForIntent(message, chatJid) {
|
|
135
|
+
const cats = detectIntentCategories(message);
|
|
136
|
+
// For short follow-ups, merge with previous intent so tools carry over
|
|
137
|
+
// Claude Code always sends all tools; we approximate by carrying forward intent
|
|
138
|
+
if (chatJid && message.split(/\s+/).length <= 6) {
|
|
139
|
+
const prev = lastIntentCats[chatJid];
|
|
140
|
+
if (prev) for (const c of prev) cats.add(c);
|
|
141
|
+
}
|
|
142
|
+
// Action words in follow-ups always need files tools (move, create, organize)
|
|
143
|
+
if (/go ahead|do it|finish|start|proceed|execute|make it|confirm/i.test(message)) {
|
|
144
|
+
cats.add("files");
|
|
145
|
+
cats.add("write");
|
|
146
|
+
}
|
|
147
|
+
const allowedNames = new Set(CORE_TOOLS);
|
|
148
|
+
for (const cat of cats) {
|
|
149
|
+
for (const name of TOOL_GROUPS[cat] || []) allowedNames.add(name);
|
|
150
|
+
}
|
|
151
|
+
// automation tools always available (reminders, watch)
|
|
152
|
+
for (const name of TOOL_GROUPS.automation) allowedNames.add(name);
|
|
153
|
+
|
|
154
|
+
// Store intent for follow-ups (only for substantive messages)
|
|
155
|
+
if (chatJid && message.split(/\s+/).length > 3) {
|
|
156
|
+
lastIntentCats[chatJid] = cats;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const filtered = TOOL_DECLARATIONS.filter((fd) => allowedNames.has(fd.name));
|
|
160
|
+
console.log(`[Pinpoint] Tools: ${filtered.length}/${TOOL_DECLARATIONS.length} (intents: ${[...cats].join(",")})`);
|
|
161
|
+
return [{ functionDeclarations: filtered }];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Clear intent memory for a chat (called on session reset)
|
|
165
|
+
function clearIntentCache(chatJid) {
|
|
166
|
+
delete lastIntentCats[chatJid];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check if chat has active intent context (for follow-up detection)
|
|
170
|
+
function hasActiveIntent(chatJid) {
|
|
171
|
+
const cats = lastIntentCats[chatJid];
|
|
172
|
+
return cats && cats.size > 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- Tool declarations for Gemini ---
|
|
176
|
+
const TOOL_DECLARATIONS = [
|
|
177
|
+
{
|
|
178
|
+
name: "search_documents",
|
|
179
|
+
description:
|
|
180
|
+
"Search indexed documents by keywords. Returns the exact matching section/paragraph (not just filenames). PREFERRED way to answer questions about document content — always try this FIRST before read_document or read_file. Also searches indexed IMAGE CAPTIONS — use file_type='image' to find photos by description (free, instant). If the file is not indexed yet, use index_file first, then search. Can filter by file type and folder.",
|
|
181
|
+
parameters: {
|
|
182
|
+
type: "OBJECT",
|
|
183
|
+
properties: {
|
|
184
|
+
query: {
|
|
185
|
+
type: "STRING",
|
|
186
|
+
description: "Search keywords extracted from the user's message.",
|
|
187
|
+
},
|
|
188
|
+
file_type: {
|
|
189
|
+
type: "STRING",
|
|
190
|
+
description: "Filter by type: pdf, docx, xlsx, pptx, txt, csv, image, epub. Optional.",
|
|
191
|
+
},
|
|
192
|
+
folder: {
|
|
193
|
+
type: "STRING",
|
|
194
|
+
description: "Only search within this folder path. Optional.",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
required: ["query"],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "read_document",
|
|
202
|
+
description:
|
|
203
|
+
"Read the full text of a document by its ID. Use ONLY when search_documents snippet isn't detailed enough and you need broader context — like summarizing an entire document, comparing two full documents, or translating. For specific questions (what does clause 7 say, what's the depreciation amount), search_documents already returns the exact section.",
|
|
204
|
+
parameters: {
|
|
205
|
+
type: "OBJECT",
|
|
206
|
+
properties: {
|
|
207
|
+
document_id: {
|
|
208
|
+
type: "INTEGER",
|
|
209
|
+
description: "The document ID from search results.",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
required: ["document_id"],
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "read_excel",
|
|
217
|
+
description:
|
|
218
|
+
"Read specific cells or ranges from an Excel (.xlsx) file. Use when the user asks about specific cells, rows, columns, or ranges. For general search, use search_documents instead.",
|
|
219
|
+
parameters: {
|
|
220
|
+
type: "OBJECT",
|
|
221
|
+
properties: {
|
|
222
|
+
path: {
|
|
223
|
+
type: "STRING",
|
|
224
|
+
description: "Absolute path to the .xlsx file.",
|
|
225
|
+
},
|
|
226
|
+
sheet_name: {
|
|
227
|
+
type: "STRING",
|
|
228
|
+
description: "Sheet name. Optional — defaults to first sheet.",
|
|
229
|
+
},
|
|
230
|
+
cell_range: {
|
|
231
|
+
type: "STRING",
|
|
232
|
+
description: "Excel range like 'A1:D10', 'B5', 'A:A'. Optional — defaults to first 20 rows.",
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
required: ["path"],
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
name: "calculate",
|
|
240
|
+
description:
|
|
241
|
+
"Evaluate a mathematical expression. Supports +, -, *, /, **, %, parentheses, and functions like round(), abs(), min(), max(), sum(), sqrt(). Use for any arithmetic: sums, averages, percentages, conversions.",
|
|
242
|
+
parameters: {
|
|
243
|
+
type: "OBJECT",
|
|
244
|
+
properties: {
|
|
245
|
+
expression: {
|
|
246
|
+
type: "STRING",
|
|
247
|
+
description: "Math expression like '45230 * 0.18' or '(12000 + 8500 + 23000) / 3'.",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
required: ["expression"],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "list_files",
|
|
255
|
+
description:
|
|
256
|
+
"List files and folders in a directory. WORKFLOW: 1) Use sort_by='size' to find large files. 2) Use name_contains to search by filename. 3) Use recursive=true to search in subfolders. 4) Use filter_ext or filter_type to narrow by type. The response includes a 'largest' field when sorted by size. Do NOT call repeatedly with different params — if you can't find a file in the first result, try name_contains or recursive.",
|
|
257
|
+
parameters: {
|
|
258
|
+
type: "OBJECT",
|
|
259
|
+
properties: {
|
|
260
|
+
folder: {
|
|
261
|
+
type: "STRING",
|
|
262
|
+
description: "Folder path to list.",
|
|
263
|
+
},
|
|
264
|
+
sort_by: {
|
|
265
|
+
type: "STRING",
|
|
266
|
+
description: "Sort order: 'name' (default), 'date' (newest first), 'size' (largest first).",
|
|
267
|
+
},
|
|
268
|
+
filter_ext: {
|
|
269
|
+
type: "STRING",
|
|
270
|
+
description: "Filter by single extension like '.pdf', '.xlsx'. Optional.",
|
|
271
|
+
},
|
|
272
|
+
filter_type: {
|
|
273
|
+
type: "STRING",
|
|
274
|
+
description:
|
|
275
|
+
"Filter by category: 'image', 'document', 'spreadsheet', 'presentation', 'video', 'audio', 'archive'. Optional.",
|
|
276
|
+
},
|
|
277
|
+
name_contains: {
|
|
278
|
+
type: "STRING",
|
|
279
|
+
description:
|
|
280
|
+
"Search by filename containing this text (case-insensitive). E.g. 'invoice' finds 'Invoice_2024.pdf'.",
|
|
281
|
+
},
|
|
282
|
+
recursive: {
|
|
283
|
+
type: "BOOLEAN",
|
|
284
|
+
description: "Search subdirectories recursively. Default false. Use when file might be in a subfolder.",
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
required: ["folder"],
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
name: "find_file",
|
|
292
|
+
description:
|
|
293
|
+
"Find any file on the computer by filename. Searches a pre-built path registry of all files in common folders (Documents, Desktop, Downloads, Pictures, Videos). INSTANT — no folder scanning needed. Use this FIRST when a user asks about a file and you don't know which folder it's in. Can filter by extension. For files YOU created, also try search_generated_files.",
|
|
294
|
+
parameters: {
|
|
295
|
+
type: "OBJECT",
|
|
296
|
+
properties: {
|
|
297
|
+
query: {
|
|
298
|
+
type: "STRING",
|
|
299
|
+
description: "Filename to search for (case-insensitive). E.g. 'rent', 'invoice', 'budget'.",
|
|
300
|
+
},
|
|
301
|
+
ext: {
|
|
302
|
+
type: "STRING",
|
|
303
|
+
description: "Filter by extension: '.pdf', '.xlsx', '.docx'. Optional.",
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
required: ["query"],
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "search_generated_files",
|
|
311
|
+
description:
|
|
312
|
+
"Search files previously CREATED by Pinpoint tools (write_file, generate_excel, run_python, download_url, merge_pdf, etc.). Use when user asks about a file they asked you to create in a previous conversation — these files are NOT indexed in documents, so search_documents won't find them. Search by filename, description, or tool name.",
|
|
313
|
+
parameters: {
|
|
314
|
+
type: "OBJECT",
|
|
315
|
+
properties: {
|
|
316
|
+
query: {
|
|
317
|
+
type: "STRING",
|
|
318
|
+
description: "Search term — matches filename and description. E.g. 'rent', 'chart', 'excel'.",
|
|
319
|
+
},
|
|
320
|
+
tool_name: {
|
|
321
|
+
type: "STRING",
|
|
322
|
+
description: "Filter by tool: write_file, generate_excel, generate_chart, run_python, download_url, merge_pdf, split_pdf, etc. Optional.",
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
required: ["query"],
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
name: "grep_files",
|
|
330
|
+
description:
|
|
331
|
+
"Search INSIDE files for text content. Finds files containing a pattern and shows matching lines. Use when you need to find which file contains specific text (a name, phone number, keyword). Works on any text file — no indexing needed.",
|
|
332
|
+
parameters: {
|
|
333
|
+
type: "OBJECT",
|
|
334
|
+
properties: {
|
|
335
|
+
pattern: {
|
|
336
|
+
type: "STRING",
|
|
337
|
+
description: "Text pattern to search for inside files (case-insensitive).",
|
|
338
|
+
},
|
|
339
|
+
folder: {
|
|
340
|
+
type: "STRING",
|
|
341
|
+
description: "Folder to search in.",
|
|
342
|
+
},
|
|
343
|
+
file_filter: {
|
|
344
|
+
type: "STRING",
|
|
345
|
+
description: "Filter by file pattern, e.g. '*.txt', '*.csv', '*.log'. Optional.",
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
required: ["pattern", "folder"],
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: "file_info",
|
|
353
|
+
description:
|
|
354
|
+
"Get detailed information about a file or folder: size, creation date, modification date, file type, and whether it's indexed in Pinpoint's database.",
|
|
355
|
+
parameters: {
|
|
356
|
+
type: "OBJECT",
|
|
357
|
+
properties: {
|
|
358
|
+
path: {
|
|
359
|
+
type: "STRING",
|
|
360
|
+
description: "File or folder path.",
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
required: ["path"],
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "move_file",
|
|
368
|
+
description: "Move, copy, or rename a single file. For moving multiple files, use batch_move instead.",
|
|
369
|
+
parameters: {
|
|
370
|
+
type: "OBJECT",
|
|
371
|
+
properties: {
|
|
372
|
+
source: {
|
|
373
|
+
type: "STRING",
|
|
374
|
+
description: "Source file path.",
|
|
375
|
+
},
|
|
376
|
+
destination: {
|
|
377
|
+
type: "STRING",
|
|
378
|
+
description: "Destination path (file path or folder).",
|
|
379
|
+
},
|
|
380
|
+
copy: {
|
|
381
|
+
type: "BOOLEAN",
|
|
382
|
+
description: "If true, copy instead of move. Default: false.",
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
required: ["source", "destination"],
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "copy_file",
|
|
390
|
+
description: "Copy a file or folder to a new location.",
|
|
391
|
+
parameters: {
|
|
392
|
+
type: "OBJECT",
|
|
393
|
+
properties: {
|
|
394
|
+
source: { type: "STRING", description: "Source file or folder path." },
|
|
395
|
+
destination: { type: "STRING", description: "Destination path." },
|
|
396
|
+
},
|
|
397
|
+
required: ["source", "destination"],
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: "batch_move",
|
|
402
|
+
description:
|
|
403
|
+
"Move or copy multiple files to a destination folder in one call. Much faster than calling move_file repeatedly. Creates destination folder if needed.",
|
|
404
|
+
parameters: {
|
|
405
|
+
type: "OBJECT",
|
|
406
|
+
properties: {
|
|
407
|
+
sources: {
|
|
408
|
+
type: "ARRAY",
|
|
409
|
+
items: { type: "STRING" },
|
|
410
|
+
description: "List of source file paths to move/copy.",
|
|
411
|
+
},
|
|
412
|
+
destination: {
|
|
413
|
+
type: "STRING",
|
|
414
|
+
description: "Destination folder path. All files will be moved/copied here.",
|
|
415
|
+
},
|
|
416
|
+
is_copy: {
|
|
417
|
+
type: "BOOLEAN",
|
|
418
|
+
description: "If true, copy files instead of moving. Default: false (move).",
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
required: ["sources", "destination"],
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: "create_folder",
|
|
426
|
+
description: "Create a new folder (directory). Creates parent folders too if they don't exist.",
|
|
427
|
+
parameters: {
|
|
428
|
+
type: "OBJECT",
|
|
429
|
+
properties: {
|
|
430
|
+
path: {
|
|
431
|
+
type: "STRING",
|
|
432
|
+
description: "Folder path to create.",
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
required: ["path"],
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
name: "delete_file",
|
|
440
|
+
description:
|
|
441
|
+
"Delete a file. SAFETY: ALWAYS ask the user for explicit confirmation before deleting. Never delete without user approval. Cannot delete folders.",
|
|
442
|
+
parameters: {
|
|
443
|
+
type: "OBJECT",
|
|
444
|
+
properties: {
|
|
445
|
+
path: {
|
|
446
|
+
type: "STRING",
|
|
447
|
+
description: "File path to delete.",
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
required: ["path"],
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
name: "send_file",
|
|
455
|
+
description:
|
|
456
|
+
"Send a file to the user on WhatsApp. ONLY use this when the user explicitly asks to receive/send/share a file. Never send files automatically. Max 16MB for images, 100MB for documents. If too large, use resize_image or compress_files first.",
|
|
457
|
+
parameters: {
|
|
458
|
+
type: "OBJECT",
|
|
459
|
+
properties: {
|
|
460
|
+
path: {
|
|
461
|
+
type: "STRING",
|
|
462
|
+
description: "Absolute path of the file to send.",
|
|
463
|
+
},
|
|
464
|
+
caption: {
|
|
465
|
+
type: "STRING",
|
|
466
|
+
description: "Short caption for the file. Optional.",
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
required: ["path"],
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
name: "get_status",
|
|
474
|
+
description: "Get indexing statistics: total files indexed, count by file type, database size.",
|
|
475
|
+
parameters: {
|
|
476
|
+
type: "OBJECT",
|
|
477
|
+
properties: {},
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: "read_file",
|
|
482
|
+
description:
|
|
483
|
+
"Read a file from disk. For images: you SEE the image visually. If the user sent a photo, it is already visible to you — do NOT call read_file on it again. For documents (PDF, DOCX, TXT): prefer index_file + search_documents for searching. For Excel: use analyze_data instead.",
|
|
484
|
+
parameters: {
|
|
485
|
+
type: "OBJECT",
|
|
486
|
+
properties: {
|
|
487
|
+
path: {
|
|
488
|
+
type: "STRING",
|
|
489
|
+
description: "Absolute path to the file to read.",
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
required: ["path"],
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: "search_history",
|
|
497
|
+
description:
|
|
498
|
+
"Search past conversation messages from previous sessions. Use when the user refers to something discussed earlier that's not in the current conversation, like 'that file from yesterday' or 'what did I search for last time?'.",
|
|
499
|
+
parameters: {
|
|
500
|
+
type: "OBJECT",
|
|
501
|
+
properties: {
|
|
502
|
+
query: {
|
|
503
|
+
type: "STRING",
|
|
504
|
+
description: "Keywords to search for in past conversations.",
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
required: ["query"],
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
name: "detect_faces",
|
|
512
|
+
description:
|
|
513
|
+
"Detect and analyze faces in an image or all images in a folder. Returns face count, bounding boxes, confidence, age, gender, head pose. Pass folder for batch processing (one call instead of many).",
|
|
514
|
+
parameters: {
|
|
515
|
+
type: "OBJECT",
|
|
516
|
+
properties: {
|
|
517
|
+
image_path: {
|
|
518
|
+
type: "STRING",
|
|
519
|
+
description: "Absolute path to a single image file.",
|
|
520
|
+
},
|
|
521
|
+
folder: {
|
|
522
|
+
type: "STRING",
|
|
523
|
+
description: "Absolute path to folder — processes ALL images in it.",
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
name: "crop_face",
|
|
530
|
+
description:
|
|
531
|
+
"Crop a specific face from an image and save it as a separate file. Use this when detect_faces found multiple faces and you need to show them to the user so they can pick which person to search for. Returns the path to the cropped face image which you can send via send_file.",
|
|
532
|
+
parameters: {
|
|
533
|
+
type: "OBJECT",
|
|
534
|
+
properties: {
|
|
535
|
+
image_path: {
|
|
536
|
+
type: "STRING",
|
|
537
|
+
description: "Absolute path to the original image.",
|
|
538
|
+
},
|
|
539
|
+
face_idx: {
|
|
540
|
+
type: "INTEGER",
|
|
541
|
+
description: "Index of the face to crop (from detect_faces result).",
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
required: ["image_path", "face_idx"],
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
name: "find_person",
|
|
549
|
+
description:
|
|
550
|
+
"Find all photos of a specific person in a folder. The reference image should contain exactly ONE face. If the reference has multiple faces, first use detect_faces + crop_face to let the user pick, then use find_person_by_face instead. Scans all images in the folder and returns matching photos sorted by similarity. First scan may take a while (caches results for instant repeat searches).",
|
|
551
|
+
parameters: {
|
|
552
|
+
type: "OBJECT",
|
|
553
|
+
properties: {
|
|
554
|
+
reference_image: {
|
|
555
|
+
type: "STRING",
|
|
556
|
+
description: "Path to the reference image containing the person's face.",
|
|
557
|
+
},
|
|
558
|
+
folder: {
|
|
559
|
+
type: "STRING",
|
|
560
|
+
description: "Absolute path to the folder to search in.",
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
required: ["reference_image", "folder"],
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: "find_person_by_face",
|
|
568
|
+
description:
|
|
569
|
+
"Find all photos of a specific person using a face index from a multi-face reference image. Use this when the reference image has multiple faces and the user has picked which face to search for (via detect_faces + crop_face). The face_idx comes from detect_faces.",
|
|
570
|
+
parameters: {
|
|
571
|
+
type: "OBJECT",
|
|
572
|
+
properties: {
|
|
573
|
+
reference_image: {
|
|
574
|
+
type: "STRING",
|
|
575
|
+
description: "Path to the reference image.",
|
|
576
|
+
},
|
|
577
|
+
face_idx: {
|
|
578
|
+
type: "INTEGER",
|
|
579
|
+
description: "Index of the chosen face (from detect_faces).",
|
|
580
|
+
},
|
|
581
|
+
folder: {
|
|
582
|
+
type: "STRING",
|
|
583
|
+
description: "Absolute path to the folder to search in.",
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
required: ["reference_image", "face_idx", "folder"],
|
|
587
|
+
},
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
name: "count_faces",
|
|
591
|
+
description:
|
|
592
|
+
"Count faces in an image, a list of images, or all images in a folder. Returns face count, age/gender breakdown. Use paths array to batch multiple specific images in ONE call instead of calling per-image.",
|
|
593
|
+
parameters: {
|
|
594
|
+
type: "OBJECT",
|
|
595
|
+
properties: {
|
|
596
|
+
image_path: {
|
|
597
|
+
type: "STRING",
|
|
598
|
+
description: "Absolute path to a single image file.",
|
|
599
|
+
},
|
|
600
|
+
paths: {
|
|
601
|
+
type: "ARRAY",
|
|
602
|
+
items: { type: "STRING" },
|
|
603
|
+
description:
|
|
604
|
+
"Array of image paths to count faces in batch. Use this instead of calling count_faces per-image.",
|
|
605
|
+
},
|
|
606
|
+
folder: {
|
|
607
|
+
type: "STRING",
|
|
608
|
+
description: "Absolute path to folder — counts faces in ALL images in folder.",
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
name: "compare_faces",
|
|
615
|
+
description:
|
|
616
|
+
"Compare two specific faces from two images to check if they are the same person. Returns similarity score (0-1) and confidence level. Use when user asks 'is this the same person?' or wants to verify identity across photos.",
|
|
617
|
+
parameters: {
|
|
618
|
+
type: "OBJECT",
|
|
619
|
+
properties: {
|
|
620
|
+
image_path_1: {
|
|
621
|
+
type: "STRING",
|
|
622
|
+
description: "Path to the first image.",
|
|
623
|
+
},
|
|
624
|
+
face_idx_1: {
|
|
625
|
+
type: "INTEGER",
|
|
626
|
+
description: "Face index in first image (default 0 for first/only face).",
|
|
627
|
+
},
|
|
628
|
+
image_path_2: {
|
|
629
|
+
type: "STRING",
|
|
630
|
+
description: "Path to the second image.",
|
|
631
|
+
},
|
|
632
|
+
face_idx_2: {
|
|
633
|
+
type: "INTEGER",
|
|
634
|
+
description: "Face index in second image (default 0 for first/only face).",
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
required: ["image_path_1", "image_path_2"],
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: "remember_face",
|
|
642
|
+
description:
|
|
643
|
+
"Save a face for future recognition. After this, detect_faces will auto-identify this person in any photo. One person can have multiple saved faces (different angles improve accuracy). Use detect_faces first to get face_idx.",
|
|
644
|
+
parameters: {
|
|
645
|
+
type: "OBJECT",
|
|
646
|
+
properties: {
|
|
647
|
+
image_path: {
|
|
648
|
+
type: "STRING",
|
|
649
|
+
description: "Absolute path to the image containing the face.",
|
|
650
|
+
},
|
|
651
|
+
face_idx: {
|
|
652
|
+
type: "INTEGER",
|
|
653
|
+
description: "Index of the face to save (from detect_faces result). Default 0 for single-face images.",
|
|
654
|
+
},
|
|
655
|
+
name: {
|
|
656
|
+
type: "STRING",
|
|
657
|
+
description: "Name to associate with this face (e.g. 'Sharika', 'Dad').",
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
required: ["image_path", "name"],
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
name: "forget_face",
|
|
665
|
+
description:
|
|
666
|
+
"Delete all saved face data for a person. After this, they will no longer be auto-recognized by detect_faces.",
|
|
667
|
+
parameters: {
|
|
668
|
+
type: "OBJECT",
|
|
669
|
+
properties: {
|
|
670
|
+
name: {
|
|
671
|
+
type: "STRING",
|
|
672
|
+
description: "Name of the person to forget (case-insensitive).",
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
required: ["name"],
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
name: "ocr",
|
|
680
|
+
description:
|
|
681
|
+
"Extract text from an image or scanned PDF using OCR. Use this when you need the text as a string for processing. For just SEEING an image, use read_file instead (sends image visually). Pass folder for batch processing. After OCR, use index_file to make the text searchable.",
|
|
682
|
+
parameters: {
|
|
683
|
+
type: "OBJECT",
|
|
684
|
+
properties: {
|
|
685
|
+
path: {
|
|
686
|
+
type: "STRING",
|
|
687
|
+
description: "Absolute path to a single image or PDF file.",
|
|
688
|
+
},
|
|
689
|
+
folder: {
|
|
690
|
+
type: "STRING",
|
|
691
|
+
description: "Absolute path to folder — OCR ALL images and PDFs in it.",
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
name: "analyze_data",
|
|
698
|
+
description:
|
|
699
|
+
"Run pandas data analysis on CSV or Excel files. WORKFLOW: 1) FIRST call with operation='columns' to see all sheets, column names, types, and sample values. 2) Use operation='search' with query to find values across ALL sheets — auto-normalizes phone/ID formats (strips dashes, parens). 3) Use filter/groupby/sort when you know the exact column name. Operations: columns (schema+sheets), search (grep-like across all cells), describe, head, filter, value_counts, groupby, corr, sort, unique, shape, eval. For phone/ID lookups ALWAYS use search — it normalizes automatically. File is cached after first load (instant repeat calls).",
|
|
700
|
+
parameters: {
|
|
701
|
+
type: "OBJECT",
|
|
702
|
+
properties: {
|
|
703
|
+
path: {
|
|
704
|
+
type: "STRING",
|
|
705
|
+
description: "Absolute path to CSV or Excel file.",
|
|
706
|
+
},
|
|
707
|
+
operation: {
|
|
708
|
+
type: "STRING",
|
|
709
|
+
description:
|
|
710
|
+
"Operation: columns (FIRST — see sheets+types), search (find values across all sheets), describe, head, filter, value_counts, groupby, corr, sort, unique, shape, eval.",
|
|
711
|
+
},
|
|
712
|
+
columns: {
|
|
713
|
+
type: "STRING",
|
|
714
|
+
description: "Column name(s). For groupby use 'group_col:agg_col'. For sort prefix with '-' for descending.",
|
|
715
|
+
},
|
|
716
|
+
query: {
|
|
717
|
+
type: "STRING",
|
|
718
|
+
description:
|
|
719
|
+
"For search: value to find (e.g. '9208896630' — auto-normalizes phone formats). For filter: pandas query like 'amount > 1000'. For eval: expression like 'df.groupby(\"Cat\")[[\"Price\"]].sum()'.",
|
|
720
|
+
},
|
|
721
|
+
sheet: {
|
|
722
|
+
type: "STRING",
|
|
723
|
+
description:
|
|
724
|
+
"Sheet name for Excel files. Omit to use first sheet (or search ALL sheets with operation='search').",
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
required: ["path", "operation"],
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
name: "index_file",
|
|
732
|
+
description:
|
|
733
|
+
"Index a single file into the search database. Extracts text, chunks it into sections, and makes it searchable. Use this BEFORE search_documents when the file hasn't been indexed yet. After indexing, use search_documents to find specific sections within the file.",
|
|
734
|
+
parameters: {
|
|
735
|
+
type: "OBJECT",
|
|
736
|
+
properties: {
|
|
737
|
+
path: {
|
|
738
|
+
type: "STRING",
|
|
739
|
+
description: "Absolute path to the file to index.",
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
required: ["path"],
|
|
743
|
+
},
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
name: "write_file",
|
|
747
|
+
description:
|
|
748
|
+
"Create or write a text file. Can append to existing files. Use for creating notes, reports, summaries, text exports.",
|
|
749
|
+
parameters: {
|
|
750
|
+
type: "OBJECT",
|
|
751
|
+
properties: {
|
|
752
|
+
path: { type: "STRING", description: "Absolute path for the file." },
|
|
753
|
+
content: { type: "STRING", description: "Text content to write." },
|
|
754
|
+
append: { type: "BOOLEAN", description: "If true, append to existing file. Default: false." },
|
|
755
|
+
},
|
|
756
|
+
required: ["path", "content"],
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
name: "generate_excel",
|
|
761
|
+
description:
|
|
762
|
+
'Create a professionally formatted Excel file. Auto-applies: dark header with white text, alternating row stripes, auto-fit column widths, freeze panes, auto-filter. Optional title row above data.\n\nExample — expense report:\n title: "March 2026 Expenses"\n data: [{"Category":"Food","Amount":1200,"Date":"2026-03-01"},{"Category":"Transport","Amount":450,"Date":"2026-03-05"}]\n\nExample — contact list:\n data: [{"Name":"Alice","Phone":"555-1234","Email":"alice@example.com"}]\n\nTips: Use descriptive column names (not "col1"). Group related data. Add a title for context.',
|
|
763
|
+
parameters: {
|
|
764
|
+
type: "OBJECT",
|
|
765
|
+
properties: {
|
|
766
|
+
path: { type: "STRING", description: "Output path for .xlsx file." },
|
|
767
|
+
data: {
|
|
768
|
+
type: "ARRAY",
|
|
769
|
+
description: 'List of row objects. Keys become column headers. E.g. [{"Name":"Alice","Amount":100}].',
|
|
770
|
+
items: { type: "OBJECT" },
|
|
771
|
+
},
|
|
772
|
+
title: { type: "STRING", description: "Optional title displayed above the table (merged across columns, large bold text). E.g. 'Q1 Sales Report'." },
|
|
773
|
+
sheet_name: { type: "STRING", description: "Sheet name. Default: Sheet1." },
|
|
774
|
+
},
|
|
775
|
+
required: ["path", "data"],
|
|
776
|
+
},
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
name: "generate_chart",
|
|
780
|
+
description:
|
|
781
|
+
"Create a chart image (bar, line, pie, scatter, hist) from data using matplotlib. Returns image path — send via send_file.",
|
|
782
|
+
parameters: {
|
|
783
|
+
type: "OBJECT",
|
|
784
|
+
properties: {
|
|
785
|
+
data: { type: "OBJECT", description: 'Chart data: {"labels": [...], "values": [...]}.' },
|
|
786
|
+
chart_type: { type: "STRING", description: "Chart type: bar, line, pie, scatter, hist." },
|
|
787
|
+
title: { type: "STRING", description: "Chart title." },
|
|
788
|
+
xlabel: { type: "STRING", description: "X-axis label." },
|
|
789
|
+
ylabel: { type: "STRING", description: "Y-axis label." },
|
|
790
|
+
output_path: { type: "STRING", description: "Output image path. Optional." },
|
|
791
|
+
},
|
|
792
|
+
required: ["data", "chart_type"],
|
|
793
|
+
},
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
name: "merge_pdf",
|
|
797
|
+
description: "Combine multiple PDFs into one file. Use for merging invoices, reports, certificates.",
|
|
798
|
+
parameters: {
|
|
799
|
+
type: "OBJECT",
|
|
800
|
+
properties: {
|
|
801
|
+
paths: { type: "ARRAY", description: "List of PDF file paths to merge.", items: { type: "STRING" } },
|
|
802
|
+
output_path: { type: "STRING", description: "Output path for merged PDF." },
|
|
803
|
+
},
|
|
804
|
+
required: ["paths", "output_path"],
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
name: "split_pdf",
|
|
809
|
+
description: "Extract specific pages from a PDF. Page format: '1-5', '3,7,10', '1-3,5,8-10'.",
|
|
810
|
+
parameters: {
|
|
811
|
+
type: "OBJECT",
|
|
812
|
+
properties: {
|
|
813
|
+
path: { type: "STRING", description: "Source PDF path." },
|
|
814
|
+
pages: { type: "STRING", description: "Pages to extract: '1-5', '3,7', '1-3,5,8-10'." },
|
|
815
|
+
output_path: { type: "STRING", description: "Output path for extracted PDF." },
|
|
816
|
+
},
|
|
817
|
+
required: ["path", "pages", "output_path"],
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
name: "organize_pdf",
|
|
822
|
+
description:
|
|
823
|
+
"Reorder, duplicate, or remove PDF pages. Pass ordered list of page numbers to define output. E.g. [3,1,2] reverses first 3 pages; [1,1,2] duplicates page 1; omit pages to remove them.",
|
|
824
|
+
parameters: {
|
|
825
|
+
type: "OBJECT",
|
|
826
|
+
properties: {
|
|
827
|
+
path: { type: "STRING", description: "Source PDF path." },
|
|
828
|
+
pages: { type: "ARRAY", items: { type: "INTEGER" }, description: "Ordered list of 1-based page numbers for output." },
|
|
829
|
+
output_path: { type: "STRING", description: "Output path for reorganized PDF." },
|
|
830
|
+
},
|
|
831
|
+
required: ["path", "pages", "output_path"],
|
|
832
|
+
},
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
name: "pdf_to_excel",
|
|
836
|
+
description: "Extract tables from a PDF and save as Excel (.xlsx). Each table becomes a separate sheet. Works on native PDFs with structured tables.",
|
|
837
|
+
parameters: {
|
|
838
|
+
type: "OBJECT",
|
|
839
|
+
properties: {
|
|
840
|
+
path: { type: "STRING", description: "PDF file path." },
|
|
841
|
+
output_path: { type: "STRING", description: "Output .xlsx path. Default: same name with .xlsx." },
|
|
842
|
+
pages: { type: "STRING", description: "Page range: '1-5', '3', 'all'. Default: all." },
|
|
843
|
+
},
|
|
844
|
+
required: ["path"],
|
|
845
|
+
},
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
name: "pdf_to_images",
|
|
849
|
+
description: "Render PDF pages as PNG images. Returns list of image paths. Use to send PDF pages via WhatsApp.",
|
|
850
|
+
parameters: {
|
|
851
|
+
type: "OBJECT",
|
|
852
|
+
properties: {
|
|
853
|
+
path: { type: "STRING", description: "PDF file path." },
|
|
854
|
+
pages: { type: "STRING", description: "Pages to render: '1-5', '3,7'. Omit for all pages." },
|
|
855
|
+
dpi: { type: "INTEGER", description: "Resolution. Default 150." },
|
|
856
|
+
output_folder: { type: "STRING", description: "Folder for output images. Default: same folder as PDF." },
|
|
857
|
+
},
|
|
858
|
+
required: ["path"],
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
name: "images_to_pdf",
|
|
863
|
+
description: "Combine multiple images into a single PDF. Supports jpg, png, webp, bmp.",
|
|
864
|
+
parameters: {
|
|
865
|
+
type: "OBJECT",
|
|
866
|
+
properties: {
|
|
867
|
+
paths: { type: "ARRAY", items: { type: "STRING" }, description: "Array of image paths to combine." },
|
|
868
|
+
output_path: { type: "STRING", description: "Output PDF path." },
|
|
869
|
+
},
|
|
870
|
+
required: ["paths", "output_path"],
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
name: "compress_pdf",
|
|
875
|
+
description: "Compress a PDF to reduce file size. Removes unused objects and deflates streams. Reports size reduction %.",
|
|
876
|
+
parameters: {
|
|
877
|
+
type: "OBJECT",
|
|
878
|
+
properties: {
|
|
879
|
+
path: { type: "STRING", description: "PDF file path to compress." },
|
|
880
|
+
output_path: { type: "STRING", description: "Output path. Omit to overwrite original." },
|
|
881
|
+
},
|
|
882
|
+
required: ["path"],
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
name: "add_page_numbers",
|
|
887
|
+
description: "Add page numbers to every page of a PDF. Supports 'Page {n} of {total}' format.",
|
|
888
|
+
parameters: {
|
|
889
|
+
type: "OBJECT",
|
|
890
|
+
properties: {
|
|
891
|
+
path: { type: "STRING", description: "PDF file path." },
|
|
892
|
+
output_path: { type: "STRING", description: "Output path. Omit to overwrite original." },
|
|
893
|
+
position: { type: "STRING", description: "Position: bottom-left, bottom-center (default), bottom-right." },
|
|
894
|
+
start: { type: "INTEGER", description: "Starting page number. Default 1." },
|
|
895
|
+
format: { type: "STRING", description: "Format string. Use {n} for number, {total} for total. Default: '{n}'." },
|
|
896
|
+
},
|
|
897
|
+
required: ["path"],
|
|
898
|
+
},
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: "pdf_to_word",
|
|
902
|
+
description: "Convert a PDF to Word (.docx). Extracts text with basic formatting (bold, italic, font size). Best for text-heavy PDFs.",
|
|
903
|
+
parameters: {
|
|
904
|
+
type: "OBJECT",
|
|
905
|
+
properties: {
|
|
906
|
+
path: { type: "STRING", description: "PDF file path." },
|
|
907
|
+
output_path: { type: "STRING", description: "Output .docx path. Default: same name with .docx extension." },
|
|
908
|
+
},
|
|
909
|
+
required: ["path"],
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
name: "resize_image",
|
|
914
|
+
description: "Resize or compress an image. Set width OR height to keep aspect ratio, or both for exact size.",
|
|
915
|
+
parameters: {
|
|
916
|
+
type: "OBJECT",
|
|
917
|
+
properties: {
|
|
918
|
+
path: { type: "STRING", description: "Image path." },
|
|
919
|
+
width: { type: "INTEGER", description: "Target width in pixels." },
|
|
920
|
+
height: { type: "INTEGER", description: "Target height in pixels." },
|
|
921
|
+
quality: { type: "INTEGER", description: "JPEG quality 1-100. Default 85." },
|
|
922
|
+
output_path: { type: "STRING", description: "Output path. If omitted, overwrites original." },
|
|
923
|
+
},
|
|
924
|
+
required: ["path"],
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
name: "convert_image",
|
|
929
|
+
description: "Convert image format. Supports: jpg, png, webp, bmp. Handles HEIC (iPhone photos) to JPG.",
|
|
930
|
+
parameters: {
|
|
931
|
+
type: "OBJECT",
|
|
932
|
+
properties: {
|
|
933
|
+
path: { type: "STRING", description: "Source image path." },
|
|
934
|
+
format: { type: "STRING", description: "Target format: jpg, png, webp, bmp." },
|
|
935
|
+
output_path: {
|
|
936
|
+
type: "STRING",
|
|
937
|
+
description: "Output path. Optional — defaults to same name with new extension.",
|
|
938
|
+
},
|
|
939
|
+
},
|
|
940
|
+
required: ["path", "format"],
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
{
|
|
944
|
+
name: "crop_image",
|
|
945
|
+
description:
|
|
946
|
+
"Crop an image to specified rectangle. Coordinates in pixels. Use file_info or read_file first to know image dimensions before cropping.",
|
|
947
|
+
parameters: {
|
|
948
|
+
type: "OBJECT",
|
|
949
|
+
properties: {
|
|
950
|
+
path: { type: "STRING", description: "Image path." },
|
|
951
|
+
x: { type: "INTEGER", description: "Left edge (pixels from left)." },
|
|
952
|
+
y: { type: "INTEGER", description: "Top edge (pixels from top)." },
|
|
953
|
+
width: { type: "INTEGER", description: "Crop width in pixels." },
|
|
954
|
+
height: { type: "INTEGER", description: "Crop height in pixels." },
|
|
955
|
+
output_path: { type: "STRING", description: "Output path. Optional." },
|
|
956
|
+
},
|
|
957
|
+
required: ["path", "x", "y", "width", "height"],
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
name: "image_metadata",
|
|
962
|
+
description:
|
|
963
|
+
"Extract EXIF metadata from photos: date taken, camera model, GPS coordinates, lens, aperture, ISO, dimensions. Use for: 'when was this taken?', 'what camera?', 'where was this shot?', 'show timeline'. Pass folder for batch metadata of all images.",
|
|
964
|
+
parameters: {
|
|
965
|
+
type: "OBJECT",
|
|
966
|
+
properties: {
|
|
967
|
+
path: { type: "STRING", description: "Absolute path to image." },
|
|
968
|
+
folder: { type: "STRING", description: "Absolute path to folder — get metadata for ALL images." },
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
name: "compress_files",
|
|
974
|
+
description: "Zip files or folders into a .zip archive. Can include multiple files and entire folders.",
|
|
975
|
+
parameters: {
|
|
976
|
+
type: "OBJECT",
|
|
977
|
+
properties: {
|
|
978
|
+
paths: { type: "ARRAY", description: "List of file/folder paths to zip.", items: { type: "STRING" } },
|
|
979
|
+
output_path: { type: "STRING", description: "Output .zip file path." },
|
|
980
|
+
},
|
|
981
|
+
required: ["paths", "output_path"],
|
|
982
|
+
},
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
name: "extract_archive",
|
|
986
|
+
description: "Extract a zip archive. If no output_path, extracts to folder with same name.",
|
|
987
|
+
parameters: {
|
|
988
|
+
type: "OBJECT",
|
|
989
|
+
properties: {
|
|
990
|
+
path: { type: "STRING", description: "Path to .zip file." },
|
|
991
|
+
output_path: { type: "STRING", description: "Folder to extract into. Optional." },
|
|
992
|
+
},
|
|
993
|
+
required: ["path"],
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
name: "download_url",
|
|
998
|
+
description: "Download a file from a URL. Saves to Downloads/Pinpoint/ by default. Use when user shares a link.",
|
|
999
|
+
parameters: {
|
|
1000
|
+
type: "OBJECT",
|
|
1001
|
+
properties: {
|
|
1002
|
+
url: { type: "STRING", description: "URL to download." },
|
|
1003
|
+
save_path: { type: "STRING", description: "Where to save. Optional." },
|
|
1004
|
+
},
|
|
1005
|
+
required: ["url"],
|
|
1006
|
+
},
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
name: "find_duplicates",
|
|
1010
|
+
description: "Find duplicate files in a folder by content hash. Returns groups of identical files for cleanup.",
|
|
1011
|
+
parameters: {
|
|
1012
|
+
type: "OBJECT",
|
|
1013
|
+
properties: {
|
|
1014
|
+
folder: { type: "STRING", description: "Folder path to scan." },
|
|
1015
|
+
},
|
|
1016
|
+
required: ["folder"],
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
name: "batch_rename",
|
|
1021
|
+
description:
|
|
1022
|
+
"Rename files matching a regex pattern. ALWAYS call with dry_run=true first to preview changes, show the user, and only execute with dry_run=false after confirmation.",
|
|
1023
|
+
parameters: {
|
|
1024
|
+
type: "OBJECT",
|
|
1025
|
+
properties: {
|
|
1026
|
+
folder: { type: "STRING", description: "Folder containing files to rename." },
|
|
1027
|
+
pattern: { type: "STRING", description: "Regex pattern to match in filenames." },
|
|
1028
|
+
replace: { type: "STRING", description: "Replacement string." },
|
|
1029
|
+
dry_run: {
|
|
1030
|
+
type: "BOOLEAN",
|
|
1031
|
+
description: "Preview only (default true). Set false to execute after user confirms.",
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
required: ["folder", "pattern", "replace"],
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
name: "run_python",
|
|
1039
|
+
description:
|
|
1040
|
+
"Execute Python code. Use for any custom operation: image manipulation, data processing, file operations, calculations, generating files. Pre-loaded: PIL, pandas, numpy, matplotlib, os, json. Working dir: /tmp/pinpoint_python/. Print results to stdout.",
|
|
1041
|
+
parameters: {
|
|
1042
|
+
type: "OBJECT",
|
|
1043
|
+
properties: {
|
|
1044
|
+
code: {
|
|
1045
|
+
type: "STRING",
|
|
1046
|
+
description: "Python code to execute. Use print() for output. Save files to WORK_DIR.",
|
|
1047
|
+
},
|
|
1048
|
+
timeout: { type: "INTEGER", description: "Max execution time in seconds. Default 30, max 120." },
|
|
1049
|
+
},
|
|
1050
|
+
required: ["code"],
|
|
1051
|
+
},
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
name: "memory_save",
|
|
1055
|
+
description:
|
|
1056
|
+
"Save a fact to persistent memory. Persists across sessions and restarts. Smart dedup: skips duplicates, merges related facts, handles contradictions. Only works when memory is enabled.",
|
|
1057
|
+
parameters: {
|
|
1058
|
+
type: "OBJECT",
|
|
1059
|
+
properties: {
|
|
1060
|
+
fact: { type: "STRING", description: "The fact to remember. Keep it short and factual." },
|
|
1061
|
+
category: {
|
|
1062
|
+
type: "STRING",
|
|
1063
|
+
description: "Category: people, places, preferences, professional, health, plans, general. Default: general.",
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1066
|
+
required: ["fact"],
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
name: "memory_search",
|
|
1071
|
+
description:
|
|
1072
|
+
"Search persistent memories by keyword. Use to recall personal facts about the user before saying 'I don't know'. Only works when memory is enabled.",
|
|
1073
|
+
parameters: {
|
|
1074
|
+
type: "OBJECT",
|
|
1075
|
+
properties: {
|
|
1076
|
+
query: { type: "STRING", description: "Search keywords. E.g. 'mom', 'trip', 'preference'." },
|
|
1077
|
+
},
|
|
1078
|
+
required: ["query"],
|
|
1079
|
+
},
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
name: "memory_delete",
|
|
1083
|
+
description: "Delete a memory by ID. Use when user asks to forget something and you have the ID.",
|
|
1084
|
+
parameters: {
|
|
1085
|
+
type: "OBJECT",
|
|
1086
|
+
properties: {
|
|
1087
|
+
id: { type: "INTEGER", description: "Memory ID to delete (from memory_search results)." },
|
|
1088
|
+
},
|
|
1089
|
+
required: ["id"],
|
|
1090
|
+
},
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
name: "memory_forget",
|
|
1094
|
+
description:
|
|
1095
|
+
"Forget a memory by description — no ID needed. Searches memories for best match and deletes it. Use when user says 'forget that I like dark mode' or 'remove the thing about Mumbai'. Preferred over memory_delete when you don't have the ID.",
|
|
1096
|
+
parameters: {
|
|
1097
|
+
type: "OBJECT",
|
|
1098
|
+
properties: {
|
|
1099
|
+
description: {
|
|
1100
|
+
type: "STRING",
|
|
1101
|
+
description:
|
|
1102
|
+
"Natural language description of what to forget. E.g. 'dark mode preference', 'living in Mumbai'.",
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
required: ["description"],
|
|
1106
|
+
},
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
name: "search_facts",
|
|
1110
|
+
description:
|
|
1111
|
+
"Search extracted facts from indexed documents. Facts are key details (names, dates, amounts, topics) auto-extracted at index time. Use for quick factual lookups like 'who is the electrician?' or 'what was the invoice amount?'. Falls back to search_documents for full-text search if facts don't have enough detail.",
|
|
1112
|
+
parameters: {
|
|
1113
|
+
type: "OBJECT",
|
|
1114
|
+
properties: {
|
|
1115
|
+
query: {
|
|
1116
|
+
type: "STRING",
|
|
1117
|
+
description: "Search keywords for facts. E.g. 'electrician', 'invoice amount', 'meeting date'.",
|
|
1118
|
+
},
|
|
1119
|
+
},
|
|
1120
|
+
required: ["query"],
|
|
1121
|
+
},
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
name: "web_search",
|
|
1125
|
+
description:
|
|
1126
|
+
"Search the web for real-world information. Use for news, weather, sports, people, products, prices, current events, comparisons — anything NOT in local files. Returns structured search results with titles, snippets, and URLs. The results are reliable and current — answer directly from them. Do NOT fall back to search_documents or search_facts for web queries. To read full content of a result, call again with that result's URL.",
|
|
1127
|
+
parameters: {
|
|
1128
|
+
type: "OBJECT",
|
|
1129
|
+
properties: {
|
|
1130
|
+
query: { type: "STRING", description: "Search query. Be specific." },
|
|
1131
|
+
url: {
|
|
1132
|
+
type: "STRING",
|
|
1133
|
+
description:
|
|
1134
|
+
"Optional: a specific URL to read full content. Use to get details from a search result.",
|
|
1135
|
+
},
|
|
1136
|
+
count: { type: "INTEGER", description: "Number of results (default 10, max 20)." },
|
|
1137
|
+
freshness: { type: "STRING", description: "Time filter: noLimit (default), day, week, month." },
|
|
1138
|
+
start: {
|
|
1139
|
+
type: "INTEGER",
|
|
1140
|
+
description: "Character offset for long pages (when reading a URL). Use the end value from previous response.",
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
required: ["query"],
|
|
1144
|
+
},
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
name: "search_images_visual",
|
|
1148
|
+
description:
|
|
1149
|
+
"Search images in a folder by text description using AI vision. IMPORTANT: Try search_documents(query, file_type='image', folder=...) FIRST — indexed image captions are free and instant. Only use this tool if search_documents returns no results or the folder isn't indexed. Returns ranked list of images matching the query. Results are RELIABLE — trust them for categorization, grouping, and answering without manually inspecting individual images. Do NOT call read_file/resize_image/ocr on photos after getting visual search results. First call may take time, subsequent queries on same folder are faster (cached). Pass queries as array for batch search (multiple queries in one call). GROUPING WORKFLOW: When organizing/grouping photos into categories — 1) first run with default limit (10) as a PREVIEW, 2) show user the proposed categories with sample counts, 3) ask 'Shall I do the full folder?', 4) if yes, re-run with limit=200 per category to cover all photos.",
|
|
1150
|
+
parameters: {
|
|
1151
|
+
type: "OBJECT",
|
|
1152
|
+
properties: {
|
|
1153
|
+
folder: { type: "STRING", description: "Absolute path to folder containing images." },
|
|
1154
|
+
query: { type: "STRING", description: "Single text query. E.g. 'bride cutting cake'." },
|
|
1155
|
+
queries: {
|
|
1156
|
+
type: "ARRAY",
|
|
1157
|
+
items: { type: "STRING" },
|
|
1158
|
+
description:
|
|
1159
|
+
"Multiple queries for batch search. E.g. ['dancing', 'flowers', 'group photo']. Use instead of query for multiple searches at once.",
|
|
1160
|
+
},
|
|
1161
|
+
limit: { type: "INTEGER", description: "Max results per query. Default 10." },
|
|
1162
|
+
},
|
|
1163
|
+
required: ["folder"],
|
|
1164
|
+
},
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
name: "search_video",
|
|
1168
|
+
description:
|
|
1169
|
+
"Search inside a video by text description. Gemini analyzes the full video natively (up to 3h). Returns timestamps of matching moments. After finding timestamps, use extract_frame to get the image — it will be auto-sent to the user.",
|
|
1170
|
+
parameters: {
|
|
1171
|
+
type: "OBJECT",
|
|
1172
|
+
properties: {
|
|
1173
|
+
video_path: { type: "STRING", description: "Absolute path to video file." },
|
|
1174
|
+
query: {
|
|
1175
|
+
type: "STRING",
|
|
1176
|
+
description: "Text description of what to find. E.g. 'person dancing', 'sunset scene'.",
|
|
1177
|
+
},
|
|
1178
|
+
fps: {
|
|
1179
|
+
type: "NUMBER",
|
|
1180
|
+
description: "Frames per second to extract. Default 1. Use 0.5 for long videos, 2 for short clips.",
|
|
1181
|
+
},
|
|
1182
|
+
limit: { type: "INTEGER", description: "Max results. Default 5." },
|
|
1183
|
+
},
|
|
1184
|
+
required: ["video_path", "query"],
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
name: "extract_frame",
|
|
1189
|
+
description:
|
|
1190
|
+
"Extract a single frame from a video at a specific timestamp. Returns the frame as an image file. Use after search_video to get the actual frame image for sending.",
|
|
1191
|
+
parameters: {
|
|
1192
|
+
type: "OBJECT",
|
|
1193
|
+
properties: {
|
|
1194
|
+
video_path: { type: "STRING", description: "Absolute path to video file." },
|
|
1195
|
+
seconds: { type: "NUMBER", description: "Timestamp in seconds to extract frame from." },
|
|
1196
|
+
},
|
|
1197
|
+
required: ["video_path", "seconds"],
|
|
1198
|
+
},
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
name: "transcribe_audio",
|
|
1202
|
+
description:
|
|
1203
|
+
"Transcribe an audio file to text using AI. Returns full transcript with timestamps. Supports: mp3, wav, flac, aac, ogg, wma, m4a, aiff. Up to 9.5 hours of audio.",
|
|
1204
|
+
parameters: {
|
|
1205
|
+
type: "OBJECT",
|
|
1206
|
+
properties: {
|
|
1207
|
+
path: { type: "STRING", description: "Absolute path to audio file." },
|
|
1208
|
+
},
|
|
1209
|
+
required: ["path"],
|
|
1210
|
+
},
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
name: "search_audio",
|
|
1214
|
+
description:
|
|
1215
|
+
"Search within an audio file for specific content (speech, sounds, topics). Returns timestamps of matching moments with relevance scores. Use for finding specific parts in podcasts, recordings, voice memos.",
|
|
1216
|
+
parameters: {
|
|
1217
|
+
type: "OBJECT",
|
|
1218
|
+
properties: {
|
|
1219
|
+
audio_path: { type: "STRING", description: "Absolute path to audio file." },
|
|
1220
|
+
query: {
|
|
1221
|
+
type: "STRING",
|
|
1222
|
+
description:
|
|
1223
|
+
"What to find in the audio. E.g. 'discussion about pricing', 'laughter', 'someone saying hello'.",
|
|
1224
|
+
},
|
|
1225
|
+
limit: { type: "INTEGER", description: "Max results. Default 5." },
|
|
1226
|
+
},
|
|
1227
|
+
required: ["audio_path", "query"],
|
|
1228
|
+
},
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
name: "set_reminder",
|
|
1232
|
+
description:
|
|
1233
|
+
"Set a reminder that will be sent to the user at a specific time. Supports one-time or recurring. Use when user says 'remind me to X at/by Y time' or 'remind me every Monday'. Persists across restarts.",
|
|
1234
|
+
parameters: {
|
|
1235
|
+
type: "OBJECT",
|
|
1236
|
+
properties: {
|
|
1237
|
+
message: { type: "STRING", description: "The reminder message. E.g. 'Buy tablets', 'Call dentist'." },
|
|
1238
|
+
time: {
|
|
1239
|
+
type: "STRING",
|
|
1240
|
+
description:
|
|
1241
|
+
"When to remind. ISO format preferred: '2026-02-27T17:00:00'. Also accepts: '17:00' (today), '5pm' (today), 'in 2 hours', 'tomorrow 9am'.",
|
|
1242
|
+
},
|
|
1243
|
+
repeat: {
|
|
1244
|
+
type: "STRING",
|
|
1245
|
+
description:
|
|
1246
|
+
"Optional. Repeat schedule: 'daily', 'weekly', 'monthly', 'weekdays'. Omit for one-time reminder.",
|
|
1247
|
+
},
|
|
1248
|
+
},
|
|
1249
|
+
required: ["message", "time"],
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
name: "list_reminders",
|
|
1254
|
+
description: "List all pending reminders. Use when user asks 'what reminders do I have?' or 'show my reminders'.",
|
|
1255
|
+
parameters: {
|
|
1256
|
+
type: "OBJECT",
|
|
1257
|
+
properties: {},
|
|
1258
|
+
},
|
|
1259
|
+
},
|
|
1260
|
+
{
|
|
1261
|
+
name: "cancel_reminder",
|
|
1262
|
+
description: "Cancel a pending reminder by its ID. For recurring reminders, this stops all future occurrences.",
|
|
1263
|
+
parameters: {
|
|
1264
|
+
type: "OBJECT",
|
|
1265
|
+
properties: {
|
|
1266
|
+
id: { type: "INTEGER", description: "Reminder ID (from list_reminders)." },
|
|
1267
|
+
},
|
|
1268
|
+
required: ["id"],
|
|
1269
|
+
},
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
name: "extract_tables",
|
|
1273
|
+
description:
|
|
1274
|
+
"Extract structured tables from a PDF. Returns headers + rows for each table found. Works on native PDFs (not scanned). Use for invoices, reports, financial statements, any PDF with tabular data.",
|
|
1275
|
+
parameters: {
|
|
1276
|
+
type: "OBJECT",
|
|
1277
|
+
properties: {
|
|
1278
|
+
path: { type: "STRING", description: "Absolute path to PDF file." },
|
|
1279
|
+
pages: { type: "STRING", description: "Page range: '1-5', '3', or 'all'. Default: all." },
|
|
1280
|
+
},
|
|
1281
|
+
required: ["path"],
|
|
1282
|
+
},
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
name: "watch_folder",
|
|
1286
|
+
description:
|
|
1287
|
+
"Start auto-indexing a folder. New or modified files will be automatically indexed for search. Persists across restarts.",
|
|
1288
|
+
parameters: {
|
|
1289
|
+
type: "OBJECT",
|
|
1290
|
+
properties: {
|
|
1291
|
+
folder: { type: "STRING", description: "Absolute path to folder to watch." },
|
|
1292
|
+
},
|
|
1293
|
+
required: ["folder"],
|
|
1294
|
+
},
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
name: "unwatch_folder",
|
|
1298
|
+
description: "Stop auto-indexing a folder.",
|
|
1299
|
+
parameters: {
|
|
1300
|
+
type: "OBJECT",
|
|
1301
|
+
properties: {
|
|
1302
|
+
folder: { type: "STRING", description: "Absolute path to folder to stop watching." },
|
|
1303
|
+
},
|
|
1304
|
+
required: ["folder"],
|
|
1305
|
+
},
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
name: "list_watched",
|
|
1309
|
+
description: "List all folders currently being watched for auto-indexing.",
|
|
1310
|
+
parameters: {
|
|
1311
|
+
type: "OBJECT",
|
|
1312
|
+
properties: {},
|
|
1313
|
+
},
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
name: "score_photo",
|
|
1317
|
+
description:
|
|
1318
|
+
"Score a single photo's quality using Gemini vision (/100). Returns technical (sharpness, exposure, composition, quality) + aesthetic (emotion, interest, keeper) breakdown with reasoning. Use for quick single-photo evaluation.",
|
|
1319
|
+
parameters: {
|
|
1320
|
+
type: "OBJECT",
|
|
1321
|
+
properties: {
|
|
1322
|
+
path: { type: "STRING", description: "Absolute path to the image file." },
|
|
1323
|
+
},
|
|
1324
|
+
required: ["path"],
|
|
1325
|
+
},
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
name: "cull_photos",
|
|
1329
|
+
description:
|
|
1330
|
+
"Auto-cull photos in a folder: score ALL images, keep top N%, move rejects to _rejects subfolder. Generates an HTML report with thumbnail gallery. WORKFLOW: 1) list_files to survey folder 2) confirm with user 3) cull_photos 4) poll cull_status until done 5) report results + send report file. Background job — use cull_status to poll.",
|
|
1331
|
+
parameters: {
|
|
1332
|
+
type: "OBJECT",
|
|
1333
|
+
properties: {
|
|
1334
|
+
folder: { type: "STRING", description: "Folder containing photos to cull." },
|
|
1335
|
+
keep_pct: { type: "INTEGER", description: "Percentage of photos to keep (1-99). Default 80." },
|
|
1336
|
+
rejects_folder: { type: "STRING", description: "Custom folder for rejects. Default: <folder>/_rejects." },
|
|
1337
|
+
},
|
|
1338
|
+
required: ["folder"],
|
|
1339
|
+
},
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
name: "cull_status",
|
|
1343
|
+
description:
|
|
1344
|
+
"Check progress of a running cull_photos job. Returns scored/total count, ETA, and final stats when done. Poll every few seconds until status is 'done'. Set cancel=true to stop the job.",
|
|
1345
|
+
parameters: {
|
|
1346
|
+
type: "OBJECT",
|
|
1347
|
+
properties: {
|
|
1348
|
+
folder: { type: "STRING", description: "The folder being culled (same as passed to cull_photos)." },
|
|
1349
|
+
cancel: { type: "BOOLEAN", description: "Set true to cancel the running job." },
|
|
1350
|
+
},
|
|
1351
|
+
required: ["folder"],
|
|
1352
|
+
},
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
name: "suggest_categories",
|
|
1356
|
+
description:
|
|
1357
|
+
"Sample ~20 photos from a folder and let Gemini suggest 4-8 grouping categories. Use BEFORE group_photos to auto-discover categories. Returns suggested category names for user confirmation.",
|
|
1358
|
+
parameters: {
|
|
1359
|
+
type: "OBJECT",
|
|
1360
|
+
properties: {
|
|
1361
|
+
folder: { type: "STRING", description: "Folder containing photos to analyze." },
|
|
1362
|
+
},
|
|
1363
|
+
required: ["folder"],
|
|
1364
|
+
},
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
name: "group_photos",
|
|
1368
|
+
description:
|
|
1369
|
+
"Auto-group ALL photos in a folder by Gemini vision classification. Each photo is sent to Gemini with the category list — Gemini picks the best match. Photos MOVED to category subfolders (destructive). Generates HTML report. Classifications cached in DB — re-runs are free. IMPORTANT: NEVER call this without user confirmation of categories first. Always show suggest_categories results and WAIT for user approval before calling this. Background job — use group_status to poll.",
|
|
1370
|
+
parameters: {
|
|
1371
|
+
type: "OBJECT",
|
|
1372
|
+
properties: {
|
|
1373
|
+
folder: { type: "STRING", description: "Folder containing photos to group." },
|
|
1374
|
+
categories: {
|
|
1375
|
+
type: "ARRAY",
|
|
1376
|
+
items: { type: "STRING" },
|
|
1377
|
+
description: "List of category names to classify into (e.g. ['Ceremony', 'Portraits', 'Family', 'Rituals']).",
|
|
1378
|
+
},
|
|
1379
|
+
},
|
|
1380
|
+
required: ["folder", "categories"],
|
|
1381
|
+
},
|
|
1382
|
+
},
|
|
1383
|
+
{
|
|
1384
|
+
name: "group_status",
|
|
1385
|
+
description:
|
|
1386
|
+
"Check progress of a running group_photos job. Returns classified/total count, ETA, and final group counts when done. Poll every few seconds until status is 'done'. Set cancel=true to stop the job.",
|
|
1387
|
+
parameters: {
|
|
1388
|
+
type: "OBJECT",
|
|
1389
|
+
properties: {
|
|
1390
|
+
folder: { type: "STRING", description: "The folder being grouped (same as passed to group_photos)." },
|
|
1391
|
+
cancel: { type: "BOOLEAN", description: "Set true to cancel the running job." },
|
|
1392
|
+
},
|
|
1393
|
+
required: ["folder"],
|
|
1394
|
+
},
|
|
1395
|
+
},
|
|
1396
|
+
// --- Google Workspace tools (gws-cli) ---
|
|
1397
|
+
{
|
|
1398
|
+
name: "gmail_send",
|
|
1399
|
+
description:
|
|
1400
|
+
"Send an email via Gmail. Can optionally attach a local file. Use when user says 'email this to X' or 'send mail'.",
|
|
1401
|
+
parameters: {
|
|
1402
|
+
type: "OBJECT",
|
|
1403
|
+
properties: {
|
|
1404
|
+
to: { type: "STRING", description: "Recipient email address." },
|
|
1405
|
+
subject: { type: "STRING", description: "Email subject line." },
|
|
1406
|
+
body: { type: "STRING", description: "Email body (plain text)." },
|
|
1407
|
+
attach: { type: "STRING", description: "Optional: absolute path to file to attach." },
|
|
1408
|
+
},
|
|
1409
|
+
required: ["to", "subject", "body"],
|
|
1410
|
+
},
|
|
1411
|
+
},
|
|
1412
|
+
{
|
|
1413
|
+
name: "gmail_search",
|
|
1414
|
+
description:
|
|
1415
|
+
"Search Gmail inbox. Uses same query syntax as Gmail search bar (from:, to:, subject:, has:attachment, after:, before:, etc).",
|
|
1416
|
+
parameters: {
|
|
1417
|
+
type: "OBJECT",
|
|
1418
|
+
properties: {
|
|
1419
|
+
query: { type: "STRING", description: "Gmail search query (e.g. 'from:john invoice after:2026/01/01')." },
|
|
1420
|
+
limit: { type: "NUMBER", description: "Max results (default 10)." },
|
|
1421
|
+
},
|
|
1422
|
+
required: ["query"],
|
|
1423
|
+
},
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
name: "gmail_triage",
|
|
1427
|
+
description: "Show unread inbox summary — sender, subject, date for each unread message. Use for 'check my email' or 'any new mail?'.",
|
|
1428
|
+
parameters: { type: "OBJECT", properties: {} },
|
|
1429
|
+
},
|
|
1430
|
+
{
|
|
1431
|
+
name: "calendar_events",
|
|
1432
|
+
description:
|
|
1433
|
+
"List upcoming calendar events. Use for 'what's on my calendar', 'am I free tomorrow', 'meetings this week'.",
|
|
1434
|
+
parameters: {
|
|
1435
|
+
type: "OBJECT",
|
|
1436
|
+
properties: {
|
|
1437
|
+
days: { type: "NUMBER", description: "Days ahead to show (default 7, max 30)." },
|
|
1438
|
+
today: { type: "BOOLEAN", description: "Set true to show only today's events." },
|
|
1439
|
+
},
|
|
1440
|
+
},
|
|
1441
|
+
},
|
|
1442
|
+
{
|
|
1443
|
+
name: "calendar_create",
|
|
1444
|
+
description:
|
|
1445
|
+
"Create a calendar event. Times must be ISO 8601 format (e.g. 2026-03-07T15:00:00+05:30). Use user's timezone (IST = +05:30).",
|
|
1446
|
+
parameters: {
|
|
1447
|
+
type: "OBJECT",
|
|
1448
|
+
properties: {
|
|
1449
|
+
summary: { type: "STRING", description: "Event title." },
|
|
1450
|
+
start: { type: "STRING", description: "Start time in ISO 8601 (e.g. 2026-03-07T15:00:00+05:30)." },
|
|
1451
|
+
end: { type: "STRING", description: "End time in ISO 8601." },
|
|
1452
|
+
location: { type: "STRING", description: "Optional: event location." },
|
|
1453
|
+
description: { type: "STRING", description: "Optional: event description." },
|
|
1454
|
+
attendees: {
|
|
1455
|
+
type: "ARRAY",
|
|
1456
|
+
items: { type: "STRING" },
|
|
1457
|
+
description: "Optional: list of attendee email addresses.",
|
|
1458
|
+
},
|
|
1459
|
+
},
|
|
1460
|
+
required: ["summary", "start", "end"],
|
|
1461
|
+
},
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
name: "drive_list",
|
|
1465
|
+
description:
|
|
1466
|
+
"List or search Google Drive files. Empty query shows recent files. Use for 'what files do I have on Drive', 'find report on Drive'.",
|
|
1467
|
+
parameters: {
|
|
1468
|
+
type: "OBJECT",
|
|
1469
|
+
properties: {
|
|
1470
|
+
query: { type: "STRING", description: "Search term (searches file names and content). Empty = recent files." },
|
|
1471
|
+
limit: { type: "NUMBER", description: "Max results (default 10)." },
|
|
1472
|
+
},
|
|
1473
|
+
},
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
name: "drive_upload",
|
|
1477
|
+
description:
|
|
1478
|
+
"Upload a local file to Google Drive. Use when user says 'upload to Drive', 'save to Drive', 'put on Drive'.",
|
|
1479
|
+
parameters: {
|
|
1480
|
+
type: "OBJECT",
|
|
1481
|
+
properties: {
|
|
1482
|
+
path: { type: "STRING", description: "Absolute path to the local file to upload." },
|
|
1483
|
+
name: { type: "STRING", description: "Optional: target filename on Drive (defaults to local filename)." },
|
|
1484
|
+
},
|
|
1485
|
+
required: ["path"],
|
|
1486
|
+
},
|
|
1487
|
+
},
|
|
1488
|
+
];
|
|
1489
|
+
|
|
1490
|
+
// --- Declarative tool routing table ---
|
|
1491
|
+
// Each entry: { m: method, p: path/fn, b: body mapper (POST only) }
|
|
1492
|
+
// Replaces 320-line switch for simple API-pass-through tools.
|
|
1493
|
+
// Note: search_documents uses MAX_RESULTS which must be passed in at init time.
|
|
1494
|
+
const enc = encodeURIComponent;
|
|
1495
|
+
|
|
1496
|
+
function buildToolRoutes(maxResults) {
|
|
1497
|
+
return {
|
|
1498
|
+
// --- GET routes ---
|
|
1499
|
+
search_documents: {
|
|
1500
|
+
m: "GET",
|
|
1501
|
+
p: (a) => {
|
|
1502
|
+
let u = `/search?q=${enc(a.query || "")}&limit=${maxResults}`;
|
|
1503
|
+
if (a.file_type) u += `&file_type=${enc(a.file_type)}`;
|
|
1504
|
+
if (a.folder) u += `&folder=${enc(a.folder)}`;
|
|
1505
|
+
return u;
|
|
1506
|
+
},
|
|
1507
|
+
},
|
|
1508
|
+
read_document: { m: "GET", p: (a) => `/document/${a.document_id}` },
|
|
1509
|
+
list_files: {
|
|
1510
|
+
m: "GET",
|
|
1511
|
+
p: (a) => {
|
|
1512
|
+
let u = `/list_files?folder=${enc(a.folder)}`;
|
|
1513
|
+
if (a.sort_by) u += `&sort_by=${enc(a.sort_by)}`;
|
|
1514
|
+
if (a.filter_ext) u += `&filter_ext=${enc(a.filter_ext)}`;
|
|
1515
|
+
if (a.filter_type) u += `&filter_type=${enc(a.filter_type)}`;
|
|
1516
|
+
if (a.name_contains) u += `&name_contains=${enc(a.name_contains)}`;
|
|
1517
|
+
if (a.recursive) u += `&recursive=true`;
|
|
1518
|
+
return u;
|
|
1519
|
+
},
|
|
1520
|
+
},
|
|
1521
|
+
find_file: {
|
|
1522
|
+
m: "GET",
|
|
1523
|
+
p: (a) => {
|
|
1524
|
+
let u = `/find-file?query=${enc(a.query)}`;
|
|
1525
|
+
if (a.ext) u += `&ext=${enc(a.ext)}`;
|
|
1526
|
+
return u;
|
|
1527
|
+
},
|
|
1528
|
+
},
|
|
1529
|
+
search_generated_files: {
|
|
1530
|
+
m: "GET",
|
|
1531
|
+
p: (a) => {
|
|
1532
|
+
let u = `/search-generated-files?query=${enc(a.query || "")}`;
|
|
1533
|
+
if (a.tool_name) u += `&tool_name=${enc(a.tool_name)}`;
|
|
1534
|
+
return u;
|
|
1535
|
+
},
|
|
1536
|
+
},
|
|
1537
|
+
file_info: { m: "GET", p: (a) => `/file_info?path=${enc(a.path)}` },
|
|
1538
|
+
get_status: { m: "GET", p: () => "/status" },
|
|
1539
|
+
search_history: { m: "GET", p: (a) => `/conversation/search?q=${enc(a.query || "")}&limit=10` },
|
|
1540
|
+
search_facts: { m: "GET", p: (a) => `/search-facts?q=${enc(a.query)}&limit=10` },
|
|
1541
|
+
list_watched: { m: "GET", p: "/watched-folders" },
|
|
1542
|
+
// --- Simple POST routes (tool args → API body) ---
|
|
1543
|
+
read_excel: {
|
|
1544
|
+
m: "POST",
|
|
1545
|
+
p: "/read_excel",
|
|
1546
|
+
b: (a) => ({ path: a.path, sheet_name: a.sheet_name || null, cell_range: a.cell_range || null }),
|
|
1547
|
+
},
|
|
1548
|
+
calculate: { m: "POST", p: "/calculate", b: (a) => ({ expression: a.expression }) },
|
|
1549
|
+
grep_files: {
|
|
1550
|
+
m: "POST",
|
|
1551
|
+
p: "/grep",
|
|
1552
|
+
b: (a) => ({ pattern: a.pattern, folder: a.folder, file_filter: a.file_filter }),
|
|
1553
|
+
},
|
|
1554
|
+
batch_move: {
|
|
1555
|
+
m: "POST",
|
|
1556
|
+
p: "/batch_move",
|
|
1557
|
+
b: (a) => ({ sources: a.sources || [], destination: a.destination, is_copy: a.is_copy || false }),
|
|
1558
|
+
},
|
|
1559
|
+
move_file: {
|
|
1560
|
+
m: "POST",
|
|
1561
|
+
p: "/move_file",
|
|
1562
|
+
b: (a) => ({ source: a.source, destination: a.destination, is_copy: a.copy || false }),
|
|
1563
|
+
},
|
|
1564
|
+
copy_file: {
|
|
1565
|
+
m: "POST",
|
|
1566
|
+
p: "/move_file",
|
|
1567
|
+
b: (a) => ({ source: a.source, destination: a.destination, is_copy: true }),
|
|
1568
|
+
},
|
|
1569
|
+
create_folder: { m: "POST", p: "/create_folder", b: (a) => ({ path: a.path }) },
|
|
1570
|
+
delete_file: { m: "POST", p: "/delete_file", b: (a) => ({ path: a.path }) },
|
|
1571
|
+
read_file: { m: "POST", p: "/read_file", b: (a) => ({ path: a.path }) },
|
|
1572
|
+
detect_faces: { m: "POST", p: "/detect-faces", b: (a) => ({ image_path: a.image_path, folder: a.folder }) },
|
|
1573
|
+
crop_face: { m: "POST", p: "/crop-face", b: (a) => ({ image_path: a.image_path, face_idx: a.face_idx }) },
|
|
1574
|
+
find_person: { m: "POST", p: "/find-person", b: (a) => ({ reference_image: a.reference_image, folder: a.folder }) },
|
|
1575
|
+
find_person_by_face: {
|
|
1576
|
+
m: "POST",
|
|
1577
|
+
p: "/find-person-by-face",
|
|
1578
|
+
b: (a) => ({ reference_image: a.reference_image, face_idx: a.face_idx, folder: a.folder }),
|
|
1579
|
+
},
|
|
1580
|
+
count_faces: {
|
|
1581
|
+
m: "POST",
|
|
1582
|
+
p: "/count-faces",
|
|
1583
|
+
b: (a) => ({ image_path: a.image_path, paths: a.paths, folder: a.folder }),
|
|
1584
|
+
},
|
|
1585
|
+
compare_faces: {
|
|
1586
|
+
m: "POST",
|
|
1587
|
+
p: "/compare-faces",
|
|
1588
|
+
b: (a) => ({
|
|
1589
|
+
image_path_1: a.image_path_1,
|
|
1590
|
+
face_idx_1: a.face_idx_1 || 0,
|
|
1591
|
+
image_path_2: a.image_path_2,
|
|
1592
|
+
face_idx_2: a.face_idx_2 || 0,
|
|
1593
|
+
}),
|
|
1594
|
+
},
|
|
1595
|
+
remember_face: {
|
|
1596
|
+
m: "POST",
|
|
1597
|
+
p: "/remember-face",
|
|
1598
|
+
b: (a) => ({ image_path: a.image_path, face_idx: a.face_idx || 0, name: a.name }),
|
|
1599
|
+
},
|
|
1600
|
+
forget_face: { m: "POST", p: "/forget-face", b: (a) => ({ name: a.name }) },
|
|
1601
|
+
ocr: { m: "POST", p: "/ocr", b: (a) => ({ path: a.path, folder: a.folder }) },
|
|
1602
|
+
analyze_data: {
|
|
1603
|
+
m: "POST",
|
|
1604
|
+
p: "/analyze-data",
|
|
1605
|
+
b: (a) => ({
|
|
1606
|
+
path: a.path,
|
|
1607
|
+
operation: a.operation || "describe",
|
|
1608
|
+
columns: a.columns || null,
|
|
1609
|
+
query: a.query || null,
|
|
1610
|
+
sheet: a.sheet || null,
|
|
1611
|
+
}),
|
|
1612
|
+
},
|
|
1613
|
+
index_file: { m: "POST", p: "/index-file", b: (a) => ({ path: a.path }) },
|
|
1614
|
+
write_file: {
|
|
1615
|
+
m: "POST",
|
|
1616
|
+
p: "/write-file",
|
|
1617
|
+
b: (a) => ({ path: a.path, content: a.content, append: a.append || false }),
|
|
1618
|
+
},
|
|
1619
|
+
generate_excel: {
|
|
1620
|
+
m: "POST",
|
|
1621
|
+
p: "/generate-excel",
|
|
1622
|
+
b: (a) => ({ path: a.path, data: a.data, sheet_name: a.sheet_name || "Sheet1", title: a.title || null }),
|
|
1623
|
+
},
|
|
1624
|
+
generate_chart: {
|
|
1625
|
+
m: "POST",
|
|
1626
|
+
p: "/generate-chart",
|
|
1627
|
+
b: (a) => ({
|
|
1628
|
+
data: a.data,
|
|
1629
|
+
chart_type: a.chart_type,
|
|
1630
|
+
title: a.title || "",
|
|
1631
|
+
xlabel: a.xlabel || "",
|
|
1632
|
+
ylabel: a.ylabel || "",
|
|
1633
|
+
output_path: a.output_path || null,
|
|
1634
|
+
}),
|
|
1635
|
+
},
|
|
1636
|
+
merge_pdf: { m: "POST", p: "/merge-pdf", b: (a) => ({ paths: a.paths, output_path: a.output_path }) },
|
|
1637
|
+
split_pdf: { m: "POST", p: "/split-pdf", b: (a) => ({ path: a.path, pages: a.pages, output_path: a.output_path }) },
|
|
1638
|
+
pdf_to_images: {
|
|
1639
|
+
m: "POST",
|
|
1640
|
+
p: "/pdf-to-images",
|
|
1641
|
+
b: (a) => ({ path: a.path, pages: a.pages || null, dpi: a.dpi || 150, output_folder: a.output_folder || null }),
|
|
1642
|
+
},
|
|
1643
|
+
images_to_pdf: { m: "POST", p: "/images-to-pdf", b: (a) => ({ paths: a.paths, output_path: a.output_path }) },
|
|
1644
|
+
compress_pdf: { m: "POST", p: "/compress-pdf", b: (a) => ({ path: a.path, output_path: a.output_path || null }) },
|
|
1645
|
+
add_page_numbers: {
|
|
1646
|
+
m: "POST",
|
|
1647
|
+
p: "/add-page-numbers",
|
|
1648
|
+
b: (a) => ({ path: a.path, output_path: a.output_path || null, position: a.position || "bottom-center", start: a.start || 1, format: a.format || "{n}" }),
|
|
1649
|
+
},
|
|
1650
|
+
pdf_to_word: { m: "POST", p: "/pdf-to-word", b: (a) => ({ path: a.path, output_path: a.output_path || null }) },
|
|
1651
|
+
organize_pdf: { m: "POST", p: "/organize-pdf", b: (a) => ({ path: a.path, pages: a.pages, output_path: a.output_path }) },
|
|
1652
|
+
pdf_to_excel: { m: "GET", p: (a) => `/pdf-to-excel?path=${enc(a.path)}${a.output_path ? "&output_path=" + enc(a.output_path) : ""}${a.pages ? "&pages=" + enc(a.pages) : ""}` },
|
|
1653
|
+
resize_image: {
|
|
1654
|
+
m: "POST",
|
|
1655
|
+
p: "/resize-image",
|
|
1656
|
+
b: (a) => ({
|
|
1657
|
+
path: a.path,
|
|
1658
|
+
width: a.width || null,
|
|
1659
|
+
height: a.height || null,
|
|
1660
|
+
quality: a.quality || 85,
|
|
1661
|
+
output_path: a.output_path || null,
|
|
1662
|
+
}),
|
|
1663
|
+
},
|
|
1664
|
+
convert_image: {
|
|
1665
|
+
m: "POST",
|
|
1666
|
+
p: "/convert-image",
|
|
1667
|
+
b: (a) => ({ path: a.path, format: a.format, output_path: a.output_path || null }),
|
|
1668
|
+
},
|
|
1669
|
+
crop_image: {
|
|
1670
|
+
m: "POST",
|
|
1671
|
+
p: "/crop-image",
|
|
1672
|
+
b: (a) => ({
|
|
1673
|
+
path: a.path,
|
|
1674
|
+
x: a.x,
|
|
1675
|
+
y: a.y,
|
|
1676
|
+
width: a.width,
|
|
1677
|
+
height: a.height,
|
|
1678
|
+
output_path: a.output_path || null,
|
|
1679
|
+
}),
|
|
1680
|
+
},
|
|
1681
|
+
image_metadata: { m: "POST", p: "/image-metadata", b: (a) => ({ path: a.path || null, folder: a.folder || null }) },
|
|
1682
|
+
compress_files: { m: "POST", p: "/compress-files", b: (a) => ({ paths: a.paths, output_path: a.output_path }) },
|
|
1683
|
+
extract_archive: {
|
|
1684
|
+
m: "POST",
|
|
1685
|
+
p: "/extract-archive",
|
|
1686
|
+
b: (a) => ({ path: a.path, output_path: a.output_path || null }),
|
|
1687
|
+
},
|
|
1688
|
+
download_url: { m: "POST", p: "/download-url", b: (a) => ({ url: a.url, save_path: a.save_path || null }) },
|
|
1689
|
+
find_duplicates: { m: "POST", p: "/find-duplicates", b: (a) => ({ folder: a.folder }) },
|
|
1690
|
+
batch_rename: {
|
|
1691
|
+
m: "POST",
|
|
1692
|
+
p: "/batch-rename",
|
|
1693
|
+
b: (a) => ({ folder: a.folder, pattern: a.pattern, replace: a.replace, dry_run: a.dry_run !== false }),
|
|
1694
|
+
},
|
|
1695
|
+
run_python: { m: "POST", p: "/run-python", b: (a) => ({ code: a.code, timeout: a.timeout || 30 }) },
|
|
1696
|
+
search_video: {
|
|
1697
|
+
m: "POST",
|
|
1698
|
+
p: "/search-video",
|
|
1699
|
+
b: (a) => ({ video_path: a.video_path, query: a.query, fps: a.fps || 1.0, limit: a.limit || 5 }),
|
|
1700
|
+
},
|
|
1701
|
+
extract_frame: { m: "POST", p: "/extract-frame", b: (a) => ({ video_path: a.video_path, seconds: a.seconds }) },
|
|
1702
|
+
transcribe_audio: { m: "POST", p: "/transcribe-audio", b: (a) => ({ path: a.path }) },
|
|
1703
|
+
search_audio: {
|
|
1704
|
+
m: "POST",
|
|
1705
|
+
p: "/search-audio",
|
|
1706
|
+
b: (a) => ({ audio_path: a.audio_path, query: a.query, limit: a.limit || 5 }),
|
|
1707
|
+
},
|
|
1708
|
+
extract_tables: {
|
|
1709
|
+
m: "POST",
|
|
1710
|
+
p: (a) => {
|
|
1711
|
+
const p = new URLSearchParams({ path: a.path });
|
|
1712
|
+
if (a.pages) p.set("pages", a.pages);
|
|
1713
|
+
return `/extract-tables?${p}`;
|
|
1714
|
+
},
|
|
1715
|
+
b: () => ({}),
|
|
1716
|
+
},
|
|
1717
|
+
watch_folder: { m: "POST", p: "/watch-folder", b: (a) => ({ path: a.folder }) },
|
|
1718
|
+
unwatch_folder: { m: "POST", p: "/unwatch-folder", b: (a) => ({ path: a.folder }) },
|
|
1719
|
+
score_photo: { m: "POST", p: "/score-photo", b: (a) => ({ path: a.path }) },
|
|
1720
|
+
cull_photos: {
|
|
1721
|
+
m: "POST",
|
|
1722
|
+
p: "/cull-photos",
|
|
1723
|
+
b: (a) => ({ folder: a.folder, keep_pct: a.keep_pct || 80, rejects_folder: a.rejects_folder || null }),
|
|
1724
|
+
},
|
|
1725
|
+
cull_status: { m: "GET", p: (a) => `/cull-photos/status?folder=${enc(a.folder)}${a.cancel ? "&cancel=true" : ""}` },
|
|
1726
|
+
suggest_categories: { m: "POST", p: "/suggest-categories", b: (a) => ({ folder: a.folder }) },
|
|
1727
|
+
group_photos: { m: "POST", p: "/group-photos", b: (a) => ({ folder: a.folder, categories: a.categories }) },
|
|
1728
|
+
group_status: {
|
|
1729
|
+
m: "GET",
|
|
1730
|
+
p: (a) => `/group-photos/status?folder=${enc(a.folder)}${a.cancel ? "&cancel=true" : ""}`,
|
|
1731
|
+
},
|
|
1732
|
+
// --- Google Workspace (gws-cli) ---
|
|
1733
|
+
gmail_send: { m: "POST", p: "/google/gmail-send", b: (a) => ({ to: a.to, subject: a.subject, body: a.body, attach: a.attach || null }) },
|
|
1734
|
+
gmail_search: { m: "GET", p: (a) => `/google/gmail-search?q=${enc(a.query || "")}&limit=${a.limit || 10}` },
|
|
1735
|
+
gmail_triage: { m: "GET", p: () => "/google/gmail-triage" },
|
|
1736
|
+
calendar_events: { m: "GET", p: (a) => `/google/calendar-events?days=${a.days || 7}${a.today ? "&today=true" : ""}` },
|
|
1737
|
+
calendar_create: {
|
|
1738
|
+
m: "POST",
|
|
1739
|
+
p: "/google/calendar-create",
|
|
1740
|
+
b: (a) => ({ summary: a.summary, start: a.start, end: a.end, location: a.location || null, description: a.description || null, attendees: a.attendees || null }),
|
|
1741
|
+
},
|
|
1742
|
+
drive_list: { m: "GET", p: (a) => `/google/drive-list?q=${enc(a.query || "")}&limit=${a.limit || 10}` },
|
|
1743
|
+
drive_upload: { m: "POST", p: (a) => `/google/drive-upload?path=${enc(a.path)}${a.name ? "&name=" + enc(a.name) : ""}`, b: () => ({}) },
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// --- Pre-validation: catch bad args before hitting the API (saves a round-trip) ---
|
|
1748
|
+
|
|
1749
|
+
// Tools that need a valid file path
|
|
1750
|
+
const fileTools = [
|
|
1751
|
+
"read_file",
|
|
1752
|
+
"read_excel",
|
|
1753
|
+
"move_file",
|
|
1754
|
+
"copy_file",
|
|
1755
|
+
"delete_file",
|
|
1756
|
+
"ocr",
|
|
1757
|
+
"detect_faces",
|
|
1758
|
+
"crop_face",
|
|
1759
|
+
"find_person",
|
|
1760
|
+
"find_person_by_face",
|
|
1761
|
+
"resize_image",
|
|
1762
|
+
"convert_image",
|
|
1763
|
+
"crop_image",
|
|
1764
|
+
"merge_pdf",
|
|
1765
|
+
"split_pdf",
|
|
1766
|
+
"pdf_to_images",
|
|
1767
|
+
"compress_pdf",
|
|
1768
|
+
"add_page_numbers",
|
|
1769
|
+
"pdf_to_word",
|
|
1770
|
+
"organize_pdf",
|
|
1771
|
+
"pdf_to_excel",
|
|
1772
|
+
"index_file",
|
|
1773
|
+
"compare_faces",
|
|
1774
|
+
"remember_face",
|
|
1775
|
+
"transcribe_audio",
|
|
1776
|
+
"score_photo",
|
|
1777
|
+
"drive_upload",
|
|
1778
|
+
];
|
|
1779
|
+
|
|
1780
|
+
// Tools that need a valid folder
|
|
1781
|
+
const folderTools = [
|
|
1782
|
+
"list_files",
|
|
1783
|
+
"grep_files",
|
|
1784
|
+
"search_images_visual",
|
|
1785
|
+
"find_person",
|
|
1786
|
+
"find_person_by_face",
|
|
1787
|
+
"create_folder",
|
|
1788
|
+
"cull_photos",
|
|
1789
|
+
"group_photos",
|
|
1790
|
+
];
|
|
1791
|
+
|
|
1792
|
+
function preValidate(name, args) {
|
|
1793
|
+
// File path validation
|
|
1794
|
+
const pathKey =
|
|
1795
|
+
name === "move_file" || name === "copy_file"
|
|
1796
|
+
? "source"
|
|
1797
|
+
: name === "find_person" || name === "find_person_by_face"
|
|
1798
|
+
? "reference_image"
|
|
1799
|
+
: name === "compare_faces"
|
|
1800
|
+
? "image_path_1"
|
|
1801
|
+
: name === "crop_face" || name === "remember_face"
|
|
1802
|
+
? "image_path"
|
|
1803
|
+
: "path";
|
|
1804
|
+
|
|
1805
|
+
if (fileTools.includes(name) && args[pathKey]) {
|
|
1806
|
+
try {
|
|
1807
|
+
if (!existsSync(args[pathKey])) return `File not found: ${args[pathKey]}. Check the path and try again.`;
|
|
1808
|
+
} catch (_) {}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// Folder validation
|
|
1812
|
+
const folderKey = "folder";
|
|
1813
|
+
if (folderTools.includes(name) && args[folderKey] && name !== "create_folder") {
|
|
1814
|
+
try {
|
|
1815
|
+
if (!existsSync(args[folderKey])) return `Folder not found: ${args[folderKey]}. Check the path and try again.`;
|
|
1816
|
+
if (!statSync(args[folderKey]).isDirectory()) return `Not a folder: ${args[folderKey]}`;
|
|
1817
|
+
} catch (_) {}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// Array path validation (merge_pdf, images_to_pdf use paths[])
|
|
1821
|
+
if ((name === "merge_pdf" || name === "images_to_pdf") && Array.isArray(args.paths)) {
|
|
1822
|
+
for (const p of args.paths) {
|
|
1823
|
+
try {
|
|
1824
|
+
if (!existsSync(p)) return `File not found: ${p}. Check the path and try again.`;
|
|
1825
|
+
} catch (_) {}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// compare_faces: validate both image paths
|
|
1830
|
+
if (name === "compare_faces" && args.image_path_2) {
|
|
1831
|
+
try {
|
|
1832
|
+
if (!existsSync(args.image_path_2)) return `File not found: ${args.image_path_2}. Check the path and try again.`;
|
|
1833
|
+
} catch (_) {}
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
// Video tools need valid video path
|
|
1837
|
+
if ((name === "search_video" || name === "extract_frame") && args.video_path) {
|
|
1838
|
+
try {
|
|
1839
|
+
if (!existsSync(args.video_path)) return `Video not found: ${args.video_path}. Check the path and try again.`;
|
|
1840
|
+
} catch (_) {}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// Audio search needs valid audio_path
|
|
1844
|
+
if (name === "search_audio" && args.audio_path) {
|
|
1845
|
+
try {
|
|
1846
|
+
if (!existsSync(args.audio_path))
|
|
1847
|
+
return `Audio file not found: ${args.audio_path}. Check the path and try again.`;
|
|
1848
|
+
} catch (_) {}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// Empty query check
|
|
1852
|
+
if (name === "search_documents" && (!args.query || !args.query.trim())) {
|
|
1853
|
+
return "Search query cannot be empty.";
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
return null; // All good
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// --- Tool result summaries (Claude Code pattern: model trusts short summaries) ---
|
|
1860
|
+
function summarizeToolResult(name, args, result) {
|
|
1861
|
+
if (!result) return null;
|
|
1862
|
+
if (result.error) return `${name}: ERROR — ${String(result.error).slice(0, 80)}`;
|
|
1863
|
+
switch (name) {
|
|
1864
|
+
case "search_documents": {
|
|
1865
|
+
const n = result.results?.length || result.total_items || 0;
|
|
1866
|
+
if (result.ambiguous_search) {
|
|
1867
|
+
return `search_documents: ambiguous — ${result.clarification_hint || "ask the user to narrow the result with a title, file name, date, person, location, or year"}`;
|
|
1868
|
+
}
|
|
1869
|
+
return `search_documents: ${n} result(s) found`;
|
|
1870
|
+
}
|
|
1871
|
+
case "search_facts": {
|
|
1872
|
+
const n = result.count || result.results?.length || 0;
|
|
1873
|
+
return `search_facts: ${n} fact(s) found`;
|
|
1874
|
+
}
|
|
1875
|
+
case "search_images_visual": {
|
|
1876
|
+
if (result._ref)
|
|
1877
|
+
return `search_images_visual: ${result.total_items || "multiple"} results (stored as ${result._ref})`;
|
|
1878
|
+
const keys = Object.keys(result).filter((k) => !k.startsWith("_"));
|
|
1879
|
+
return `search_images_visual: results for ${keys.length} queries`;
|
|
1880
|
+
}
|
|
1881
|
+
case "list_files": {
|
|
1882
|
+
const n = result.total || result.total_items || result.showing || 0;
|
|
1883
|
+
return `list_files: ${n} item(s) in ${args?.folder || "folder"}`;
|
|
1884
|
+
}
|
|
1885
|
+
case "find_file": {
|
|
1886
|
+
const n = result.count || 0;
|
|
1887
|
+
return `find_file: ${n} file(s) matching '${args?.query || ""}'`;
|
|
1888
|
+
}
|
|
1889
|
+
case "search_generated_files": {
|
|
1890
|
+
const n = result.count || 0;
|
|
1891
|
+
return `search_generated_files: ${n} file(s) found`;
|
|
1892
|
+
}
|
|
1893
|
+
case "detect_faces": {
|
|
1894
|
+
const n = result.images_processed || result.face_count || 0;
|
|
1895
|
+
return result.images_processed ? `detect_faces: ${n} images processed` : `detect_faces: ${n} face(s) found`;
|
|
1896
|
+
}
|
|
1897
|
+
case "count_faces":
|
|
1898
|
+
return `count_faces: ${result.images_processed ? result.images_processed + " images counted" : result.count + " face(s)"}`;
|
|
1899
|
+
case "analyze_data": {
|
|
1900
|
+
const op = args?.operation || "?";
|
|
1901
|
+
return `analyze_data(${op}): ${result.shape ? result.shape[0] + " rows x " + result.shape[1] + " cols" : "done"}`;
|
|
1902
|
+
}
|
|
1903
|
+
case "find_person":
|
|
1904
|
+
case "find_person_by_face": {
|
|
1905
|
+
const n = result.count || result.matches?.length || 0;
|
|
1906
|
+
return `${name}: ${n} matching photo(s)`;
|
|
1907
|
+
}
|
|
1908
|
+
case "read_file":
|
|
1909
|
+
return `read_file: ${result.type || "text"} file loaded`;
|
|
1910
|
+
case "find_duplicates": {
|
|
1911
|
+
const n = result.groups?.length || result.total_items || 0;
|
|
1912
|
+
return `find_duplicates: ${n} duplicate group(s)`;
|
|
1913
|
+
}
|
|
1914
|
+
case "batch_move": {
|
|
1915
|
+
const moved = result.moved_count ?? 0;
|
|
1916
|
+
const skipped = result.skipped_count ?? 0;
|
|
1917
|
+
const errors = result.error_count ?? 0;
|
|
1918
|
+
const action = result.action || "moved";
|
|
1919
|
+
if (moved === 0) return `batch_move: WARNING — 0 files ${action}. ${skipped} skipped, ${errors} errors`;
|
|
1920
|
+
return `batch_move: ${moved} ${action}, ${skipped} skipped, ${errors} errors → ${args?.destination || "dest"}`;
|
|
1921
|
+
}
|
|
1922
|
+
case "move_file":
|
|
1923
|
+
case "copy_file": {
|
|
1924
|
+
const action = result.action || (name === "copy_file" ? "copied" : "moved");
|
|
1925
|
+
return `${name}: ${result.success ? action : "FAILED"} ${args?.source ? pathModule.basename(args.source) : "file"}`;
|
|
1926
|
+
}
|
|
1927
|
+
case "delete_file":
|
|
1928
|
+
return `delete_file: ${result.success ? "deleted" : "FAILED"} ${args?.path ? pathModule.basename(args.path) : "file"}`;
|
|
1929
|
+
case "write_file":
|
|
1930
|
+
return `write_file: ${result.success ? "created" : "FAILED"} ${result.path ? pathModule.basename(result.path) : "file"}`;
|
|
1931
|
+
case "create_folder":
|
|
1932
|
+
return `create_folder: ${result.already_existed ? "already existed" : "created"} ${result.path || "folder"}`;
|
|
1933
|
+
case "batch_rename": {
|
|
1934
|
+
const n = result.renamed_count ?? result.renamed ?? 0;
|
|
1935
|
+
return `batch_rename: ${n} renamed, ${result.error_count ?? 0} errors`;
|
|
1936
|
+
}
|
|
1937
|
+
case "search_video": {
|
|
1938
|
+
const n = result.results?.length || 0;
|
|
1939
|
+
return `search_video: ${n} matching moment(s) found`;
|
|
1940
|
+
}
|
|
1941
|
+
case "transcribe_audio":
|
|
1942
|
+
return `transcribe_audio: ${result.text ? result.text.length + " chars transcribed" : "done"}`;
|
|
1943
|
+
case "search_audio": {
|
|
1944
|
+
const n = result.results?.length || 0;
|
|
1945
|
+
return `search_audio: ${n} matching moment(s) found`;
|
|
1946
|
+
}
|
|
1947
|
+
case "compress_pdf":
|
|
1948
|
+
return `compress_pdf: ${result.reduction_percent ?? 0}% smaller — ${result.path ? pathModule.basename(result.path) : "file"}`;
|
|
1949
|
+
case "add_page_numbers":
|
|
1950
|
+
return `add_page_numbers: ${result.pages_numbered ?? 0} pages numbered`;
|
|
1951
|
+
case "pdf_to_word":
|
|
1952
|
+
return `pdf_to_word: ${result.pages_converted ?? 0} pages → ${result.path ? pathModule.basename(result.path) : "docx"}${result.ocr_pages ? ` (${result.ocr_pages} OCR'd)` : ""}`;
|
|
1953
|
+
case "organize_pdf":
|
|
1954
|
+
return `organize_pdf: ${result.output_pages ?? 0} pages → ${result.path ? pathModule.basename(result.path) : "pdf"}`;
|
|
1955
|
+
case "pdf_to_excel":
|
|
1956
|
+
return `pdf_to_excel: ${result.tables_exported ?? 0} table(s) → ${result.path ? pathModule.basename(result.path) : "xlsx"}`;
|
|
1957
|
+
case "score_photo":
|
|
1958
|
+
return `score_photo: ${result.total ?? "?"}${"/100"} — ${(result.reasoning || "").slice(0, 60)}`;
|
|
1959
|
+
case "cull_photos":
|
|
1960
|
+
return `cull_photos: ${result.started ? `started ${result.total_images} photos` : "FAILED"}`;
|
|
1961
|
+
case "cull_status": {
|
|
1962
|
+
if (result.status === "done")
|
|
1963
|
+
return `cull_status: done — kept ${result.kept}, rejected ${result.rejected}, report at ${result.report_path || "N/A"}`;
|
|
1964
|
+
if (result.status === "cancelled" || result.status === "cancelling")
|
|
1965
|
+
return `cull_status: cancelled after ${result.scored || 0}/${result.total || "?"} scored`;
|
|
1966
|
+
return `cull_status: ${result.status} — ${result.scored || 0}/${result.total || "?"} scored`;
|
|
1967
|
+
}
|
|
1968
|
+
case "suggest_categories":
|
|
1969
|
+
return `suggest_categories: ${result.categories ? `suggested ${result.categories.length} categories: ${result.categories.join(", ")}` : "FAILED"}`;
|
|
1970
|
+
case "group_photos":
|
|
1971
|
+
return `group_photos: ${result.started ? `started ${result.total_images} photos → ${result.categories?.length ?? "?"} categories` : "FAILED"}`;
|
|
1972
|
+
case "group_status": {
|
|
1973
|
+
if (result.status === "done")
|
|
1974
|
+
return `group_status: done — ${result.moved} grouped, report at ${result.report_path || "N/A"}`;
|
|
1975
|
+
if (result.status === "cancelled" || result.status === "cancelling")
|
|
1976
|
+
return `group_status: cancelled after ${result.classified || 0}/${result.total || "?"} classified`;
|
|
1977
|
+
return `group_status: ${result.status} — ${result.classified || 0}/${result.total || "?"} classified`;
|
|
1978
|
+
}
|
|
1979
|
+
case "gmail_send":
|
|
1980
|
+
return `gmail_send: ${result.error ? "FAILED" : `sent to ${args?.to || "recipient"}`}`;
|
|
1981
|
+
case "gmail_search":
|
|
1982
|
+
return `gmail_search: ${result.count ?? 0} email(s) found`;
|
|
1983
|
+
case "gmail_triage":
|
|
1984
|
+
return `gmail_triage: inbox summary loaded`;
|
|
1985
|
+
case "calendar_events":
|
|
1986
|
+
return `calendar_events: agenda loaded`;
|
|
1987
|
+
case "calendar_create":
|
|
1988
|
+
return `calendar_create: ${result.error ? "FAILED" : `event '${args?.summary || ""}' created`}`;
|
|
1989
|
+
case "drive_list":
|
|
1990
|
+
return `drive_list: ${result.count ?? 0} file(s) found`;
|
|
1991
|
+
case "drive_upload":
|
|
1992
|
+
return `drive_upload: ${result.error ? "FAILED" : "uploaded to Drive"}`;
|
|
1993
|
+
default:
|
|
1994
|
+
return `${name}: done`;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
module.exports = {
|
|
1999
|
+
CORE_TOOLS,
|
|
2000
|
+
TOOL_GROUPS,
|
|
2001
|
+
INTENT_KEYWORDS,
|
|
2002
|
+
SKILL_CATEGORIES,
|
|
2003
|
+
detectIntentCategories,
|
|
2004
|
+
getToolsForIntent,
|
|
2005
|
+
clearIntentCache,
|
|
2006
|
+
hasActiveIntent,
|
|
2007
|
+
TOOL_DECLARATIONS,
|
|
2008
|
+
buildToolRoutes,
|
|
2009
|
+
preValidate,
|
|
2010
|
+
summarizeToolResult,
|
|
2011
|
+
fileTools,
|
|
2012
|
+
folderTools,
|
|
2013
|
+
};
|