@kolisachint/hoocode-agent 0.4.106 → 0.4.107
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/CHANGELOG.md +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -4
- package/dist/cli/args.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +0 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +2 -2
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +1 -1
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/extension-mcp-servers.d.ts +1 -1
- package/dist/core/extension-mcp-servers.d.ts.map +1 -1
- package/dist/core/extension-mcp-servers.js +1 -1
- package/dist/core/extension-mcp-servers.js.map +1 -1
- package/dist/core/extensions/plugins/index.d.ts.map +1 -1
- package/dist/core/extensions/plugins/index.js +1 -1
- package/dist/core/extensions/plugins/index.js.map +1 -1
- package/dist/core/extensions/types.d.ts +1 -3
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/mode-prompts.d.ts +1 -1
- package/dist/core/mode-prompts.d.ts.map +1 -1
- package/dist/core/mode-prompts.js +1 -1
- package/dist/core/mode-prompts.js.map +1 -1
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +1 -1
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -2
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -97
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +4 -65
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +2 -3
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/telemetry.d.ts.map +1 -1
- package/dist/core/telemetry.js +1 -1
- package/dist/core/telemetry.js.map +1 -1
- package/dist/core/timings.d.ts +1 -1
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +2 -2
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/{browser-continue.d.ts → browser/browser-continue.d.ts} +1 -1
- package/dist/core/tools/browser/browser-continue.d.ts.map +1 -0
- package/dist/core/tools/{browser-continue.js → browser/browser-continue.js} +2 -2
- package/dist/core/tools/browser/browser-continue.js.map +1 -0
- package/dist/core/tools/{browser-run.d.ts → browser/browser-run.d.ts} +2 -2
- package/dist/core/tools/browser/browser-run.d.ts.map +1 -0
- package/dist/core/tools/{browser-run.js → browser/browser-run.js} +2 -2
- package/dist/core/tools/browser/browser-run.js.map +1 -0
- package/dist/core/tools/browser/browsertools-shared.d.ts.map +1 -0
- package/dist/core/tools/{browsertools-shared.js → browser/browsertools-shared.js} +1 -1
- package/dist/core/tools/browser/browsertools-shared.js.map +1 -0
- package/dist/core/tools/browser/index.d.ts +8 -0
- package/dist/core/tools/browser/index.d.ts.map +1 -0
- package/dist/core/tools/browser/index.js +8 -0
- package/dist/core/tools/browser/index.js.map +1 -0
- package/dist/core/tools/{docedit.d.ts → doc/docedit.d.ts} +2 -2
- package/dist/core/tools/doc/docedit.d.ts.map +1 -0
- package/dist/core/tools/{docedit.js → doc/docedit.js} +7 -7
- package/dist/core/tools/doc/docedit.js.map +1 -0
- package/dist/core/tools/{docgrep.d.ts → doc/docgrep.d.ts} +2 -2
- package/dist/core/tools/doc/docgrep.d.ts.map +1 -0
- package/dist/core/tools/{docgrep.js → doc/docgrep.js} +6 -6
- package/dist/core/tools/doc/docgrep.js.map +1 -0
- package/dist/core/tools/{docpeek.d.ts → doc/docpeek.d.ts} +2 -2
- package/dist/core/tools/{docpeek.js.map → doc/docpeek.d.ts.map} +1 -1
- package/dist/core/tools/{docpeek.js → doc/docpeek.js} +6 -6
- package/dist/core/tools/doc/docpeek.js.map +1 -0
- package/dist/core/tools/{docread.d.ts → doc/docread.d.ts} +2 -2
- package/dist/core/tools/doc/docread.d.ts.map +1 -0
- package/dist/core/tools/{docread.js → doc/docread.js} +6 -6
- package/dist/core/tools/doc/docread.js.map +1 -0
- package/dist/core/tools/{docscan.d.ts → doc/docscan.d.ts} +2 -2
- package/dist/core/tools/doc/docscan.d.ts.map +1 -0
- package/dist/core/tools/{docscan.js → doc/docscan.js} +6 -6
- package/dist/core/tools/doc/docscan.js.map +1 -0
- package/dist/core/tools/{docwrite.d.ts → doc/docwrite.d.ts} +1 -1
- package/dist/core/tools/doc/docwrite.d.ts.map +1 -0
- package/dist/core/tools/{docwrite.js → doc/docwrite.js} +6 -6
- package/dist/core/tools/doc/docwrite.js.map +1 -0
- package/dist/core/tools/doc/index.d.ts +11 -0
- package/dist/core/tools/doc/index.d.ts.map +1 -0
- package/dist/core/tools/doc/index.js +11 -0
- package/dist/core/tools/doc/index.js.map +1 -0
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +42 -28
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +61 -199
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +1 -1
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +1 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/extensions/core/ask-options.d.ts +10 -0
- package/dist/extensions/core/ask-options.d.ts.map +1 -0
- package/dist/extensions/core/ask-options.js +83 -0
- package/dist/extensions/core/ask-options.js.map +1 -0
- package/dist/extensions/core/config.d.ts +80 -0
- package/dist/extensions/core/config.d.ts.map +1 -0
- package/dist/extensions/core/config.js +110 -0
- package/dist/extensions/core/config.js.map +1 -0
- package/dist/extensions/core/cost.d.ts +11 -0
- package/dist/extensions/core/cost.d.ts.map +1 -0
- package/dist/extensions/core/cost.js +64 -0
- package/dist/extensions/core/cost.js.map +1 -0
- package/dist/extensions/core/hoo-core.d.ts +16 -179
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +27 -1663
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/extensions/core/loop.d.ts +15 -0
- package/dist/extensions/core/loop.d.ts.map +1 -0
- package/dist/extensions/core/loop.js +235 -0
- package/dist/extensions/core/loop.js.map +1 -0
- package/dist/extensions/core/marketplace.d.ts +17 -0
- package/dist/extensions/core/marketplace.d.ts.map +1 -0
- package/dist/extensions/core/marketplace.js +167 -0
- package/dist/extensions/core/marketplace.js.map +1 -0
- package/dist/extensions/core/mcp-loader.d.ts +26 -0
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -0
- package/dist/extensions/core/mcp-loader.js +391 -0
- package/dist/extensions/core/mcp-loader.js.map +1 -0
- package/dist/extensions/core/modes.d.ts +56 -0
- package/dist/extensions/core/modes.d.ts.map +1 -0
- package/dist/extensions/core/modes.js +268 -0
- package/dist/extensions/core/modes.js.map +1 -0
- package/dist/extensions/core/permission-gate.d.ts +10 -0
- package/dist/extensions/core/permission-gate.d.ts.map +1 -0
- package/dist/extensions/core/permission-gate.js +167 -0
- package/dist/extensions/core/permission-gate.js.map +1 -0
- package/dist/extensions/core/scaffold.d.ts +9 -0
- package/dist/extensions/core/scaffold.d.ts.map +1 -0
- package/dist/extensions/core/scaffold.js +146 -0
- package/dist/extensions/core/scaffold.js.map +1 -0
- package/dist/extensions/core/thinking-escalation.d.ts +18 -0
- package/dist/extensions/core/thinking-escalation.d.ts.map +1 -0
- package/dist/extensions/core/thinking-escalation.js +70 -0
- package/dist/extensions/core/thinking-escalation.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -2
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/command-executor.d.ts +0 -5
- package/dist/modes/interactive/command-executor.d.ts.map +1 -1
- package/dist/modes/interactive/command-executor.js +0 -7
- package/dist/modes/interactive/command-executor.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts +1 -1
- package/dist/modes/interactive/components/custom-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-message.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +0 -2
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +0 -2
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/extension-chrome.d.ts +56 -0
- package/dist/modes/interactive/extension-chrome.d.ts.map +1 -0
- package/dist/modes/interactive/extension-chrome.js +172 -0
- package/dist/modes/interactive/extension-chrome.js.map +1 -0
- package/dist/modes/interactive/extension-dialogs.d.ts +82 -0
- package/dist/modes/interactive/extension-dialogs.d.ts.map +1 -0
- package/dist/modes/interactive/extension-dialogs.js +259 -0
- package/dist/modes/interactive/extension-dialogs.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +11 -161
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +261 -1584
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/resource-display.d.ts +59 -0
- package/dist/modes/interactive/resource-display.d.ts.map +1 -0
- package/dist/modes/interactive/resource-display.js +499 -0
- package/dist/modes/interactive/resource-display.js.map +1 -0
- package/dist/modes/interactive/startup-checks.d.ts +17 -0
- package/dist/modes/interactive/startup-checks.d.ts.map +1 -0
- package/dist/modes/interactive/startup-checks.js +102 -0
- package/dist/modes/interactive/startup-checks.js.map +1 -0
- package/dist/modes/interactive/team-focus.d.ts +56 -0
- package/dist/modes/interactive/team-focus.d.ts.map +1 -0
- package/dist/modes/interactive/team-focus.js +167 -0
- package/dist/modes/interactive/team-focus.js.map +1 -0
- package/dist/modes/interactive/voice/voice-controller.d.ts +66 -0
- package/dist/modes/interactive/voice/voice-controller.d.ts.map +1 -0
- package/dist/modes/interactive/voice/voice-controller.js +312 -0
- package/dist/modes/interactive/voice/voice-controller.js.map +1 -0
- package/dist/modes/interactive/voice/voice-panel.d.ts.map +1 -0
- package/dist/modes/interactive/voice/voice-panel.js.map +1 -0
- package/dist/modes/interactive/voice/voice-transcribe.d.ts.map +1 -0
- package/dist/modes/interactive/voice/voice-transcribe.js.map +1 -0
- package/dist/modes/rpc/rpc-client.d.ts +1 -2
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +1 -2
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/resources-cli.d.ts.map +1 -1
- package/dist/resources-cli.js +1 -1
- package/dist/resources-cli.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +1 -1
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts.map +1 -1
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-check.js.map +1 -1
- package/docs/project-local-resources.md +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
- package/dist/core/compaction/branch-summarization.d.ts +0 -88
- package/dist/core/compaction/branch-summarization.d.ts.map +0 -1
- package/dist/core/compaction/branch-summarization.js +0 -243
- package/dist/core/compaction/branch-summarization.js.map +0 -1
- package/dist/core/compaction/compaction.d.ts +0 -123
- package/dist/core/compaction/compaction.d.ts.map +0 -1
- package/dist/core/compaction/compaction.js +0 -635
- package/dist/core/compaction/compaction.js.map +0 -1
- package/dist/core/compaction/index.d.ts +0 -7
- package/dist/core/compaction/index.d.ts.map +0 -1
- package/dist/core/compaction/index.js +0 -7
- package/dist/core/compaction/index.js.map +0 -1
- package/dist/core/compaction/utils.d.ts +0 -38
- package/dist/core/compaction/utils.d.ts.map +0 -1
- package/dist/core/compaction/utils.js +0 -156
- package/dist/core/compaction/utils.js.map +0 -1
- package/dist/core/messages.d.ts +0 -124
- package/dist/core/messages.d.ts.map +0 -1
- package/dist/core/messages.js +0 -221
- package/dist/core/messages.js.map +0 -1
- package/dist/core/tools/browser-continue.d.ts.map +0 -1
- package/dist/core/tools/browser-continue.js.map +0 -1
- package/dist/core/tools/browser-run.d.ts.map +0 -1
- package/dist/core/tools/browser-run.js.map +0 -1
- package/dist/core/tools/browsertools-shared.d.ts.map +0 -1
- package/dist/core/tools/browsertools-shared.js.map +0 -1
- package/dist/core/tools/docedit.d.ts.map +0 -1
- package/dist/core/tools/docedit.js.map +0 -1
- package/dist/core/tools/docgrep.d.ts.map +0 -1
- package/dist/core/tools/docgrep.js.map +0 -1
- package/dist/core/tools/docpeek.d.ts.map +0 -1
- package/dist/core/tools/docread.d.ts.map +0 -1
- package/dist/core/tools/docread.js.map +0 -1
- package/dist/core/tools/docscan.d.ts.map +0 -1
- package/dist/core/tools/docscan.js.map +0 -1
- package/dist/core/tools/docwrite.d.ts.map +0 -1
- package/dist/core/tools/docwrite.js.map +0 -1
- package/dist/core/tools/output-compression.d.ts +0 -53
- package/dist/core/tools/output-compression.d.ts.map +0 -1
- package/dist/core/tools/output-compression.js +0 -392
- package/dist/core/tools/output-compression.js.map +0 -1
- package/dist/modes/interactive/components/armin.d.ts +0 -34
- package/dist/modes/interactive/components/armin.d.ts.map +0 -1
- package/dist/modes/interactive/components/armin.js +0 -333
- package/dist/modes/interactive/components/armin.js.map +0 -1
- package/dist/modes/interactive/components/daxnuts.d.ts +0 -23
- package/dist/modes/interactive/components/daxnuts.d.ts.map +0 -1
- package/dist/modes/interactive/components/daxnuts.js +0 -140
- package/dist/modes/interactive/components/daxnuts.js.map +0 -1
- package/dist/modes/interactive/components/voice-panel.d.ts.map +0 -1
- package/dist/modes/interactive/components/voice-panel.js.map +0 -1
- package/dist/modes/interactive/voice-transcribe.d.ts.map +0 -1
- package/dist/modes/interactive/voice-transcribe.js.map +0 -1
- /package/dist/core/tools/{browsertools-shared.d.ts → browser/browsertools-shared.d.ts} +0 -0
- /package/dist/modes/interactive/{components → voice}/voice-panel.d.ts +0 -0
- /package/dist/modes/interactive/{components → voice}/voice-panel.js +0 -0
- /package/dist/modes/interactive/{voice-transcribe.d.ts → voice/voice-transcribe.d.ts} +0 -0
- /package/dist/modes/interactive/{voice-transcribe.js → voice/voice-transcribe.js} +0 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server loader — discovers server configs (standard mcp.json locations,
|
|
3
|
+
* hoocode's per-server JSON files, plugin registrations), connects via JSON-RPC
|
|
4
|
+
* 2.0 over stdio, and registers each server tool as `mcp_<server>_<tool>`.
|
|
5
|
+
*
|
|
6
|
+
* Config sources (first-wins by server name):
|
|
7
|
+
* 1. ~/.agents/mcp.json (user), ./.agents/mcp.json (project),
|
|
8
|
+
* ~/.config/claude/mcp.json (Claude Desktop)
|
|
9
|
+
* 2. ~/.hoocode/mcp-servers/*.json and ./.hoocode/mcp-servers/*.json
|
|
10
|
+
* 3. MCP servers registered by plugins/extensions during load
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
+
import { readdir } from "node:fs/promises";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { createInterface } from "node:readline";
|
|
18
|
+
import { summarizeArgs } from "@kolisachint/hoocode-agent-core";
|
|
19
|
+
import { Text } from "@kolisachint/hoocode-tui";
|
|
20
|
+
import { Type } from "typebox";
|
|
21
|
+
import { getHooCodeDir } from "../../config.js";
|
|
22
|
+
import { getExtensionMcpServers } from "../../core/extension-mcp-servers.js";
|
|
23
|
+
import { subagentSkipMcp } from "../../core/subagent-depth.js";
|
|
24
|
+
import { taskStore } from "../../core/task-store.js";
|
|
25
|
+
const HOOCODE_DIR = getHooCodeDir();
|
|
26
|
+
const mcpConnections = new Map();
|
|
27
|
+
/**
|
|
28
|
+
* Server configs retained by name so a tool call can transparently reconnect a
|
|
29
|
+
* dropped server (process churn between turns, server exit, a racing teardown)
|
|
30
|
+
* instead of permanently failing with "not connected".
|
|
31
|
+
*/
|
|
32
|
+
const mcpServerConfigs = new Map();
|
|
33
|
+
/** Timeout for the connection handshake (initialize / tools/list). Tool calls
|
|
34
|
+
* themselves are left untimed since MCP tools can be long-running. */
|
|
35
|
+
const MCP_HANDSHAKE_TIMEOUT_MS = 15000;
|
|
36
|
+
function spawnMcpServer(config) {
|
|
37
|
+
const proc = spawn(config.command, config.args ?? [], {
|
|
38
|
+
env: { ...process.env, ...(config.env ?? {}) },
|
|
39
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
40
|
+
});
|
|
41
|
+
let nextId = 1;
|
|
42
|
+
const pending = new Map();
|
|
43
|
+
const rl = createInterface({ input: proc.stdout });
|
|
44
|
+
rl.on("line", (line) => {
|
|
45
|
+
if (!line.trim())
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
const msg = JSON.parse(line);
|
|
49
|
+
if (msg.id === undefined)
|
|
50
|
+
return;
|
|
51
|
+
const cb = pending.get(msg.id);
|
|
52
|
+
if (!cb)
|
|
53
|
+
return;
|
|
54
|
+
pending.delete(msg.id);
|
|
55
|
+
if (msg.error)
|
|
56
|
+
cb.reject(new Error(msg.error.message));
|
|
57
|
+
else
|
|
58
|
+
cb.resolve(msg.result);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// ignore non-JSON server startup output
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
proc.on("exit", () => {
|
|
65
|
+
for (const cb of pending.values())
|
|
66
|
+
cb.reject(new Error(`MCP server "${config.name}" exited unexpectedly`));
|
|
67
|
+
pending.clear();
|
|
68
|
+
mcpConnections.delete(config.name);
|
|
69
|
+
});
|
|
70
|
+
function rpc(method, params, timeoutMs) {
|
|
71
|
+
const id = nextId++;
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
let timer;
|
|
74
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
75
|
+
timer = setTimeout(() => {
|
|
76
|
+
if (pending.delete(id)) {
|
|
77
|
+
reject(new Error(`MCP server "${config.name}" timed out after ${timeoutMs}ms on ${method}`));
|
|
78
|
+
}
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
timer.unref?.();
|
|
81
|
+
}
|
|
82
|
+
pending.set(id, {
|
|
83
|
+
resolve: (r) => {
|
|
84
|
+
if (timer)
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
resolve(r);
|
|
87
|
+
},
|
|
88
|
+
reject: (e) => {
|
|
89
|
+
if (timer)
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
reject(e);
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
proc.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function notify(method, params) {
|
|
98
|
+
proc.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
rpc,
|
|
102
|
+
notify,
|
|
103
|
+
terminate: () => {
|
|
104
|
+
rl.close();
|
|
105
|
+
proc.kill();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function connectMcpServer(config) {
|
|
110
|
+
mcpConnections.get(config.name)?.terminate();
|
|
111
|
+
const conn = spawnMcpServer(config);
|
|
112
|
+
mcpConnections.set(config.name, conn);
|
|
113
|
+
await conn.rpc("initialize", {
|
|
114
|
+
protocolVersion: "2024-11-05",
|
|
115
|
+
capabilities: { tools: {} },
|
|
116
|
+
clientInfo: { name: "hoocode", version: "1.0.0" },
|
|
117
|
+
}, MCP_HANDSHAKE_TIMEOUT_MS);
|
|
118
|
+
// Per the MCP spec the client must acknowledge a successful initialize with the
|
|
119
|
+
// initialized notification before issuing further requests; strict servers gate
|
|
120
|
+
// tools/call on it.
|
|
121
|
+
conn.notify("notifications/initialized");
|
|
122
|
+
const toolsResult = (await conn.rpc("tools/list", {}, MCP_HANDSHAKE_TIMEOUT_MS));
|
|
123
|
+
return { conn, tools: toolsResult.tools ?? [] };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Return a live connection for a server, lazily reconnecting from the retained
|
|
127
|
+
* config when the previous connection was torn down. Returns undefined only when
|
|
128
|
+
* no config is known or a fresh connect attempt fails.
|
|
129
|
+
*/
|
|
130
|
+
async function getOrConnectMcp(name) {
|
|
131
|
+
const existing = mcpConnections.get(name);
|
|
132
|
+
if (existing)
|
|
133
|
+
return existing;
|
|
134
|
+
const config = mcpServerConfigs.get(name);
|
|
135
|
+
if (!config)
|
|
136
|
+
return undefined;
|
|
137
|
+
try {
|
|
138
|
+
const { conn } = await connectMcpServer(config);
|
|
139
|
+
return conn;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
let mcpExitCleanupInstalled = false;
|
|
146
|
+
/** Kill spawned MCP servers when the host process exits so they don't linger as
|
|
147
|
+
* orphans (their stdin merely goes idle, which doesn't terminate them). */
|
|
148
|
+
function installMcpExitCleanup() {
|
|
149
|
+
if (mcpExitCleanupInstalled)
|
|
150
|
+
return;
|
|
151
|
+
mcpExitCleanupInstalled = true;
|
|
152
|
+
process.once("exit", () => {
|
|
153
|
+
for (const conn of mcpConnections.values()) {
|
|
154
|
+
try {
|
|
155
|
+
conn.terminate();
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// best-effort cleanup
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
mcpConnections.clear();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function buildMcpSchema(tool) {
|
|
165
|
+
const props = tool.inputSchema?.properties ?? {};
|
|
166
|
+
const required = new Set(tool.inputSchema?.required ?? []);
|
|
167
|
+
const shape = {};
|
|
168
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
169
|
+
let field;
|
|
170
|
+
switch (prop.type) {
|
|
171
|
+
case "number":
|
|
172
|
+
case "integer":
|
|
173
|
+
field = Type.Number({ description: prop.description });
|
|
174
|
+
break;
|
|
175
|
+
case "boolean":
|
|
176
|
+
field = Type.Boolean({ description: prop.description });
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
field = Type.String({ description: prop.description });
|
|
180
|
+
}
|
|
181
|
+
shape[key] = required.has(key) ? field : Type.Optional(field);
|
|
182
|
+
}
|
|
183
|
+
return Type.Object(shape);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Parse standard MCP config format (used by Claude Desktop, VS Code, etc.)
|
|
187
|
+
* into hoocode's McpServerConfig format.
|
|
188
|
+
*/
|
|
189
|
+
function parseStandardMcpConfig(config, _source) {
|
|
190
|
+
if (!config.mcpServers)
|
|
191
|
+
return [];
|
|
192
|
+
const servers = [];
|
|
193
|
+
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
194
|
+
servers.push({
|
|
195
|
+
name,
|
|
196
|
+
command: serverConfig.command,
|
|
197
|
+
args: serverConfig.args,
|
|
198
|
+
env: serverConfig.env,
|
|
199
|
+
background: serverConfig.background,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return servers;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Load MCP servers from a standard mcp.json file.
|
|
206
|
+
* Returns an array of McpServerConfig, or empty array if file doesn't exist or is invalid.
|
|
207
|
+
*/
|
|
208
|
+
function loadStandardMcpFile(filePath) {
|
|
209
|
+
if (!existsSync(filePath))
|
|
210
|
+
return [];
|
|
211
|
+
try {
|
|
212
|
+
const content = readFileSync(filePath, "utf8");
|
|
213
|
+
const config = JSON.parse(content);
|
|
214
|
+
return parseStandardMcpConfig(config, filePath);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export function setupMcpLoader(pi) {
|
|
221
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
222
|
+
// A spawned subagent whose tool allowlist has no MCP tools is told by its
|
|
223
|
+
// parent to skip server connection entirely (see SUBAGENT_SKIP_MCP_ENV).
|
|
224
|
+
// Each connect is a ~15s-timeout handshake; doing it for a subagent that can
|
|
225
|
+
// never call the tools is pure startup latency.
|
|
226
|
+
if (subagentSkipMcp())
|
|
227
|
+
return;
|
|
228
|
+
installMcpExitCleanup();
|
|
229
|
+
const allServerConfigs = [];
|
|
230
|
+
const seenNames = new Set();
|
|
231
|
+
// 1. Load from standard mcp.json locations
|
|
232
|
+
// User-level: ~/.agents/mcp.json
|
|
233
|
+
const userAgentsConfig = loadStandardMcpFile(join(homedir(), ".agents", "mcp.json"));
|
|
234
|
+
for (const config of userAgentsConfig) {
|
|
235
|
+
if (!seenNames.has(config.name)) {
|
|
236
|
+
seenNames.add(config.name);
|
|
237
|
+
allServerConfigs.push(config);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Project-level: ./.agents/mcp.json
|
|
241
|
+
const projectAgentsConfig = loadStandardMcpFile(join(ctx.cwd, ".agents", "mcp.json"));
|
|
242
|
+
for (const config of projectAgentsConfig) {
|
|
243
|
+
if (!seenNames.has(config.name)) {
|
|
244
|
+
seenNames.add(config.name);
|
|
245
|
+
allServerConfigs.push(config);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// Claude Desktop: ~/.config/claude/mcp.json
|
|
249
|
+
const claudeDesktopConfig = loadStandardMcpFile(join(homedir(), ".config", "claude", "mcp.json"));
|
|
250
|
+
for (const config of claudeDesktopConfig) {
|
|
251
|
+
if (!seenNames.has(config.name)) {
|
|
252
|
+
seenNames.add(config.name);
|
|
253
|
+
allServerConfigs.push(config);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// 2. Load from hoocode's per-server format (existing behavior)
|
|
257
|
+
const searchDirs = [join(HOOCODE_DIR, "mcp-servers"), join(ctx.cwd, ".hoocode", "mcp-servers")];
|
|
258
|
+
for (const dir of searchDirs) {
|
|
259
|
+
if (!existsSync(dir))
|
|
260
|
+
continue;
|
|
261
|
+
let files;
|
|
262
|
+
try {
|
|
263
|
+
files = (await readdir(dir)).filter((f) => f.endsWith(".json"));
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
for (const file of files) {
|
|
269
|
+
const cfgPath = join(dir, file);
|
|
270
|
+
let serverConfig;
|
|
271
|
+
try {
|
|
272
|
+
serverConfig = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
273
|
+
if (!serverConfig.name || !serverConfig.command) {
|
|
274
|
+
ctx.ui.notify(`MCP: config "${file}" is missing required "name" or "command"`, "warning");
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
ctx.ui.notify(`MCP: failed to parse "${file}": ${String(err)}`, "error");
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// Skip if already loaded from standard config
|
|
283
|
+
if (seenNames.has(serverConfig.name))
|
|
284
|
+
continue;
|
|
285
|
+
seenNames.add(serverConfig.name);
|
|
286
|
+
allServerConfigs.push(serverConfig);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// 2b. Load from plugins/extensions that registered MCP servers during load.
|
|
290
|
+
for (const entry of getExtensionMcpServers()) {
|
|
291
|
+
for (const serverConfig of parseStandardMcpConfig({ mcpServers: entry.mcpServers }, `plugin:${entry.source}`)) {
|
|
292
|
+
if (seenNames.has(serverConfig.name))
|
|
293
|
+
continue;
|
|
294
|
+
seenNames.add(serverConfig.name);
|
|
295
|
+
allServerConfigs.push(serverConfig);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// 3. Connect to all servers and register tools
|
|
299
|
+
for (const serverConfig of allServerConfigs) {
|
|
300
|
+
// Retain the config so a tool call can lazily reconnect a dropped server.
|
|
301
|
+
mcpServerConfigs.set(serverConfig.name, serverConfig);
|
|
302
|
+
try {
|
|
303
|
+
const { tools } = await connectMcpServer(serverConfig);
|
|
304
|
+
for (const tool of tools) {
|
|
305
|
+
const toolName = `mcp_${serverConfig.name}_${tool.name}`;
|
|
306
|
+
const schema = buildMcpSchema(tool);
|
|
307
|
+
const capturedServer = serverConfig.name;
|
|
308
|
+
const capturedTool = tool.name;
|
|
309
|
+
// MCP tools default to background mode since they are external processes with potential high latency
|
|
310
|
+
const isBackground = serverConfig.background !== false;
|
|
311
|
+
pi.registerTool({
|
|
312
|
+
name: toolName,
|
|
313
|
+
label: `[MCP] ${serverConfig.name} › ${tool.name}`,
|
|
314
|
+
description: tool.description,
|
|
315
|
+
parameters: schema,
|
|
316
|
+
background: isBackground,
|
|
317
|
+
// Render a clean, prefixed title in chat — `MCP [server › tool] <args>` —
|
|
318
|
+
// parallel to the subagent `Task [type] <desc>` line. Without this the
|
|
319
|
+
// ToolExecutionComponent falls back to the raw `mcp_<server>_<tool>` name.
|
|
320
|
+
// The args summary reuses the same helper as the background start/finish
|
|
321
|
+
// messages so the chat title stays in sync with them.
|
|
322
|
+
renderCall(args, theme) {
|
|
323
|
+
const summary = summarizeArgs((args ?? {}));
|
|
324
|
+
const text = theme.fg("toolTitle", theme.bold("MCP ")) +
|
|
325
|
+
theme.fg("accent", `[${capturedServer} › ${capturedTool}]`) +
|
|
326
|
+
(summary ? theme.fg("dim", ` ${summary}`) : "");
|
|
327
|
+
return new Text(text, 0, 0);
|
|
328
|
+
},
|
|
329
|
+
async execute(_toolCallId, params, signal, _onUpdate) {
|
|
330
|
+
// Background MCP tools get a task store entry so they appear in the task pane.
|
|
331
|
+
// Foreground tools skip this (their result is awaited inline). The server
|
|
332
|
+
// name rides in subagentMode and becomes the row's `[server]` origin tag;
|
|
333
|
+
// the title carries just the tool.
|
|
334
|
+
const task = isBackground
|
|
335
|
+
? taskStore.create(capturedTool, { source: "mcp", subagentMode: capturedServer })
|
|
336
|
+
: undefined;
|
|
337
|
+
if (task)
|
|
338
|
+
taskStore.update(task.id, { status: "in_progress" });
|
|
339
|
+
// Lazily (re)connect: a dropped connection (server exit, process churn
|
|
340
|
+
// between turns, a racing teardown) should transparently reconnect from
|
|
341
|
+
// the retained config rather than permanently fail with "not connected".
|
|
342
|
+
const activeConn = await getOrConnectMcp(capturedServer);
|
|
343
|
+
if (!activeConn) {
|
|
344
|
+
if (task)
|
|
345
|
+
taskStore.update(task.id, { status: "failed" });
|
|
346
|
+
return {
|
|
347
|
+
content: [
|
|
348
|
+
{
|
|
349
|
+
type: "text",
|
|
350
|
+
text: `MCP server "${capturedServer}" is not connected (reconnect attempt failed)`,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
details: undefined,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
const abortPromise = new Promise((_, reject) => {
|
|
358
|
+
signal.addEventListener("abort", () => reject(new Error("Aborted")));
|
|
359
|
+
});
|
|
360
|
+
const result = await Promise.race([
|
|
361
|
+
activeConn.rpc("tools/call", {
|
|
362
|
+
name: capturedTool,
|
|
363
|
+
arguments: params,
|
|
364
|
+
}),
|
|
365
|
+
abortPromise,
|
|
366
|
+
]);
|
|
367
|
+
if (task)
|
|
368
|
+
taskStore.update(task.id, { status: "done" });
|
|
369
|
+
return {
|
|
370
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
371
|
+
details: undefined,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
375
|
+
if (task)
|
|
376
|
+
taskStore.update(task.id, { status: "failed" });
|
|
377
|
+
throw error;
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
const bgMode = serverConfig.background !== false ? "background" : "foreground";
|
|
383
|
+
ctx.ui.notify(`MCP: connected "${serverConfig.name}" (${tools.length} tool${tools.length === 1 ? "" : "s"}, ${bgMode})`, "info");
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
ctx.ui.notify(`MCP: failed to connect "${serverConfig.name}": ${String(err)}`, "error");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
//# sourceMappingURL=mcp-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-loader.js","sourceRoot":"","sources":["../../../src/extensions/core/mcp-loader.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * MCP server loader — discovers server configs (standard mcp.json locations,\n * hoocode's per-server JSON files, plugin registrations), connects via JSON-RPC\n * 2.0 over stdio, and registers each server tool as `mcp_<server>_<tool>`.\n *\n * Config sources (first-wins by server name):\n * 1. ~/.agents/mcp.json (user), ./.agents/mcp.json (project),\n * ~/.config/claude/mcp.json (Claude Desktop)\n * 2. ~/.hoocode/mcp-servers/*.json and ./.hoocode/mcp-servers/*.json\n * 3. MCP servers registered by plugins/extensions during load\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createInterface } from \"node:readline\";\nimport type { AgentToolResult, AgentToolUpdateCallback } from \"@kolisachint/hoocode-agent-core\";\nimport { summarizeArgs } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { type Static, Type } from \"typebox\";\nimport { getHooCodeDir } from \"../../config.js\";\nimport { getExtensionMcpServers } from \"../../core/extension-mcp-servers.js\";\nimport type { ExtensionAPI, ExtensionContext, SessionStartEvent } from \"../../core/extensions/types.js\";\nimport { subagentSkipMcp } from \"../../core/subagent-depth.js\";\nimport { taskStore } from \"../../core/task-store.js\";\n\nconst HOOCODE_DIR = getHooCodeDir();\n\ninterface McpToolDef {\n\tname: string;\n\tdescription: string;\n\tinputSchema?: {\n\t\ttype?: string;\n\t\tproperties?: Record<string, { type?: string; description?: string }>;\n\t\trequired?: string[];\n\t};\n}\n\nexport interface McpServerConfig {\n\t/** Unique server identifier used as prefix for registered tool names */\n\tname: string;\n\t/** Executable to spawn */\n\tcommand: string;\n\t/** Optional arguments passed to the command */\n\targs?: string[];\n\t/** Optional extra environment variables for the server process */\n\tenv?: Record<string, string>;\n\t/** Run MCP tools in background by default (default: true for MCP servers) */\n\tbackground?: boolean;\n}\n\n/** Standard MCP config format used by Claude Desktop and other tools */\ninterface StandardMcpServerConfig {\n\tcommand: string;\n\targs?: string[];\n\tenv?: Record<string, string>;\n\tbackground?: boolean;\n}\n\ninterface StandardMcpConfig {\n\tmcpServers?: Record<string, StandardMcpServerConfig>;\n}\n\ninterface McpConnection {\n\trpc(method: string, params?: unknown, timeoutMs?: number): Promise<unknown>;\n\t/** Send a JSON-RPC notification (no id, no response expected). */\n\tnotify(method: string, params?: unknown): void;\n\tterminate(): void;\n}\n\nconst mcpConnections = new Map<string, McpConnection>();\n/**\n * Server configs retained by name so a tool call can transparently reconnect a\n * dropped server (process churn between turns, server exit, a racing teardown)\n * instead of permanently failing with \"not connected\".\n */\nconst mcpServerConfigs = new Map<string, McpServerConfig>();\n\n/** Timeout for the connection handshake (initialize / tools/list). Tool calls\n * themselves are left untimed since MCP tools can be long-running. */\nconst MCP_HANDSHAKE_TIMEOUT_MS = 15000;\n\nfunction spawnMcpServer(config: McpServerConfig): McpConnection {\n\tconst proc: ChildProcess = spawn(config.command, config.args ?? [], {\n\t\tenv: { ...process.env, ...(config.env ?? {}) },\n\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t});\n\n\tlet nextId = 1;\n\tconst pending = new Map<number, { resolve: (r: unknown) => void; reject: (e: Error) => void }>();\n\n\tconst rl = createInterface({ input: proc.stdout! });\n\trl.on(\"line\", (line) => {\n\t\tif (!line.trim()) return;\n\t\ttry {\n\t\t\tconst msg = JSON.parse(line) as {\n\t\t\t\tid?: number;\n\t\t\t\tresult?: unknown;\n\t\t\t\terror?: { message: string };\n\t\t\t};\n\t\t\tif (msg.id === undefined) return;\n\t\t\tconst cb = pending.get(msg.id);\n\t\t\tif (!cb) return;\n\t\t\tpending.delete(msg.id);\n\t\t\tif (msg.error) cb.reject(new Error(msg.error.message));\n\t\t\telse cb.resolve(msg.result);\n\t\t} catch {\n\t\t\t// ignore non-JSON server startup output\n\t\t}\n\t});\n\n\tproc.on(\"exit\", () => {\n\t\tfor (const cb of pending.values()) cb.reject(new Error(`MCP server \"${config.name}\" exited unexpectedly`));\n\t\tpending.clear();\n\t\tmcpConnections.delete(config.name);\n\t});\n\n\tfunction rpc(method: string, params?: unknown, timeoutMs?: number): Promise<unknown> {\n\t\tconst id = nextId++;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tlet timer: NodeJS.Timeout | undefined;\n\t\t\tif (timeoutMs && timeoutMs > 0) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tif (pending.delete(id)) {\n\t\t\t\t\t\treject(new Error(`MCP server \"${config.name}\" timed out after ${timeoutMs}ms on ${method}`));\n\t\t\t\t\t}\n\t\t\t\t}, timeoutMs);\n\t\t\t\ttimer.unref?.();\n\t\t\t}\n\t\t\tpending.set(id, {\n\t\t\t\tresolve: (r) => {\n\t\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\t\tresolve(r);\n\t\t\t\t},\n\t\t\t\treject: (e) => {\n\t\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\t\treject(e);\n\t\t\t\t},\n\t\t\t});\n\t\t\tproc.stdin!.write(`${JSON.stringify({ jsonrpc: \"2.0\", id, method, params })}\\n`);\n\t\t});\n\t}\n\n\tfunction notify(method: string, params?: unknown): void {\n\t\tproc.stdin!.write(`${JSON.stringify({ jsonrpc: \"2.0\", method, params })}\\n`);\n\t}\n\n\treturn {\n\t\trpc,\n\t\tnotify,\n\t\tterminate: () => {\n\t\t\trl.close();\n\t\t\tproc.kill();\n\t\t},\n\t};\n}\n\nasync function connectMcpServer(config: McpServerConfig): Promise<{ conn: McpConnection; tools: McpToolDef[] }> {\n\tmcpConnections.get(config.name)?.terminate();\n\n\tconst conn = spawnMcpServer(config);\n\tmcpConnections.set(config.name, conn);\n\n\tawait conn.rpc(\n\t\t\"initialize\",\n\t\t{\n\t\t\tprotocolVersion: \"2024-11-05\",\n\t\t\tcapabilities: { tools: {} },\n\t\t\tclientInfo: { name: \"hoocode\", version: \"1.0.0\" },\n\t\t},\n\t\tMCP_HANDSHAKE_TIMEOUT_MS,\n\t);\n\n\t// Per the MCP spec the client must acknowledge a successful initialize with the\n\t// initialized notification before issuing further requests; strict servers gate\n\t// tools/call on it.\n\tconn.notify(\"notifications/initialized\");\n\n\tconst toolsResult = (await conn.rpc(\"tools/list\", {}, MCP_HANDSHAKE_TIMEOUT_MS)) as {\n\t\ttools?: McpToolDef[];\n\t};\n\treturn { conn, tools: toolsResult.tools ?? [] };\n}\n\n/**\n * Return a live connection for a server, lazily reconnecting from the retained\n * config when the previous connection was torn down. Returns undefined only when\n * no config is known or a fresh connect attempt fails.\n */\nasync function getOrConnectMcp(name: string): Promise<McpConnection | undefined> {\n\tconst existing = mcpConnections.get(name);\n\tif (existing) return existing;\n\tconst config = mcpServerConfigs.get(name);\n\tif (!config) return undefined;\n\ttry {\n\t\tconst { conn } = await connectMcpServer(config);\n\t\treturn conn;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nlet mcpExitCleanupInstalled = false;\n/** Kill spawned MCP servers when the host process exits so they don't linger as\n * orphans (their stdin merely goes idle, which doesn't terminate them). */\nfunction installMcpExitCleanup(): void {\n\tif (mcpExitCleanupInstalled) return;\n\tmcpExitCleanupInstalled = true;\n\tprocess.once(\"exit\", () => {\n\t\tfor (const conn of mcpConnections.values()) {\n\t\t\ttry {\n\t\t\t\tconn.terminate();\n\t\t\t} catch {\n\t\t\t\t// best-effort cleanup\n\t\t\t}\n\t\t}\n\t\tmcpConnections.clear();\n\t});\n}\n\nfunction buildMcpSchema(tool: McpToolDef): ReturnType<typeof Type.Object> {\n\tconst props = tool.inputSchema?.properties ?? {};\n\tconst required = new Set(tool.inputSchema?.required ?? []);\n\tconst shape: Record<string, ReturnType<typeof Type.String>> = {};\n\n\tfor (const [key, prop] of Object.entries(props)) {\n\t\tlet field: ReturnType<typeof Type.String>;\n\t\tswitch (prop.type) {\n\t\t\tcase \"number\":\n\t\t\tcase \"integer\":\n\t\t\t\tfield = Type.Number({ description: prop.description }) as unknown as ReturnType<typeof Type.String>;\n\t\t\t\tbreak;\n\t\t\tcase \"boolean\":\n\t\t\t\tfield = Type.Boolean({ description: prop.description }) as unknown as ReturnType<typeof Type.String>;\n\t\t\t\tbreak;\n\t\t\tdefault:\n\t\t\t\tfield = Type.String({ description: prop.description });\n\t\t}\n\t\tshape[key] = required.has(key) ? field : (Type.Optional(field) as unknown as ReturnType<typeof Type.String>);\n\t}\n\n\treturn Type.Object(shape);\n}\n\n/**\n * Parse standard MCP config format (used by Claude Desktop, VS Code, etc.)\n * into hoocode's McpServerConfig format.\n */\nfunction parseStandardMcpConfig(config: StandardMcpConfig, _source: string): McpServerConfig[] {\n\tif (!config.mcpServers) return [];\n\n\tconst servers: McpServerConfig[] = [];\n\tfor (const [name, serverConfig] of Object.entries(config.mcpServers)) {\n\t\tservers.push({\n\t\t\tname,\n\t\t\tcommand: serverConfig.command,\n\t\t\targs: serverConfig.args,\n\t\t\tenv: serverConfig.env,\n\t\t\tbackground: serverConfig.background,\n\t\t});\n\t}\n\treturn servers;\n}\n\n/**\n * Load MCP servers from a standard mcp.json file.\n * Returns an array of McpServerConfig, or empty array if file doesn't exist or is invalid.\n */\nfunction loadStandardMcpFile(filePath: string): McpServerConfig[] {\n\tif (!existsSync(filePath)) return [];\n\n\ttry {\n\t\tconst content = readFileSync(filePath, \"utf8\");\n\t\tconst config = JSON.parse(content) as StandardMcpConfig;\n\t\treturn parseStandardMcpConfig(config, filePath);\n\t} catch {\n\t\treturn [];\n\t}\n}\n\nexport function setupMcpLoader(pi: ExtensionAPI): void {\n\tpi.on(\"session_start\", async (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\t// A spawned subagent whose tool allowlist has no MCP tools is told by its\n\t\t// parent to skip server connection entirely (see SUBAGENT_SKIP_MCP_ENV).\n\t\t// Each connect is a ~15s-timeout handshake; doing it for a subagent that can\n\t\t// never call the tools is pure startup latency.\n\t\tif (subagentSkipMcp()) return;\n\n\t\tinstallMcpExitCleanup();\n\t\tconst allServerConfigs: McpServerConfig[] = [];\n\t\tconst seenNames = new Set<string>();\n\n\t\t// 1. Load from standard mcp.json locations\n\t\t// User-level: ~/.agents/mcp.json\n\t\tconst userAgentsConfig = loadStandardMcpFile(join(homedir(), \".agents\", \"mcp.json\"));\n\t\tfor (const config of userAgentsConfig) {\n\t\t\tif (!seenNames.has(config.name)) {\n\t\t\t\tseenNames.add(config.name);\n\t\t\t\tallServerConfigs.push(config);\n\t\t\t}\n\t\t}\n\n\t\t// Project-level: ./.agents/mcp.json\n\t\tconst projectAgentsConfig = loadStandardMcpFile(join(ctx.cwd, \".agents\", \"mcp.json\"));\n\t\tfor (const config of projectAgentsConfig) {\n\t\t\tif (!seenNames.has(config.name)) {\n\t\t\t\tseenNames.add(config.name);\n\t\t\t\tallServerConfigs.push(config);\n\t\t\t}\n\t\t}\n\n\t\t// Claude Desktop: ~/.config/claude/mcp.json\n\t\tconst claudeDesktopConfig = loadStandardMcpFile(join(homedir(), \".config\", \"claude\", \"mcp.json\"));\n\t\tfor (const config of claudeDesktopConfig) {\n\t\t\tif (!seenNames.has(config.name)) {\n\t\t\t\tseenNames.add(config.name);\n\t\t\t\tallServerConfigs.push(config);\n\t\t\t}\n\t\t}\n\n\t\t// 2. Load from hoocode's per-server format (existing behavior)\n\t\tconst searchDirs = [join(HOOCODE_DIR, \"mcp-servers\"), join(ctx.cwd, \".hoocode\", \"mcp-servers\")];\n\n\t\tfor (const dir of searchDirs) {\n\t\t\tif (!existsSync(dir)) continue;\n\n\t\t\tlet files: string[];\n\t\t\ttry {\n\t\t\t\tfiles = (await readdir(dir)).filter((f) => f.endsWith(\".json\"));\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tfor (const file of files) {\n\t\t\t\tconst cfgPath = join(dir, file);\n\t\t\t\tlet serverConfig: McpServerConfig;\n\n\t\t\t\ttry {\n\t\t\t\t\tserverConfig = JSON.parse(readFileSync(cfgPath, \"utf8\")) as McpServerConfig;\n\t\t\t\t\tif (!serverConfig.name || !serverConfig.command) {\n\t\t\t\t\t\tctx.ui.notify(`MCP: config \"${file}\" is missing required \"name\" or \"command\"`, \"warning\");\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.ui.notify(`MCP: failed to parse \"${file}\": ${String(err)}`, \"error\");\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Skip if already loaded from standard config\n\t\t\t\tif (seenNames.has(serverConfig.name)) continue;\n\t\t\t\tseenNames.add(serverConfig.name);\n\t\t\t\tallServerConfigs.push(serverConfig);\n\t\t\t}\n\t\t}\n\n\t\t// 2b. Load from plugins/extensions that registered MCP servers during load.\n\t\tfor (const entry of getExtensionMcpServers()) {\n\t\t\tfor (const serverConfig of parseStandardMcpConfig(\n\t\t\t\t{ mcpServers: entry.mcpServers },\n\t\t\t\t`plugin:${entry.source}`,\n\t\t\t)) {\n\t\t\t\tif (seenNames.has(serverConfig.name)) continue;\n\t\t\t\tseenNames.add(serverConfig.name);\n\t\t\t\tallServerConfigs.push(serverConfig);\n\t\t\t}\n\t\t}\n\n\t\t// 3. Connect to all servers and register tools\n\t\tfor (const serverConfig of allServerConfigs) {\n\t\t\t// Retain the config so a tool call can lazily reconnect a dropped server.\n\t\t\tmcpServerConfigs.set(serverConfig.name, serverConfig);\n\t\t\ttry {\n\t\t\t\tconst { tools } = await connectMcpServer(serverConfig);\n\n\t\t\t\tfor (const tool of tools) {\n\t\t\t\t\tconst toolName = `mcp_${serverConfig.name}_${tool.name}`;\n\t\t\t\t\tconst schema = buildMcpSchema(tool);\n\t\t\t\t\tconst capturedServer = serverConfig.name;\n\t\t\t\t\tconst capturedTool = tool.name;\n\t\t\t\t\t// MCP tools default to background mode since they are external processes with potential high latency\n\t\t\t\t\tconst isBackground = serverConfig.background !== false;\n\n\t\t\t\t\tpi.registerTool({\n\t\t\t\t\t\tname: toolName,\n\t\t\t\t\t\tlabel: `[MCP] ${serverConfig.name} › ${tool.name}`,\n\t\t\t\t\t\tdescription: tool.description,\n\t\t\t\t\t\tparameters: schema,\n\t\t\t\t\t\tbackground: isBackground,\n\t\t\t\t\t\t// Render a clean, prefixed title in chat — `MCP [server › tool] <args>` —\n\t\t\t\t\t\t// parallel to the subagent `Task [type] <desc>` line. Without this the\n\t\t\t\t\t\t// ToolExecutionComponent falls back to the raw `mcp_<server>_<tool>` name.\n\t\t\t\t\t\t// The args summary reuses the same helper as the background start/finish\n\t\t\t\t\t\t// messages so the chat title stays in sync with them.\n\t\t\t\t\t\trenderCall(args, theme) {\n\t\t\t\t\t\t\tconst summary = summarizeArgs((args ?? {}) as Record<string, unknown>);\n\t\t\t\t\t\t\tconst text =\n\t\t\t\t\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"MCP \")) +\n\t\t\t\t\t\t\t\ttheme.fg(\"accent\", `[${capturedServer} › ${capturedTool}]`) +\n\t\t\t\t\t\t\t\t(summary ? theme.fg(\"dim\", ` ${summary}`) : \"\");\n\t\t\t\t\t\t\treturn new Text(text, 0, 0);\n\t\t\t\t\t\t},\n\t\t\t\t\t\tasync execute(\n\t\t\t\t\t\t\t_toolCallId: string,\n\t\t\t\t\t\t\tparams: Static<typeof schema>,\n\t\t\t\t\t\t\tsignal: AbortSignal,\n\t\t\t\t\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t\t\t\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\t\t\t\t\t// Background MCP tools get a task store entry so they appear in the task pane.\n\t\t\t\t\t\t\t// Foreground tools skip this (their result is awaited inline). The server\n\t\t\t\t\t\t\t// name rides in subagentMode and becomes the row's `[server]` origin tag;\n\t\t\t\t\t\t\t// the title carries just the tool.\n\t\t\t\t\t\t\tconst task = isBackground\n\t\t\t\t\t\t\t\t? taskStore.create(capturedTool, { source: \"mcp\", subagentMode: capturedServer })\n\t\t\t\t\t\t\t\t: undefined;\n\t\t\t\t\t\t\tif (task) taskStore.update(task.id, { status: \"in_progress\" });\n\n\t\t\t\t\t\t\t// Lazily (re)connect: a dropped connection (server exit, process churn\n\t\t\t\t\t\t\t// between turns, a racing teardown) should transparently reconnect from\n\t\t\t\t\t\t\t// the retained config rather than permanently fail with \"not connected\".\n\t\t\t\t\t\t\tconst activeConn = await getOrConnectMcp(capturedServer);\n\t\t\t\t\t\t\tif (!activeConn) {\n\t\t\t\t\t\t\t\tif (task) taskStore.update(task.id, { status: \"failed\" });\n\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\t\ttext: `MCP server \"${capturedServer}\" is not connected (reconnect attempt failed)`,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst abortPromise = new Promise<never>((_, reject) => {\n\t\t\t\t\t\t\t\t\tsignal.addEventListener(\"abort\", () => reject(new Error(\"Aborted\")));\n\t\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t\tconst result = await Promise.race([\n\t\t\t\t\t\t\t\t\tactiveConn.rpc(\"tools/call\", {\n\t\t\t\t\t\t\t\t\t\tname: capturedTool,\n\t\t\t\t\t\t\t\t\t\targuments: params,\n\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\tabortPromise,\n\t\t\t\t\t\t\t\t]);\n\n\t\t\t\t\t\t\t\tif (task) taskStore.update(task.id, { status: \"done\" });\n\t\t\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n\t\t\t\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t\t\t\t};\n\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\tif (task) taskStore.update(task.id, { status: \"failed\" });\n\t\t\t\t\t\t\t\tthrow error;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tconst bgMode = serverConfig.background !== false ? \"background\" : \"foreground\";\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t`MCP: connected \"${serverConfig.name}\" (${tools.length} tool${tools.length === 1 ? \"\" : \"s\"}, ${bgMode})`,\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t} catch (err) {\n\t\t\t\tctx.ui.notify(`MCP: failed to connect \"${serverConfig.name}\": ${String(err)}`, \"error\");\n\t\t\t}\n\t\t}\n\t});\n}\n"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mode system — resolves the active mode (ask/plan/build/debug), loads the
|
|
3
|
+
* mode's system prompt, filters active tools, and exposes the /mode, /plan,
|
|
4
|
+
* and /approve commands.
|
|
5
|
+
*
|
|
6
|
+
* Mode prompt search order (first hit wins):
|
|
7
|
+
* - `./.hoocode/modes/{mode}/system.md`
|
|
8
|
+
* - `~/.hoocode/modes/{mode}/system.md`
|
|
9
|
+
* - each configured/contributed external dir in declared order
|
|
10
|
+
* - built-in MODE_DEFAULTS for the four known modes
|
|
11
|
+
*/
|
|
12
|
+
import type { ExtensionAPI } from "../../core/extensions/types.js";
|
|
13
|
+
/**
|
|
14
|
+
* Returns the system prompt for the active mode.
|
|
15
|
+
*
|
|
16
|
+
* Search order (first hit wins):
|
|
17
|
+
* - `./.hoocode/modes/{mode}/system.md`
|
|
18
|
+
* - `~/.hoocode/modes/{mode}/system.md`
|
|
19
|
+
* - each of `externalDirs` in declared order (config + CLI + extension contributions)
|
|
20
|
+
* - built-in MODE_DEFAULTS for the four known modes
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildSystemPrompt(mode: string, cwd: string, options?: {
|
|
23
|
+
modePaths?: string[];
|
|
24
|
+
}): string | undefined;
|
|
25
|
+
export interface PlanSections {
|
|
26
|
+
goal?: string;
|
|
27
|
+
filesToModify?: string;
|
|
28
|
+
newFiles?: string;
|
|
29
|
+
tests?: string;
|
|
30
|
+
verification?: string;
|
|
31
|
+
/** Original full text, used as fallback if no sections parsed */
|
|
32
|
+
raw: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parses `.hoocode/plan.md` into named sections.
|
|
36
|
+
*
|
|
37
|
+
* Recognises both ATX headings (`## Goal`) and bold labels (`**Goal**`).
|
|
38
|
+
* Section names matched (case-insensitive): Goal, Files to modify, New files,
|
|
39
|
+
* Tests, Verification.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parsePlanSections(planContent: string): PlanSections;
|
|
42
|
+
/**
|
|
43
|
+
* Builds the user message sent to the agent when `/approve` is run.
|
|
44
|
+
*
|
|
45
|
+
* If the plan has recognisable sections, each is presented as a numbered step
|
|
46
|
+
* so the agent works through them sequentially. Otherwise the raw plan is used.
|
|
47
|
+
*
|
|
48
|
+
* Execution order:
|
|
49
|
+
* 1. Modify existing files
|
|
50
|
+
* 2. Create new files
|
|
51
|
+
* 3. Update / add tests
|
|
52
|
+
* 4. Run verification commands
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildApproveMessage(sections: PlanSections): string;
|
|
55
|
+
export declare function setupMode(pi: ExtensionAPI): void;
|
|
56
|
+
//# sourceMappingURL=modes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modes.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/modes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAGX,YAAY,EAIZ,MAAM,gCAAgC,CAAC;AA8CxC;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CAGnH;AAMD,MAAM,WAAW,YAAY;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iEAAiE;IACjE,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,YAAY,CA2BnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,YAAY,GAAG,MAAM,CAwBlE;AAMD,wBAAgB,SAAS,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CA6IhD","sourcesContent":["/**\n * Mode system — resolves the active mode (ask/plan/build/debug), loads the\n * mode's system prompt, filters active tools, and exposes the /mode, /plan,\n * and /approve commands.\n *\n * Mode prompt search order (first hit wins):\n * - `./.hoocode/modes/{mode}/system.md`\n * - `~/.hoocode/modes/{mode}/system.md`\n * - each configured/contributed external dir in declared order\n * - built-in MODE_DEFAULTS for the four known modes\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\nimport { getHooCodeDir } from \"../../config.js\";\nimport type {\n\tBeforeAgentStartEvent,\n\tBeforeAgentStartEventResult,\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { DEFAULT_MODE, DEFAULT_MODE_PROMPTS as MODE_DEFAULTS } from \"../../core/mode-prompts.js\";\nimport { mergeSearchPaths, readConfig, readMergedConfig, writeConfig } from \"./config.js\";\n\nconst HOOCODE_DIR = getHooCodeDir();\n\n/**\n * Per-session plan file path. Keying on sessionId lets concurrent or resumed\n * plan sessions keep distinct plans instead of clobbering each other.\n */\nfunction getPlanPath(cwd: string, sessionId: string): string {\n\treturn join(cwd, \".hoocode\", \"plans\", `${sessionId}.md`);\n}\n\n/** Legacy single-file plan location, retained as a read-only fallback for /approve. */\nfunction getLegacyPlanPath(cwd: string): string {\n\treturn join(cwd, \".hoocode\", \"plan.md\");\n}\n\nfunction tryReadFile(path: string): string | undefined {\n\tif (!existsSync(path)) return undefined;\n\ttry {\n\t\tconst text = readFileSync(path, \"utf8\").trim();\n\t\treturn text || undefined;\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Walks search dirs in precedence order and returns the first existing\n * `modes/{name}/system.md` content. Order: project → user → externalDirs.\n */\nfunction resolveModeFile(name: string, cwd: string, externalDirs: string[]): string | undefined {\n\tconst candidates: string[] = [\n\t\tjoin(cwd, \".hoocode\", \"modes\", name, \"system.md\"),\n\t\tjoin(HOOCODE_DIR, \"modes\", name, \"system.md\"),\n\t\t...externalDirs.map((dir) => join(dir, name, \"system.md\")),\n\t];\n\tfor (const candidate of candidates) {\n\t\tconst content = tryReadFile(candidate);\n\t\tif (content !== undefined) return content;\n\t}\n\treturn undefined;\n}\n\n/**\n * Returns the system prompt for the active mode.\n *\n * Search order (first hit wins):\n * - `./.hoocode/modes/{mode}/system.md`\n * - `~/.hoocode/modes/{mode}/system.md`\n * - each of `externalDirs` in declared order (config + CLI + extension contributions)\n * - built-in MODE_DEFAULTS for the four known modes\n */\nexport function buildSystemPrompt(mode: string, cwd: string, options?: { modePaths?: string[] }): string | undefined {\n\tconst modePaths = options?.modePaths ?? [];\n\treturn resolveModeFile(mode, cwd, modePaths) ?? MODE_DEFAULTS[mode];\n}\n\n// ============================================================================\n// Plan file: section parsing and step-by-step execution message\n// ============================================================================\n\nexport interface PlanSections {\n\tgoal?: string;\n\tfilesToModify?: string;\n\tnewFiles?: string;\n\ttests?: string;\n\tverification?: string;\n\t/** Original full text, used as fallback if no sections parsed */\n\traw: string;\n}\n\n/**\n * Parses `.hoocode/plan.md` into named sections.\n *\n * Recognises both ATX headings (`## Goal`) and bold labels (`**Goal**`).\n * Section names matched (case-insensitive): Goal, Files to modify, New files,\n * Tests, Verification.\n */\nexport function parsePlanSections(planContent: string): PlanSections {\n\tconst result: PlanSections = { raw: planContent };\n\n\t// Match `## Heading text` or `**Heading text**` followed by content until\n\t// the next heading of the same style.\n\tconst sectionPattern =\n\t\t/^(?:#{1,3}\\s+(.+?)|(?:\\*\\*(.+?)\\*\\*))\\s*\\n([\\s\\S]*?)(?=(?:^#{1,3}\\s+|\\*\\*[^*\\n]+\\*\\*\\s*\\n)|$)/gm;\n\n\tfor (const match of planContent.matchAll(sectionPattern)) {\n\t\tconst heading = (match[1] ?? match[2] ?? \"\").toLowerCase().trim();\n\t\tconst content = match[3].trim();\n\t\tif (!content) continue;\n\n\t\tif (/^goal/.test(heading)) {\n\t\t\tresult.goal = content;\n\t\t} else if (/files?\\s+to\\s+modif|^modif/.test(heading)) {\n\t\t\tresult.filesToModify = content;\n\t\t} else if (/new\\s+files?/.test(heading)) {\n\t\t\tresult.newFiles = content;\n\t\t} else if (/^tests?/.test(heading)) {\n\t\t\tresult.tests = content;\n\t\t} else if (/^verif/.test(heading)) {\n\t\t\tresult.verification = content;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Builds the user message sent to the agent when `/approve` is run.\n *\n * If the plan has recognisable sections, each is presented as a numbered step\n * so the agent works through them sequentially. Otherwise the raw plan is used.\n *\n * Execution order:\n * 1. Modify existing files\n * 2. Create new files\n * 3. Update / add tests\n * 4. Run verification commands\n */\nexport function buildApproveMessage(sections: PlanSections): string {\n\tconst steps: string[] = [];\n\n\tif (sections.goal) {\n\t\tsteps.push(`**Goal:** ${sections.goal}`);\n\t}\n\tif (sections.filesToModify) {\n\t\tsteps.push(`**Step 1 — Modify existing files:**\\n${sections.filesToModify}`);\n\t}\n\tif (sections.newFiles) {\n\t\tsteps.push(`**Step 2 — Create new files:**\\n${sections.newFiles}`);\n\t}\n\tif (sections.tests) {\n\t\tsteps.push(`**Step 3 — Update tests:**\\n${sections.tests}`);\n\t}\n\tif (sections.verification) {\n\t\tsteps.push(`**Step 4 — Verify:**\\n${sections.verification}`);\n\t}\n\n\tif (steps.length === 0) {\n\t\treturn `Execute the following plan:\\n\\n${sections.raw}`;\n\t}\n\n\treturn `Execute this plan step by step. Complete each step fully before moving to the next.\\n\\n${steps.join(\"\\n\\n\")}`;\n}\n\n// ============================================================================\n// setupMode\n// ============================================================================\n\nexport function setupMode(pi: ExtensionAPI): void {\n\tlet cachedMode = DEFAULT_MODE;\n\tlet cachedSystemPrompt: string | undefined;\n\tlet cachedPlanPath: string | undefined;\n\n\t// ── session_start ─────────────────────────────────────────────────────────\n\t// Config resolution order:\n\t// 1. Read global config (~/.hoocode/hoo-config.json)\n\t// 2. Read project config (./.hoocode/hoo-config.json) if present\n\t// 3. Merge — project scalars win; arrays are unioned\n\t// 4. Re-resolve active_mode from the merged result\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\t// Steps 1–3: merge global + project configs\n\t\tconst config = readMergedConfig(ctx.cwd);\n\n\t\t// Step 4: resolve mode from the merged config\n\t\tcachedMode = config.active_mode ?? DEFAULT_MODE;\n\t\t// External search dirs come from two channels:\n\t\t// - HooConfig.mode_paths (config-declared)\n\t\t// - pi.addModeSearchPath (CLI flags + extension contributions)\n\t\tconst modePaths = mergeSearchPaths(config.mode_paths, pi.getModeSearchPaths());\n\t\tconst rawSystemPrompt = buildSystemPrompt(cachedMode, ctx.cwd, { modePaths });\n\n\t\t// Per-session plan path so concurrent sessions don't overwrite each other.\n\t\t// The `{{PLAN_PATH}}` token in plan-mode templates is substituted here.\n\t\tcachedPlanPath = getPlanPath(ctx.cwd, ctx.sessionManager.getSessionId());\n\t\tconst relPlanPath = relative(ctx.cwd, cachedPlanPath) || cachedPlanPath;\n\t\tcachedSystemPrompt = rawSystemPrompt?.replace(/\\{\\{PLAN_PATH\\}\\}/g, relPlanPath);\n\n\t\t// Update footer with active mode\n\t\tif (ctx.hasUI) {\n\t\t\tctx.ui.setMode(cachedMode);\n\t\t}\n\n\t\t// Apply tool filter from mode enabled_tools\n\t\tconst modeCfg = config.modes?.[cachedMode];\n\t\tif (modeCfg?.enabled_tools && modeCfg.enabled_tools.length > 0) {\n\t\t\tpi.setActiveTools(modeCfg.enabled_tools);\n\t\t}\n\t});\n\n\t// ── before_agent_start ────────────────────────────────────────────────────\n\n\tpi.on(\"before_agent_start\", (event: BeforeAgentStartEvent): BeforeAgentStartEventResult | undefined => {\n\t\tif (!cachedSystemPrompt) return;\n\t\treturn {\n\t\t\tsystemPrompt: `${event.systemPrompt}\\n\\n<!-- hoo-core: mode=${cachedMode} -->\\n${cachedSystemPrompt}`,\n\t\t};\n\t});\n\n\t// ── /mode command ─────────────────────────────────────────────────────────\n\n\tconst KNOWN_MODES = [\"ask\", \"plan\", \"build\", \"debug\"];\n\n\tpi.registerCommand(\"mode\", {\n\t\tdescription: \"Switch active mode. Usage: /mode <ask|plan|build|debug>\",\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\tKNOWN_MODES.filter((m) => m.startsWith(prefix)).map((m) => ({ value: m, label: m })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst name = args.trim();\n\t\t\tif (!name) {\n\t\t\t\tctx.ui.notify(`Active mode: ${cachedMode}`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst config = readConfig();\n\t\t\tconfig.active_mode = name === DEFAULT_MODE ? undefined : name;\n\t\t\twriteConfig(config);\n\t\t\tctx.ui.notify(`Mode set to \"${name}\" — reloading…`, \"info\");\n\t\t\tawait ctx.reload();\n\t\t},\n\t});\n\n\t// ── /plan command (shorthand for /mode plan) ──────────────────────────────\n\n\tpi.registerCommand(\"plan\", {\n\t\tdescription: \"Switch to plan mode. Shorthand for /mode plan.\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (_args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst config = readConfig();\n\t\t\tconfig.active_mode = \"plan\";\n\t\t\twriteConfig(config);\n\t\t\tctx.ui.notify(`Mode set to \"plan\" — reloading…`, \"info\");\n\t\t\tawait ctx.reload();\n\t\t},\n\t});\n\n\t// ── /approve command ──────────────────────────────────────────────────────\n\t// Reads .hoocode/plan.md, parses it into named sections (Goal, Files to\n\t// modify, New files, Tests, Verification), switches to build mode, then\n\t// injects a step-by-step execution message into the new session.\n\n\tpi.registerCommand(\"approve\", {\n\t\tdescription: \"Approve the current plan and switch to build mode to execute it.\",\n\t\tgetArgumentCompletions: () => [],\n\t\thandler: async (_args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tif (cachedMode !== \"plan\") {\n\t\t\t\tctx.ui.notify(`/approve is only available in plan mode (current mode: \"${cachedMode}\")`, \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Prefer the per-session plan file, fall back to the legacy single file.\n\t\t\tconst sessionPlanPath = cachedPlanPath ?? getPlanPath(ctx.cwd, ctx.sessionManager.getSessionId());\n\t\t\tconst candidatePaths = [sessionPlanPath, getLegacyPlanPath(ctx.cwd)];\n\t\t\tlet approveMessage: string | undefined;\n\n\t\t\tfor (const planPath of candidatePaths) {\n\t\t\t\tif (!existsSync(planPath)) continue;\n\t\t\t\ttry {\n\t\t\t\t\tconst raw = readFileSync(planPath, \"utf8\").trim();\n\t\t\t\t\tif (raw) {\n\t\t\t\t\t\tconst sections = parsePlanSections(raw);\n\t\t\t\t\t\tapproveMessage = buildApproveMessage(sections);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\tctx.ui.notify(`Could not read ${relative(ctx.cwd, planPath) || planPath}`, \"error\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Switch global config to build mode\n\t\t\tconst config = readConfig();\n\t\t\tconfig.active_mode = \"build\";\n\t\t\twriteConfig(config);\n\n\t\t\tif (approveMessage) {\n\t\t\t\t// Open a new build-mode session and deliver the parsed plan as the\n\t\t\t\t// first user message so the agent starts executing immediately\n\t\t\t\tawait ctx.newSession({\n\t\t\t\t\twithSession: async (replacedCtx) => {\n\t\t\t\t\t\tawait replacedCtx.sendUserMessage(approveMessage!, { deliverAs: \"followUp\" });\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tconst relPlan = relative(ctx.cwd, sessionPlanPath) || sessionPlanPath;\n\t\t\t\tctx.ui.notify(`Switched to build mode. No ${relPlan} found — describe what to build.`, \"info\");\n\t\t\t\tawait ctx.reload();\n\t\t\t}\n\t\t},\n\t});\n}\n"]}
|