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
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Agent Profiles
|
|
2
|
+
* Agent Profiles - tool presets for common sub-agent roles.
|
|
3
3
|
*
|
|
4
4
|
* Rather than giving every sub-agent all 33 tools, profiles provide
|
|
5
5
|
* focused tool sets matched to the task type. Inspired by the research
|
|
6
6
|
* finding that specialized context windows outperform bloated ones.
|
|
7
7
|
*
|
|
8
8
|
* Usage in spawnAgent / parallelAgents:
|
|
9
|
-
* { profile: "coder" }
|
|
10
|
-
* { profile: "researcher", extraTools: ["writeFile"] }
|
|
11
|
-
* { tools: ["readFile", "webSearch"] }
|
|
9
|
+
* { profile: "coder" } - preset tool list
|
|
10
|
+
* { profile: "researcher", extraTools: ["writeFile"] } - preset + additions
|
|
11
|
+
* { tools: ["readFile", "webSearch"] } - explicit list (overrides profile)
|
|
12
12
|
*
|
|
13
|
-
* spawnAgent and parallelAgents are injected dynamically based on depth
|
|
13
|
+
* spawnAgent and parallelAgents are injected dynamically based on depth - not in profiles.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
export const agentProfiles = {
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* researcher
|
|
19
|
+
* researcher - gather, analyze, summarize, produce findings.
|
|
20
20
|
* Reads files, searches web, fetches URLs, analyzes images. Saves findings to files.
|
|
21
|
-
* Does NOT ask the user what to look for
|
|
21
|
+
* Does NOT ask the user what to look for - searches until it has enough to answer fully.
|
|
22
22
|
* Produces structured output: facts, sources, analysis, recommendations.
|
|
23
23
|
*/
|
|
24
24
|
researcher: [
|
|
@@ -33,7 +33,7 @@ export const agentProfiles = {
|
|
|
33
33
|
],
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* coder
|
|
36
|
+
* coder - build, fix, test, verify.
|
|
37
37
|
* Full ownership: writes code, runs builds, starts dev servers, tests UI visually,
|
|
38
38
|
* writes test cases, runs them, fixes failures. Does everything without asking the user.
|
|
39
39
|
*/
|
|
@@ -51,9 +51,9 @@ export const agentProfiles = {
|
|
|
51
51
|
],
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
|
-
* writer
|
|
54
|
+
* writer - produce polished documents, reports, content.
|
|
55
55
|
* Reads existing content for context, researches via web, produces clean output.
|
|
56
|
-
* Does NOT ask what tone/format to use unless genuinely ambiguous
|
|
56
|
+
* Does NOT ask what tone/format to use unless genuinely ambiguous - infers from context.
|
|
57
57
|
* Delivers the final document, not a draft asking for feedback.
|
|
58
58
|
*/
|
|
59
59
|
writer: [
|
|
@@ -65,7 +65,7 @@ export const agentProfiles = {
|
|
|
65
65
|
],
|
|
66
66
|
|
|
67
67
|
/**
|
|
68
|
-
* analyst
|
|
68
|
+
* analyst - process data, run scripts, extract insights, produce output.
|
|
69
69
|
* Shell execution for data processing + web + vision for charts/visuals.
|
|
70
70
|
* Runs scripts, parses output, draws conclusions. Delivers findings, not raw data.
|
|
71
71
|
*/
|
|
@@ -86,14 +86,14 @@ export const agentProfiles = {
|
|
|
86
86
|
*
|
|
87
87
|
* Covers the majority of tasks while excluding high-blast-radius tools
|
|
88
88
|
* that sub-agents rarely need and that carry side effects beyond task scope:
|
|
89
|
-
* - cron
|
|
90
|
-
* - sendEmail
|
|
91
|
-
* - messageChannel
|
|
92
|
-
* - screenCapture
|
|
93
|
-
* - manageAgents
|
|
94
|
-
* - delegateToAgent
|
|
89
|
+
* - cron - schedules recurring tasks that outlive the sub-agent
|
|
90
|
+
* - sendEmail - sub-agents shouldn't initiate email
|
|
91
|
+
* - messageChannel - sub-agents shouldn't send messages
|
|
92
|
+
* - screenCapture - sub-agents don't need screen access
|
|
93
|
+
* - manageAgents - sub-agents shouldn't kill/steer other agents
|
|
94
|
+
* - delegateToAgent - A2A from sub-agents is unpredictable
|
|
95
95
|
*
|
|
96
|
-
* spawnAgent and parallelAgents are NOT listed here
|
|
96
|
+
* spawnAgent and parallelAgents are NOT listed here - they are injected
|
|
97
97
|
* dynamically into sub-agents by SubAgentManager based on recursion depth.
|
|
98
98
|
*/
|
|
99
99
|
export const defaultSubAgentTools = [
|
|
@@ -114,7 +114,7 @@ export const defaultSubAgentTools = [
|
|
|
114
114
|
"searchMemory", "pruneMemory", "listMemoryCategories",
|
|
115
115
|
// Project tracking
|
|
116
116
|
"projectTracker",
|
|
117
|
-
// MCP (via specialist agent
|
|
117
|
+
// MCP (via specialist agent - no direct mcp__ tools)
|
|
118
118
|
"manageMCP",
|
|
119
119
|
"useMCP",
|
|
120
120
|
];
|
package/src/config/channels.js
CHANGED
package/src/config/default.js
CHANGED
|
@@ -87,7 +87,7 @@ export const config = {
|
|
|
87
87
|
isolateFilesystem: process.env.TENANT_ISOLATE_FILESYSTEM === "true",
|
|
88
88
|
},
|
|
89
89
|
|
|
90
|
-
// Sandbox
|
|
90
|
+
// Sandbox - OS-level command isolation
|
|
91
91
|
// "process" (default): commands run in the current process, tool-level path guards apply.
|
|
92
92
|
// "docker": commands run inside a Docker container, providing kernel-level isolation.
|
|
93
93
|
// Requires Docker installed. Container gets no network by default (DOCKER_NETWORK=none).
|
|
@@ -101,12 +101,12 @@ export const config = {
|
|
|
101
101
|
|
|
102
102
|
// Channels
|
|
103
103
|
// Each channel supports two universal options:
|
|
104
|
-
// allowlist
|
|
104
|
+
// allowlist - comma-separated IDs/numbers/usernames in env var (e.g. TELEGRAM_ALLOWLIST="123456789,987654321")
|
|
105
105
|
// If empty or not set → open to everyone. Set this to lock down your bot.
|
|
106
|
-
// model
|
|
106
|
+
// model - per-channel model override (e.g. TELEGRAM_MODEL="anthropic:claude-opus-4-6")
|
|
107
107
|
// If not set → global DEFAULT_MODEL is used.
|
|
108
108
|
channels: {
|
|
109
|
-
// HTTP channel is disabled
|
|
109
|
+
// HTTP channel is disabled - unauthenticated, see src/channels/index.js
|
|
110
110
|
http: { enabled: false },
|
|
111
111
|
|
|
112
112
|
telegram: {
|
|
@@ -130,7 +130,12 @@ export const config = {
|
|
|
130
130
|
},
|
|
131
131
|
|
|
132
132
|
email: {
|
|
133
|
-
|
|
133
|
+
// Enabled if EITHER Resend (outbound) OR Gmail/SMTP (full) is configured
|
|
134
|
+
enabled: !!(process.env.RESEND_API_KEY || process.env.EMAIL_USER),
|
|
135
|
+
// Resend (recommended - just an API key for outbound sending)
|
|
136
|
+
resendApiKey: process.env.RESEND_API_KEY || null,
|
|
137
|
+
resendFrom: process.env.RESEND_FROM || null,
|
|
138
|
+
// Traditional IMAP/SMTP (needed for inbox polling / inbound)
|
|
134
139
|
imap: {
|
|
135
140
|
host: process.env.EMAIL_IMAP_HOST || "imap.gmail.com",
|
|
136
141
|
port: parseInt(process.env.EMAIL_IMAP_PORT || "993", 10),
|
|
@@ -139,8 +144,8 @@ export const config = {
|
|
|
139
144
|
host: process.env.EMAIL_SMTP_HOST || "smtp.gmail.com",
|
|
140
145
|
port: parseInt(process.env.EMAIL_SMTP_PORT || "587", 10),
|
|
141
146
|
},
|
|
142
|
-
user: process.env.EMAIL_USER,
|
|
143
|
-
password: process.env.EMAIL_PASSWORD,
|
|
147
|
+
user: process.env.EMAIL_USER || null,
|
|
148
|
+
password: process.env.EMAIL_PASSWORD || null,
|
|
144
149
|
allowlist: process.env.EMAIL_ALLOWLIST
|
|
145
150
|
? process.env.EMAIL_ALLOWLIST.split(",").map((s) => s.trim()).filter(Boolean)
|
|
146
151
|
: [],
|
package/src/config/models.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Model registry
|
|
2
|
+
* Model registry - metadata for all supported models.
|
|
3
3
|
* Used by ModelRouter for selection, cost tracking, and compaction thresholds.
|
|
4
4
|
*/
|
|
5
5
|
export const models = {
|
|
@@ -79,7 +79,7 @@ export const models = {
|
|
|
79
79
|
tier: "cheap",
|
|
80
80
|
},
|
|
81
81
|
|
|
82
|
-
// Ollama (local
|
|
82
|
+
// Ollama (local - no cost)
|
|
83
83
|
"ollama:llama3": {
|
|
84
84
|
provider: "ollama",
|
|
85
85
|
model: "llama3",
|
|
@@ -113,7 +113,7 @@ export const models = {
|
|
|
113
113
|
};
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
|
-
* Fallback chains
|
|
116
|
+
* Fallback chains - if preferred model fails, try next in chain.
|
|
117
117
|
*/
|
|
118
118
|
export const fallbackChains = {
|
|
119
119
|
cheap: ["openai:gpt-4.1-mini", "anthropic:claude-haiku-4-5", "google:gemini-2.0-flash"],
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
export const permissionTiers = {
|
|
7
7
|
minimal: {
|
|
8
8
|
name: "Minimal (Read-Only)",
|
|
9
|
-
description: "Agent can read files, search, and browse the web
|
|
9
|
+
description: "Agent can read files, search, and browse the web - no writes, no shell, no communication.",
|
|
10
10
|
allowedTools: [
|
|
11
11
|
// File reads
|
|
12
12
|
"readFile",
|
|
@@ -60,7 +60,7 @@ export const permissionTiers = {
|
|
|
60
60
|
"screenCapture",
|
|
61
61
|
// Project tracking
|
|
62
62
|
"projectTracker",
|
|
63
|
-
// MCP inspection (read-only
|
|
63
|
+
// MCP inspection (read-only - no side effects)
|
|
64
64
|
"manageMCP",
|
|
65
65
|
],
|
|
66
66
|
},
|
package/src/core/AgentLoop.js
CHANGED
|
@@ -13,7 +13,7 @@ import supervisor from "../agents/Supervisor.js";
|
|
|
13
13
|
import gitRollback from "../safety/GitRollback.js";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Core agent loop
|
|
16
|
+
* Core agent loop - model-agnostic via Vercel AI SDK.
|
|
17
17
|
*
|
|
18
18
|
* Extracted from the original openai.js. This is the brain of the agent:
|
|
19
19
|
* 1. Send messages to model (any provider)
|
|
@@ -38,14 +38,14 @@ export async function runAgentLoop({
|
|
|
38
38
|
taskId = null,
|
|
39
39
|
approvalMode = "auto", // "auto" | "dangerous-only" | "every-tool"
|
|
40
40
|
channelMeta = null, // passed through to HumanApproval so channel can notify user
|
|
41
|
-
signal = null, // AbortController.signal
|
|
42
|
-
steerQueue = null, // shared mutable array
|
|
43
|
-
apiKeys = {}, // per-tenant API key overlay
|
|
41
|
+
signal = null, // AbortController.signal - hard-kills the loop mid-call
|
|
42
|
+
steerQueue = null, // shared mutable array - push strings here to steer the agent
|
|
43
|
+
apiKeys = {}, // per-tenant API key overlay - passed through to provider factory
|
|
44
44
|
}) {
|
|
45
45
|
const selectedModelId = modelId || config.defaultModel;
|
|
46
46
|
const { model, meta, modelId: resolvedModelId } = getModelWithFallback(selectedModelId, apiKeys);
|
|
47
47
|
|
|
48
|
-
// Build set of known secret values to redact from tool outputs (dynamic
|
|
48
|
+
// Build set of known secret values to redact from tool outputs (dynamic - catches tenant keys)
|
|
49
49
|
const _knownSecrets = new Set([
|
|
50
50
|
...Object.values(apiKeys),
|
|
51
51
|
process.env.OPENAI_API_KEY,
|
|
@@ -111,7 +111,7 @@ export async function runAgentLoop({
|
|
|
111
111
|
while (steerQueue.length > 0) {
|
|
112
112
|
const item = steerQueue.shift();
|
|
113
113
|
if (item && typeof item === "object" && item.type === "user") {
|
|
114
|
-
// User sent a follow-up mid-task
|
|
114
|
+
// User sent a follow-up mid-task - inject as a natural user turn
|
|
115
115
|
console.log(`[AgentLoop] User follow-up injected: "${item.content.slice(0, 80)}"`);
|
|
116
116
|
messages.push({ role: "user", content: item.content });
|
|
117
117
|
} else {
|
|
@@ -259,7 +259,7 @@ export async function runAgentLoop({
|
|
|
259
259
|
continue;
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
// Git snapshot
|
|
262
|
+
// Git snapshot - before the first write tool in this task
|
|
263
263
|
if (!gitSnapshotDone && WRITE_TOOLS.has(tool_name)) {
|
|
264
264
|
gitRollback.snapshot(taskId);
|
|
265
265
|
gitSnapshotDone = true;
|
|
@@ -331,7 +331,7 @@ export async function runAgentLoop({
|
|
|
331
331
|
});
|
|
332
332
|
}
|
|
333
333
|
} else {
|
|
334
|
-
console.log(`[Step ${stepCount}] Unknown tool: ${tool_name}
|
|
334
|
+
console.log(`[Step ${stepCount}] Unknown tool: ${tool_name} - skipping`);
|
|
335
335
|
messages.push({
|
|
336
336
|
role: "user",
|
|
337
337
|
content: JSON.stringify({
|
|
@@ -347,7 +347,7 @@ export async function runAgentLoop({
|
|
|
347
347
|
// --- Final response handling ---
|
|
348
348
|
if (parsedOutput.finalResponse || parsedOutput.type === "text") {
|
|
349
349
|
if (!parsedOutput.text_content) {
|
|
350
|
-
console.log(`[Loop ${loopCount}] Model signaled done but text_content is null
|
|
350
|
+
console.log(`[Loop ${loopCount}] Model signaled done but text_content is null - asking for summary`);
|
|
351
351
|
messages.push({
|
|
352
352
|
role: "user",
|
|
353
353
|
content:
|
|
@@ -363,7 +363,7 @@ export async function runAgentLoop({
|
|
|
363
363
|
const lastUserMsg = msgs[msgs.length - 1]?.content?.toLowerCase() || "";
|
|
364
364
|
const isAck = /^(ok|okay|yes|yeah|sure|thanks|thank you|no|nah|k|yep|yup|got it|cool|nice|great|good|alright|👍)\.?$/i.test(lastUserMsg.trim());
|
|
365
365
|
if (!isAck && lastUserMsg.length > 5) {
|
|
366
|
-
console.log(`[Loop ${loopCount}] LAZY MODEL DETECTED
|
|
366
|
+
console.log(`[Loop ${loopCount}] LAZY MODEL DETECTED - claimed done but used 0 tools. Forcing tool use.`);
|
|
367
367
|
messages.push({
|
|
368
368
|
role: "user",
|
|
369
369
|
content: `You responded without using any tools. You MUST actually use tools (readFile, editFile, writeFile, executeCommand, etc.) to complete the task. Do NOT claim you fixed or changed something without actually doing it. Use your tools NOW to fulfill the request.`,
|
|
@@ -375,10 +375,10 @@ export async function runAgentLoop({
|
|
|
375
375
|
// SAFEGUARD 2: Model used only READ tools but claims it modified/fixed something
|
|
376
376
|
// If the response contains action words but no write tool was ever called, push back.
|
|
377
377
|
if (!writeToolUsed && stepCount > 0 && loopCount <= 4 && ACTION_WORDS.test(parsedOutput.text_content)) {
|
|
378
|
-
console.log(`[Loop ${loopCount}] PHANTOM WRITE DETECTED
|
|
378
|
+
console.log(`[Loop ${loopCount}] PHANTOM WRITE DETECTED - model claims "${parsedOutput.text_content.slice(0, 80)}..." but only used read tools. Forcing actual writes.`);
|
|
379
379
|
messages.push({
|
|
380
380
|
role: "user",
|
|
381
|
-
content: `You claim to have made changes but you only used read tools
|
|
381
|
+
content: `You claim to have made changes but you only used read tools - you never called writeFile or editFile to actually modify any file. The files are UNCHANGED. You must use writeFile or editFile to actually make the changes. Do it now.`,
|
|
382
382
|
});
|
|
383
383
|
continue;
|
|
384
384
|
}
|
|
@@ -406,7 +406,7 @@ export async function runAgentLoop({
|
|
|
406
406
|
return { text: parsedOutput.text_content, messages: conversationMessages, cost };
|
|
407
407
|
}
|
|
408
408
|
} catch (error) {
|
|
409
|
-
// Abort signal fires as an error
|
|
409
|
+
// Abort signal fires as an error - exit cleanly
|
|
410
410
|
if (signal?.aborted || error.name === "AbortError") {
|
|
411
411
|
console.log(`[Loop ${loopCount}] Aborted mid-call.`);
|
|
412
412
|
return {
|
package/src/core/Compaction.js
CHANGED
|
@@ -28,7 +28,7 @@ export function estimateTokens(messages) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
|
-
* Prune a single tool output
|
|
31
|
+
* Prune a single tool output - truncate if too long.
|
|
32
32
|
*/
|
|
33
33
|
function pruneToolOutput(content, maxChars = 5000) {
|
|
34
34
|
if (typeof content !== "string") content = JSON.stringify(content);
|
|
@@ -52,7 +52,7 @@ function persistLargeOutput(content, taskId, stepIndex) {
|
|
|
52
52
|
const filename = `${taskId || "unknown"}-step${stepIndex}-${Date.now()}.txt`;
|
|
53
53
|
const filePath = `${dir}/${filename}`;
|
|
54
54
|
writeFileSync(filePath, content);
|
|
55
|
-
return `[Output saved to disk: ${filePath}
|
|
55
|
+
return `[Output saved to disk: ${filePath} - ${content.length} chars]`;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
@@ -82,7 +82,7 @@ export async function compactIfNeeded(messages, modelMeta, taskId = null) {
|
|
|
82
82
|
const oldMessages = messages.slice(1, -recentCount);
|
|
83
83
|
|
|
84
84
|
if (oldMessages.length === 0) {
|
|
85
|
-
// Nothing to compact
|
|
85
|
+
// Nothing to compact - all messages are recent
|
|
86
86
|
return messages;
|
|
87
87
|
}
|
|
88
88
|
|
package/src/core/CostTracker.js
CHANGED
|
@@ -33,7 +33,7 @@ export function logCost({ taskId, modelId, inputTokens, outputTokens, estimatedC
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
* Get total cost spent today (global
|
|
36
|
+
* Get total cost spent today (global - all tenants combined).
|
|
37
37
|
*/
|
|
38
38
|
export function getTodayCost() {
|
|
39
39
|
const logPath = getTodayLogPath();
|
|
@@ -101,7 +101,7 @@ export function estimateCost(modelId, inputTokens, outputTokens) {
|
|
|
101
101
|
return (inputTokens / 1000) * meta.costPer1kInput + (outputTokens / 1000) * meta.costPer1kOutput;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
// Auto-log costs from EventBus
|
|
104
|
+
// Auto-log costs from EventBus - includes tenantId from TenantContext
|
|
105
105
|
eventBus.on("model:called", (data) => {
|
|
106
106
|
const tenantId = tenantContext.getStore()?.tenant?.id || null;
|
|
107
107
|
const cost = estimateCost(data.modelId, data.inputTokens || 0, data.outputTokens || 0);
|
package/src/core/EventBus.js
CHANGED
|
@@ -4,21 +4,21 @@ import { EventEmitter } from "events";
|
|
|
4
4
|
* Global event bus for inter-module communication.
|
|
5
5
|
*
|
|
6
6
|
* Events:
|
|
7
|
-
* task:created
|
|
8
|
-
* task:started
|
|
9
|
-
* task:completed
|
|
10
|
-
* task:failed
|
|
11
|
-
* tool:before
|
|
12
|
-
* tool:after
|
|
13
|
-
* tool:blocked
|
|
14
|
-
* agent:spawned
|
|
15
|
-
* agent:finished
|
|
16
|
-
* agent:killed
|
|
17
|
-
* model:called
|
|
18
|
-
* compact:triggered
|
|
19
|
-
* memory:written
|
|
20
|
-
* secret:detected
|
|
21
|
-
* audit:event
|
|
7
|
+
* task:created - new task enqueued
|
|
8
|
+
* task:started - task picked up by runner
|
|
9
|
+
* task:completed - task finished successfully
|
|
10
|
+
* task:failed - task failed
|
|
11
|
+
* tool:before - about to execute a tool (PreToolUse hook point)
|
|
12
|
+
* tool:after - tool execution finished (PostToolUse hook point)
|
|
13
|
+
* tool:blocked - tool call was blocked by safety
|
|
14
|
+
* agent:spawned - sub-agent created
|
|
15
|
+
* agent:finished - sub-agent completed
|
|
16
|
+
* agent:killed - sub-agent terminated by supervisor
|
|
17
|
+
* model:called - LLM API call made (for cost tracking)
|
|
18
|
+
* compact:triggered - context compaction started
|
|
19
|
+
* memory:written - memory entry added
|
|
20
|
+
* secret:detected - secret found and redacted
|
|
21
|
+
* audit:event - generic audit event
|
|
22
22
|
*/
|
|
23
23
|
class AgentEventBus extends EventEmitter {
|
|
24
24
|
constructor() {
|
package/src/core/TaskQueue.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createTask, startTask, completeTask, failTask } from "./Task.js";
|
|
2
|
-
import { saveTask, loadTask, recoverStaleTasks } from "../storage/TaskStore.js";
|
|
2
|
+
import { saveTask, loadTask, recoverStaleTasks, loadPendingTasks } from "../storage/TaskStore.js";
|
|
3
3
|
import eventBus from "./EventBus.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -21,11 +21,24 @@ class TaskQueue {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
* Initialize queue
|
|
24
|
+
* Initialize queue - recover stale tasks from a previous crash/restart and
|
|
25
|
+
* re-hydrate the in-memory queue so they execute automatically without human
|
|
26
|
+
* re-input. Tasks that were "running" when the process died are reset to
|
|
27
|
+
* "pending" on disk first, then all pending tasks are loaded back into queue.
|
|
25
28
|
*/
|
|
26
29
|
init() {
|
|
27
|
-
recoverStaleTasks();
|
|
28
|
-
|
|
30
|
+
const recovered = recoverStaleTasks();
|
|
31
|
+
|
|
32
|
+
const pending = loadPendingTasks();
|
|
33
|
+
for (const task of pending) {
|
|
34
|
+
this.queue.push(task);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (pending.length > 0) {
|
|
38
|
+
console.log(`[TaskQueue] Requeued ${pending.length} pending task(s) from previous session (${recovered} recovered from crash)`);
|
|
39
|
+
} else {
|
|
40
|
+
console.log(`[TaskQueue] Initialized (no pending tasks from previous session)`);
|
|
41
|
+
}
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
/**
|
|
@@ -91,11 +104,15 @@ class TaskQueue {
|
|
|
91
104
|
|
|
92
105
|
eventBus.emitEvent("task:completed", { taskId: task.id, cost: task.cost });
|
|
93
106
|
|
|
94
|
-
// Resolve any sync waiters
|
|
107
|
+
// Resolve any sync waiters (normal flow - channel is waiting for completion)
|
|
95
108
|
const waiter = this.waiters.get(taskId);
|
|
96
109
|
if (waiter) {
|
|
97
110
|
waiter.resolve(task);
|
|
98
111
|
this.waiters.delete(taskId);
|
|
112
|
+
} else if (task.channel && task.channel !== "http" && task.channel !== "a2a") {
|
|
113
|
+
// No waiter = recovered task (agent restarted while task was in-flight).
|
|
114
|
+
// Emit so ChannelRegistry can route the reply back to the user automatically.
|
|
115
|
+
eventBus.emitEvent("task:reply:needed", { task });
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
return task;
|
|
@@ -117,7 +134,7 @@ class TaskQueue {
|
|
|
117
134
|
// Reject any sync waiters
|
|
118
135
|
const waiter = this.waiters.get(taskId);
|
|
119
136
|
if (waiter) {
|
|
120
|
-
waiter.resolve(task); // resolve not reject
|
|
137
|
+
waiter.resolve(task); // resolve not reject - caller handles error state
|
|
121
138
|
this.waiters.delete(taskId);
|
|
122
139
|
}
|
|
123
140
|
|
|
@@ -137,7 +154,7 @@ class TaskQueue {
|
|
|
137
154
|
return;
|
|
138
155
|
}
|
|
139
156
|
|
|
140
|
-
// Timeout guard
|
|
157
|
+
// Timeout guard - prevent hanging forever (default 5 min)
|
|
141
158
|
const timer = setTimeout(() => {
|
|
142
159
|
this.waiters.delete(taskId);
|
|
143
160
|
resolve({
|
package/src/core/TaskRunner.js
CHANGED
|
@@ -7,9 +7,10 @@ import { isDailyBudgetExceeded, isTenantDailyBudgetExceeded } from "./CostTracke
|
|
|
7
7
|
import { config } from "../config/default.js";
|
|
8
8
|
import tenantManager from "../tenants/TenantManager.js";
|
|
9
9
|
import tenantContext from "../tenants/TenantContext.js";
|
|
10
|
+
import inputSanitizer from "../safety/InputSanitizer.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* Task runner
|
|
13
|
+
* Task runner - worker loop that picks tasks from the queue and executes them.
|
|
13
14
|
*
|
|
14
15
|
* Configurable concurrency (default: 2 parallel tasks).
|
|
15
16
|
*/
|
|
@@ -48,7 +49,7 @@ class TaskRunner {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
|
-
* Poll tick
|
|
52
|
+
* Poll tick - pick up work if available and under concurrency limit.
|
|
52
53
|
*/
|
|
53
54
|
tick() {
|
|
54
55
|
if (!this.running) return;
|
|
@@ -57,13 +58,13 @@ class TaskRunner {
|
|
|
57
58
|
// ── Steer/inject: if next task belongs to an already-running session,
|
|
58
59
|
// append its message into the live agent loop rather than spawning a
|
|
59
60
|
// second loop. The agent picks it up between tool calls (like Claude Code).
|
|
60
|
-
// This runs regardless of concurrency
|
|
61
|
+
// This runs regardless of concurrency - no extra slot needed.
|
|
61
62
|
const nextTask = taskQueue.peek();
|
|
62
63
|
if (nextTask?.sessionId && this.sessionSteerQueues.has(nextTask.sessionId)) {
|
|
63
64
|
const steerTask = taskQueue.dequeue(); // consume from queue
|
|
64
65
|
const steerQueue = this.sessionSteerQueues.get(steerTask.sessionId);
|
|
65
66
|
steerQueue.push({ type: "user", content: steerTask.input }); // inject into live loop
|
|
66
|
-
taskQueue.merge(steerTask.id); // complete silently
|
|
67
|
+
taskQueue.merge(steerTask.id); // complete silently - no duplicate reply
|
|
67
68
|
console.log(`[TaskRunner] Follow-up "${steerTask.input.slice(0, 60)}" injected into running session ${steerTask.sessionId}`);
|
|
68
69
|
return;
|
|
69
70
|
}
|
|
@@ -106,6 +107,18 @@ class TaskRunner {
|
|
|
106
107
|
async processTask(task, steerQueue = []) {
|
|
107
108
|
console.log(`\n[TaskRunner] Processing task ${task.id} from ${task.channel}`);
|
|
108
109
|
|
|
110
|
+
// ── Prompt injection detection ─────────────────────────────────────────────
|
|
111
|
+
// Check user input for jailbreak / credential-extraction attempts.
|
|
112
|
+
// We don't block — we prepend a SECURITY_NOTICE so the agent is warned.
|
|
113
|
+
// Channel-sourced tasks only (not http/a2a which have their own auth).
|
|
114
|
+
if (task.input && task.channel && task.channel !== "http" && task.channel !== "a2a") {
|
|
115
|
+
const injCheck = inputSanitizer.detectInjection(task.input);
|
|
116
|
+
if (injCheck.suspicious) {
|
|
117
|
+
task.input = injCheck.warningPrefix + task.input;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
109
122
|
// ── Multi-tenant: resolve tenant and effective config ──────────────────────
|
|
110
123
|
// Derive userId from sessionId: sessionId = "${channel}-${userId}"
|
|
111
124
|
let tenant = null;
|
|
@@ -121,7 +134,7 @@ class TaskRunner {
|
|
|
121
134
|
const reason = tenant.suspendReason
|
|
122
135
|
? `Account suspended: ${tenant.suspendReason}`
|
|
123
136
|
: "Account suspended. Contact the operator.";
|
|
124
|
-
console.log(`[TaskRunner] Task ${task.id} rejected
|
|
137
|
+
console.log(`[TaskRunner] Task ${task.id} rejected - tenant ${tenant.id} is suspended`);
|
|
125
138
|
taskQueue.fail(task.id, reason);
|
|
126
139
|
return;
|
|
127
140
|
}
|
|
@@ -132,7 +145,7 @@ class TaskRunner {
|
|
|
132
145
|
// Per-tenant daily budget check (separate from global budget)
|
|
133
146
|
if (tenant && resolvedConfig.maxDailyCost) {
|
|
134
147
|
if (isTenantDailyBudgetExceeded(tenant.id, resolvedConfig.maxDailyCost)) {
|
|
135
|
-
console.log(`[TaskRunner] Task ${task.id} rejected
|
|
148
|
+
console.log(`[TaskRunner] Task ${task.id} rejected - tenant ${tenant.id} daily budget ($${resolvedConfig.maxDailyCost}) reached`);
|
|
136
149
|
taskQueue.fail(task.id, `Daily budget of $${resolvedConfig.maxDailyCost} reached. Tasks resume tomorrow.`);
|
|
137
150
|
return;
|
|
138
151
|
}
|
|
@@ -9,11 +9,11 @@ const SERVICE_NAME = "daemora-agent";
|
|
|
9
9
|
const SERVICE_LABEL = "com.daemora.agent";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Daemon Manager
|
|
12
|
+
* Daemon Manager - native OS service management.
|
|
13
13
|
*
|
|
14
14
|
* Like OpenClaw: uses the OS's native service system, NOT pm2.
|
|
15
|
-
* - macOS: LaunchAgent (launchctl)
|
|
16
|
-
* - Linux: systemd user service
|
|
15
|
+
* - macOS: LaunchAgent (launchctl) - ~/Library/LaunchAgents/
|
|
16
|
+
* - Linux: systemd user service - ~/.config/systemd/user/
|
|
17
17
|
* - Windows: Scheduled Task (schtasks)
|
|
18
18
|
*
|
|
19
19
|
* Features:
|
|
@@ -204,7 +204,7 @@ export class DaemonManager {
|
|
|
204
204
|
const unitPath = join(unitDir, `${SERVICE_NAME}.service`);
|
|
205
205
|
|
|
206
206
|
const unit = `[Unit]
|
|
207
|
-
Description=Daemora
|
|
207
|
+
Description=Daemora - 24/7 AI Digital Worker
|
|
208
208
|
After=network-online.target
|
|
209
209
|
Wants=network-online.target
|
|
210
210
|
|
package/src/hooks/HookRunner.js
CHANGED
|
@@ -5,7 +5,7 @@ import { config } from "../config/default.js";
|
|
|
5
5
|
import eventBus from "../core/EventBus.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Hook Runner
|
|
8
|
+
* Hook Runner - event-driven interception at tool lifecycle points.
|
|
9
9
|
* Inspired by Claude Code's hook system.
|
|
10
10
|
*
|
|
11
11
|
* Supports:
|
|
@@ -46,7 +46,7 @@ class HookRunner {
|
|
|
46
46
|
// Create default hooks file
|
|
47
47
|
this.hooks = {};
|
|
48
48
|
this.loaded = true;
|
|
49
|
-
console.log(`[HookRunner] No hooks.json found
|
|
49
|
+
console.log(`[HookRunner] No hooks.json found - hooks disabled`);
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -110,7 +110,7 @@ class HookRunner {
|
|
|
110
110
|
console.log(
|
|
111
111
|
`[HookRunner] Hook error (${event}/${hook.matcher}): ${error.message}`
|
|
112
112
|
);
|
|
113
|
-
// Hook errors don't block execution
|
|
113
|
+
// Hook errors don't block execution - fail open
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -172,7 +172,7 @@ class HookRunner {
|
|
|
172
172
|
return { decision: "allow", output };
|
|
173
173
|
}
|
|
174
174
|
} catch (error) {
|
|
175
|
-
// Command failed
|
|
175
|
+
// Command failed - treat as allow (fail open)
|
|
176
176
|
return { decision: "allow", error: error.message };
|
|
177
177
|
}
|
|
178
178
|
}
|
package/src/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import scheduler from "./scheduler/Scheduler.js";
|
|
|
18
18
|
import heartbeat from "./scheduler/Heartbeat.js";
|
|
19
19
|
import { mountAgentCard } from "./a2a/AgentCard.js";
|
|
20
20
|
import { mountA2AServer } from "./a2a/A2AServer.js";
|
|
21
|
+
import voiceWebhook from "./voice/VoiceWebhook.js";
|
|
21
22
|
import daemonManager from "./daemon/DaemonManager.js";
|
|
22
23
|
import secretVault from "./safety/SecretVault.js";
|
|
23
24
|
import tenantManager from "./tenants/TenantManager.js";
|
|
@@ -51,6 +52,9 @@ app.use(express.json());
|
|
|
51
52
|
mountAgentCard(app);
|
|
52
53
|
mountA2AServer(app);
|
|
53
54
|
|
|
55
|
+
// Mount voice call webhooks (Twilio callbacks during live calls)
|
|
56
|
+
app.use("/voice", voiceWebhook);
|
|
57
|
+
|
|
54
58
|
// --- Health check ---
|
|
55
59
|
app.get("/health", (req, res) => {
|
|
56
60
|
res.json({
|
|
@@ -65,11 +69,11 @@ app.get("/health", (req, res) => {
|
|
|
65
69
|
});
|
|
66
70
|
});
|
|
67
71
|
|
|
68
|
-
// --- Chat endpoint (DISABLED
|
|
72
|
+
// --- Chat endpoint (DISABLED - no authentication, anyone on the network could submit tasks) ---
|
|
69
73
|
// Uncomment and add authentication middleware before re-enabling.
|
|
70
74
|
// app.post("/chat", async (req, res) => { ... });
|
|
71
75
|
|
|
72
|
-
// --- Task submit endpoint (DISABLED
|
|
76
|
+
// --- Task submit endpoint (DISABLED - same reason, unauthenticated) ---
|
|
73
77
|
// app.post("/tasks", (req, res) => { ... });
|
|
74
78
|
|
|
75
79
|
app.get("/tasks/:id", (req, res) => {
|
|
@@ -2,7 +2,7 @@ import { spawnSubAgent } from "../agents/SubAgentManager.js";
|
|
|
2
2
|
import mcpManager from "./MCPManager.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* MCP Agent Runner
|
|
5
|
+
* MCP Agent Runner - spawns specialist sub-agents for individual MCP servers.
|
|
6
6
|
*
|
|
7
7
|
* Each specialist gets:
|
|
8
8
|
* - ONLY the tools from its assigned MCP server (no built-in tools)
|
|
@@ -49,7 +49,7 @@ All MCP tool params must be passed as a single JSON string (the first and only a
|
|
|
49
49
|
tool_name: "mcp__${serverName}__someToolName"
|
|
50
50
|
params: ['{"param1":"value1","param2":"value2"}']
|
|
51
51
|
|
|
52
|
-
# Rules
|
|
52
|
+
# Rules - You Own This Task
|
|
53
53
|
|
|
54
54
|
- **Do the work, don't describe it.** Your first response must be a tool_call, not a plan.
|
|
55
55
|
- **Chain calls until fully done.** After each tool result, decide: need more tools? Call another. Only set finalResponse true when the task is genuinely complete.
|
|
@@ -106,7 +106,7 @@ export async function runMCPAgent(serverName, taskDescription, options = {}) {
|
|
|
106
106
|
...options,
|
|
107
107
|
toolOverride: serverTools,
|
|
108
108
|
systemPromptOverride,
|
|
109
|
-
// MCP agents are always depth 1
|
|
109
|
+
// MCP agents are always depth 1 - they don't spawn further sub-agents
|
|
110
110
|
depth: 1,
|
|
111
111
|
});
|
|
112
112
|
}
|