daemora 1.0.0
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 +666 -0
- package/SOUL.md +104 -0
- package/config/hooks.json +14 -0
- package/config/mcp.json +145 -0
- package/package.json +86 -0
- package/skills/.gitkeep +0 -0
- package/skills/apple-notes.md +193 -0
- package/skills/apple-reminders.md +189 -0
- package/skills/camsnap.md +162 -0
- package/skills/coding.md +14 -0
- package/skills/documents.md +13 -0
- package/skills/email.md +13 -0
- package/skills/gif-search.md +196 -0
- package/skills/healthcheck.md +225 -0
- package/skills/image-gen.md +147 -0
- package/skills/model-usage.md +182 -0
- package/skills/obsidian.md +207 -0
- package/skills/pdf.md +211 -0
- package/skills/research.md +13 -0
- package/skills/skill-creator.md +142 -0
- package/skills/spotify.md +149 -0
- package/skills/summarize.md +230 -0
- package/skills/things.md +199 -0
- package/skills/tmux.md +204 -0
- package/skills/trello.md +183 -0
- package/skills/video-frames.md +202 -0
- package/skills/weather.md +127 -0
- package/src/a2a/A2AClient.js +136 -0
- package/src/a2a/A2AServer.js +316 -0
- package/src/a2a/AgentCard.js +79 -0
- package/src/agents/SubAgentManager.js +369 -0
- package/src/agents/Supervisor.js +192 -0
- package/src/channels/BaseChannel.js +104 -0
- package/src/channels/DiscordChannel.js +288 -0
- package/src/channels/EmailChannel.js +172 -0
- package/src/channels/GoogleChatChannel.js +316 -0
- package/src/channels/HttpChannel.js +26 -0
- package/src/channels/LineChannel.js +168 -0
- package/src/channels/SignalChannel.js +186 -0
- package/src/channels/SlackChannel.js +329 -0
- package/src/channels/TeamsChannel.js +272 -0
- package/src/channels/TelegramChannel.js +347 -0
- package/src/channels/WhatsAppChannel.js +219 -0
- package/src/channels/index.js +198 -0
- package/src/cli.js +1267 -0
- package/src/config/agentProfiles.js +120 -0
- package/src/config/channels.js +32 -0
- package/src/config/default.js +206 -0
- package/src/config/models.js +123 -0
- package/src/config/permissions.js +167 -0
- package/src/core/AgentLoop.js +446 -0
- package/src/core/Compaction.js +143 -0
- package/src/core/CostTracker.js +116 -0
- package/src/core/EventBus.js +46 -0
- package/src/core/Task.js +67 -0
- package/src/core/TaskQueue.js +206 -0
- package/src/core/TaskRunner.js +226 -0
- package/src/daemon/DaemonManager.js +301 -0
- package/src/hooks/HookRunner.js +230 -0
- package/src/index.js +482 -0
- package/src/mcp/MCPAgentRunner.js +112 -0
- package/src/mcp/MCPClient.js +186 -0
- package/src/mcp/MCPManager.js +412 -0
- package/src/models/ModelRouter.js +180 -0
- package/src/safety/AuditLog.js +135 -0
- package/src/safety/CircuitBreaker.js +126 -0
- package/src/safety/FilesystemGuard.js +169 -0
- package/src/safety/GitRollback.js +139 -0
- package/src/safety/HumanApproval.js +156 -0
- package/src/safety/InputSanitizer.js +72 -0
- package/src/safety/PermissionGuard.js +83 -0
- package/src/safety/Sandbox.js +70 -0
- package/src/safety/SecretScanner.js +100 -0
- package/src/safety/SecretVault.js +250 -0
- package/src/scheduler/Heartbeat.js +115 -0
- package/src/scheduler/Scheduler.js +228 -0
- package/src/services/models/outputSchema.js +15 -0
- package/src/services/openai.js +25 -0
- package/src/services/sessions.js +65 -0
- package/src/setup/theme.js +110 -0
- package/src/setup/wizard.js +788 -0
- package/src/skills/SkillLoader.js +168 -0
- package/src/storage/TaskStore.js +69 -0
- package/src/systemPrompt.js +526 -0
- package/src/tenants/TenantContext.js +19 -0
- package/src/tenants/TenantManager.js +379 -0
- package/src/tools/ToolRegistry.js +141 -0
- package/src/tools/applyPatch.js +144 -0
- package/src/tools/browserAutomation.js +223 -0
- package/src/tools/createDocument.js +265 -0
- package/src/tools/cronTool.js +105 -0
- package/src/tools/editFile.js +139 -0
- package/src/tools/executeCommand.js +123 -0
- package/src/tools/glob.js +67 -0
- package/src/tools/grep.js +121 -0
- package/src/tools/imageAnalysis.js +120 -0
- package/src/tools/index.js +173 -0
- package/src/tools/listDirectory.js +47 -0
- package/src/tools/manageAgents.js +47 -0
- package/src/tools/manageMCP.js +159 -0
- package/src/tools/memory.js +478 -0
- package/src/tools/messageChannel.js +45 -0
- package/src/tools/projectTracker.js +259 -0
- package/src/tools/readFile.js +52 -0
- package/src/tools/screenCapture.js +112 -0
- package/src/tools/searchContent.js +76 -0
- package/src/tools/searchFiles.js +75 -0
- package/src/tools/sendEmail.js +118 -0
- package/src/tools/sendFile.js +63 -0
- package/src/tools/textToSpeech.js +161 -0
- package/src/tools/transcribeAudio.js +82 -0
- package/src/tools/useMCP.js +29 -0
- package/src/tools/webFetch.js +150 -0
- package/src/tools/webSearch.js +134 -0
- package/src/tools/writeFile.js +26 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, unlinkSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { config } from "../config/default.js";
|
|
4
|
+
import { v4 as uuidv4 } from "uuid";
|
|
5
|
+
|
|
6
|
+
const WORKSPACES_DIR = join(config.dataDir, "workspaces");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Project Tracker — SQLite-equivalent task/project tracking for the agent.
|
|
10
|
+
*
|
|
11
|
+
* The agent uses this to plan multi-step work, track what's done vs pending,
|
|
12
|
+
* and resume from where it left off if interrupted.
|
|
13
|
+
*
|
|
14
|
+
* Actions:
|
|
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
|
+
*
|
|
22
|
+
* Storage: data/projects/<id>.json (JSON files, no external deps)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const PROJECTS_DIR = join(config.dataDir, "projects");
|
|
26
|
+
|
|
27
|
+
function ensureDir() {
|
|
28
|
+
if (!existsSync(PROJECTS_DIR)) mkdirSync(PROJECTS_DIR, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function loadProject(projectId) {
|
|
32
|
+
const path = join(PROJECTS_DIR, `${projectId}.json`);
|
|
33
|
+
if (!existsSync(path)) return null;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function saveProject(project) {
|
|
42
|
+
ensureDir();
|
|
43
|
+
project.updatedAt = new Date().toISOString();
|
|
44
|
+
writeFileSync(join(PROJECTS_DIR, `${project.id}.json`), JSON.stringify(project, null, 2));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const STATUS_ICON = {
|
|
48
|
+
pending: "⬜",
|
|
49
|
+
in_progress: "🔄",
|
|
50
|
+
done: "✅",
|
|
51
|
+
failed: "❌",
|
|
52
|
+
skipped: "⏭️",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const VALID_TASK_STATUSES = ["pending", "in_progress", "done", "failed", "skipped"];
|
|
56
|
+
|
|
57
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export function projectTracker(action, paramsJson) {
|
|
60
|
+
ensureDir();
|
|
61
|
+
|
|
62
|
+
const params = paramsJson
|
|
63
|
+
? (typeof paramsJson === "string" ? JSON.parse(paramsJson) : paramsJson)
|
|
64
|
+
: {};
|
|
65
|
+
|
|
66
|
+
switch (action) {
|
|
67
|
+
|
|
68
|
+
// ── Create project ───────────────────────────────────────────────────────
|
|
69
|
+
case "createProject": {
|
|
70
|
+
const { name, description = "", tasks = [] } = params;
|
|
71
|
+
if (!name) return "Error: name is required";
|
|
72
|
+
|
|
73
|
+
const projectId = uuidv4().slice(0, 8);
|
|
74
|
+
const workspace = join(WORKSPACES_DIR, projectId);
|
|
75
|
+
mkdirSync(workspace, { recursive: true });
|
|
76
|
+
|
|
77
|
+
const project = {
|
|
78
|
+
id: projectId,
|
|
79
|
+
name,
|
|
80
|
+
description,
|
|
81
|
+
workspace,
|
|
82
|
+
status: "in_progress",
|
|
83
|
+
createdAt: new Date().toISOString(),
|
|
84
|
+
updatedAt: new Date().toISOString(),
|
|
85
|
+
tasks: tasks.map((t, i) => ({
|
|
86
|
+
id: `t${i + 1}`,
|
|
87
|
+
title: typeof t === "string" ? t : t.title,
|
|
88
|
+
description: typeof t === "string" ? "" : (t.description || ""),
|
|
89
|
+
status: "pending",
|
|
90
|
+
notes: "",
|
|
91
|
+
createdAt: new Date().toISOString(),
|
|
92
|
+
updatedAt: new Date().toISOString(),
|
|
93
|
+
})),
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
saveProject(project);
|
|
97
|
+
|
|
98
|
+
const taskList = project.tasks.length > 0
|
|
99
|
+
? project.tasks.map(t => ` ${STATUS_ICON.pending} [${t.id}] ${t.title}`).join("\n")
|
|
100
|
+
: " (no tasks yet — use addTask to add them)";
|
|
101
|
+
|
|
102
|
+
return `Project created: ${project.id}\nName: ${name}${description ? `\nDescription: ${description}` : ""}\nWorkspace: ${workspace}\nTasks (${project.tasks.length}):\n${taskList}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Add task ─────────────────────────────────────────────────────────────
|
|
106
|
+
case "addTask": {
|
|
107
|
+
const { projectId, title, description = "" } = params;
|
|
108
|
+
if (!projectId || !title) return "Error: projectId and title are required";
|
|
109
|
+
|
|
110
|
+
const project = loadProject(projectId);
|
|
111
|
+
if (!project) return `Error: Project "${projectId}" not found`;
|
|
112
|
+
|
|
113
|
+
const taskNum = project.tasks.length + 1;
|
|
114
|
+
const task = {
|
|
115
|
+
id: `t${taskNum}`,
|
|
116
|
+
title,
|
|
117
|
+
description,
|
|
118
|
+
status: "pending",
|
|
119
|
+
notes: "",
|
|
120
|
+
createdAt: new Date().toISOString(),
|
|
121
|
+
updatedAt: new Date().toISOString(),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
project.tasks.push(task);
|
|
125
|
+
saveProject(project);
|
|
126
|
+
|
|
127
|
+
return `Task added: ${STATUS_ICON.pending} [${task.id}] ${title}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Update task ──────────────────────────────────────────────────────────
|
|
131
|
+
case "updateTask": {
|
|
132
|
+
const { projectId, taskId, status, notes = "" } = params;
|
|
133
|
+
if (!projectId || !taskId || !status) {
|
|
134
|
+
return "Error: projectId, taskId, and status are required";
|
|
135
|
+
}
|
|
136
|
+
if (!VALID_TASK_STATUSES.includes(status)) {
|
|
137
|
+
return `Error: status must be one of: ${VALID_TASK_STATUSES.join(", ")}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const project = loadProject(projectId);
|
|
141
|
+
if (!project) return `Error: Project "${projectId}" not found`;
|
|
142
|
+
|
|
143
|
+
const task = project.tasks.find(t => t.id === taskId);
|
|
144
|
+
if (!task) return `Error: Task "${taskId}" not found in project ${projectId}`;
|
|
145
|
+
|
|
146
|
+
const oldStatus = task.status;
|
|
147
|
+
task.status = status;
|
|
148
|
+
if (notes) task.notes = notes;
|
|
149
|
+
task.updatedAt = new Date().toISOString();
|
|
150
|
+
|
|
151
|
+
// Auto-close project when all tasks are in a final state
|
|
152
|
+
const allFinal = project.tasks.every(t => ["done", "failed", "skipped"].includes(t.status));
|
|
153
|
+
if (allFinal) {
|
|
154
|
+
const anyFailed = project.tasks.some(t => t.status === "failed");
|
|
155
|
+
project.status = anyFailed ? "partial" : "done";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
saveProject(project);
|
|
159
|
+
|
|
160
|
+
const icon = STATUS_ICON[status] || "?";
|
|
161
|
+
const noteStr = notes ? ` | Notes: ${notes}` : "";
|
|
162
|
+
return `Task [${taskId}] "${task.title}": ${oldStatus} → ${status} ${icon}${noteStr}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Get project ──────────────────────────────────────────────────────────
|
|
166
|
+
case "getProject": {
|
|
167
|
+
const { projectId } = params;
|
|
168
|
+
if (!projectId) return "Error: projectId is required";
|
|
169
|
+
|
|
170
|
+
const project = loadProject(projectId);
|
|
171
|
+
if (!project) return `Error: Project "${projectId}" not found`;
|
|
172
|
+
|
|
173
|
+
const done = project.tasks.filter(t => t.status === "done").length;
|
|
174
|
+
const total = project.tasks.length;
|
|
175
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
176
|
+
const pending = project.tasks.filter(t => t.status === "pending").length;
|
|
177
|
+
const active = project.tasks.filter(t => t.status === "in_progress").length;
|
|
178
|
+
|
|
179
|
+
const taskLines = project.tasks.map(t => {
|
|
180
|
+
const icon = STATUS_ICON[t.status] || "?";
|
|
181
|
+
const notes = t.notes ? ` ← ${t.notes}` : "";
|
|
182
|
+
const desc = t.description ? `\n ${t.description}` : "";
|
|
183
|
+
return ` ${icon} [${t.id}] ${t.title}${notes}${desc}`;
|
|
184
|
+
}).join("\n");
|
|
185
|
+
|
|
186
|
+
const summary = [
|
|
187
|
+
`Project: ${project.name} [${project.id}]`,
|
|
188
|
+
`Status: ${project.status} | Progress: ${done}/${total} done (${pct}%)`,
|
|
189
|
+
pending > 0 ? `Pending: ${pending} tasks` : "",
|
|
190
|
+
active > 0 ? `In progress: ${active} tasks` : "",
|
|
191
|
+
project.description ? `Description: ${project.description}` : "",
|
|
192
|
+
"",
|
|
193
|
+
"Tasks:",
|
|
194
|
+
taskLines || " (no tasks)",
|
|
195
|
+
].filter(l => l !== null && l !== undefined).join("\n");
|
|
196
|
+
|
|
197
|
+
return summary;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── List all projects ────────────────────────────────────────────────────
|
|
201
|
+
case "listProjects": {
|
|
202
|
+
const files = readdirSync(PROJECTS_DIR).filter(f => f.endsWith(".json"));
|
|
203
|
+
if (files.length === 0) return "No projects found. Use createProject to start one.";
|
|
204
|
+
|
|
205
|
+
const projects = files
|
|
206
|
+
.map(f => {
|
|
207
|
+
try { return JSON.parse(readFileSync(join(PROJECTS_DIR, f), "utf-8")); }
|
|
208
|
+
catch { return null; }
|
|
209
|
+
})
|
|
210
|
+
.filter(Boolean);
|
|
211
|
+
|
|
212
|
+
projects.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
213
|
+
|
|
214
|
+
const { status: filterStatus, limit = 20 } = params;
|
|
215
|
+
const filtered = filterStatus
|
|
216
|
+
? projects.filter(p => p.status === filterStatus)
|
|
217
|
+
: projects;
|
|
218
|
+
|
|
219
|
+
if (filtered.length === 0) return `No projects with status "${filterStatus}".`;
|
|
220
|
+
|
|
221
|
+
return filtered.slice(0, limit).map(p => {
|
|
222
|
+
const done = p.tasks.filter(t => t.status === "done").length;
|
|
223
|
+
const total = p.tasks.length;
|
|
224
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
225
|
+
const date = p.updatedAt.slice(0, 10);
|
|
226
|
+
const icon = p.status === "done" ? "✅" : p.status === "partial" ? "⚠️" : "🔄";
|
|
227
|
+
return `${icon} [${p.id}] ${p.name} | ${done}/${total} (${pct}%) | updated ${date}`;
|
|
228
|
+
}).join("\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Delete project ───────────────────────────────────────────────────────
|
|
232
|
+
case "deleteProject": {
|
|
233
|
+
const { projectId } = params;
|
|
234
|
+
if (!projectId) return "Error: projectId is required";
|
|
235
|
+
|
|
236
|
+
const path = join(PROJECTS_DIR, `${projectId}.json`);
|
|
237
|
+
if (!existsSync(path)) return `Error: Project "${projectId}" not found`;
|
|
238
|
+
|
|
239
|
+
const project = loadProject(projectId);
|
|
240
|
+
unlinkSync(path);
|
|
241
|
+
return `Project "${project?.name || projectId}" deleted.`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
default:
|
|
245
|
+
return `Unknown action: "${action}". Valid actions: createProject, addTask, updateTask, getProject, listProjects, deleteProject`;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const projectTrackerDescription =
|
|
250
|
+
`projectTracker(action: string, paramsJson?: string) - Track multi-step project progress. Use this to plan work, mark tasks done/failed, and check what's remaining.
|
|
251
|
+
Actions:
|
|
252
|
+
createProject - {"name":"Todo App","description":"...","tasks":["Create HTML","Create CSS","Create JS"]}
|
|
253
|
+
addTask - {"projectId":"abc123","title":"Add dark mode","description":"optional detail"}
|
|
254
|
+
updateTask - {"projectId":"abc123","taskId":"t1","status":"done","notes":"Created index.html with 8 components"}
|
|
255
|
+
getProject - {"projectId":"abc123"} → shows all tasks with ✅⬜🔄❌ status
|
|
256
|
+
listProjects - {} or {"status":"in_progress"} → summary of all projects
|
|
257
|
+
deleteProject - {"projectId":"abc123"}
|
|
258
|
+
Task statuses: pending | in_progress | done | failed | skipped
|
|
259
|
+
BEST PRACTICE: Always createProject + list all tasks FIRST, then work through them updating status as you go.`;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Read a file with optional offset and limit (like Claude Code).
|
|
6
|
+
*
|
|
7
|
+
* @param {string} filePath - Path to the file
|
|
8
|
+
* @param {string} [offsetStr] - Line number to start from (1-based), default "1"
|
|
9
|
+
* @param {string} [limitStr] - Max lines to return, default "2000"
|
|
10
|
+
*/
|
|
11
|
+
export function readFile(filePath, offsetStr, limitStr) {
|
|
12
|
+
const offset = offsetStr ? parseInt(offsetStr, 10) : 1;
|
|
13
|
+
const limit = limitStr ? parseInt(limitStr, 10) : 2000;
|
|
14
|
+
|
|
15
|
+
// Filesystem security check
|
|
16
|
+
const guard = filesystemGuard.checkRead(filePath);
|
|
17
|
+
if (!guard.allowed) {
|
|
18
|
+
console.log(` [readFile] BLOCKED: ${guard.reason}`);
|
|
19
|
+
return guard.reason;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(` [readFile] Reading: ${filePath} (offset: ${offset}, limit: ${limit})`);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const content = readFileSync(filePath, { encoding: "utf-8" });
|
|
26
|
+
const lines = content.split("\n");
|
|
27
|
+
const totalLines = lines.length;
|
|
28
|
+
|
|
29
|
+
// Apply offset (1-based) and limit
|
|
30
|
+
const startIdx = Math.max(0, offset - 1);
|
|
31
|
+
const endIdx = Math.min(totalLines, startIdx + limit);
|
|
32
|
+
const selectedLines = lines.slice(startIdx, endIdx);
|
|
33
|
+
|
|
34
|
+
const numbered = selectedLines
|
|
35
|
+
.map((line, i) => `${startIdx + i + 1} | ${line}`)
|
|
36
|
+
.join("\n");
|
|
37
|
+
|
|
38
|
+
let result = numbered;
|
|
39
|
+
if (endIdx < totalLines) {
|
|
40
|
+
result += `\n\n[... ${totalLines - endIdx} more lines. Use offset=${endIdx + 1} to continue reading.]`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(` [readFile] Done — showing lines ${startIdx + 1}-${endIdx} of ${totalLines}`);
|
|
44
|
+
return result;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.log(` [readFile] Failed: ${error.message}`);
|
|
47
|
+
return `Error reading file: ${error.message}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const readFileDescription =
|
|
52
|
+
"readFile(filePath: string, offset?: string, limit?: string) - Reads a file with line numbers. Optional offset (start line, 1-based) and limit (max lines, default 2000). For large files, read in chunks.";
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* screenCapture(optionsJson?) — Take a screenshot or record a screen video.
|
|
3
|
+
*
|
|
4
|
+
* Modes:
|
|
5
|
+
* screenshot (default) — single still image (PNG)
|
|
6
|
+
* video — screen recording (MP4), uses `duration` seconds (default 10)
|
|
7
|
+
*
|
|
8
|
+
* macOS: uses built-in `screencapture` command.
|
|
9
|
+
* Linux: screenshots via ImageMagick/gnome-screenshot/scrot; video via ffmpeg.
|
|
10
|
+
*
|
|
11
|
+
* Returns the path to the saved file. Chain with imageAnalysis for screenshots,
|
|
12
|
+
* or sendFile to deliver the result to the user.
|
|
13
|
+
*/
|
|
14
|
+
import { execSync } from "node:child_process";
|
|
15
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
16
|
+
import { platform } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
export function screenCapture(optionsJson) {
|
|
20
|
+
try {
|
|
21
|
+
const opts = optionsJson ? JSON.parse(optionsJson) : {};
|
|
22
|
+
const outputDir = opts.outputDir || "/tmp";
|
|
23
|
+
const region = opts.region; // { x, y, width, height } — screenshot only
|
|
24
|
+
const mode = (opts.mode || "screenshot").toLowerCase();
|
|
25
|
+
const duration = parseInt(opts.duration || "10", 10); // seconds — video only
|
|
26
|
+
|
|
27
|
+
if (!existsSync(outputDir)) {
|
|
28
|
+
mkdirSync(outputDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
32
|
+
const os = platform();
|
|
33
|
+
|
|
34
|
+
// ── Screenshot mode ──────────────────────────────────────────────────────
|
|
35
|
+
if (mode === "screenshot") {
|
|
36
|
+
const outputPath = join(outputDir, `screenshot-${timestamp}.png`);
|
|
37
|
+
|
|
38
|
+
if (os === "darwin") {
|
|
39
|
+
if (region) {
|
|
40
|
+
const { x = 0, y = 0, width = 800, height = 600 } = region;
|
|
41
|
+
execSync(`screencapture -x -R ${x},${y},${width},${height} "${outputPath}"`, { timeout: 10000 });
|
|
42
|
+
} else {
|
|
43
|
+
execSync(`screencapture -x "${outputPath}"`, { timeout: 10000 });
|
|
44
|
+
}
|
|
45
|
+
} else if (os === "linux") {
|
|
46
|
+
const tools = [
|
|
47
|
+
`import -window root "${outputPath}"`,
|
|
48
|
+
`gnome-screenshot -f "${outputPath}"`,
|
|
49
|
+
`scrot "${outputPath}"`,
|
|
50
|
+
`xwd -root -silent | convert xwd:- "${outputPath}"`,
|
|
51
|
+
];
|
|
52
|
+
let captured = false;
|
|
53
|
+
for (const cmd of tools) {
|
|
54
|
+
try { execSync(cmd, { timeout: 10000 }); captured = true; break; } catch {}
|
|
55
|
+
}
|
|
56
|
+
if (!captured) {
|
|
57
|
+
return "Error: No screenshot tool available. Install ImageMagick: sudo apt install imagemagick";
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
return `Error: screenCapture is not supported on ${os}. Supported: macOS (darwin), Linux.`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!existsSync(outputPath)) {
|
|
64
|
+
return "Error: Screenshot command ran but no file was created.";
|
|
65
|
+
}
|
|
66
|
+
return `Screenshot saved to: ${outputPath}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Video mode ────────────────────────────────────────────────────────────
|
|
70
|
+
if (mode === "video") {
|
|
71
|
+
if (duration < 1 || duration > 300) {
|
|
72
|
+
return "Error: duration must be between 1 and 300 seconds.";
|
|
73
|
+
}
|
|
74
|
+
const outputPath = join(outputDir, `video-${timestamp}.mp4`);
|
|
75
|
+
const timeoutMs = (duration + 30) * 1000;
|
|
76
|
+
|
|
77
|
+
if (os === "darwin") {
|
|
78
|
+
// screencapture -V records video. Available macOS 10.15+.
|
|
79
|
+
execSync(`screencapture -V ${duration} "${outputPath}"`, { timeout: timeoutMs });
|
|
80
|
+
} else if (os === "linux") {
|
|
81
|
+
// Try ffmpeg first (most capable), then recordmydesktop
|
|
82
|
+
const ffmpegCmd = `ffmpeg -y -f x11grab -t ${duration} -i :0.0 -c:v libx264 -preset fast "${outputPath}" 2>/dev/null`;
|
|
83
|
+
const rmdCmd = `recordmydesktop --no-sound --fps 15 -o "${outputPath}" & sleep ${duration} && kill %1`;
|
|
84
|
+
|
|
85
|
+
let recorded = false;
|
|
86
|
+
for (const cmd of [ffmpegCmd, rmdCmd]) {
|
|
87
|
+
try { execSync(cmd, { timeout: timeoutMs }); recorded = true; break; } catch {}
|
|
88
|
+
}
|
|
89
|
+
if (!recorded) {
|
|
90
|
+
return "Error: No video recording tool available. Install ffmpeg: sudo apt install ffmpeg";
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
return `Error: Video recording is not supported on ${os}.`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!existsSync(outputPath)) {
|
|
97
|
+
return "Error: Video recording ran but no file was created.";
|
|
98
|
+
}
|
|
99
|
+
return `Video saved to: ${outputPath} (${duration}s)`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return `Error: Unknown mode "${mode}". Use "screenshot" or "video".`;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
return `Error in screenCapture: ${error.message}`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const screenCaptureDescription =
|
|
109
|
+
'screenCapture(optionsJson?) — Capture a screenshot or record a screen video. ' +
|
|
110
|
+
'optionsJson: {"mode":"screenshot"|"video","outputDir":"/tmp","duration":10,"region":{"x":0,"y":0,"width":800,"height":600}}. ' +
|
|
111
|
+
'mode defaults to "screenshot". duration (seconds) only applies to video mode. ' +
|
|
112
|
+
'Returns the file path. Chain with imageAnalysis to analyze screenshots, or sendFile to deliver to user.';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* searchContent(pattern, directory?, optionsJson?) — Search file contents.
|
|
3
|
+
* Upgraded: context lines, case-insensitive, file type filter, extended regex support.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
7
|
+
|
|
8
|
+
function escapeShellArg(str) {
|
|
9
|
+
return `'${str.replace(/'/g, "'\\''")}'`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function searchContent(pattern, directory = ".", optionsJson) {
|
|
13
|
+
// Support old 3-arg API (pattern, directory, limitStr) and new optionsJson
|
|
14
|
+
let opts = {};
|
|
15
|
+
if (optionsJson && !isNaN(parseInt(optionsJson))) {
|
|
16
|
+
opts = { limit: parseInt(optionsJson) };
|
|
17
|
+
} else if (optionsJson) {
|
|
18
|
+
try { opts = JSON.parse(optionsJson); } catch {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const limit = opts.limit || 50;
|
|
22
|
+
const caseInsensitive = opts.caseInsensitive || false;
|
|
23
|
+
const contextLines = opts.contextLines ? parseInt(opts.contextLines) : 0;
|
|
24
|
+
const fileType = opts.fileType; // e.g., "js", "ts", "py"
|
|
25
|
+
const useExtendedRegex = opts.regex || false;
|
|
26
|
+
|
|
27
|
+
const guard = filesystemGuard.checkRead(directory);
|
|
28
|
+
if (!guard.allowed) {
|
|
29
|
+
console.log(` [searchContent] BLOCKED: ${guard.reason}`);
|
|
30
|
+
return guard.reason;
|
|
31
|
+
}
|
|
32
|
+
console.log(` [searchContent] "${pattern}" in ${directory} (limit: ${limit})`);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
// Build grep command with flags
|
|
36
|
+
let flags = "-rn";
|
|
37
|
+
if (caseInsensitive) flags += "i";
|
|
38
|
+
if (useExtendedRegex) flags += "E";
|
|
39
|
+
if (contextLines > 0) flags += ` -C ${contextLines}`;
|
|
40
|
+
|
|
41
|
+
let includeFlag = "";
|
|
42
|
+
if (fileType) {
|
|
43
|
+
includeFlag = `--include=${escapeShellArg(`*.${fileType}`)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cmd = [
|
|
47
|
+
`grep ${flags}`,
|
|
48
|
+
escapeShellArg(pattern),
|
|
49
|
+
escapeShellArg(directory),
|
|
50
|
+
"--exclude-dir=node_modules",
|
|
51
|
+
"--exclude-dir=.git",
|
|
52
|
+
"--exclude-dir=dist",
|
|
53
|
+
"--exclude-dir=.next",
|
|
54
|
+
includeFlag,
|
|
55
|
+
"2>/dev/null",
|
|
56
|
+
`| head -${limit}`,
|
|
57
|
+
].filter(Boolean).join(" ");
|
|
58
|
+
|
|
59
|
+
const output = execSync(cmd, { encoding: "utf-8", maxBuffer: 1024 * 1024 });
|
|
60
|
+
const trimmed = output.trim();
|
|
61
|
+
if (!trimmed) {
|
|
62
|
+
return `No matches found for "${pattern}" in ${directory}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const lines = trimmed.split("\n");
|
|
66
|
+
const suffix = lines.length >= limit ? ` (limit: ${limit}, may have more — increase with optionsJson {"limit":200})` : "";
|
|
67
|
+
console.log(` [searchContent] Found ${lines.length} match(es)`);
|
|
68
|
+
return `Found ${lines.length} match(es) for "${pattern}"${suffix}:\n\n${trimmed}`;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error.status === 1) return `No matches found for "${pattern}" in ${directory}`;
|
|
71
|
+
return `Error searching content: ${error.message}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const searchContentDescription =
|
|
76
|
+
'searchContent(pattern: string, directory?: string, optionsJson?: string) - Search file contents with grep. optionsJson: {"limit":50,"caseInsensitive":true,"contextLines":2,"fileType":"js","regex":true}. Returns matching lines with file:line format.';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* searchFiles(pattern, directory?, optionsJson?) — Find files by name pattern.
|
|
3
|
+
* Upgraded: modification time sorting, depth control, size filters.
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { statSync } from "node:fs";
|
|
7
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
8
|
+
|
|
9
|
+
function escapeShellArg(str) {
|
|
10
|
+
return `'${str.replace(/'/g, "'\\''")}'`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function searchFiles(pattern, directory = ".", optionsJson) {
|
|
14
|
+
let opts = {};
|
|
15
|
+
if (optionsJson) {
|
|
16
|
+
try { opts = JSON.parse(optionsJson); } catch {}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const sortBy = opts.sortBy; // "modified" | undefined
|
|
20
|
+
const maxDepth = opts.maxDepth ? parseInt(opts.maxDepth) : null;
|
|
21
|
+
const minSize = opts.minSize; // e.g., "+10k"
|
|
22
|
+
const maxSize = opts.maxSize; // e.g., "-1m"
|
|
23
|
+
|
|
24
|
+
const guard = filesystemGuard.checkRead(directory);
|
|
25
|
+
if (!guard.allowed) {
|
|
26
|
+
console.log(` [searchFiles] BLOCKED: ${guard.reason}`);
|
|
27
|
+
return guard.reason;
|
|
28
|
+
}
|
|
29
|
+
console.log(` [searchFiles] Pattern: "${pattern}" in ${directory}`);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const parts = [
|
|
33
|
+
`find ${escapeShellArg(directory)}`,
|
|
34
|
+
`-name ${escapeShellArg(pattern)}`,
|
|
35
|
+
maxDepth ? `-maxdepth ${maxDepth}` : "",
|
|
36
|
+
minSize ? `-size +${minSize}` : "",
|
|
37
|
+
maxSize ? `-size -${maxSize}` : "",
|
|
38
|
+
`-not -path "*/node_modules/*"`,
|
|
39
|
+
`-not -path "*/.git/*"`,
|
|
40
|
+
`-not -path "*/dist/*"`,
|
|
41
|
+
`2>/dev/null`,
|
|
42
|
+
].filter(Boolean).join(" ");
|
|
43
|
+
|
|
44
|
+
const output = execSync(parts, { encoding: "utf-8", maxBuffer: 1024 * 1024 });
|
|
45
|
+
const trimmed = output.trim();
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return `No files found matching "${pattern}" in ${directory}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let files = trimmed.split("\n").filter(Boolean);
|
|
51
|
+
|
|
52
|
+
// Sort by modification time if requested
|
|
53
|
+
if (sortBy === "modified") {
|
|
54
|
+
files = files
|
|
55
|
+
.map((f) => {
|
|
56
|
+
try {
|
|
57
|
+
return { path: f, mtime: statSync(f).mtimeMs };
|
|
58
|
+
} catch {
|
|
59
|
+
return { path: f, mtime: 0 };
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
63
|
+
.map((f) => f.path);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(` [searchFiles] Found ${files.length} file(s)`);
|
|
67
|
+
return `Found ${files.length} file(s) matching "${pattern}":\n\n${files.join("\n")}`;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (!error.stdout?.trim()) return `No files found matching "${pattern}" in ${directory}`;
|
|
70
|
+
return `Error searching files: ${error.message}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const searchFilesDescription =
|
|
75
|
+
'searchFiles(pattern: string, directory?: string, optionsJson?: string) - Find files by name (supports wildcards: *.js, *.ts). optionsJson: {"sortBy":"modified","maxDepth":3,"minSize":"10k","maxSize":"1m"}. Skips node_modules/.git/dist.';
|