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
package/src/tools/manageMCP.js
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
1
|
import mcpManager from "../mcp/MCPManager.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Strip credentials from an MCP server config object before it can reach agent output.
|
|
5
|
+
* env contains API keys (GITHUB_TOKEN etc.), headers contains Bearer tokens.
|
|
6
|
+
* We replace values with "[REDACTED]" rather than deleting keys so the agent can see
|
|
7
|
+
* which credential fields are configured without seeing the actual values.
|
|
8
|
+
*/
|
|
9
|
+
function _stripCredentials(serverConfig) {
|
|
10
|
+
if (!serverConfig || typeof serverConfig !== "object") return serverConfig;
|
|
11
|
+
const safe = { ...serverConfig };
|
|
12
|
+
if (safe.env && typeof safe.env === "object") {
|
|
13
|
+
safe.env = Object.fromEntries(Object.keys(safe.env).map(k => [k, "[REDACTED]"]));
|
|
14
|
+
}
|
|
15
|
+
if (safe.headers && typeof safe.headers === "object") {
|
|
16
|
+
safe.headers = Object.fromEntries(Object.keys(safe.headers).map(k => [k, "[REDACTED]"]));
|
|
17
|
+
}
|
|
18
|
+
return safe;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* manageMCP - inspect, add, remove, and reload MCP server connections at runtime.
|
|
5
23
|
*
|
|
6
24
|
* Actions:
|
|
7
|
-
* list
|
|
8
|
-
* tools
|
|
9
|
-
* status
|
|
10
|
-
* add
|
|
11
|
-
* remove
|
|
12
|
-
* enable
|
|
13
|
-
* disable
|
|
14
|
-
* reload
|
|
25
|
+
* list - all configured servers and their tool names
|
|
26
|
+
* tools - full tool list with descriptions for a specific server
|
|
27
|
+
* status - connection status summary (same as list)
|
|
28
|
+
* add - add a new MCP server (saved to config/mcp.json + connected immediately)
|
|
29
|
+
* remove - disconnect and remove a server from config
|
|
30
|
+
* enable - enable a disabled server (reconnects it)
|
|
31
|
+
* disable - disable a server (disconnects it, keeps in config)
|
|
32
|
+
* reload - reconnect a server (useful after config changes)
|
|
15
33
|
*/
|
|
16
34
|
export async function manageMCP(action, paramsJson) {
|
|
17
35
|
const params = paramsJson
|
|
@@ -33,13 +51,13 @@ export async function manageMCP(action, paramsJson) {
|
|
|
33
51
|
|
|
34
52
|
const lines = [];
|
|
35
53
|
for (const name of configuredNames) {
|
|
36
|
-
const cfg = allConfig[name];
|
|
54
|
+
const cfg = _stripCredentials(allConfig[name]);
|
|
37
55
|
const live = connected.find(s => s.name === name);
|
|
38
56
|
if (cfg.enabled === false) {
|
|
39
57
|
lines.push(`⏸️ disabled ${name}`);
|
|
40
58
|
} else if (live?.connected) {
|
|
41
59
|
const toolList = live.tools.length > 0 ? live.tools.join(", ") : "(no tools)";
|
|
42
|
-
lines.push(`✅ connected ${name}: ${live.tools.length} tools
|
|
60
|
+
lines.push(`✅ connected ${name}: ${live.tools.length} tools - ${toolList}`);
|
|
43
61
|
} else {
|
|
44
62
|
lines.push(`❌ disconnected ${name}`);
|
|
45
63
|
}
|
|
@@ -77,12 +95,12 @@ export async function manageMCP(action, paramsJson) {
|
|
|
77
95
|
|
|
78
96
|
const serverConfig = {};
|
|
79
97
|
if (command) {
|
|
80
|
-
// stdio
|
|
98
|
+
// stdio - credentials go as env vars injected into the subprocess
|
|
81
99
|
serverConfig.command = command;
|
|
82
100
|
if (args) serverConfig.args = args;
|
|
83
101
|
if (env) serverConfig.env = env; // { "GITHUB_TOKEN": "ghp_..." }
|
|
84
102
|
} else {
|
|
85
|
-
// http/sse
|
|
103
|
+
// http/sse - credentials go as HTTP request headers
|
|
86
104
|
serverConfig.url = url;
|
|
87
105
|
if (transport) serverConfig.transport = transport; // "sse" or omit for HTTP
|
|
88
106
|
if (headers) serverConfig.headers = headers; // { "Authorization": "Bearer ${TOKEN}" }
|
|
@@ -143,8 +161,8 @@ export async function manageMCP(action, paramsJson) {
|
|
|
143
161
|
export const manageMCPDescription =
|
|
144
162
|
`manageMCP(action: string, paramsJson?: string) - Manage MCP server connections at runtime. Changes saved to config/mcp.json.
|
|
145
163
|
Actions:
|
|
146
|
-
list/status - no params
|
|
147
|
-
tools - {"server":"github"}
|
|
164
|
+
list/status - no params - all servers with connection status and tool names
|
|
165
|
+
tools - {"server":"github"} - full tool list for a server, or {} for all servers
|
|
148
166
|
add - Add and immediately connect a server:
|
|
149
167
|
stdio (auth via env vars passed to subprocess):
|
|
150
168
|
{"name":"github","command":"npx","args":["-y","@modelcontextprotocol/server-github"],"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"ghp_..."}}
|
|
@@ -152,8 +170,8 @@ export const manageMCPDescription =
|
|
|
152
170
|
{"name":"myapi","url":"https://api.example.com/mcp","headers":{"Authorization":"Bearer \${MY_TOKEN}"}}
|
|
153
171
|
SSE (auth via request headers, applied to both GET stream and POST calls):
|
|
154
172
|
{"name":"myapi","url":"https://api.example.com/sse","transport":"sse","headers":{"Authorization":"Bearer \${MY_TOKEN}","X-API-Key":"\${MY_KEY}"}}
|
|
155
|
-
Header values support \${VAR_NAME}
|
|
156
|
-
remove - {"name":"github"}
|
|
157
|
-
enable - {"name":"github"}
|
|
158
|
-
disable - {"name":"github"}
|
|
159
|
-
reload - {"name":"github"}
|
|
173
|
+
Header values support \${VAR_NAME} - expanded from process.env at connect time.
|
|
174
|
+
remove - {"name":"github"} - disconnect and remove from config
|
|
175
|
+
enable - {"name":"github"} - re-enable a disabled server (reconnects)
|
|
176
|
+
disable - {"name":"github"} - disconnect and mark disabled in config
|
|
177
|
+
reload - {"name":"github"} - reconnect (useful after editing config)`;
|
package/src/tools/memory.js
CHANGED
|
@@ -3,11 +3,12 @@ import { join } from "path";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { config } from "../config/default.js";
|
|
5
5
|
import tenantContext from "../tenants/TenantContext.js";
|
|
6
|
+
import { generateEmbedding, getEmbeddingProvider } from "../utils/Embeddings.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
* Memory tools
|
|
9
|
+
* Memory tools - read/write/search/prune persistent agent memory.
|
|
9
10
|
* Upgraded: category tags, context lines in search, pruning old entries.
|
|
10
|
-
* Phase 17: Per-tenant isolation
|
|
11
|
+
* Phase 17: Per-tenant isolation - each tenant gets their own memory dir.
|
|
11
12
|
*
|
|
12
13
|
* - MEMORY.md: Long-term facts (timestamped entries with optional category)
|
|
13
14
|
* - data/memory/YYYY-MM-DD.md: Daily logs
|
|
@@ -22,7 +23,7 @@ const _GLOBAL_EMBEDDINGS_PATH = join(config.memoryDir, "embeddings.json");
|
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Get memory paths for the current tenant context (or global paths if no tenant).
|
|
25
|
-
* Called at runtime from each function
|
|
26
|
+
* Called at runtime from each function - NOT at module load - so AsyncLocalStorage is active.
|
|
26
27
|
*/
|
|
27
28
|
function _getMemoryPaths() {
|
|
28
29
|
const store = tenantContext.getStore();
|
|
@@ -55,11 +56,11 @@ function _getPathsForTenantId(tenantId) {
|
|
|
55
56
|
|
|
56
57
|
// ─── Vector / Semantic Memory ─────────────────────────────────────────────────
|
|
57
58
|
// Stored separately from MEMORY.md so the markdown file stays human-readable.
|
|
58
|
-
// Uses OpenAI text-embedding-3-small (512 dims)
|
|
59
|
+
// Uses OpenAI text-embedding-3-small (512 dims) - 3x smaller than default 1536,
|
|
59
60
|
// same key as the rest of Daemora, no extra deps.
|
|
60
61
|
// Falls back to keyword search if OPENAI_API_KEY is absent.
|
|
61
62
|
|
|
62
|
-
// Patterns from adversarial testing
|
|
63
|
+
// Patterns from adversarial testing - prevent memory from becoming a prompt-injection vector
|
|
63
64
|
const _INJECTION_PATTERNS = [
|
|
64
65
|
/ignore (all|any|previous|above|prior) instructions/i,
|
|
65
66
|
/do not follow (the )?(system|developer)/i,
|
|
@@ -97,7 +98,7 @@ function _loadEmbeddingsForPath(embeddingsPath) {
|
|
|
97
98
|
try { return JSON.parse(readFileSync(embeddingsPath, "utf-8")); } catch { return []; }
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
// Standard cosine similarity
|
|
101
|
+
// Standard cosine similarity - correct metric for text embeddings (unlike OpenClaw's L2)
|
|
101
102
|
function _cosineSim(a, b) {
|
|
102
103
|
let dot = 0, na = 0, nb = 0;
|
|
103
104
|
for (let i = 0; i < a.length; i++) {
|
|
@@ -109,22 +110,11 @@ function _cosineSim(a, b) {
|
|
|
109
110
|
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const { default: OpenAI } = await import("openai");
|
|
118
|
-
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
|
119
|
-
const res = await client.embeddings.create({
|
|
120
|
-
model: "text-embedding-3-small",
|
|
121
|
-
input: text.slice(0, 8000), // API hard limit
|
|
122
|
-
dimensions: 512,
|
|
123
|
-
});
|
|
124
|
-
return res.data[0].embedding;
|
|
125
|
-
} catch {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
113
|
+
// Delegate to shared provider-agnostic embedding utility.
|
|
114
|
+
// Supports OpenAI, Google Gemini, and Ollama - auto-detected from available API keys.
|
|
115
|
+
// Returns null if no provider configured → callers fall back to keyword search.
|
|
116
|
+
function _generateEmbedding(text) {
|
|
117
|
+
return generateEmbedding(text);
|
|
128
118
|
}
|
|
129
119
|
|
|
130
120
|
// Store a new memory entry's embedding. Called as fire-and-forget from writeMemory.
|
|
@@ -140,7 +130,8 @@ async function _indexEntry(text, category, timestamp) {
|
|
|
140
130
|
if (e.vector && _cosineSim(e.vector, vector) > 0.92) return;
|
|
141
131
|
}
|
|
142
132
|
|
|
143
|
-
|
|
133
|
+
const provider = getEmbeddingProvider() || "openai";
|
|
134
|
+
entries.push({ id: randomUUID(), timestamp, category: category || "general", text, vector, provider });
|
|
144
135
|
_saveEmbeddings(entries);
|
|
145
136
|
}
|
|
146
137
|
|
|
@@ -162,8 +153,9 @@ export async function getRelevantMemories(taskInput, topK = 5, tenantId = null)
|
|
|
162
153
|
const entries = _loadEmbeddingsForPath(paths.embeddingsPath);
|
|
163
154
|
if (entries.length === 0) return null;
|
|
164
155
|
|
|
156
|
+
const currentProvider = getEmbeddingProvider() || "openai";
|
|
165
157
|
const scored = entries
|
|
166
|
-
.filter((e) => e.vector)
|
|
158
|
+
.filter((e) => e.vector && (e.provider === currentProvider || (!e.provider && currentProvider === "openai")))
|
|
167
159
|
.map((e) => ({ ...e, score: _cosineSim(e.vector, queryVector) }))
|
|
168
160
|
.filter((e) => e.score >= 0.40)
|
|
169
161
|
.sort((a, b) => b.score - a.score)
|
|
@@ -248,7 +240,7 @@ export async function writeMemory(entry, category) {
|
|
|
248
240
|
writeFileSync(memoryPath, existing + formatted, "utf-8");
|
|
249
241
|
console.log(` [memory] Entry added (${entry.length} chars)${category ? ` category=${category}` : ""}`);
|
|
250
242
|
|
|
251
|
-
// Generate and store embedding in background
|
|
243
|
+
// Generate and store embedding in background - does not block the tool response
|
|
252
244
|
_indexEntry(entry, category, timestamp).catch(() => {});
|
|
253
245
|
|
|
254
246
|
return `Memory saved${category ? ` [${category}]` : ""}: "${entry.slice(0, 80)}${entry.length > 80 ? "..." : ""}"`;
|
|
@@ -282,10 +274,10 @@ export function writeDailyLog(entry) {
|
|
|
282
274
|
if (existsSync(logPath)) {
|
|
283
275
|
existing = readFileSync(logPath, "utf-8");
|
|
284
276
|
} else {
|
|
285
|
-
existing = `# Daily Log
|
|
277
|
+
existing = `# Daily Log - ${today}\n\n`;
|
|
286
278
|
}
|
|
287
279
|
|
|
288
|
-
const formatted = `- **${timestamp}**
|
|
280
|
+
const formatted = `- **${timestamp}** - ${entry.trim()}\n`;
|
|
289
281
|
writeFileSync(logPath, existing + formatted, "utf-8");
|
|
290
282
|
|
|
291
283
|
console.log(` [memory] Daily log entry added`);
|
|
@@ -309,16 +301,17 @@ export async function searchMemory(query, optionsJson) {
|
|
|
309
301
|
const mode = opts.mode || "auto"; // "auto" | "semantic" | "keyword"
|
|
310
302
|
|
|
311
303
|
// ── Semantic search (cosine similarity on stored embeddings) ─────────────────
|
|
312
|
-
if (mode !== "keyword" &&
|
|
304
|
+
if (mode !== "keyword" && getEmbeddingProvider()) {
|
|
313
305
|
const queryVector = await _generateEmbedding(query);
|
|
314
306
|
if (queryVector) {
|
|
307
|
+
const currentProvider = getEmbeddingProvider() || "openai";
|
|
315
308
|
let entries = _loadEmbeddings();
|
|
316
309
|
if (filterCategory) {
|
|
317
310
|
entries = entries.filter((e) => e.category === filterCategory.toLowerCase());
|
|
318
311
|
}
|
|
319
312
|
|
|
320
313
|
const scored = entries
|
|
321
|
-
.filter((e) => e.vector)
|
|
314
|
+
.filter((e) => e.vector && (e.provider === currentProvider || (!e.provider && currentProvider === "openai")))
|
|
322
315
|
.map((e) => ({ ...e, score: _cosineSim(e.vector, queryVector) }))
|
|
323
316
|
.filter((e) => e.score >= minScore)
|
|
324
317
|
.sort((a, b) => b.score - a.score)
|
|
@@ -329,12 +322,12 @@ export async function searchMemory(query, optionsJson) {
|
|
|
329
322
|
const lines = scored.map(
|
|
330
323
|
(e, i) =>
|
|
331
324
|
`${i + 1}. [${e.category}] (${(e.score * 100).toFixed(0)}% match) ` +
|
|
332
|
-
`${_escapeForPrompt(e.text)}\n
|
|
325
|
+
`${_escapeForPrompt(e.text)}\n - ${e.timestamp.split("T")[0]}`
|
|
333
326
|
);
|
|
334
327
|
return `Found ${scored.length} semantic match(es) for "${query}":\n\n${lines.join("\n")}`;
|
|
335
328
|
}
|
|
336
329
|
|
|
337
|
-
// Nothing above threshold
|
|
330
|
+
// Nothing above threshold - fall through to keyword unless semantic-only requested
|
|
338
331
|
if (mode === "semantic") {
|
|
339
332
|
return `No semantic matches found for "${query}" (threshold: ${minScore})`;
|
|
340
333
|
}
|
|
@@ -399,7 +392,7 @@ export function pruneMemory(maxAgeDaysStr) {
|
|
|
399
392
|
let prunedMemory = 0;
|
|
400
393
|
let prunedLogs = 0;
|
|
401
394
|
|
|
402
|
-
// Prune MEMORY.md
|
|
395
|
+
// Prune MEMORY.md - keep entries newer than cutoff
|
|
403
396
|
if (existsSync(memoryPath)) {
|
|
404
397
|
const content = readFileSync(memoryPath, "utf-8");
|
|
405
398
|
const entries = parseEntries(content);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* messageChannel(channel, target, message)
|
|
2
|
+
* messageChannel(channel, target, message) - Send a message to any configured channel.
|
|
3
3
|
* Allows the agent to proactively message users, not just reply to inbound tasks.
|
|
4
4
|
* Inspired by OpenClaw's message tool.
|
|
5
5
|
*/
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notification - Send desktop / mobile push notifications.
|
|
3
|
+
* macOS: osascript / terminal-notifier. Linux: notify-send. Windows: PowerShell.
|
|
4
|
+
* Cross-platform mobile: Pushover, Pushbullet, Ntfy.sh.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
function platform() { return process.platform; }
|
|
9
|
+
|
|
10
|
+
export async function notification(title, message, options = {}) {
|
|
11
|
+
if (!title) return "Error: title is required";
|
|
12
|
+
if (!message) return "Error: message is required";
|
|
13
|
+
|
|
14
|
+
const opts = typeof options === "string" ? JSON.parse(options) : (options || {});
|
|
15
|
+
const { sound = false, url = null, service = "desktop", topic = null } = opts;
|
|
16
|
+
|
|
17
|
+
// ── Desktop notification ────────────────────────────────────────────────
|
|
18
|
+
if (service === "desktop") {
|
|
19
|
+
try {
|
|
20
|
+
if (platform() === "darwin") {
|
|
21
|
+
// Use osascript for macOS (built-in, no deps)
|
|
22
|
+
const script = `display notification ${JSON.stringify(message)} with title ${JSON.stringify(title)}${sound ? " sound name \"Glass\"" : ""}`;
|
|
23
|
+
execSync(`osascript -e '${script.replace(/'/g, "\\'")}'`, { timeout: 5000 });
|
|
24
|
+
return `Notification sent: "${title}"`;
|
|
25
|
+
} else if (platform() === "linux") {
|
|
26
|
+
const urgency = opts.urgency || "normal";
|
|
27
|
+
const expireMs = opts.expireMs || 5000;
|
|
28
|
+
execSync(`notify-send -u ${urgency} -t ${expireMs} ${JSON.stringify(title)} ${JSON.stringify(message)}`, { timeout: 5000 });
|
|
29
|
+
return `Notification sent: "${title}"`;
|
|
30
|
+
} else if (platform() === "win32") {
|
|
31
|
+
const ps = `[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null; $template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02); $template.SelectSingleNode('//text[@id=1]').AppendChild($template.CreateTextNode('${title.replace(/'/g, "''")}')) > $null; $template.SelectSingleNode('//text[@id=2]').AppendChild($template.CreateTextNode('${message.replace(/'/g, "''")}')) > $null; $toast = [Windows.UI.Notifications.ToastNotification]::new($template); [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('Daemora').Show($toast)`;
|
|
32
|
+
execSync(`powershell -Command "${ps}"`, { timeout: 5000 });
|
|
33
|
+
return `Notification sent: "${title}"`;
|
|
34
|
+
} else {
|
|
35
|
+
return "Error: desktop notifications not supported on this platform";
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return `Notification error: ${err.message}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Ntfy.sh (HTTP push — open source, self-hostable) ───────────────────
|
|
43
|
+
if (service === "ntfy") {
|
|
44
|
+
const ntfyUrl = process.env.NTFY_URL || "https://ntfy.sh";
|
|
45
|
+
const ntfyTopic = topic || process.env.NTFY_TOPIC;
|
|
46
|
+
if (!ntfyTopic) return "Error: NTFY_TOPIC env var or topic option required for ntfy service";
|
|
47
|
+
|
|
48
|
+
const { default: fetch } = await import("node-fetch").catch(() => ({ default: globalThis.fetch }));
|
|
49
|
+
const res = await fetch(`${ntfyUrl}/${ntfyTopic}`, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
body: message,
|
|
52
|
+
headers: {
|
|
53
|
+
"Title": title,
|
|
54
|
+
...(url ? { "Click": url } : {}),
|
|
55
|
+
...(process.env.NTFY_TOKEN ? { "Authorization": `Bearer ${process.env.NTFY_TOKEN}` } : {}),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
if (!res.ok) return `Ntfy error: ${res.status} ${await res.text()}`;
|
|
59
|
+
return `Ntfy notification sent to topic "${ntfyTopic}": "${title}"`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Pushover ────────────────────────────────────────────────────────────
|
|
63
|
+
if (service === "pushover") {
|
|
64
|
+
const token = process.env.PUSHOVER_API_TOKEN;
|
|
65
|
+
const user = process.env.PUSHOVER_USER_KEY;
|
|
66
|
+
if (!token || !user) return "Error: PUSHOVER_API_TOKEN and PUSHOVER_USER_KEY env vars required";
|
|
67
|
+
|
|
68
|
+
const body = new URLSearchParams({ token, user, title, message });
|
|
69
|
+
if (url) body.set("url", url);
|
|
70
|
+
|
|
71
|
+
const { default: fetch } = await import("node-fetch").catch(() => ({ default: globalThis.fetch }));
|
|
72
|
+
const res = await fetch("https://api.pushover.net/1/messages.json", { method: "POST", body });
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
if (data.status !== 1) return `Pushover error: ${JSON.stringify(data.errors)}`;
|
|
75
|
+
return `Pushover notification sent: "${title}"`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return `Unknown service: "${service}". Valid: desktop, ntfy, pushover`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const notificationDescription =
|
|
82
|
+
`notification(title: string, message: string, options?: object) - Send desktop or push notifications.
|
|
83
|
+
options.service: "desktop" (default) | "ntfy" | "pushover"
|
|
84
|
+
options.sound: boolean (macOS only, plays Glass sound)
|
|
85
|
+
options.url: URL to open on click (ntfy/pushover)
|
|
86
|
+
options.topic: ntfy topic name (or set NTFY_TOPIC env)
|
|
87
|
+
Env vars: NTFY_URL, NTFY_TOPIC, NTFY_TOKEN, PUSHOVER_API_TOKEN, PUSHOVER_USER_KEY
|
|
88
|
+
Examples:
|
|
89
|
+
notification("Task done", "Your report is ready") → desktop alert
|
|
90
|
+
notification("Alert", "Server down", {"service":"ntfy","topic":"myalerts"})`;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* philipsHue - Control Philips Hue smart lights via local Bridge API.
|
|
3
|
+
* Requires HUE_BRIDGE_IP and HUE_API_KEY env vars.
|
|
4
|
+
* All requests go to the local bridge — no cloud dependency.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export async function philipsHue(action, paramsJson) {
|
|
8
|
+
if (!action) return "Error: action required. Valid: list, on, off, color, brightness, scene, discover";
|
|
9
|
+
const params = paramsJson
|
|
10
|
+
? (typeof paramsJson === "string" ? JSON.parse(paramsJson) : paramsJson)
|
|
11
|
+
: {};
|
|
12
|
+
|
|
13
|
+
const bridgeIp = params.bridgeIp || process.env.HUE_BRIDGE_IP;
|
|
14
|
+
const apiKey = params.apiKey || process.env.HUE_API_KEY;
|
|
15
|
+
|
|
16
|
+
// Discovery doesn't require credentials
|
|
17
|
+
if (action === "discover") {
|
|
18
|
+
const fetchFn = globalThis.fetch || (await import("node-fetch")).default;
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetchFn("https://discovery.meethue.com/");
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
if (!data.length) return "No Hue bridges found on network";
|
|
23
|
+
return data.map(b => `Bridge: ${b.id} at ${b.internalipaddress}`).join("\n");
|
|
24
|
+
} catch (err) {
|
|
25
|
+
return `Discovery error: ${err.message}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!bridgeIp) return "Error: HUE_BRIDGE_IP env var or bridgeIp param required";
|
|
30
|
+
if (!apiKey) return "Error: HUE_API_KEY env var or apiKey param required";
|
|
31
|
+
|
|
32
|
+
const BASE = `http://${bridgeIp}/api/${apiKey}`;
|
|
33
|
+
const fetchFn = globalThis.fetch || (await import("node-fetch")).default;
|
|
34
|
+
|
|
35
|
+
const hueReq = async (method, path, body = null) => {
|
|
36
|
+
const opts = { method, headers: { "Content-Type": "application/json" } };
|
|
37
|
+
if (body) opts.body = JSON.stringify(body);
|
|
38
|
+
const res = await fetchFn(`${BASE}${path}`, opts);
|
|
39
|
+
return res.json();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (action === "list") {
|
|
43
|
+
const data = await hueReq("GET", "/lights");
|
|
44
|
+
if (!data || typeof data !== "object") return "Error reading lights";
|
|
45
|
+
const entries = Object.entries(data);
|
|
46
|
+
if (!entries.length) return "No lights found";
|
|
47
|
+
return entries.map(([id, light]) =>
|
|
48
|
+
`[${id}] ${light.name} — ${light.state.on ? "ON" : "OFF"} — brightness: ${light.state.bri || "N/A"} — ${light.state.reachable ? "reachable" : "unreachable"}`
|
|
49
|
+
).join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const { lightId, groupId } = params;
|
|
53
|
+
const targetPath = groupId
|
|
54
|
+
? `/groups/${groupId}/action`
|
|
55
|
+
: lightId
|
|
56
|
+
? `/lights/${lightId}/state`
|
|
57
|
+
: null;
|
|
58
|
+
|
|
59
|
+
if (action === "on") {
|
|
60
|
+
if (!targetPath) return "Error: lightId or groupId required";
|
|
61
|
+
await hueReq("PUT", targetPath, { on: true });
|
|
62
|
+
return `Light ${lightId || `group ${groupId}`} turned ON`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (action === "off") {
|
|
66
|
+
if (!targetPath) return "Error: lightId or groupId required";
|
|
67
|
+
await hueReq("PUT", targetPath, { on: false });
|
|
68
|
+
return `Light ${lightId || `group ${groupId}`} turned OFF`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (action === "brightness") {
|
|
72
|
+
if (!targetPath) return "Error: lightId or groupId required";
|
|
73
|
+
const { level } = params;
|
|
74
|
+
if (level === undefined) return "Error: level (0-254) required";
|
|
75
|
+
const bri = Math.max(0, Math.min(254, Math.round(level)));
|
|
76
|
+
await hueReq("PUT", targetPath, { on: true, bri });
|
|
77
|
+
return `Brightness set to ${level} for light ${lightId || `group ${groupId}`}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (action === "color") {
|
|
81
|
+
if (!targetPath) return "Error: lightId or groupId required";
|
|
82
|
+
const { hue, sat, bri, xy, colorTemp, hex } = params;
|
|
83
|
+
|
|
84
|
+
let state = { on: true };
|
|
85
|
+
|
|
86
|
+
if (hex) {
|
|
87
|
+
// Convert hex to XY (approximate using CIE 1931 color space)
|
|
88
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
89
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
90
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
91
|
+
// Gamma correction
|
|
92
|
+
const toLinear = c => c > 0.04045 ? Math.pow((c + 0.055) / 1.055, 2.4) : c / 12.92;
|
|
93
|
+
const rL = toLinear(r), gL = toLinear(g), bL = toLinear(b);
|
|
94
|
+
const X = rL * 0.664511 + gL * 0.154324 + bL * 0.162028;
|
|
95
|
+
const Y = rL * 0.283881 + gL * 0.668433 + bL * 0.047685;
|
|
96
|
+
const Z = rL * 0.000088 + gL * 0.072310 + bL * 0.986039;
|
|
97
|
+
const sum = X + Y + Z || 1;
|
|
98
|
+
state.xy = [X / sum, Y / sum];
|
|
99
|
+
state.bri = Math.round(Y * 254);
|
|
100
|
+
} else if (xy) {
|
|
101
|
+
state.xy = xy;
|
|
102
|
+
} else if (hue !== undefined) {
|
|
103
|
+
state.hue = hue;
|
|
104
|
+
if (sat !== undefined) state.sat = sat;
|
|
105
|
+
if (bri !== undefined) state.bri = bri;
|
|
106
|
+
} else if (colorTemp !== undefined) {
|
|
107
|
+
state.ct = colorTemp; // Mired color temperature (153=cool, 500=warm)
|
|
108
|
+
} else {
|
|
109
|
+
return "Error: provide hex, xy, hue/sat, or colorTemp";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await hueReq("PUT", targetPath, state);
|
|
113
|
+
return `Color set for light ${lightId || `group ${groupId}`}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (action === "scene") {
|
|
117
|
+
const { sceneId } = params;
|
|
118
|
+
const gId = groupId || "0";
|
|
119
|
+
if (!sceneId) {
|
|
120
|
+
// List scenes
|
|
121
|
+
const data = await hueReq("GET", "/scenes");
|
|
122
|
+
const entries = Object.entries(data || {});
|
|
123
|
+
if (!entries.length) return "No scenes found";
|
|
124
|
+
return entries.map(([id, s]) => `[${id}] ${s.name}`).join("\n");
|
|
125
|
+
}
|
|
126
|
+
await hueReq("PUT", `/groups/${gId}/action`, { scene: sceneId });
|
|
127
|
+
return `Scene "${sceneId}" activated`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return `Unknown action: "${action}". Valid: list, on, off, brightness, color, scene, discover`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const philipsHueDescription =
|
|
134
|
+
`philipsHue(action: string, paramsJson?: object) - Control Philips Hue smart lights via local bridge.
|
|
135
|
+
action: "list" | "on" | "off" | "brightness" | "color" | "scene" | "discover"
|
|
136
|
+
list: {} → shows all lights with status
|
|
137
|
+
on/off: { lightId?: "1", groupId?: "1" }
|
|
138
|
+
brightness: { lightId, level: 0-254 }
|
|
139
|
+
color: { lightId, hex?: "#ff6600" | hue?: 0-65535, sat?: 0-254 | xy?: [x,y] | colorTemp?: 153-500 }
|
|
140
|
+
scene: { groupId?, sceneId? } (omit sceneId to list scenes)
|
|
141
|
+
discover: {} → finds bridges on local network
|
|
142
|
+
Env vars: HUE_BRIDGE_IP, HUE_API_KEY
|
|
143
|
+
Examples:
|
|
144
|
+
philipsHue("list")
|
|
145
|
+
philipsHue("on", {"lightId":"1"})
|
|
146
|
+
philipsHue("color", {"lightId":"2","hex":"#ff6600"})
|
|
147
|
+
philipsHue("brightness", {"groupId":"1","level":128})`;
|
|
@@ -6,18 +6,18 @@ import { v4 as uuidv4 } from "uuid";
|
|
|
6
6
|
const WORKSPACES_DIR = join(config.dataDir, "workspaces");
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Project Tracker
|
|
9
|
+
* Project Tracker - SQLite-equivalent task/project tracking for the agent.
|
|
10
10
|
*
|
|
11
11
|
* The agent uses this to plan multi-step work, track what's done vs pending,
|
|
12
12
|
* and resume from where it left off if interrupted.
|
|
13
13
|
*
|
|
14
14
|
* Actions:
|
|
15
|
-
* createProject
|
|
16
|
-
* addTask
|
|
17
|
-
* updateTask
|
|
18
|
-
* getProject
|
|
19
|
-
* listProjects
|
|
20
|
-
* deleteProject
|
|
15
|
+
* createProject - create a project with optional initial task list
|
|
16
|
+
* addTask - add a task to an existing project
|
|
17
|
+
* updateTask - mark a task as in_progress / done / failed / skipped
|
|
18
|
+
* getProject - full status of one project (what's done, what's pending)
|
|
19
|
+
* listProjects - all projects with summary
|
|
20
|
+
* deleteProject - remove a completed/stale project
|
|
21
21
|
*
|
|
22
22
|
* Storage: data/projects/<id>.json (JSON files, no external deps)
|
|
23
23
|
*/
|
|
@@ -97,7 +97,7 @@ export function projectTracker(action, paramsJson) {
|
|
|
97
97
|
|
|
98
98
|
const taskList = project.tasks.length > 0
|
|
99
99
|
? project.tasks.map(t => ` ${STATUS_ICON.pending} [${t.id}] ${t.title}`).join("\n")
|
|
100
|
-
: " (no tasks yet
|
|
100
|
+
: " (no tasks yet - use addTask to add them)";
|
|
101
101
|
|
|
102
102
|
return `Project created: ${project.id}\nName: ${name}${description ? `\nDescription: ${description}` : ""}\nWorkspace: ${workspace}\nTasks (${project.tasks.length}):\n${taskList}`;
|
|
103
103
|
}
|
package/src/tools/readFile.js
CHANGED
|
@@ -40,7 +40,7 @@ export function readFile(filePath, offsetStr, limitStr) {
|
|
|
40
40
|
result += `\n\n[... ${totalLines - endIdx} more lines. Use offset=${endIdx + 1} to continue reading.]`;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
console.log(` [readFile] Done
|
|
43
|
+
console.log(` [readFile] Done - showing lines ${startIdx + 1}-${endIdx} of ${totalLines}`);
|
|
44
44
|
return result;
|
|
45
45
|
} catch (error) {
|
|
46
46
|
console.log(` [readFile] Failed: ${error.message}`);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* readPDF - Extract text content from a PDF file.
|
|
3
|
+
* Uses pdftotext (poppler) if available, falls back to OpenAI vision API.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
8
|
+
import tenantContext from "../tenants/TenantContext.js";
|
|
9
|
+
|
|
10
|
+
export async function readPDF(filePath, optionsJson) {
|
|
11
|
+
if (!filePath) return "Error: filePath is required.";
|
|
12
|
+
|
|
13
|
+
const guard = filesystemGuard.checkRead(filePath);
|
|
14
|
+
if (!guard.allowed) return `Access denied: ${guard.reason}`;
|
|
15
|
+
if (!existsSync(filePath)) return `Error: File not found: ${filePath}`;
|
|
16
|
+
|
|
17
|
+
let opts = {};
|
|
18
|
+
if (optionsJson) { try { opts = JSON.parse(optionsJson); } catch {} }
|
|
19
|
+
const { pages = null, method = "auto" } = opts;
|
|
20
|
+
|
|
21
|
+
// Method 1: pdftotext (poppler-utils) — fast, no API cost
|
|
22
|
+
if (method === "auto" || method === "pdftotext") {
|
|
23
|
+
try {
|
|
24
|
+
const pageFlag = pages ? `-f ${pages.split("-")[0]} -l ${pages.split("-")[1] || pages.split("-")[0]}` : "";
|
|
25
|
+
const text = execSync(`pdftotext ${pageFlag} "${filePath}" -`, { encoding: "utf-8", timeout: 30000 });
|
|
26
|
+
if (text.trim()) return text.trim();
|
|
27
|
+
} catch {
|
|
28
|
+
// pdftotext not available, fall through
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Method 2: OpenAI vision API — works without pdftotext installed
|
|
33
|
+
if (method === "auto" || method === "vision") {
|
|
34
|
+
const store = tenantContext.getStore();
|
|
35
|
+
const apiKey = store?.apiKeys?.OPENAI_API_KEY || process.env.OPENAI_API_KEY;
|
|
36
|
+
if (!apiKey) return "Error: pdftotext not found and OPENAI_API_KEY not set. Install poppler-utils or set OPENAI_API_KEY.";
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const fileBytes = readFileSync(filePath);
|
|
40
|
+
const b64 = fileBytes.toString("base64");
|
|
41
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
44
|
+
body: JSON.stringify({
|
|
45
|
+
model: "gpt-4o",
|
|
46
|
+
messages: [{
|
|
47
|
+
role: "user",
|
|
48
|
+
content: [
|
|
49
|
+
{ type: "text", text: "Extract all text content from this PDF. Return only the text, preserve structure." },
|
|
50
|
+
{ type: "image_url", image_url: { url: `data:application/pdf;base64,${b64}` } },
|
|
51
|
+
],
|
|
52
|
+
}],
|
|
53
|
+
max_tokens: 4096,
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
const data = await res.json();
|
|
57
|
+
if (!res.ok) return `Error: ${data.error?.message || res.status}`;
|
|
58
|
+
return data.choices?.[0]?.message?.content || "No text extracted.";
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return `Error extracting PDF: ${err.message}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return "Error: No extraction method available. Install poppler-utils (brew install poppler) or set OPENAI_API_KEY.";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const readPDFDescription =
|
|
68
|
+
`readPDF(filePath: string, optionsJson?: string) - Extract text from a PDF file.
|
|
69
|
+
filePath: path to the PDF file
|
|
70
|
+
optionsJson: {"pages":"1-5","method":"auto"}
|
|
71
|
+
method: "auto" (pdftotext first, then vision), "pdftotext", "vision"
|
|
72
|
+
pages: page range like "1-5" (pdftotext only)
|
|
73
|
+
Returns extracted text content.`;
|