whale-code 6.4.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 (319) hide show
  1. package/README.md +95 -0
  2. package/bin/swag-agent.js +9 -0
  3. package/bin/swagmanager-mcp.js +321 -0
  4. package/dist/cli/app.d.ts +26 -0
  5. package/dist/cli/app.js +64 -0
  6. package/dist/cli/chat/AgentSelector.d.ts +14 -0
  7. package/dist/cli/chat/AgentSelector.js +14 -0
  8. package/dist/cli/chat/ChatApp.d.ts +9 -0
  9. package/dist/cli/chat/ChatApp.js +267 -0
  10. package/dist/cli/chat/ChatInput.d.ts +39 -0
  11. package/dist/cli/chat/ChatInput.js +509 -0
  12. package/dist/cli/chat/MarkdownText.d.ts +10 -0
  13. package/dist/cli/chat/MarkdownText.js +20 -0
  14. package/dist/cli/chat/MessageList.d.ts +37 -0
  15. package/dist/cli/chat/MessageList.js +80 -0
  16. package/dist/cli/chat/ModelSelector.d.ts +20 -0
  17. package/dist/cli/chat/ModelSelector.js +73 -0
  18. package/dist/cli/chat/RewindViewer.d.ts +26 -0
  19. package/dist/cli/chat/RewindViewer.js +185 -0
  20. package/dist/cli/chat/StoreSelector.d.ts +14 -0
  21. package/dist/cli/chat/StoreSelector.js +24 -0
  22. package/dist/cli/chat/StreamingText.d.ts +12 -0
  23. package/dist/cli/chat/StreamingText.js +12 -0
  24. package/dist/cli/chat/SubagentPanel.d.ts +45 -0
  25. package/dist/cli/chat/SubagentPanel.js +110 -0
  26. package/dist/cli/chat/TeamPanel.d.ts +21 -0
  27. package/dist/cli/chat/TeamPanel.js +42 -0
  28. package/dist/cli/chat/ToolIndicator.d.ts +25 -0
  29. package/dist/cli/chat/ToolIndicator.js +436 -0
  30. package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
  31. package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
  32. package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
  33. package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
  34. package/dist/cli/commands/config-cmd.d.ts +10 -0
  35. package/dist/cli/commands/config-cmd.js +99 -0
  36. package/dist/cli/commands/doctor.d.ts +14 -0
  37. package/dist/cli/commands/doctor.js +172 -0
  38. package/dist/cli/commands/init.d.ts +16 -0
  39. package/dist/cli/commands/init.js +278 -0
  40. package/dist/cli/commands/mcp.d.ts +12 -0
  41. package/dist/cli/commands/mcp.js +162 -0
  42. package/dist/cli/login/LoginApp.d.ts +7 -0
  43. package/dist/cli/login/LoginApp.js +157 -0
  44. package/dist/cli/print-mode.d.ts +31 -0
  45. package/dist/cli/print-mode.js +202 -0
  46. package/dist/cli/serve-mode.d.ts +37 -0
  47. package/dist/cli/serve-mode.js +636 -0
  48. package/dist/cli/services/agent-definitions.d.ts +25 -0
  49. package/dist/cli/services/agent-definitions.js +91 -0
  50. package/dist/cli/services/agent-events.d.ts +178 -0
  51. package/dist/cli/services/agent-events.js +175 -0
  52. package/dist/cli/services/agent-loop.d.ts +90 -0
  53. package/dist/cli/services/agent-loop.js +762 -0
  54. package/dist/cli/services/agent-worker-base.d.ts +97 -0
  55. package/dist/cli/services/agent-worker-base.js +220 -0
  56. package/dist/cli/services/auth-service.d.ts +30 -0
  57. package/dist/cli/services/auth-service.js +160 -0
  58. package/dist/cli/services/background-processes.d.ts +126 -0
  59. package/dist/cli/services/background-processes.js +318 -0
  60. package/dist/cli/services/browser-auth.d.ts +24 -0
  61. package/dist/cli/services/browser-auth.js +180 -0
  62. package/dist/cli/services/claude-md-loader.d.ts +16 -0
  63. package/dist/cli/services/claude-md-loader.js +58 -0
  64. package/dist/cli/services/config-store.d.ts +47 -0
  65. package/dist/cli/services/config-store.js +79 -0
  66. package/dist/cli/services/debug-log.d.ts +10 -0
  67. package/dist/cli/services/debug-log.js +52 -0
  68. package/dist/cli/services/error-logger.d.ts +58 -0
  69. package/dist/cli/services/error-logger.js +269 -0
  70. package/dist/cli/services/file-history.d.ts +21 -0
  71. package/dist/cli/services/file-history.js +83 -0
  72. package/dist/cli/services/format-server-response.d.ts +16 -0
  73. package/dist/cli/services/format-server-response.js +440 -0
  74. package/dist/cli/services/git-context.d.ts +11 -0
  75. package/dist/cli/services/git-context.js +66 -0
  76. package/dist/cli/services/hooks.d.ts +85 -0
  77. package/dist/cli/services/hooks.js +258 -0
  78. package/dist/cli/services/interactive-tools.d.ts +125 -0
  79. package/dist/cli/services/interactive-tools.js +260 -0
  80. package/dist/cli/services/keybinding-manager.d.ts +52 -0
  81. package/dist/cli/services/keybinding-manager.js +115 -0
  82. package/dist/cli/services/local-tools.d.ts +22 -0
  83. package/dist/cli/services/local-tools.js +697 -0
  84. package/dist/cli/services/lsp-manager.d.ts +18 -0
  85. package/dist/cli/services/lsp-manager.js +717 -0
  86. package/dist/cli/services/mcp-client.d.ts +48 -0
  87. package/dist/cli/services/mcp-client.js +157 -0
  88. package/dist/cli/services/memory-manager.d.ts +16 -0
  89. package/dist/cli/services/memory-manager.js +57 -0
  90. package/dist/cli/services/model-manager.d.ts +18 -0
  91. package/dist/cli/services/model-manager.js +71 -0
  92. package/dist/cli/services/model-router.d.ts +26 -0
  93. package/dist/cli/services/model-router.js +149 -0
  94. package/dist/cli/services/permission-modes.d.ts +13 -0
  95. package/dist/cli/services/permission-modes.js +43 -0
  96. package/dist/cli/services/rewind.d.ts +84 -0
  97. package/dist/cli/services/rewind.js +194 -0
  98. package/dist/cli/services/ripgrep.d.ts +28 -0
  99. package/dist/cli/services/ripgrep.js +138 -0
  100. package/dist/cli/services/sandbox.d.ts +29 -0
  101. package/dist/cli/services/sandbox.js +97 -0
  102. package/dist/cli/services/server-tools.d.ts +61 -0
  103. package/dist/cli/services/server-tools.js +543 -0
  104. package/dist/cli/services/session-persistence.d.ts +23 -0
  105. package/dist/cli/services/session-persistence.js +99 -0
  106. package/dist/cli/services/subagent-worker.d.ts +19 -0
  107. package/dist/cli/services/subagent-worker.js +41 -0
  108. package/dist/cli/services/subagent.d.ts +47 -0
  109. package/dist/cli/services/subagent.js +647 -0
  110. package/dist/cli/services/system-prompt.d.ts +7 -0
  111. package/dist/cli/services/system-prompt.js +198 -0
  112. package/dist/cli/services/team-lead.d.ts +73 -0
  113. package/dist/cli/services/team-lead.js +512 -0
  114. package/dist/cli/services/team-state.d.ts +77 -0
  115. package/dist/cli/services/team-state.js +398 -0
  116. package/dist/cli/services/teammate.d.ts +31 -0
  117. package/dist/cli/services/teammate.js +689 -0
  118. package/dist/cli/services/telemetry.d.ts +61 -0
  119. package/dist/cli/services/telemetry.js +209 -0
  120. package/dist/cli/services/tools/agent-tools.d.ts +14 -0
  121. package/dist/cli/services/tools/agent-tools.js +347 -0
  122. package/dist/cli/services/tools/file-ops.d.ts +15 -0
  123. package/dist/cli/services/tools/file-ops.js +487 -0
  124. package/dist/cli/services/tools/search-tools.d.ts +8 -0
  125. package/dist/cli/services/tools/search-tools.js +186 -0
  126. package/dist/cli/services/tools/shell-exec.d.ts +10 -0
  127. package/dist/cli/services/tools/shell-exec.js +168 -0
  128. package/dist/cli/services/tools/task-manager.d.ts +28 -0
  129. package/dist/cli/services/tools/task-manager.js +209 -0
  130. package/dist/cli/services/tools/web-tools.d.ts +11 -0
  131. package/dist/cli/services/tools/web-tools.js +395 -0
  132. package/dist/cli/setup/SetupApp.d.ts +9 -0
  133. package/dist/cli/setup/SetupApp.js +191 -0
  134. package/dist/cli/shared/MatrixIntro.d.ts +4 -0
  135. package/dist/cli/shared/MatrixIntro.js +83 -0
  136. package/dist/cli/shared/Theme.d.ts +74 -0
  137. package/dist/cli/shared/Theme.js +127 -0
  138. package/dist/cli/shared/WhaleBanner.d.ts +10 -0
  139. package/dist/cli/shared/WhaleBanner.js +12 -0
  140. package/dist/cli/shared/markdown.d.ts +21 -0
  141. package/dist/cli/shared/markdown.js +756 -0
  142. package/dist/cli/status/StatusApp.d.ts +4 -0
  143. package/dist/cli/status/StatusApp.js +105 -0
  144. package/dist/cli/stores/StoreApp.d.ts +7 -0
  145. package/dist/cli/stores/StoreApp.js +81 -0
  146. package/dist/index.d.ts +15 -0
  147. package/dist/index.js +538 -0
  148. package/dist/local-agent/connection.d.ts +48 -0
  149. package/dist/local-agent/connection.js +332 -0
  150. package/dist/local-agent/discovery.d.ts +18 -0
  151. package/dist/local-agent/discovery.js +146 -0
  152. package/dist/local-agent/executor.d.ts +34 -0
  153. package/dist/local-agent/executor.js +241 -0
  154. package/dist/local-agent/index.d.ts +14 -0
  155. package/dist/local-agent/index.js +198 -0
  156. package/dist/node/adapters/base.d.ts +35 -0
  157. package/dist/node/adapters/base.js +10 -0
  158. package/dist/node/adapters/discord.d.ts +29 -0
  159. package/dist/node/adapters/discord.js +299 -0
  160. package/dist/node/adapters/email.d.ts +23 -0
  161. package/dist/node/adapters/email.js +218 -0
  162. package/dist/node/adapters/imessage.d.ts +17 -0
  163. package/dist/node/adapters/imessage.js +118 -0
  164. package/dist/node/adapters/slack.d.ts +26 -0
  165. package/dist/node/adapters/slack.js +259 -0
  166. package/dist/node/adapters/sms.d.ts +23 -0
  167. package/dist/node/adapters/sms.js +161 -0
  168. package/dist/node/adapters/telegram.d.ts +17 -0
  169. package/dist/node/adapters/telegram.js +101 -0
  170. package/dist/node/adapters/webchat.d.ts +27 -0
  171. package/dist/node/adapters/webchat.js +160 -0
  172. package/dist/node/adapters/whatsapp.d.ts +28 -0
  173. package/dist/node/adapters/whatsapp.js +230 -0
  174. package/dist/node/cli.d.ts +2 -0
  175. package/dist/node/cli.js +325 -0
  176. package/dist/node/config.d.ts +17 -0
  177. package/dist/node/config.js +31 -0
  178. package/dist/node/runtime.d.ts +50 -0
  179. package/dist/node/runtime.js +351 -0
  180. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
  181. package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
  182. package/dist/server/handlers/analytics.d.ts +17 -0
  183. package/dist/server/handlers/analytics.js +266 -0
  184. package/dist/server/handlers/api-keys.d.ts +6 -0
  185. package/dist/server/handlers/api-keys.js +221 -0
  186. package/dist/server/handlers/billing.d.ts +33 -0
  187. package/dist/server/handlers/billing.js +272 -0
  188. package/dist/server/handlers/browser.d.ts +10 -0
  189. package/dist/server/handlers/browser.js +517 -0
  190. package/dist/server/handlers/catalog.d.ts +99 -0
  191. package/dist/server/handlers/catalog.js +976 -0
  192. package/dist/server/handlers/comms.d.ts +254 -0
  193. package/dist/server/handlers/comms.js +588 -0
  194. package/dist/server/handlers/creations.d.ts +6 -0
  195. package/dist/server/handlers/creations.js +479 -0
  196. package/dist/server/handlers/crm.d.ts +89 -0
  197. package/dist/server/handlers/crm.js +538 -0
  198. package/dist/server/handlers/discovery.d.ts +6 -0
  199. package/dist/server/handlers/discovery.js +288 -0
  200. package/dist/server/handlers/embeddings.d.ts +92 -0
  201. package/dist/server/handlers/embeddings.js +197 -0
  202. package/dist/server/handlers/enrichment.d.ts +8 -0
  203. package/dist/server/handlers/enrichment.js +768 -0
  204. package/dist/server/handlers/image-gen.d.ts +6 -0
  205. package/dist/server/handlers/image-gen.js +409 -0
  206. package/dist/server/handlers/inventory.d.ts +319 -0
  207. package/dist/server/handlers/inventory.js +447 -0
  208. package/dist/server/handlers/kali.d.ts +10 -0
  209. package/dist/server/handlers/kali.js +210 -0
  210. package/dist/server/handlers/llm-providers.d.ts +6 -0
  211. package/dist/server/handlers/llm-providers.js +673 -0
  212. package/dist/server/handlers/local-agent.d.ts +6 -0
  213. package/dist/server/handlers/local-agent.js +118 -0
  214. package/dist/server/handlers/meta-ads.d.ts +111 -0
  215. package/dist/server/handlers/meta-ads.js +2279 -0
  216. package/dist/server/handlers/nodes.d.ts +33 -0
  217. package/dist/server/handlers/nodes.js +699 -0
  218. package/dist/server/handlers/operations.d.ts +138 -0
  219. package/dist/server/handlers/operations.js +131 -0
  220. package/dist/server/handlers/platform.d.ts +23 -0
  221. package/dist/server/handlers/platform.js +227 -0
  222. package/dist/server/handlers/supply-chain.d.ts +19 -0
  223. package/dist/server/handlers/supply-chain.js +327 -0
  224. package/dist/server/handlers/transcription.d.ts +17 -0
  225. package/dist/server/handlers/transcription.js +121 -0
  226. package/dist/server/handlers/video-gen.d.ts +6 -0
  227. package/dist/server/handlers/video-gen.js +466 -0
  228. package/dist/server/handlers/voice.d.ts +8 -0
  229. package/dist/server/handlers/voice.js +1146 -0
  230. package/dist/server/handlers/workflow-steps.d.ts +86 -0
  231. package/dist/server/handlers/workflow-steps.js +2349 -0
  232. package/dist/server/handlers/workflows.d.ts +7 -0
  233. package/dist/server/handlers/workflows.js +989 -0
  234. package/dist/server/index.d.ts +1 -0
  235. package/dist/server/index.js +2427 -0
  236. package/dist/server/lib/batch-client.d.ts +80 -0
  237. package/dist/server/lib/batch-client.js +467 -0
  238. package/dist/server/lib/code-worker-pool.d.ts +31 -0
  239. package/dist/server/lib/code-worker-pool.js +224 -0
  240. package/dist/server/lib/code-worker.d.ts +1 -0
  241. package/dist/server/lib/code-worker.js +188 -0
  242. package/dist/server/lib/compaction-service.d.ts +32 -0
  243. package/dist/server/lib/compaction-service.js +162 -0
  244. package/dist/server/lib/logger.d.ts +19 -0
  245. package/dist/server/lib/logger.js +46 -0
  246. package/dist/server/lib/otel.d.ts +38 -0
  247. package/dist/server/lib/otel.js +126 -0
  248. package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
  249. package/dist/server/lib/pg-rate-limiter.js +86 -0
  250. package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
  251. package/dist/server/lib/prompt-sanitizer.js +177 -0
  252. package/dist/server/lib/provider-capabilities.d.ts +85 -0
  253. package/dist/server/lib/provider-capabilities.js +190 -0
  254. package/dist/server/lib/provider-failover.d.ts +74 -0
  255. package/dist/server/lib/provider-failover.js +210 -0
  256. package/dist/server/lib/rate-limiter.d.ts +39 -0
  257. package/dist/server/lib/rate-limiter.js +147 -0
  258. package/dist/server/lib/server-agent-loop.d.ts +107 -0
  259. package/dist/server/lib/server-agent-loop.js +667 -0
  260. package/dist/server/lib/server-subagent.d.ts +78 -0
  261. package/dist/server/lib/server-subagent.js +203 -0
  262. package/dist/server/lib/session-checkpoint.d.ts +51 -0
  263. package/dist/server/lib/session-checkpoint.js +145 -0
  264. package/dist/server/lib/ssrf-guard.d.ts +13 -0
  265. package/dist/server/lib/ssrf-guard.js +240 -0
  266. package/dist/server/lib/supabase-client.d.ts +7 -0
  267. package/dist/server/lib/supabase-client.js +78 -0
  268. package/dist/server/lib/template-resolver.d.ts +31 -0
  269. package/dist/server/lib/template-resolver.js +215 -0
  270. package/dist/server/lib/utils.d.ts +16 -0
  271. package/dist/server/lib/utils.js +147 -0
  272. package/dist/server/local-agent-gateway.d.ts +82 -0
  273. package/dist/server/local-agent-gateway.js +426 -0
  274. package/dist/server/providers/anthropic.d.ts +20 -0
  275. package/dist/server/providers/anthropic.js +199 -0
  276. package/dist/server/providers/bedrock.d.ts +20 -0
  277. package/dist/server/providers/bedrock.js +194 -0
  278. package/dist/server/providers/gemini.d.ts +24 -0
  279. package/dist/server/providers/gemini.js +486 -0
  280. package/dist/server/providers/openai.d.ts +24 -0
  281. package/dist/server/providers/openai.js +522 -0
  282. package/dist/server/providers/registry.d.ts +32 -0
  283. package/dist/server/providers/registry.js +58 -0
  284. package/dist/server/providers/shared.d.ts +32 -0
  285. package/dist/server/providers/shared.js +124 -0
  286. package/dist/server/providers/types.d.ts +92 -0
  287. package/dist/server/providers/types.js +12 -0
  288. package/dist/server/proxy-handlers.d.ts +6 -0
  289. package/dist/server/proxy-handlers.js +89 -0
  290. package/dist/server/tool-router.d.ts +149 -0
  291. package/dist/server/tool-router.js +803 -0
  292. package/dist/server/validation.d.ts +24 -0
  293. package/dist/server/validation.js +301 -0
  294. package/dist/server/worker.d.ts +19 -0
  295. package/dist/server/worker.js +201 -0
  296. package/dist/setup.d.ts +8 -0
  297. package/dist/setup.js +181 -0
  298. package/dist/shared/agent-core.d.ts +157 -0
  299. package/dist/shared/agent-core.js +534 -0
  300. package/dist/shared/anthropic-types.d.ts +105 -0
  301. package/dist/shared/anthropic-types.js +7 -0
  302. package/dist/shared/api-client.d.ts +90 -0
  303. package/dist/shared/api-client.js +379 -0
  304. package/dist/shared/constants.d.ts +33 -0
  305. package/dist/shared/constants.js +80 -0
  306. package/dist/shared/sse-parser.d.ts +26 -0
  307. package/dist/shared/sse-parser.js +259 -0
  308. package/dist/shared/tool-dispatch.d.ts +52 -0
  309. package/dist/shared/tool-dispatch.js +191 -0
  310. package/dist/shared/types.d.ts +72 -0
  311. package/dist/shared/types.js +7 -0
  312. package/dist/updater.d.ts +25 -0
  313. package/dist/updater.js +140 -0
  314. package/dist/webchat/widget.d.ts +0 -0
  315. package/dist/webchat/widget.js +397 -0
  316. package/package.json +95 -0
  317. package/src/cli/services/builtin-skills/commit.md +19 -0
  318. package/src/cli/services/builtin-skills/review-pr.md +21 -0
  319. package/src/cli/services/builtin-skills/review.md +18 -0
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Hooks System — shell commands that run before/after tool calls and at session lifecycle events.
3
+ *
4
+ * Hooks are configured in:
5
+ * - Project: .whale/hooks.json (array of HookConfig)
6
+ * - User: ~/.swagmanager/hooks.json (array of HookConfig)
7
+ *
8
+ * Both are loaded and merged (project hooks run first).
9
+ *
10
+ * Hook process receives JSON on stdin:
11
+ * { event, tool_name?, tool_input?, tool_output?, tool_success?, session_id? }
12
+ *
13
+ * Hook process may output JSON on stdout:
14
+ * { allow?: boolean, message?: string, modified_input?: object, modified_output?: string }
15
+ *
16
+ * If allow: false, the tool call is blocked and message is returned as the tool result.
17
+ * If hook exits non-zero, it is logged as a warning but does not crash the session.
18
+ */
19
+ import { existsSync, readFileSync } from "fs";
20
+ import { join } from "path";
21
+ import { homedir } from "os";
22
+ import { spawn } from "child_process";
23
+ const DEFAULT_TIMEOUT_MS = 10_000;
24
+ // ============================================================================
25
+ // GLOB MATCHING
26
+ // ============================================================================
27
+ /**
28
+ * Match a tool name against a simple glob pattern.
29
+ * Supports * (any chars) and ? (single char).
30
+ */
31
+ export function matchGlob(pattern, name) {
32
+ // Escape regex special chars except * and ?
33
+ const regex = pattern
34
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
35
+ .replace(/\*/g, ".*")
36
+ .replace(/\?/g, ".");
37
+ return new RegExp(`^${regex}$`).test(name);
38
+ }
39
+ // ============================================================================
40
+ // LOAD HOOKS
41
+ // ============================================================================
42
+ const VALID_EVENTS = [
43
+ "BeforeTool",
44
+ "AfterTool",
45
+ "SessionStart",
46
+ "SessionEnd",
47
+ "Notification",
48
+ ];
49
+ /**
50
+ * Load hooks from project (.whale/hooks.json) and user (~/.swagmanager/hooks.json).
51
+ * Project hooks come first in the array, then user hooks.
52
+ * Invalid files are skipped with a warning.
53
+ */
54
+ export function loadHooks(cwd) {
55
+ const hooks = [];
56
+ // Project hooks (higher priority, run first)
57
+ const projectPath = join(cwd, ".whale", "hooks.json");
58
+ const projectHooks = loadHooksFromFile(projectPath);
59
+ if (projectHooks)
60
+ hooks.push(...projectHooks);
61
+ // User hooks
62
+ const userPath = join(homedir(), ".swagmanager", "hooks.json");
63
+ const userHooks = loadHooksFromFile(userPath);
64
+ if (userHooks)
65
+ hooks.push(...userHooks);
66
+ return hooks;
67
+ }
68
+ function loadHooksFromFile(filePath) {
69
+ if (!existsSync(filePath))
70
+ return null;
71
+ try {
72
+ const raw = readFileSync(filePath, "utf-8");
73
+ const parsed = JSON.parse(raw);
74
+ if (!Array.isArray(parsed)) {
75
+ console.error(`[hooks] Warning: ${filePath} should be a JSON array, skipping`);
76
+ return null;
77
+ }
78
+ // Validate each hook config
79
+ return parsed.filter((h) => {
80
+ if (!h || typeof h !== "object")
81
+ return false;
82
+ const hook = h;
83
+ if (typeof hook.event !== "string" || typeof hook.command !== "string") {
84
+ console.error(`[hooks] Warning: Invalid hook in ${filePath}, missing event or command`);
85
+ return false;
86
+ }
87
+ if (!VALID_EVENTS.includes(hook.event)) {
88
+ console.error(`[hooks] Warning: Invalid event "${hook.event}" in ${filePath}`);
89
+ return false;
90
+ }
91
+ return true;
92
+ });
93
+ }
94
+ catch (err) {
95
+ console.error(`[hooks] Warning: Failed to parse ${filePath}: ${err instanceof Error ? err.message : err}`);
96
+ return null;
97
+ }
98
+ }
99
+ // ============================================================================
100
+ // EXECUTE HOOK
101
+ // ============================================================================
102
+ /**
103
+ * Execute a hook command with JSON payload on stdin.
104
+ * Returns parsed JSON response from stdout, or null on error/timeout.
105
+ */
106
+ export async function executeHook(hook, payload) {
107
+ const timeout = hook.timeout ?? DEFAULT_TIMEOUT_MS;
108
+ return new Promise((resolve) => {
109
+ let stdout = "";
110
+ let stderr = "";
111
+ let settled = false;
112
+ const child = spawn("/bin/sh", ["-c", hook.command], {
113
+ stdio: ["pipe", "pipe", "pipe"],
114
+ env: { ...process.env },
115
+ });
116
+ let timer;
117
+ const settle = (result) => {
118
+ if (settled)
119
+ return;
120
+ settled = true;
121
+ if (timer)
122
+ clearTimeout(timer);
123
+ resolve(result);
124
+ };
125
+ // Kill on timeout
126
+ timer = setTimeout(() => {
127
+ if (!settled) {
128
+ console.error(`[hooks] Warning: Hook "${hook.command}" timed out after ${timeout}ms, killing`);
129
+ try {
130
+ child.kill("SIGKILL");
131
+ }
132
+ catch {
133
+ /* ignore */
134
+ }
135
+ settle(null);
136
+ }
137
+ }, timeout);
138
+ child.stdout?.on("data", (data) => {
139
+ stdout += data.toString();
140
+ });
141
+ child.stderr?.on("data", (data) => {
142
+ stderr += data.toString();
143
+ });
144
+ child.on("error", (err) => {
145
+ console.error(`[hooks] Warning: Hook "${hook.command}" failed to start: ${err.message}`);
146
+ settle(null);
147
+ });
148
+ child.on("close", (code) => {
149
+ if (code !== 0) {
150
+ if (stderr) {
151
+ console.error(`[hooks] Warning: Hook "${hook.command}" exited with code ${code}: ${stderr.trim()}`);
152
+ }
153
+ else {
154
+ console.error(`[hooks] Warning: Hook "${hook.command}" exited with code ${code}`);
155
+ }
156
+ settle(null);
157
+ return;
158
+ }
159
+ // Parse stdout as JSON
160
+ const trimmed = stdout.trim();
161
+ if (!trimmed) {
162
+ settle(null);
163
+ return;
164
+ }
165
+ try {
166
+ settle(JSON.parse(trimmed));
167
+ }
168
+ catch {
169
+ // Non-JSON output is not an error, just no structured response
170
+ settle(null);
171
+ }
172
+ });
173
+ // Write payload to stdin
174
+ try {
175
+ child.stdin?.write(JSON.stringify(payload));
176
+ child.stdin?.end();
177
+ }
178
+ catch {
179
+ // stdin may already be closed — that is fine
180
+ }
181
+ });
182
+ }
183
+ // ============================================================================
184
+ // PUBLIC API
185
+ // ============================================================================
186
+ /**
187
+ * Run BeforeTool hooks for a given tool call.
188
+ * Returns whether the tool call should proceed and optionally modified input.
189
+ */
190
+ export async function runBeforeToolHook(hooks, toolName, input) {
191
+ const matching = hooks.filter((h) => h.event === "BeforeTool" &&
192
+ (!h.pattern || matchGlob(h.pattern, toolName)));
193
+ if (matching.length === 0)
194
+ return { allow: true };
195
+ const payload = {
196
+ event: "BeforeTool",
197
+ tool_name: toolName,
198
+ tool_input: input,
199
+ };
200
+ for (const hook of matching) {
201
+ const response = await executeHook(hook, payload);
202
+ if (!response)
203
+ continue;
204
+ // If any hook blocks, stop immediately
205
+ if (response.allow === false) {
206
+ return {
207
+ allow: false,
208
+ message: response.message || `Blocked by hook: ${hook.command}`,
209
+ };
210
+ }
211
+ // If hook modifies input and is allowed to
212
+ if (hook.allowModify && response.modified_input) {
213
+ return { allow: true, modifiedInput: response.modified_input };
214
+ }
215
+ }
216
+ return { allow: true };
217
+ }
218
+ /**
219
+ * Run AfterTool hooks for a given tool result.
220
+ * Returns optionally modified output.
221
+ */
222
+ export async function runAfterToolHook(hooks, toolName, output, success) {
223
+ const matching = hooks.filter((h) => h.event === "AfterTool" &&
224
+ (!h.pattern || matchGlob(h.pattern, toolName)));
225
+ if (matching.length === 0)
226
+ return {};
227
+ const payload = {
228
+ event: "AfterTool",
229
+ tool_name: toolName,
230
+ tool_output: output,
231
+ tool_success: success,
232
+ };
233
+ for (const hook of matching) {
234
+ const response = await executeHook(hook, payload);
235
+ if (!response)
236
+ continue;
237
+ // If hook modifies output and is allowed to
238
+ if (hook.allowModify && response.modified_output !== undefined) {
239
+ return { modifiedOutput: response.modified_output };
240
+ }
241
+ }
242
+ return {};
243
+ }
244
+ /**
245
+ * Run session lifecycle hooks (SessionStart, SessionEnd, Notification).
246
+ * Fire-and-forget — errors are logged but don't affect session.
247
+ */
248
+ export async function runSessionHook(hooks, event, data) {
249
+ const matching = hooks.filter((h) => h.event === event);
250
+ if (matching.length === 0)
251
+ return;
252
+ const payload = {
253
+ event,
254
+ ...(data || {}),
255
+ };
256
+ // Run all session hooks concurrently — don't wait or fail
257
+ await Promise.allSettled(matching.map((hook) => executeHook(hook, payload)));
258
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Interactive Tools — user interaction during agent execution
3
+ *
4
+ * - AskUserQuestion: Multi-choice questions with optional custom input
5
+ * - Plan Mode: Structured planning workflow with user approval
6
+ */
7
+ import { EventEmitter } from "events";
8
+ export interface QuestionOption {
9
+ label: string;
10
+ description: string;
11
+ }
12
+ export interface Question {
13
+ question: string;
14
+ header: string;
15
+ options: QuestionOption[];
16
+ multiSelect: boolean;
17
+ }
18
+ export interface QuestionRequest {
19
+ id: string;
20
+ questions: Question[];
21
+ resolve: (answers: Record<string, string | string[]>) => void;
22
+ reject: (error: Error) => void;
23
+ }
24
+ export interface PlanModeState {
25
+ active: boolean;
26
+ planFile?: string;
27
+ planContent?: string;
28
+ startedAt?: Date;
29
+ }
30
+ export declare const interactiveEvents: EventEmitter<[never]>;
31
+ export declare function createQuestionRequest(questions: Question[]): QuestionRequest;
32
+ export declare function getPendingQuestion(): QuestionRequest | undefined;
33
+ export declare function resolveQuestion(id: string, answers: Record<string, string | string[]>): boolean;
34
+ export declare function rejectQuestion(id: string, error: Error): boolean;
35
+ export declare function enterPlanMode(planFile?: string): {
36
+ success: boolean;
37
+ message: string;
38
+ };
39
+ export declare function exitPlanMode(): {
40
+ success: boolean;
41
+ message: string;
42
+ };
43
+ export declare function isPlanMode(): boolean;
44
+ export declare function getPlanModeState(): PlanModeState;
45
+ export declare const INTERACTIVE_TOOL_DEFINITIONS: ({
46
+ name: string;
47
+ description: string;
48
+ input_schema: {
49
+ type: string;
50
+ properties: {
51
+ questions: {
52
+ type: string;
53
+ description: string;
54
+ items: {
55
+ type: string;
56
+ properties: {
57
+ question: {
58
+ type: string;
59
+ description: string;
60
+ };
61
+ header: {
62
+ type: string;
63
+ description: string;
64
+ };
65
+ options: {
66
+ type: string;
67
+ description: string;
68
+ items: {
69
+ type: string;
70
+ properties: {
71
+ label: {
72
+ type: string;
73
+ description: string;
74
+ };
75
+ description: {
76
+ type: string;
77
+ description: string;
78
+ };
79
+ };
80
+ required: string[];
81
+ };
82
+ };
83
+ multiSelect: {
84
+ type: string;
85
+ description: string;
86
+ };
87
+ };
88
+ required: string[];
89
+ };
90
+ };
91
+ plan_file?: undefined;
92
+ };
93
+ required: string[];
94
+ };
95
+ } | {
96
+ name: string;
97
+ description: string;
98
+ input_schema: {
99
+ type: string;
100
+ properties: {
101
+ plan_file: {
102
+ type: string;
103
+ description: string;
104
+ };
105
+ questions?: undefined;
106
+ };
107
+ required: never[];
108
+ };
109
+ } | {
110
+ name: string;
111
+ description: string;
112
+ input_schema: {
113
+ type: string;
114
+ properties: {
115
+ questions?: undefined;
116
+ plan_file?: undefined;
117
+ };
118
+ required: never[];
119
+ };
120
+ })[];
121
+ export declare function executeInteractiveTool(name: string, input: Record<string, unknown>): Promise<{
122
+ success: boolean;
123
+ output: string;
124
+ pendingQuestion?: QuestionRequest;
125
+ }>;
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Interactive Tools — user interaction during agent execution
3
+ *
4
+ * - AskUserQuestion: Multi-choice questions with optional custom input
5
+ * - Plan Mode: Structured planning workflow with user approval
6
+ */
7
+ import { EventEmitter } from "events";
8
+ import { readFileSync, existsSync } from "fs";
9
+ import { resolve } from "path";
10
+ // ============================================================================
11
+ // GLOBAL STATE
12
+ // ============================================================================
13
+ // Pending question requests (resolved by UI)
14
+ const pendingQuestions = new Map();
15
+ // Plan mode state
16
+ let planModeState = { active: false };
17
+ // Event emitter for UI coordination
18
+ export const interactiveEvents = new EventEmitter();
19
+ // ============================================================================
20
+ // ASK USER QUESTION
21
+ // ============================================================================
22
+ export function createQuestionRequest(questions) {
23
+ const id = `question-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
24
+ return new Promise((resolve, reject) => {
25
+ const request = {
26
+ id,
27
+ questions,
28
+ resolve,
29
+ reject,
30
+ };
31
+ pendingQuestions.set(id, request);
32
+ // Emit event for UI to pick up
33
+ interactiveEvents.emit("question", request);
34
+ });
35
+ }
36
+ export function getPendingQuestion() {
37
+ // Return first pending question
38
+ return pendingQuestions.values().next().value;
39
+ }
40
+ export function resolveQuestion(id, answers) {
41
+ const request = pendingQuestions.get(id);
42
+ if (!request)
43
+ return false;
44
+ pendingQuestions.delete(id);
45
+ request.resolve(answers);
46
+ return true;
47
+ }
48
+ export function rejectQuestion(id, error) {
49
+ const request = pendingQuestions.get(id);
50
+ if (!request)
51
+ return false;
52
+ pendingQuestions.delete(id);
53
+ request.reject(error);
54
+ return true;
55
+ }
56
+ // ============================================================================
57
+ // PLAN MODE
58
+ // ============================================================================
59
+ export function enterPlanMode(planFile) {
60
+ if (planModeState.active) {
61
+ return {
62
+ success: false,
63
+ message: "Already in plan mode. Use ExitPlanMode to finish planning.",
64
+ };
65
+ }
66
+ planModeState = {
67
+ active: true,
68
+ planFile: planFile || ".whale/plan.md",
69
+ startedAt: new Date(),
70
+ };
71
+ interactiveEvents.emit("planModeEntered", planModeState);
72
+ return {
73
+ success: true,
74
+ message: `Entered plan mode. Write your plan to ${planModeState.planFile}, then use ExitPlanMode when ready for approval.`,
75
+ };
76
+ }
77
+ export function exitPlanMode() {
78
+ if (!planModeState.active) {
79
+ return {
80
+ success: false,
81
+ message: "Not in plan mode. Use EnterPlanMode first.",
82
+ };
83
+ }
84
+ const planFile = planModeState.planFile || ".whale/plan.md";
85
+ planModeState = { active: false };
86
+ interactiveEvents.emit("planModeExited", { planFile });
87
+ // Read the plan file to display its content
88
+ const fullPath = resolve(process.cwd(), planFile);
89
+ let planContent = "";
90
+ if (existsSync(fullPath)) {
91
+ try {
92
+ planContent = readFileSync(fullPath, "utf-8").trim();
93
+ }
94
+ catch { /* ignore read errors */ }
95
+ }
96
+ if (planContent) {
97
+ return {
98
+ success: true,
99
+ message: planContent,
100
+ };
101
+ }
102
+ return {
103
+ success: true,
104
+ message: `Plan mode complete. Plan saved to ${planFile}.`,
105
+ };
106
+ }
107
+ export function isPlanMode() {
108
+ return planModeState.active;
109
+ }
110
+ export function getPlanModeState() {
111
+ return { ...planModeState };
112
+ }
113
+ // ============================================================================
114
+ // TOOL DEFINITIONS
115
+ // ============================================================================
116
+ export const INTERACTIVE_TOOL_DEFINITIONS = [
117
+ {
118
+ name: "ask_user_question",
119
+ description: `Ask the user questions during execution. Use this to:
120
+ - Gather user preferences or requirements
121
+ - Clarify ambiguous instructions
122
+ - Get decisions on implementation choices
123
+ - Offer choices about direction
124
+
125
+ Users can always select "Other" to provide custom input.`,
126
+ input_schema: {
127
+ type: "object",
128
+ properties: {
129
+ questions: {
130
+ type: "array",
131
+ description: "1-4 questions to ask the user",
132
+ items: {
133
+ type: "object",
134
+ properties: {
135
+ question: {
136
+ type: "string",
137
+ description: "The complete question to ask (ends with ?)",
138
+ },
139
+ header: {
140
+ type: "string",
141
+ description: "Short label (max 12 chars) like 'Auth method' or 'Library'",
142
+ },
143
+ options: {
144
+ type: "array",
145
+ description: "2-4 choices (Other is added automatically)",
146
+ items: {
147
+ type: "object",
148
+ properties: {
149
+ label: {
150
+ type: "string",
151
+ description: "Concise choice text (1-5 words)",
152
+ },
153
+ description: {
154
+ type: "string",
155
+ description: "Explanation of what this choice means",
156
+ },
157
+ },
158
+ required: ["label", "description"],
159
+ },
160
+ },
161
+ multiSelect: {
162
+ type: "boolean",
163
+ description: "Allow selecting multiple options (default: false)",
164
+ },
165
+ },
166
+ required: ["question", "header", "options"],
167
+ },
168
+ },
169
+ },
170
+ required: ["questions"],
171
+ },
172
+ },
173
+ {
174
+ name: "enter_plan_mode",
175
+ description: `Enter plan mode for complex tasks requiring careful planning before implementation.
176
+
177
+ Use this when:
178
+ - Multiple valid approaches exist with trade-offs
179
+ - Significant architectural decisions are needed
180
+ - Large-scale changes touch many files
181
+ - Requirements are unclear and need exploration
182
+ - You need to ask clarifying questions before starting
183
+
184
+ In plan mode, you explore the codebase and design an approach, then present it for user approval before implementing.`,
185
+ input_schema: {
186
+ type: "object",
187
+ properties: {
188
+ plan_file: {
189
+ type: "string",
190
+ description: "File to write the plan to (default: .whale/plan.md)",
191
+ },
192
+ },
193
+ required: [],
194
+ },
195
+ },
196
+ {
197
+ name: "exit_plan_mode",
198
+ description: `Exit plan mode after writing your plan. The user will review and approve the plan before implementation begins.
199
+
200
+ Only use this after you have:
201
+ 1. Thoroughly explored the codebase
202
+ 2. Written a clear plan to the plan file
203
+ 3. Resolved any ambiguities with the user`,
204
+ input_schema: {
205
+ type: "object",
206
+ properties: {},
207
+ required: [],
208
+ },
209
+ },
210
+ ];
211
+ // ============================================================================
212
+ // EXECUTE INTERACTIVE TOOLS
213
+ // ============================================================================
214
+ export async function executeInteractiveTool(name, input) {
215
+ switch (name) {
216
+ case "ask_user_question": {
217
+ const questions = input.questions;
218
+ if (!Array.isArray(questions) || questions.length === 0) {
219
+ return { success: false, output: "questions array is required" };
220
+ }
221
+ if (questions.length > 4) {
222
+ return { success: false, output: "Maximum 4 questions allowed" };
223
+ }
224
+ // Create the request — UI will handle it
225
+ const id = `question-${Date.now()}`;
226
+ const request = {
227
+ id,
228
+ questions: questions.map((q) => ({
229
+ question: q.question,
230
+ header: q.header?.slice(0, 12) || "Question",
231
+ options: (q.options || []).slice(0, 4).map((o) => ({
232
+ label: o.label,
233
+ description: o.description || "",
234
+ })),
235
+ multiSelect: q.multiSelect || false,
236
+ })),
237
+ resolve: () => { },
238
+ reject: () => { },
239
+ };
240
+ // Store for UI to pick up
241
+ pendingQuestions.set(id, request);
242
+ interactiveEvents.emit("question", request);
243
+ return {
244
+ success: true,
245
+ output: `Question pending: ${questions[0].question}`,
246
+ pendingQuestion: request,
247
+ };
248
+ }
249
+ case "enter_plan_mode": {
250
+ const result = enterPlanMode(input.plan_file);
251
+ return { success: result.success, output: result.message };
252
+ }
253
+ case "exit_plan_mode": {
254
+ const result = exitPlanMode();
255
+ return { success: result.success, output: result.message };
256
+ }
257
+ default:
258
+ return { success: false, output: `Unknown interactive tool: ${name}` };
259
+ }
260
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Keybinding Manager — configurable keyboard shortcuts
3
+ *
4
+ * Loads overrides from ~/.swagmanager/keybindings.json.
5
+ * Only listed bindings are configurable; Enter, backspace, arrows stay hardcoded.
6
+ */
7
+ export interface KeybindingConfig {
8
+ cancel_stream?: string;
9
+ toggle_expand?: string;
10
+ toggle_thinking?: string;
11
+ exit?: string;
12
+ clear_line?: string;
13
+ delete_word?: string;
14
+ home?: string;
15
+ }
16
+ export interface InkKey {
17
+ ctrl: boolean;
18
+ meta: boolean;
19
+ shift: boolean;
20
+ escape: boolean;
21
+ return: boolean;
22
+ tab: boolean;
23
+ backspace: boolean;
24
+ delete: boolean;
25
+ upArrow: boolean;
26
+ downArrow: boolean;
27
+ leftArrow: boolean;
28
+ rightArrow: boolean;
29
+ pageUp: boolean;
30
+ pageDown: boolean;
31
+ }
32
+ export declare function loadKeybindings(): Required<KeybindingConfig>;
33
+ /**
34
+ * Check if an Ink useInput event matches a binding string.
35
+ *
36
+ * Binding format: "ctrl+<key>", "escape", "meta+<key>", or single char.
37
+ * Examples: "ctrl+e", "escape", "ctrl+c", "ctrl+t"
38
+ */
39
+ export declare function matchesBinding(binding: string, input: string, key: Partial<InkKey>): boolean;
40
+ /**
41
+ * Convert a binding string to a raw control character for stdin matching.
42
+ * Returns the string as-is if not a ctrl+<key> binding.
43
+ *
44
+ * "ctrl+u" -> "\x15"
45
+ * "ctrl+w" -> "\x17"
46
+ * "escape" -> "\x1b"
47
+ */
48
+ export declare function bindingToControlChar(binding: string): string;
49
+ /**
50
+ * Reset cached keybindings (for testing or hot-reload).
51
+ */
52
+ export declare function resetKeybindingCache(): void;