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,301 @@
|
|
|
1
|
+
import { execSync, execFileSync } from "child_process";
|
|
2
|
+
import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { config } from "../config/default.js";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const SERVICE_NAME = "daemora-agent";
|
|
9
|
+
const SERVICE_LABEL = "com.daemora.agent";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Daemon Manager — native OS service management.
|
|
13
|
+
*
|
|
14
|
+
* Like OpenClaw: uses the OS's native service system, NOT pm2.
|
|
15
|
+
* - macOS: LaunchAgent (launchctl) — ~/Library/LaunchAgents/
|
|
16
|
+
* - Linux: systemd user service — ~/.config/systemd/user/
|
|
17
|
+
* - Windows: Scheduled Task (schtasks)
|
|
18
|
+
*
|
|
19
|
+
* Features:
|
|
20
|
+
* - Auto-starts on machine boot/login
|
|
21
|
+
* - User can stop/start/restart via CLI
|
|
22
|
+
* - Graceful shutdown
|
|
23
|
+
* - Crash auto-restart
|
|
24
|
+
* - Logs to data/logs/
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export class DaemonManager {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.platform = process.platform;
|
|
30
|
+
this.entryPoint = join(config.rootDir, "src", "index.js");
|
|
31
|
+
this.nodeExe = process.execPath;
|
|
32
|
+
this.logsDir = join(config.dataDir, "logs");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Install the daemon service (auto-start on boot).
|
|
37
|
+
*/
|
|
38
|
+
install() {
|
|
39
|
+
mkdirSync(this.logsDir, { recursive: true });
|
|
40
|
+
|
|
41
|
+
if (this.platform === "darwin") {
|
|
42
|
+
return this.installMacOS();
|
|
43
|
+
} else if (this.platform === "linux") {
|
|
44
|
+
return this.installLinux();
|
|
45
|
+
} else if (this.platform === "win32") {
|
|
46
|
+
return this.installWindows();
|
|
47
|
+
} else {
|
|
48
|
+
throw new Error(`Unsupported platform: ${this.platform}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Uninstall the daemon service (remove auto-start).
|
|
54
|
+
*/
|
|
55
|
+
uninstall() {
|
|
56
|
+
if (this.platform === "darwin") {
|
|
57
|
+
return this.uninstallMacOS();
|
|
58
|
+
} else if (this.platform === "linux") {
|
|
59
|
+
return this.uninstallLinux();
|
|
60
|
+
} else if (this.platform === "win32") {
|
|
61
|
+
return this.uninstallWindows();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Start the daemon.
|
|
67
|
+
*/
|
|
68
|
+
start() {
|
|
69
|
+
if (this.platform === "darwin") {
|
|
70
|
+
execSync(`launchctl load ~/Library/LaunchAgents/${SERVICE_LABEL}.plist 2>/dev/null; launchctl start ${SERVICE_LABEL}`);
|
|
71
|
+
} else if (this.platform === "linux") {
|
|
72
|
+
execSync(`systemctl --user start ${SERVICE_NAME}`);
|
|
73
|
+
} else if (this.platform === "win32") {
|
|
74
|
+
execSync(`schtasks /Run /TN "${SERVICE_NAME}"`);
|
|
75
|
+
}
|
|
76
|
+
console.log(`[Daemon] Started`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Stop the daemon.
|
|
81
|
+
*/
|
|
82
|
+
stop() {
|
|
83
|
+
if (this.platform === "darwin") {
|
|
84
|
+
execSync(`launchctl stop ${SERVICE_LABEL} 2>/dev/null || true`);
|
|
85
|
+
} else if (this.platform === "linux") {
|
|
86
|
+
execSync(`systemctl --user stop ${SERVICE_NAME} 2>/dev/null || true`);
|
|
87
|
+
} else if (this.platform === "win32") {
|
|
88
|
+
execSync(`taskkill /IM node.exe /F 2>nul || true`);
|
|
89
|
+
}
|
|
90
|
+
console.log(`[Daemon] Stopped`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Restart the daemon.
|
|
95
|
+
*/
|
|
96
|
+
restart() {
|
|
97
|
+
this.stop();
|
|
98
|
+
this.start();
|
|
99
|
+
console.log(`[Daemon] Restarted`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get daemon status.
|
|
104
|
+
*/
|
|
105
|
+
status() {
|
|
106
|
+
try {
|
|
107
|
+
if (this.platform === "darwin") {
|
|
108
|
+
const out = execSync(`launchctl list ${SERVICE_LABEL} 2>/dev/null`, { encoding: "utf-8" });
|
|
109
|
+
const pidMatch = out.match(/"PID"\s*=\s*(\d+)/);
|
|
110
|
+
return { running: !!pidMatch, pid: pidMatch ? parseInt(pidMatch[1]) : null, platform: "launchd" };
|
|
111
|
+
} else if (this.platform === "linux") {
|
|
112
|
+
const out = execSync(`systemctl --user is-active ${SERVICE_NAME} 2>/dev/null`, { encoding: "utf-8" }).trim();
|
|
113
|
+
return { running: out === "active", platform: "systemd" };
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
return { running: false, platform: this.platform };
|
|
117
|
+
}
|
|
118
|
+
return { running: false, platform: this.platform };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ===== macOS LaunchAgent =====
|
|
122
|
+
|
|
123
|
+
installMacOS() {
|
|
124
|
+
const plistPath = join(
|
|
125
|
+
process.env.HOME,
|
|
126
|
+
"Library",
|
|
127
|
+
"LaunchAgents",
|
|
128
|
+
`${SERVICE_LABEL}.plist`
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const envPath = join(config.rootDir, ".env");
|
|
132
|
+
|
|
133
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
134
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
135
|
+
<plist version="1.0">
|
|
136
|
+
<dict>
|
|
137
|
+
<key>Label</key>
|
|
138
|
+
<string>${SERVICE_LABEL}</string>
|
|
139
|
+
<key>ProgramArguments</key>
|
|
140
|
+
<array>
|
|
141
|
+
<string>${this.nodeExe}</string>
|
|
142
|
+
<string>${this.entryPoint}</string>
|
|
143
|
+
</array>
|
|
144
|
+
<key>WorkingDirectory</key>
|
|
145
|
+
<string>${config.rootDir}</string>
|
|
146
|
+
<key>RunAtLoad</key>
|
|
147
|
+
<true/>
|
|
148
|
+
<key>KeepAlive</key>
|
|
149
|
+
<dict>
|
|
150
|
+
<key>SuccessfulExit</key>
|
|
151
|
+
<false/>
|
|
152
|
+
</dict>
|
|
153
|
+
<key>ThrottleInterval</key>
|
|
154
|
+
<integer>10</integer>
|
|
155
|
+
<key>StandardOutPath</key>
|
|
156
|
+
<string>${this.logsDir}/daemon-stdout.log</string>
|
|
157
|
+
<key>StandardErrorPath</key>
|
|
158
|
+
<string>${this.logsDir}/daemon-stderr.log</string>
|
|
159
|
+
<key>EnvironmentVariables</key>
|
|
160
|
+
<dict>
|
|
161
|
+
<key>NODE_ENV</key>
|
|
162
|
+
<string>production</string>
|
|
163
|
+
<key>DAEMON_MODE</key>
|
|
164
|
+
<string>true</string>
|
|
165
|
+
</dict>
|
|
166
|
+
</dict>
|
|
167
|
+
</plist>`;
|
|
168
|
+
|
|
169
|
+
mkdirSync(dirname(plistPath), { recursive: true });
|
|
170
|
+
writeFileSync(plistPath, plist, "utf-8");
|
|
171
|
+
|
|
172
|
+
// Load the service
|
|
173
|
+
try {
|
|
174
|
+
execSync(`launchctl unload ${plistPath} 2>/dev/null || true`);
|
|
175
|
+
} catch {}
|
|
176
|
+
execSync(`launchctl load ${plistPath}`);
|
|
177
|
+
|
|
178
|
+
console.log(`[Daemon] macOS LaunchAgent installed: ${plistPath}`);
|
|
179
|
+
console.log(`[Daemon] Will auto-start on login`);
|
|
180
|
+
console.log(`[Daemon] Logs: ${this.logsDir}/daemon-*.log`);
|
|
181
|
+
return { plistPath };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
uninstallMacOS() {
|
|
185
|
+
const plistPath = join(
|
|
186
|
+
process.env.HOME,
|
|
187
|
+
"Library",
|
|
188
|
+
"LaunchAgents",
|
|
189
|
+
`${SERVICE_LABEL}.plist`
|
|
190
|
+
);
|
|
191
|
+
try {
|
|
192
|
+
execSync(`launchctl unload ${plistPath} 2>/dev/null || true`);
|
|
193
|
+
} catch {}
|
|
194
|
+
if (existsSync(plistPath)) {
|
|
195
|
+
unlinkSync(plistPath);
|
|
196
|
+
}
|
|
197
|
+
console.log(`[Daemon] macOS LaunchAgent uninstalled`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ===== Linux systemd =====
|
|
201
|
+
|
|
202
|
+
installLinux() {
|
|
203
|
+
const unitDir = join(process.env.HOME, ".config", "systemd", "user");
|
|
204
|
+
const unitPath = join(unitDir, `${SERVICE_NAME}.service`);
|
|
205
|
+
|
|
206
|
+
const unit = `[Unit]
|
|
207
|
+
Description=Daemora — 24/7 AI Digital Worker
|
|
208
|
+
After=network-online.target
|
|
209
|
+
Wants=network-online.target
|
|
210
|
+
|
|
211
|
+
[Service]
|
|
212
|
+
Type=simple
|
|
213
|
+
ExecStart=${this.nodeExe} ${this.entryPoint}
|
|
214
|
+
WorkingDirectory=${config.rootDir}
|
|
215
|
+
Environment=NODE_ENV=production
|
|
216
|
+
Environment=DAEMON_MODE=true
|
|
217
|
+
Restart=always
|
|
218
|
+
RestartSec=5
|
|
219
|
+
KillMode=process
|
|
220
|
+
StandardOutput=append:${this.logsDir}/daemon-stdout.log
|
|
221
|
+
StandardError=append:${this.logsDir}/daemon-stderr.log
|
|
222
|
+
|
|
223
|
+
[Install]
|
|
224
|
+
WantedBy=default.target
|
|
225
|
+
`;
|
|
226
|
+
|
|
227
|
+
mkdirSync(unitDir, { recursive: true });
|
|
228
|
+
writeFileSync(unitPath, unit, "utf-8");
|
|
229
|
+
|
|
230
|
+
execSync("systemctl --user daemon-reload");
|
|
231
|
+
execSync(`systemctl --user enable ${SERVICE_NAME}`);
|
|
232
|
+
execSync(`systemctl --user start ${SERVICE_NAME}`);
|
|
233
|
+
|
|
234
|
+
// Enable lingering so service runs even when user is not logged in
|
|
235
|
+
try {
|
|
236
|
+
execSync(`loginctl enable-linger ${process.env.USER}`);
|
|
237
|
+
} catch {
|
|
238
|
+
console.log(`[Daemon] Warning: Could not enable linger. Service may stop on logout.`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log(`[Daemon] systemd user service installed: ${unitPath}`);
|
|
242
|
+
console.log(`[Daemon] Will auto-start on boot`);
|
|
243
|
+
console.log(`[Daemon] Logs: ${this.logsDir}/daemon-*.log`);
|
|
244
|
+
return { unitPath };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
uninstallLinux() {
|
|
248
|
+
try {
|
|
249
|
+
execSync(`systemctl --user stop ${SERVICE_NAME} 2>/dev/null || true`);
|
|
250
|
+
execSync(`systemctl --user disable ${SERVICE_NAME} 2>/dev/null || true`);
|
|
251
|
+
} catch {}
|
|
252
|
+
const unitPath = join(
|
|
253
|
+
process.env.HOME,
|
|
254
|
+
".config",
|
|
255
|
+
"systemd",
|
|
256
|
+
"user",
|
|
257
|
+
`${SERVICE_NAME}.service`
|
|
258
|
+
);
|
|
259
|
+
if (existsSync(unitPath)) {
|
|
260
|
+
unlinkSync(unitPath);
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
execSync("systemctl --user daemon-reload");
|
|
264
|
+
} catch {}
|
|
265
|
+
console.log(`[Daemon] systemd user service uninstalled`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ===== Windows Scheduled Task =====
|
|
269
|
+
|
|
270
|
+
installWindows() {
|
|
271
|
+
const batPath = join(config.dataDir, `${SERVICE_NAME}.cmd`);
|
|
272
|
+
const bat = `@echo off
|
|
273
|
+
set NODE_ENV=production
|
|
274
|
+
set DAEMON_MODE=true
|
|
275
|
+
cd /d "${config.rootDir}"
|
|
276
|
+
"${this.nodeExe}" "${this.entryPoint}" >> "${this.logsDir}\\daemon-stdout.log" 2>> "${this.logsDir}\\daemon-stderr.log"
|
|
277
|
+
`;
|
|
278
|
+
writeFileSync(batPath, bat, "utf-8");
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
execSync(`schtasks /Delete /TN "${SERVICE_NAME}" /F 2>nul`, { stdio: "ignore" });
|
|
282
|
+
} catch {}
|
|
283
|
+
execSync(
|
|
284
|
+
`schtasks /Create /TN "${SERVICE_NAME}" /TR "${batPath}" /SC ONLOGON /RL LIMITED /F`
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
console.log(`[Daemon] Windows Scheduled Task installed: ${SERVICE_NAME}`);
|
|
288
|
+
console.log(`[Daemon] Will auto-start on login`);
|
|
289
|
+
return { batPath };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
uninstallWindows() {
|
|
293
|
+
try {
|
|
294
|
+
execSync(`schtasks /Delete /TN "${SERVICE_NAME}" /F 2>nul`);
|
|
295
|
+
} catch {}
|
|
296
|
+
console.log(`[Daemon] Windows Scheduled Task uninstalled`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const daemonManager = new DaemonManager();
|
|
301
|
+
export default daemonManager;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import { readFileSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { config } from "../config/default.js";
|
|
5
|
+
import eventBus from "../core/EventBus.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hook Runner — event-driven interception at tool lifecycle points.
|
|
9
|
+
* Inspired by Claude Code's hook system.
|
|
10
|
+
*
|
|
11
|
+
* Supports:
|
|
12
|
+
* - PreToolUse: Before a tool executes. Can block/allow/modify.
|
|
13
|
+
* - PostToolUse: After a tool executes. Can log/warn/react.
|
|
14
|
+
* - TaskStart: When a task begins processing.
|
|
15
|
+
* - TaskEnd: When a task completes.
|
|
16
|
+
* - MemoryWrite: Before writing to memory. Can validate.
|
|
17
|
+
*
|
|
18
|
+
* Hook types:
|
|
19
|
+
* - "command": Run a shell command. Receives env vars TOOL_NAME, TOOL_INPUT, etc.
|
|
20
|
+
* - "js": Run a JavaScript function inline.
|
|
21
|
+
*
|
|
22
|
+
* Hook output: { decision: "allow"|"block"|"ask", reason: string, modifiedInput?: object }
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const HOOK_EVENTS = [
|
|
26
|
+
"PreToolUse",
|
|
27
|
+
"PostToolUse",
|
|
28
|
+
"TaskStart",
|
|
29
|
+
"TaskEnd",
|
|
30
|
+
"MemoryWrite",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
class HookRunner {
|
|
34
|
+
constructor() {
|
|
35
|
+
this.hooks = {};
|
|
36
|
+
this.loaded = false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load hooks from config/hooks.json
|
|
41
|
+
*/
|
|
42
|
+
load() {
|
|
43
|
+
const hooksPath = join(config.rootDir, "config", "hooks.json");
|
|
44
|
+
|
|
45
|
+
if (!existsSync(hooksPath)) {
|
|
46
|
+
// Create default hooks file
|
|
47
|
+
this.hooks = {};
|
|
48
|
+
this.loaded = true;
|
|
49
|
+
console.log(`[HookRunner] No hooks.json found — hooks disabled`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const raw = readFileSync(hooksPath, "utf-8");
|
|
55
|
+
this.hooks = JSON.parse(raw);
|
|
56
|
+
this.loaded = true;
|
|
57
|
+
|
|
58
|
+
const count = Object.values(this.hooks).reduce(
|
|
59
|
+
(sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0),
|
|
60
|
+
0
|
|
61
|
+
);
|
|
62
|
+
console.log(`[HookRunner] Loaded ${count} hooks from hooks.json`);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.log(`[HookRunner] Error loading hooks: ${error.message}`);
|
|
65
|
+
this.hooks = {};
|
|
66
|
+
this.loaded = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Run all hooks for an event.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} event - Hook event name (e.g., "PreToolUse")
|
|
74
|
+
* @param {object} context - Event context { toolName, toolInput, taskId, ... }
|
|
75
|
+
* @returns {object} Merged result: { decision, reason, modifiedInput }
|
|
76
|
+
*/
|
|
77
|
+
async run(event, context) {
|
|
78
|
+
if (!this.loaded) this.load();
|
|
79
|
+
|
|
80
|
+
const eventHooks = this.hooks[event];
|
|
81
|
+
if (!eventHooks || !Array.isArray(eventHooks) || eventHooks.length === 0) {
|
|
82
|
+
return { decision: "allow" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const results = [];
|
|
86
|
+
|
|
87
|
+
for (const hook of eventHooks) {
|
|
88
|
+
// Check matcher
|
|
89
|
+
if (hook.matcher && hook.matcher !== "*") {
|
|
90
|
+
if (context.toolName && context.toolName !== hook.matcher) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const result = await this.executeHook(hook, event, context);
|
|
97
|
+
results.push(result);
|
|
98
|
+
|
|
99
|
+
// If any hook blocks, stop immediately
|
|
100
|
+
if (result.decision === "block") {
|
|
101
|
+
eventBus.emitEvent("hook:blocked", {
|
|
102
|
+
event,
|
|
103
|
+
hook: hook.matcher || "*",
|
|
104
|
+
reason: result.reason,
|
|
105
|
+
toolName: context.toolName,
|
|
106
|
+
});
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.log(
|
|
111
|
+
`[HookRunner] Hook error (${event}/${hook.matcher}): ${error.message}`
|
|
112
|
+
);
|
|
113
|
+
// Hook errors don't block execution — fail open
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Merge results: first "ask" wins, otherwise "allow"
|
|
118
|
+
const askResult = results.find((r) => r.decision === "ask");
|
|
119
|
+
if (askResult) return askResult;
|
|
120
|
+
|
|
121
|
+
// Check for modified input
|
|
122
|
+
const modifiedResult = results.find((r) => r.modifiedInput);
|
|
123
|
+
if (modifiedResult) {
|
|
124
|
+
return { decision: "allow", modifiedInput: modifiedResult.modifiedInput };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { decision: "allow" };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Execute a single hook.
|
|
132
|
+
*/
|
|
133
|
+
async executeHook(hook, event, context) {
|
|
134
|
+
const timeout = hook.timeout || 5000;
|
|
135
|
+
|
|
136
|
+
if (hook.type === "command") {
|
|
137
|
+
return this.runCommandHook(hook.command, context, timeout);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (hook.type === "js") {
|
|
141
|
+
return this.runJsHook(hook.code, context);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { decision: "allow" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Run a shell command hook.
|
|
149
|
+
* Environment variables: TOOL_NAME, TOOL_INPUT, TASK_ID, EVENT
|
|
150
|
+
*/
|
|
151
|
+
runCommandHook(command, context, timeout) {
|
|
152
|
+
const env = {
|
|
153
|
+
...process.env,
|
|
154
|
+
TOOL_NAME: context.toolName || "",
|
|
155
|
+
TOOL_INPUT: JSON.stringify(context.toolInput || {}),
|
|
156
|
+
TASK_ID: context.taskId || "",
|
|
157
|
+
EVENT: context.event || "",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const output = execSync(command, {
|
|
162
|
+
encoding: "utf-8",
|
|
163
|
+
timeout,
|
|
164
|
+
env,
|
|
165
|
+
}).trim();
|
|
166
|
+
|
|
167
|
+
// Try parsing JSON output
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(output);
|
|
170
|
+
} catch {
|
|
171
|
+
// Non-JSON output = allow
|
|
172
|
+
return { decision: "allow", output };
|
|
173
|
+
}
|
|
174
|
+
} catch (error) {
|
|
175
|
+
// Command failed — treat as allow (fail open)
|
|
176
|
+
return { decision: "allow", error: error.message };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Run an inline JavaScript hook.
|
|
182
|
+
*/
|
|
183
|
+
runJsHook(code, context) {
|
|
184
|
+
try {
|
|
185
|
+
const fn = new Function("context", code);
|
|
186
|
+
const result = fn(context);
|
|
187
|
+
if (result && typeof result === "object" && result.decision) {
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
return { decision: "allow" };
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return { decision: "allow", error: error.message };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Convenience: run PreToolUse hooks.
|
|
198
|
+
*/
|
|
199
|
+
async preToolUse(toolName, toolInput, taskId) {
|
|
200
|
+
return this.run("PreToolUse", { toolName, toolInput, taskId });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Convenience: run PostToolUse hooks.
|
|
205
|
+
*/
|
|
206
|
+
async postToolUse(toolName, toolInput, toolOutput, taskId) {
|
|
207
|
+
return this.run("PostToolUse", {
|
|
208
|
+
toolName,
|
|
209
|
+
toolInput,
|
|
210
|
+
toolOutput,
|
|
211
|
+
taskId,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get hook stats.
|
|
217
|
+
*/
|
|
218
|
+
stats() {
|
|
219
|
+
if (!this.loaded) this.load();
|
|
220
|
+
return Object.fromEntries(
|
|
221
|
+
Object.entries(this.hooks).map(([k, v]) => [
|
|
222
|
+
k,
|
|
223
|
+
Array.isArray(v) ? v.length : 0,
|
|
224
|
+
])
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const hookRunner = new HookRunner();
|
|
230
|
+
export default hookRunner;
|