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.
Files changed (115) hide show
  1. package/README.md +666 -0
  2. package/SOUL.md +104 -0
  3. package/config/hooks.json +14 -0
  4. package/config/mcp.json +145 -0
  5. package/package.json +86 -0
  6. package/skills/.gitkeep +0 -0
  7. package/skills/apple-notes.md +193 -0
  8. package/skills/apple-reminders.md +189 -0
  9. package/skills/camsnap.md +162 -0
  10. package/skills/coding.md +14 -0
  11. package/skills/documents.md +13 -0
  12. package/skills/email.md +13 -0
  13. package/skills/gif-search.md +196 -0
  14. package/skills/healthcheck.md +225 -0
  15. package/skills/image-gen.md +147 -0
  16. package/skills/model-usage.md +182 -0
  17. package/skills/obsidian.md +207 -0
  18. package/skills/pdf.md +211 -0
  19. package/skills/research.md +13 -0
  20. package/skills/skill-creator.md +142 -0
  21. package/skills/spotify.md +149 -0
  22. package/skills/summarize.md +230 -0
  23. package/skills/things.md +199 -0
  24. package/skills/tmux.md +204 -0
  25. package/skills/trello.md +183 -0
  26. package/skills/video-frames.md +202 -0
  27. package/skills/weather.md +127 -0
  28. package/src/a2a/A2AClient.js +136 -0
  29. package/src/a2a/A2AServer.js +316 -0
  30. package/src/a2a/AgentCard.js +79 -0
  31. package/src/agents/SubAgentManager.js +369 -0
  32. package/src/agents/Supervisor.js +192 -0
  33. package/src/channels/BaseChannel.js +104 -0
  34. package/src/channels/DiscordChannel.js +288 -0
  35. package/src/channels/EmailChannel.js +172 -0
  36. package/src/channels/GoogleChatChannel.js +316 -0
  37. package/src/channels/HttpChannel.js +26 -0
  38. package/src/channels/LineChannel.js +168 -0
  39. package/src/channels/SignalChannel.js +186 -0
  40. package/src/channels/SlackChannel.js +329 -0
  41. package/src/channels/TeamsChannel.js +272 -0
  42. package/src/channels/TelegramChannel.js +347 -0
  43. package/src/channels/WhatsAppChannel.js +219 -0
  44. package/src/channels/index.js +198 -0
  45. package/src/cli.js +1267 -0
  46. package/src/config/agentProfiles.js +120 -0
  47. package/src/config/channels.js +32 -0
  48. package/src/config/default.js +206 -0
  49. package/src/config/models.js +123 -0
  50. package/src/config/permissions.js +167 -0
  51. package/src/core/AgentLoop.js +446 -0
  52. package/src/core/Compaction.js +143 -0
  53. package/src/core/CostTracker.js +116 -0
  54. package/src/core/EventBus.js +46 -0
  55. package/src/core/Task.js +67 -0
  56. package/src/core/TaskQueue.js +206 -0
  57. package/src/core/TaskRunner.js +226 -0
  58. package/src/daemon/DaemonManager.js +301 -0
  59. package/src/hooks/HookRunner.js +230 -0
  60. package/src/index.js +482 -0
  61. package/src/mcp/MCPAgentRunner.js +112 -0
  62. package/src/mcp/MCPClient.js +186 -0
  63. package/src/mcp/MCPManager.js +412 -0
  64. package/src/models/ModelRouter.js +180 -0
  65. package/src/safety/AuditLog.js +135 -0
  66. package/src/safety/CircuitBreaker.js +126 -0
  67. package/src/safety/FilesystemGuard.js +169 -0
  68. package/src/safety/GitRollback.js +139 -0
  69. package/src/safety/HumanApproval.js +156 -0
  70. package/src/safety/InputSanitizer.js +72 -0
  71. package/src/safety/PermissionGuard.js +83 -0
  72. package/src/safety/Sandbox.js +70 -0
  73. package/src/safety/SecretScanner.js +100 -0
  74. package/src/safety/SecretVault.js +250 -0
  75. package/src/scheduler/Heartbeat.js +115 -0
  76. package/src/scheduler/Scheduler.js +228 -0
  77. package/src/services/models/outputSchema.js +15 -0
  78. package/src/services/openai.js +25 -0
  79. package/src/services/sessions.js +65 -0
  80. package/src/setup/theme.js +110 -0
  81. package/src/setup/wizard.js +788 -0
  82. package/src/skills/SkillLoader.js +168 -0
  83. package/src/storage/TaskStore.js +69 -0
  84. package/src/systemPrompt.js +526 -0
  85. package/src/tenants/TenantContext.js +19 -0
  86. package/src/tenants/TenantManager.js +379 -0
  87. package/src/tools/ToolRegistry.js +141 -0
  88. package/src/tools/applyPatch.js +144 -0
  89. package/src/tools/browserAutomation.js +223 -0
  90. package/src/tools/createDocument.js +265 -0
  91. package/src/tools/cronTool.js +105 -0
  92. package/src/tools/editFile.js +139 -0
  93. package/src/tools/executeCommand.js +123 -0
  94. package/src/tools/glob.js +67 -0
  95. package/src/tools/grep.js +121 -0
  96. package/src/tools/imageAnalysis.js +120 -0
  97. package/src/tools/index.js +173 -0
  98. package/src/tools/listDirectory.js +47 -0
  99. package/src/tools/manageAgents.js +47 -0
  100. package/src/tools/manageMCP.js +159 -0
  101. package/src/tools/memory.js +478 -0
  102. package/src/tools/messageChannel.js +45 -0
  103. package/src/tools/projectTracker.js +259 -0
  104. package/src/tools/readFile.js +52 -0
  105. package/src/tools/screenCapture.js +112 -0
  106. package/src/tools/searchContent.js +76 -0
  107. package/src/tools/searchFiles.js +75 -0
  108. package/src/tools/sendEmail.js +118 -0
  109. package/src/tools/sendFile.js +63 -0
  110. package/src/tools/textToSpeech.js +161 -0
  111. package/src/tools/transcribeAudio.js +82 -0
  112. package/src/tools/useMCP.js +29 -0
  113. package/src/tools/webFetch.js +150 -0
  114. package/src/tools/webSearch.js +134 -0
  115. 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;