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.
- package/README.md +106 -76
- package/SOUL.md +100 -28
- package/config/mcp.json +9 -9
- package/package.json +15 -8
- package/skills/apple-notes.md +0 -52
- package/skills/apple-reminders.md +1 -87
- package/skills/camsnap.md +20 -144
- package/skills/coding.md +7 -7
- package/skills/documents.md +6 -6
- package/skills/email.md +6 -6
- package/skills/gif-search.md +28 -171
- package/skills/healthcheck.md +21 -203
- package/skills/image-gen.md +24 -123
- package/skills/model-usage.md +18 -165
- package/skills/obsidian.md +28 -174
- package/skills/pdf.md +30 -181
- package/skills/research.md +6 -6
- package/skills/skill-creator.md +35 -111
- package/skills/spotify.md +2 -17
- package/skills/summarize.md +36 -193
- package/skills/things.md +23 -175
- package/skills/tmux.md +1 -91
- package/skills/trello.md +32 -157
- package/skills/video-frames.md +26 -166
- package/skills/weather.md +6 -6
- package/src/a2a/A2AClient.js +2 -2
- package/src/a2a/A2AServer.js +6 -6
- package/src/a2a/AgentCard.js +2 -2
- package/src/agents/SubAgentManager.js +61 -19
- package/src/agents/Supervisor.js +4 -4
- package/src/channels/BaseChannel.js +6 -6
- package/src/channels/BlueBubblesChannel.js +112 -0
- package/src/channels/DiscordChannel.js +8 -8
- package/src/channels/EmailChannel.js +54 -26
- package/src/channels/FeishuChannel.js +140 -0
- package/src/channels/GoogleChatChannel.js +8 -8
- package/src/channels/HttpChannel.js +2 -2
- package/src/channels/IRCChannel.js +144 -0
- package/src/channels/LineChannel.js +13 -13
- package/src/channels/MatrixChannel.js +97 -0
- package/src/channels/MattermostChannel.js +119 -0
- package/src/channels/NextcloudChannel.js +133 -0
- package/src/channels/NostrChannel.js +175 -0
- package/src/channels/SignalChannel.js +9 -9
- package/src/channels/SlackChannel.js +10 -10
- package/src/channels/TeamsChannel.js +10 -10
- package/src/channels/TelegramChannel.js +8 -8
- package/src/channels/TwitchChannel.js +128 -0
- package/src/channels/WhatsAppChannel.js +10 -10
- package/src/channels/ZaloChannel.js +119 -0
- package/src/channels/iMessageChannel.js +150 -0
- package/src/channels/index.js +241 -11
- package/src/cli.js +835 -38
- package/src/config/agentProfiles.js +19 -19
- package/src/config/channels.js +1 -1
- package/src/config/default.js +12 -7
- package/src/config/models.js +3 -3
- package/src/config/permissions.js +2 -2
- package/src/core/AgentLoop.js +13 -13
- package/src/core/Compaction.js +3 -3
- package/src/core/CostTracker.js +2 -2
- package/src/core/EventBus.js +15 -15
- package/src/core/TaskQueue.js +24 -7
- package/src/core/TaskRunner.js +19 -6
- package/src/daemon/DaemonManager.js +4 -4
- package/src/hooks/HookRunner.js +4 -4
- package/src/index.js +6 -2
- package/src/mcp/MCPAgentRunner.js +3 -3
- package/src/mcp/MCPClient.js +9 -9
- package/src/mcp/MCPManager.js +14 -14
- package/src/models/ModelRouter.js +2 -2
- package/src/safety/AuditLog.js +3 -3
- package/src/safety/CircuitBreaker.js +2 -2
- package/src/safety/CommandGuard.js +132 -0
- package/src/safety/FilesystemGuard.js +23 -3
- package/src/safety/GitRollback.js +5 -5
- package/src/safety/HumanApproval.js +9 -9
- package/src/safety/InputSanitizer.js +81 -8
- package/src/safety/PermissionGuard.js +2 -2
- package/src/safety/Sandbox.js +1 -1
- package/src/safety/SecretScanner.js +90 -28
- package/src/safety/SecretVault.js +2 -2
- package/src/scheduler/Heartbeat.js +3 -3
- package/src/scheduler/Scheduler.js +6 -6
- package/src/setup/theme.js +171 -66
- package/src/setup/wizard.js +432 -57
- package/src/skills/SkillLoader.js +145 -8
- package/src/storage/TaskStore.js +39 -15
- package/src/systemPrompt.js +45 -43
- package/src/tenants/TenantManager.js +79 -22
- package/src/tools/ToolRegistry.js +3 -3
- package/src/tools/applyPatch.js +2 -2
- package/src/tools/browserAutomation.js +4 -4
- package/src/tools/calendar.js +155 -0
- package/src/tools/clipboard.js +71 -0
- package/src/tools/contacts.js +138 -0
- package/src/tools/createDocument.js +2 -2
- package/src/tools/cronTool.js +14 -14
- package/src/tools/database.js +165 -0
- package/src/tools/editFile.js +10 -10
- package/src/tools/executeCommand.js +11 -3
- package/src/tools/generateImage.js +79 -0
- package/src/tools/gitTool.js +141 -0
- package/src/tools/glob.js +1 -1
- package/src/tools/googlePlaces.js +136 -0
- package/src/tools/grep.js +2 -2
- package/src/tools/iMessageTool.js +86 -0
- package/src/tools/imageAnalysis.js +3 -3
- package/src/tools/index.js +56 -2
- package/src/tools/makeVoiceCall.js +283 -0
- package/src/tools/manageAgents.js +2 -2
- package/src/tools/manageMCP.js +38 -20
- package/src/tools/memory.js +25 -32
- package/src/tools/messageChannel.js +1 -1
- package/src/tools/notification.js +90 -0
- package/src/tools/philipsHue.js +147 -0
- package/src/tools/projectTracker.js +8 -8
- package/src/tools/readFile.js +1 -1
- package/src/tools/readPDF.js +73 -0
- package/src/tools/screenCapture.js +6 -6
- package/src/tools/searchContent.js +2 -2
- package/src/tools/searchFiles.js +1 -1
- package/src/tools/sendEmail.js +79 -24
- package/src/tools/sendFile.js +4 -4
- package/src/tools/sonos.js +137 -0
- package/src/tools/sshTool.js +130 -0
- package/src/tools/textToSpeech.js +5 -5
- package/src/tools/transcribeAudio.js +4 -4
- package/src/tools/useMCP.js +4 -4
- package/src/tools/webFetch.js +2 -2
- package/src/tools/webSearch.js +1 -1
- package/src/utils/Embeddings.js +79 -0
- package/src/voice/VoiceSessionManager.js +170 -0
- 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
|
|
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
|
|
16
|
-
* allowedPaths
|
|
17
|
-
* blockedPaths
|
|
18
|
-
* maxCostPerTask
|
|
19
|
-
* maxDailyCost
|
|
20
|
-
* tools
|
|
21
|
-
* mcpServers
|
|
22
|
-
* modelRoutes
|
|
23
|
-
* encryptedApiKeys
|
|
24
|
-
* suspended
|
|
25
|
-
* plan
|
|
26
|
-
* createdAt
|
|
27
|
-
* lastSeenAt
|
|
28
|
-
* totalCost
|
|
29
|
-
* taskCount
|
|
30
|
-
* 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
|
|
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) : {},
|
|
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,
|
|
364
|
-
modelRoutes: null,
|
|
365
|
-
encryptedApiKeys: {},
|
|
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 }
|
|
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
|
|
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
|
|
53
|
+
* Build tool docs for system prompt - grouped by category.
|
|
54
54
|
*/
|
|
55
55
|
buildToolDocs() {
|
|
56
56
|
const categories = {};
|
package/src/tools/applyPatch.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* applyPatch(filePath, patch)
|
|
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
|
|
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
|
|
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
|
|
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()}
|
|
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}
|
|
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
|
|
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
|
|
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)`;
|