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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* googlePlaces - Search and get details from Google Places API.
|
|
3
|
+
* Requires GOOGLE_PLACES_API_KEY env var.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export async function googlePlaces(action, paramsJson) {
|
|
7
|
+
if (!action) return "Error: action required. Valid: search, details, nearby, autocomplete";
|
|
8
|
+
const params = paramsJson
|
|
9
|
+
? (typeof paramsJson === "string" ? JSON.parse(paramsJson) : paramsJson)
|
|
10
|
+
: {};
|
|
11
|
+
|
|
12
|
+
const apiKey = params.apiKey || process.env.GOOGLE_PLACES_API_KEY;
|
|
13
|
+
if (!apiKey) return "Error: GOOGLE_PLACES_API_KEY env var required";
|
|
14
|
+
|
|
15
|
+
const fetchFn = globalThis.fetch || (await import("node-fetch")).default;
|
|
16
|
+
const BASE = "https://maps.googleapis.com/maps/api";
|
|
17
|
+
|
|
18
|
+
if (action === "search") {
|
|
19
|
+
const { query, location, radius = 5000, type } = params;
|
|
20
|
+
if (!query) return "Error: query is required";
|
|
21
|
+
|
|
22
|
+
const qs = new URLSearchParams({
|
|
23
|
+
query,
|
|
24
|
+
key: apiKey,
|
|
25
|
+
...(location ? { location } : {}),
|
|
26
|
+
...(radius ? { radius: String(radius) } : {}),
|
|
27
|
+
...(type ? { type } : {}),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const res = await fetchFn(`${BASE}/place/textsearch/json?${qs}`);
|
|
31
|
+
const data = await res.json();
|
|
32
|
+
if (data.status !== "OK" && data.status !== "ZERO_RESULTS") {
|
|
33
|
+
return `Places API error: ${data.status} — ${data.error_message || ""}`;
|
|
34
|
+
}
|
|
35
|
+
if (!data.results?.length) return `No places found for "${query}"`;
|
|
36
|
+
|
|
37
|
+
return data.results.slice(0, params.limit || 5).map(p => [
|
|
38
|
+
`Name: ${p.name}`,
|
|
39
|
+
`Address: ${p.formatted_address}`,
|
|
40
|
+
`Rating: ${p.rating || "N/A"} (${p.user_ratings_total || 0} reviews)`,
|
|
41
|
+
`Place ID: ${p.place_id}`,
|
|
42
|
+
p.opening_hours?.open_now !== undefined ? `Open now: ${p.opening_hours.open_now}` : "",
|
|
43
|
+
].filter(Boolean).join("\n")).join("\n\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (action === "details") {
|
|
47
|
+
const { placeId, fields = "name,formatted_address,formatted_phone_number,website,rating,opening_hours,reviews" } = params;
|
|
48
|
+
if (!placeId) return "Error: placeId is required";
|
|
49
|
+
|
|
50
|
+
const qs = new URLSearchParams({ place_id: placeId, fields, key: apiKey });
|
|
51
|
+
const res = await fetchFn(`${BASE}/place/details/json?${qs}`);
|
|
52
|
+
const data = await res.json();
|
|
53
|
+
if (data.status !== "OK") return `Places details error: ${data.status}`;
|
|
54
|
+
|
|
55
|
+
const r = data.result;
|
|
56
|
+
const lines = [
|
|
57
|
+
`Name: ${r.name}`,
|
|
58
|
+
`Address: ${r.formatted_address}`,
|
|
59
|
+
r.formatted_phone_number ? `Phone: ${r.formatted_phone_number}` : null,
|
|
60
|
+
r.website ? `Website: ${r.website}` : null,
|
|
61
|
+
r.rating ? `Rating: ${r.rating}/5 (${r.user_ratings_total} reviews)` : null,
|
|
62
|
+
].filter(Boolean);
|
|
63
|
+
|
|
64
|
+
if (r.opening_hours?.weekday_text) {
|
|
65
|
+
lines.push("Hours:\n" + r.opening_hours.weekday_text.map(h => ` ${h}`).join("\n"));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (r.reviews?.length && params.includeReviews) {
|
|
69
|
+
lines.push("Top reviews:");
|
|
70
|
+
r.reviews.slice(0, 3).forEach(rev => {
|
|
71
|
+
lines.push(` ⭐${rev.rating} — ${rev.author_name}: "${rev.text?.slice(0, 100)}..."`);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (action === "nearby") {
|
|
79
|
+
const { location, radius = 1000, type } = params;
|
|
80
|
+
if (!location) return "Error: location is required (e.g. '37.7749,-122.4194')";
|
|
81
|
+
|
|
82
|
+
const qs = new URLSearchParams({
|
|
83
|
+
location,
|
|
84
|
+
radius: String(radius),
|
|
85
|
+
key: apiKey,
|
|
86
|
+
...(type ? { type } : {}),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const res = await fetchFn(`${BASE}/place/nearbysearch/json?${qs}`);
|
|
90
|
+
const data = await res.json();
|
|
91
|
+
if (data.status !== "OK" && data.status !== "ZERO_RESULTS") {
|
|
92
|
+
return `Nearby search error: ${data.status}`;
|
|
93
|
+
}
|
|
94
|
+
if (!data.results?.length) return "No places found nearby";
|
|
95
|
+
|
|
96
|
+
return data.results.slice(0, params.limit || 5).map(p =>
|
|
97
|
+
`${p.name} — ${p.vicinity} (Rating: ${p.rating || "N/A"})`
|
|
98
|
+
).join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (action === "autocomplete") {
|
|
102
|
+
const { input, location, radius } = params;
|
|
103
|
+
if (!input) return "Error: input is required";
|
|
104
|
+
|
|
105
|
+
const qs = new URLSearchParams({
|
|
106
|
+
input,
|
|
107
|
+
key: apiKey,
|
|
108
|
+
...(location ? { location } : {}),
|
|
109
|
+
...(radius ? { radius: String(radius) } : {}),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const res = await fetchFn(`${BASE}/place/autocomplete/json?${qs}`);
|
|
113
|
+
const data = await res.json();
|
|
114
|
+
if (data.status !== "OK" && data.status !== "ZERO_RESULTS") {
|
|
115
|
+
return `Autocomplete error: ${data.status}`;
|
|
116
|
+
}
|
|
117
|
+
if (!data.predictions?.length) return `No suggestions for "${input}"`;
|
|
118
|
+
|
|
119
|
+
return data.predictions.map(p => `${p.description} (${p.place_id})`).join("\n");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return `Unknown action: "${action}". Valid: search, details, nearby, autocomplete`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const googlePlacesDescription =
|
|
126
|
+
`googlePlaces(action: string, paramsJson?: object) - Search places, get details, find nearby locations.
|
|
127
|
+
action: "search" | "details" | "nearby" | "autocomplete"
|
|
128
|
+
search params: { query, location?: "lat,lng", radius?: 5000, type?, limit?: 5 }
|
|
129
|
+
details params: { placeId, fields?, includeReviews?: false }
|
|
130
|
+
nearby params: { location: "lat,lng", radius?: 1000, type?, limit?: 5 }
|
|
131
|
+
autocomplete params: { input, location?, radius? }
|
|
132
|
+
Env var: GOOGLE_PLACES_API_KEY
|
|
133
|
+
Examples:
|
|
134
|
+
googlePlaces("search", {"query":"coffee shops in San Francisco"})
|
|
135
|
+
googlePlaces("nearby", {"location":"37.7749,-122.4194","radius":500,"type":"restaurant"})
|
|
136
|
+
googlePlaces("details", {"placeId":"ChIJN1t_tDeuEmsRUsoyG83frY4"})`;
|
package/src/tools/grep.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* grep(pattern, optionsJson?)
|
|
3
|
-
* Inspired by Claude Code's Grep tool. Pure Node.js
|
|
2
|
+
* grep(pattern, optionsJson?) - Advanced content search with context lines.
|
|
3
|
+
* Inspired by Claude Code's Grep tool. Pure Node.js - no shell dependency.
|
|
4
4
|
*/
|
|
5
5
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
6
|
import { join, extname, relative } from "node:path";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iMessageTool - Send iMessages and SMS via macOS Messages app.
|
|
3
|
+
* Requires macOS with Messages app configured and accessibility permissions.
|
|
4
|
+
* Uses osascript (AppleScript) — no external deps.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
export async function iMessageTool(action, paramsJson) {
|
|
9
|
+
if (!action) return "Error: action required. Valid: send, read";
|
|
10
|
+
const params = paramsJson
|
|
11
|
+
? (typeof paramsJson === "string" ? JSON.parse(paramsJson) : paramsJson)
|
|
12
|
+
: {};
|
|
13
|
+
|
|
14
|
+
if (process.platform !== "darwin") {
|
|
15
|
+
return "Error: iMessage tool is macOS-only (requires Messages app via AppleScript)";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (action === "send") {
|
|
19
|
+
const { to, message, service = "iMessage" } = params;
|
|
20
|
+
if (!to) return "Error: 'to' (phone number or email) is required";
|
|
21
|
+
if (!message) return "Error: 'message' is required";
|
|
22
|
+
|
|
23
|
+
// Validate service type
|
|
24
|
+
const validServices = ["iMessage", "SMS"];
|
|
25
|
+
if (!validServices.includes(service)) {
|
|
26
|
+
return `Error: invalid service "${service}". Valid: iMessage, SMS`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const script = `
|
|
30
|
+
tell application "Messages"
|
|
31
|
+
set targetService to 1st service whose service type = ${service}
|
|
32
|
+
set targetBuddy to buddy "${to.replace(/"/g, '\\"')}" of targetService
|
|
33
|
+
send "${message.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}" to targetBuddy
|
|
34
|
+
end tell
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
execSync(`osascript -e '${script.replace(/'/g, "\\'")}'`, { timeout: 10000 });
|
|
39
|
+
return `${service} sent to ${to}: "${message.length > 60 ? message.slice(0, 60) + "..." : message}"`;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return `iMessage error: ${err.message}. Make sure Messages app is open and has the contact.`;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (action === "read") {
|
|
46
|
+
const { count = 10 } = params;
|
|
47
|
+
// AppleScript to read recent messages
|
|
48
|
+
const script = `
|
|
49
|
+
tell application "Messages"
|
|
50
|
+
set output to ""
|
|
51
|
+
set allChats to chats
|
|
52
|
+
repeat with aChat in allChats
|
|
53
|
+
set msgCount to count of messages of aChat
|
|
54
|
+
if msgCount > 0 then
|
|
55
|
+
set lastMsg to last message of aChat
|
|
56
|
+
set output to output & name of aChat & ": " & content of lastMsg & "\\n"
|
|
57
|
+
end if
|
|
58
|
+
end repeat
|
|
59
|
+
return output
|
|
60
|
+
end tell
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const result = execSync(`osascript -e '${script.replace(/'/g, "\\'")}'`, {
|
|
65
|
+
encoding: "utf-8",
|
|
66
|
+
timeout: 10000,
|
|
67
|
+
});
|
|
68
|
+
if (!result.trim()) return "No recent messages found";
|
|
69
|
+
return `Recent messages:\n${result.trim()}`;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return `Error reading messages: ${err.message}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return `Unknown action: "${action}". Valid: send, read`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const iMessageToolDescription =
|
|
79
|
+
`iMessageTool(action: string, paramsJson?: object) - Send and read iMessages/SMS on macOS.
|
|
80
|
+
action: "send" | "read"
|
|
81
|
+
send params: { to: "+1234567890"|"email@icloud.com", message: "text", service?: "iMessage"|"SMS" }
|
|
82
|
+
read params: { count?: 10 }
|
|
83
|
+
Requires: macOS with Messages app configured + Accessibility permissions for osascript
|
|
84
|
+
Examples:
|
|
85
|
+
iMessageTool("send", {"to":"+15551234567","message":"Hello!"})
|
|
86
|
+
iMessageTool("read", {"count":5})`;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* imageAnalysis(imagePath, prompt?)
|
|
2
|
+
* imageAnalysis(imagePath, prompt?) - Analyze images using vision AI models.
|
|
3
3
|
* Supports local files, URLs, and data: URIs.
|
|
4
4
|
* Uses the Vercel AI SDK with whatever vision-capable model is configured.
|
|
5
5
|
*/
|
|
@@ -38,13 +38,13 @@ export async function imageAnalysis(imagePath, prompt) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
if (imagePath.startsWith("data:")) {
|
|
41
|
-
// data: URI
|
|
41
|
+
// data: URI - extract base64 and mime type
|
|
42
42
|
const match = imagePath.match(/^data:([^;]+);base64,(.+)$/);
|
|
43
43
|
if (!match) return "Error: Invalid data: URI format";
|
|
44
44
|
mimeType = match[1];
|
|
45
45
|
imageData = match[2];
|
|
46
46
|
} else if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
47
|
-
// URL
|
|
47
|
+
// URL - fetch and convert to base64
|
|
48
48
|
const controller = new AbortController();
|
|
49
49
|
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
50
50
|
try {
|
package/src/tools/index.js
CHANGED
|
@@ -28,7 +28,7 @@ import { transcribeAudio, transcribeAudioDescription } from "./transcribeAudio.j
|
|
|
28
28
|
import { sendFile, sendFileDescription } from "./sendFile.js";
|
|
29
29
|
import { textToSpeech, textToSpeechDescription } from "./textToSpeech.js";
|
|
30
30
|
|
|
31
|
-
// ───
|
|
31
|
+
// ─── Search & code tools ───────────────────────────────────────────────────────
|
|
32
32
|
import { globSearch, globSearchDescription } from "./glob.js";
|
|
33
33
|
import { grep, grepDescription } from "./grep.js";
|
|
34
34
|
import { applyPatch, applyPatchDescription } from "./applyPatch.js";
|
|
@@ -40,6 +40,22 @@ import { messageChannel, messageChannelDescription } from "./messageChannel.js";
|
|
|
40
40
|
import { projectTracker, projectTrackerDescription } from "./projectTracker.js";
|
|
41
41
|
import { manageMCP, manageMCPDescription } from "./manageMCP.js";
|
|
42
42
|
import { useMCP, useMCPDescription } from "./useMCP.js";
|
|
43
|
+
import { makeVoiceCall, makeVoiceCallDescription } from "./makeVoiceCall.js";
|
|
44
|
+
|
|
45
|
+
// ─── Phase 24 tools ────────────────────────────────────────────────────────────
|
|
46
|
+
import { generateImage, generateImageDescription } from "./generateImage.js";
|
|
47
|
+
import { readPDF, readPDFDescription } from "./readPDF.js";
|
|
48
|
+
import { gitTool, gitToolDescription } from "./gitTool.js";
|
|
49
|
+
import { clipboard, clipboardDescription } from "./clipboard.js";
|
|
50
|
+
import { notification, notificationDescription } from "./notification.js";
|
|
51
|
+
import { iMessageTool, iMessageToolDescription } from "./iMessageTool.js";
|
|
52
|
+
import { calendar, calendarDescription } from "./calendar.js";
|
|
53
|
+
import { sshTool, sshToolDescription } from "./sshTool.js";
|
|
54
|
+
import { database, databaseDescription } from "./database.js";
|
|
55
|
+
import { contacts, contactsDescription } from "./contacts.js";
|
|
56
|
+
import { googlePlaces, googlePlacesDescription } from "./googlePlaces.js";
|
|
57
|
+
import { philipsHue, philipsHueDescription } from "./philipsHue.js";
|
|
58
|
+
import { sonos, sonosDescription } from "./sonos.js";
|
|
43
59
|
|
|
44
60
|
// ─── Wrap spawnAgent for the tool interface ────────────────────────────────────
|
|
45
61
|
function spawnAgent(taskDescription, optionsJson) {
|
|
@@ -74,7 +90,7 @@ export const toolFunctions = {
|
|
|
74
90
|
listDirectory,
|
|
75
91
|
searchFiles,
|
|
76
92
|
searchContent,
|
|
77
|
-
// Advanced search
|
|
93
|
+
// Advanced search
|
|
78
94
|
glob: globSearch,
|
|
79
95
|
grep,
|
|
80
96
|
applyPatch,
|
|
@@ -116,6 +132,25 @@ export const toolFunctions = {
|
|
|
116
132
|
// MCP management
|
|
117
133
|
manageMCP,
|
|
118
134
|
useMCP,
|
|
135
|
+
// Voice
|
|
136
|
+
makeVoiceCall,
|
|
137
|
+
// Phase 24: Image & document
|
|
138
|
+
generateImage,
|
|
139
|
+
readPDF,
|
|
140
|
+
// Phase 24: Developer tools
|
|
141
|
+
gitTool,
|
|
142
|
+
clipboard,
|
|
143
|
+
sshTool,
|
|
144
|
+
database,
|
|
145
|
+
// Phase 24: macOS / notifications
|
|
146
|
+
notification,
|
|
147
|
+
iMessageTool,
|
|
148
|
+
calendar,
|
|
149
|
+
contacts,
|
|
150
|
+
// Phase 24: External services
|
|
151
|
+
googlePlaces,
|
|
152
|
+
philipsHue,
|
|
153
|
+
sonos,
|
|
119
154
|
};
|
|
120
155
|
|
|
121
156
|
// ─── Tool Descriptions Array ───────────────────────────────────────────────────
|
|
@@ -170,4 +205,23 @@ export const toolDescriptions = [
|
|
|
170
205
|
// MCP management
|
|
171
206
|
manageMCPDescription,
|
|
172
207
|
useMCPDescription,
|
|
208
|
+
// Voice
|
|
209
|
+
makeVoiceCallDescription,
|
|
210
|
+
// Phase 24: Image & document
|
|
211
|
+
generateImageDescription,
|
|
212
|
+
readPDFDescription,
|
|
213
|
+
// Phase 24: Developer tools
|
|
214
|
+
gitToolDescription,
|
|
215
|
+
clipboardDescription,
|
|
216
|
+
sshToolDescription,
|
|
217
|
+
databaseDescription,
|
|
218
|
+
// Phase 24: macOS / notifications
|
|
219
|
+
notificationDescription,
|
|
220
|
+
iMessageToolDescription,
|
|
221
|
+
calendarDescription,
|
|
222
|
+
contactsDescription,
|
|
223
|
+
// Phase 24: External services
|
|
224
|
+
googlePlacesDescription,
|
|
225
|
+
philipsHueDescription,
|
|
226
|
+
sonosDescription,
|
|
173
227
|
];
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* makeVoiceCall - Interactive outbound voice calls via Twilio REST API.
|
|
3
|
+
*
|
|
4
|
+
* Supports two modes:
|
|
5
|
+
*
|
|
6
|
+
* ONE-SHOT: initiate a call that plays a message and hangs up
|
|
7
|
+
* makeVoiceCall("call", "+1555...", {"message":"Your order is ready"})
|
|
8
|
+
*
|
|
9
|
+
* INTERACTIVE: full two-way conversation — agent speaks, listens, responds
|
|
10
|
+
* makeVoiceCall("initiate", "+1555...", {"greeting":"Hi, how can I help?"})
|
|
11
|
+
* makeVoiceCall("listen", sessionId) ← blocks until caller speaks
|
|
12
|
+
* makeVoiceCall("speak", sessionId, {"message":"..."}) ← agent says something
|
|
13
|
+
* makeVoiceCall("end", sessionId) ← hang up
|
|
14
|
+
*
|
|
15
|
+
* Credential resolution order (first match wins):
|
|
16
|
+
* 1. Per-tenant channel config (daemora tenant channel set <id> twilio_account_sid ...)
|
|
17
|
+
* 2. Global .env (TWILIO_ACCOUNT_SID / TWILIO_AUTH_TOKEN / TWILIO_PHONE_FROM)
|
|
18
|
+
*
|
|
19
|
+
* Required env for interactive mode:
|
|
20
|
+
* VOICE_WEBHOOK_BASE_URL=https://your-public-url.com (Twilio must reach this URL)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import tenantContext from "../tenants/TenantContext.js";
|
|
24
|
+
import voiceSessionManager from "../voice/VoiceSessionManager.js";
|
|
25
|
+
|
|
26
|
+
const TWILIO_API = "https://api.twilio.com/2010-04-01";
|
|
27
|
+
|
|
28
|
+
/** Resolve Twilio credentials: tenant config first, then global env */
|
|
29
|
+
function _getCreds() {
|
|
30
|
+
const store = tenantContext.getStore();
|
|
31
|
+
const ch = store?.resolvedConfig?.channelConfig || {};
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
accountSid: ch.twilio_account_sid || process.env.TWILIO_ACCOUNT_SID || null,
|
|
35
|
+
authToken: ch.twilio_auth_token || process.env.TWILIO_AUTH_TOKEN || null,
|
|
36
|
+
fromNumber: ch.twilio_phone_from || process.env.TWILIO_PHONE_FROM || null,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Make an authenticated Twilio REST request */
|
|
41
|
+
async function _twilioRequest(accountSid, authToken, method, path, body = null) {
|
|
42
|
+
const url = `${TWILIO_API}/Accounts/${accountSid}${path}`;
|
|
43
|
+
const headers = {
|
|
44
|
+
Authorization: "Basic " + Buffer.from(`${accountSid}:${authToken}`).toString("base64"),
|
|
45
|
+
Accept: "application/json",
|
|
46
|
+
};
|
|
47
|
+
const opts = { method, headers };
|
|
48
|
+
|
|
49
|
+
if (body) {
|
|
50
|
+
opts.headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
51
|
+
opts.body = new URLSearchParams(body).toString();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const res = await fetch(url, opts);
|
|
55
|
+
const text = await res.text();
|
|
56
|
+
try {
|
|
57
|
+
return { ok: res.ok, status: res.status, data: JSON.parse(text) };
|
|
58
|
+
} catch {
|
|
59
|
+
return { ok: res.ok, status: res.status, data: { message: text } };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Build simple one-shot TwiML (speak a message, hang up) */
|
|
64
|
+
function _buildSayTwiML(message, voice = "Polly.Joanna", language = "en-US") {
|
|
65
|
+
const escaped = message
|
|
66
|
+
.replace(/&/g, "&")
|
|
67
|
+
.replace(/</g, "<")
|
|
68
|
+
.replace(/>/g, ">");
|
|
69
|
+
return `<?xml version="1.0" encoding="UTF-8"?><Response><Say voice="${voice}" language="${language}">${escaped}</Say></Response>`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Main tool function ────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export async function makeVoiceCall(action, phoneNumberOrSessionId, optionsJson) {
|
|
75
|
+
if (!action) return 'Error: action required. Use: initiate|listen|speak|end|status|list (or "call" for one-shot)';
|
|
76
|
+
|
|
77
|
+
const { accountSid, authToken, fromNumber } = _getCreds();
|
|
78
|
+
|
|
79
|
+
// ── Non-Twilio session actions (don't need credentials) ─────────────────────
|
|
80
|
+
// listen / speak / end operate on an existing session
|
|
81
|
+
|
|
82
|
+
if (action === "listen") {
|
|
83
|
+
const sessionId = phoneNumberOrSessionId;
|
|
84
|
+
if (!sessionId) return "Error: provide the session ID returned by initiate.";
|
|
85
|
+
const session = voiceSessionManager.get(sessionId);
|
|
86
|
+
if (!session) return `Error: No active session "${sessionId}". Did the call end?`;
|
|
87
|
+
if (session.status === "ended") return "Call has already ended.";
|
|
88
|
+
|
|
89
|
+
let opts = {};
|
|
90
|
+
if (optionsJson) { try { opts = JSON.parse(optionsJson); } catch {} }
|
|
91
|
+
const timeout = (opts.timeout || 120) * 1000;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const text = await session.waitForCallerInput(timeout);
|
|
95
|
+
return `Caller said: "${text}"`;
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return `${err.message}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (action === "speak") {
|
|
102
|
+
const sessionId = phoneNumberOrSessionId;
|
|
103
|
+
if (!sessionId) return "Error: provide the session ID returned by initiate.";
|
|
104
|
+
const session = voiceSessionManager.get(sessionId);
|
|
105
|
+
if (!session) return `Error: No active session "${sessionId}".`;
|
|
106
|
+
if (session.status === "ended") return "Call has already ended.";
|
|
107
|
+
|
|
108
|
+
let opts = {};
|
|
109
|
+
if (optionsJson) { try { opts = JSON.parse(optionsJson); } catch {} }
|
|
110
|
+
const message = opts.message;
|
|
111
|
+
if (!message) return 'Error: optionsJson {"message":"..."} is required for speak.';
|
|
112
|
+
|
|
113
|
+
session.setAgentResponse(message);
|
|
114
|
+
return `Speaking to caller: "${message}"`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (action === "end") {
|
|
118
|
+
const sessionId = phoneNumberOrSessionId;
|
|
119
|
+
if (!sessionId) return "Error: provide the session ID returned by initiate.";
|
|
120
|
+
const session = voiceSessionManager.get(sessionId);
|
|
121
|
+
if (!session) return `Error: No active session "${sessionId}".`;
|
|
122
|
+
|
|
123
|
+
session.setAgentResponse("__HANGUP__");
|
|
124
|
+
return `Ending call for session ${sessionId}.`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (action === "status") {
|
|
128
|
+
const sessionId = phoneNumberOrSessionId;
|
|
129
|
+
const session = voiceSessionManager.get(sessionId);
|
|
130
|
+
if (!session) return `No active session "${sessionId}".`;
|
|
131
|
+
|
|
132
|
+
const turns = session.transcript.length;
|
|
133
|
+
const lastEntry = session.transcript[turns - 1];
|
|
134
|
+
const lastTurn = lastEntry ? ` | Last: [${lastEntry.role}] "${lastEntry.text.slice(0, 60)}"` : "";
|
|
135
|
+
return `Session ${session.id} | Status: ${session.status} | Turns: ${turns}${lastTurn}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Twilio API actions — credentials required ────────────────────────────────
|
|
139
|
+
|
|
140
|
+
if (!accountSid || !authToken) {
|
|
141
|
+
return "Error: Twilio not configured. Set TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN in .env, or: daemora tenant channel set <id> twilio_account_sid <sid>";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let opts = {};
|
|
145
|
+
if (optionsJson) { try { opts = JSON.parse(optionsJson); } catch { return "Error: optionsJson must be valid JSON."; } }
|
|
146
|
+
|
|
147
|
+
// ── initiate — start an interactive call ────────────────────────────────────
|
|
148
|
+
if (action === "initiate") {
|
|
149
|
+
const to = phoneNumberOrSessionId;
|
|
150
|
+
if (!to) return "Error: phoneNumber is required for initiate.";
|
|
151
|
+
const from = opts.from || fromNumber;
|
|
152
|
+
if (!from) return 'Error: No outbound number. Set TWILIO_PHONE_FROM in .env or pass optionsJson {"from":"+1555..."}';
|
|
153
|
+
|
|
154
|
+
const webhookBase = process.env.VOICE_WEBHOOK_BASE_URL?.replace(/\/$/, "");
|
|
155
|
+
if (!webhookBase) {
|
|
156
|
+
return "Error: VOICE_WEBHOOK_BASE_URL not set. Set it to your public URL (e.g. https://myagent.example.com) so Twilio can reach the webhooks.";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Create session before the call so the webhook URL is ready
|
|
160
|
+
const session = voiceSessionManager.create({
|
|
161
|
+
callSid: null, // updated after Twilio responds
|
|
162
|
+
greeting: opts.greeting || null,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Store voice model preference on session for TaskRunner to pick up
|
|
166
|
+
// Voice uses the fastest model by default (VOICE_MODEL env var or haiku/flash)
|
|
167
|
+
session.preferredModel = opts.model || process.env.VOICE_MODEL || null;
|
|
168
|
+
|
|
169
|
+
const body = {
|
|
170
|
+
To: to,
|
|
171
|
+
From: from,
|
|
172
|
+
Url: `${webhookBase}/voice/answer/${session.id}`,
|
|
173
|
+
StatusCallback: `${webhookBase}/voice/status/${session.id}`,
|
|
174
|
+
StatusCallbackMethod: "POST",
|
|
175
|
+
Method: "POST",
|
|
176
|
+
};
|
|
177
|
+
if (opts.timeout) body.Timeout = String(opts.timeout);
|
|
178
|
+
if (opts.record === true) body.Record = "true";
|
|
179
|
+
if (opts.machineDetection) body.MachineDetection = opts.machineDetection;
|
|
180
|
+
|
|
181
|
+
console.log(`[makeVoiceCall] Initiating interactive call to ${to} from ${from} | session ${session.id}`);
|
|
182
|
+
const result = await _twilioRequest(accountSid, authToken, "POST", "/Calls.json", body);
|
|
183
|
+
|
|
184
|
+
if (!result.ok) {
|
|
185
|
+
voiceSessionManager.delete(session.id);
|
|
186
|
+
return `Failed to initiate call: ${result.data?.message || result.data?.code || result.status}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Update session with real callSid from Twilio
|
|
190
|
+
session.callSid = result.data.sid;
|
|
191
|
+
|
|
192
|
+
const d = result.data;
|
|
193
|
+
return (
|
|
194
|
+
`Interactive call started.\n` +
|
|
195
|
+
`Session ID: ${session.id} ← use this for listen/speak/end\n` +
|
|
196
|
+
`Call SID: ${d.sid}\n` +
|
|
197
|
+
`Status: ${d.status} (will update to "in-progress" when answered)\n` +
|
|
198
|
+
`To: ${d.to} | From: ${d.from}\n\n` +
|
|
199
|
+
`Next step: call makeVoiceCall("listen", "${session.id}") to wait for the caller to speak.`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── call — one-shot call that speaks a message and hangs up ─────────────────
|
|
204
|
+
if (action === "call") {
|
|
205
|
+
const to = phoneNumberOrSessionId;
|
|
206
|
+
if (!to) return "Error: phoneNumber is required for call.";
|
|
207
|
+
const from = opts.from || fromNumber;
|
|
208
|
+
if (!from) return 'Error: No outbound number. Set TWILIO_PHONE_FROM in .env or pass optionsJson {"from":"+1555..."}';
|
|
209
|
+
|
|
210
|
+
const body = { To: to, From: from };
|
|
211
|
+
|
|
212
|
+
if (opts.url) {
|
|
213
|
+
body.Url = opts.url;
|
|
214
|
+
} else if (opts.message) {
|
|
215
|
+
body.Twiml = _buildSayTwiML(opts.message, opts.voice || "Polly.Joanna", opts.language || "en-US");
|
|
216
|
+
} else {
|
|
217
|
+
return 'Error: Provide optionsJson {"message":"Hello"} to speak a message, or {"url":"https://twiml-url"} for custom TwiML.';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (opts.statusCallback) body.StatusCallback = opts.statusCallback;
|
|
221
|
+
if (opts.timeout) body.Timeout = String(opts.timeout);
|
|
222
|
+
if (opts.record === true) body.Record = "true";
|
|
223
|
+
|
|
224
|
+
console.log(`[makeVoiceCall] One-shot call to ${to} from ${from}`);
|
|
225
|
+
const result = await _twilioRequest(accountSid, authToken, "POST", "/Calls.json", body);
|
|
226
|
+
|
|
227
|
+
if (!result.ok) {
|
|
228
|
+
return `Failed to initiate call: ${result.data?.message || result.data?.code || result.status}`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const d = result.data;
|
|
232
|
+
return `Call initiated. SID: ${d.sid} | Status: ${d.status} | To: ${d.to} | From: ${d.from}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── hangup — end a call by SID (non-session, admin use) ─────────────────────
|
|
236
|
+
if (action === "hangup") {
|
|
237
|
+
const sid = opts.sid || phoneNumberOrSessionId;
|
|
238
|
+
if (!sid) return 'Error: Provide call SID via optionsJson {"sid":"CA..."} or as the second argument.';
|
|
239
|
+
|
|
240
|
+
const result = await _twilioRequest(accountSid, authToken, "POST", `/Calls/${sid}.json`, { Status: "completed" });
|
|
241
|
+
if (!result.ok) return `Failed to hang up: ${result.data?.message || result.status}`;
|
|
242
|
+
return `Call ${result.data.sid} ended. Final status: ${result.data.status}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── list — recent calls ──────────────────────────────────────────────────────
|
|
246
|
+
if (action === "list") {
|
|
247
|
+
const limit = opts.limit || 20;
|
|
248
|
+
const statusFilter = opts.status ? `&Status=${opts.status}` : "";
|
|
249
|
+
const toFilter = opts.to ? `&To=${encodeURIComponent(opts.to)}` : "";
|
|
250
|
+
|
|
251
|
+
const result = await _twilioRequest(
|
|
252
|
+
accountSid, authToken, "GET",
|
|
253
|
+
`/Calls.json?PageSize=${limit}${statusFilter}${toFilter}`
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
if (!result.ok) return `Failed to list calls: ${result.data?.message || result.status}`;
|
|
257
|
+
|
|
258
|
+
const calls = result.data?.calls || [];
|
|
259
|
+
if (calls.length === 0) return "No calls found.";
|
|
260
|
+
|
|
261
|
+
const lines = calls.map((c) => {
|
|
262
|
+
const dur = c.duration ? ` (${c.duration}s)` : "";
|
|
263
|
+
return ` ${c.sid} | ${c.status.padEnd(12)} | ${c.to} ← ${c.from}${dur} | ${c.start_time || c.date_created}`;
|
|
264
|
+
});
|
|
265
|
+
return `Recent calls (${calls.length}):\n${lines.join("\n")}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return `Error: Unknown action "${action}". Supported: initiate, listen, speak, end, status, call, hangup, list.`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export const makeVoiceCallDescription =
|
|
272
|
+
'makeVoiceCall(action: string, phoneNumberOrSessionId?: string, optionsJson?: string) - Outbound voice calls via Twilio.\n' +
|
|
273
|
+
'INTERACTIVE MODE (two-way conversation):\n' +
|
|
274
|
+
' action=initiate: start a call. optionsJson: {"greeting":"Hi, how can I help?","from":"+1555..."}. Returns sessionId.\n' +
|
|
275
|
+
' action=listen: wait for caller to speak. First arg = sessionId. Returns: "Caller said: \\"...\\""\n' +
|
|
276
|
+
' action=speak: say something to caller. First arg = sessionId. optionsJson: {"message":"Got it, one moment..."}\n' +
|
|
277
|
+
' action=end: hang up the call. First arg = sessionId.\n' +
|
|
278
|
+
' action=status: show session state and transcript. First arg = sessionId.\n' +
|
|
279
|
+
'ONE-SHOT MODE:\n' +
|
|
280
|
+
' action=call: dial a number and speak a message. optionsJson: {"message":"Your order is ready"} or {"url":"https://twiml-url"}\n' +
|
|
281
|
+
' action=hangup: end a call by SID. optionsJson: {"sid":"CA..."}\n' +
|
|
282
|
+
' action=list: recent calls. optionsJson: {"limit":20,"status":"completed","to":"+1555..."}\n' +
|
|
283
|
+
'Credentials: TWILIO_ACCOUNT_SID + TWILIO_AUTH_TOKEN + TWILIO_PHONE_FROM in .env. Interactive mode also needs VOICE_WEBHOOK_BASE_URL.';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* manageAgents(action, paramsJson?)
|
|
2
|
+
* manageAgents(action, paramsJson?) - List, kill, or steer running sub-agents.
|
|
3
3
|
* Inspired by OpenClaw's subagents tool.
|
|
4
4
|
*/
|
|
5
5
|
import {
|
|
@@ -17,7 +17,7 @@ export function manageAgents(action, paramsJson) {
|
|
|
17
17
|
const agents = listActiveAgents();
|
|
18
18
|
if (agents.length === 0) return "No active sub-agents running.";
|
|
19
19
|
const lines = agents.map(
|
|
20
|
-
(a) => `• ${a.id}
|
|
20
|
+
(a) => `• ${a.id} - "${a.task}" (running ${Math.round(a.elapsedMs / 1000)}s)`
|
|
21
21
|
);
|
|
22
22
|
return `Active sub-agents (${agents.length}):\n${lines.join("\n")}`;
|
|
23
23
|
}
|