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.
- package/README.md +95 -0
- package/bin/swag-agent.js +9 -0
- package/bin/swagmanager-mcp.js +321 -0
- package/dist/cli/app.d.ts +26 -0
- package/dist/cli/app.js +64 -0
- package/dist/cli/chat/AgentSelector.d.ts +14 -0
- package/dist/cli/chat/AgentSelector.js +14 -0
- package/dist/cli/chat/ChatApp.d.ts +9 -0
- package/dist/cli/chat/ChatApp.js +267 -0
- package/dist/cli/chat/ChatInput.d.ts +39 -0
- package/dist/cli/chat/ChatInput.js +509 -0
- package/dist/cli/chat/MarkdownText.d.ts +10 -0
- package/dist/cli/chat/MarkdownText.js +20 -0
- package/dist/cli/chat/MessageList.d.ts +37 -0
- package/dist/cli/chat/MessageList.js +80 -0
- package/dist/cli/chat/ModelSelector.d.ts +20 -0
- package/dist/cli/chat/ModelSelector.js +73 -0
- package/dist/cli/chat/RewindViewer.d.ts +26 -0
- package/dist/cli/chat/RewindViewer.js +185 -0
- package/dist/cli/chat/StoreSelector.d.ts +14 -0
- package/dist/cli/chat/StoreSelector.js +24 -0
- package/dist/cli/chat/StreamingText.d.ts +12 -0
- package/dist/cli/chat/StreamingText.js +12 -0
- package/dist/cli/chat/SubagentPanel.d.ts +45 -0
- package/dist/cli/chat/SubagentPanel.js +110 -0
- package/dist/cli/chat/TeamPanel.d.ts +21 -0
- package/dist/cli/chat/TeamPanel.js +42 -0
- package/dist/cli/chat/ToolIndicator.d.ts +25 -0
- package/dist/cli/chat/ToolIndicator.js +436 -0
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
- package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
- package/dist/cli/commands/config-cmd.d.ts +10 -0
- package/dist/cli/commands/config-cmd.js +99 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +172 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +278 -0
- package/dist/cli/commands/mcp.d.ts +12 -0
- package/dist/cli/commands/mcp.js +162 -0
- package/dist/cli/login/LoginApp.d.ts +7 -0
- package/dist/cli/login/LoginApp.js +157 -0
- package/dist/cli/print-mode.d.ts +31 -0
- package/dist/cli/print-mode.js +202 -0
- package/dist/cli/serve-mode.d.ts +37 -0
- package/dist/cli/serve-mode.js +636 -0
- package/dist/cli/services/agent-definitions.d.ts +25 -0
- package/dist/cli/services/agent-definitions.js +91 -0
- package/dist/cli/services/agent-events.d.ts +178 -0
- package/dist/cli/services/agent-events.js +175 -0
- package/dist/cli/services/agent-loop.d.ts +90 -0
- package/dist/cli/services/agent-loop.js +762 -0
- package/dist/cli/services/agent-worker-base.d.ts +97 -0
- package/dist/cli/services/agent-worker-base.js +220 -0
- package/dist/cli/services/auth-service.d.ts +30 -0
- package/dist/cli/services/auth-service.js +160 -0
- package/dist/cli/services/background-processes.d.ts +126 -0
- package/dist/cli/services/background-processes.js +318 -0
- package/dist/cli/services/browser-auth.d.ts +24 -0
- package/dist/cli/services/browser-auth.js +180 -0
- package/dist/cli/services/claude-md-loader.d.ts +16 -0
- package/dist/cli/services/claude-md-loader.js +58 -0
- package/dist/cli/services/config-store.d.ts +47 -0
- package/dist/cli/services/config-store.js +79 -0
- package/dist/cli/services/debug-log.d.ts +10 -0
- package/dist/cli/services/debug-log.js +52 -0
- package/dist/cli/services/error-logger.d.ts +58 -0
- package/dist/cli/services/error-logger.js +269 -0
- package/dist/cli/services/file-history.d.ts +21 -0
- package/dist/cli/services/file-history.js +83 -0
- package/dist/cli/services/format-server-response.d.ts +16 -0
- package/dist/cli/services/format-server-response.js +440 -0
- package/dist/cli/services/git-context.d.ts +11 -0
- package/dist/cli/services/git-context.js +66 -0
- package/dist/cli/services/hooks.d.ts +85 -0
- package/dist/cli/services/hooks.js +258 -0
- package/dist/cli/services/interactive-tools.d.ts +125 -0
- package/dist/cli/services/interactive-tools.js +260 -0
- package/dist/cli/services/keybinding-manager.d.ts +52 -0
- package/dist/cli/services/keybinding-manager.js +115 -0
- package/dist/cli/services/local-tools.d.ts +22 -0
- package/dist/cli/services/local-tools.js +697 -0
- package/dist/cli/services/lsp-manager.d.ts +18 -0
- package/dist/cli/services/lsp-manager.js +717 -0
- package/dist/cli/services/mcp-client.d.ts +48 -0
- package/dist/cli/services/mcp-client.js +157 -0
- package/dist/cli/services/memory-manager.d.ts +16 -0
- package/dist/cli/services/memory-manager.js +57 -0
- package/dist/cli/services/model-manager.d.ts +18 -0
- package/dist/cli/services/model-manager.js +71 -0
- package/dist/cli/services/model-router.d.ts +26 -0
- package/dist/cli/services/model-router.js +149 -0
- package/dist/cli/services/permission-modes.d.ts +13 -0
- package/dist/cli/services/permission-modes.js +43 -0
- package/dist/cli/services/rewind.d.ts +84 -0
- package/dist/cli/services/rewind.js +194 -0
- package/dist/cli/services/ripgrep.d.ts +28 -0
- package/dist/cli/services/ripgrep.js +138 -0
- package/dist/cli/services/sandbox.d.ts +29 -0
- package/dist/cli/services/sandbox.js +97 -0
- package/dist/cli/services/server-tools.d.ts +61 -0
- package/dist/cli/services/server-tools.js +543 -0
- package/dist/cli/services/session-persistence.d.ts +23 -0
- package/dist/cli/services/session-persistence.js +99 -0
- package/dist/cli/services/subagent-worker.d.ts +19 -0
- package/dist/cli/services/subagent-worker.js +41 -0
- package/dist/cli/services/subagent.d.ts +47 -0
- package/dist/cli/services/subagent.js +647 -0
- package/dist/cli/services/system-prompt.d.ts +7 -0
- package/dist/cli/services/system-prompt.js +198 -0
- package/dist/cli/services/team-lead.d.ts +73 -0
- package/dist/cli/services/team-lead.js +512 -0
- package/dist/cli/services/team-state.d.ts +77 -0
- package/dist/cli/services/team-state.js +398 -0
- package/dist/cli/services/teammate.d.ts +31 -0
- package/dist/cli/services/teammate.js +689 -0
- package/dist/cli/services/telemetry.d.ts +61 -0
- package/dist/cli/services/telemetry.js +209 -0
- package/dist/cli/services/tools/agent-tools.d.ts +14 -0
- package/dist/cli/services/tools/agent-tools.js +347 -0
- package/dist/cli/services/tools/file-ops.d.ts +15 -0
- package/dist/cli/services/tools/file-ops.js +487 -0
- package/dist/cli/services/tools/search-tools.d.ts +8 -0
- package/dist/cli/services/tools/search-tools.js +186 -0
- package/dist/cli/services/tools/shell-exec.d.ts +10 -0
- package/dist/cli/services/tools/shell-exec.js +168 -0
- package/dist/cli/services/tools/task-manager.d.ts +28 -0
- package/dist/cli/services/tools/task-manager.js +209 -0
- package/dist/cli/services/tools/web-tools.d.ts +11 -0
- package/dist/cli/services/tools/web-tools.js +395 -0
- package/dist/cli/setup/SetupApp.d.ts +9 -0
- package/dist/cli/setup/SetupApp.js +191 -0
- package/dist/cli/shared/MatrixIntro.d.ts +4 -0
- package/dist/cli/shared/MatrixIntro.js +83 -0
- package/dist/cli/shared/Theme.d.ts +74 -0
- package/dist/cli/shared/Theme.js +127 -0
- package/dist/cli/shared/WhaleBanner.d.ts +10 -0
- package/dist/cli/shared/WhaleBanner.js +12 -0
- package/dist/cli/shared/markdown.d.ts +21 -0
- package/dist/cli/shared/markdown.js +756 -0
- package/dist/cli/status/StatusApp.d.ts +4 -0
- package/dist/cli/status/StatusApp.js +105 -0
- package/dist/cli/stores/StoreApp.d.ts +7 -0
- package/dist/cli/stores/StoreApp.js +81 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +538 -0
- package/dist/local-agent/connection.d.ts +48 -0
- package/dist/local-agent/connection.js +332 -0
- package/dist/local-agent/discovery.d.ts +18 -0
- package/dist/local-agent/discovery.js +146 -0
- package/dist/local-agent/executor.d.ts +34 -0
- package/dist/local-agent/executor.js +241 -0
- package/dist/local-agent/index.d.ts +14 -0
- package/dist/local-agent/index.js +198 -0
- package/dist/node/adapters/base.d.ts +35 -0
- package/dist/node/adapters/base.js +10 -0
- package/dist/node/adapters/discord.d.ts +29 -0
- package/dist/node/adapters/discord.js +299 -0
- package/dist/node/adapters/email.d.ts +23 -0
- package/dist/node/adapters/email.js +218 -0
- package/dist/node/adapters/imessage.d.ts +17 -0
- package/dist/node/adapters/imessage.js +118 -0
- package/dist/node/adapters/slack.d.ts +26 -0
- package/dist/node/adapters/slack.js +259 -0
- package/dist/node/adapters/sms.d.ts +23 -0
- package/dist/node/adapters/sms.js +161 -0
- package/dist/node/adapters/telegram.d.ts +17 -0
- package/dist/node/adapters/telegram.js +101 -0
- package/dist/node/adapters/webchat.d.ts +27 -0
- package/dist/node/adapters/webchat.js +160 -0
- package/dist/node/adapters/whatsapp.d.ts +28 -0
- package/dist/node/adapters/whatsapp.js +230 -0
- package/dist/node/cli.d.ts +2 -0
- package/dist/node/cli.js +325 -0
- package/dist/node/config.d.ts +17 -0
- package/dist/node/config.js +31 -0
- package/dist/node/runtime.d.ts +50 -0
- package/dist/node/runtime.js +351 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
- package/dist/server/handlers/analytics.d.ts +17 -0
- package/dist/server/handlers/analytics.js +266 -0
- package/dist/server/handlers/api-keys.d.ts +6 -0
- package/dist/server/handlers/api-keys.js +221 -0
- package/dist/server/handlers/billing.d.ts +33 -0
- package/dist/server/handlers/billing.js +272 -0
- package/dist/server/handlers/browser.d.ts +10 -0
- package/dist/server/handlers/browser.js +517 -0
- package/dist/server/handlers/catalog.d.ts +99 -0
- package/dist/server/handlers/catalog.js +976 -0
- package/dist/server/handlers/comms.d.ts +254 -0
- package/dist/server/handlers/comms.js +588 -0
- package/dist/server/handlers/creations.d.ts +6 -0
- package/dist/server/handlers/creations.js +479 -0
- package/dist/server/handlers/crm.d.ts +89 -0
- package/dist/server/handlers/crm.js +538 -0
- package/dist/server/handlers/discovery.d.ts +6 -0
- package/dist/server/handlers/discovery.js +288 -0
- package/dist/server/handlers/embeddings.d.ts +92 -0
- package/dist/server/handlers/embeddings.js +197 -0
- package/dist/server/handlers/enrichment.d.ts +8 -0
- package/dist/server/handlers/enrichment.js +768 -0
- package/dist/server/handlers/image-gen.d.ts +6 -0
- package/dist/server/handlers/image-gen.js +409 -0
- package/dist/server/handlers/inventory.d.ts +319 -0
- package/dist/server/handlers/inventory.js +447 -0
- package/dist/server/handlers/kali.d.ts +10 -0
- package/dist/server/handlers/kali.js +210 -0
- package/dist/server/handlers/llm-providers.d.ts +6 -0
- package/dist/server/handlers/llm-providers.js +673 -0
- package/dist/server/handlers/local-agent.d.ts +6 -0
- package/dist/server/handlers/local-agent.js +118 -0
- package/dist/server/handlers/meta-ads.d.ts +111 -0
- package/dist/server/handlers/meta-ads.js +2279 -0
- package/dist/server/handlers/nodes.d.ts +33 -0
- package/dist/server/handlers/nodes.js +699 -0
- package/dist/server/handlers/operations.d.ts +138 -0
- package/dist/server/handlers/operations.js +131 -0
- package/dist/server/handlers/platform.d.ts +23 -0
- package/dist/server/handlers/platform.js +227 -0
- package/dist/server/handlers/supply-chain.d.ts +19 -0
- package/dist/server/handlers/supply-chain.js +327 -0
- package/dist/server/handlers/transcription.d.ts +17 -0
- package/dist/server/handlers/transcription.js +121 -0
- package/dist/server/handlers/video-gen.d.ts +6 -0
- package/dist/server/handlers/video-gen.js +466 -0
- package/dist/server/handlers/voice.d.ts +8 -0
- package/dist/server/handlers/voice.js +1146 -0
- package/dist/server/handlers/workflow-steps.d.ts +86 -0
- package/dist/server/handlers/workflow-steps.js +2349 -0
- package/dist/server/handlers/workflows.d.ts +7 -0
- package/dist/server/handlers/workflows.js +989 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2427 -0
- package/dist/server/lib/batch-client.d.ts +80 -0
- package/dist/server/lib/batch-client.js +467 -0
- package/dist/server/lib/code-worker-pool.d.ts +31 -0
- package/dist/server/lib/code-worker-pool.js +224 -0
- package/dist/server/lib/code-worker.d.ts +1 -0
- package/dist/server/lib/code-worker.js +188 -0
- package/dist/server/lib/compaction-service.d.ts +32 -0
- package/dist/server/lib/compaction-service.js +162 -0
- package/dist/server/lib/logger.d.ts +19 -0
- package/dist/server/lib/logger.js +46 -0
- package/dist/server/lib/otel.d.ts +38 -0
- package/dist/server/lib/otel.js +126 -0
- package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
- package/dist/server/lib/pg-rate-limiter.js +86 -0
- package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
- package/dist/server/lib/prompt-sanitizer.js +177 -0
- package/dist/server/lib/provider-capabilities.d.ts +85 -0
- package/dist/server/lib/provider-capabilities.js +190 -0
- package/dist/server/lib/provider-failover.d.ts +74 -0
- package/dist/server/lib/provider-failover.js +210 -0
- package/dist/server/lib/rate-limiter.d.ts +39 -0
- package/dist/server/lib/rate-limiter.js +147 -0
- package/dist/server/lib/server-agent-loop.d.ts +107 -0
- package/dist/server/lib/server-agent-loop.js +667 -0
- package/dist/server/lib/server-subagent.d.ts +78 -0
- package/dist/server/lib/server-subagent.js +203 -0
- package/dist/server/lib/session-checkpoint.d.ts +51 -0
- package/dist/server/lib/session-checkpoint.js +145 -0
- package/dist/server/lib/ssrf-guard.d.ts +13 -0
- package/dist/server/lib/ssrf-guard.js +240 -0
- package/dist/server/lib/supabase-client.d.ts +7 -0
- package/dist/server/lib/supabase-client.js +78 -0
- package/dist/server/lib/template-resolver.d.ts +31 -0
- package/dist/server/lib/template-resolver.js +215 -0
- package/dist/server/lib/utils.d.ts +16 -0
- package/dist/server/lib/utils.js +147 -0
- package/dist/server/local-agent-gateway.d.ts +82 -0
- package/dist/server/local-agent-gateway.js +426 -0
- package/dist/server/providers/anthropic.d.ts +20 -0
- package/dist/server/providers/anthropic.js +199 -0
- package/dist/server/providers/bedrock.d.ts +20 -0
- package/dist/server/providers/bedrock.js +194 -0
- package/dist/server/providers/gemini.d.ts +24 -0
- package/dist/server/providers/gemini.js +486 -0
- package/dist/server/providers/openai.d.ts +24 -0
- package/dist/server/providers/openai.js +522 -0
- package/dist/server/providers/registry.d.ts +32 -0
- package/dist/server/providers/registry.js +58 -0
- package/dist/server/providers/shared.d.ts +32 -0
- package/dist/server/providers/shared.js +124 -0
- package/dist/server/providers/types.d.ts +92 -0
- package/dist/server/providers/types.js +12 -0
- package/dist/server/proxy-handlers.d.ts +6 -0
- package/dist/server/proxy-handlers.js +89 -0
- package/dist/server/tool-router.d.ts +149 -0
- package/dist/server/tool-router.js +803 -0
- package/dist/server/validation.d.ts +24 -0
- package/dist/server/validation.js +301 -0
- package/dist/server/worker.d.ts +19 -0
- package/dist/server/worker.js +201 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +181 -0
- package/dist/shared/agent-core.d.ts +157 -0
- package/dist/shared/agent-core.js +534 -0
- package/dist/shared/anthropic-types.d.ts +105 -0
- package/dist/shared/anthropic-types.js +7 -0
- package/dist/shared/api-client.d.ts +90 -0
- package/dist/shared/api-client.js +379 -0
- package/dist/shared/constants.d.ts +33 -0
- package/dist/shared/constants.js +80 -0
- package/dist/shared/sse-parser.d.ts +26 -0
- package/dist/shared/sse-parser.js +259 -0
- package/dist/shared/tool-dispatch.d.ts +52 -0
- package/dist/shared/tool-dispatch.js +191 -0
- package/dist/shared/types.d.ts +72 -0
- package/dist/shared/types.js +7 -0
- package/dist/updater.d.ts +25 -0
- package/dist/updater.js +140 -0
- package/dist/webchat/widget.d.ts +0 -0
- package/dist/webchat/widget.js +397 -0
- package/package.json +95 -0
- package/src/cli/services/builtin-skills/commit.md +19 -0
- package/src/cli/services/builtin-skills/review-pr.md +21 -0
- 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;
|