terry-core 0.1.2 → 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 +291 -22
- 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,12 +1,259 @@
|
|
|
1
|
+
const KNOWN_COMMANDS = ["git", "npm", "pnpm", "yarn", "node", "npx"];
|
|
1
2
|
const TOOL_HINTS = [
|
|
2
|
-
{ tool: "
|
|
3
|
-
{ tool: "
|
|
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 },
|
|
13
|
+
{ tool: "runCommand", match: /\b(run|execute)\b|\b(git|npm|pnpm|yarn|node|npx)\b/i },
|
|
14
|
+
{ tool: "writeFile", match: /\b(write|create|update|edit|save)\b.*\b(file|readme|json|md|ts|js)\b/i },
|
|
15
|
+
{ tool: "readFile", match: /\b(read|open|show)\b.*\b(file|readme|json|md|ts|js)\b/i },
|
|
4
16
|
{ tool: "searchInFiles", match: /\b(search|find|grep|look for)\b/i },
|
|
5
17
|
{ tool: "gitStatus", match: /\bgit status|repo status|what changed\b/i },
|
|
6
18
|
{ tool: "gitDiff", match: /\bgit diff|show diff|changes\b/i },
|
|
7
|
-
{ tool: "runCommand", match: /\b(run|execute|command|npm|pnpm|yarn|test|build)\b/i },
|
|
8
19
|
{ tool: "listDirectory", match: /\b(list|ls|folders?|directories?)\b/i }
|
|
9
20
|
];
|
|
21
|
+
function stripWrappers(value) {
|
|
22
|
+
return value.trim().replace(/^["'`]|["'`]$/g, "");
|
|
23
|
+
}
|
|
24
|
+
function tokenizeCommand(raw) {
|
|
25
|
+
const tokens = [];
|
|
26
|
+
let current = "";
|
|
27
|
+
let quote = "";
|
|
28
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
29
|
+
const ch = raw[i];
|
|
30
|
+
if (!ch)
|
|
31
|
+
continue;
|
|
32
|
+
if (quote) {
|
|
33
|
+
if (ch === quote) {
|
|
34
|
+
quote = "";
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
current += ch;
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (ch === '"' || ch === "'") {
|
|
42
|
+
quote = ch;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (/\s/.test(ch)) {
|
|
46
|
+
if (current) {
|
|
47
|
+
tokens.push(current);
|
|
48
|
+
current = "";
|
|
49
|
+
}
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
current += ch;
|
|
53
|
+
}
|
|
54
|
+
if (current)
|
|
55
|
+
tokens.push(current);
|
|
56
|
+
return tokens;
|
|
57
|
+
}
|
|
58
|
+
function extractFirstPath(content) {
|
|
59
|
+
const quoted = content.match(/["'`]([^"'`]+)["'`]/);
|
|
60
|
+
if (quoted?.[1])
|
|
61
|
+
return quoted[1].trim();
|
|
62
|
+
const direct = content.match(/\b([./\\]?[a-zA-Z0-9_\-/\\.]+\.[a-zA-Z0-9_]+)\b/);
|
|
63
|
+
if (direct?.[1])
|
|
64
|
+
return direct[1].trim();
|
|
65
|
+
return null;
|
|
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
|
+
}
|
|
118
|
+
function parseRunCommand(content) {
|
|
119
|
+
const backtick = content.match(/`([^`]+)`/);
|
|
120
|
+
const candidate = backtick?.[1] ??
|
|
121
|
+
content.match(/\b(?:run|execute)(?:\s+the)?(?:\s+command)?\s+(.+)$/i)?.[1] ??
|
|
122
|
+
content.match(new RegExp(`^\\s*(${KNOWN_COMMANDS.join("|")})\\b(.+)?$`, "i"))?.[0] ??
|
|
123
|
+
"";
|
|
124
|
+
const normalized = candidate.trim().replace(/[.;,\s]+$/g, "");
|
|
125
|
+
if (!normalized)
|
|
126
|
+
return null;
|
|
127
|
+
const tokens = tokenizeCommand(normalized);
|
|
128
|
+
if (tokens.length === 0)
|
|
129
|
+
return null;
|
|
130
|
+
const [cmd, ...args] = tokens;
|
|
131
|
+
if (!cmd || !KNOWN_COMMANDS.includes(cmd.toLowerCase())) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return { cmd, args };
|
|
135
|
+
}
|
|
136
|
+
function parseReadFileArgs(content) {
|
|
137
|
+
const pathMatch = content.match(/\b(?:read|open|show)(?:\s+the)?(?:\s+file)?\s+["'`]([^"'`]+)["'`]/i)?.[1] ??
|
|
138
|
+
extractFirstPath(content);
|
|
139
|
+
return { pathRelativeToWorkspace: pathMatch ?? "README.md" };
|
|
140
|
+
}
|
|
141
|
+
function parseWriteFileArgs(content) {
|
|
142
|
+
const pathMatch = content.match(/\b(?:write|create|update|edit|save)(?:\s+(?:to|into))?(?:\s+file)?\s+["'`]([^"'`]+)["'`]/i)?.[1] ??
|
|
143
|
+
content.match(/\b(?:write|create|update|edit|save)(?:\s+(?:to|into))?(?:\s+file)?\s+([^\s]+)\b/i)?.[1] ??
|
|
144
|
+
extractFirstPath(content) ??
|
|
145
|
+
"README.md";
|
|
146
|
+
const contentMatch = content.match(/\b(?:with content|with text|saying|that says)\s+([\s\S]+)$/i)?.[1] ??
|
|
147
|
+
content.match(/:\s*([\s\S]+)$/)?.[1] ??
|
|
148
|
+
`# Terry update\n\nGenerated from task:\n${content}\n`;
|
|
149
|
+
return {
|
|
150
|
+
pathRelativeToWorkspace: stripWrappers(pathMatch),
|
|
151
|
+
content: contentMatch.trim()
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function parseSearchArgs(content) {
|
|
155
|
+
const quoted = content.match(/["'`]([^"'`]+)["'`]/)?.[1];
|
|
156
|
+
const phrase = quoted ??
|
|
157
|
+
content.match(/\b(?:search|find|grep|look for)\b\s+([\s\S]+)$/i)?.[1] ??
|
|
158
|
+
content;
|
|
159
|
+
return { query: phrase.replace(/^for\s+/i, "").trim() };
|
|
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
|
+
}
|
|
170
|
+
function parseListDirectoryArgs(content) {
|
|
171
|
+
const target = content.match(/\b(?:in|inside|under)\s+["'`]([^"'`]+)["'`]/i)?.[1] ??
|
|
172
|
+
content.match(/\b(?:in|inside|under)\s+([^\s]+)\b/i)?.[1] ??
|
|
173
|
+
".";
|
|
174
|
+
return { pathRelativeToWorkspace: stripWrappers(target) || "." };
|
|
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
|
+
}
|
|
10
257
|
function inferTool(content) {
|
|
11
258
|
for (const hint of TOOL_HINTS) {
|
|
12
259
|
if (hint.match.test(content))
|
|
@@ -15,27 +262,42 @@ function inferTool(content) {
|
|
|
15
262
|
return null;
|
|
16
263
|
}
|
|
17
264
|
function inferArgs(tool, content) {
|
|
18
|
-
if (tool === "searchInFiles")
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (tool === "runCommand")
|
|
23
|
-
return
|
|
24
|
-
}
|
|
265
|
+
if (tool === "searchInFiles")
|
|
266
|
+
return parseSearchArgs(content);
|
|
267
|
+
if (tool === "webSearch")
|
|
268
|
+
return parseWebSearchArgs(content);
|
|
269
|
+
if (tool === "runCommand")
|
|
270
|
+
return parseRunCommand(content);
|
|
25
271
|
if (tool === "listDirectory")
|
|
26
|
-
return
|
|
272
|
+
return parseListDirectoryArgs(content);
|
|
27
273
|
if (tool === "gitStatus")
|
|
28
274
|
return {};
|
|
29
|
-
if (tool === "gitDiff")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return { pathRelativeToWorkspace: "README.md" };
|
|
33
|
-
if (tool === "writeFile") {
|
|
34
|
-
return {
|
|
35
|
-
pathRelativeToWorkspace: "README.md",
|
|
36
|
-
content: `# Terry update\n\nGenerated from task:\n${content}\n`
|
|
37
|
-
};
|
|
275
|
+
if (tool === "gitDiff") {
|
|
276
|
+
const target = extractFirstPath(content);
|
|
277
|
+
return target ? { path: target } : {};
|
|
38
278
|
}
|
|
279
|
+
if (tool === "readFile")
|
|
280
|
+
return parseReadFileArgs(content);
|
|
281
|
+
if (tool === "writeFile")
|
|
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);
|
|
39
301
|
return {};
|
|
40
302
|
}
|
|
41
303
|
export const rulesOrchestrator = (messages) => {
|
|
@@ -46,13 +308,20 @@ export const rulesOrchestrator = (messages) => {
|
|
|
46
308
|
if (!tool) {
|
|
47
309
|
return {
|
|
48
310
|
kind: "summarize",
|
|
49
|
-
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."
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const args = inferArgs(tool, lastUser.content);
|
|
315
|
+
if (tool === "runCommand" && !args) {
|
|
316
|
+
return {
|
|
317
|
+
kind: "summarize",
|
|
318
|
+
summary: "I can run approved workspace commands. Please provide an explicit command, for example: run `npm test`."
|
|
50
319
|
};
|
|
51
320
|
}
|
|
52
321
|
return {
|
|
53
322
|
kind: "ask_permission",
|
|
54
323
|
tool,
|
|
55
|
-
args
|
|
324
|
+
args,
|
|
56
325
|
reason: `Detected request likely needs tool: ${tool}.`
|
|
57
326
|
};
|
|
58
327
|
};
|
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;
|