daemora 1.0.1 → 1.0.3

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.
Files changed (134) hide show
  1. package/README.md +106 -76
  2. package/SOUL.md +100 -28
  3. package/config/mcp.json +9 -9
  4. package/package.json +15 -8
  5. package/skills/apple-notes.md +0 -52
  6. package/skills/apple-reminders.md +1 -87
  7. package/skills/camsnap.md +20 -144
  8. package/skills/coding.md +7 -7
  9. package/skills/documents.md +6 -6
  10. package/skills/email.md +6 -6
  11. package/skills/gif-search.md +28 -171
  12. package/skills/healthcheck.md +21 -203
  13. package/skills/image-gen.md +24 -123
  14. package/skills/model-usage.md +18 -165
  15. package/skills/obsidian.md +28 -174
  16. package/skills/pdf.md +30 -181
  17. package/skills/research.md +6 -6
  18. package/skills/skill-creator.md +35 -111
  19. package/skills/spotify.md +2 -17
  20. package/skills/summarize.md +36 -193
  21. package/skills/things.md +23 -175
  22. package/skills/tmux.md +1 -91
  23. package/skills/trello.md +32 -157
  24. package/skills/video-frames.md +26 -166
  25. package/skills/weather.md +6 -6
  26. package/src/a2a/A2AClient.js +2 -2
  27. package/src/a2a/A2AServer.js +6 -6
  28. package/src/a2a/AgentCard.js +2 -2
  29. package/src/agents/SubAgentManager.js +61 -19
  30. package/src/agents/Supervisor.js +4 -4
  31. package/src/channels/BaseChannel.js +6 -6
  32. package/src/channels/BlueBubblesChannel.js +112 -0
  33. package/src/channels/DiscordChannel.js +8 -8
  34. package/src/channels/EmailChannel.js +54 -26
  35. package/src/channels/FeishuChannel.js +140 -0
  36. package/src/channels/GoogleChatChannel.js +8 -8
  37. package/src/channels/HttpChannel.js +2 -2
  38. package/src/channels/IRCChannel.js +144 -0
  39. package/src/channels/LineChannel.js +13 -13
  40. package/src/channels/MatrixChannel.js +97 -0
  41. package/src/channels/MattermostChannel.js +119 -0
  42. package/src/channels/NextcloudChannel.js +133 -0
  43. package/src/channels/NostrChannel.js +175 -0
  44. package/src/channels/SignalChannel.js +9 -9
  45. package/src/channels/SlackChannel.js +10 -10
  46. package/src/channels/TeamsChannel.js +10 -10
  47. package/src/channels/TelegramChannel.js +8 -8
  48. package/src/channels/TwitchChannel.js +128 -0
  49. package/src/channels/WhatsAppChannel.js +10 -10
  50. package/src/channels/ZaloChannel.js +119 -0
  51. package/src/channels/iMessageChannel.js +150 -0
  52. package/src/channels/index.js +241 -11
  53. package/src/cli.js +835 -38
  54. package/src/config/agentProfiles.js +19 -19
  55. package/src/config/channels.js +1 -1
  56. package/src/config/default.js +12 -7
  57. package/src/config/models.js +3 -3
  58. package/src/config/permissions.js +2 -2
  59. package/src/core/AgentLoop.js +13 -13
  60. package/src/core/Compaction.js +3 -3
  61. package/src/core/CostTracker.js +2 -2
  62. package/src/core/EventBus.js +15 -15
  63. package/src/core/TaskQueue.js +24 -7
  64. package/src/core/TaskRunner.js +19 -6
  65. package/src/daemon/DaemonManager.js +4 -4
  66. package/src/hooks/HookRunner.js +4 -4
  67. package/src/index.js +6 -2
  68. package/src/mcp/MCPAgentRunner.js +3 -3
  69. package/src/mcp/MCPClient.js +9 -9
  70. package/src/mcp/MCPManager.js +14 -14
  71. package/src/models/ModelRouter.js +2 -2
  72. package/src/safety/AuditLog.js +3 -3
  73. package/src/safety/CircuitBreaker.js +2 -2
  74. package/src/safety/CommandGuard.js +132 -0
  75. package/src/safety/FilesystemGuard.js +23 -3
  76. package/src/safety/GitRollback.js +5 -5
  77. package/src/safety/HumanApproval.js +9 -9
  78. package/src/safety/InputSanitizer.js +81 -8
  79. package/src/safety/PermissionGuard.js +2 -2
  80. package/src/safety/Sandbox.js +1 -1
  81. package/src/safety/SecretScanner.js +90 -28
  82. package/src/safety/SecretVault.js +2 -2
  83. package/src/scheduler/Heartbeat.js +3 -3
  84. package/src/scheduler/Scheduler.js +6 -6
  85. package/src/setup/theme.js +171 -66
  86. package/src/setup/wizard.js +432 -57
  87. package/src/skills/SkillLoader.js +145 -8
  88. package/src/storage/TaskStore.js +39 -15
  89. package/src/systemPrompt.js +45 -43
  90. package/src/tenants/TenantManager.js +79 -22
  91. package/src/tools/ToolRegistry.js +3 -3
  92. package/src/tools/applyPatch.js +2 -2
  93. package/src/tools/browserAutomation.js +4 -4
  94. package/src/tools/calendar.js +155 -0
  95. package/src/tools/clipboard.js +71 -0
  96. package/src/tools/contacts.js +138 -0
  97. package/src/tools/createDocument.js +2 -2
  98. package/src/tools/cronTool.js +14 -14
  99. package/src/tools/database.js +165 -0
  100. package/src/tools/editFile.js +10 -10
  101. package/src/tools/executeCommand.js +11 -3
  102. package/src/tools/generateImage.js +79 -0
  103. package/src/tools/gitTool.js +141 -0
  104. package/src/tools/glob.js +1 -1
  105. package/src/tools/googlePlaces.js +136 -0
  106. package/src/tools/grep.js +2 -2
  107. package/src/tools/iMessageTool.js +86 -0
  108. package/src/tools/imageAnalysis.js +3 -3
  109. package/src/tools/index.js +56 -2
  110. package/src/tools/makeVoiceCall.js +283 -0
  111. package/src/tools/manageAgents.js +2 -2
  112. package/src/tools/manageMCP.js +38 -20
  113. package/src/tools/memory.js +25 -32
  114. package/src/tools/messageChannel.js +1 -1
  115. package/src/tools/notification.js +90 -0
  116. package/src/tools/philipsHue.js +147 -0
  117. package/src/tools/projectTracker.js +8 -8
  118. package/src/tools/readFile.js +1 -1
  119. package/src/tools/readPDF.js +73 -0
  120. package/src/tools/screenCapture.js +6 -6
  121. package/src/tools/searchContent.js +2 -2
  122. package/src/tools/searchFiles.js +1 -1
  123. package/src/tools/sendEmail.js +79 -24
  124. package/src/tools/sendFile.js +4 -4
  125. package/src/tools/sonos.js +137 -0
  126. package/src/tools/sshTool.js +130 -0
  127. package/src/tools/textToSpeech.js +5 -5
  128. package/src/tools/transcribeAudio.js +4 -4
  129. package/src/tools/useMCP.js +4 -4
  130. package/src/tools/webFetch.js +2 -2
  131. package/src/tools/webSearch.js +1 -1
  132. package/src/utils/Embeddings.js +79 -0
  133. package/src/voice/VoiceSessionManager.js +170 -0
  134. package/src/voice/VoiceWebhook.js +188 -0
@@ -4,7 +4,7 @@ import { randomBytes, createCipheriv, createDecipheriv, scryptSync } from "node:
4
4
  import { config } from "../config/default.js";
5
5
 
6
6
  /**
7
- * TenantManager per-user configuration and isolation for multi-tenant deployments.
7
+ * TenantManager - per-user configuration and isolation for multi-tenant deployments.
8
8
  *
9
9
  * A tenant is any unique user identified by their channel + userId:
10
10
  * tenantId = "telegram:123456789"
@@ -12,22 +12,22 @@ import { config } from "../config/default.js";
12
12
  * tenantId = "email:user@example.com"
13
13
  *
14
14
  * Each tenant can have:
15
- * model override the default model (e.g. cheaper model for free tier)
16
- * allowedPaths filesystem paths this tenant's tasks can access
17
- * blockedPaths paths always blocked for this tenant
18
- * maxCostPerTask per-task spend limit (overrides global)
19
- * maxDailyCost per-tenant daily budget
20
- * tools tool allowlist (empty = all tools allowed by permission tier)
21
- * mcpServers MCP server allowlist: ["github","linear"] | null (null = all allowed)
22
- * modelRoutes task-type model overrides: { coder: "anthropic:...", researcher: "google:..." }
23
- * encryptedApiKeys AES-256-GCM encrypted per-tenant API keys (managed via setApiKey/deleteApiKey)
24
- * suspended block all tasks from this tenant
25
- * plan "free" | "pro" | "admin" (for display / future rate limiting)
26
- * createdAt ISO timestamp of first message
27
- * lastSeenAt ISO timestamp of last message
28
- * totalCost lifetime spend for this tenant
29
- * taskCount total tasks submitted
30
- * notes free-text operator notes
15
+ * model - override the default model (e.g. cheaper model for free tier)
16
+ * allowedPaths - filesystem paths this tenant's tasks can access
17
+ * blockedPaths - paths always blocked for this tenant
18
+ * maxCostPerTask - per-task spend limit (overrides global)
19
+ * maxDailyCost - per-tenant daily budget
20
+ * tools - tool allowlist (empty = all tools allowed by permission tier)
21
+ * mcpServers - MCP server allowlist: ["github","linear"] | null (null = all allowed)
22
+ * modelRoutes - task-type model overrides: { coder: "anthropic:...", researcher: "google:..." }
23
+ * encryptedApiKeys - AES-256-GCM encrypted per-tenant API keys (managed via setApiKey/deleteApiKey)
24
+ * suspended - block all tasks from this tenant
25
+ * plan - "free" | "pro" | "admin" (for display / future rate limiting)
26
+ * createdAt - ISO timestamp of first message
27
+ * lastSeenAt - ISO timestamp of last message
28
+ * totalCost - lifetime spend for this tenant
29
+ * taskCount - total tasks submitted
30
+ * notes - free-text operator notes
31
31
  *
32
32
  * Storage: data/tenants/tenants.json (flat JSON map of tenantId → config)
33
33
  * Workspaces: data/tenants/{tenantId}/workspace/ (isolated per-tenant directory)
@@ -124,7 +124,7 @@ class TenantManager {
124
124
  }
125
125
 
126
126
  /**
127
- * Update tenant config (partial update only provided keys are changed).
127
+ * Update tenant config (partial update - only provided keys are changed).
128
128
  */
129
129
  set(tenantId, updates) {
130
130
  const tenants = this._load();
@@ -197,6 +197,61 @@ class TenantManager {
197
197
  return true;
198
198
  }
199
199
 
200
+ // ── Per-Tenant Channel Config ─────────────────────────────────────────────
201
+
202
+ /**
203
+ * Store a per-tenant channel credential, encrypted with AES-256-GCM.
204
+ * Valid keys: email, email_password, resend_api_key, resend_from
205
+ *
206
+ * @param {string} tenantId - e.g. "telegram:123"
207
+ * @param {string} key - e.g. "email"
208
+ * @param {string} value - plaintext credential value
209
+ */
210
+ setChannelConfig(tenantId, key, value) {
211
+ const tenants = this._load();
212
+ if (!tenants[tenantId]) tenants[tenantId] = _defaultTenant(tenantId);
213
+ tenants[tenantId].encryptedChannelConfig = tenants[tenantId].encryptedChannelConfig || {};
214
+ tenants[tenantId].encryptedChannelConfig[key] = _encryptTenantValue(value);
215
+ tenants[tenantId].updatedAt = new Date().toISOString();
216
+ this._save(tenants);
217
+ return true;
218
+ }
219
+
220
+ /**
221
+ * Delete a per-tenant channel credential.
222
+ */
223
+ deleteChannelConfig(tenantId, key) {
224
+ const tenants = this._load();
225
+ if (!tenants[tenantId]?.encryptedChannelConfig?.[key]) return false;
226
+ delete tenants[tenantId].encryptedChannelConfig[key];
227
+ tenants[tenantId].updatedAt = new Date().toISOString();
228
+ this._save(tenants);
229
+ return true;
230
+ }
231
+
232
+ /**
233
+ * List the credential keys stored for a tenant (not values).
234
+ */
235
+ listChannelConfigKeys(tenantId) {
236
+ const tenants = this._load();
237
+ return Object.keys(tenants[tenantId]?.encryptedChannelConfig || {});
238
+ }
239
+
240
+ /**
241
+ * Decrypt and return all channel credentials for a tenant.
242
+ * Returns {} if none stored.
243
+ */
244
+ getDecryptedChannelConfig(tenantId) {
245
+ const tenants = this._load();
246
+ const encrypted = tenants[tenantId]?.encryptedChannelConfig || {};
247
+ const result = {};
248
+ for (const [key, val] of Object.entries(encrypted)) {
249
+ const decrypted = _decryptTenantValue(val);
250
+ if (decrypted !== null) result[key] = decrypted;
251
+ }
252
+ return result;
253
+ }
254
+
200
255
  // ── Per-Tenant API Key Management ─────────────────────────────────────────
201
256
 
202
257
  /**
@@ -312,7 +367,8 @@ class TenantManager {
312
367
  sandbox: config.sandbox?.mode || "process",
313
368
  mcpServers: tenant?.mcpServers ?? null, // null = all MCP servers allowed
314
369
  modelRoutes: tenant?.modelRoutes || null, // null = use global env vars
315
- apiKeys: tenant?.id ? this.getDecryptedApiKeys(tenant.id) : {}, // per-tenant API keys
370
+ apiKeys: tenant?.id ? this.getDecryptedApiKeys(tenant.id) : {}, // per-tenant AI provider keys
371
+ channelConfig: tenant?.id ? this.getDecryptedChannelConfig(tenant.id) : {}, // per-tenant channel credentials
316
372
  };
317
373
  }
318
374
 
@@ -360,9 +416,10 @@ function _defaultTenant(id) {
360
416
  maxCostPerTask: null,
361
417
  maxDailyCost: null,
362
418
  tools: null,
363
- mcpServers: null, // null = all MCP servers allowed; ["github","linear"] = allowlist
364
- modelRoutes: null, // null = use global env vars; { coder: "anthropic:..." }
365
- encryptedApiKeys: {}, // AES-256-GCM encrypted per-tenant API keys
419
+ mcpServers: null, // null = all MCP servers allowed; ["github","linear"] = allowlist
420
+ modelRoutes: null, // null = use global env vars; { coder: "anthropic:..." }
421
+ encryptedApiKeys: {}, // AES-256-GCM encrypted per-tenant AI provider keys
422
+ encryptedChannelConfig: {}, // AES-256-GCM encrypted per-tenant channel credentials (email, resend, etc.)
366
423
  suspended: false,
367
424
  suspendReason: "",
368
425
  plan: "free",
@@ -25,7 +25,7 @@ class ToolRegistry {
25
25
  }
26
26
 
27
27
  /**
28
- * Get tool function map { name: fn } backward compatible with current toolFunctions export.
28
+ * Get tool function map { name: fn } - backward compatible with current toolFunctions export.
29
29
  */
30
30
  getToolFunctions() {
31
31
  const fns = {};
@@ -36,7 +36,7 @@ class ToolRegistry {
36
36
  }
37
37
 
38
38
  /**
39
- * Get tool description strings backward compatible with current toolDescriptions export.
39
+ * Get tool description strings - backward compatible with current toolDescriptions export.
40
40
  */
41
41
  getToolDescriptions() {
42
42
  const descs = [];
@@ -50,7 +50,7 @@ class ToolRegistry {
50
50
  }
51
51
 
52
52
  /**
53
- * Build tool docs for system prompt grouped by category.
53
+ * Build tool docs for system prompt - grouped by category.
54
54
  */
55
55
  buildToolDocs() {
56
56
  const categories = {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * applyPatch(filePath, patch) Apply a unified diff patch to a file.
2
+ * applyPatch(filePath, patch) - Apply a unified diff patch to a file.
3
3
  * Handles multi-hunk edits that editFile's single find-replace can't do.
4
4
  * Inspired by OpenClaw's apply_patch tool.
5
5
  */
@@ -53,7 +53,7 @@ function applyHunk(fileLines, hunk, offset) {
53
53
  let ri = pos;
54
54
  for (const hunkLine of hunk.lines) {
55
55
  if (hunkLine.startsWith(" ") || hunkLine === "") {
56
- // context line must match
56
+ // context line - must match
57
57
  if (ri >= fileLines.length) return null;
58
58
  if (fileLines[ri].trimEnd() !== hunkLine.slice(1).trimEnd()) return null;
59
59
  ri++;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Browser Automation Playwright-based web interaction.
2
+ * Browser Automation - Playwright-based web interaction.
3
3
  * Upgraded: multi-tab, navigation guard, dialog handling, waitFor, cookies.
4
4
  */
5
5
 
@@ -7,7 +7,7 @@ let browser = null;
7
7
  let browserContext = null;
8
8
  const pages = []; // Multi-tab support
9
9
 
10
- // Blocked navigation patterns SSRF / security guard
10
+ // Blocked navigation patterns - SSRF / security guard
11
11
  const NAV_BLOCKLIST = [
12
12
  /^file:\/\//i,
13
13
  /^(http:\/\/|https:\/\/)(127\.|0\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.)/,
@@ -37,7 +37,7 @@ async function ensureBrowser() {
37
37
 
38
38
  // Auto-handle dialogs (accept by default)
39
39
  browserContext.on("dialog", async (dialog) => {
40
- console.log(` [browser] Auto-dismissed dialog: ${dialog.type()} "${dialog.message().slice(0, 80)}"`);
40
+ console.log(` [browser] Auto-dismissed dialog: ${dialog.type()} - "${dialog.message().slice(0, 80)}"`);
41
41
  await dialog.dismiss();
42
42
  });
43
43
 
@@ -148,7 +148,7 @@ export async function browserAction(action, param1, param2) {
148
148
  if (p.isClosed()) return ` ${i}: [closed]`;
149
149
  const title = await p.title().catch(() => "?");
150
150
  const url = p.url();
151
- return ` ${i}${i === pages.length - 1 ? " (active)" : ""}: ${title} ${url}`;
151
+ return ` ${i}${i === pages.length - 1 ? " (active)" : ""}: ${title} - ${url}`;
152
152
  })
153
153
  );
154
154
  return `Open tabs (${pages.length}):\n${titles.join("\n")}`;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * calendar - Read and create calendar events.
3
+ * macOS: AppleScript (Calendar app). Google Calendar: Google Calendar API.
4
+ * Supports: list upcoming events, create event, delete event.
5
+ */
6
+ import { execSync } from "node:child_process";
7
+
8
+ export async function calendar(action, paramsJson) {
9
+ if (!action) return "Error: action required. Valid: list, create, delete, search";
10
+ const params = paramsJson
11
+ ? (typeof paramsJson === "string" ? JSON.parse(paramsJson) : paramsJson)
12
+ : {};
13
+
14
+ const { provider = "macos" } = params;
15
+
16
+ // ── macOS Calendar (AppleScript) ────────────────────────────────────────
17
+ if (provider === "macos") {
18
+ if (process.platform !== "darwin") {
19
+ return "Error: macOS Calendar provider only works on macOS";
20
+ }
21
+
22
+ if (action === "list") {
23
+ const days = params.days || 7;
24
+ const script = `
25
+ tell application "Calendar"
26
+ set startDate to current date
27
+ set endDate to (current date) + (${days} * days)
28
+ set eventList to ""
29
+ set theCalendars to every calendar
30
+ repeat with aCal in theCalendars
31
+ set theEvents to (every event of aCal whose start date >= startDate and start date <= endDate)
32
+ repeat with anEvent in theEvents
33
+ set eventList to eventList & summary of anEvent & " | " & (start date of anEvent as string) & "\\n"
34
+ end repeat
35
+ end repeat
36
+ return eventList
37
+ end tell
38
+ `;
39
+ try {
40
+ const out = execSync(`osascript -e '${script.replace(/'/g, "\\'")}'`, { encoding: "utf-8", timeout: 15000 });
41
+ if (!out.trim()) return `No events found in the next ${days} days`;
42
+ return `Upcoming events (next ${days} days):\n${out.trim()}`;
43
+ } catch (err) {
44
+ return `Calendar error: ${err.message}`;
45
+ }
46
+ }
47
+
48
+ if (action === "create") {
49
+ const { title, startDate, endDate, calendarName = "Calendar", notes = "" } = params;
50
+ if (!title) return "Error: title is required";
51
+ if (!startDate) return "Error: startDate is required (e.g. '2026-03-10T10:00:00')";
52
+
53
+ const start = new Date(startDate);
54
+ const end = endDate ? new Date(endDate) : new Date(start.getTime() + 60 * 60 * 1000);
55
+
56
+ const fmtDate = (d) => {
57
+ return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:00`;
58
+ };
59
+
60
+ const script = `
61
+ tell application "Calendar"
62
+ tell calendar "${calendarName.replace(/"/g, '\\"')}"
63
+ make new event with properties {summary:"${title.replace(/"/g, '\\"')}", start date:date "${fmtDate(start)}", end date:date "${fmtDate(end)}"${notes ? `, description:"${notes.replace(/"/g, '\\"')}"` : ""}}
64
+ end tell
65
+ end tell
66
+ `;
67
+ try {
68
+ execSync(`osascript -e '${script.replace(/'/g, "\\'")}'`, { timeout: 10000 });
69
+ return `Event created: "${title}" on ${start.toLocaleString()}`;
70
+ } catch (err) {
71
+ return `Calendar create error: ${err.message}`;
72
+ }
73
+ }
74
+
75
+ if (action === "search") {
76
+ const { query } = params;
77
+ if (!query) return "Error: query is required";
78
+ const script = `
79
+ tell application "Calendar"
80
+ set hits to ""
81
+ repeat with aCal in every calendar
82
+ repeat with anEvent in (every event of aCal whose summary contains "${query.replace(/"/g, '\\"')}")
83
+ set hits to hits & summary of anEvent & " | " & (start date of anEvent as string) & "\\n"
84
+ end repeat
85
+ end repeat
86
+ return hits
87
+ end tell
88
+ `;
89
+ try {
90
+ const out = execSync(`osascript -e '${script.replace(/'/g, "\\'")}'`, { encoding: "utf-8", timeout: 15000 });
91
+ if (!out.trim()) return `No events found matching "${query}"`;
92
+ return `Events matching "${query}":\n${out.trim()}`;
93
+ } catch (err) {
94
+ return `Calendar search error: ${err.message}`;
95
+ }
96
+ }
97
+ }
98
+
99
+ // ── Google Calendar (API) ────────────────────────────────────────────────
100
+ if (provider === "google") {
101
+ const apiKey = process.env.GOOGLE_CALENDAR_API_KEY;
102
+ const calId = params.calendarId || process.env.GOOGLE_CALENDAR_ID || "primary";
103
+ if (!apiKey) return "Error: GOOGLE_CALENDAR_API_KEY env var required for Google Calendar provider";
104
+
105
+ const fetchFn = globalThis.fetch || (await import("node-fetch")).default;
106
+
107
+ if (action === "list") {
108
+ const timeMin = new Date().toISOString();
109
+ const maxResults = params.maxResults || 10;
110
+ const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calId)}/events?key=${apiKey}&timeMin=${timeMin}&maxResults=${maxResults}&singleEvents=true&orderBy=startTime`;
111
+ const res = await fetchFn(url);
112
+ const data = await res.json();
113
+ if (!res.ok) return `Google Calendar error: ${data.error?.message}`;
114
+ if (!data.items?.length) return "No upcoming events";
115
+ return data.items.map(e => `${e.summary} | ${e.start?.dateTime || e.start?.date}`).join("\n");
116
+ }
117
+
118
+ if (action === "create") {
119
+ const accessToken = process.env.GOOGLE_CALENDAR_ACCESS_TOKEN;
120
+ if (!accessToken) return "Error: GOOGLE_CALENDAR_ACCESS_TOKEN required to create Google Calendar events";
121
+ const { title, startDate, endDate, notes } = params;
122
+ if (!title || !startDate) return "Error: title and startDate required";
123
+ const event = {
124
+ summary: title,
125
+ description: notes || "",
126
+ start: { dateTime: new Date(startDate).toISOString() },
127
+ end: { dateTime: new Date(endDate || new Date(startDate).getTime() + 3600000).toISOString() },
128
+ };
129
+ const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calId)}/events`;
130
+ const res = await fetchFn(url, {
131
+ method: "POST",
132
+ headers: { "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json" },
133
+ body: JSON.stringify(event),
134
+ });
135
+ const data = await res.json();
136
+ if (!res.ok) return `Google Calendar create error: ${data.error?.message}`;
137
+ return `Event created: "${data.summary}" on ${data.start.dateTime}`;
138
+ }
139
+ }
140
+
141
+ return `Unknown action: "${action}" for provider "${provider}". Valid actions: list, create, search`;
142
+ }
143
+
144
+ export const calendarDescription =
145
+ `calendar(action: string, paramsJson?: object) - Read/create calendar events.
146
+ action: "list" | "create" | "search"
147
+ params.provider: "macos" (default, AppleScript) | "google" (Google Calendar API)
148
+ list params: { days?: 7, provider, calendarId? }
149
+ create params: { title, startDate: "ISO string", endDate?, calendarName?, notes?, provider }
150
+ search params: { query, provider }
151
+ Env vars: GOOGLE_CALENDAR_API_KEY, GOOGLE_CALENDAR_ID, GOOGLE_CALENDAR_ACCESS_TOKEN
152
+ Examples:
153
+ calendar("list", {"days":3})
154
+ calendar("create", {"title":"Team standup","startDate":"2026-03-10T09:00:00"})
155
+ calendar("list", {"provider":"google","maxResults":5})`;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * clipboard - Read from or write to the system clipboard.
3
+ * macOS: pbpaste/pbcopy. Linux: xclip/xsel. Windows: clip/powershell.
4
+ */
5
+ import { execSync, execFileSync } from "node:child_process";
6
+
7
+ function platform() {
8
+ return process.platform;
9
+ }
10
+
11
+ export async function clipboard(action, text) {
12
+ if (!action) return 'Error: action required. Valid: read, write, clear';
13
+
14
+ try {
15
+ if (action === "read") {
16
+ let out;
17
+ if (platform() === "darwin") {
18
+ out = execSync("pbpaste", { encoding: "utf-8", timeout: 5000 });
19
+ } else if (platform() === "linux") {
20
+ try {
21
+ out = execSync("xclip -selection clipboard -o", { encoding: "utf-8", timeout: 5000 });
22
+ } catch {
23
+ out = execSync("xsel --clipboard --output", { encoding: "utf-8", timeout: 5000 });
24
+ }
25
+ } else if (platform() === "win32") {
26
+ out = execSync("powershell -command Get-Clipboard", { encoding: "utf-8", timeout: 5000 });
27
+ } else {
28
+ return "Error: clipboard read not supported on this platform.";
29
+ }
30
+ const content = out.trim();
31
+ if (!content) return "(clipboard is empty)";
32
+ return `Clipboard content:\n${content}`;
33
+ }
34
+
35
+ if (action === "write") {
36
+ if (!text) return "Error: text is required for write.";
37
+ if (platform() === "darwin") {
38
+ execSync(`echo ${JSON.stringify(text)} | pbcopy`, { timeout: 5000 });
39
+ } else if (platform() === "linux") {
40
+ try {
41
+ execSync(`echo ${JSON.stringify(text)} | xclip -selection clipboard`, { timeout: 5000 });
42
+ } catch {
43
+ execSync(`echo ${JSON.stringify(text)} | xsel --clipboard --input`, { timeout: 5000 });
44
+ }
45
+ } else if (platform() === "win32") {
46
+ execSync(`echo ${text} | clip`, { timeout: 5000 });
47
+ } else {
48
+ return "Error: clipboard write not supported on this platform.";
49
+ }
50
+ const preview = text.length > 80 ? text.slice(0, 80) + "..." : text;
51
+ return `Copied to clipboard: "${preview}"`;
52
+ }
53
+
54
+ if (action === "clear") {
55
+ return await clipboard("write", "");
56
+ }
57
+
58
+ return `Unknown action: "${action}". Valid: read, write, clear`;
59
+ } catch (err) {
60
+ return `Clipboard error: ${err.message}. Make sure xclip or xsel is installed on Linux.`;
61
+ }
62
+ }
63
+
64
+ export const clipboardDescription =
65
+ `clipboard(action: string, text?: string) - Read or write the system clipboard.
66
+ action: "read" | "write" | "clear"
67
+ text: content to write (required for write)
68
+ Examples:
69
+ clipboard("read") → returns clipboard contents
70
+ clipboard("write", "Hello World") → copies text to clipboard
71
+ clipboard("clear") → clears clipboard`;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * contacts - Read and search contacts.
3
+ * macOS: AppleScript (Contacts app). Google: People API.
4
+ */
5
+ import { execSync } from "node:child_process";
6
+
7
+ export async function contacts(action, paramsJson) {
8
+ if (!action) return "Error: action required. Valid: search, list, get";
9
+ const params = paramsJson
10
+ ? (typeof paramsJson === "string" ? JSON.parse(paramsJson) : paramsJson)
11
+ : {};
12
+
13
+ const { provider = "macos" } = params;
14
+
15
+ // ── macOS Contacts (AppleScript) ────────────────────────────────────────
16
+ if (provider === "macos") {
17
+ if (process.platform !== "darwin") {
18
+ return "Error: macOS Contacts provider only works on macOS";
19
+ }
20
+
21
+ if (action === "search" || action === "list") {
22
+ const query = params.query || "";
23
+ const limit = params.limit || 20;
24
+
25
+ const searchFilter = query
26
+ ? `whose (first name contains "${query.replace(/"/g, '\\"')}" or last name contains "${query.replace(/"/g, '\\"')}" or (email addresses is not {} and value of item 1 of email addresses contains "${query.replace(/"/g, '\\"')}"))`
27
+ : "";
28
+
29
+ const script = `
30
+ tell application "Contacts"
31
+ set output to ""
32
+ set peopleList to every person ${searchFilter}
33
+ set resultCount to 0
34
+ repeat with aPerson in peopleList
35
+ if resultCount >= ${limit} then exit repeat
36
+ set personName to (first name of aPerson & " " & last name of aPerson)
37
+ set emails to ""
38
+ if (count of email addresses of aPerson) > 0 then
39
+ set emails to value of item 1 of email addresses of aPerson
40
+ end if
41
+ set phones to ""
42
+ if (count of phones of aPerson) > 0 then
43
+ set phones to value of item 1 of phones of aPerson
44
+ end if
45
+ set output to output & personName & " | " & emails & " | " & phones & "\\n"
46
+ set resultCount to resultCount + 1
47
+ end repeat
48
+ return output
49
+ end tell
50
+ `;
51
+
52
+ try {
53
+ const out = execSync(`osascript -e '${script.replace(/'/g, "\\'")}'`, { encoding: "utf-8", timeout: 15000 });
54
+ if (!out.trim()) return query ? `No contacts found matching "${query}"` : "No contacts found";
55
+ return `Contacts${query ? ` matching "${query}"` : ""}:\n${out.trim()}`;
56
+ } catch (err) {
57
+ return `Contacts error: ${err.message}. Make sure Contacts app access is granted.`;
58
+ }
59
+ }
60
+
61
+ if (action === "get") {
62
+ const { name } = params;
63
+ if (!name) return "Error: name is required";
64
+
65
+ const script = `
66
+ tell application "Contacts"
67
+ set aPerson to first person whose (first name & " " & last name) contains "${name.replace(/"/g, '\\"')}"
68
+ set output to "Name: " & first name of aPerson & " " & last name of aPerson & "\\n"
69
+ if (count of email addresses of aPerson) > 0 then
70
+ repeat with anEmail in email addresses of aPerson
71
+ set output to output & "Email (" & label of anEmail & "): " & value of anEmail & "\\n"
72
+ end repeat
73
+ end if
74
+ if (count of phones of aPerson) > 0 then
75
+ repeat with aPhone in phones of aPerson
76
+ set output to output & "Phone (" & label of aPhone & "): " & value of aPhone & "\\n"
77
+ end repeat
78
+ end if
79
+ if organization of aPerson is not missing value then
80
+ set output to output & "Company: " & organization of aPerson & "\\n"
81
+ end if
82
+ return output
83
+ end tell
84
+ `;
85
+
86
+ try {
87
+ const out = execSync(`osascript -e '${script.replace(/'/g, "\\'")}'`, { encoding: "utf-8", timeout: 10000 });
88
+ return out.trim() || `No contact found matching "${name}"`;
89
+ } catch (err) {
90
+ return `Contact lookup error: ${err.message}`;
91
+ }
92
+ }
93
+ }
94
+
95
+ // ── Google People API ────────────────────────────────────────────────────
96
+ if (provider === "google") {
97
+ const accessToken = process.env.GOOGLE_CONTACTS_ACCESS_TOKEN;
98
+ if (!accessToken) return "Error: GOOGLE_CONTACTS_ACCESS_TOKEN env var required";
99
+
100
+ const fetchFn = globalThis.fetch || (await import("node-fetch")).default;
101
+
102
+ if (action === "list" || action === "search") {
103
+ const query = params.query || "";
104
+ let url;
105
+ if (query) {
106
+ url = `https://people.googleapis.com/v1/people:searchContacts?query=${encodeURIComponent(query)}&readMask=names,emailAddresses,phoneNumbers`;
107
+ } else {
108
+ url = `https://people.googleapis.com/v1/people/me/connections?personFields=names,emailAddresses,phoneNumbers&pageSize=${params.limit || 20}`;
109
+ }
110
+ const res = await fetchFn(url, { headers: { "Authorization": `Bearer ${accessToken}` } });
111
+ const data = await res.json();
112
+ if (!res.ok) return `Google Contacts error: ${data.error?.message}`;
113
+ const people = data.connections || data.results?.map(r => r.person) || [];
114
+ if (!people.length) return "No contacts found";
115
+ return people.map(p => {
116
+ const name = p.names?.[0]?.displayName || "Unknown";
117
+ const email = p.emailAddresses?.[0]?.value || "";
118
+ const phone = p.phoneNumbers?.[0]?.value || "";
119
+ return `${name} | ${email} | ${phone}`;
120
+ }).join("\n");
121
+ }
122
+ }
123
+
124
+ return `Unknown action: "${action}" for provider "${provider}". Valid: search, list, get`;
125
+ }
126
+
127
+ export const contactsDescription =
128
+ `contacts(action: string, paramsJson?: object) - Search and read contacts.
129
+ action: "search" | "list" | "get"
130
+ params.provider: "macos" (default, Contacts app) | "google" (People API)
131
+ search/list params: { query?, limit?: 20 }
132
+ get params: { name }
133
+ Env vars: GOOGLE_CONTACTS_ACCESS_TOKEN
134
+ Examples:
135
+ contacts("search", {"query":"John"})
136
+ contacts("list", {"limit":10})
137
+ contacts("get", {"name":"Alice Smith"})
138
+ contacts("search", {"provider":"google","query":"alice"})`;
@@ -2,7 +2,7 @@ import { writeFileSync, mkdirSync, readFileSync } from "fs";
2
2
  import { dirname } from "path";
3
3
 
4
4
  /**
5
- * Create Document creates Markdown, text, PDF, or DOCX documents.
5
+ * Create Document - creates Markdown, text, PDF, or DOCX documents.
6
6
  * Upgraded: better PDF rendering (bold, italic, code, tables, numbered lists),
7
7
  * optional DOCX support via 'docx' package.
8
8
  */
@@ -26,7 +26,7 @@ export async function createDocument(filePath, content, format) {
26
26
  return await createDOCX(filePath, content);
27
27
  }
28
28
 
29
- // Markdown / text / html write as-is
29
+ // Markdown / text / html - write as-is
30
30
  writeFileSync(filePath, content, "utf-8");
31
31
  console.log(` [createDocument] Written: ${filePath} (${content.length} chars)`);
32
32
  return `Document created: ${filePath} (${content.length} characters)`;