terry-core 0.1.5 → 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/dist/config.d.ts +1 -0
- package/dist/config.js +22 -1
- package/dist/orchestrator.js +172 -1
- package/dist/permissions.js +9 -0
- package/dist/policy.js +9 -1
- package/dist/tools.d.ts +11 -0
- package/dist/tools.js +214 -0
- package/dist/types.d.ts +45 -1
- package/package.json +1 -1
package/dist/config.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare function workspacesFile(): string;
|
|
|
4
4
|
export declare function auditLogFile(): string;
|
|
5
5
|
export declare function usageDir(): string;
|
|
6
6
|
export declare function cloudSyncConfigFile(): string;
|
|
7
|
+
export declare function memoryFile(): string;
|
|
7
8
|
export declare function ensureTerryDirs(): Promise<void>;
|
|
8
9
|
type WorkspaceMap = Record<string, WorkspaceSettings>;
|
|
9
10
|
export declare function loadWorkspaceMap(): Promise<WorkspaceMap>;
|
package/dist/config.js
CHANGED
|
@@ -8,9 +8,19 @@ const DEFAULT_TOOLS = {
|
|
|
8
8
|
readFile: "enabled",
|
|
9
9
|
writeFile: "enabled",
|
|
10
10
|
searchInFiles: "enabled",
|
|
11
|
+
webSearch: "enabled",
|
|
11
12
|
gitStatus: "enabled",
|
|
12
13
|
gitDiff: "enabled",
|
|
13
|
-
runCommand: "enabled"
|
|
14
|
+
runCommand: "enabled",
|
|
15
|
+
memorySet: "enabled",
|
|
16
|
+
memoryGet: "enabled",
|
|
17
|
+
openCalendarEvent: "ask",
|
|
18
|
+
openGitHubIssue: "ask",
|
|
19
|
+
openNotionPage: "ask",
|
|
20
|
+
openLinearIssue: "ask",
|
|
21
|
+
openSlackCompose: "ask",
|
|
22
|
+
openEmail: "ask",
|
|
23
|
+
openMapsDirections: "ask"
|
|
14
24
|
};
|
|
15
25
|
export function terryHomeDir() {
|
|
16
26
|
return path.join(os.homedir(), ".terry");
|
|
@@ -27,6 +37,9 @@ export function usageDir() {
|
|
|
27
37
|
export function cloudSyncConfigFile() {
|
|
28
38
|
return path.join(terryHomeDir(), "cloud-sync.json");
|
|
29
39
|
}
|
|
40
|
+
export function memoryFile() {
|
|
41
|
+
return path.join(terryHomeDir(), "memory.json");
|
|
42
|
+
}
|
|
30
43
|
export async function ensureTerryDirs() {
|
|
31
44
|
await fs.mkdir(terryHomeDir(), { recursive: true });
|
|
32
45
|
await fs.mkdir(usageDir(), { recursive: true });
|
|
@@ -85,6 +98,14 @@ export async function getWorkspaceSettings(workspacePath) {
|
|
|
85
98
|
await saveWorkspaceMap(map);
|
|
86
99
|
}
|
|
87
100
|
}
|
|
101
|
+
const mergedTools = { ...DEFAULT_TOOLS, ...(map[key].tools ?? {}) };
|
|
102
|
+
if (JSON.stringify(mergedTools) !== JSON.stringify(map[key].tools ?? {})) {
|
|
103
|
+
map[key] = {
|
|
104
|
+
...map[key],
|
|
105
|
+
tools: mergedTools
|
|
106
|
+
};
|
|
107
|
+
await saveWorkspaceMap(map);
|
|
108
|
+
}
|
|
88
109
|
return map[key];
|
|
89
110
|
}
|
|
90
111
|
export async function setWorkspaceSettings(workspacePath, patch) {
|
package/dist/orchestrator.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
const KNOWN_COMMANDS = ["git", "npm", "pnpm", "yarn", "node", "npx"];
|
|
2
2
|
const TOOL_HINTS = [
|
|
3
|
+
{ tool: "webSearch", match: /\b(web search|search web|lookup online|latest on|news on|current events?)\b/i },
|
|
4
|
+
{ tool: "memorySet", match: /\bremember(?:\s+that)?\b/i },
|
|
5
|
+
{ tool: "memoryGet", match: /\b(what do you remember|what did i tell you|recall|remember my|what is my)\b/i },
|
|
6
|
+
{ tool: "openCalendarEvent", match: /\b(schedule|calendar|meeting|event)\b/i },
|
|
7
|
+
{ tool: "openGitHubIssue", match: /\b(github issue|create issue|open issue)\b/i },
|
|
8
|
+
{ tool: "openNotionPage", match: /\bnotion\b.*\b(page|note)\b|\bcreate notion\b/i },
|
|
9
|
+
{ tool: "openLinearIssue", match: /\b(linear|jira)\b.*\b(issue|ticket)\b/i },
|
|
10
|
+
{ tool: "openSlackCompose", match: /\bslack\b.*\b(message|send|compose)\b/i },
|
|
11
|
+
{ tool: "openEmail", match: /\b(draft an email|send an email|mailto|email)\b/i },
|
|
12
|
+
{ tool: "openMapsDirections", match: /\b(directions?|navigate|maps?)\b/i },
|
|
3
13
|
{ tool: "runCommand", match: /\b(run|execute)\b|\b(git|npm|pnpm|yarn|node|npx)\b/i },
|
|
4
14
|
{ tool: "writeFile", match: /\b(write|create|update|edit|save)\b.*\b(file|readme|json|md|ts|js)\b/i },
|
|
5
15
|
{ tool: "readFile", match: /\b(read|open|show)\b.*\b(file|readme|json|md|ts|js)\b/i },
|
|
@@ -54,6 +64,57 @@ function extractFirstPath(content) {
|
|
|
54
64
|
return direct[1].trim();
|
|
55
65
|
return null;
|
|
56
66
|
}
|
|
67
|
+
function extractQuoted(content) {
|
|
68
|
+
return content.match(/["'`]([^"'`]+)["'`]/)?.[1]?.trim() ?? null;
|
|
69
|
+
}
|
|
70
|
+
function extractEmails(content) {
|
|
71
|
+
return [...content.matchAll(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi)].map((match) => match[0]);
|
|
72
|
+
}
|
|
73
|
+
function parseTime(content) {
|
|
74
|
+
const match = content.match(/\b(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\b/i);
|
|
75
|
+
if (!match?.[1])
|
|
76
|
+
return null;
|
|
77
|
+
let hour = Number(match[1]);
|
|
78
|
+
const minute = Number(match[2] ?? "0");
|
|
79
|
+
const meridiem = (match[3] ?? "").toLowerCase();
|
|
80
|
+
if (meridiem === "pm" && hour < 12)
|
|
81
|
+
hour += 12;
|
|
82
|
+
if (meridiem === "am" && hour === 12)
|
|
83
|
+
hour = 0;
|
|
84
|
+
if (Number.isNaN(hour) || Number.isNaN(minute))
|
|
85
|
+
return null;
|
|
86
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
|
|
87
|
+
return null;
|
|
88
|
+
return { hour, minute };
|
|
89
|
+
}
|
|
90
|
+
function parseDatePhrase(content) {
|
|
91
|
+
const lower = content.toLowerCase();
|
|
92
|
+
const now = new Date();
|
|
93
|
+
const explicitDate = content.match(/\b(\d{4}-\d{2}-\d{2})(?:[ t](\d{1,2}):(\d{2}))?/);
|
|
94
|
+
if (explicitDate?.[1]) {
|
|
95
|
+
const base = new Date(`${explicitDate[1]}T00:00:00`);
|
|
96
|
+
const hh = Number(explicitDate[2] ?? "9");
|
|
97
|
+
const mm = Number(explicitDate[3] ?? "0");
|
|
98
|
+
base.setHours(hh, mm, 0, 0);
|
|
99
|
+
return base;
|
|
100
|
+
}
|
|
101
|
+
const parsedTime = parseTime(content) ?? { hour: 9, minute: 0 };
|
|
102
|
+
const base = new Date();
|
|
103
|
+
if (lower.includes("tomorrow")) {
|
|
104
|
+
base.setDate(base.getDate() + 1);
|
|
105
|
+
}
|
|
106
|
+
else if (lower.includes("today")) {
|
|
107
|
+
// keep today
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const parsed = Date.parse(content);
|
|
111
|
+
if (!Number.isNaN(parsed))
|
|
112
|
+
return new Date(parsed);
|
|
113
|
+
base.setDate(now.getDate() + 1);
|
|
114
|
+
}
|
|
115
|
+
base.setHours(parsedTime.hour, parsedTime.minute, 0, 0);
|
|
116
|
+
return base;
|
|
117
|
+
}
|
|
57
118
|
function parseRunCommand(content) {
|
|
58
119
|
const backtick = content.match(/`([^`]+)`/);
|
|
59
120
|
const candidate = backtick?.[1] ??
|
|
@@ -97,12 +158,102 @@ function parseSearchArgs(content) {
|
|
|
97
158
|
content;
|
|
98
159
|
return { query: phrase.replace(/^for\s+/i, "").trim() };
|
|
99
160
|
}
|
|
161
|
+
function parseWebSearchArgs(content) {
|
|
162
|
+
const quoted = extractQuoted(content);
|
|
163
|
+
const phrase = quoted ??
|
|
164
|
+
content
|
|
165
|
+
.replace(/\b(web search|search web|lookup online|latest on|news on|current events?)\b/gi, "")
|
|
166
|
+
.replace(/^for\s+/i, "")
|
|
167
|
+
.trim();
|
|
168
|
+
return { query: phrase || content.trim(), maxResults: 5 };
|
|
169
|
+
}
|
|
100
170
|
function parseListDirectoryArgs(content) {
|
|
101
171
|
const target = content.match(/\b(?:in|inside|under)\s+["'`]([^"'`]+)["'`]/i)?.[1] ??
|
|
102
172
|
content.match(/\b(?:in|inside|under)\s+([^\s]+)\b/i)?.[1] ??
|
|
103
173
|
".";
|
|
104
174
|
return { pathRelativeToWorkspace: stripWrappers(target) || "." };
|
|
105
175
|
}
|
|
176
|
+
function parseMemorySetArgs(content) {
|
|
177
|
+
const remember = content.match(/\bremember(?:\s+that)?\s+(.+?)\s+(?:is|=|as)\s+([\s\S]+)$/i) ??
|
|
178
|
+
content.match(/\bremember(?:\s+that)?\s+(.+?)\s*:\s*([\s\S]+)$/i);
|
|
179
|
+
if (remember?.[1] && remember?.[2]) {
|
|
180
|
+
return { key: remember[1].trim(), value: remember[2].trim() };
|
|
181
|
+
}
|
|
182
|
+
const fallback = content.replace(/\bremember(?:\s+that)?\s*/i, "").trim();
|
|
183
|
+
return { key: "note", value: fallback || content };
|
|
184
|
+
}
|
|
185
|
+
function parseMemoryGetArgs(content) {
|
|
186
|
+
const parsed = content.match(/\b(?:what(?:'s| is)\s+(?:my|the)\s+|recall\s+|remember\s+)([\w\s._/-]+)\??$/i)?.[1] ??
|
|
187
|
+
content.replace(/\?+$/, "");
|
|
188
|
+
return { key: parsed.trim() || "note" };
|
|
189
|
+
}
|
|
190
|
+
function parseCalendarArgs(content) {
|
|
191
|
+
const quoted = extractQuoted(content);
|
|
192
|
+
const title = quoted ??
|
|
193
|
+
content.match(/\bcalled\s+([^.,]+?)(?:\s+(?:with|tomorrow|today|at|on)\b|$)/i)?.[1]?.trim() ??
|
|
194
|
+
"Terry Meeting";
|
|
195
|
+
const start = parseDatePhrase(content);
|
|
196
|
+
const end = new Date(start.getTime() + 60 * 60 * 1000);
|
|
197
|
+
const attendees = extractEmails(content);
|
|
198
|
+
return {
|
|
199
|
+
title,
|
|
200
|
+
startIso: start.toISOString(),
|
|
201
|
+
endIso: end.toISOString(),
|
|
202
|
+
attendees: attendees.length ? attendees : undefined
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function parseGitHubIssueArgs(content) {
|
|
206
|
+
const title = extractQuoted(content) ??
|
|
207
|
+
content.match(/\b(?:issue|ticket)\s*:\s*(.+?)(?=\s+\bwith\s+labels?\b|\s+\bwith\s+label\b|\s+\b(?:in|for)\s+[a-z0-9_.-]+\/[a-z0-9_.-]+|$)/i)?.[1]?.trim() ??
|
|
208
|
+
content.match(/\b(?:issue|ticket)\s+(.+?)(?=\s+\bwith\s+labels?\b|\s+\bwith\s+label\b|\s+\b(?:in|for)\s+[a-z0-9_.-]+\/[a-z0-9_.-]+|$)/i)?.[1]?.trim() ??
|
|
209
|
+
"New issue";
|
|
210
|
+
const labelsRaw = content.match(/\blabels?\s+(.+?)(?=\s+\b(?:in|for)\s+[a-z0-9_.-]+\/[a-z0-9_.-]+\b|$)/i)?.[1];
|
|
211
|
+
const repo = content.match(/\b(?:in|for)\s+([a-z0-9_.-]+\/[a-z0-9_.-]+)\b/i)?.[1];
|
|
212
|
+
return {
|
|
213
|
+
title,
|
|
214
|
+
labels: labelsRaw
|
|
215
|
+
? labelsRaw
|
|
216
|
+
.split(/[,\s]+/)
|
|
217
|
+
.map((label) => label.trim())
|
|
218
|
+
.filter(Boolean)
|
|
219
|
+
: undefined,
|
|
220
|
+
repo
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function parseNotionArgs(content) {
|
|
224
|
+
const title = extractQuoted(content) ??
|
|
225
|
+
content.match(/\b(?:called|named|title)\s+([^.,]+)$/i)?.[1]?.trim() ??
|
|
226
|
+
"New Terry Note";
|
|
227
|
+
return { title };
|
|
228
|
+
}
|
|
229
|
+
function parseLinearArgs(content) {
|
|
230
|
+
const title = extractQuoted(content) ??
|
|
231
|
+
content.match(/\b(?:ticket|issue)\s*:\s*(.+?)(?=\s+\b(?:in|for)\s+project\b|$)/i)?.[1]?.trim() ??
|
|
232
|
+
content.match(/\b(?:ticket|issue)\s+(.+?)(?=\s+\b(?:in|for)\s+project\b|$)/i)?.[1]?.trim() ??
|
|
233
|
+
"New ticket";
|
|
234
|
+
const project = content.match(/\bproject\s+([a-z0-9_-]+)\b/i)?.[1];
|
|
235
|
+
return { title, project };
|
|
236
|
+
}
|
|
237
|
+
function parseSlackArgs(content) {
|
|
238
|
+
const channel = content.match(/#([a-z0-9_-]+)/i)?.[1];
|
|
239
|
+
const quoted = extractQuoted(content);
|
|
240
|
+
const afterColon = content.match(/:\s*([\s\S]+)$/)?.[1]?.trim();
|
|
241
|
+
const message = quoted ?? afterColon ?? "Update from Terry";
|
|
242
|
+
return { channel, message };
|
|
243
|
+
}
|
|
244
|
+
function parseEmailArgs(content) {
|
|
245
|
+
const to = extractEmails(content)[0];
|
|
246
|
+
const subject = content.match(/\bsubject\s+["'`]([^"'`]+)["'`]/i)?.[1] ??
|
|
247
|
+
content.match(/\babout\s+([^.,]+)$/i)?.[1]?.trim() ??
|
|
248
|
+
"Terry note";
|
|
249
|
+
const body = extractQuoted(content) ?? content.match(/:\s*([\s\S]+)$/)?.[1]?.trim() ?? undefined;
|
|
250
|
+
return { to, subject, body };
|
|
251
|
+
}
|
|
252
|
+
function parseDirectionsArgs(content) {
|
|
253
|
+
const destination = content.match(/\b(?:to|for)\s+([^.,]+)$/i)?.[1]?.trim() ?? extractQuoted(content) ?? content;
|
|
254
|
+
const provider = /\bapple maps?\b/i.test(content) ? "apple" : "google";
|
|
255
|
+
return { destination, provider };
|
|
256
|
+
}
|
|
106
257
|
function inferTool(content) {
|
|
107
258
|
for (const hint of TOOL_HINTS) {
|
|
108
259
|
if (hint.match.test(content))
|
|
@@ -113,6 +264,8 @@ function inferTool(content) {
|
|
|
113
264
|
function inferArgs(tool, content) {
|
|
114
265
|
if (tool === "searchInFiles")
|
|
115
266
|
return parseSearchArgs(content);
|
|
267
|
+
if (tool === "webSearch")
|
|
268
|
+
return parseWebSearchArgs(content);
|
|
116
269
|
if (tool === "runCommand")
|
|
117
270
|
return parseRunCommand(content);
|
|
118
271
|
if (tool === "listDirectory")
|
|
@@ -127,6 +280,24 @@ function inferArgs(tool, content) {
|
|
|
127
280
|
return parseReadFileArgs(content);
|
|
128
281
|
if (tool === "writeFile")
|
|
129
282
|
return parseWriteFileArgs(content);
|
|
283
|
+
if (tool === "memorySet")
|
|
284
|
+
return parseMemorySetArgs(content);
|
|
285
|
+
if (tool === "memoryGet")
|
|
286
|
+
return parseMemoryGetArgs(content);
|
|
287
|
+
if (tool === "openCalendarEvent")
|
|
288
|
+
return parseCalendarArgs(content);
|
|
289
|
+
if (tool === "openGitHubIssue")
|
|
290
|
+
return parseGitHubIssueArgs(content);
|
|
291
|
+
if (tool === "openNotionPage")
|
|
292
|
+
return parseNotionArgs(content);
|
|
293
|
+
if (tool === "openLinearIssue")
|
|
294
|
+
return parseLinearArgs(content);
|
|
295
|
+
if (tool === "openSlackCompose")
|
|
296
|
+
return parseSlackArgs(content);
|
|
297
|
+
if (tool === "openEmail")
|
|
298
|
+
return parseEmailArgs(content);
|
|
299
|
+
if (tool === "openMapsDirections")
|
|
300
|
+
return parseDirectionsArgs(content);
|
|
130
301
|
return {};
|
|
131
302
|
}
|
|
132
303
|
export const rulesOrchestrator = (messages) => {
|
|
@@ -137,7 +308,7 @@ export const rulesOrchestrator = (messages) => {
|
|
|
137
308
|
if (!tool) {
|
|
138
309
|
return {
|
|
139
310
|
kind: "summarize",
|
|
140
|
-
summary: "I can help with workspace files, search, git, and
|
|
311
|
+
summary: "I can help with workspace files, search, git, memory, and assistant actions like calendar/email/deep-links."
|
|
141
312
|
};
|
|
142
313
|
}
|
|
143
314
|
const args = inferArgs(tool, lastUser.content);
|
package/dist/permissions.js
CHANGED
|
@@ -20,5 +20,14 @@ export function toolRiskLevel(tool) {
|
|
|
20
20
|
return "high";
|
|
21
21
|
if (tool === "writeFile")
|
|
22
22
|
return "medium";
|
|
23
|
+
if (tool === "openCalendarEvent" ||
|
|
24
|
+
tool === "openGitHubIssue" ||
|
|
25
|
+
tool === "openNotionPage" ||
|
|
26
|
+
tool === "openLinearIssue" ||
|
|
27
|
+
tool === "openSlackCompose" ||
|
|
28
|
+
tool === "openEmail" ||
|
|
29
|
+
tool === "openMapsDirections") {
|
|
30
|
+
return "medium";
|
|
31
|
+
}
|
|
23
32
|
return "low";
|
|
24
33
|
}
|
package/dist/policy.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
const READ_ONLY_TOOLS = [
|
|
1
|
+
const READ_ONLY_TOOLS = [
|
|
2
|
+
"listDirectory",
|
|
3
|
+
"readFile",
|
|
4
|
+
"searchInFiles",
|
|
5
|
+
"webSearch",
|
|
6
|
+
"gitStatus",
|
|
7
|
+
"gitDiff",
|
|
8
|
+
"memoryGet"
|
|
9
|
+
];
|
|
2
10
|
const COMMAND_ALLOWLIST = new Set(["git", "npm", "pnpm", "yarn", "node", "npx"]);
|
|
3
11
|
export function isReadOnlyTool(tool) {
|
|
4
12
|
return READ_ONLY_TOOLS.includes(tool);
|
package/dist/tools.d.ts
CHANGED
|
@@ -2,6 +2,17 @@ import type { ToolArgsMap, ToolName, ToolResult, WorkspaceSettings } from "./typ
|
|
|
2
2
|
type ToolContext = {
|
|
3
3
|
workspacePath: string;
|
|
4
4
|
settings: WorkspaceSettings;
|
|
5
|
+
cloud?: {
|
|
6
|
+
webSearch?: (query: string, maxResults?: number) => Promise<{
|
|
7
|
+
query: string;
|
|
8
|
+
provider: string;
|
|
9
|
+
results: Array<{
|
|
10
|
+
title: string;
|
|
11
|
+
url: string;
|
|
12
|
+
snippet: string;
|
|
13
|
+
}>;
|
|
14
|
+
}>;
|
|
15
|
+
};
|
|
5
16
|
};
|
|
6
17
|
export declare function executeTool<T extends ToolName>(context: ToolContext, tool: T, args: ToolArgsMap[T]): Promise<ToolResult>;
|
|
7
18
|
export {};
|
package/dist/tools.js
CHANGED
|
@@ -6,10 +6,110 @@ import fg from "fast-glob";
|
|
|
6
6
|
import { resolveInWorkspace } from "./sandbox.js";
|
|
7
7
|
import { appendAudit } from "./audit.js";
|
|
8
8
|
import { applyPatchProposal, createPatchProposal } from "./patches.js";
|
|
9
|
+
import { memoryFile } from "./config.js";
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
function dryRunResult(tool, files = []) {
|
|
11
12
|
return { ok: true, output: `[dry-run] ${tool} skipped execution.`, files };
|
|
12
13
|
}
|
|
14
|
+
function normalizeMemoryKey(key) {
|
|
15
|
+
return key.trim().toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
async function loadMemoryStore() {
|
|
18
|
+
try {
|
|
19
|
+
const raw = await fs.readFile(memoryFile(), "utf8");
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
if (!parsed || typeof parsed !== "object")
|
|
22
|
+
return {};
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function saveMemoryStore(store) {
|
|
30
|
+
await fs.mkdir(path.dirname(memoryFile()), { recursive: true });
|
|
31
|
+
await fs.writeFile(memoryFile(), JSON.stringify(store, null, 2), "utf8");
|
|
32
|
+
}
|
|
33
|
+
function formatCalendarDate(date) {
|
|
34
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
35
|
+
}
|
|
36
|
+
function toCalendarRange(startIso, endIso) {
|
|
37
|
+
const start = Number.isNaN(Date.parse(startIso)) ? new Date() : new Date(startIso);
|
|
38
|
+
const end = endIso && !Number.isNaN(Date.parse(endIso))
|
|
39
|
+
? new Date(endIso)
|
|
40
|
+
: new Date(start.getTime() + 60 * 60 * 1000);
|
|
41
|
+
if (end.getTime() <= start.getTime()) {
|
|
42
|
+
end.setTime(start.getTime() + 60 * 60 * 1000);
|
|
43
|
+
}
|
|
44
|
+
return `${formatCalendarDate(start)}/${formatCalendarDate(end)}`;
|
|
45
|
+
}
|
|
46
|
+
function buildCalendarUrl(args) {
|
|
47
|
+
const params = new URLSearchParams({
|
|
48
|
+
action: "TEMPLATE",
|
|
49
|
+
text: args.title,
|
|
50
|
+
dates: toCalendarRange(args.startIso, args.endIso)
|
|
51
|
+
});
|
|
52
|
+
if (args.attendees?.length) {
|
|
53
|
+
params.set("add", args.attendees.join(","));
|
|
54
|
+
}
|
|
55
|
+
return `https://calendar.google.com/calendar/render?${params.toString()}`;
|
|
56
|
+
}
|
|
57
|
+
function buildGitHubIssueUrl(args) {
|
|
58
|
+
const base = args.repo ? `https://github.com/${args.repo}/issues/new` : "https://github.com/issues/new";
|
|
59
|
+
const params = new URLSearchParams({ title: args.title });
|
|
60
|
+
if (args.body)
|
|
61
|
+
params.set("body", args.body);
|
|
62
|
+
if (args.labels?.length)
|
|
63
|
+
params.set("labels", args.labels.join(","));
|
|
64
|
+
return `${base}?${params.toString()}`;
|
|
65
|
+
}
|
|
66
|
+
function buildNotionUrl(args) {
|
|
67
|
+
const params = new URLSearchParams({ title: args.title });
|
|
68
|
+
return `https://www.notion.so/new?${params.toString()}`;
|
|
69
|
+
}
|
|
70
|
+
function buildLinearUrl(args) {
|
|
71
|
+
const params = new URLSearchParams({ title: args.title });
|
|
72
|
+
if (args.description)
|
|
73
|
+
params.set("description", args.description);
|
|
74
|
+
if (args.project)
|
|
75
|
+
params.set("project", args.project);
|
|
76
|
+
return `https://linear.app/new?${params.toString()}`;
|
|
77
|
+
}
|
|
78
|
+
function buildSlackUrl(args) {
|
|
79
|
+
const params = new URLSearchParams();
|
|
80
|
+
if (args.channel)
|
|
81
|
+
params.set("id", args.channel);
|
|
82
|
+
params.set("message", args.message);
|
|
83
|
+
return `slack://channel?${params.toString()}`;
|
|
84
|
+
}
|
|
85
|
+
function buildMailtoUrl(args) {
|
|
86
|
+
const recipient = args.to ?? "";
|
|
87
|
+
const params = new URLSearchParams();
|
|
88
|
+
if (args.subject)
|
|
89
|
+
params.set("subject", args.subject);
|
|
90
|
+
if (args.body)
|
|
91
|
+
params.set("body", args.body);
|
|
92
|
+
const query = params.toString();
|
|
93
|
+
return query ? `mailto:${encodeURIComponent(recipient)}?${query}` : `mailto:${encodeURIComponent(recipient)}`;
|
|
94
|
+
}
|
|
95
|
+
function buildMapsUrl(args) {
|
|
96
|
+
if (args.provider === "apple") {
|
|
97
|
+
return `https://maps.apple.com/?daddr=${encodeURIComponent(args.destination)}`;
|
|
98
|
+
}
|
|
99
|
+
return `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(args.destination)}`;
|
|
100
|
+
}
|
|
101
|
+
async function openExternalUrl(url) {
|
|
102
|
+
if (process.platform === "win32") {
|
|
103
|
+
const escaped = url.replace(/'/g, "''");
|
|
104
|
+
await execFileAsync("powershell.exe", ["-NoProfile", "-Command", `Start-Process '${escaped}'`]);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (process.platform === "darwin") {
|
|
108
|
+
await execFileAsync("open", [url]);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await execFileAsync("xdg-open", [url]);
|
|
112
|
+
}
|
|
13
113
|
async function withAudit(context, tool, args, affectedFiles, runner) {
|
|
14
114
|
try {
|
|
15
115
|
const result = await runner();
|
|
@@ -114,6 +214,29 @@ export async function executeTool(context, tool, args) {
|
|
|
114
214
|
}
|
|
115
215
|
return { ok: true, output: hits.join("\n") || "No matches.", files: hits };
|
|
116
216
|
});
|
|
217
|
+
case "webSearch":
|
|
218
|
+
return withAudit(context, tool, args, [], async () => {
|
|
219
|
+
const parsed = args;
|
|
220
|
+
const query = parsed.query?.trim();
|
|
221
|
+
if (!query)
|
|
222
|
+
return { ok: false, error: "Search query is required." };
|
|
223
|
+
if (context.settings.dryRun)
|
|
224
|
+
return { ok: true, output: `[dry-run] would search web for: ${query}` };
|
|
225
|
+
if (!context.cloud?.webSearch) {
|
|
226
|
+
return { ok: false, error: "webSearch requires cloud relay integration." };
|
|
227
|
+
}
|
|
228
|
+
const payload = await context.cloud.webSearch(query, parsed.maxResults);
|
|
229
|
+
const lines = payload.results.map((item, index) => {
|
|
230
|
+
const title = item.title?.trim() || "Untitled";
|
|
231
|
+
const snippet = item.snippet?.trim() || "No snippet.";
|
|
232
|
+
const url = item.url?.trim() || "";
|
|
233
|
+
return `${index + 1}. ${title}\n ${snippet}\n ${url}`;
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
ok: true,
|
|
237
|
+
output: [`Provider: ${payload.provider}`, `Query: ${payload.query}`, "", ...lines].join("\n")
|
|
238
|
+
};
|
|
239
|
+
});
|
|
117
240
|
case "gitStatus":
|
|
118
241
|
return withAudit(context, tool, args, [], async () => {
|
|
119
242
|
if (context.settings.dryRun)
|
|
@@ -142,6 +265,97 @@ export async function executeTool(context, tool, args) {
|
|
|
142
265
|
const { stdout, stderr } = await execFileAsync(parsed.cmd, parsed.args ?? [], { cwd });
|
|
143
266
|
return { ok: true, output: `${stdout}${stderr}`.trim() };
|
|
144
267
|
});
|
|
268
|
+
case "memorySet":
|
|
269
|
+
return withAudit(context, tool, args, [memoryFile()], async () => {
|
|
270
|
+
const parsed = args;
|
|
271
|
+
const key = normalizeMemoryKey(parsed.key);
|
|
272
|
+
if (!key)
|
|
273
|
+
return { ok: false, error: "Memory key is required." };
|
|
274
|
+
if (context.settings.dryRun)
|
|
275
|
+
return dryRunResult(tool, [memoryFile()]);
|
|
276
|
+
const store = await loadMemoryStore();
|
|
277
|
+
store[key] = parsed.value;
|
|
278
|
+
await saveMemoryStore(store);
|
|
279
|
+
return { ok: true, output: `Saved memory key "${key}".`, files: [memoryFile()] };
|
|
280
|
+
});
|
|
281
|
+
case "memoryGet":
|
|
282
|
+
return withAudit(context, tool, args, [memoryFile()], async () => {
|
|
283
|
+
const parsed = args;
|
|
284
|
+
const key = normalizeMemoryKey(parsed.key);
|
|
285
|
+
if (!key)
|
|
286
|
+
return { ok: false, error: "Memory key is required." };
|
|
287
|
+
if (context.settings.dryRun)
|
|
288
|
+
return dryRunResult(tool, [memoryFile()]);
|
|
289
|
+
const store = await loadMemoryStore();
|
|
290
|
+
const value = store[key];
|
|
291
|
+
if (!value) {
|
|
292
|
+
return { ok: true, output: `No memory found for "${key}".`, files: [memoryFile()] };
|
|
293
|
+
}
|
|
294
|
+
return { ok: true, output: value, files: [memoryFile()] };
|
|
295
|
+
});
|
|
296
|
+
case "openCalendarEvent":
|
|
297
|
+
return withAudit(context, tool, args, [], async () => {
|
|
298
|
+
const parsed = args;
|
|
299
|
+
const url = buildCalendarUrl(parsed);
|
|
300
|
+
if (context.settings.dryRun)
|
|
301
|
+
return { ok: true, output: `[dry-run] would open ${url}` };
|
|
302
|
+
await openExternalUrl(url);
|
|
303
|
+
return { ok: true, output: `Opened calendar event draft.\n${url}` };
|
|
304
|
+
});
|
|
305
|
+
case "openGitHubIssue":
|
|
306
|
+
return withAudit(context, tool, args, [], async () => {
|
|
307
|
+
const parsed = args;
|
|
308
|
+
const url = buildGitHubIssueUrl(parsed);
|
|
309
|
+
if (context.settings.dryRun)
|
|
310
|
+
return { ok: true, output: `[dry-run] would open ${url}` };
|
|
311
|
+
await openExternalUrl(url);
|
|
312
|
+
return { ok: true, output: `Opened GitHub issue draft.\n${url}` };
|
|
313
|
+
});
|
|
314
|
+
case "openNotionPage":
|
|
315
|
+
return withAudit(context, tool, args, [], async () => {
|
|
316
|
+
const parsed = args;
|
|
317
|
+
const url = buildNotionUrl(parsed);
|
|
318
|
+
if (context.settings.dryRun)
|
|
319
|
+
return { ok: true, output: `[dry-run] would open ${url}` };
|
|
320
|
+
await openExternalUrl(url);
|
|
321
|
+
return { ok: true, output: `Opened Notion page draft.\n${url}` };
|
|
322
|
+
});
|
|
323
|
+
case "openLinearIssue":
|
|
324
|
+
return withAudit(context, tool, args, [], async () => {
|
|
325
|
+
const parsed = args;
|
|
326
|
+
const url = buildLinearUrl(parsed);
|
|
327
|
+
if (context.settings.dryRun)
|
|
328
|
+
return { ok: true, output: `[dry-run] would open ${url}` };
|
|
329
|
+
await openExternalUrl(url);
|
|
330
|
+
return { ok: true, output: `Opened Linear issue draft.\n${url}` };
|
|
331
|
+
});
|
|
332
|
+
case "openSlackCompose":
|
|
333
|
+
return withAudit(context, tool, args, [], async () => {
|
|
334
|
+
const parsed = args;
|
|
335
|
+
const url = buildSlackUrl(parsed);
|
|
336
|
+
if (context.settings.dryRun)
|
|
337
|
+
return { ok: true, output: `[dry-run] would open ${url}` };
|
|
338
|
+
await openExternalUrl(url);
|
|
339
|
+
return { ok: true, output: `Opened Slack compose deep link.\n${url}` };
|
|
340
|
+
});
|
|
341
|
+
case "openEmail":
|
|
342
|
+
return withAudit(context, tool, args, [], async () => {
|
|
343
|
+
const parsed = args;
|
|
344
|
+
const url = buildMailtoUrl(parsed);
|
|
345
|
+
if (context.settings.dryRun)
|
|
346
|
+
return { ok: true, output: `[dry-run] would open ${url}` };
|
|
347
|
+
await openExternalUrl(url);
|
|
348
|
+
return { ok: true, output: `Opened email draft.\n${url}` };
|
|
349
|
+
});
|
|
350
|
+
case "openMapsDirections":
|
|
351
|
+
return withAudit(context, tool, args, [], async () => {
|
|
352
|
+
const parsed = args;
|
|
353
|
+
const url = buildMapsUrl(parsed);
|
|
354
|
+
if (context.settings.dryRun)
|
|
355
|
+
return { ok: true, output: `[dry-run] would open ${url}` };
|
|
356
|
+
await openExternalUrl(url);
|
|
357
|
+
return { ok: true, output: `Opened map directions.\n${url}` };
|
|
358
|
+
});
|
|
145
359
|
default:
|
|
146
360
|
return { ok: false, error: "Unknown tool." };
|
|
147
361
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type PermissionLevel = "disabled" | "enabled" | "ask";
|
|
2
2
|
export type AgentMode = "general" | "focus" | "builder";
|
|
3
3
|
export type ExecutionMode = "plan" | "safe" | "autonomous";
|
|
4
|
-
export type ToolName = "listDirectory" | "readFile" | "writeFile" | "searchInFiles" | "gitStatus" | "gitDiff" | "runCommand";
|
|
4
|
+
export type ToolName = "listDirectory" | "readFile" | "writeFile" | "searchInFiles" | "webSearch" | "gitStatus" | "gitDiff" | "runCommand" | "memorySet" | "memoryGet" | "openCalendarEvent" | "openGitHubIssue" | "openNotionPage" | "openLinearIssue" | "openSlackCompose" | "openEmail" | "openMapsDirections";
|
|
5
5
|
export type ToolArgsMap = {
|
|
6
6
|
listDirectory: {
|
|
7
7
|
pathRelativeToWorkspace?: string;
|
|
@@ -16,6 +16,10 @@ export type ToolArgsMap = {
|
|
|
16
16
|
searchInFiles: {
|
|
17
17
|
query: string;
|
|
18
18
|
};
|
|
19
|
+
webSearch: {
|
|
20
|
+
query: string;
|
|
21
|
+
maxResults?: number;
|
|
22
|
+
};
|
|
19
23
|
gitStatus: Record<string, never>;
|
|
20
24
|
gitDiff: {
|
|
21
25
|
path?: string;
|
|
@@ -25,6 +29,46 @@ export type ToolArgsMap = {
|
|
|
25
29
|
args?: string[];
|
|
26
30
|
cwdRelativeToWorkspace?: string;
|
|
27
31
|
};
|
|
32
|
+
memorySet: {
|
|
33
|
+
key: string;
|
|
34
|
+
value: string;
|
|
35
|
+
};
|
|
36
|
+
memoryGet: {
|
|
37
|
+
key: string;
|
|
38
|
+
};
|
|
39
|
+
openCalendarEvent: {
|
|
40
|
+
title: string;
|
|
41
|
+
startIso: string;
|
|
42
|
+
endIso?: string;
|
|
43
|
+
attendees?: string[];
|
|
44
|
+
};
|
|
45
|
+
openGitHubIssue: {
|
|
46
|
+
title: string;
|
|
47
|
+
body?: string;
|
|
48
|
+
labels?: string[];
|
|
49
|
+
repo?: string;
|
|
50
|
+
};
|
|
51
|
+
openNotionPage: {
|
|
52
|
+
title: string;
|
|
53
|
+
};
|
|
54
|
+
openLinearIssue: {
|
|
55
|
+
title: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
project?: string;
|
|
58
|
+
};
|
|
59
|
+
openSlackCompose: {
|
|
60
|
+
channel?: string;
|
|
61
|
+
message: string;
|
|
62
|
+
};
|
|
63
|
+
openEmail: {
|
|
64
|
+
to?: string;
|
|
65
|
+
subject?: string;
|
|
66
|
+
body?: string;
|
|
67
|
+
};
|
|
68
|
+
openMapsDirections: {
|
|
69
|
+
destination: string;
|
|
70
|
+
provider?: "google" | "apple";
|
|
71
|
+
};
|
|
28
72
|
};
|
|
29
73
|
export type ToolResult = {
|
|
30
74
|
ok: boolean;
|