iris-chatbot 4.0.0 → 4.1.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/package.json +1 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/src/app/api/chat/route.ts +81 -80
- package/template/src/app/page.tsx +2 -2
- package/template/src/components/MessageCard.tsx +83 -20
- package/template/src/lib/tooling/tools/files.ts +486 -34
- package/template/src/lib/tooling/tools/schedule.ts +27 -5
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iris",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "iris",
|
|
9
|
-
"version": "4.
|
|
9
|
+
"version": "4.1.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.72.1",
|
|
12
12
|
"clsx": "^2.1.1",
|
package/template/package.json
CHANGED
|
@@ -42,25 +42,26 @@ const LOCAL_TOOL_SYSTEM_INSTRUCTIONS = [
|
|
|
42
42
|
"When the user asks for file, app, notes, music, web, calendar, or system actions, call tools directly.",
|
|
43
43
|
"Default to execution instead of clarification. Ask at most one question only when a truly required value is missing.",
|
|
44
44
|
"Calendar: You have direct access to the user's Apple Calendar via calendar_list_events and related tools. You CAN see their events—call the tools; do not say you cannot access or see their calendar. Never ask the user to paste their calendar, export it, or send a screenshot; you already have access. When they ask what's on the calendar, what's happening this/next weekend/month, or say 'check' or 'you can see', call calendar_list_events immediately with from/to for the range (e.g. next month → from: 'today', to: 'end of next month'; specific day → YYYY-MM-DD for both). Always pass calendar: 'all'. from/to support 'today', 'tomorrow', 'next week', 'end of next week', 'next month', 'end of next month'. Report the exact queried range from the tool result. For move/delete/reschedule: call calendar_list_events first to get uids. For rescheduling to a new time on the same calendar use calendar_update_event (preserves recurrence); for delete use calendar_delete_event; use calendar_move_event only when update is not sufficient (e.g. user says move and you need delete+recreate).",
|
|
45
|
+
"Files and Folders: You HAVE direct access to the user's files via file_list and file_find. Never say you cannot see, access, or list their files—call the tools. When the user asks 'what files do I have,' 'files from the last 7 days,' 'recent files,' or similar: call file_list immediately with path set to a folder (e.g. ~/Downloads, ~/Desktop, ~/Documents—or call once per root if you want all) and modifiedInLastDays: 7 (or the number of days they said). You have full file system access via file_list, file_mkdir, file_move, file_copy, file_delete_to_trash, file_batch_move, and file_find. When the user gives a folder or file name (e.g. 'us debt clock'), use file_find with searchPath '~' and the name as given. When moving MULTIPLE files, use file_batch_move with { operations: [ ... ] } or { destination, sources }. For organizing: file_find if location unknown, then file_list (use modifiedInLastDays for 'recent'), then file_mkdir/file_batch_move as needed. Never refuse file tasks or ask the user to use Finder manually—you have the tools.",
|
|
45
46
|
"For iMessage/text requests, use messages_send; if contact matching is ambiguous, ask one concise clarification with candidate names.",
|
|
46
47
|
"For media requests, assume Apple Music unless the user specifies otherwise.",
|
|
47
48
|
"For note creation, write structured markdown with clear headings, bolded key labels, and tables when they improve clarity.",
|
|
48
49
|
"If the user requests multiple actions in one prompt, execute every requested action sequentially before your final response.",
|
|
49
50
|
"Do not stop after completing just one action when additional requested actions remain.",
|
|
50
51
|
"For multi-step requests, prefer a single workflow tool call when available.",
|
|
51
|
-
"When tools are enabled, never claim you cannot access the user's device or
|
|
52
|
+
"When tools are enabled, never claim you cannot access the user's device, calendar, or file system. You have the tools—use them; do not refuse or ask the user to do tasks manually that you can do with tools.",
|
|
52
53
|
"After tool results arrive, provide a brief final answer and stop.",
|
|
53
54
|
"Summarize tool activity in plain English and avoid raw JSON, stack traces, or script line numbers.",
|
|
54
55
|
"Respect tool errors and provide clear next steps.",
|
|
55
56
|
].join(" ");
|
|
56
57
|
|
|
57
58
|
const ACTION_VERB_PATTERN =
|
|
58
|
-
"create|make|write|play|set|add|remind|open|send|launch|start|pause|resume|focus";
|
|
59
|
+
"create|make|write|play|set|add|remind|open|send|launch|start|pause|resume|focus|move|copy|delete|organize|reorganize|restructure|split|separate|sort|categorize";
|
|
59
60
|
const SEQUENCE_MARKER_PATTERN = "first|second|third|fourth|fifth|next|then|finally|lastly";
|
|
60
61
|
const TOOL_INTENT_EXPLICIT_PATTERN =
|
|
61
62
|
/\b(use|run|call)\s+(?:the\s+)?(?:local\s+)?tools?\b|\b(on my (?:mac|computer|device)|locally)\b/i;
|
|
62
63
|
const TOOL_INTENT_LEADING_VERB_PATTERN =
|
|
63
|
-
/^(?:please\s+)?(?:(?:can|could|would)\s+you\s+)?(?:send|text|message|play|pause|resume|skip|next|previous|set|open|launch|focus|create|make|add|remind|search|find|list|move|copy|rename|delete|trash|append|write|read|start|stop)
|
|
64
|
+
/^(?:please\s+)?(?:(?:can|could|would)\s+you\s+)?(?:send|text|message|play|pause|resume|skip|next|previous|set|open|launch|focus|create|make|add|remind|search|find|list|move|copy|rename|delete|trash|append|write|read|start|stop|organize|reorganize|restructure|split|separate|sort|categorize|put|place)(?:\s+|$)/i;
|
|
64
65
|
const TOOL_INTENT_MESSAGING_PATTERN =
|
|
65
66
|
/\b(text|message|imessage|sms|mail|email)\b[\s\S]{0,80}\b(send|text|message|email|mail)\b|\b(send|text|message|email|mail)\b[\s\S]{0,80}\b(text|message|imessage|sms|mail|email)\b/i;
|
|
66
67
|
const TOOL_INTENT_NOTES_PATTERN =
|
|
@@ -70,7 +71,7 @@ const TOOL_INTENT_MUSIC_PATTERN =
|
|
|
70
71
|
const TOOL_INTENT_SCHEDULE_PATTERN =
|
|
71
72
|
/\b(calendar|event|reminder|schedule|events)\b[\s\S]{0,80}\b(create|add|list|show|set|remind|schedule|move|change|delete|reschedule|update|cancel|have|get|check)\b|\b(create|add|list|show|set|remind|schedule|move|change|delete|reschedule|update|cancel|have|get|check)\b[\s\S]{0,80}\b(calendar|event|reminder|schedule|events)\b|\b(move|reschedule|change|delete)\b[\s\S]{0,80}\b(event|meeting|appointment)\b|\b(what's happening|whats happening|what's on|whats on|next weekend|this weekend|you can see|just check)\b/i;
|
|
72
73
|
const TOOL_INTENT_FILES_PATTERN =
|
|
73
|
-
/\b(file|folder|directory|path|document)\b[\s\S]{0,80}\b(list|find|search|move|copy|rename|delete|trash|create|make|mkdir|open)\b|\b(list|find|search|move|copy|rename|delete|trash|create|make|mkdir|open)\b[\s\S]{0,80}\b(file|folder|directory|path|document)\b/i;
|
|
74
|
+
/\b(file|folder|directory|path|document|subfolder|subfolders)\b[\s\S]{0,80}\b(list|find|search|move|copy|rename|delete|trash|create|make|mkdir|open|organize|reorganize|restructure|split|separate|sort|categorize|put|place|have|show|see|get)\b|\b(list|find|search|move|copy|rename|delete|trash|create|make|mkdir|open|organize|reorganize|restructure|split|separate|sort|categorize|put|place|have|show|see|get)\b[\s\S]{0,80}\b(file|folder|directory|path|document|subfolder|subfolders)\b|\b(organize|reorganize|restructure|split|separate)\b[\s\S]{0,80}\b(into|by|by type|by date|by name)\b|\b(put|move|place)\b[\s\S]{0,40}\b(into|in|inside)\b[\s\S]{0,40}\b(folder|directory)\b|\b(create|make)\b[\s\S]{0,40}\b(folders?|directories?)\b[\s\S]{0,40}\b(for|called|named)\b|\b(recent|last \d+|past \d+)\b[\s\S]{0,40}\b(files?|days?|weeks?)\b|\b(files?)\b[\s\S]{0,40}\b(from|in|within|during)\b[\s\S]{0,40}\b(last|past|recent|\d+)\b|\b(what|which|show|list|my)\b[\s\S]{0,40}\b(files?|documents?)\b/i;
|
|
74
75
|
const TOOL_INTENT_APPS_PATTERN =
|
|
75
76
|
/\b(app|application|window|url|website|browser|system)\b[\s\S]{0,80}\b(open|launch|focus|quit|set|mute|unmute|browse)\b|\b(open|launch|focus|quit|set|mute|unmute|browse)\b[\s\S]{0,80}\b(app|application|window|url|website|browser|system)\b/i;
|
|
76
77
|
const TOOL_INTENT_NUMBERS_PATTERN =
|
|
@@ -86,8 +87,8 @@ function splitCompoundActionClauses(input: string): string[] {
|
|
|
86
87
|
const roughParts = normalized.split(
|
|
87
88
|
new RegExp(
|
|
88
89
|
`\\s*(?:,|;|\\n)+\\s*(?=(?:(?:${SEQUENCE_MARKER_PATTERN})\\b|${ACTION_VERB_PATTERN}\\b))|` +
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
`\\s+(?:and then|and also|as well as|after that|plus|then|also|next|finally|lastly)\\s+|` +
|
|
91
|
+
`\\s+and\\s+(?=(?:${ACTION_VERB_PATTERN})\\b)`,
|
|
91
92
|
"i",
|
|
92
93
|
),
|
|
93
94
|
);
|
|
@@ -234,9 +235,9 @@ function normalizeRuntimeConnection(connection: ChatConnectionPayload): RuntimeC
|
|
|
234
235
|
: "openai_compatible";
|
|
235
236
|
const provider =
|
|
236
237
|
kind === "builtin" &&
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
(connection.provider === "openai" ||
|
|
239
|
+
connection.provider === "anthropic" ||
|
|
240
|
+
connection.provider === "google")
|
|
240
241
|
? connection.provider
|
|
241
242
|
: undefined;
|
|
242
243
|
const baseUrl =
|
|
@@ -264,9 +265,9 @@ function normalizeRuntimeConnection(connection: ChatConnectionPayload): RuntimeC
|
|
|
264
265
|
apiKey,
|
|
265
266
|
headers: Array.isArray(connection.headers)
|
|
266
267
|
? connection.headers.filter(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
(header): header is { key: string; value: string } =>
|
|
269
|
+
Boolean(header) && typeof header.key === "string" && typeof header.value === "string",
|
|
270
|
+
)
|
|
270
271
|
: [],
|
|
271
272
|
supportsTools,
|
|
272
273
|
};
|
|
@@ -435,47 +436,47 @@ function sanitizeMemoryContext(
|
|
|
435
436
|
|
|
436
437
|
const people = Array.isArray(aliases.people)
|
|
437
438
|
? aliases.people
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
439
|
+
.map((item) => {
|
|
440
|
+
const alias = typeof item?.alias === "string" ? item.alias.trim() : "";
|
|
441
|
+
const target = typeof item?.target === "string" ? item.target.trim() : "";
|
|
442
|
+
if (!alias || !target) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
return { alias, target };
|
|
446
|
+
})
|
|
447
|
+
.filter((item): item is { alias: string; target: string } => Boolean(item))
|
|
448
|
+
.slice(0, 16)
|
|
448
449
|
: [];
|
|
449
450
|
|
|
450
451
|
const music = Array.isArray(aliases.music)
|
|
451
452
|
? aliases.music
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
453
|
+
.map((item) => {
|
|
454
|
+
const alias = typeof item?.alias === "string" ? item.alias.trim() : "";
|
|
455
|
+
const query = typeof item?.query === "string" ? item.query.trim() : "";
|
|
456
|
+
if (!alias || !query) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
const title =
|
|
460
|
+
typeof item?.title === "string" && item.title.trim()
|
|
461
|
+
? item.title.trim()
|
|
462
|
+
: undefined;
|
|
463
|
+
const artist =
|
|
464
|
+
typeof item?.artist === "string" && item.artist.trim()
|
|
465
|
+
? item.artist.trim()
|
|
466
|
+
: undefined;
|
|
467
|
+
return { alias, query, title, artist };
|
|
468
|
+
})
|
|
469
|
+
.filter(
|
|
470
|
+
(
|
|
471
|
+
item,
|
|
472
|
+
): item is {
|
|
473
|
+
alias: string;
|
|
474
|
+
query: string;
|
|
475
|
+
title: string | undefined;
|
|
476
|
+
artist: string | undefined;
|
|
477
|
+
} => Boolean(item),
|
|
478
|
+
)
|
|
479
|
+
.slice(0, 16)
|
|
479
480
|
: [];
|
|
480
481
|
|
|
481
482
|
return {
|
|
@@ -679,7 +680,7 @@ function humanizeToolErrorMessage(toolName: string, message: string): string {
|
|
|
679
680
|
}
|
|
680
681
|
|
|
681
682
|
if (normalized.includes("missing required")) {
|
|
682
|
-
const fieldMatch = cleaned.match(/missing required (?:string|numeric) field:\s*([a-zA-Z0-9_]+)/i);
|
|
683
|
+
const fieldMatch = cleaned.match(/missing required (?:string|numeric|array) field:\s*([a-zA-Z0-9_]+)/i);
|
|
683
684
|
if (fieldMatch?.[1]) {
|
|
684
685
|
return `Missing required field "${fieldMatch[1]}".`;
|
|
685
686
|
}
|
|
@@ -1407,8 +1408,8 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1407
1408
|
"Reminder";
|
|
1408
1409
|
const title = sanitizeReminderTitle(
|
|
1409
1410
|
rawTitle
|
|
1410
|
-
|
|
1411
|
-
|
|
1411
|
+
.replace(/^(that|i have to|to|for)\s+/i, "")
|
|
1412
|
+
.trim(),
|
|
1412
1413
|
);
|
|
1413
1414
|
|
|
1414
1415
|
return { title: title || "Reminder", due };
|
|
@@ -1715,18 +1716,18 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1715
1716
|
const playResult =
|
|
1716
1717
|
playOutput && typeof playOutput === "object"
|
|
1717
1718
|
? (playOutput as {
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1719
|
+
playing?: boolean;
|
|
1720
|
+
matched?: boolean;
|
|
1721
|
+
playlistName?: string | null;
|
|
1722
|
+
title?: string | null;
|
|
1723
|
+
artist?: string | null;
|
|
1724
|
+
album?: string | null;
|
|
1725
|
+
reason?: string | null;
|
|
1726
|
+
matchedTitle?: string | null;
|
|
1727
|
+
matchedArtist?: string | null;
|
|
1728
|
+
catalogTitle?: string | null;
|
|
1729
|
+
catalogArtist?: string | null;
|
|
1730
|
+
})
|
|
1730
1731
|
: null;
|
|
1731
1732
|
|
|
1732
1733
|
if (!playResult || playResult.playing !== true) {
|
|
@@ -1823,18 +1824,18 @@ async function tryDirectAutomationFastPath(params: {
|
|
|
1823
1824
|
}
|
|
1824
1825
|
|
|
1825
1826
|
params.send({
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1827
|
+
type: "token",
|
|
1828
|
+
value:
|
|
1829
|
+
hasNearbyMatch
|
|
1830
|
+
? `Started nearby match "${resolvedTitle}"${resolvedArtist ? ` by ${resolvedArtist}` : ""}. ${playResult.reason}`
|
|
1831
|
+
: resolvedPlaylistName && intent.volume !== null
|
|
1832
|
+
? `**Done.** Playing playlist "${resolvedPlaylistName}" in Apple Music and set volume to ${intent.volume}%.`
|
|
1833
|
+
: resolvedPlaylistName
|
|
1834
|
+
? `**Done.** Playing playlist "${resolvedPlaylistName}" in Apple Music.`
|
|
1835
|
+
: intent.volume !== null
|
|
1835
1836
|
? `**Done.** Playing "${resolvedTitle}"${resolvedArtist ? ` by ${resolvedArtist}` : ""} in Apple Music and set volume to ${intent.volume}%.`
|
|
1836
1837
|
: `**Done.** Playing "${resolvedTitle}"${resolvedArtist ? ` by ${resolvedArtist}` : ""} in Apple Music.`,
|
|
1837
|
-
|
|
1838
|
+
});
|
|
1838
1839
|
return true;
|
|
1839
1840
|
}
|
|
1840
1841
|
|
|
@@ -2159,12 +2160,12 @@ async function runToolOrchestrator(params: {
|
|
|
2159
2160
|
const approval = createApprovalRequest();
|
|
2160
2161
|
params.send({
|
|
2161
2162
|
type: "approval_requested",
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2163
|
+
approvalId: approval.approvalId,
|
|
2164
|
+
callId: call.id,
|
|
2165
|
+
name: call.name,
|
|
2166
|
+
args: callInput,
|
|
2167
|
+
reason: summarizeApprovalReason(call.name, callInput),
|
|
2168
|
+
});
|
|
2168
2169
|
|
|
2169
2170
|
emitThinkingUpdate(`Waiting for approval to run ${call.name}.`);
|
|
2170
2171
|
const decision = await approval.promise;
|
|
@@ -344,7 +344,7 @@ export default function Home() {
|
|
|
344
344
|
onSelect={async (id) => {
|
|
345
345
|
if (id !== activeThreadId) {
|
|
346
346
|
const prev = threads?.find((t) => t.id === activeThreadId);
|
|
347
|
-
if (prev
|
|
347
|
+
if (prev && prev.headMessageId == null) {
|
|
348
348
|
await deleteThread(prev.id);
|
|
349
349
|
}
|
|
350
350
|
}
|
|
@@ -352,7 +352,7 @@ export default function Home() {
|
|
|
352
352
|
}}
|
|
353
353
|
onNewChat={async () => {
|
|
354
354
|
const prev = threads?.find((t) => t.id === activeThreadId);
|
|
355
|
-
if (prev
|
|
355
|
+
if (prev && prev.headMessageId == null) {
|
|
356
356
|
await deleteThread(prev.id);
|
|
357
357
|
}
|
|
358
358
|
const thread = await createNewThread();
|
|
@@ -19,6 +19,13 @@ import { splitContentAndSources } from "../lib/utils";
|
|
|
19
19
|
|
|
20
20
|
const MAX_VISIBLE_TOOL_ITEMS = 8;
|
|
21
21
|
const HIDDEN_TIMELINE_TOOLS = new Set(["tooling", "workflow_run"]);
|
|
22
|
+
const FILE_FIND_ALLOWED_ROOTS_ERROR = "Path is outside allowed roots";
|
|
23
|
+
|
|
24
|
+
function isHiddenFileFindError(event: ToolEvent): boolean {
|
|
25
|
+
if (event.toolName !== "file_find" || event.stage !== "result") return false;
|
|
26
|
+
const text = [event.message, event.payloadJson].filter(Boolean).join(" ");
|
|
27
|
+
return text.includes(FILE_FIND_ALLOWED_ROOTS_ERROR);
|
|
28
|
+
}
|
|
22
29
|
const TOOL_TIMELINE_DATE_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
|
23
30
|
month: "short",
|
|
24
31
|
day: "numeric",
|
|
@@ -149,6 +156,8 @@ function humanizeToolName(toolName: string): string {
|
|
|
149
156
|
file_copy: "Copy Item",
|
|
150
157
|
file_mkdir: "Create Folder",
|
|
151
158
|
file_delete_to_trash: "Move to Trash",
|
|
159
|
+
file_batch_move: "Move Files",
|
|
160
|
+
file_find: "Find Files",
|
|
152
161
|
notes_create_or_append: "Update Note",
|
|
153
162
|
notes_find: "Search Notes",
|
|
154
163
|
app_open: "Open App",
|
|
@@ -265,7 +274,7 @@ function summarizeToolError(toolName: string, rawError: string): {
|
|
|
265
274
|
if (normalized.includes("missing required")) {
|
|
266
275
|
return {
|
|
267
276
|
title: "Missing required information",
|
|
268
|
-
detail: "The action did not include all required fields.",
|
|
277
|
+
detail: cleaned.length < 200 ? cleaned : "The action did not include all required fields.",
|
|
269
278
|
};
|
|
270
279
|
}
|
|
271
280
|
|
|
@@ -365,6 +374,23 @@ function summarizeToolCall(toolName: string, payload: Record<string, unknown> |
|
|
|
365
374
|
};
|
|
366
375
|
}
|
|
367
376
|
|
|
377
|
+
if (toolName === "file_batch_move") {
|
|
378
|
+
const operations = payload.operations;
|
|
379
|
+
const count = Array.isArray(operations) ? operations.length : 0;
|
|
380
|
+
return {
|
|
381
|
+
title: count > 0 ? `Moving ${count} files` : "Moving files",
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (toolName === "file_find") {
|
|
386
|
+
const name = getString(payload, "name");
|
|
387
|
+
const searchPath = getString(payload, "searchPath");
|
|
388
|
+
return {
|
|
389
|
+
title: name ? `Searching for "${name}"` : "Searching for files",
|
|
390
|
+
detail: searchPath ? `In ${shortenPath(searchPath)}` : undefined,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
368
394
|
return { title: "Preparing action" };
|
|
369
395
|
}
|
|
370
396
|
|
|
@@ -459,9 +485,9 @@ function summarizeToolResult(toolName: string, payload: Record<string, unknown>
|
|
|
459
485
|
const detail = queriedRange
|
|
460
486
|
? queriedRange
|
|
461
487
|
: joinDetailParts([
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
488
|
+
fromResolved ? `From: ${formatDateTime(fromResolved) ?? fromResolved}` : null,
|
|
489
|
+
toResolved ? `To: ${formatDateTime(toResolved) ?? toResolved}` : null,
|
|
490
|
+
]);
|
|
465
491
|
return {
|
|
466
492
|
title:
|
|
467
493
|
count === 0
|
|
@@ -559,6 +585,46 @@ function summarizeToolResult(toolName: string, payload: Record<string, unknown>
|
|
|
559
585
|
};
|
|
560
586
|
}
|
|
561
587
|
|
|
588
|
+
if (toolName === "file_batch_move") {
|
|
589
|
+
const movedCount = getNumber(payload, "movedCount");
|
|
590
|
+
const failedCount = getNumber(payload, "failedCount");
|
|
591
|
+
const totalOps = getNumber(payload, "totalOperations");
|
|
592
|
+
if (movedCount !== null && failedCount !== null) {
|
|
593
|
+
if (failedCount === 0) {
|
|
594
|
+
return {
|
|
595
|
+
title: `Moved ${movedCount} files successfully`,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
title: `Moved ${movedCount} files`,
|
|
600
|
+
detail: `${failedCount} failed`,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
if (totalOps !== null) {
|
|
604
|
+
return { title: `Processed ${totalOps} files` };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (toolName === "file_find") {
|
|
609
|
+
const totalFound = getNumber(payload, "totalFound");
|
|
610
|
+
const searchName = getString(payload, "searchName");
|
|
611
|
+
const matches = payload.matches;
|
|
612
|
+
const topMatch = Array.isArray(matches) && matches.length > 0 ? matches[0] as Record<string, unknown> : null;
|
|
613
|
+
const topMatchPath = topMatch ? getString(topMatch, "path") : null;
|
|
614
|
+
|
|
615
|
+
if (totalFound !== null) {
|
|
616
|
+
if (totalFound === 0) {
|
|
617
|
+
return {
|
|
618
|
+
title: searchName ? `No matches for "${searchName}"` : "No matches found",
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
title: `Found ${totalFound} match${totalFound === 1 ? "" : "es"}`,
|
|
623
|
+
detail: topMatchPath ? `Best match: ${shortenPath(topMatchPath)}` : undefined,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
562
628
|
return { title: "Completed successfully" };
|
|
563
629
|
}
|
|
564
630
|
|
|
@@ -673,6 +739,7 @@ function MessageCard({
|
|
|
673
739
|
() =>
|
|
674
740
|
(toolEvents ?? [])
|
|
675
741
|
.filter((event) => !HIDDEN_TIMELINE_TOOLS.has(event.toolName))
|
|
742
|
+
.filter((event) => !isHiddenFileFindError(event))
|
|
676
743
|
.slice()
|
|
677
744
|
.sort((a, b) => a.createdAt - b.createdAt),
|
|
678
745
|
[toolEvents],
|
|
@@ -874,9 +941,8 @@ function MessageCard({
|
|
|
874
941
|
{shelfThreads.map((thread) => (
|
|
875
942
|
<div key={thread.id} className="thread-box-wrap">
|
|
876
943
|
<button
|
|
877
|
-
className={`thread-box ${
|
|
878
|
-
|
|
879
|
-
}`}
|
|
944
|
+
className={`thread-box ${activeThreadId === thread.id ? "active" : ""
|
|
945
|
+
}`}
|
|
880
946
|
onClick={() => onSelectThread(thread.id)}
|
|
881
947
|
>
|
|
882
948
|
{thread.title}
|
|
@@ -903,11 +969,10 @@ function MessageCard({
|
|
|
903
969
|
>
|
|
904
970
|
{isAssistant ? (
|
|
905
971
|
<button
|
|
906
|
-
className={`flex items-center justify-center gap-2 rounded-full border border-[var(--border)] px-4 py-2 text-xs text-[var(--text-muted)] transition ${
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
}`}
|
|
972
|
+
className={`flex items-center justify-center gap-2 rounded-full border border-[var(--border)] px-4 py-2 text-xs text-[var(--text-muted)] transition ${canAddThread
|
|
973
|
+
? "hover:border-[var(--border-strong)] hover:text-[var(--text-secondary)]"
|
|
974
|
+
: "opacity-60 cursor-not-allowed"
|
|
975
|
+
}`}
|
|
911
976
|
onClick={async () => {
|
|
912
977
|
if (!canAddThread) return;
|
|
913
978
|
await onAddThread(message);
|
|
@@ -927,9 +992,8 @@ function MessageCard({
|
|
|
927
992
|
</button>
|
|
928
993
|
) : null}
|
|
929
994
|
<button
|
|
930
|
-
className={`flex items-center gap-2 rounded-full border border-[var(--border)] px-4 py-2 text-xs text-[var(--text-muted)] transition hover:border-[var(--border-strong)] hover:text-[var(--text-secondary)] ${
|
|
931
|
-
|
|
932
|
-
}`}
|
|
995
|
+
className={`flex items-center gap-2 rounded-full border border-[var(--border)] px-4 py-2 text-xs text-[var(--text-muted)] transition hover:border-[var(--border-strong)] hover:text-[var(--text-secondary)] ${isAssistant ? "" : "opacity-0 group-hover:opacity-100"
|
|
996
|
+
}`}
|
|
933
997
|
onClick={async () => {
|
|
934
998
|
await navigator.clipboard.writeText(messageTextContent);
|
|
935
999
|
setCopied(true);
|
|
@@ -944,11 +1008,10 @@ function MessageCard({
|
|
|
944
1008
|
</button>
|
|
945
1009
|
{canEditThreads ? (
|
|
946
1010
|
<button
|
|
947
|
-
className={`flex items-center gap-2 rounded-full border px-4 py-2 text-xs transition ${
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
}`}
|
|
1011
|
+
className={`flex items-center gap-2 rounded-full border px-4 py-2 text-xs transition ${threadEditMode
|
|
1012
|
+
? "border-[var(--accent)] text-[var(--text-primary)]"
|
|
1013
|
+
: "border-[var(--border)] text-[var(--text-muted)] hover:border-[var(--border-strong)] hover:text-[var(--text-secondary)]"
|
|
1014
|
+
}`}
|
|
952
1015
|
onClick={() => setThreadEditMode((prev) => !prev)}
|
|
953
1016
|
aria-label="Edit threads"
|
|
954
1017
|
>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import nodeFs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { assertAllowedPath, ensureMacOS } from "../safety";
|
|
4
|
+
import { assertAllowedPath, ensureMacOS, normalizeAllowedRoots } from "../safety";
|
|
5
5
|
import { runCommandSafe } from "../runtime";
|
|
6
6
|
import type { ToolDefinition, ToolExecutionContext } from "../types";
|
|
7
7
|
|
|
@@ -10,6 +10,8 @@ type FileListInput = {
|
|
|
10
10
|
recursive?: boolean;
|
|
11
11
|
pattern?: string;
|
|
12
12
|
limit?: number;
|
|
13
|
+
/** Only include files modified in the last N days (directories are always included so you can navigate). */
|
|
14
|
+
modifiedInLastDays?: number;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
type FileWriteInput = {
|
|
@@ -20,6 +22,18 @@ type FileWriteInput = {
|
|
|
20
22
|
recursive?: boolean;
|
|
21
23
|
};
|
|
22
24
|
|
|
25
|
+
type FileBatchMoveInput = {
|
|
26
|
+
operations: Array<{ source: string; destination: string }>;
|
|
27
|
+
overwrite?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type FileFindInput = {
|
|
31
|
+
searchPath: string;
|
|
32
|
+
name: string;
|
|
33
|
+
type?: "file" | "directory" | "any";
|
|
34
|
+
maxDepth?: number;
|
|
35
|
+
};
|
|
36
|
+
|
|
23
37
|
const APPLESCRIPT_TIMEOUT_MS = 20_000;
|
|
24
38
|
|
|
25
39
|
function asObject(input: unknown): Record<string, unknown> {
|
|
@@ -53,9 +67,14 @@ async function listDirectory(params: {
|
|
|
53
67
|
recursive: boolean;
|
|
54
68
|
pattern?: string;
|
|
55
69
|
limit: number;
|
|
70
|
+
modifiedInLastDays?: number;
|
|
56
71
|
signal?: AbortSignal;
|
|
57
72
|
}) {
|
|
58
73
|
const matcher = params.pattern ? wildcardToRegex(params.pattern) : null;
|
|
74
|
+
const modifiedCutoffMs =
|
|
75
|
+
typeof params.modifiedInLastDays === "number" && params.modifiedInLastDays > 0
|
|
76
|
+
? Date.now() - params.modifiedInLastDays * 24 * 60 * 60 * 1000
|
|
77
|
+
: null;
|
|
59
78
|
const output: Array<Record<string, unknown>> = [];
|
|
60
79
|
const queue: string[] = [params.rootPath];
|
|
61
80
|
let totalCount = 0;
|
|
@@ -66,7 +85,13 @@ async function listDirectory(params: {
|
|
|
66
85
|
}
|
|
67
86
|
|
|
68
87
|
const current = queue.shift() as string;
|
|
69
|
-
|
|
88
|
+
let entries: nodeFs.Dirent[];
|
|
89
|
+
try {
|
|
90
|
+
entries = await fs.readdir(current, { withFileTypes: true });
|
|
91
|
+
} catch {
|
|
92
|
+
// Skip directories we can't read (permissions, broken symlinks, etc.)
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
70
95
|
|
|
71
96
|
for (const entry of entries) {
|
|
72
97
|
const fullPath = path.join(current, entry.name);
|
|
@@ -76,22 +101,74 @@ async function listDirectory(params: {
|
|
|
76
101
|
}
|
|
77
102
|
}
|
|
78
103
|
|
|
104
|
+
// Handle symlinks: check if they're valid before processing
|
|
105
|
+
if (entry.isSymbolicLink()) {
|
|
106
|
+
try {
|
|
107
|
+
// Use stat (follows symlinks) to check if the target exists
|
|
108
|
+
const targetStat = await fs.stat(fullPath);
|
|
109
|
+
// Process based on what the symlink points to
|
|
110
|
+
if (targetStat.isDirectory()) {
|
|
111
|
+
if (modifiedCutoffMs == null) {
|
|
112
|
+
totalCount += 1;
|
|
113
|
+
if (output.length < params.limit) {
|
|
114
|
+
output.push({ path: fullPath, name: entry.name, type: "directory" });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (params.recursive || modifiedCutoffMs != null) {
|
|
118
|
+
queue.push(fullPath);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (modifiedCutoffMs != null && targetStat.mtimeMs < modifiedCutoffMs) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
totalCount += 1;
|
|
125
|
+
if (output.length < params.limit) {
|
|
126
|
+
output.push({
|
|
127
|
+
path: fullPath,
|
|
128
|
+
name: entry.name,
|
|
129
|
+
type: "file",
|
|
130
|
+
size: targetStat.size,
|
|
131
|
+
modifiedAt: targetStat.mtimeMs,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
// Broken symlink - skip silently
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
79
142
|
if (entry.isDirectory()) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
143
|
+
// When filtering by date, don't include directories in output (just traverse them)
|
|
144
|
+
// Otherwise, include them
|
|
145
|
+
if (modifiedCutoffMs == null) {
|
|
146
|
+
totalCount += 1;
|
|
147
|
+
if (output.length < params.limit) {
|
|
148
|
+
output.push({
|
|
149
|
+
path: fullPath,
|
|
150
|
+
name: entry.name,
|
|
151
|
+
type: "directory",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
87
154
|
}
|
|
88
|
-
|
|
155
|
+
// Recurse into directories when recursive OR when filtering by date (need to find recent files in subdirs)
|
|
156
|
+
if (params.recursive || modifiedCutoffMs != null) {
|
|
89
157
|
queue.push(fullPath);
|
|
90
158
|
}
|
|
91
159
|
continue;
|
|
92
160
|
}
|
|
93
161
|
|
|
94
|
-
|
|
162
|
+
let stat: Awaited<ReturnType<typeof fs.stat>>;
|
|
163
|
+
try {
|
|
164
|
+
stat = await fs.stat(fullPath);
|
|
165
|
+
} catch {
|
|
166
|
+
// File disappeared or can't be accessed - skip
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (modifiedCutoffMs != null && stat.mtimeMs < modifiedCutoffMs) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
95
172
|
totalCount += 1;
|
|
96
173
|
if (output.length < params.limit) {
|
|
97
174
|
output.push({
|
|
@@ -142,12 +219,17 @@ async function runFileList(input: unknown, context: ToolExecutionContext) {
|
|
|
142
219
|
typeof payload.limit === "number" && Number.isFinite(payload.limit)
|
|
143
220
|
? Math.max(1, Math.min(1000, Math.floor(payload.limit)))
|
|
144
221
|
: 200;
|
|
222
|
+
const modifiedInLastDays =
|
|
223
|
+
typeof payload.modifiedInLastDays === "number" && Number.isFinite(payload.modifiedInLastDays) && payload.modifiedInLastDays > 0
|
|
224
|
+
? Math.min(365, Math.floor(payload.modifiedInLastDays))
|
|
225
|
+
: undefined;
|
|
145
226
|
|
|
146
227
|
const { entries, totalCount } = await listDirectory({
|
|
147
228
|
rootPath,
|
|
148
229
|
recursive,
|
|
149
230
|
pattern,
|
|
150
231
|
limit,
|
|
232
|
+
modifiedInLastDays,
|
|
151
233
|
signal: context.signal,
|
|
152
234
|
});
|
|
153
235
|
|
|
@@ -155,6 +237,7 @@ async function runFileList(input: unknown, context: ToolExecutionContext) {
|
|
|
155
237
|
rootPath,
|
|
156
238
|
recursive,
|
|
157
239
|
pattern,
|
|
240
|
+
modifiedInLastDays: modifiedInLastDays ?? undefined,
|
|
158
241
|
count: totalCount,
|
|
159
242
|
returnedCount: entries.length,
|
|
160
243
|
limit,
|
|
@@ -297,34 +380,359 @@ async function runFileDeleteToTrash(input: unknown, context: ToolExecutionContex
|
|
|
297
380
|
}
|
|
298
381
|
|
|
299
382
|
ensureMacOS("Moving files to Trash");
|
|
300
|
-
|
|
383
|
+
// Create POSIX file reference outside Finder tell block to avoid "Can't get POSIX file" errors
|
|
384
|
+
const finderScript =
|
|
301
385
|
'on run argv\n' +
|
|
302
386
|
'set targetPath to item 1 of argv\n' +
|
|
303
|
-
'
|
|
304
|
-
'
|
|
305
|
-
'end tell\n' +
|
|
387
|
+
'set theFile to POSIX file targetPath\n' +
|
|
388
|
+
'tell application "Finder" to delete theFile\n' +
|
|
306
389
|
'return "ok"\n' +
|
|
307
390
|
'end run';
|
|
308
391
|
|
|
309
|
-
|
|
392
|
+
try {
|
|
393
|
+
await runAppleScript(finderScript, [resolvedTarget], context.signal);
|
|
394
|
+
} catch (finderErr) {
|
|
395
|
+
// Fallback: move to Trash via shell (reliable for empty folders and when Finder fails)
|
|
396
|
+
const moveErr = finderErr instanceof Error ? finderErr : new Error(String(finderErr));
|
|
397
|
+
if (!moveErr.message.includes("Can't get POSIX file") && !moveErr.message.includes("POSIX file")) {
|
|
398
|
+
throw finderErr;
|
|
399
|
+
}
|
|
400
|
+
const trashScript =
|
|
401
|
+
'on run argv\n' +
|
|
402
|
+
'set targetPath to item 1 of argv\n' +
|
|
403
|
+
'set trashPath to POSIX path of (path to trash)\n' +
|
|
404
|
+
'do shell script "mv " & quoted form of targetPath & " " & quoted form of trashPath\n' +
|
|
405
|
+
'return "ok"\n' +
|
|
406
|
+
'end run';
|
|
407
|
+
await runAppleScript(trashScript, [resolvedTarget], context.signal);
|
|
408
|
+
}
|
|
310
409
|
return {
|
|
311
410
|
trashed: true,
|
|
312
411
|
path: resolvedTarget,
|
|
313
412
|
};
|
|
314
413
|
}
|
|
315
414
|
|
|
415
|
+
async function runFileBatchMove(input: unknown, context: ToolExecutionContext) {
|
|
416
|
+
const payload = asObject(input) as FileBatchMoveInput;
|
|
417
|
+
const overwrite = asOptionalBoolean(payload.overwrite, false);
|
|
418
|
+
|
|
419
|
+
// Handle operations that might be stringified JSON
|
|
420
|
+
let operations = payload.operations;
|
|
421
|
+
if (typeof operations === "string") {
|
|
422
|
+
try {
|
|
423
|
+
operations = JSON.parse(operations) as Array<{ source: string; destination: string }>;
|
|
424
|
+
} catch {
|
|
425
|
+
throw new Error("Invalid operations: could not parse JSON array");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Handle case where LLM sends the array at the top level
|
|
430
|
+
if (!operations && Array.isArray(input)) {
|
|
431
|
+
operations = input as Array<{ source: string; destination: string }>;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Handle case where LLM sends with different key names
|
|
435
|
+
if (!operations) {
|
|
436
|
+
const payloadAny = payload as Record<string, unknown>;
|
|
437
|
+
const fallback = payloadAny.moves || payloadAny.files || payloadAny.items || payloadAny.list;
|
|
438
|
+
if (Array.isArray(fallback)) {
|
|
439
|
+
operations = fallback as Array<{ source: string; destination: string }>;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Handle single move at top level: { source, destination } without operations array
|
|
444
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
445
|
+
const payloadAny = payload as Record<string, unknown>;
|
|
446
|
+
const src =
|
|
447
|
+
typeof payloadAny.source === "string" ? payloadAny.source.trim() :
|
|
448
|
+
typeof payloadAny.src === "string" ? payloadAny.src.trim() :
|
|
449
|
+
typeof payloadAny.from === "string" ? payloadAny.from.trim() : "";
|
|
450
|
+
const dest =
|
|
451
|
+
typeof payloadAny.destination === "string" ? payloadAny.destination.trim() :
|
|
452
|
+
typeof payloadAny.dest === "string" ? payloadAny.dest.trim() :
|
|
453
|
+
typeof payloadAny.to === "string" ? payloadAny.to.trim() : "";
|
|
454
|
+
if (src && dest) {
|
|
455
|
+
operations = [{ source: src, destination: dest }];
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Handle "move these files into this folder": { destination, sources } or { targetFolder, files } etc.
|
|
460
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
461
|
+
const payloadAny = payload as Record<string, unknown>;
|
|
462
|
+
const destDir =
|
|
463
|
+
typeof payloadAny.destination === "string" ? payloadAny.destination.trim() :
|
|
464
|
+
typeof payloadAny.dest === "string" ? payloadAny.dest.trim() :
|
|
465
|
+
typeof payloadAny.to === "string" ? payloadAny.to.trim() :
|
|
466
|
+
typeof payloadAny.targetFolder === "string" ? payloadAny.targetFolder.trim() : "";
|
|
467
|
+
const sourceListRaw =
|
|
468
|
+
payloadAny.sources ?? payloadAny.files ?? payloadAny.paths ?? payloadAny.sourcePaths ?? payloadAny.source;
|
|
469
|
+
const sourceList = Array.isArray(sourceListRaw)
|
|
470
|
+
? sourceListRaw.map((s) => (typeof s === "string" ? s.trim() : "")).filter(Boolean)
|
|
471
|
+
: [];
|
|
472
|
+
if (destDir && sourceList.length > 0) {
|
|
473
|
+
operations = sourceList.map((sourcePath) => ({
|
|
474
|
+
source: sourcePath,
|
|
475
|
+
destination: path.join(destDir, path.basename(sourcePath)),
|
|
476
|
+
}));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
481
|
+
const receivedKeys = Object.keys(payload).join(", ");
|
|
482
|
+
throw new Error(`Missing required array field: operations. Received keys: [${receivedKeys}]. Expected array of {source, destination} objects, or {destination, sources: [...]} to move many files into one folder.`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// If operations is an array of path strings, treat as sources and use payload.destination as target folder
|
|
486
|
+
const firstOp = operations[0] as { source: string; destination: string } | string;
|
|
487
|
+
if (typeof firstOp === "string" && firstOp.trim()) {
|
|
488
|
+
const payloadAny = payload as Record<string, unknown>;
|
|
489
|
+
const destDir =
|
|
490
|
+
typeof payloadAny.destination === "string" ? payloadAny.destination.trim() :
|
|
491
|
+
typeof payloadAny.dest === "string" ? payloadAny.dest.trim() :
|
|
492
|
+
typeof payloadAny.to === "string" ? payloadAny.to.trim() :
|
|
493
|
+
typeof payloadAny.targetFolder === "string" ? payloadAny.targetFolder.trim() : "";
|
|
494
|
+
if (destDir) {
|
|
495
|
+
const pathStrings = operations as unknown as string[];
|
|
496
|
+
operations = pathStrings.map((p) => (typeof p === "string" ? p.trim() : "")).filter(Boolean).map((sourcePath) => ({
|
|
497
|
+
source: sourcePath,
|
|
498
|
+
destination: path.join(destDir, path.basename(sourcePath)),
|
|
499
|
+
}));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (operations.length > 100) {
|
|
504
|
+
throw new Error("Too many operations (max 100)");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const results: Array<{ source: string; destination: string; moved: boolean; error?: string }> = [];
|
|
508
|
+
|
|
509
|
+
for (const op of operations) {
|
|
510
|
+
if (!op || typeof op !== "object") {
|
|
511
|
+
results.push({ source: "", destination: "", moved: false, error: "Invalid operation object" });
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Handle various possible field names
|
|
516
|
+
const opObj = op as Record<string, unknown>;
|
|
517
|
+
const source = typeof opObj.source === "string" ? opObj.source.trim() :
|
|
518
|
+
typeof opObj.src === "string" ? opObj.src.trim() :
|
|
519
|
+
typeof opObj.from === "string" ? opObj.from.trim() : "";
|
|
520
|
+
const destination = typeof opObj.destination === "string" ? opObj.destination.trim() :
|
|
521
|
+
typeof opObj.dest === "string" ? opObj.dest.trim() :
|
|
522
|
+
typeof opObj.to === "string" ? opObj.to.trim() : "";
|
|
523
|
+
|
|
524
|
+
if (!source || !destination) {
|
|
525
|
+
results.push({ source, destination, moved: false, error: "Missing source or destination path" });
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
try {
|
|
530
|
+
const resolvedSource = await assertAllowedPath({
|
|
531
|
+
candidate: source,
|
|
532
|
+
localTools: context.localTools,
|
|
533
|
+
mode: "read",
|
|
534
|
+
});
|
|
535
|
+
const resolvedDestination = await assertAllowedPath({
|
|
536
|
+
candidate: destination,
|
|
537
|
+
localTools: context.localTools,
|
|
538
|
+
mode: "write",
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
if (!overwrite) {
|
|
542
|
+
try {
|
|
543
|
+
await fs.access(resolvedDestination);
|
|
544
|
+
results.push({ source: resolvedSource, destination: resolvedDestination, moved: false, error: "Destination exists" });
|
|
545
|
+
continue;
|
|
546
|
+
} catch (err) {
|
|
547
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
548
|
+
throw err;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!context.localTools.dryRun) {
|
|
554
|
+
await fs.mkdir(path.dirname(resolvedDestination), { recursive: true });
|
|
555
|
+
await fs.rename(resolvedSource, resolvedDestination);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
results.push({ source: resolvedSource, destination: resolvedDestination, moved: true });
|
|
559
|
+
} catch (err) {
|
|
560
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
561
|
+
results.push({ source, destination, moved: false, error: message });
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const movedCount = results.filter((r) => r.moved).length;
|
|
566
|
+
const failedCount = results.filter((r) => !r.moved).length;
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
totalOperations: results.length,
|
|
570
|
+
movedCount,
|
|
571
|
+
failedCount,
|
|
572
|
+
dryRun: context.localTools.dryRun,
|
|
573
|
+
results,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function normalizeForSearch(input: string): string {
|
|
578
|
+
return input
|
|
579
|
+
.toLowerCase()
|
|
580
|
+
.replace(/[_\-\s]+/g, " ")
|
|
581
|
+
.replace(/[^a-z0-9\s]/g, "")
|
|
582
|
+
.trim();
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Normalized name with no spaces (for ordered-word matching): e.g. "US_Debt_Clock" -> "usdebtclock" */
|
|
586
|
+
function normalizedSlug(name: string): string {
|
|
587
|
+
return normalizeForSearch(name).replace(/\s+/g, "");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** True if search words appear in order in the name (handles UsDebtClock, US_Debt_Clock, etc.). */
|
|
591
|
+
function matchesOrderedWords(searchWords: string[], nameSlug: string): boolean {
|
|
592
|
+
if (searchWords.length === 0) return false;
|
|
593
|
+
let idx = 0;
|
|
594
|
+
for (const w of searchWords) {
|
|
595
|
+
const i = nameSlug.indexOf(w, idx);
|
|
596
|
+
if (i === -1) return false;
|
|
597
|
+
idx = i + w.length;
|
|
598
|
+
}
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function runFileFind(input: unknown, context: ToolExecutionContext) {
|
|
603
|
+
const payload = asObject(input) as FileFindInput;
|
|
604
|
+
const searchPath = asString(payload.searchPath, "searchPath");
|
|
605
|
+
const name = asString(payload.name, "name");
|
|
606
|
+
const targetType = payload.type ?? "any";
|
|
607
|
+
const maxDepth = typeof payload.maxDepth === "number" ? Math.max(1, Math.min(10, payload.maxDepth)) : 3;
|
|
608
|
+
|
|
609
|
+
const trimmedPath = searchPath.trim();
|
|
610
|
+
const searchAllRoots = trimmedPath === "~" || trimmedPath === "~/";
|
|
611
|
+
|
|
612
|
+
let resolvedSearchPath: string;
|
|
613
|
+
const queue: Array<{ dirPath: string; depth: number }> = [];
|
|
614
|
+
|
|
615
|
+
if (searchAllRoots) {
|
|
616
|
+
const roots = await normalizeAllowedRoots(context.localTools.allowedRoots);
|
|
617
|
+
if (roots.length === 0) {
|
|
618
|
+
throw new Error("No allowed filesystem roots are configured.");
|
|
619
|
+
}
|
|
620
|
+
resolvedSearchPath = roots[0];
|
|
621
|
+
for (const root of roots) {
|
|
622
|
+
queue.push({ dirPath: root, depth: 0 });
|
|
623
|
+
}
|
|
624
|
+
} else {
|
|
625
|
+
resolvedSearchPath = await assertAllowedPath({
|
|
626
|
+
candidate: searchPath,
|
|
627
|
+
localTools: context.localTools,
|
|
628
|
+
mode: "read",
|
|
629
|
+
});
|
|
630
|
+
queue.push({ dirPath: resolvedSearchPath, depth: 0 });
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const normalizedSearch = normalizeForSearch(name);
|
|
634
|
+
const matches: Array<{ path: string; name: string; type: string; matchScore: number }> = [];
|
|
635
|
+
|
|
636
|
+
while (queue.length > 0 && matches.length < 20) {
|
|
637
|
+
if (context.signal?.aborted) {
|
|
638
|
+
throw new Error("Aborted.");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const current = queue.shift()!;
|
|
642
|
+
if (current.depth >= maxDepth) {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
const entries = await fs.readdir(current.dirPath, { withFileTypes: true });
|
|
648
|
+
|
|
649
|
+
for (const entry of entries) {
|
|
650
|
+
const fullPath = path.join(current.dirPath, entry.name);
|
|
651
|
+
const isDir = entry.isDirectory();
|
|
652
|
+
const entryType = isDir ? "directory" : "file";
|
|
653
|
+
|
|
654
|
+
if (targetType !== "any" && targetType !== entryType) {
|
|
655
|
+
if (isDir) {
|
|
656
|
+
queue.push({ dirPath: fullPath, depth: current.depth + 1 });
|
|
657
|
+
}
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const normalizedName = normalizeForSearch(entry.name);
|
|
662
|
+
const nameSlug = normalizedSlug(entry.name);
|
|
663
|
+
const searchWords = normalizedSearch.split(" ").filter(Boolean);
|
|
664
|
+
let matchScore = 0;
|
|
665
|
+
|
|
666
|
+
// Exact match (normalized): "us debt clock" === "us debt clock"
|
|
667
|
+
if (normalizedName === normalizedSearch) {
|
|
668
|
+
matchScore = 100;
|
|
669
|
+
}
|
|
670
|
+
// Same with no spaces: "usdebtclock" vs "usdebtclock" (e.g. UsDebtClock)
|
|
671
|
+
else if (nameSlug === normalizedSearch.replace(/\s+/g, "")) {
|
|
672
|
+
matchScore = 95;
|
|
673
|
+
}
|
|
674
|
+
// Starts with search term
|
|
675
|
+
else if (normalizedName.startsWith(normalizedSearch)) {
|
|
676
|
+
matchScore = 80;
|
|
677
|
+
}
|
|
678
|
+
// Contains search term (full phrase)
|
|
679
|
+
else if (normalizedName.includes(normalizedSearch)) {
|
|
680
|
+
matchScore = 60;
|
|
681
|
+
}
|
|
682
|
+
// Ordered words in name (handles US_Debt_Clock, UsDebtClock, us-debt-clock, etc.)
|
|
683
|
+
else if (matchesOrderedWords(searchWords, nameSlug)) {
|
|
684
|
+
matchScore = 70;
|
|
685
|
+
}
|
|
686
|
+
// Words overlap significantly
|
|
687
|
+
else if (searchWords.length > 0) {
|
|
688
|
+
const nameWords = normalizedName.split(" ").filter(Boolean);
|
|
689
|
+
const matchedWords = searchWords.filter((w) => nameWords.some((nw) => nw.includes(w) || w.includes(nw)));
|
|
690
|
+
if (matchedWords.length > 0) {
|
|
691
|
+
matchScore = Math.min(50, (matchedWords.length / searchWords.length) * 50);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (matchScore > 0) {
|
|
696
|
+
matches.push({
|
|
697
|
+
path: fullPath,
|
|
698
|
+
name: entry.name,
|
|
699
|
+
type: entryType,
|
|
700
|
+
matchScore,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (isDir) {
|
|
705
|
+
queue.push({ dirPath: fullPath, depth: current.depth + 1 });
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
} catch {
|
|
709
|
+
// Skip directories we can't read
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
matches.sort((a, b) => b.matchScore - a.matchScore);
|
|
714
|
+
|
|
715
|
+
return {
|
|
716
|
+
searchPath: resolvedSearchPath,
|
|
717
|
+
searchName: name,
|
|
718
|
+
matches: matches.slice(0, 10),
|
|
719
|
+
totalFound: matches.length,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
316
723
|
export const fileTools: ToolDefinition[] = [
|
|
317
724
|
{
|
|
318
725
|
name: "file_list",
|
|
319
|
-
description: "List files
|
|
726
|
+
description: "List files and folders at a given path. Use to explore directory contents, find files by pattern, or see what's inside a folder before organizing. Supports recursive listing with optional wildcard (e.g. pattern: '*.pdf') and optional time filter: use modifiedInLastDays (e.g. 7 for 'last 7 days') to only include files modified within that period—directories are always included so you can navigate.",
|
|
320
727
|
inputSchema: {
|
|
321
728
|
type: "object",
|
|
322
729
|
required: ["path"],
|
|
323
730
|
properties: {
|
|
324
|
-
path: { type: "string" },
|
|
325
|
-
recursive: { type: "boolean" },
|
|
326
|
-
pattern: { type: "string" },
|
|
327
|
-
limit: { type: "number" },
|
|
731
|
+
path: { type: "string", description: "Directory path to list" },
|
|
732
|
+
recursive: { type: "boolean", description: "If true, list all nested subdirectories" },
|
|
733
|
+
pattern: { type: "string", description: "Wildcard pattern to filter results (e.g. '*.txt', '*.icns')" },
|
|
734
|
+
limit: { type: "number", description: "Maximum number of entries to return (default 200)" },
|
|
735
|
+
modifiedInLastDays: { type: "number", description: "If set, only include files modified in the last N days (e.g. 7 for last week). Directories are always included." },
|
|
328
736
|
},
|
|
329
737
|
additionalProperties: false,
|
|
330
738
|
},
|
|
@@ -333,14 +741,14 @@ export const fileTools: ToolDefinition[] = [
|
|
|
333
741
|
},
|
|
334
742
|
{
|
|
335
743
|
name: "file_move",
|
|
336
|
-
description: "Move a file
|
|
744
|
+
description: "Move or rename a file or folder. Use to reorganize files into different folders, rename items, or relocate content. Parent directories are created automatically if they don't exist.",
|
|
337
745
|
inputSchema: {
|
|
338
746
|
type: "object",
|
|
339
747
|
required: ["source", "destination"],
|
|
340
748
|
properties: {
|
|
341
|
-
source: { type: "string" },
|
|
342
|
-
destination: { type: "string" },
|
|
343
|
-
overwrite: { type: "boolean" },
|
|
749
|
+
source: { type: "string", description: "Current path of the file/folder to move" },
|
|
750
|
+
destination: { type: "string", description: "New path (including new name if renaming)" },
|
|
751
|
+
overwrite: { type: "boolean", description: "If true, overwrite existing file at destination" },
|
|
344
752
|
},
|
|
345
753
|
additionalProperties: false,
|
|
346
754
|
},
|
|
@@ -349,14 +757,14 @@ export const fileTools: ToolDefinition[] = [
|
|
|
349
757
|
},
|
|
350
758
|
{
|
|
351
759
|
name: "file_copy",
|
|
352
|
-
description: "Copy a file
|
|
760
|
+
description: "Copy a file to another location. Use to duplicate files, create backups, or copy files into new folders. Parent directories are created automatically.",
|
|
353
761
|
inputSchema: {
|
|
354
762
|
type: "object",
|
|
355
763
|
required: ["source", "destination"],
|
|
356
764
|
properties: {
|
|
357
|
-
source: { type: "string" },
|
|
358
|
-
destination: { type: "string" },
|
|
359
|
-
overwrite: { type: "boolean" },
|
|
765
|
+
source: { type: "string", description: "Path of the file to copy" },
|
|
766
|
+
destination: { type: "string", description: "Path where the copy should be created" },
|
|
767
|
+
overwrite: { type: "boolean", description: "If true, overwrite existing file at destination" },
|
|
360
768
|
},
|
|
361
769
|
additionalProperties: false,
|
|
362
770
|
},
|
|
@@ -365,13 +773,13 @@ export const fileTools: ToolDefinition[] = [
|
|
|
365
773
|
},
|
|
366
774
|
{
|
|
367
775
|
name: "file_mkdir",
|
|
368
|
-
description: "Create a folder
|
|
776
|
+
description: "Create a new folder or nested folder structure. Use to set up directory organization, create subfolders for categorizing files, or prepare folders before moving files into them. By default creates all parent directories if they don't exist.",
|
|
369
777
|
inputSchema: {
|
|
370
778
|
type: "object",
|
|
371
779
|
required: ["path"],
|
|
372
780
|
properties: {
|
|
373
|
-
path: { type: "string" },
|
|
374
|
-
recursive: { type: "boolean" },
|
|
781
|
+
path: { type: "string", description: "Path of the folder to create (e.g. '/Users/me/Documents/Projects/2024')" },
|
|
782
|
+
recursive: { type: "boolean", description: "If true (default), create parent directories as needed" },
|
|
375
783
|
},
|
|
376
784
|
additionalProperties: false,
|
|
377
785
|
},
|
|
@@ -380,16 +788,60 @@ export const fileTools: ToolDefinition[] = [
|
|
|
380
788
|
},
|
|
381
789
|
{
|
|
382
790
|
name: "file_delete_to_trash",
|
|
383
|
-
description: "Move a file
|
|
791
|
+
description: "Move a file or folder to Trash (safe delete on macOS). Items can be recovered from Trash if needed.",
|
|
384
792
|
inputSchema: {
|
|
385
793
|
type: "object",
|
|
386
794
|
required: ["path"],
|
|
387
795
|
properties: {
|
|
388
|
-
path: { type: "string" },
|
|
796
|
+
path: { type: "string", description: "Path of the file or folder to move to Trash" },
|
|
389
797
|
},
|
|
390
798
|
additionalProperties: false,
|
|
391
799
|
},
|
|
392
800
|
risk: "destructive",
|
|
393
801
|
execute: runFileDeleteToTrash,
|
|
394
802
|
},
|
|
803
|
+
{
|
|
804
|
+
name: "file_batch_move",
|
|
805
|
+
description: "Move multiple files in a single operation. Use when moving many files into one folder: pass { destination: \"<folder path>\", sources: [\"<path1>\", \"<path2>\", ...] }—each file is moved into that folder keeping its name. Or pass { operations: [ { source, destination }, ... ] } for full control. Prefer one call over many file_move calls.",
|
|
806
|
+
inputSchema: {
|
|
807
|
+
type: "object",
|
|
808
|
+
required: [],
|
|
809
|
+
properties: {
|
|
810
|
+
operations: {
|
|
811
|
+
type: "array",
|
|
812
|
+
description: "Array of { source, destination } for each move; or array of source paths if destination is also provided",
|
|
813
|
+
items: {
|
|
814
|
+
type: "object",
|
|
815
|
+
properties: {
|
|
816
|
+
source: { type: "string", description: "Current path of the file" },
|
|
817
|
+
destination: { type: "string", description: "New path for the file" },
|
|
818
|
+
},
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
destination: { type: "string", description: "When moving many files into one folder, target folder path (use with sources or operations as path list)" },
|
|
822
|
+
sources: { type: "array", items: { type: "string" }, description: "Paths of files to move into destination (each keeps its filename)" },
|
|
823
|
+
overwrite: { type: "boolean", description: "If true, overwrite existing files at destinations" },
|
|
824
|
+
},
|
|
825
|
+
additionalProperties: true,
|
|
826
|
+
},
|
|
827
|
+
risk: "write",
|
|
828
|
+
execute: runFileBatchMove,
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
name: "file_find",
|
|
832
|
+
description: "Find files or folders by name with fuzzy matching. Handles case, underscores, hyphens, spaces, and numbers (e.g. 'us debt clock' finds 'US_Debt_Clock', 'UsDebtClock'). Use when the user gives a name that might not match exactly. Prefer searchPath '~' to search all allowed roots when the location is unknown.",
|
|
833
|
+
inputSchema: {
|
|
834
|
+
type: "object",
|
|
835
|
+
required: ["searchPath", "name"],
|
|
836
|
+
properties: {
|
|
837
|
+
searchPath: { type: "string", description: "Directory to search within (e.g. ~/Downloads). Use '~' to search all allowed roots (Desktop, Documents, Downloads, etc.) in one call." },
|
|
838
|
+
name: { type: "string", description: "Name to search for (fuzzy matching: case, underscores, spaces ignored)" },
|
|
839
|
+
type: { type: "string", enum: ["file", "directory", "any"], description: "Filter by type (default: any)" },
|
|
840
|
+
maxDepth: { type: "number", description: "How deep to search in subdirectories (default: 3, max: 10)" },
|
|
841
|
+
},
|
|
842
|
+
additionalProperties: false,
|
|
843
|
+
},
|
|
844
|
+
risk: "read",
|
|
845
|
+
execute: runFileFind,
|
|
846
|
+
},
|
|
395
847
|
];
|
|
@@ -8,6 +8,8 @@ type CalendarCreateInput = {
|
|
|
8
8
|
end?: string;
|
|
9
9
|
location?: string;
|
|
10
10
|
notes?: string;
|
|
11
|
+
/** Calendar name to create the event on (e.g. "Personal", "Work"). If omitted, uses default calendar. */
|
|
12
|
+
calendar?: string;
|
|
11
13
|
};
|
|
12
14
|
|
|
13
15
|
type CalendarListInput = {
|
|
@@ -606,6 +608,7 @@ async function runCalendarCreateEvent(input: unknown, context: ToolExecutionCont
|
|
|
606
608
|
const end = endInput || start;
|
|
607
609
|
const location = typeof payload.location === "string" ? payload.location.trim() : "";
|
|
608
610
|
const notes = typeof payload.notes === "string" ? payload.notes.trim() : "";
|
|
611
|
+
const calendarName = typeof payload.calendar === "string" && payload.calendar.trim() ? payload.calendar.trim() : "";
|
|
609
612
|
const startParts = parseDateTimeParts(start, { defaultHour: 9, defaultMinute: 0 });
|
|
610
613
|
if (!startParts) {
|
|
611
614
|
throw new Error(`Invalid start date/time: ${start}`);
|
|
@@ -631,6 +634,7 @@ async function runCalendarCreateEvent(input: unknown, context: ToolExecutionCont
|
|
|
631
634
|
endParts,
|
|
632
635
|
location,
|
|
633
636
|
notes,
|
|
637
|
+
calendar: calendarName || null,
|
|
634
638
|
};
|
|
635
639
|
}
|
|
636
640
|
|
|
@@ -649,11 +653,27 @@ async function runCalendarCreateEvent(input: unknown, context: ToolExecutionCont
|
|
|
649
653
|
'set endMinute to (item 11 of argv) as integer\n' +
|
|
650
654
|
'set eventLocation to item 12 of argv\n' +
|
|
651
655
|
'set eventNotes to item 13 of argv\n' +
|
|
656
|
+
'set requestedCalendar to item 14 of argv\n' +
|
|
652
657
|
'tell application "Calendar"\n' +
|
|
653
658
|
'set targetCalendar to missing value\n' +
|
|
659
|
+
// First try the requested calendar name (case-insensitive)
|
|
660
|
+
'if requestedCalendar is not "" then\n' +
|
|
661
|
+
'repeat with cal in calendars\n' +
|
|
662
|
+
'ignoring case\n' +
|
|
663
|
+
'if (name of cal as text) is equal to requestedCalendar then\n' +
|
|
664
|
+
'set targetCalendar to cal\n' +
|
|
665
|
+
'exit repeat\n' +
|
|
666
|
+
'end if\n' +
|
|
667
|
+
'end ignoring\n' +
|
|
668
|
+
'end repeat\n' +
|
|
669
|
+
'end if\n' +
|
|
670
|
+
// Fallback: try "Home" calendar if no calendar specified
|
|
671
|
+
'if targetCalendar is missing value and requestedCalendar is "" then\n' +
|
|
654
672
|
'try\n' +
|
|
655
673
|
'set targetCalendar to calendar "Home"\n' +
|
|
656
674
|
'end try\n' +
|
|
675
|
+
'end if\n' +
|
|
676
|
+
// Last fallback: first available calendar
|
|
657
677
|
'if targetCalendar is missing value then\n' +
|
|
658
678
|
'try\n' +
|
|
659
679
|
'set targetCalendar to first calendar\n' +
|
|
@@ -673,9 +693,9 @@ async function runCalendarCreateEvent(input: unknown, context: ToolExecutionCont
|
|
|
673
693
|
'tell targetCalendar\n' +
|
|
674
694
|
'set createdEvent to make new event with properties {summary:eventTitle, start date:startDate, end date:endDate, location:eventLocation, description:eventNotes}\n' +
|
|
675
695
|
"end tell\n" +
|
|
676
|
-
'set
|
|
696
|
+
'set calendarUsed to (name of targetCalendar as text)\n' +
|
|
677
697
|
"end tell\n" +
|
|
678
|
-
'return "created" & tab &
|
|
698
|
+
'return "created" & tab & calendarUsed\n' +
|
|
679
699
|
"end run";
|
|
680
700
|
const output = await runAppleScript(
|
|
681
701
|
script,
|
|
@@ -693,10 +713,11 @@ async function runCalendarCreateEvent(input: unknown, context: ToolExecutionCont
|
|
|
693
713
|
String(endParts.minute),
|
|
694
714
|
location,
|
|
695
715
|
notes,
|
|
716
|
+
calendarName,
|
|
696
717
|
],
|
|
697
718
|
context.signal,
|
|
698
719
|
);
|
|
699
|
-
const [,
|
|
720
|
+
const [, calendarUsed] = output.split("\t");
|
|
700
721
|
return {
|
|
701
722
|
created: true,
|
|
702
723
|
title,
|
|
@@ -712,7 +733,7 @@ async function runCalendarCreateEvent(input: unknown, context: ToolExecutionCont
|
|
|
712
733
|
).padStart(2, "0")}T${String(endParts.hour).padStart(2, "0")}:${String(
|
|
713
734
|
endParts.minute,
|
|
714
735
|
).padStart(2, "0")}:00`,
|
|
715
|
-
calendar:
|
|
736
|
+
calendar: calendarUsed?.trim() || null,
|
|
716
737
|
location,
|
|
717
738
|
};
|
|
718
739
|
}
|
|
@@ -1244,7 +1265,7 @@ export const scheduleTools: ToolDefinition[] = [
|
|
|
1244
1265
|
},
|
|
1245
1266
|
{
|
|
1246
1267
|
name: "calendar_create_event",
|
|
1247
|
-
description: "Create a Calendar event. Use explicit dates/times (e.g. YYYY-MM-DD or 'Friday at 2pm') so the event is created on the correct day.",
|
|
1268
|
+
description: "Create a Calendar event. Use explicit dates/times (e.g. YYYY-MM-DD or 'Friday at 2pm') so the event is created on the correct day. Specify calendar name (e.g. 'Personal', 'Work') to target a specific calendar.",
|
|
1248
1269
|
inputSchema: {
|
|
1249
1270
|
type: "object",
|
|
1250
1271
|
required: ["title", "start"],
|
|
@@ -1254,6 +1275,7 @@ export const scheduleTools: ToolDefinition[] = [
|
|
|
1254
1275
|
end: { type: "string" },
|
|
1255
1276
|
location: { type: "string" },
|
|
1256
1277
|
notes: { type: "string" },
|
|
1278
|
+
calendar: { type: "string", description: "Calendar name (e.g. 'Personal', 'Work'). Case-insensitive. If omitted, defaults to Home or first calendar." },
|
|
1257
1279
|
},
|
|
1258
1280
|
additionalProperties: false,
|
|
1259
1281
|
},
|