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 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) {
@@ -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 command planning. Ask a concrete repo or file operation."
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);
@@ -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 = ["listDirectory", "readFile", "searchInFiles", "gitStatus", "gitDiff"];
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terry-core",
3
- "version": "0.1.5",
3
+ "version": "1.0.0",
4
4
  "description": "Core policy, tooling, and orchestration modules for Terry Agent.",
5
5
  "license": "MIT",
6
6
  "author": "ELEVAREL",