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,139 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Multi-strategy file editing — inspired by Gemini CLI.
|
|
6
|
+
*
|
|
7
|
+
* Strategy chain:
|
|
8
|
+
* 1. EXACT match — direct string replacement
|
|
9
|
+
* 2. FLEXIBLE match — line-by-line, ignoring leading/trailing whitespace
|
|
10
|
+
* 3. If all fail — show helpful error with nearby context
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function exactMatch(content, oldString, newString) {
|
|
14
|
+
if (!content.includes(oldString)) return null;
|
|
15
|
+
const occurrences = content.split(oldString).length - 1;
|
|
16
|
+
const updated = content.replaceAll(oldString, newString);
|
|
17
|
+
return { updated, occurrences, strategy: "exact" };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function flexibleMatch(content, oldString, newString) {
|
|
21
|
+
const contentLines = content.split("\n");
|
|
22
|
+
const searchLines = oldString.split("\n").map((l) => l.trim());
|
|
23
|
+
|
|
24
|
+
// Find the search block in content, comparing trimmed lines
|
|
25
|
+
for (let i = 0; i <= contentLines.length - searchLines.length; i++) {
|
|
26
|
+
let match = true;
|
|
27
|
+
for (let j = 0; j < searchLines.length; j++) {
|
|
28
|
+
if (contentLines[i + j].trim() !== searchLines[j]) {
|
|
29
|
+
match = false;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (match) {
|
|
35
|
+
// Preserve the indentation of the first matched line
|
|
36
|
+
const indent = contentLines[i].match(/^(\s*)/)[1];
|
|
37
|
+
const newLines = newString.split("\n").map((line, idx) => {
|
|
38
|
+
if (idx === 0) return indent + line.trimStart();
|
|
39
|
+
return indent + line; // keep relative indentation from newString
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const result = [
|
|
43
|
+
...contentLines.slice(0, i),
|
|
44
|
+
...newLines,
|
|
45
|
+
...contentLines.slice(i + searchLines.length),
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
return { updated: result.join("\n"), occurrences: 1, strategy: "flexible" };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findNearbyContext(content, oldString, maxContext = 3) {
|
|
55
|
+
const lines = content.split("\n");
|
|
56
|
+
const searchFirstLine = oldString.split("\n")[0].trim();
|
|
57
|
+
|
|
58
|
+
// Find lines similar to the first line of oldString
|
|
59
|
+
const candidates = [];
|
|
60
|
+
for (let i = 0; i < lines.length; i++) {
|
|
61
|
+
if (lines[i].trim().includes(searchFirstLine.slice(0, 30))) {
|
|
62
|
+
const start = Math.max(0, i - maxContext);
|
|
63
|
+
const end = Math.min(lines.length, i + maxContext + 1);
|
|
64
|
+
candidates.push({
|
|
65
|
+
lineNumber: i + 1,
|
|
66
|
+
context: lines
|
|
67
|
+
.slice(start, end)
|
|
68
|
+
.map((l, idx) => `${start + idx + 1} | ${l}`)
|
|
69
|
+
.join("\n"),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return candidates.slice(0, 3);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function editFile(filePath, oldString, newString) {
|
|
77
|
+
// Parameter validation — model sometimes passes wrong number of args
|
|
78
|
+
if (!filePath || typeof filePath !== "string") {
|
|
79
|
+
return "Error: editFile requires filePath as the first parameter.";
|
|
80
|
+
}
|
|
81
|
+
if (!oldString || typeof oldString !== "string") {
|
|
82
|
+
return "Error: editFile requires oldString as the second parameter — the text to find and replace.";
|
|
83
|
+
}
|
|
84
|
+
if (newString === undefined || newString === null || typeof newString !== "string") {
|
|
85
|
+
return "Error: editFile requires 3 parameters: editFile(filePath, oldString, newString). You only passed 2. oldString is the text to FIND in the file, newString is what to REPLACE it with. If you want to append content, use writeFile instead to rewrite the full file, or use editFile with an existing line as oldString and provide the replacement that includes the new content.";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Filesystem security check
|
|
89
|
+
const guard = filesystemGuard.checkWrite(filePath);
|
|
90
|
+
if (!guard.allowed) {
|
|
91
|
+
console.log(` [editFile] BLOCKED: ${guard.reason}`);
|
|
92
|
+
return guard.reason;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log(` [editFile] File: ${filePath}`);
|
|
96
|
+
console.log(` [editFile] Find: "${oldString.slice(0, 60)}${oldString.length > 60 ? "..." : ""}"`);
|
|
97
|
+
console.log(` [editFile] Replace: "${newString.slice(0, 60)}${newString.length > 60 ? "..." : ""}"`);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const content = readFileSync(filePath, { encoding: "utf-8" });
|
|
101
|
+
|
|
102
|
+
// Strategy 1: Exact match
|
|
103
|
+
let result = exactMatch(content, oldString, newString);
|
|
104
|
+
if (result) {
|
|
105
|
+
writeFileSync(filePath, result.updated, { encoding: "utf-8" });
|
|
106
|
+
console.log(` [editFile] Done — ${result.strategy} match, replaced ${result.occurrences} occurrence(s)`);
|
|
107
|
+
return `File ${filePath} edited successfully (${result.strategy} match). Replaced ${result.occurrences} occurrence(s).`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Strategy 2: Flexible match (whitespace-tolerant)
|
|
111
|
+
result = flexibleMatch(content, oldString, newString);
|
|
112
|
+
if (result) {
|
|
113
|
+
writeFileSync(filePath, result.updated, { encoding: "utf-8" });
|
|
114
|
+
console.log(` [editFile] Done — ${result.strategy} match`);
|
|
115
|
+
return `File ${filePath} edited successfully (${result.strategy} match). Replaced ${result.occurrences} occurrence(s).`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// All strategies failed — provide helpful error
|
|
119
|
+
const nearby = findNearbyContext(content, oldString);
|
|
120
|
+
let errorMsg = `Error: Could not find the string to replace in ${filePath}.\n`;
|
|
121
|
+
errorMsg += `Make sure oldString matches the file content (including whitespace/indentation).\n`;
|
|
122
|
+
|
|
123
|
+
if (nearby.length > 0) {
|
|
124
|
+
errorMsg += `\nSimilar content found near:\n`;
|
|
125
|
+
for (const c of nearby) {
|
|
126
|
+
errorMsg += `\n--- Line ${c.lineNumber} ---\n${c.context}\n`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(` [editFile] Failed — no match found`);
|
|
131
|
+
return errorMsg;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.log(` [editFile] Failed: ${error.message}`);
|
|
134
|
+
return `Error editing file: ${error.message}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const editFileDescription =
|
|
139
|
+
"editFile(filePath: string, oldString: string, newString: string) - Finds oldString in the file and replaces ALL occurrences with newString. Supports exact and flexible matching (whitespace-tolerant). Shows nearby context on failure.";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* executeCommand(cmd, optionsJson?) — Execute a shell command with advanced options.
|
|
3
|
+
* Upgraded from 23-line basic version to support: cwd, timeout, env, background mode, stderr.
|
|
4
|
+
*
|
|
5
|
+
* Filesystem scoping:
|
|
6
|
+
* - If ALLOWED_PATHS is set, the cwd of every command must be within an allowed directory.
|
|
7
|
+
* - If RESTRICT_COMMANDS=true, absolute paths referenced in the command string are also checked.
|
|
8
|
+
* This prevents "cd / && rm -rf ~/Desktop" style escapes when scoped mode is active.
|
|
9
|
+
*/
|
|
10
|
+
import { execSync, spawn } from "node:child_process";
|
|
11
|
+
import { existsSync } from "node:fs";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { config } from "../config/default.js";
|
|
14
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 120_000; // 2 minutes default
|
|
17
|
+
const MAX_TIMEOUT_MS = 600_000; // 10 minutes hard max
|
|
18
|
+
const MAX_BUFFER = 10 * 1024 * 1024; // 10MB
|
|
19
|
+
|
|
20
|
+
export function executeCommand(cmd, optionsJson) {
|
|
21
|
+
const opts = optionsJson ? JSON.parse(optionsJson) : {};
|
|
22
|
+
const {
|
|
23
|
+
cwd: cwdRaw = null,
|
|
24
|
+
timeout: timeoutRaw = null,
|
|
25
|
+
env: extraEnv = null,
|
|
26
|
+
background = false,
|
|
27
|
+
} = opts;
|
|
28
|
+
|
|
29
|
+
// Resolve working directory
|
|
30
|
+
let cwd = process.cwd();
|
|
31
|
+
if (cwdRaw) {
|
|
32
|
+
const resolved = resolve(cwdRaw);
|
|
33
|
+
if (!existsSync(resolved)) {
|
|
34
|
+
return `Error: Working directory not found: ${cwdRaw}`;
|
|
35
|
+
}
|
|
36
|
+
cwd = resolved;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Filesystem scope enforcement ───────────────────────────────────────────
|
|
40
|
+
const allowedPaths = config.filesystem?.allowedPaths || [];
|
|
41
|
+
if (allowedPaths.length > 0) {
|
|
42
|
+
// Always check that the cwd is inside an allowed directory
|
|
43
|
+
const cwdGuard = filesystemGuard.checkRead(cwd);
|
|
44
|
+
if (!cwdGuard.allowed) {
|
|
45
|
+
return `Access denied: Working directory "${cwd}" is outside the allowed paths. ` +
|
|
46
|
+
`Allowed: ${allowedPaths.join(", ")}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// When RESTRICT_COMMANDS=true, also scan command string for absolute path references
|
|
50
|
+
if (config.filesystem?.restrictCommands) {
|
|
51
|
+
// Extract absolute paths from the command (Unix + Windows style)
|
|
52
|
+
const absPathPattern = /(\/[^\s'";|&><$]+|[A-Za-z]:\\[^\s'";|&><$]+)/g;
|
|
53
|
+
const matches = [...cmd.matchAll(absPathPattern)].map((m) => m[1]);
|
|
54
|
+
for (const p of matches) {
|
|
55
|
+
const check = filesystemGuard.checkRead(p);
|
|
56
|
+
if (!check.allowed) {
|
|
57
|
+
return `Access denied: Command references a path outside allowed directories: "${p}". ` +
|
|
58
|
+
`Allowed: ${allowedPaths.join(", ")}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
// Clamp timeout
|
|
66
|
+
const timeout = timeoutRaw
|
|
67
|
+
? Math.min(parseInt(timeoutRaw), MAX_TIMEOUT_MS)
|
|
68
|
+
: DEFAULT_TIMEOUT_MS;
|
|
69
|
+
|
|
70
|
+
// Merge env
|
|
71
|
+
const env = extraEnv ? { ...process.env, ...extraEnv } : process.env;
|
|
72
|
+
|
|
73
|
+
console.log(` [executeCommand] Running: ${cmd}${cwdRaw ? ` (cwd: ${cwdRaw})` : ""}${background ? " [background]" : ""}`);
|
|
74
|
+
|
|
75
|
+
// Background mode — spawn detached, return PID immediately
|
|
76
|
+
if (background) {
|
|
77
|
+
try {
|
|
78
|
+
const child = spawn("sh", ["-c", cmd], {
|
|
79
|
+
cwd,
|
|
80
|
+
env,
|
|
81
|
+
detached: true,
|
|
82
|
+
stdio: "ignore",
|
|
83
|
+
});
|
|
84
|
+
child.unref();
|
|
85
|
+
return `Background process started (PID: ${child.pid}). Command: ${cmd}`;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
return `Error starting background process: ${error.message}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Foreground mode — wait for result
|
|
92
|
+
try {
|
|
93
|
+
const result = execSync(cmd, {
|
|
94
|
+
encoding: "utf-8",
|
|
95
|
+
timeout,
|
|
96
|
+
maxBuffer: MAX_BUFFER,
|
|
97
|
+
cwd,
|
|
98
|
+
env,
|
|
99
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log(` [executeCommand] Done`);
|
|
103
|
+
return result.toString() || "(command completed with no output)";
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error.killed) {
|
|
106
|
+
return `Command timed out after ${timeout / 1000}s. Try a shorter-running command or use background mode: {"background":true}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Build a useful error message with stdout+stderr
|
|
110
|
+
const parts = [];
|
|
111
|
+
if (error.stdout) parts.push(`stdout:\n${error.stdout.slice(0, 2000)}`);
|
|
112
|
+
if (error.stderr) parts.push(`stderr:\n${error.stderr.slice(0, 2000)}`);
|
|
113
|
+
const exitMsg = error.status !== undefined ? ` (exit code: ${error.status})` : "";
|
|
114
|
+
|
|
115
|
+
if (parts.length > 0) {
|
|
116
|
+
return `Command failed${exitMsg}:\n${parts.join("\n---\n")}`;
|
|
117
|
+
}
|
|
118
|
+
return `Command failed${exitMsg}: ${error.message}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const executeCommandDescription =
|
|
123
|
+
'executeCommand(cmd: string, optionsJson?: string) - Execute a shell command. optionsJson: {"cwd":"./src","timeout":30000,"env":{"NODE_ENV":"test"},"background":false}. timeout is in ms (max 600000). Use background:true for long-running processes (returns PID immediately).';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* glob(pattern, directory?) — Pattern-based file search with modification time sorting.
|
|
3
|
+
* Inspired by Claude Code's Glob tool and Gemini CLI's FindFiles.
|
|
4
|
+
*/
|
|
5
|
+
import { glob as globFn } from "glob";
|
|
6
|
+
import { statSync } from "node:fs";
|
|
7
|
+
import { resolve, relative } from "node:path";
|
|
8
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
9
|
+
|
|
10
|
+
const TWENTY_FOUR_HOURS = 24 * 60 * 60 * 1000;
|
|
11
|
+
|
|
12
|
+
export async function globSearch(pattern, directory) {
|
|
13
|
+
try {
|
|
14
|
+
const dir = directory ? resolve(directory) : process.cwd();
|
|
15
|
+
|
|
16
|
+
const guard = filesystemGuard.checkRead(dir);
|
|
17
|
+
if (!guard.allowed) return guard.reason;
|
|
18
|
+
|
|
19
|
+
const matches = await globFn(pattern, {
|
|
20
|
+
cwd: dir,
|
|
21
|
+
nodir: true,
|
|
22
|
+
dot: true,
|
|
23
|
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
|
24
|
+
maxDepth: 20,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (matches.length === 0) {
|
|
28
|
+
return `No files found matching "${pattern}" in ${dir}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Stat files and sort: recently modified first, then alphabetical
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const filesWithStats = matches.map((file) => {
|
|
34
|
+
const fullPath = resolve(dir, file);
|
|
35
|
+
try {
|
|
36
|
+
const stat = statSync(fullPath);
|
|
37
|
+
return { path: file, mtime: stat.mtimeMs, recent: now - stat.mtimeMs < TWENTY_FOUR_HOURS };
|
|
38
|
+
} catch {
|
|
39
|
+
return { path: file, mtime: 0, recent: false };
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Recent files first (sorted by mtime desc), then rest alphabetically
|
|
44
|
+
filesWithStats.sort((a, b) => {
|
|
45
|
+
if (a.recent && !b.recent) return -1;
|
|
46
|
+
if (!a.recent && b.recent) return 1;
|
|
47
|
+
if (a.recent && b.recent) return b.mtime - a.mtime;
|
|
48
|
+
return a.path.localeCompare(b.path);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Cap at 200 results
|
|
52
|
+
const limited = filesWithStats.slice(0, 200);
|
|
53
|
+
const lines = limited.map((f) => f.path);
|
|
54
|
+
|
|
55
|
+
let result = `Found ${matches.length} file(s) matching "${pattern}":\n`;
|
|
56
|
+
result += lines.join("\n");
|
|
57
|
+
if (matches.length > 200) {
|
|
58
|
+
result += `\n\n... and ${matches.length - 200} more files (showing first 200)`;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return `Error searching for "${pattern}": ${error.message}`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const globSearchDescription =
|
|
67
|
+
'globSearch(pattern: string, directory?: string) - Find files matching a glob pattern (e.g., "**/*.js", "src/**/*.ts"). Returns files sorted by modification time (recent first).';
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* grep(pattern, optionsJson?) — Advanced content search with context lines.
|
|
3
|
+
* Inspired by Claude Code's Grep tool. Pure Node.js — no shell dependency.
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { join, extname, relative } from "node:path";
|
|
7
|
+
import filesystemGuard from "../safety/FilesystemGuard.js";
|
|
8
|
+
|
|
9
|
+
const EXCLUDED_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".cache"]);
|
|
10
|
+
|
|
11
|
+
function walkDir(dir, fileType, results = []) {
|
|
12
|
+
try {
|
|
13
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (entry.isDirectory()) {
|
|
16
|
+
if (!EXCLUDED_DIRS.has(entry.name)) {
|
|
17
|
+
walkDir(join(dir, entry.name), fileType, results);
|
|
18
|
+
}
|
|
19
|
+
} else if (entry.isFile()) {
|
|
20
|
+
if (!fileType || extname(entry.name) === `.${fileType}` || entry.name.endsWith(`.${fileType}`)) {
|
|
21
|
+
results.push(join(dir, entry.name));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
} catch {}
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function grep(pattern, optionsJson) {
|
|
30
|
+
try {
|
|
31
|
+
const opts = optionsJson ? JSON.parse(optionsJson) : {};
|
|
32
|
+
const {
|
|
33
|
+
directory = process.cwd(),
|
|
34
|
+
contextLines = 0,
|
|
35
|
+
caseInsensitive = false,
|
|
36
|
+
fileType = null,
|
|
37
|
+
outputMode = "content", // "content" | "files_only" | "count"
|
|
38
|
+
limit = 50,
|
|
39
|
+
} = opts;
|
|
40
|
+
|
|
41
|
+
const flags = caseInsensitive ? "gi" : "g";
|
|
42
|
+
const regex = new RegExp(pattern, flags);
|
|
43
|
+
|
|
44
|
+
const guard = filesystemGuard.checkRead(directory);
|
|
45
|
+
if (!guard.allowed) return guard.reason;
|
|
46
|
+
|
|
47
|
+
const files = walkDir(directory, fileType);
|
|
48
|
+
if (files.length === 0) {
|
|
49
|
+
return `No files found to search in ${directory}${fileType ? ` (type: ${fileType})` : ""}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const matchingFiles = [];
|
|
53
|
+
let totalMatches = 0;
|
|
54
|
+
const outputLines = [];
|
|
55
|
+
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
let content;
|
|
58
|
+
try {
|
|
59
|
+
content = readFileSync(file, "utf-8");
|
|
60
|
+
} catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const lines = content.split("\n");
|
|
65
|
+
const fileMatches = [];
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < lines.length; i++) {
|
|
68
|
+
regex.lastIndex = 0;
|
|
69
|
+
if (regex.test(lines[i])) {
|
|
70
|
+
fileMatches.push(i);
|
|
71
|
+
totalMatches++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (fileMatches.length === 0) continue;
|
|
76
|
+
|
|
77
|
+
const relPath = relative(process.cwd(), file);
|
|
78
|
+
matchingFiles.push(relPath);
|
|
79
|
+
|
|
80
|
+
if (outputMode === "content" && outputLines.length < limit) {
|
|
81
|
+
for (const lineIdx of fileMatches) {
|
|
82
|
+
const start = Math.max(0, lineIdx - contextLines);
|
|
83
|
+
const end = Math.min(lines.length - 1, lineIdx + contextLines);
|
|
84
|
+
|
|
85
|
+
for (let j = start; j <= end; j++) {
|
|
86
|
+
const prefix = j === lineIdx ? `${relPath}:${j + 1}:` : `${relPath}-${j + 1}-`;
|
|
87
|
+
outputLines.push(`${prefix}${lines[j]}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (contextLines > 0 && lineIdx !== fileMatches[fileMatches.length - 1]) {
|
|
91
|
+
outputLines.push("--");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (matchingFiles.length === 0) {
|
|
98
|
+
return `No matches found for "${pattern}"`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (outputMode === "files_only") {
|
|
102
|
+
return `Files containing "${pattern}" (${matchingFiles.length}):\n${matchingFiles.join("\n")}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (outputMode === "count") {
|
|
106
|
+
return `"${pattern}" found ${totalMatches} time(s) in ${matchingFiles.length} file(s)`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// content mode
|
|
110
|
+
let result = outputLines.join("\n");
|
|
111
|
+
if (totalMatches > limit) {
|
|
112
|
+
result += `\n\n... showing first ${limit} matches of ${totalMatches} total`;
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return `Error: ${error.message}`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const grepDescription =
|
|
121
|
+
'grep(pattern: string, optionsJson?: string) - Search file contents with regex. optionsJson: {"directory":"./src","contextLines":2,"caseInsensitive":true,"fileType":"js","outputMode":"content|files_only|count","limit":50}';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* imageAnalysis(imagePath, prompt?) — Analyze images using vision AI models.
|
|
3
|
+
* Supports local files, URLs, and data: URIs.
|
|
4
|
+
* Uses the Vercel AI SDK with whatever vision-capable model is configured.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { extname } from "node:path";
|
|
8
|
+
import { generateText } from "ai";
|
|
9
|
+
import { getModelWithFallback } from "../models/ModelRouter.js";
|
|
10
|
+
|
|
11
|
+
const MIME_MAP = {
|
|
12
|
+
".png": "image/png",
|
|
13
|
+
".jpg": "image/jpeg",
|
|
14
|
+
".jpeg": "image/jpeg",
|
|
15
|
+
".gif": "image/gif",
|
|
16
|
+
".webp": "image/webp",
|
|
17
|
+
".svg": "image/svg+xml",
|
|
18
|
+
".bmp": "image/bmp",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Vision-capable models to prefer (in order)
|
|
22
|
+
const VISION_MODEL_PREFERENCE = [
|
|
23
|
+
"google:gemini-2.0-flash",
|
|
24
|
+
"openai:gpt-4.1",
|
|
25
|
+
"openai:gpt-4.1-mini",
|
|
26
|
+
"anthropic:claude-sonnet-4-6",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
export async function imageAnalysis(imagePath, prompt) {
|
|
30
|
+
try {
|
|
31
|
+
const description = prompt || "Describe this image in detail. Include all visible text, UI elements, code, diagrams, or any other relevant content.";
|
|
32
|
+
|
|
33
|
+
let imageData;
|
|
34
|
+
let mimeType;
|
|
35
|
+
|
|
36
|
+
if (!imagePath) {
|
|
37
|
+
return "Error: imagePath is required";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (imagePath.startsWith("data:")) {
|
|
41
|
+
// data: URI — extract base64 and mime type
|
|
42
|
+
const match = imagePath.match(/^data:([^;]+);base64,(.+)$/);
|
|
43
|
+
if (!match) return "Error: Invalid data: URI format";
|
|
44
|
+
mimeType = match[1];
|
|
45
|
+
imageData = match[2];
|
|
46
|
+
} else if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) {
|
|
47
|
+
// URL — fetch and convert to base64
|
|
48
|
+
const controller = new AbortController();
|
|
49
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
50
|
+
try {
|
|
51
|
+
const res = await fetch(imagePath, {
|
|
52
|
+
signal: controller.signal,
|
|
53
|
+
headers: { "User-Agent": "Daemora/1.0" },
|
|
54
|
+
});
|
|
55
|
+
clearTimeout(timeout);
|
|
56
|
+
if (!res.ok) return `Error fetching image: HTTP ${res.status}`;
|
|
57
|
+
const contentType = res.headers.get("content-type") || "image/jpeg";
|
|
58
|
+
mimeType = contentType.split(";")[0].trim();
|
|
59
|
+
const buffer = await res.arrayBuffer();
|
|
60
|
+
imageData = Buffer.from(buffer).toString("base64");
|
|
61
|
+
} catch (err) {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
return `Error fetching image URL: ${err.message}`;
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// Local file
|
|
67
|
+
if (!existsSync(imagePath)) {
|
|
68
|
+
return `Error: File not found: ${imagePath}`;
|
|
69
|
+
}
|
|
70
|
+
const ext = extname(imagePath).toLowerCase();
|
|
71
|
+
mimeType = MIME_MAP[ext];
|
|
72
|
+
if (!mimeType) {
|
|
73
|
+
return `Error: Unsupported image type: ${ext}. Supported: ${Object.keys(MIME_MAP).join(", ")}`;
|
|
74
|
+
}
|
|
75
|
+
const buffer = readFileSync(imagePath);
|
|
76
|
+
imageData = buffer.toString("base64");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Try to get a vision-capable model
|
|
80
|
+
let selectedModel = null;
|
|
81
|
+
for (const modelId of VISION_MODEL_PREFERENCE) {
|
|
82
|
+
try {
|
|
83
|
+
const { model } = getModelWithFallback(modelId);
|
|
84
|
+
if (model) { selectedModel = model; break; }
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
if (!selectedModel) {
|
|
88
|
+
const { model } = getModelWithFallback(null);
|
|
89
|
+
selectedModel = model;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const response = await generateText({
|
|
93
|
+
model: selectedModel,
|
|
94
|
+
messages: [
|
|
95
|
+
{
|
|
96
|
+
role: "user",
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "image",
|
|
100
|
+
image: imageData,
|
|
101
|
+
mimeType,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: description,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
maxTokens: 2048,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return response.text || "No analysis returned from model.";
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return `Error analyzing image: ${error.message}`;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const imageAnalysisDescription =
|
|
120
|
+
'imageAnalysis(imagePath: string, prompt?: string) - Analyze an image using AI vision. imagePath can be a local file path, URL, or data: URI. prompt is optional (defaults to "describe this image").';
|