@kolisachint/hoocode-agent 0.4.155 → 0.4.157
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 +73 -0
- package/dist/core/context-files.d.ts +14 -4
- package/dist/core/context-files.d.ts.map +1 -1
- package/dist/core/context-files.js +7 -4
- package/dist/core/context-files.js.map +1 -1
- package/dist/core/format-tokens.d.ts +9 -0
- package/dist/core/format-tokens.d.ts.map +1 -0
- package/dist/core/format-tokens.js +19 -0
- package/dist/core/format-tokens.js.map +1 -0
- package/dist/core/mcp-status.d.ts +32 -0
- package/dist/core/mcp-status.d.ts.map +1 -0
- package/dist/core/mcp-status.js +27 -0
- package/dist/core/mcp-status.js.map +1 -0
- package/dist/core/mode-prompts.d.ts.map +1 -1
- package/dist/core/mode-prompts.js +3 -1
- package/dist/core/mode-prompts.js.map +1 -1
- package/dist/core/resource-loader.d.ts +6 -16
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/wordmark.d.ts +2 -2
- package/dist/core/wordmark.d.ts.map +1 -1
- package/dist/core/wordmark.js +4 -4
- package/dist/core/wordmark.js.map +1 -1
- package/dist/extensions/core/hoo-core.d.ts +3 -0
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +3 -0
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/extensions/core/loop.d.ts +22 -0
- package/dist/extensions/core/loop.d.ts.map +1 -1
- package/dist/extensions/core/loop.js +36 -6
- package/dist/extensions/core/loop.js.map +1 -1
- package/dist/extensions/core/mcp-loader.d.ts.map +1 -1
- package/dist/extensions/core/mcp-loader.js +27 -3
- package/dist/extensions/core/mcp-loader.js.map +1 -1
- package/dist/extensions/core/modes.d.ts +52 -1
- package/dist/extensions/core/modes.d.ts.map +1 -1
- package/dist/extensions/core/modes.js +221 -18
- package/dist/extensions/core/modes.js.map +1 -1
- package/dist/init-templates.generated.d.ts.map +1 -1
- package/dist/init-templates.generated.js +1 -1
- package/dist/init-templates.generated.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +1 -11
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +1 -9
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +14 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +50 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/model-controller.d.ts +6 -0
- package/dist/modes/interactive/model-controller.d.ts.map +1 -1
- package/dist/modes/interactive/model-controller.js +33 -14
- package/dist/modes/interactive/model-controller.js.map +1 -1
- package/dist/modes/interactive/resource-display.d.ts +22 -0
- package/dist/modes/interactive/resource-display.d.ts.map +1 -1
- package/dist/modes/interactive/resource-display.js +73 -16
- package/dist/modes/interactive/resource-display.js.map +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/templates/modes/plan/system.md +4 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wordmark.d.ts","sourceRoot":"","sources":["../../src/core/wordmark.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,eAAO,MAAM,QAAQ,QAQT,CAAC;AAEb,8CAA8C;AAC9C,eAAO,MAAM,gBAAgB,
|
|
1
|
+
{"version":3,"file":"wordmark.d.ts","sourceRoot":"","sources":["../../src/core/wordmark.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,eAAO,MAAM,QAAQ,QAQT,CAAC;AAEb,8CAA8C;AAC9C,eAAO,MAAM,gBAAgB,4BAAuB,CAAC;AASrD,eAAO,MAAM,eAAe,QAEhB,CAAC;AAEb,iFAAiF;AACjF,eAAO,MAAM,cAAc,UAAoC,CAAC;AAKhE,MAAM,WAAW,sBAAsB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gDAAgD;IAChD,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACjC,uDAAuD;IACvD,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9B,iDAAiD;IACjD,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAChC,2EAA2E;IAC3E,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAClC,uEAAuE;IACvE,IAAI,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAkB5E","sourcesContent":["/**\n * ASCII wordmark for the startup banner.\n * Rendered in the terminal with accent color on the \"hoo\" portion.\n * Source: design-system/assets/wordmark.txt\n */\n\nimport { WORDMARK_SYMBOL_BLOCKS } from \"./wordmark-symbol.generated.js\";\n\nexport const WORDMARK = [\n\t\" __ __ ______ __\",\n\t\" / / / /___ ____ / ____/___ ____/ /__\",\n\t\" / /_/ / __ \\\\/ __ \\\\/ / / __ \\\\/ __ / _ \\\\\",\n\t\" / __ / /_/ / /_/ / /___/ /_/ / /_/ / __/\",\n\t\" /_/ /_/\\\\____/\\\\____/\\\\____/\\\\____/\\\\__,_/\\\\___/\",\n\t\"\",\n\t\" coding agent > hoo\",\n].join(\"\\n\");\n\n/** Compact one-line logo for tight spaces. */\nexport const WORDMARK_COMPACT = \"hoo — coding agent\";\n\n/**\n * Colored half-block owl symbol, indented to sit centered above the ASCII\n * wordmark. Carries baked-in brand colors (Signal Cyan + white), so it only\n * renders on truecolor terminals; callers fall back to {@link WORDMARK}\n * otherwise.\n */\nconst SYMBOL_INDENT = \" \".repeat(15);\nexport const WORDMARK_SYMBOL = WORDMARK_SYMBOL_BLOCKS.split(\"\\n\")\n\t.map((line) => SYMBOL_INDENT + line)\n\t.join(\"\\n\");\n\n/** Compact three-line owl glyph rendered beside the brand text in the banner. */\nexport const WORDMARK_GLYPH = [\"▟▀▀▀▀▀▙\", \"▌▟▙ ▟▙▐\", \"▜▄▄▄▄▄▛\"];\n\nconst GLYPH_INDENT = \" \".repeat(3);\nconst GLYPH_GAP = \" \".repeat(2);\n\nexport interface CompactWordmarkOptions {\n\tappName: string;\n\tversion: string;\n\tcwd: string;\n\t/** Tagline shown next to the version. */\n\ttagline?: string;\n\t/** Colorize the brand \"hoo\" portion / glyph. */\n\taccent: (text: string) => string;\n\t/** Colorize secondary text (tagline, version, cwd). */\n\tdim: (text: string) => string;\n\t/** Colorize separators and the glyph outline. */\n\tmuted: (text: string) => string;\n\t/** Render the trailing blinking cursor (e.g. blink + accent). Optional. */\n\tcursor?: (text: string) => string;\n\t/** Optional note appended to the cwd line (e.g. a keybinding hint). */\n\tnote?: () => string;\n}\n\n/**\n * Build the compact startup banner: a small owl glyph beside the brand name,\n * tagline + version, and the working directory.\n *\n * ▟▀▀▀▀▀▙ hoocode\n * ▌▟▙ ▟▙▐ coding agent · v0.1.0\n * ▜▄▄▄▄▄▛ ~/project\n */\nexport function buildCompactWordmark(options: CompactWordmarkOptions): string {\n\tconst { appName, version, cwd, accent, dim, muted } = options;\n\tconst tagline = options.tagline ?? \"coding agent\";\n\n\t// Highlight the \"hoo\" prefix when present, otherwise accent the whole name.\n\tconst name = appName.startsWith(\"hoo\") ? accent(\"hoo\") + muted(\"│\") + appName.slice(3) : accent(appName);\n\tconst brand = options.cursor ? name + options.cursor(\"_\") : name;\n\n\tconst right = [\n\t\tbrand,\n\t\t`${dim(tagline)} ${muted(\"·\")} ${dim(`v${version}`)}`,\n\t\toptions.note ? `${dim(cwd)}${options.note()}` : dim(cwd),\n\t];\n\n\treturn WORDMARK_GLYPH.map((glyphLine, index) => {\n\t\tconst text = right[index] ?? \"\";\n\t\treturn `${GLYPH_INDENT}${accent(glyphLine)}${GLYPH_GAP}${text}`;\n\t}).join(\"\\n\");\n}\n"]}
|
package/dist/core/wordmark.js
CHANGED
|
@@ -11,10 +11,10 @@ export const WORDMARK = [
|
|
|
11
11
|
" / __ / /_/ / /_/ / /___/ /_/ / /_/ / __/",
|
|
12
12
|
" /_/ /_/\\____/\\____/\\____/\\____/\\__,_/\\___/",
|
|
13
13
|
"",
|
|
14
|
-
"
|
|
14
|
+
" coding agent > hoo",
|
|
15
15
|
].join("\n");
|
|
16
16
|
/** Compact one-line logo for tight spaces. */
|
|
17
|
-
export const WORDMARK_COMPACT = "hoo —
|
|
17
|
+
export const WORDMARK_COMPACT = "hoo — coding agent";
|
|
18
18
|
/**
|
|
19
19
|
* Colored half-block owl symbol, indented to sit centered above the ASCII
|
|
20
20
|
* wordmark. Carries baked-in brand colors (Signal Cyan + white), so it only
|
|
@@ -34,12 +34,12 @@ const GLYPH_GAP = " ".repeat(2);
|
|
|
34
34
|
* tagline + version, and the working directory.
|
|
35
35
|
*
|
|
36
36
|
* ▟▀▀▀▀▀▙ hoocode
|
|
37
|
-
* ▌▟▙ ▟▙▐
|
|
37
|
+
* ▌▟▙ ▟▙▐ coding agent · v0.1.0
|
|
38
38
|
* ▜▄▄▄▄▄▛ ~/project
|
|
39
39
|
*/
|
|
40
40
|
export function buildCompactWordmark(options) {
|
|
41
41
|
const { appName, version, cwd, accent, dim, muted } = options;
|
|
42
|
-
const tagline = options.tagline ?? "
|
|
42
|
+
const tagline = options.tagline ?? "coding agent";
|
|
43
43
|
// Highlight the "hoo" prefix when present, otherwise accent the whole name.
|
|
44
44
|
const name = appName.startsWith("hoo") ? accent("hoo") + muted("│") + appName.slice(3) : accent(appName);
|
|
45
45
|
const brand = options.cursor ? name + options.cursor("_") : name;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wordmark.js","sourceRoot":"","sources":["../../src/core/wordmark.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAExE,MAAM,CAAC,MAAM,QAAQ,GAAG;IACvB,yDAAyD;IACzD,2DAA2D;IAC3D,gEAAgE;IAChE,4DAA4D;IAC5D,iEAAiE;IACjE,EAAE;IACF,
|
|
1
|
+
{"version":3,"file":"wordmark.js","sourceRoot":"","sources":["../../src/core/wordmark.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAExE,MAAM,CAAC,MAAM,QAAQ,GAAG;IACvB,yDAAyD;IACzD,2DAA2D;IAC3D,gEAAgE;IAChE,4DAA4D;IAC5D,iEAAiE;IACjE,EAAE;IACF,sCAAsC;CACtC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,8CAA8C;AAC9C,MAAM,CAAC,MAAM,gBAAgB,GAAG,sBAAoB,CAAC;AAErD;;;;;GAKG;AACH,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AACrC,MAAM,CAAC,MAAM,eAAe,GAAG,sBAAsB,CAAC,KAAK,CAAC,IAAI,CAAC;KAC/D,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,GAAG,IAAI,CAAC;KACnC,IAAI,CAAC,IAAI,CAAC,CAAC;AAEb,iFAAiF;AACjF,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,uBAAS,EAAE,qBAAS,EAAE,uBAAS,CAAC,CAAC;AAEhE,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAoBhC;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAA+B,EAAU;IAC7E,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAC9D,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,cAAc,CAAC;IAElD,4EAA4E;IAC5E,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,KAAG,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACzG,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEjE,MAAM,KAAK,GAAG;QACb,KAAK;QACL,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,IAAG,CAAC,IAAI,GAAG,CAAC,IAAI,OAAO,EAAE,CAAC,EAAE;QACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;KACxD,CAAC;IAEF,OAAO,cAAc,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC;QAC/C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAChC,OAAO,GAAG,YAAY,GAAG,MAAM,CAAC,SAAS,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,CAAC;IAAA,CAChE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACd","sourcesContent":["/**\n * ASCII wordmark for the startup banner.\n * Rendered in the terminal with accent color on the \"hoo\" portion.\n * Source: design-system/assets/wordmark.txt\n */\n\nimport { WORDMARK_SYMBOL_BLOCKS } from \"./wordmark-symbol.generated.js\";\n\nexport const WORDMARK = [\n\t\" __ __ ______ __\",\n\t\" / / / /___ ____ / ____/___ ____/ /__\",\n\t\" / /_/ / __ \\\\/ __ \\\\/ / / __ \\\\/ __ / _ \\\\\",\n\t\" / __ / /_/ / /_/ / /___/ /_/ / /_/ / __/\",\n\t\" /_/ /_/\\\\____/\\\\____/\\\\____/\\\\____/\\\\__,_/\\\\___/\",\n\t\"\",\n\t\" coding agent > hoo\",\n].join(\"\\n\");\n\n/** Compact one-line logo for tight spaces. */\nexport const WORDMARK_COMPACT = \"hoo — coding agent\";\n\n/**\n * Colored half-block owl symbol, indented to sit centered above the ASCII\n * wordmark. Carries baked-in brand colors (Signal Cyan + white), so it only\n * renders on truecolor terminals; callers fall back to {@link WORDMARK}\n * otherwise.\n */\nconst SYMBOL_INDENT = \" \".repeat(15);\nexport const WORDMARK_SYMBOL = WORDMARK_SYMBOL_BLOCKS.split(\"\\n\")\n\t.map((line) => SYMBOL_INDENT + line)\n\t.join(\"\\n\");\n\n/** Compact three-line owl glyph rendered beside the brand text in the banner. */\nexport const WORDMARK_GLYPH = [\"▟▀▀▀▀▀▙\", \"▌▟▙ ▟▙▐\", \"▜▄▄▄▄▄▛\"];\n\nconst GLYPH_INDENT = \" \".repeat(3);\nconst GLYPH_GAP = \" \".repeat(2);\n\nexport interface CompactWordmarkOptions {\n\tappName: string;\n\tversion: string;\n\tcwd: string;\n\t/** Tagline shown next to the version. */\n\ttagline?: string;\n\t/** Colorize the brand \"hoo\" portion / glyph. */\n\taccent: (text: string) => string;\n\t/** Colorize secondary text (tagline, version, cwd). */\n\tdim: (text: string) => string;\n\t/** Colorize separators and the glyph outline. */\n\tmuted: (text: string) => string;\n\t/** Render the trailing blinking cursor (e.g. blink + accent). Optional. */\n\tcursor?: (text: string) => string;\n\t/** Optional note appended to the cwd line (e.g. a keybinding hint). */\n\tnote?: () => string;\n}\n\n/**\n * Build the compact startup banner: a small owl glyph beside the brand name,\n * tagline + version, and the working directory.\n *\n * ▟▀▀▀▀▀▙ hoocode\n * ▌▟▙ ▟▙▐ coding agent · v0.1.0\n * ▜▄▄▄▄▄▛ ~/project\n */\nexport function buildCompactWordmark(options: CompactWordmarkOptions): string {\n\tconst { appName, version, cwd, accent, dim, muted } = options;\n\tconst tagline = options.tagline ?? \"coding agent\";\n\n\t// Highlight the \"hoo\" prefix when present, otherwise accent the whole name.\n\tconst name = appName.startsWith(\"hoo\") ? accent(\"hoo\") + muted(\"│\") + appName.slice(3) : accent(appName);\n\tconst brand = options.cursor ? name + options.cursor(\"_\") : name;\n\n\tconst right = [\n\t\tbrand,\n\t\t`${dim(tagline)} ${muted(\"·\")} ${dim(`v${version}`)}`,\n\t\toptions.note ? `${dim(cwd)}${options.note()}` : dim(cwd),\n\t];\n\n\treturn WORDMARK_GLYPH.map((glyphLine, index) => {\n\t\tconst text = right[index] ?? \"\";\n\t\treturn `${GLYPH_INDENT}${accent(glyphLine)}${GLYPH_GAP}${text}`;\n\t}).join(\"\\n\");\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hoo-core.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/hoo-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAYnE,iBAAS,OAAO,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAWvC
|
|
1
|
+
{"version":3,"file":"hoo-core.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/hoo-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAYnE,iBAAS,OAAO,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAWvC;;;;;;;AAMD,eAAe,OAAO,CAAC","sourcesContent":["/**\n * hoo-core — HooCode built-in core extension (composition root).\n *\n * Each concern lives in its own module in this directory:\n * - permission-gate.ts — prompts before bash/write/edit; hard tool/command policy\n * - mcp-loader.ts — discovers MCP server configs, connects, registers tools\n * - modes.ts — active mode resolution, mode prompts, /mode /plan /approve\n * - cost.ts — /cost session token + cost totals\n * - scaffold.ts — /new-skill /new-agent /new-command\n * - ask-options.ts — the ask_options tool (inline decision pane)\n * - thinking-escalation.ts — raise thinking after tool errors, then restore\n * - loop.ts — /loop cron scheduling + autonomous continuation\n * - marketplace.ts — /plugin marketplace + install/remove\n * - prompt-reactive/ — runtime plugin-reuse nudge (reactive to tool/turn cues)\n * - config.ts — hoo-config.json types, I/O, and merge rules\n *\n * `bin/hoocode.js` loads this module's default export as the built-in\n * extension factory.\n */\n\nimport type { ExtensionAPI } from \"../../core/extensions/types.js\";\nimport { setupAskOptions } from \"./ask-options.js\";\nimport { setupCost } from \"./cost.js\";\nimport { setupLoop } from \"./loop.js\";\nimport { setupMarketplace } from \"./marketplace.js\";\nimport { setupMcpLoader } from \"./mcp-loader.js\";\nimport { setupMode } from \"./modes.js\";\nimport { setupPermissionGate } from \"./permission-gate.js\";\nimport { setupPromptReactiveNudges } from \"./prompt-reactive/nudges.js\";\nimport { setupScaffold } from \"./scaffold.js\";\nimport { setupThinkingEscalation } from \"./thinking-escalation.js\";\n\nfunction hooCore(pi: ExtensionAPI): void {\n\tsetupPermissionGate(pi);\n\tsetupMcpLoader(pi);\n\tsetupMode(pi);\n\tsetupCost(pi);\n\tsetupScaffold(pi);\n\tsetupAskOptions(pi);\n\tsetupThinkingEscalation(pi);\n\tsetupLoop(pi);\n\tsetupMarketplace(pi);\n\tsetupPromptReactiveNudges(pi);\n}\n\nhooCore.displayName = \"hoo-core\";\n// Built-in plumbing, not a user-installed extension: it is always present, so\n// listing it in the startup resource summary is noise, not information.\nhooCore.internal = true;\nexport default hooCore;\n"]}
|
|
@@ -40,5 +40,8 @@ function hooCore(pi) {
|
|
|
40
40
|
setupPromptReactiveNudges(pi);
|
|
41
41
|
}
|
|
42
42
|
hooCore.displayName = "hoo-core";
|
|
43
|
+
// Built-in plumbing, not a user-installed extension: it is always present, so
|
|
44
|
+
// listing it in the startup resource summary is noise, not information.
|
|
45
|
+
hooCore.internal = true;
|
|
43
46
|
export default hooCore;
|
|
44
47
|
//# sourceMappingURL=hoo-core.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hoo-core.js","sourceRoot":"","sources":["../../../src/extensions/core/hoo-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,SAAS,OAAO,CAAC,EAAgB,EAAQ;IACxC,mBAAmB,CAAC,EAAE,CAAC,CAAC;IACxB,cAAc,CAAC,EAAE,CAAC,CAAC;IACnB,SAAS,CAAC,EAAE,CAAC,CAAC;IACd,SAAS,CAAC,EAAE,CAAC,CAAC;IACd,aAAa,CAAC,EAAE,CAAC,CAAC;IAClB,eAAe,CAAC,EAAE,CAAC,CAAC;IACpB,uBAAuB,CAAC,EAAE,CAAC,CAAC;IAC5B,SAAS,CAAC,EAAE,CAAC,CAAC;IACd,gBAAgB,CAAC,EAAE,CAAC,CAAC;IACrB,yBAAyB,CAAC,EAAE,CAAC,CAAC;AAAA,CAC9B;AAED,OAAO,CAAC,WAAW,GAAG,UAAU,CAAC;AACjC,eAAe,OAAO,CAAC","sourcesContent":["/**\n * hoo-core — HooCode built-in core extension (composition root).\n *\n * Each concern lives in its own module in this directory:\n * - permission-gate.ts — prompts before bash/write/edit; hard tool/command policy\n * - mcp-loader.ts — discovers MCP server configs, connects, registers tools\n * - modes.ts — active mode resolution, mode prompts, /mode /plan /approve\n * - cost.ts — /cost session token + cost totals\n * - scaffold.ts — /new-skill /new-agent /new-command\n * - ask-options.ts — the ask_options tool (inline decision pane)\n * - thinking-escalation.ts — raise thinking after tool errors, then restore\n * - loop.ts — /loop cron scheduling + autonomous continuation\n * - marketplace.ts — /plugin marketplace + install/remove\n * - prompt-reactive/ — runtime plugin-reuse nudge (reactive to tool/turn cues)\n * - config.ts — hoo-config.json types, I/O, and merge rules\n *\n * `bin/hoocode.js` loads this module's default export as the built-in\n * extension factory.\n */\n\nimport type { ExtensionAPI } from \"../../core/extensions/types.js\";\nimport { setupAskOptions } from \"./ask-options.js\";\nimport { setupCost } from \"./cost.js\";\nimport { setupLoop } from \"./loop.js\";\nimport { setupMarketplace } from \"./marketplace.js\";\nimport { setupMcpLoader } from \"./mcp-loader.js\";\nimport { setupMode } from \"./modes.js\";\nimport { setupPermissionGate } from \"./permission-gate.js\";\nimport { setupPromptReactiveNudges } from \"./prompt-reactive/nudges.js\";\nimport { setupScaffold } from \"./scaffold.js\";\nimport { setupThinkingEscalation } from \"./thinking-escalation.js\";\n\nfunction hooCore(pi: ExtensionAPI): void {\n\tsetupPermissionGate(pi);\n\tsetupMcpLoader(pi);\n\tsetupMode(pi);\n\tsetupCost(pi);\n\tsetupScaffold(pi);\n\tsetupAskOptions(pi);\n\tsetupThinkingEscalation(pi);\n\tsetupLoop(pi);\n\tsetupMarketplace(pi);\n\tsetupPromptReactiveNudges(pi);\n}\n\nhooCore.displayName = \"hoo-core\";\nexport default hooCore;\n"]}
|
|
1
|
+
{"version":3,"file":"hoo-core.js","sourceRoot":"","sources":["../../../src/extensions/core/hoo-core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AACvC,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,SAAS,OAAO,CAAC,EAAgB,EAAQ;IACxC,mBAAmB,CAAC,EAAE,CAAC,CAAC;IACxB,cAAc,CAAC,EAAE,CAAC,CAAC;IACnB,SAAS,CAAC,EAAE,CAAC,CAAC;IACd,SAAS,CAAC,EAAE,CAAC,CAAC;IACd,aAAa,CAAC,EAAE,CAAC,CAAC;IAClB,eAAe,CAAC,EAAE,CAAC,CAAC;IACpB,uBAAuB,CAAC,EAAE,CAAC,CAAC;IAC5B,SAAS,CAAC,EAAE,CAAC,CAAC;IACd,gBAAgB,CAAC,EAAE,CAAC,CAAC;IACrB,yBAAyB,CAAC,EAAE,CAAC,CAAC;AAAA,CAC9B;AAED,OAAO,CAAC,WAAW,GAAG,UAAU,CAAC;AACjC,8EAA8E;AAC9E,wEAAwE;AACxE,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AACxB,eAAe,OAAO,CAAC","sourcesContent":["/**\n * hoo-core — HooCode built-in core extension (composition root).\n *\n * Each concern lives in its own module in this directory:\n * - permission-gate.ts — prompts before bash/write/edit; hard tool/command policy\n * - mcp-loader.ts — discovers MCP server configs, connects, registers tools\n * - modes.ts — active mode resolution, mode prompts, /mode /plan /approve\n * - cost.ts — /cost session token + cost totals\n * - scaffold.ts — /new-skill /new-agent /new-command\n * - ask-options.ts — the ask_options tool (inline decision pane)\n * - thinking-escalation.ts — raise thinking after tool errors, then restore\n * - loop.ts — /loop cron scheduling + autonomous continuation\n * - marketplace.ts — /plugin marketplace + install/remove\n * - prompt-reactive/ — runtime plugin-reuse nudge (reactive to tool/turn cues)\n * - config.ts — hoo-config.json types, I/O, and merge rules\n *\n * `bin/hoocode.js` loads this module's default export as the built-in\n * extension factory.\n */\n\nimport type { ExtensionAPI } from \"../../core/extensions/types.js\";\nimport { setupAskOptions } from \"./ask-options.js\";\nimport { setupCost } from \"./cost.js\";\nimport { setupLoop } from \"./loop.js\";\nimport { setupMarketplace } from \"./marketplace.js\";\nimport { setupMcpLoader } from \"./mcp-loader.js\";\nimport { setupMode } from \"./modes.js\";\nimport { setupPermissionGate } from \"./permission-gate.js\";\nimport { setupPromptReactiveNudges } from \"./prompt-reactive/nudges.js\";\nimport { setupScaffold } from \"./scaffold.js\";\nimport { setupThinkingEscalation } from \"./thinking-escalation.js\";\n\nfunction hooCore(pi: ExtensionAPI): void {\n\tsetupPermissionGate(pi);\n\tsetupMcpLoader(pi);\n\tsetupMode(pi);\n\tsetupCost(pi);\n\tsetupScaffold(pi);\n\tsetupAskOptions(pi);\n\tsetupThinkingEscalation(pi);\n\tsetupLoop(pi);\n\tsetupMarketplace(pi);\n\tsetupPromptReactiveNudges(pi);\n}\n\nhooCore.displayName = \"hoo-core\";\n// Built-in plumbing, not a user-installed extension: it is always present, so\n// listing it in the startup resource summary is noise, not information.\nhooCore.internal = true;\nexport default hooCore;\n"]}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE
|
|
12
12
|
*/
|
|
13
13
|
import type { ExtensionAPI } from "../../core/extensions/types.js";
|
|
14
|
+
/** Sentinel the agent replies with to declare the autonomous loop finished. */
|
|
15
|
+
export declare const AUTO_LOOP_DONE_TOKEN = "LOOP_DONE";
|
|
14
16
|
/**
|
|
15
17
|
* Event-bus channel: the autonomous-loop active state changed.
|
|
16
18
|
* Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops
|
|
@@ -23,5 +25,25 @@ export declare const LOOP_AUTO_CHANGED = "loop:auto-changed";
|
|
|
23
25
|
* blocker that requires a human decision the loop cannot safely make on its own.
|
|
24
26
|
*/
|
|
25
27
|
export declare const LOOP_HALT = "loop:halt";
|
|
28
|
+
/**
|
|
29
|
+
* Event-bus channel: start the autonomous loop.
|
|
30
|
+
* Payload: {@link LoopAutoStartPayload}. Mirrors LOOP_HALT in the opposite
|
|
31
|
+
* direction, letting another extension (e.g. `/goal`) drive the loop without
|
|
32
|
+
* reaching into state that lives inside this module's closure.
|
|
33
|
+
*/
|
|
34
|
+
export declare const LOOP_AUTO_START = "loop:auto-start";
|
|
35
|
+
/** Payload for {@link LOOP_AUTO_START}. */
|
|
36
|
+
export interface LoopAutoStartPayload {
|
|
37
|
+
/** The task to work toward, delivered as the loop's first user message. */
|
|
38
|
+
task: string;
|
|
39
|
+
/** Turn budget before the loop stops itself. Defaults to 10. */
|
|
40
|
+
maxTurns?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Message re-sent on each continuation. Callers with a concrete completion
|
|
43
|
+
* condition should restate it here, so the target does not drift out of view
|
|
44
|
+
* as the transcript grows. Defaults to a generic nudge.
|
|
45
|
+
*/
|
|
46
|
+
continuePrompt?: string;
|
|
47
|
+
}
|
|
26
48
|
export declare function setupLoop(pi: ExtensionAPI): void;
|
|
27
49
|
//# sourceMappingURL=loop.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EACX,YAAY,EAIZ,MAAM,gCAAgC,CAAC;AAOxC;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,SAAS,cAAc,CAAC;AAkCrC,wBAAgB,SAAS,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAsOhD","sourcesContent":["/**\n * /loop — cron scheduler, Cron* tools, and autonomous continuation.\n *\n * `/loop` schedules prompts via cron and drives autonomous continuation. The same\n * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.\n *\n * /loop \"<cron>\" <prompt> schedule recurring (5-field cron, local time)\n * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval\n * /loop once \"<cron>\" <prompt> schedule a one-shot\n * /loop list | /loop delete <id> | /loop stop\n * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE\n */\n\nimport { join } from \"node:path\";\nimport { Type } from \"typebox\";\nimport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { defineTool } from \"../../core/extensions/types.js\";\nimport { TaskScheduler } from \"../../core/scheduler.js\";\n\nconst AUTO_LOOP_DONE_TOKEN = \"LOOP_DONE\";\nconst DEFAULT_AUTO_MAX_TURNS = 10;\n\n/**\n * Event-bus channel: the autonomous-loop active state changed.\n * Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops\n * so other extensions (e.g. ask_options) can adapt to running unattended.\n */\nexport const LOOP_AUTO_CHANGED = \"loop:auto-changed\";\n\n/**\n * Event-bus channel: request to halt the autonomous loop.\n * Payload: `{ reason: string }`. Sent by another extension when it hits a\n * blocker that requires a human decision the loop cannot safely make on its own.\n */\nexport const LOOP_HALT = \"loop:halt\";\n\n/** Convert a simple interval token (\"5m\", \"2h\", \"1d\") to a 5-field cron, or null. */\nfunction intervalToCron(token: string): string | null {\n\tconst m = /^(\\d+)(m|h|d)$/.exec(token.trim());\n\tif (!m) return null;\n\tconst n = Number(m[1]);\n\tif (n < 1) return null;\n\tif (m[2] === \"m\") return `*/${n} * * * *`;\n\tif (m[2] === \"h\") return `0 */${n} * * *`;\n\treturn `0 0 */${n} * *`; // days\n}\n\n/** Pull a quoted cron expression off the front of an argument string. */\nfunction extractQuotedCron(args: string): { cron: string; rest: string } | null {\n\tconst m = /^\"([^\"]+)\"\\s*(.*)$/.exec(args.trim());\n\treturn m ? { cron: m[1].trim(), rest: m[2].trim() } : null;\n}\n\nfunction isFiveFieldCron(expr: string): boolean {\n\treturn expr.trim().split(/\\s+/).length === 5;\n}\n\n/** Flatten an assistant message's text blocks. */\nfunction assistantText(message: { content: unknown }): string {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((b): b is { type: \"text\"; text: string } => !!b && (b as { type?: string }).type === \"text\")\n\t\t.map((b) => b.text)\n\t\t.join(\"\\n\");\n}\n\nexport function setupLoop(pi: ExtensionAPI): void {\n\tlet scheduler: TaskScheduler | undefined;\n\tlet auto: { remaining: number } | null = null;\n\tlet activeCtx: ExtensionContext | undefined;\n\n\t/** Set the autonomous-loop state and broadcast the active flag on the bus. */\n\tfunction setAuto(next: { remaining: number } | null): void {\n\t\tconst was = auto !== null;\n\t\tauto = next;\n\t\tif (was !== (next !== null)) pi.events.emit(LOOP_AUTO_CHANGED, { active: next !== null });\n\t}\n\n\t// Another extension (e.g. ask_options) hit a decision it cannot safely make\n\t// while unattended. Stop iterating and let the model report the blocker.\n\tpi.events.on(LOOP_HALT, (data) => {\n\t\tif (!auto) return;\n\t\tconst reason = (data as { reason?: string })?.reason?.trim() || \"a decision that needs the user.\";\n\t\tsetAuto(null);\n\t\tactiveCtx?.ui.notify(`Autonomous loop halted: ${reason}`, \"warning\");\n\t});\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t\tif (scheduler) return;\n\t\tconst isIdle = () => {\n\t\t\ttry {\n\t\t\t\treturn ctx.isIdle();\n\t\t\t} catch {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t\tscheduler = new TaskScheduler({\n\t\t\t// `.agents/` is the primary, cross-vendor home; the legacy `.hoocode/`\n\t\t\t// store is read once and migrates forward on the next persist.\n\t\t\tstorePath: join(ctx.cwd, \".agents\", \"scheduled_tasks.json\"),\n\t\t\tlegacyStorePath: join(ctx.cwd, \".hoocode\", \"scheduled_tasks.json\"),\n\t\t\tfire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: \"followUp\" }),\n\t\t\tisIdle,\n\t\t});\n\t\tscheduler.start();\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tscheduler?.stop();\n\t\tsetAuto(null);\n\t});\n\n\t// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.\n\tpi.on(\"agent_end\", (event, ctx) => {\n\t\tif (!auto) return;\n\t\tif (ctx.hasPendingMessages()) return; // user is steering — yield\n\t\tconst last = [...event.messages].reverse().find((m) => m.role === \"assistant\");\n\t\tconst text = last ? assistantText(last) : \"\";\n\t\tif (text.includes(AUTO_LOOP_DONE_TOKEN)) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop complete.\", \"info\");\n\t\t\treturn;\n\t\t}\n\t\tif (auto.remaining <= 0) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop stopped: max turns reached.\", \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tauto.remaining -= 1;\n\t\tpi.sendUserMessage(`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`, {\n\t\t\tdeliverAs: \"followUp\",\n\t\t});\n\t});\n\n\t// ── Cron* tools (agent-callable) ──────────────────────────────────────────\n\tconst toolText = (s: string) => ({ content: [{ type: \"text\" as const, text: s }], details: undefined });\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronCreate\",\n\t\t\tlabel: \"Schedule Task\",\n\t\t\tdescription:\n\t\t\t\t\"Schedule a prompt to be re-submitted on a cron schedule (5-field, local time: minute hour day-of-month month day-of-week). recurring=false fires once then deletes.\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tcron: Type.String({ description: \"5-field cron expression in local time\" }),\n\t\t\t\tprompt: Type.String({ description: \"Prompt to enqueue at each fire time\" }),\n\t\t\t\trecurring: Type.Optional(Type.Boolean({ description: \"Fire repeatedly (default true) or once\" })),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tif (!scheduler) return toolText(\"Scheduler not ready.\");\n\t\t\t\tif (!isFiveFieldCron(params.cron)) return toolText(`Invalid cron \"${params.cron}\" (need 5 fields).`);\n\t\t\t\tconst task = scheduler.create({\n\t\t\t\t\tcron: params.cron,\n\t\t\t\t\tprompt: params.prompt,\n\t\t\t\t\trecurring: params.recurring ?? true,\n\t\t\t\t});\n\t\t\t\treturn toolText(`Scheduled ${task.id}: \"${task.cron}\" (${task.recurring ? \"recurring\" : \"once\"})`);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronList\",\n\t\t\tlabel: \"List Scheduled Tasks\",\n\t\t\tdescription: \"List all scheduled tasks (id, cron, recurring, prompt).\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst tasks = scheduler?.list() ?? [];\n\t\t\t\tif (tasks.length === 0) return toolText(\"No scheduled tasks.\");\n\t\t\t\treturn toolText(\n\t\t\t\t\ttasks\n\t\t\t\t\t\t.map((t) => `${t.id} ${t.cron} ${t.recurring ? \"recurring\" : \"once\"} ${JSON.stringify(t.prompt)}`)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronDelete\",\n\t\t\tlabel: \"Delete Scheduled Task\",\n\t\t\tdescription: \"Delete a scheduled task by id.\",\n\t\t\tparameters: Type.Object({ id: Type.String({ description: \"Task id from CronCreate/CronList\" }) }),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst removed = scheduler?.delete(params.id) ?? false;\n\t\t\t\treturn toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);\n\t\t\t},\n\t\t}),\n\t);\n\n\t// ── /loop command ─────────────────────────────────────────────────────────\n\tpi.registerCommand(\"loop\", {\n\t\tdescription:\n\t\t\t'Schedule prompts via cron or run an autonomous loop. /loop \"<cron>\" <prompt> | /loop <5m|2h> <prompt> | /loop once \"<cron>\" <prompt> | /loop list | /loop delete <id> | /loop stop | /loop auto [--max-turns N] <task>',\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"list\", \"delete\", \"stop\", \"once\", \"auto\"]\n\t\t\t\t.filter((s) => s.startsWith(prefix))\n\t\t\t\t.map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tif (!scheduler) {\n\t\t\t\tctx.ui.notify(\"Scheduler not ready yet.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed || trimmed === \"list\") {\n\t\t\t\tconst tasks = scheduler.list();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\ttasks.length === 0\n\t\t\t\t\t\t? auto\n\t\t\t\t\t\t\t? `Autonomous loop active (${auto.remaining} turns left).`\n\t\t\t\t\t\t\t: \"No scheduled tasks.\"\n\t\t\t\t\t\t: tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? \"\" : \"(once) \"}— ${t.prompt}`).join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed === \"stop\") {\n\t\t\t\tconst had = scheduler.list().length > 0 || auto !== null;\n\t\t\t\tscheduler.clear();\n\t\t\t\tsetAuto(null);\n\t\t\t\tctx.ui.notify(had ? \"Stopped all loops and scheduled tasks.\" : \"Nothing to stop.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"delete\")) {\n\t\t\t\tconst id = trimmed.slice(\"delete\".length).trim();\n\t\t\t\tif (!id) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop delete <id>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"auto\")) {\n\t\t\t\tlet rest = trimmed.slice(\"auto\".length).trim();\n\t\t\t\tlet maxTurns = DEFAULT_AUTO_MAX_TURNS;\n\t\t\t\tconst flag = /^--max-turns\\s+(\\d+)\\s*(.*)$/.exec(rest);\n\t\t\t\tif (flag) {\n\t\t\t\t\tmaxTurns = Number(flag[1]);\n\t\t\t\t\trest = flag[2].trim();\n\t\t\t\t}\n\t\t\t\tif (!rest) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop auto [--max-turns N] <task>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsetAuto({ remaining: maxTurns });\n\t\t\t\tpi.sendUserMessage(\n\t\t\t\t\t`${rest}\\n\\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`,\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t\tctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Scheduling: one-shot or recurring, via quoted cron or interval token.\n\t\t\tlet recurring = true;\n\t\t\tlet body = trimmed;\n\t\t\tif (body.startsWith(\"once\")) {\n\t\t\t\trecurring = false;\n\t\t\t\tbody = body.slice(\"once\".length).trim();\n\t\t\t}\n\n\t\t\tlet cron: string | null = null;\n\t\t\tlet prompt = \"\";\n\t\t\tconst quoted = extractQuotedCron(body);\n\t\t\tif (quoted) {\n\t\t\t\tcron = quoted.cron;\n\t\t\t\tprompt = quoted.rest;\n\t\t\t} else {\n\t\t\t\tconst [first, ...restWords] = body.split(/\\s+/);\n\t\t\t\tcron = intervalToCron(first);\n\t\t\t\tprompt = restWords.join(\" \").trim();\n\t\t\t}\n\n\t\t\tif (!cron || !isFiveFieldCron(cron)) {\n\t\t\t\tctx.ui.notify('Usage: /loop \"<cron>\" <prompt> or /loop <5m|2h|1d> <prompt>', \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!prompt) {\n\t\t\t\tctx.ui.notify(\"Nothing to schedule — provide a prompt after the schedule.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst task = scheduler.create({ cron, prompt, recurring });\n\t\t\tctx.ui.notify(\n\t\t\t\t`Scheduled ${task.id}: \"${cron}\" ${recurring ? \"recurring\" : \"once\"} — \"${prompt}\". Manage with /loop list • /loop delete ${task.id}.`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EACX,YAAY,EAIZ,MAAM,gCAAgC,CAAC;AAIxC,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,cAAc,CAAC;AAGhD;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,SAAS,cAAc,CAAC;AAErC;;;;;GAKG;AACH,eAAO,MAAM,eAAe,oBAAoB,CAAC;AAEjD,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACpC,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAkCD,wBAAgB,SAAS,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAiQhD","sourcesContent":["/**\n * /loop — cron scheduler, Cron* tools, and autonomous continuation.\n *\n * `/loop` schedules prompts via cron and drives autonomous continuation. The same\n * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.\n *\n * /loop \"<cron>\" <prompt> schedule recurring (5-field cron, local time)\n * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval\n * /loop once \"<cron>\" <prompt> schedule a one-shot\n * /loop list | /loop delete <id> | /loop stop\n * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE\n */\n\nimport { join } from \"node:path\";\nimport { Type } from \"typebox\";\nimport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { defineTool } from \"../../core/extensions/types.js\";\nimport { TaskScheduler } from \"../../core/scheduler.js\";\n\n/** Sentinel the agent replies with to declare the autonomous loop finished. */\nexport const AUTO_LOOP_DONE_TOKEN = \"LOOP_DONE\";\nconst DEFAULT_AUTO_MAX_TURNS = 10;\n\n/**\n * Event-bus channel: the autonomous-loop active state changed.\n * Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops\n * so other extensions (e.g. ask_options) can adapt to running unattended.\n */\nexport const LOOP_AUTO_CHANGED = \"loop:auto-changed\";\n\n/**\n * Event-bus channel: request to halt the autonomous loop.\n * Payload: `{ reason: string }`. Sent by another extension when it hits a\n * blocker that requires a human decision the loop cannot safely make on its own.\n */\nexport const LOOP_HALT = \"loop:halt\";\n\n/**\n * Event-bus channel: start the autonomous loop.\n * Payload: {@link LoopAutoStartPayload}. Mirrors LOOP_HALT in the opposite\n * direction, letting another extension (e.g. `/goal`) drive the loop without\n * reaching into state that lives inside this module's closure.\n */\nexport const LOOP_AUTO_START = \"loop:auto-start\";\n\n/** Payload for {@link LOOP_AUTO_START}. */\nexport interface LoopAutoStartPayload {\n\t/** The task to work toward, delivered as the loop's first user message. */\n\ttask: string;\n\t/** Turn budget before the loop stops itself. Defaults to 10. */\n\tmaxTurns?: number;\n\t/**\n\t * Message re-sent on each continuation. Callers with a concrete completion\n\t * condition should restate it here, so the target does not drift out of view\n\t * as the transcript grows. Defaults to a generic nudge.\n\t */\n\tcontinuePrompt?: string;\n}\n\n/** Convert a simple interval token (\"5m\", \"2h\", \"1d\") to a 5-field cron, or null. */\nfunction intervalToCron(token: string): string | null {\n\tconst m = /^(\\d+)(m|h|d)$/.exec(token.trim());\n\tif (!m) return null;\n\tconst n = Number(m[1]);\n\tif (n < 1) return null;\n\tif (m[2] === \"m\") return `*/${n} * * * *`;\n\tif (m[2] === \"h\") return `0 */${n} * * *`;\n\treturn `0 0 */${n} * *`; // days\n}\n\n/** Pull a quoted cron expression off the front of an argument string. */\nfunction extractQuotedCron(args: string): { cron: string; rest: string } | null {\n\tconst m = /^\"([^\"]+)\"\\s*(.*)$/.exec(args.trim());\n\treturn m ? { cron: m[1].trim(), rest: m[2].trim() } : null;\n}\n\nfunction isFiveFieldCron(expr: string): boolean {\n\treturn expr.trim().split(/\\s+/).length === 5;\n}\n\n/** Flatten an assistant message's text blocks. */\nfunction assistantText(message: { content: unknown }): string {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((b): b is { type: \"text\"; text: string } => !!b && (b as { type?: string }).type === \"text\")\n\t\t.map((b) => b.text)\n\t\t.join(\"\\n\");\n}\n\nexport function setupLoop(pi: ExtensionAPI): void {\n\tlet scheduler: TaskScheduler | undefined;\n\tlet auto: { remaining: number; continuePrompt?: string } | null = null;\n\tlet activeCtx: ExtensionContext | undefined;\n\n\t/** Set the autonomous-loop state and broadcast the active flag on the bus. */\n\tfunction setAuto(next: { remaining: number; continuePrompt?: string } | null): void {\n\t\tconst was = auto !== null;\n\t\tauto = next;\n\t\tif (was !== (next !== null)) pi.events.emit(LOOP_AUTO_CHANGED, { active: next !== null });\n\t}\n\n\t/**\n\t * Arms the turn budget and delivers the task. Shared by `/loop auto` and the\n\t * LOOP_AUTO_START channel so both start from identical state. Returns the\n\t * budget actually applied, for the caller's notification.\n\t *\n\t * A caller-supplied 0 is honoured (the loop stops at the first continuation);\n\t * only a missing or nonsensical budget falls back to the default.\n\t */\n\tfunction startAuto({ task, maxTurns, continuePrompt }: LoopAutoStartPayload): number {\n\t\tconst budget =\n\t\t\ttypeof maxTurns === \"number\" && Number.isFinite(maxTurns) && maxTurns >= 0 ? maxTurns : DEFAULT_AUTO_MAX_TURNS;\n\t\tsetAuto({ remaining: budget, continuePrompt });\n\t\tpi.sendUserMessage(\n\t\t\t`${task}\\n\\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`,\n\t\t\t{ deliverAs: \"followUp\" },\n\t\t);\n\t\treturn budget;\n\t}\n\n\t// Another extension asked for an autonomous run. Ignore payloads without a\n\t// task rather than arming a loop with nothing to work on.\n\tpi.events.on(LOOP_AUTO_START, (data) => {\n\t\tconst payload = (data ?? {}) as Partial<LoopAutoStartPayload>;\n\t\tconst task = payload.task?.trim();\n\t\tif (!task) return;\n\t\tconst budget = startAuto({ task, maxTurns: payload.maxTurns, continuePrompt: payload.continuePrompt });\n\t\tactiveCtx?.ui.notify(`Autonomous loop started (max ${budget} turns). Stop with /loop stop.`, \"info\");\n\t});\n\n\t// Another extension (e.g. ask_options) hit a decision it cannot safely make\n\t// while unattended. Stop iterating and let the model report the blocker.\n\tpi.events.on(LOOP_HALT, (data) => {\n\t\tif (!auto) return;\n\t\tconst reason = (data as { reason?: string })?.reason?.trim() || \"a decision that needs the user.\";\n\t\tsetAuto(null);\n\t\tactiveCtx?.ui.notify(`Autonomous loop halted: ${reason}`, \"warning\");\n\t});\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t\tif (scheduler) return;\n\t\tconst isIdle = () => {\n\t\t\ttry {\n\t\t\t\treturn ctx.isIdle();\n\t\t\t} catch {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t\tscheduler = new TaskScheduler({\n\t\t\t// `.agents/` is the primary, cross-vendor home; the legacy `.hoocode/`\n\t\t\t// store is read once and migrates forward on the next persist.\n\t\t\tstorePath: join(ctx.cwd, \".agents\", \"scheduled_tasks.json\"),\n\t\t\tlegacyStorePath: join(ctx.cwd, \".hoocode\", \"scheduled_tasks.json\"),\n\t\t\tfire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: \"followUp\" }),\n\t\t\tisIdle,\n\t\t});\n\t\tscheduler.start();\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tscheduler?.stop();\n\t\tsetAuto(null);\n\t});\n\n\t// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.\n\tpi.on(\"agent_end\", (event, ctx) => {\n\t\tif (!auto) return;\n\t\tif (ctx.hasPendingMessages()) return; // user is steering — yield\n\t\tconst last = [...event.messages].reverse().find((m) => m.role === \"assistant\");\n\t\tconst text = last ? assistantText(last) : \"\";\n\t\tif (text.includes(AUTO_LOOP_DONE_TOKEN)) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop complete.\", \"info\");\n\t\t\treturn;\n\t\t}\n\t\tif (auto.remaining <= 0) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop stopped: max turns reached.\", \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tauto.remaining -= 1;\n\t\tpi.sendUserMessage(\n\t\t\tauto.continuePrompt ??\n\t\t\t\t`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`,\n\t\t\t{ deliverAs: \"followUp\" },\n\t\t);\n\t});\n\n\t// ── Cron* tools (agent-callable) ──────────────────────────────────────────\n\tconst toolText = (s: string) => ({ content: [{ type: \"text\" as const, text: s }], details: undefined });\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronCreate\",\n\t\t\tlabel: \"Schedule Task\",\n\t\t\tdescription:\n\t\t\t\t\"Schedule a prompt to be re-submitted on a cron schedule (5-field, local time: minute hour day-of-month month day-of-week). recurring=false fires once then deletes.\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tcron: Type.String({ description: \"5-field cron expression in local time\" }),\n\t\t\t\tprompt: Type.String({ description: \"Prompt to enqueue at each fire time\" }),\n\t\t\t\trecurring: Type.Optional(Type.Boolean({ description: \"Fire repeatedly (default true) or once\" })),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tif (!scheduler) return toolText(\"Scheduler not ready.\");\n\t\t\t\tif (!isFiveFieldCron(params.cron)) return toolText(`Invalid cron \"${params.cron}\" (need 5 fields).`);\n\t\t\t\tconst task = scheduler.create({\n\t\t\t\t\tcron: params.cron,\n\t\t\t\t\tprompt: params.prompt,\n\t\t\t\t\trecurring: params.recurring ?? true,\n\t\t\t\t});\n\t\t\t\treturn toolText(`Scheduled ${task.id}: \"${task.cron}\" (${task.recurring ? \"recurring\" : \"once\"})`);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronList\",\n\t\t\tlabel: \"List Scheduled Tasks\",\n\t\t\tdescription: \"List all scheduled tasks (id, cron, recurring, prompt).\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst tasks = scheduler?.list() ?? [];\n\t\t\t\tif (tasks.length === 0) return toolText(\"No scheduled tasks.\");\n\t\t\t\treturn toolText(\n\t\t\t\t\ttasks\n\t\t\t\t\t\t.map((t) => `${t.id} ${t.cron} ${t.recurring ? \"recurring\" : \"once\"} ${JSON.stringify(t.prompt)}`)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronDelete\",\n\t\t\tlabel: \"Delete Scheduled Task\",\n\t\t\tdescription: \"Delete a scheduled task by id.\",\n\t\t\tparameters: Type.Object({ id: Type.String({ description: \"Task id from CronCreate/CronList\" }) }),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst removed = scheduler?.delete(params.id) ?? false;\n\t\t\t\treturn toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);\n\t\t\t},\n\t\t}),\n\t);\n\n\t// ── /loop command ─────────────────────────────────────────────────────────\n\tpi.registerCommand(\"loop\", {\n\t\tdescription:\n\t\t\t'Schedule prompts via cron or run an autonomous loop. /loop \"<cron>\" <prompt> | /loop <5m|2h> <prompt> | /loop once \"<cron>\" <prompt> | /loop list | /loop delete <id> | /loop stop | /loop auto [--max-turns N] <task>',\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"list\", \"delete\", \"stop\", \"once\", \"auto\"]\n\t\t\t\t.filter((s) => s.startsWith(prefix))\n\t\t\t\t.map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tif (!scheduler) {\n\t\t\t\tctx.ui.notify(\"Scheduler not ready yet.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed || trimmed === \"list\") {\n\t\t\t\tconst tasks = scheduler.list();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\ttasks.length === 0\n\t\t\t\t\t\t? auto\n\t\t\t\t\t\t\t? `Autonomous loop active (${auto.remaining} turns left).`\n\t\t\t\t\t\t\t: \"No scheduled tasks.\"\n\t\t\t\t\t\t: tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? \"\" : \"(once) \"}— ${t.prompt}`).join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed === \"stop\") {\n\t\t\t\tconst had = scheduler.list().length > 0 || auto !== null;\n\t\t\t\tscheduler.clear();\n\t\t\t\tsetAuto(null);\n\t\t\t\tctx.ui.notify(had ? \"Stopped all loops and scheduled tasks.\" : \"Nothing to stop.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"delete\")) {\n\t\t\t\tconst id = trimmed.slice(\"delete\".length).trim();\n\t\t\t\tif (!id) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop delete <id>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"auto\")) {\n\t\t\t\tlet rest = trimmed.slice(\"auto\".length).trim();\n\t\t\t\tlet maxTurns = DEFAULT_AUTO_MAX_TURNS;\n\t\t\t\tconst flag = /^--max-turns\\s+(\\d+)\\s*(.*)$/.exec(rest);\n\t\t\t\tif (flag) {\n\t\t\t\t\tmaxTurns = Number(flag[1]);\n\t\t\t\t\trest = flag[2].trim();\n\t\t\t\t}\n\t\t\t\tif (!rest) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop auto [--max-turns N] <task>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tstartAuto({ task: rest, maxTurns });\n\t\t\t\tctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Scheduling: one-shot or recurring, via quoted cron or interval token.\n\t\t\tlet recurring = true;\n\t\t\tlet body = trimmed;\n\t\t\tif (body.startsWith(\"once\")) {\n\t\t\t\trecurring = false;\n\t\t\t\tbody = body.slice(\"once\".length).trim();\n\t\t\t}\n\n\t\t\tlet cron: string | null = null;\n\t\t\tlet prompt = \"\";\n\t\t\tconst quoted = extractQuotedCron(body);\n\t\t\tif (quoted) {\n\t\t\t\tcron = quoted.cron;\n\t\t\t\tprompt = quoted.rest;\n\t\t\t} else {\n\t\t\t\tconst [first, ...restWords] = body.split(/\\s+/);\n\t\t\t\tcron = intervalToCron(first);\n\t\t\t\tprompt = restWords.join(\" \").trim();\n\t\t\t}\n\n\t\t\tif (!cron || !isFiveFieldCron(cron)) {\n\t\t\t\tctx.ui.notify('Usage: /loop \"<cron>\" <prompt> or /loop <5m|2h|1d> <prompt>', \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!prompt) {\n\t\t\t\tctx.ui.notify(\"Nothing to schedule — provide a prompt after the schedule.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst task = scheduler.create({ cron, prompt, recurring });\n\t\t\tctx.ui.notify(\n\t\t\t\t`Scheduled ${task.id}: \"${cron}\" ${recurring ? \"recurring\" : \"once\"} — \"${prompt}\". Manage with /loop list • /loop delete ${task.id}.`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
|
|
@@ -14,7 +14,8 @@ import { join } from "node:path";
|
|
|
14
14
|
import { Type } from "typebox";
|
|
15
15
|
import { defineTool } from "../../core/extensions/types.js";
|
|
16
16
|
import { TaskScheduler } from "../../core/scheduler.js";
|
|
17
|
-
|
|
17
|
+
/** Sentinel the agent replies with to declare the autonomous loop finished. */
|
|
18
|
+
export const AUTO_LOOP_DONE_TOKEN = "LOOP_DONE";
|
|
18
19
|
const DEFAULT_AUTO_MAX_TURNS = 10;
|
|
19
20
|
/**
|
|
20
21
|
* Event-bus channel: the autonomous-loop active state changed.
|
|
@@ -28,6 +29,13 @@ export const LOOP_AUTO_CHANGED = "loop:auto-changed";
|
|
|
28
29
|
* blocker that requires a human decision the loop cannot safely make on its own.
|
|
29
30
|
*/
|
|
30
31
|
export const LOOP_HALT = "loop:halt";
|
|
32
|
+
/**
|
|
33
|
+
* Event-bus channel: start the autonomous loop.
|
|
34
|
+
* Payload: {@link LoopAutoStartPayload}. Mirrors LOOP_HALT in the opposite
|
|
35
|
+
* direction, letting another extension (e.g. `/goal`) drive the loop without
|
|
36
|
+
* reaching into state that lives inside this module's closure.
|
|
37
|
+
*/
|
|
38
|
+
export const LOOP_AUTO_START = "loop:auto-start";
|
|
31
39
|
/** Convert a simple interval token ("5m", "2h", "1d") to a 5-field cron, or null. */
|
|
32
40
|
function intervalToCron(token) {
|
|
33
41
|
const m = /^(\d+)(m|h|d)$/.exec(token.trim());
|
|
@@ -73,6 +81,30 @@ export function setupLoop(pi) {
|
|
|
73
81
|
if (was !== (next !== null))
|
|
74
82
|
pi.events.emit(LOOP_AUTO_CHANGED, { active: next !== null });
|
|
75
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Arms the turn budget and delivers the task. Shared by `/loop auto` and the
|
|
86
|
+
* LOOP_AUTO_START channel so both start from identical state. Returns the
|
|
87
|
+
* budget actually applied, for the caller's notification.
|
|
88
|
+
*
|
|
89
|
+
* A caller-supplied 0 is honoured (the loop stops at the first continuation);
|
|
90
|
+
* only a missing or nonsensical budget falls back to the default.
|
|
91
|
+
*/
|
|
92
|
+
function startAuto({ task, maxTurns, continuePrompt }) {
|
|
93
|
+
const budget = typeof maxTurns === "number" && Number.isFinite(maxTurns) && maxTurns >= 0 ? maxTurns : DEFAULT_AUTO_MAX_TURNS;
|
|
94
|
+
setAuto({ remaining: budget, continuePrompt });
|
|
95
|
+
pi.sendUserMessage(`${task}\n\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`, { deliverAs: "followUp" });
|
|
96
|
+
return budget;
|
|
97
|
+
}
|
|
98
|
+
// Another extension asked for an autonomous run. Ignore payloads without a
|
|
99
|
+
// task rather than arming a loop with nothing to work on.
|
|
100
|
+
pi.events.on(LOOP_AUTO_START, (data) => {
|
|
101
|
+
const payload = (data ?? {});
|
|
102
|
+
const task = payload.task?.trim();
|
|
103
|
+
if (!task)
|
|
104
|
+
return;
|
|
105
|
+
const budget = startAuto({ task, maxTurns: payload.maxTurns, continuePrompt: payload.continuePrompt });
|
|
106
|
+
activeCtx?.ui.notify(`Autonomous loop started (max ${budget} turns). Stop with /loop stop.`, "info");
|
|
107
|
+
});
|
|
76
108
|
// Another extension (e.g. ask_options) hit a decision it cannot safely make
|
|
77
109
|
// while unattended. Stop iterating and let the model report the blocker.
|
|
78
110
|
pi.events.on(LOOP_HALT, (data) => {
|
|
@@ -127,9 +159,8 @@ export function setupLoop(pi) {
|
|
|
127
159
|
return;
|
|
128
160
|
}
|
|
129
161
|
auto.remaining -= 1;
|
|
130
|
-
pi.sendUserMessage(
|
|
131
|
-
deliverAs: "followUp"
|
|
132
|
-
});
|
|
162
|
+
pi.sendUserMessage(auto.continuePrompt ??
|
|
163
|
+
`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`, { deliverAs: "followUp" });
|
|
133
164
|
});
|
|
134
165
|
// ── Cron* tools (agent-callable) ──────────────────────────────────────────
|
|
135
166
|
const toolText = (s) => ({ content: [{ type: "text", text: s }], details: undefined });
|
|
@@ -228,8 +259,7 @@ export function setupLoop(pi) {
|
|
|
228
259
|
ctx.ui.notify("Usage: /loop auto [--max-turns N] <task>", "warning");
|
|
229
260
|
return;
|
|
230
261
|
}
|
|
231
|
-
|
|
232
|
-
pi.sendUserMessage(`${rest}\n\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`, { deliverAs: "followUp" });
|
|
262
|
+
startAuto({ task: rest, maxTurns });
|
|
233
263
|
ctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, "info");
|
|
234
264
|
return;
|
|
235
265
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.js","sourceRoot":"","sources":["../../../src/extensions/core/loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * /loop — cron scheduler, Cron* tools, and autonomous continuation.\n *\n * `/loop` schedules prompts via cron and drives autonomous continuation. The same\n * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.\n *\n * /loop \"<cron>\" <prompt> schedule recurring (5-field cron, local time)\n * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval\n * /loop once \"<cron>\" <prompt> schedule a one-shot\n * /loop list | /loop delete <id> | /loop stop\n * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE\n */\n\nimport { join } from \"node:path\";\nimport { Type } from \"typebox\";\nimport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { defineTool } from \"../../core/extensions/types.js\";\nimport { TaskScheduler } from \"../../core/scheduler.js\";\n\nconst AUTO_LOOP_DONE_TOKEN = \"LOOP_DONE\";\nconst DEFAULT_AUTO_MAX_TURNS = 10;\n\n/**\n * Event-bus channel: the autonomous-loop active state changed.\n * Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops\n * so other extensions (e.g. ask_options) can adapt to running unattended.\n */\nexport const LOOP_AUTO_CHANGED = \"loop:auto-changed\";\n\n/**\n * Event-bus channel: request to halt the autonomous loop.\n * Payload: `{ reason: string }`. Sent by another extension when it hits a\n * blocker that requires a human decision the loop cannot safely make on its own.\n */\nexport const LOOP_HALT = \"loop:halt\";\n\n/** Convert a simple interval token (\"5m\", \"2h\", \"1d\") to a 5-field cron, or null. */\nfunction intervalToCron(token: string): string | null {\n\tconst m = /^(\\d+)(m|h|d)$/.exec(token.trim());\n\tif (!m) return null;\n\tconst n = Number(m[1]);\n\tif (n < 1) return null;\n\tif (m[2] === \"m\") return `*/${n} * * * *`;\n\tif (m[2] === \"h\") return `0 */${n} * * *`;\n\treturn `0 0 */${n} * *`; // days\n}\n\n/** Pull a quoted cron expression off the front of an argument string. */\nfunction extractQuotedCron(args: string): { cron: string; rest: string } | null {\n\tconst m = /^\"([^\"]+)\"\\s*(.*)$/.exec(args.trim());\n\treturn m ? { cron: m[1].trim(), rest: m[2].trim() } : null;\n}\n\nfunction isFiveFieldCron(expr: string): boolean {\n\treturn expr.trim().split(/\\s+/).length === 5;\n}\n\n/** Flatten an assistant message's text blocks. */\nfunction assistantText(message: { content: unknown }): string {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((b): b is { type: \"text\"; text: string } => !!b && (b as { type?: string }).type === \"text\")\n\t\t.map((b) => b.text)\n\t\t.join(\"\\n\");\n}\n\nexport function setupLoop(pi: ExtensionAPI): void {\n\tlet scheduler: TaskScheduler | undefined;\n\tlet auto: { remaining: number } | null = null;\n\tlet activeCtx: ExtensionContext | undefined;\n\n\t/** Set the autonomous-loop state and broadcast the active flag on the bus. */\n\tfunction setAuto(next: { remaining: number } | null): void {\n\t\tconst was = auto !== null;\n\t\tauto = next;\n\t\tif (was !== (next !== null)) pi.events.emit(LOOP_AUTO_CHANGED, { active: next !== null });\n\t}\n\n\t// Another extension (e.g. ask_options) hit a decision it cannot safely make\n\t// while unattended. Stop iterating and let the model report the blocker.\n\tpi.events.on(LOOP_HALT, (data) => {\n\t\tif (!auto) return;\n\t\tconst reason = (data as { reason?: string })?.reason?.trim() || \"a decision that needs the user.\";\n\t\tsetAuto(null);\n\t\tactiveCtx?.ui.notify(`Autonomous loop halted: ${reason}`, \"warning\");\n\t});\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t\tif (scheduler) return;\n\t\tconst isIdle = () => {\n\t\t\ttry {\n\t\t\t\treturn ctx.isIdle();\n\t\t\t} catch {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t\tscheduler = new TaskScheduler({\n\t\t\t// `.agents/` is the primary, cross-vendor home; the legacy `.hoocode/`\n\t\t\t// store is read once and migrates forward on the next persist.\n\t\t\tstorePath: join(ctx.cwd, \".agents\", \"scheduled_tasks.json\"),\n\t\t\tlegacyStorePath: join(ctx.cwd, \".hoocode\", \"scheduled_tasks.json\"),\n\t\t\tfire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: \"followUp\" }),\n\t\t\tisIdle,\n\t\t});\n\t\tscheduler.start();\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tscheduler?.stop();\n\t\tsetAuto(null);\n\t});\n\n\t// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.\n\tpi.on(\"agent_end\", (event, ctx) => {\n\t\tif (!auto) return;\n\t\tif (ctx.hasPendingMessages()) return; // user is steering — yield\n\t\tconst last = [...event.messages].reverse().find((m) => m.role === \"assistant\");\n\t\tconst text = last ? assistantText(last) : \"\";\n\t\tif (text.includes(AUTO_LOOP_DONE_TOKEN)) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop complete.\", \"info\");\n\t\t\treturn;\n\t\t}\n\t\tif (auto.remaining <= 0) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop stopped: max turns reached.\", \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tauto.remaining -= 1;\n\t\tpi.sendUserMessage(`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`, {\n\t\t\tdeliverAs: \"followUp\",\n\t\t});\n\t});\n\n\t// ── Cron* tools (agent-callable) ──────────────────────────────────────────\n\tconst toolText = (s: string) => ({ content: [{ type: \"text\" as const, text: s }], details: undefined });\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronCreate\",\n\t\t\tlabel: \"Schedule Task\",\n\t\t\tdescription:\n\t\t\t\t\"Schedule a prompt to be re-submitted on a cron schedule (5-field, local time: minute hour day-of-month month day-of-week). recurring=false fires once then deletes.\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tcron: Type.String({ description: \"5-field cron expression in local time\" }),\n\t\t\t\tprompt: Type.String({ description: \"Prompt to enqueue at each fire time\" }),\n\t\t\t\trecurring: Type.Optional(Type.Boolean({ description: \"Fire repeatedly (default true) or once\" })),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tif (!scheduler) return toolText(\"Scheduler not ready.\");\n\t\t\t\tif (!isFiveFieldCron(params.cron)) return toolText(`Invalid cron \"${params.cron}\" (need 5 fields).`);\n\t\t\t\tconst task = scheduler.create({\n\t\t\t\t\tcron: params.cron,\n\t\t\t\t\tprompt: params.prompt,\n\t\t\t\t\trecurring: params.recurring ?? true,\n\t\t\t\t});\n\t\t\t\treturn toolText(`Scheduled ${task.id}: \"${task.cron}\" (${task.recurring ? \"recurring\" : \"once\"})`);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronList\",\n\t\t\tlabel: \"List Scheduled Tasks\",\n\t\t\tdescription: \"List all scheduled tasks (id, cron, recurring, prompt).\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst tasks = scheduler?.list() ?? [];\n\t\t\t\tif (tasks.length === 0) return toolText(\"No scheduled tasks.\");\n\t\t\t\treturn toolText(\n\t\t\t\t\ttasks\n\t\t\t\t\t\t.map((t) => `${t.id} ${t.cron} ${t.recurring ? \"recurring\" : \"once\"} ${JSON.stringify(t.prompt)}`)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronDelete\",\n\t\t\tlabel: \"Delete Scheduled Task\",\n\t\t\tdescription: \"Delete a scheduled task by id.\",\n\t\t\tparameters: Type.Object({ id: Type.String({ description: \"Task id from CronCreate/CronList\" }) }),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst removed = scheduler?.delete(params.id) ?? false;\n\t\t\t\treturn toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);\n\t\t\t},\n\t\t}),\n\t);\n\n\t// ── /loop command ─────────────────────────────────────────────────────────\n\tpi.registerCommand(\"loop\", {\n\t\tdescription:\n\t\t\t'Schedule prompts via cron or run an autonomous loop. /loop \"<cron>\" <prompt> | /loop <5m|2h> <prompt> | /loop once \"<cron>\" <prompt> | /loop list | /loop delete <id> | /loop stop | /loop auto [--max-turns N] <task>',\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"list\", \"delete\", \"stop\", \"once\", \"auto\"]\n\t\t\t\t.filter((s) => s.startsWith(prefix))\n\t\t\t\t.map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tif (!scheduler) {\n\t\t\t\tctx.ui.notify(\"Scheduler not ready yet.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed || trimmed === \"list\") {\n\t\t\t\tconst tasks = scheduler.list();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\ttasks.length === 0\n\t\t\t\t\t\t? auto\n\t\t\t\t\t\t\t? `Autonomous loop active (${auto.remaining} turns left).`\n\t\t\t\t\t\t\t: \"No scheduled tasks.\"\n\t\t\t\t\t\t: tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? \"\" : \"(once) \"}— ${t.prompt}`).join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed === \"stop\") {\n\t\t\t\tconst had = scheduler.list().length > 0 || auto !== null;\n\t\t\t\tscheduler.clear();\n\t\t\t\tsetAuto(null);\n\t\t\t\tctx.ui.notify(had ? \"Stopped all loops and scheduled tasks.\" : \"Nothing to stop.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"delete\")) {\n\t\t\t\tconst id = trimmed.slice(\"delete\".length).trim();\n\t\t\t\tif (!id) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop delete <id>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"auto\")) {\n\t\t\t\tlet rest = trimmed.slice(\"auto\".length).trim();\n\t\t\t\tlet maxTurns = DEFAULT_AUTO_MAX_TURNS;\n\t\t\t\tconst flag = /^--max-turns\\s+(\\d+)\\s*(.*)$/.exec(rest);\n\t\t\t\tif (flag) {\n\t\t\t\t\tmaxTurns = Number(flag[1]);\n\t\t\t\t\trest = flag[2].trim();\n\t\t\t\t}\n\t\t\t\tif (!rest) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop auto [--max-turns N] <task>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsetAuto({ remaining: maxTurns });\n\t\t\t\tpi.sendUserMessage(\n\t\t\t\t\t`${rest}\\n\\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`,\n\t\t\t\t\t{ deliverAs: \"followUp\" },\n\t\t\t\t);\n\t\t\t\tctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Scheduling: one-shot or recurring, via quoted cron or interval token.\n\t\t\tlet recurring = true;\n\t\t\tlet body = trimmed;\n\t\t\tif (body.startsWith(\"once\")) {\n\t\t\t\trecurring = false;\n\t\t\t\tbody = body.slice(\"once\".length).trim();\n\t\t\t}\n\n\t\t\tlet cron: string | null = null;\n\t\t\tlet prompt = \"\";\n\t\t\tconst quoted = extractQuotedCron(body);\n\t\t\tif (quoted) {\n\t\t\t\tcron = quoted.cron;\n\t\t\t\tprompt = quoted.rest;\n\t\t\t} else {\n\t\t\t\tconst [first, ...restWords] = body.split(/\\s+/);\n\t\t\t\tcron = intervalToCron(first);\n\t\t\t\tprompt = restWords.join(\" \").trim();\n\t\t\t}\n\n\t\t\tif (!cron || !isFiveFieldCron(cron)) {\n\t\t\t\tctx.ui.notify('Usage: /loop \"<cron>\" <prompt> or /loop <5m|2h|1d> <prompt>', \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!prompt) {\n\t\t\t\tctx.ui.notify(\"Nothing to schedule — provide a prompt after the schedule.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst task = scheduler.create({ cron, prompt, recurring });\n\t\t\tctx.ui.notify(\n\t\t\t\t`Scheduled ${task.id}: \"${cron}\" ${recurring ? \"recurring\" : \"once\"} — \"${prompt}\". Manage with /loop list • /loop delete ${task.id}.`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"loop.js","sourceRoot":"","sources":["../../../src/extensions/core/loop.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * /loop — cron scheduler, Cron* tools, and autonomous continuation.\n *\n * `/loop` schedules prompts via cron and drives autonomous continuation. The same\n * scheduler backs the agent-callable CronCreate/CronList/CronDelete tools.\n *\n * /loop \"<cron>\" <prompt> schedule recurring (5-field cron, local time)\n * /loop <5m|2h|1d> <prompt> schedule recurring at a simple interval\n * /loop once \"<cron>\" <prompt> schedule a one-shot\n * /loop list | /loop delete <id> | /loop stop\n * /loop auto [--max-turns N] <task> keep iterating until the task says LOOP_DONE\n */\n\nimport { join } from \"node:path\";\nimport { Type } from \"typebox\";\nimport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tSessionStartEvent,\n} from \"../../core/extensions/types.js\";\nimport { defineTool } from \"../../core/extensions/types.js\";\nimport { TaskScheduler } from \"../../core/scheduler.js\";\n\n/** Sentinel the agent replies with to declare the autonomous loop finished. */\nexport const AUTO_LOOP_DONE_TOKEN = \"LOOP_DONE\";\nconst DEFAULT_AUTO_MAX_TURNS = 10;\n\n/**\n * Event-bus channel: the autonomous-loop active state changed.\n * Payload: `{ active: boolean }`. Emitted whenever `/loop auto` starts or stops\n * so other extensions (e.g. ask_options) can adapt to running unattended.\n */\nexport const LOOP_AUTO_CHANGED = \"loop:auto-changed\";\n\n/**\n * Event-bus channel: request to halt the autonomous loop.\n * Payload: `{ reason: string }`. Sent by another extension when it hits a\n * blocker that requires a human decision the loop cannot safely make on its own.\n */\nexport const LOOP_HALT = \"loop:halt\";\n\n/**\n * Event-bus channel: start the autonomous loop.\n * Payload: {@link LoopAutoStartPayload}. Mirrors LOOP_HALT in the opposite\n * direction, letting another extension (e.g. `/goal`) drive the loop without\n * reaching into state that lives inside this module's closure.\n */\nexport const LOOP_AUTO_START = \"loop:auto-start\";\n\n/** Payload for {@link LOOP_AUTO_START}. */\nexport interface LoopAutoStartPayload {\n\t/** The task to work toward, delivered as the loop's first user message. */\n\ttask: string;\n\t/** Turn budget before the loop stops itself. Defaults to 10. */\n\tmaxTurns?: number;\n\t/**\n\t * Message re-sent on each continuation. Callers with a concrete completion\n\t * condition should restate it here, so the target does not drift out of view\n\t * as the transcript grows. Defaults to a generic nudge.\n\t */\n\tcontinuePrompt?: string;\n}\n\n/** Convert a simple interval token (\"5m\", \"2h\", \"1d\") to a 5-field cron, or null. */\nfunction intervalToCron(token: string): string | null {\n\tconst m = /^(\\d+)(m|h|d)$/.exec(token.trim());\n\tif (!m) return null;\n\tconst n = Number(m[1]);\n\tif (n < 1) return null;\n\tif (m[2] === \"m\") return `*/${n} * * * *`;\n\tif (m[2] === \"h\") return `0 */${n} * * *`;\n\treturn `0 0 */${n} * *`; // days\n}\n\n/** Pull a quoted cron expression off the front of an argument string. */\nfunction extractQuotedCron(args: string): { cron: string; rest: string } | null {\n\tconst m = /^\"([^\"]+)\"\\s*(.*)$/.exec(args.trim());\n\treturn m ? { cron: m[1].trim(), rest: m[2].trim() } : null;\n}\n\nfunction isFiveFieldCron(expr: string): boolean {\n\treturn expr.trim().split(/\\s+/).length === 5;\n}\n\n/** Flatten an assistant message's text blocks. */\nfunction assistantText(message: { content: unknown }): string {\n\tconst content = message.content;\n\tif (typeof content === \"string\") return content;\n\tif (!Array.isArray(content)) return \"\";\n\treturn content\n\t\t.filter((b): b is { type: \"text\"; text: string } => !!b && (b as { type?: string }).type === \"text\")\n\t\t.map((b) => b.text)\n\t\t.join(\"\\n\");\n}\n\nexport function setupLoop(pi: ExtensionAPI): void {\n\tlet scheduler: TaskScheduler | undefined;\n\tlet auto: { remaining: number; continuePrompt?: string } | null = null;\n\tlet activeCtx: ExtensionContext | undefined;\n\n\t/** Set the autonomous-loop state and broadcast the active flag on the bus. */\n\tfunction setAuto(next: { remaining: number; continuePrompt?: string } | null): void {\n\t\tconst was = auto !== null;\n\t\tauto = next;\n\t\tif (was !== (next !== null)) pi.events.emit(LOOP_AUTO_CHANGED, { active: next !== null });\n\t}\n\n\t/**\n\t * Arms the turn budget and delivers the task. Shared by `/loop auto` and the\n\t * LOOP_AUTO_START channel so both start from identical state. Returns the\n\t * budget actually applied, for the caller's notification.\n\t *\n\t * A caller-supplied 0 is honoured (the loop stops at the first continuation);\n\t * only a missing or nonsensical budget falls back to the default.\n\t */\n\tfunction startAuto({ task, maxTurns, continuePrompt }: LoopAutoStartPayload): number {\n\t\tconst budget =\n\t\t\ttypeof maxTurns === \"number\" && Number.isFinite(maxTurns) && maxTurns >= 0 ? maxTurns : DEFAULT_AUTO_MAX_TURNS;\n\t\tsetAuto({ remaining: budget, continuePrompt });\n\t\tpi.sendUserMessage(\n\t\t\t`${task}\\n\\n(Autonomous loop: keep working until the task is fully complete, then reply with ${AUTO_LOOP_DONE_TOKEN}.)`,\n\t\t\t{ deliverAs: \"followUp\" },\n\t\t);\n\t\treturn budget;\n\t}\n\n\t// Another extension asked for an autonomous run. Ignore payloads without a\n\t// task rather than arming a loop with nothing to work on.\n\tpi.events.on(LOOP_AUTO_START, (data) => {\n\t\tconst payload = (data ?? {}) as Partial<LoopAutoStartPayload>;\n\t\tconst task = payload.task?.trim();\n\t\tif (!task) return;\n\t\tconst budget = startAuto({ task, maxTurns: payload.maxTurns, continuePrompt: payload.continuePrompt });\n\t\tactiveCtx?.ui.notify(`Autonomous loop started (max ${budget} turns). Stop with /loop stop.`, \"info\");\n\t});\n\n\t// Another extension (e.g. ask_options) hit a decision it cannot safely make\n\t// while unattended. Stop iterating and let the model report the blocker.\n\tpi.events.on(LOOP_HALT, (data) => {\n\t\tif (!auto) return;\n\t\tconst reason = (data as { reason?: string })?.reason?.trim() || \"a decision that needs the user.\";\n\t\tsetAuto(null);\n\t\tactiveCtx?.ui.notify(`Autonomous loop halted: ${reason}`, \"warning\");\n\t});\n\n\tpi.on(\"session_start\", (_event: SessionStartEvent, ctx: ExtensionContext) => {\n\t\tactiveCtx = ctx;\n\t\tif (scheduler) return;\n\t\tconst isIdle = () => {\n\t\t\ttry {\n\t\t\t\treturn ctx.isIdle();\n\t\t\t} catch {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t};\n\t\tscheduler = new TaskScheduler({\n\t\t\t// `.agents/` is the primary, cross-vendor home; the legacy `.hoocode/`\n\t\t\t// store is read once and migrates forward on the next persist.\n\t\t\tstorePath: join(ctx.cwd, \".agents\", \"scheduled_tasks.json\"),\n\t\t\tlegacyStorePath: join(ctx.cwd, \".hoocode\", \"scheduled_tasks.json\"),\n\t\t\tfire: (prompt) => pi.sendUserMessage(prompt, { deliverAs: \"followUp\" }),\n\t\t\tisIdle,\n\t\t});\n\t\tscheduler.start();\n\t});\n\n\tpi.on(\"session_shutdown\", () => {\n\t\tscheduler?.stop();\n\t\tsetAuto(null);\n\t});\n\n\t// Autonomous continuation: re-prompt on each agent_end until LOOP_DONE or budget.\n\tpi.on(\"agent_end\", (event, ctx) => {\n\t\tif (!auto) return;\n\t\tif (ctx.hasPendingMessages()) return; // user is steering — yield\n\t\tconst last = [...event.messages].reverse().find((m) => m.role === \"assistant\");\n\t\tconst text = last ? assistantText(last) : \"\";\n\t\tif (text.includes(AUTO_LOOP_DONE_TOKEN)) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop complete.\", \"info\");\n\t\t\treturn;\n\t\t}\n\t\tif (auto.remaining <= 0) {\n\t\t\tsetAuto(null);\n\t\t\tctx.ui.notify(\"Autonomous loop stopped: max turns reached.\", \"warning\");\n\t\t\treturn;\n\t\t}\n\t\tauto.remaining -= 1;\n\t\tpi.sendUserMessage(\n\t\t\tauto.continuePrompt ??\n\t\t\t\t`Continue working toward the goal. Reply with ${AUTO_LOOP_DONE_TOKEN} when fully complete.`,\n\t\t\t{ deliverAs: \"followUp\" },\n\t\t);\n\t});\n\n\t// ── Cron* tools (agent-callable) ──────────────────────────────────────────\n\tconst toolText = (s: string) => ({ content: [{ type: \"text\" as const, text: s }], details: undefined });\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronCreate\",\n\t\t\tlabel: \"Schedule Task\",\n\t\t\tdescription:\n\t\t\t\t\"Schedule a prompt to be re-submitted on a cron schedule (5-field, local time: minute hour day-of-month month day-of-week). recurring=false fires once then deletes.\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tcron: Type.String({ description: \"5-field cron expression in local time\" }),\n\t\t\t\tprompt: Type.String({ description: \"Prompt to enqueue at each fire time\" }),\n\t\t\t\trecurring: Type.Optional(Type.Boolean({ description: \"Fire repeatedly (default true) or once\" })),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tif (!scheduler) return toolText(\"Scheduler not ready.\");\n\t\t\t\tif (!isFiveFieldCron(params.cron)) return toolText(`Invalid cron \"${params.cron}\" (need 5 fields).`);\n\t\t\t\tconst task = scheduler.create({\n\t\t\t\t\tcron: params.cron,\n\t\t\t\t\tprompt: params.prompt,\n\t\t\t\t\trecurring: params.recurring ?? true,\n\t\t\t\t});\n\t\t\t\treturn toolText(`Scheduled ${task.id}: \"${task.cron}\" (${task.recurring ? \"recurring\" : \"once\"})`);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronList\",\n\t\t\tlabel: \"List Scheduled Tasks\",\n\t\t\tdescription: \"List all scheduled tasks (id, cron, recurring, prompt).\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst tasks = scheduler?.list() ?? [];\n\t\t\t\tif (tasks.length === 0) return toolText(\"No scheduled tasks.\");\n\t\t\t\treturn toolText(\n\t\t\t\t\ttasks\n\t\t\t\t\t\t.map((t) => `${t.id} ${t.cron} ${t.recurring ? \"recurring\" : \"once\"} ${JSON.stringify(t.prompt)}`)\n\t\t\t\t\t\t.join(\"\\n\"),\n\t\t\t\t);\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"CronDelete\",\n\t\t\tlabel: \"Delete Scheduled Task\",\n\t\t\tdescription: \"Delete a scheduled task by id.\",\n\t\t\tparameters: Type.Object({ id: Type.String({ description: \"Task id from CronCreate/CronList\" }) }),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst removed = scheduler?.delete(params.id) ?? false;\n\t\t\t\treturn toolText(removed ? `Deleted ${params.id}.` : `No task ${params.id}.`);\n\t\t\t},\n\t\t}),\n\t);\n\n\t// ── /loop command ─────────────────────────────────────────────────────────\n\tpi.registerCommand(\"loop\", {\n\t\tdescription:\n\t\t\t'Schedule prompts via cron or run an autonomous loop. /loop \"<cron>\" <prompt> | /loop <5m|2h> <prompt> | /loop once \"<cron>\" <prompt> | /loop list | /loop delete <id> | /loop stop | /loop auto [--max-turns N] <task>',\n\t\tgetArgumentCompletions: (prefix: string) =>\n\t\t\t[\"list\", \"delete\", \"stop\", \"once\", \"auto\"]\n\t\t\t\t.filter((s) => s.startsWith(prefix))\n\t\t\t\t.map((s) => ({ value: s, label: s })),\n\t\thandler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {\n\t\t\tconst trimmed = args.trim();\n\t\t\tif (!scheduler) {\n\t\t\t\tctx.ui.notify(\"Scheduler not ready yet.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (!trimmed || trimmed === \"list\") {\n\t\t\t\tconst tasks = scheduler.list();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\ttasks.length === 0\n\t\t\t\t\t\t? auto\n\t\t\t\t\t\t\t? `Autonomous loop active (${auto.remaining} turns left).`\n\t\t\t\t\t\t\t: \"No scheduled tasks.\"\n\t\t\t\t\t\t: tasks.map((t) => `${t.id}: ${t.cron} ${t.recurring ? \"\" : \"(once) \"}— ${t.prompt}`).join(\"\\n\"),\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed === \"stop\") {\n\t\t\t\tconst had = scheduler.list().length > 0 || auto !== null;\n\t\t\t\tscheduler.clear();\n\t\t\t\tsetAuto(null);\n\t\t\t\tctx.ui.notify(had ? \"Stopped all loops and scheduled tasks.\" : \"Nothing to stop.\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"delete\")) {\n\t\t\t\tconst id = trimmed.slice(\"delete\".length).trim();\n\t\t\t\tif (!id) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop delete <id>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tctx.ui.notify(scheduler.delete(id) ? `Deleted ${id}.` : `No task ${id}.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (trimmed.startsWith(\"auto\")) {\n\t\t\t\tlet rest = trimmed.slice(\"auto\".length).trim();\n\t\t\t\tlet maxTurns = DEFAULT_AUTO_MAX_TURNS;\n\t\t\t\tconst flag = /^--max-turns\\s+(\\d+)\\s*(.*)$/.exec(rest);\n\t\t\t\tif (flag) {\n\t\t\t\t\tmaxTurns = Number(flag[1]);\n\t\t\t\t\trest = flag[2].trim();\n\t\t\t\t}\n\t\t\t\tif (!rest) {\n\t\t\t\t\tctx.ui.notify(\"Usage: /loop auto [--max-turns N] <task>\", \"warning\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tstartAuto({ task: rest, maxTurns });\n\t\t\t\tctx.ui.notify(`Autonomous loop started (max ${maxTurns} turns). Stop with /loop stop.`, \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Scheduling: one-shot or recurring, via quoted cron or interval token.\n\t\t\tlet recurring = true;\n\t\t\tlet body = trimmed;\n\t\t\tif (body.startsWith(\"once\")) {\n\t\t\t\trecurring = false;\n\t\t\t\tbody = body.slice(\"once\".length).trim();\n\t\t\t}\n\n\t\t\tlet cron: string | null = null;\n\t\t\tlet prompt = \"\";\n\t\t\tconst quoted = extractQuotedCron(body);\n\t\t\tif (quoted) {\n\t\t\t\tcron = quoted.cron;\n\t\t\t\tprompt = quoted.rest;\n\t\t\t} else {\n\t\t\t\tconst [first, ...restWords] = body.split(/\\s+/);\n\t\t\t\tcron = intervalToCron(first);\n\t\t\t\tprompt = restWords.join(\" \").trim();\n\t\t\t}\n\n\t\t\tif (!cron || !isFiveFieldCron(cron)) {\n\t\t\t\tctx.ui.notify('Usage: /loop \"<cron>\" <prompt> or /loop <5m|2h|1d> <prompt>', \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (!prompt) {\n\t\t\t\tctx.ui.notify(\"Nothing to schedule — provide a prompt after the schedule.\", \"warning\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst task = scheduler.create({ cron, prompt, recurring });\n\t\t\tctx.ui.notify(\n\t\t\t\t`Scheduled ${task.id}: \"${cron}\" ${recurring ? \"recurring\" : \"once\"} — \"${prompt}\". Manage with /loop list • /loop delete ${task.id}.`,\n\t\t\t\t\"info\",\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-loader.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/mcp-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAcH,OAAO,KAAK,EAAE,YAAY,EAAuD,MAAM,gCAAgC,CAAC;AAiBxH,MAAM,WAAW,eAAe;IAC/B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,6FAA6F;IAC7F,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;IAChC,8CAA8C;IAC9C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAgXD,wBAAgB,cAAc,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAiNrD","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 (`command`), Streamable HTTP (`{ \"type\": \"http\", \"url\": ... }`),\n * or legacy SSE (`\"type\": \"sse\"`), and registers each server tool as\n * `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, McpRemoteOptions } from \"@kolisachint/hoocode-agent-core\";\nimport { connectHttpMcpServer, 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, ToolDefinition } from \"../../core/extensions/types.js\";\nimport { deferMcpSchemas, subagentSkipMcp } from \"../../core/subagent-depth.js\";\nimport { taskStore } from \"../../core/task-store.js\";\nimport { type DeferredMcpToolEntry, formatDeferredCatalog, selectResolvable } from \"./mcp-deferred.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 (stdio transport). One of command/url is required. */\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/** Transport: \"stdio\" (default with command), \"http\" (Streamable HTTP), or \"sse\" (legacy) */\n\ttype?: \"stdio\" | \"http\" | \"sse\";\n\t/** Remote server URL (http/sse transports) */\n\turl?: string;\n\t/** Extra HTTP headers (e.g. Authorization) for remote transports */\n\theaders?: 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, Claude Code, VS Code / Copilot */\ninterface StandardMcpServerConfig {\n\tcommand?: string;\n\targs?: string[];\n\tenv?: Record<string, string>;\n\ttype?: \"stdio\" | \"http\" | \"sse\";\n\turl?: string;\n\theaders?: 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/** Remote-transport options (OAuth storage, auth callbacks) per server name. */\nconst mcpRemoteOptions = new Map<string, McpRemoteOptions>();\n/**\n * Servers with a browser authorization flow in flight. While a name is here,\n * a failed handshake must NOT tear the connection down (that would kill the\n * OAuth loopback listener); the auth-completion handler reconnects instead.\n */\nconst mcpAuthPending = new Set<string>();\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 & { command: string }): 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\n/** Remote servers use `{ type: \"http\" | \"sse\", url }`; stdio servers use `command`. */\nfunction isRemoteServer(config: McpServerConfig): boolean {\n\treturn config.type === \"http\" || config.type === \"sse\" || (!config.command && typeof config.url === \"string\");\n}\n\nfunction openMcpConnection(config: McpServerConfig): McpConnection {\n\tif (isRemoteServer(config)) {\n\t\tif (typeof config.url !== \"string\") {\n\t\t\tthrow new Error(`MCP server \"${config.name}\" has type \"${config.type}\" but no \"url\"`);\n\t\t}\n\t\treturn connectHttpMcpServer(\n\t\t\t{\n\t\t\t\tname: config.name,\n\t\t\t\turl: config.url,\n\t\t\t\theaders: config.headers,\n\t\t\t\ttype: config.type === \"sse\" ? \"sse\" : \"http\",\n\t\t\t},\n\t\t\tmcpRemoteOptions.get(config.name),\n\t\t);\n\t}\n\tif (typeof config.command !== \"string\") {\n\t\tthrow new Error(`MCP server \"${config.name}\" needs a \"command\" (stdio) or a \"url\" (http/sse)`);\n\t}\n\treturn spawnMcpServer(config as McpServerConfig & { command: string });\n}\n\nasync function connectMcpServer(config: McpServerConfig): Promise<{ conn: McpConnection; tools: McpToolDef[] }> {\n\tmcpConnections.get(config.name)?.terminate();\n\n\tconst conn = openMcpConnection(config);\n\tmcpConnections.set(config.name, conn);\n\n\ttry {\n\t\tawait conn.rpc(\n\t\t\t\"initialize\",\n\t\t\t{\n\t\t\t\tprotocolVersion: \"2024-11-05\",\n\t\t\t\tcapabilities: { tools: {} },\n\t\t\t\tclientInfo: { name: \"hoocode\", version: \"1.0.0\" },\n\t\t\t},\n\t\t\tMCP_HANDSHAKE_TIMEOUT_MS,\n\t\t);\n\n\t\t// Per the MCP spec the client must acknowledge a successful initialize with the\n\t\t// initialized notification before issuing further requests; strict servers gate\n\t\t// tools/call on it.\n\t\tconn.notify(\"notifications/initialized\");\n\n\t\tconst toolsResult = (await conn.rpc(\"tools/list\", {}, MCP_HANDSHAKE_TIMEOUT_MS)) as {\n\t\t\ttools?: McpToolDef[];\n\t\t};\n\t\treturn { conn, tools: toolsResult.tools ?? [] };\n\t} catch (error) {\n\t\t// Don't leave a half-connected entry behind: remote transports have no\n\t\t// process-exit event to evict them, so getOrConnectMcp would keep handing\n\t\t// out the dead connection instead of reconnecting. Exception: a pending\n\t\t// browser authorization — terminating would kill the OAuth loopback\n\t\t// listener; the auth-completion handler reconnects that server itself.\n\t\tif (!mcpAuthPending.has(config.name)) {\n\t\t\tconn.terminate();\n\t\t\tif (mcpConnections.get(config.name) === conn) mcpConnections.delete(config.name);\n\t\t}\n\t\tthrow error;\n\t}\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\ttype: serverConfig.type,\n\t\t\turl: serverConfig.url,\n\t\t\theaders: serverConfig.headers,\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\n/**\n * Build the full {@link ToolDefinition} for one MCP tool — the complete JSON\n * schema plus the connect/execute machinery. Shared by the eager path (register\n * every tool up front) and the deferred path (materialize on resolve), so both\n * produce identical, callable tools.\n */\nfunction buildMcpToolDefinition(serverConfig: McpServerConfig, tool: McpToolDef): ToolDefinition {\n\tconst toolName = `mcp_${serverConfig.name}_${tool.name}`;\n\tconst schema = buildMcpSchema(tool);\n\tconst capturedServer = serverConfig.name;\n\tconst capturedTool = tool.name;\n\t// MCP tools default to background mode since they are external processes with potential high latency\n\tconst isBackground = serverConfig.background !== false;\n\n\treturn {\n\t\tname: toolName,\n\t\tlabel: `[MCP] ${serverConfig.name} › ${tool.name}`,\n\t\tdescription: tool.description,\n\t\tparameters: schema,\n\t\tbackground: isBackground,\n\t\t// Render a clean, prefixed title in chat — `MCP [server › tool] <args>` —\n\t\t// parallel to the subagent `Task [type] <desc>` line. Without this the\n\t\t// ToolExecutionComponent falls back to the raw `mcp_<server>_<tool>` name.\n\t\t// The args summary reuses the same helper as the background start/finish\n\t\t// messages so the chat title stays in sync with them.\n\t\trenderCall(args, theme) {\n\t\t\tconst summary = summarizeArgs((args ?? {}) as Record<string, unknown>);\n\t\t\tconst text =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"MCP \")) +\n\t\t\t\ttheme.fg(\"accent\", `[${capturedServer} › ${capturedTool}]`) +\n\t\t\t\t(summary ? theme.fg(\"dim\", ` ${summary}`) : \"\");\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t\tasync execute(\n\t\t\t_toolCallId: string,\n\t\t\tparams: Static<typeof schema>,\n\t\t\tsignal: AbortSignal,\n\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\t// Background MCP tools get a task store entry so they appear in the task pane.\n\t\t\t// Foreground tools skip this (their result is awaited inline). The server\n\t\t\t// name rides in subagentMode and becomes the row's `[server]` origin tag;\n\t\t\t// the title carries just the tool.\n\t\t\tconst task = isBackground\n\t\t\t\t? taskStore.create(capturedTool, { source: \"mcp\", subagentMode: capturedServer })\n\t\t\t\t: undefined;\n\t\t\tif (task) taskStore.update(task.id, { status: \"in_progress\" });\n\n\t\t\t// Lazily (re)connect: a dropped connection (server exit, process churn\n\t\t\t// between turns, a racing teardown) should transparently reconnect from\n\t\t\t// the retained config rather than permanently fail with \"not connected\".\n\t\t\tconst activeConn = await getOrConnectMcp(capturedServer);\n\t\t\tif (!activeConn) {\n\t\t\t\tif (task) taskStore.update(task.id, { status: \"failed\" });\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: `MCP server \"${capturedServer}\" is not connected (reconnect attempt failed)` },\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst abortPromise = new Promise<never>((_, reject) => {\n\t\t\t\t\tsignal.addEventListener(\"abort\", () => reject(new Error(\"Aborted\")));\n\t\t\t\t});\n\n\t\t\t\tconst result = await Promise.race([\n\t\t\t\t\tactiveConn.rpc(\"tools/call\", { name: capturedTool, arguments: params }),\n\t\t\t\t\tabortPromise,\n\t\t\t\t]);\n\n\t\t\t\tif (task) taskStore.update(task.id, { status: \"done\" });\n\t\t\t\treturn { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }], details: undefined };\n\t\t\t} catch (error) {\n\t\t\t\tif (task) taskStore.update(task.id, { status: \"failed\" });\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\t} as ToolDefinition;\n}\n\nconst RESOLVE_MCP_TOOLS_NAME = \"ResolveMcpTools\";\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 && !serverConfig.url)) {\n\t\t\t\t\t\tctx.ui.notify(`MCP: config \"${file}\" is missing required \"name\" or \"command\"/\"url\"`, \"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// Deferral (spec §2): inject MCP tool names only and materialize each schema\n\t\t// on demand via ResolveMcpTools. Default-on and top-level only — a subagent that\n\t\t// needs MCP has this env cleared, so it eager-registers its allowlisted tools\n\t\t// at dispatch (the dispatch ↔ schema interaction) and they are immediately callable.\n\t\tconst defer = deferMcpSchemas();\n\t\tconst deferredCatalog: DeferredMcpToolEntry[] = [];\n\t\t// Retain each deferred tool's raw definition + config so ResolveMcpTools can\n\t\t// build the full ToolDefinition on request.\n\t\tconst deferredByName = new Map<string, { serverConfig: McpServerConfig; tool: McpToolDef }>();\n\t\tconst resolvedNames = new Set<string>();\n\n\t\t// Remote servers get OAuth support: tokens persist under\n\t\t// ~/.hoocode/mcp-auth, and when a server demands interactive (browser)\n\t\t// authorization the handshake fails fast while the flow keeps running in\n\t\t// the background — on completion the server is reconnected and its tools\n\t\t// registered (eagerly, since the deferred catalog is already sealed).\n\t\tconst buildRemoteOptions = (serverConfig: McpServerConfig): McpRemoteOptions => ({\n\t\t\tauthStorageDir: join(HOOCODE_DIR, \"mcp-auth\"),\n\t\t\tonAuthRequired: (authorizationUrl, completed) => {\n\t\t\t\tmcpAuthPending.add(serverConfig.name);\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t`MCP: \"${serverConfig.name}\" requires authorization — complete the sign-in opened in your browser` +\n\t\t\t\t\t\t(authorizationUrl ? `: ${authorizationUrl}` : \"\"),\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t\tcompleted\n\t\t\t\t\t.then(async () => {\n\t\t\t\t\t\tmcpAuthPending.delete(serverConfig.name);\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst { tools } = await connectMcpServer(serverConfig);\n\t\t\t\t\t\t\tfor (const tool of tools) pi.registerTool(buildMcpToolDefinition(serverConfig, tool));\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`MCP: connected \"${serverConfig.name}\" after authorization (${tools.length} tool${tools.length === 1 ? \"\" : \"s\"})`,\n\t\t\t\t\t\t\t\t\"info\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`MCP: failed to connect \"${serverConfig.name}\" after authorization: ${String(err)}`,\n\t\t\t\t\t\t\t\t\"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\t.catch((err: unknown) => {\n\t\t\t\t\t\tmcpAuthPending.delete(serverConfig.name);\n\t\t\t\t\t\tctx.ui.notify(`MCP: authorization for \"${serverConfig.name}\" failed: ${String(err)}`, \"error\");\n\t\t\t\t\t});\n\t\t\t},\n\t\t});\n\n\t\t// 3. Connect to all servers and register (or defer) 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\tmcpRemoteOptions.set(serverConfig.name, buildRemoteOptions(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\tif (defer) {\n\t\t\t\t\t\tdeferredCatalog.push({ toolName, server: serverConfig.name, description: tool.description });\n\t\t\t\t\t\tdeferredByName.set(toolName, { serverConfig, tool });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpi.registerTool(buildMcpToolDefinition(serverConfig, tool));\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\tconst suffix = defer ? \", schemas deferred\" : \"\";\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}${suffix})`,\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t} catch (err) {\n\t\t\t\tif (mcpAuthPending.has(serverConfig.name)) {\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t`MCP: \"${serverConfig.name}\" is waiting for browser authorization; tools will register once it completes`,\n\t\t\t\t\t\t\"info\",\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tctx.ui.notify(`MCP: failed to connect \"${serverConfig.name}\": ${String(err)}`, \"error\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// 4. In deferred mode, register the single resolver that materializes schemas on demand.\n\t\tif (defer && deferredCatalog.length > 0) {\n\t\t\tconst resolveParams = Type.Object(\n\t\t\t\t{\n\t\t\t\t\tnames: Type.Array(Type.String(), {\n\t\t\t\t\t\tdescription: \"MCP tool names to make callable (e.g. 'mcp_github_create_pr' or 'create_pr').\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{ additionalProperties: false },\n\t\t\t);\n\t\t\tpi.registerTool({\n\t\t\t\tname: RESOLVE_MCP_TOOLS_NAME,\n\t\t\t\tlabel: RESOLVE_MCP_TOOLS_NAME,\n\t\t\t\tdescription:\n\t\t\t\t\t\"MCP tools are connected but their schemas are loaded on demand to keep context small. Call this with the tool name(s) you need to make them callable, then call the tool(s). Available MCP tools:\\n\" +\n\t\t\t\t\tformatDeferredCatalog(deferredCatalog),\n\t\t\t\tpromptSnippet: \"Resolve deferred MCP tool schemas by name before calling them.\",\n\t\t\t\tparameters: resolveParams,\n\t\t\t\tasync execute(_toolCallId: string, params: Static<typeof resolveParams>) {\n\t\t\t\t\tconst matched = selectResolvable(deferredCatalog, params.names ?? []);\n\t\t\t\t\tconst newlyResolved: string[] = [];\n\t\t\t\t\tfor (const entry of matched) {\n\t\t\t\t\t\tif (resolvedNames.has(entry.toolName)) continue;\n\t\t\t\t\t\tconst raw = deferredByName.get(entry.toolName);\n\t\t\t\t\t\tif (!raw) continue;\n\t\t\t\t\t\tpi.registerTool(buildMcpToolDefinition(raw.serverConfig, raw.tool));\n\t\t\t\t\t\tresolvedNames.add(entry.toolName);\n\t\t\t\t\t\tnewlyResolved.push(entry.toolName);\n\t\t\t\t\t}\n\t\t\t\t\tconst text = matched.length\n\t\t\t\t\t\t? `Resolved ${newlyResolved.length} MCP tool(s): ${matched.map((m) => m.toolName).join(\", \")}. They are now callable.`\n\t\t\t\t\t\t: `No MCP tools matched: ${(params.names ?? []).join(\", \") || \"(none)\"}. See ResolveMcpTools for the catalog.`;\n\t\t\t\t\treturn { content: [{ type: \"text\" as const, text }], details: undefined };\n\t\t\t\t},\n\t\t\t} as ToolDefinition);\n\t\t}\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"mcp-loader.d.ts","sourceRoot":"","sources":["../../../src/extensions/core/mcp-loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAcH,OAAO,KAAK,EAAE,YAAY,EAAuD,MAAM,gCAAgC,CAAC;AAkBxH,MAAM,WAAW,eAAe;IAC/B,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,6FAA6F;IAC7F,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;IAChC,8CAA8C;IAC9C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAgXD,wBAAgB,cAAc,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAqOrD","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 (`command`), Streamable HTTP (`{ \"type\": \"http\", \"url\": ... }`),\n * or legacy SSE (`\"type\": \"sse\"`), and registers each server tool as\n * `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, McpRemoteOptions } from \"@kolisachint/hoocode-agent-core\";\nimport { connectHttpMcpServer, 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, ToolDefinition } from \"../../core/extensions/types.js\";\nimport { clearMcpServerStatuses, setMcpServerStatus } from \"../../core/mcp-status.js\";\nimport { deferMcpSchemas, subagentSkipMcp } from \"../../core/subagent-depth.js\";\nimport { taskStore } from \"../../core/task-store.js\";\nimport { type DeferredMcpToolEntry, formatDeferredCatalog, selectResolvable } from \"./mcp-deferred.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 (stdio transport). One of command/url is required. */\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/** Transport: \"stdio\" (default with command), \"http\" (Streamable HTTP), or \"sse\" (legacy) */\n\ttype?: \"stdio\" | \"http\" | \"sse\";\n\t/** Remote server URL (http/sse transports) */\n\turl?: string;\n\t/** Extra HTTP headers (e.g. Authorization) for remote transports */\n\theaders?: 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, Claude Code, VS Code / Copilot */\ninterface StandardMcpServerConfig {\n\tcommand?: string;\n\targs?: string[];\n\tenv?: Record<string, string>;\n\ttype?: \"stdio\" | \"http\" | \"sse\";\n\turl?: string;\n\theaders?: 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/** Remote-transport options (OAuth storage, auth callbacks) per server name. */\nconst mcpRemoteOptions = new Map<string, McpRemoteOptions>();\n/**\n * Servers with a browser authorization flow in flight. While a name is here,\n * a failed handshake must NOT tear the connection down (that would kill the\n * OAuth loopback listener); the auth-completion handler reconnects instead.\n */\nconst mcpAuthPending = new Set<string>();\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 & { command: string }): 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\n/** Remote servers use `{ type: \"http\" | \"sse\", url }`; stdio servers use `command`. */\nfunction isRemoteServer(config: McpServerConfig): boolean {\n\treturn config.type === \"http\" || config.type === \"sse\" || (!config.command && typeof config.url === \"string\");\n}\n\nfunction openMcpConnection(config: McpServerConfig): McpConnection {\n\tif (isRemoteServer(config)) {\n\t\tif (typeof config.url !== \"string\") {\n\t\t\tthrow new Error(`MCP server \"${config.name}\" has type \"${config.type}\" but no \"url\"`);\n\t\t}\n\t\treturn connectHttpMcpServer(\n\t\t\t{\n\t\t\t\tname: config.name,\n\t\t\t\turl: config.url,\n\t\t\t\theaders: config.headers,\n\t\t\t\ttype: config.type === \"sse\" ? \"sse\" : \"http\",\n\t\t\t},\n\t\t\tmcpRemoteOptions.get(config.name),\n\t\t);\n\t}\n\tif (typeof config.command !== \"string\") {\n\t\tthrow new Error(`MCP server \"${config.name}\" needs a \"command\" (stdio) or a \"url\" (http/sse)`);\n\t}\n\treturn spawnMcpServer(config as McpServerConfig & { command: string });\n}\n\nasync function connectMcpServer(config: McpServerConfig): Promise<{ conn: McpConnection; tools: McpToolDef[] }> {\n\tmcpConnections.get(config.name)?.terminate();\n\n\tconst conn = openMcpConnection(config);\n\tmcpConnections.set(config.name, conn);\n\n\ttry {\n\t\tawait conn.rpc(\n\t\t\t\"initialize\",\n\t\t\t{\n\t\t\t\tprotocolVersion: \"2024-11-05\",\n\t\t\t\tcapabilities: { tools: {} },\n\t\t\t\tclientInfo: { name: \"hoocode\", version: \"1.0.0\" },\n\t\t\t},\n\t\t\tMCP_HANDSHAKE_TIMEOUT_MS,\n\t\t);\n\n\t\t// Per the MCP spec the client must acknowledge a successful initialize with the\n\t\t// initialized notification before issuing further requests; strict servers gate\n\t\t// tools/call on it.\n\t\tconn.notify(\"notifications/initialized\");\n\n\t\tconst toolsResult = (await conn.rpc(\"tools/list\", {}, MCP_HANDSHAKE_TIMEOUT_MS)) as {\n\t\t\ttools?: McpToolDef[];\n\t\t};\n\t\treturn { conn, tools: toolsResult.tools ?? [] };\n\t} catch (error) {\n\t\t// Don't leave a half-connected entry behind: remote transports have no\n\t\t// process-exit event to evict them, so getOrConnectMcp would keep handing\n\t\t// out the dead connection instead of reconnecting. Exception: a pending\n\t\t// browser authorization — terminating would kill the OAuth loopback\n\t\t// listener; the auth-completion handler reconnects that server itself.\n\t\tif (!mcpAuthPending.has(config.name)) {\n\t\t\tconn.terminate();\n\t\t\tif (mcpConnections.get(config.name) === conn) mcpConnections.delete(config.name);\n\t\t}\n\t\tthrow error;\n\t}\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\ttype: serverConfig.type,\n\t\t\turl: serverConfig.url,\n\t\t\theaders: serverConfig.headers,\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\n/**\n * Build the full {@link ToolDefinition} for one MCP tool — the complete JSON\n * schema plus the connect/execute machinery. Shared by the eager path (register\n * every tool up front) and the deferred path (materialize on resolve), so both\n * produce identical, callable tools.\n */\nfunction buildMcpToolDefinition(serverConfig: McpServerConfig, tool: McpToolDef): ToolDefinition {\n\tconst toolName = `mcp_${serverConfig.name}_${tool.name}`;\n\tconst schema = buildMcpSchema(tool);\n\tconst capturedServer = serverConfig.name;\n\tconst capturedTool = tool.name;\n\t// MCP tools default to background mode since they are external processes with potential high latency\n\tconst isBackground = serverConfig.background !== false;\n\n\treturn {\n\t\tname: toolName,\n\t\tlabel: `[MCP] ${serverConfig.name} › ${tool.name}`,\n\t\tdescription: tool.description,\n\t\tparameters: schema,\n\t\tbackground: isBackground,\n\t\t// Render a clean, prefixed title in chat — `MCP [server › tool] <args>` —\n\t\t// parallel to the subagent `Task [type] <desc>` line. Without this the\n\t\t// ToolExecutionComponent falls back to the raw `mcp_<server>_<tool>` name.\n\t\t// The args summary reuses the same helper as the background start/finish\n\t\t// messages so the chat title stays in sync with them.\n\t\trenderCall(args, theme) {\n\t\t\tconst summary = summarizeArgs((args ?? {}) as Record<string, unknown>);\n\t\t\tconst text =\n\t\t\t\ttheme.fg(\"toolTitle\", theme.bold(\"MCP \")) +\n\t\t\t\ttheme.fg(\"accent\", `[${capturedServer} › ${capturedTool}]`) +\n\t\t\t\t(summary ? theme.fg(\"dim\", ` ${summary}`) : \"\");\n\t\t\treturn new Text(text, 0, 0);\n\t\t},\n\t\tasync execute(\n\t\t\t_toolCallId: string,\n\t\t\tparams: Static<typeof schema>,\n\t\t\tsignal: AbortSignal,\n\t\t\t_onUpdate: AgentToolUpdateCallback,\n\t\t): Promise<AgentToolResult<undefined>> {\n\t\t\t// Background MCP tools get a task store entry so they appear in the task pane.\n\t\t\t// Foreground tools skip this (their result is awaited inline). The server\n\t\t\t// name rides in subagentMode and becomes the row's `[server]` origin tag;\n\t\t\t// the title carries just the tool.\n\t\t\tconst task = isBackground\n\t\t\t\t? taskStore.create(capturedTool, { source: \"mcp\", subagentMode: capturedServer })\n\t\t\t\t: undefined;\n\t\t\tif (task) taskStore.update(task.id, { status: \"in_progress\" });\n\n\t\t\t// Lazily (re)connect: a dropped connection (server exit, process churn\n\t\t\t// between turns, a racing teardown) should transparently reconnect from\n\t\t\t// the retained config rather than permanently fail with \"not connected\".\n\t\t\tconst activeConn = await getOrConnectMcp(capturedServer);\n\t\t\tif (!activeConn) {\n\t\t\t\tif (task) taskStore.update(task.id, { status: \"failed\" });\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{ type: \"text\", text: `MCP server \"${capturedServer}\" is not connected (reconnect attempt failed)` },\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst abortPromise = new Promise<never>((_, reject) => {\n\t\t\t\t\tsignal.addEventListener(\"abort\", () => reject(new Error(\"Aborted\")));\n\t\t\t\t});\n\n\t\t\t\tconst result = await Promise.race([\n\t\t\t\t\tactiveConn.rpc(\"tools/call\", { name: capturedTool, arguments: params }),\n\t\t\t\t\tabortPromise,\n\t\t\t\t]);\n\n\t\t\t\tif (task) taskStore.update(task.id, { status: \"done\" });\n\t\t\t\treturn { content: [{ type: \"text\", text: JSON.stringify(result, null, 2) }], details: undefined };\n\t\t\t} catch (error) {\n\t\t\t\tif (task) taskStore.update(task.id, { status: \"failed\" });\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\t} as ToolDefinition;\n}\n\nconst RESOLVE_MCP_TOOLS_NAME = \"ResolveMcpTools\";\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\t// Rebuild the status registry from scratch: a reload or session switch runs\n\t\t// this pass again and the startup summary must not double-count.\n\t\tclearMcpServerStatuses();\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 && !serverConfig.url)) {\n\t\t\t\t\t\tctx.ui.notify(`MCP: config \"${file}\" is missing required \"name\" or \"command\"/\"url\"`, \"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// Deferral (spec §2): inject MCP tool names only and materialize each schema\n\t\t// on demand via ResolveMcpTools. Default-on and top-level only — a subagent that\n\t\t// needs MCP has this env cleared, so it eager-registers its allowlisted tools\n\t\t// at dispatch (the dispatch ↔ schema interaction) and they are immediately callable.\n\t\tconst defer = deferMcpSchemas();\n\t\tconst deferredCatalog: DeferredMcpToolEntry[] = [];\n\t\t// Retain each deferred tool's raw definition + config so ResolveMcpTools can\n\t\t// build the full ToolDefinition on request.\n\t\tconst deferredByName = new Map<string, { serverConfig: McpServerConfig; tool: McpToolDef }>();\n\t\tconst resolvedNames = new Set<string>();\n\n\t\t// Remote servers get OAuth support: tokens persist under\n\t\t// ~/.hoocode/mcp-auth, and when a server demands interactive (browser)\n\t\t// authorization the handshake fails fast while the flow keeps running in\n\t\t// the background — on completion the server is reconnected and its tools\n\t\t// registered (eagerly, since the deferred catalog is already sealed).\n\t\tconst buildRemoteOptions = (serverConfig: McpServerConfig): McpRemoteOptions => ({\n\t\t\tauthStorageDir: join(HOOCODE_DIR, \"mcp-auth\"),\n\t\t\tonAuthRequired: (authorizationUrl, completed) => {\n\t\t\t\tmcpAuthPending.add(serverConfig.name);\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t`MCP: \"${serverConfig.name}\" requires authorization — complete the sign-in opened in your browser` +\n\t\t\t\t\t\t(authorizationUrl ? `: ${authorizationUrl}` : \"\"),\n\t\t\t\t\t\"warning\",\n\t\t\t\t);\n\t\t\t\tcompleted\n\t\t\t\t\t.then(async () => {\n\t\t\t\t\t\tmcpAuthPending.delete(serverConfig.name);\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst { tools } = await connectMcpServer(serverConfig);\n\t\t\t\t\t\t\tfor (const tool of tools) pi.registerTool(buildMcpToolDefinition(serverConfig, tool));\n\t\t\t\t\t\t\tsetMcpServerStatus({\n\t\t\t\t\t\t\t\tname: serverConfig.name,\n\t\t\t\t\t\t\t\ttoolCount: tools.length,\n\t\t\t\t\t\t\t\tbackground: serverConfig.background !== false,\n\t\t\t\t\t\t\t\tdeferred: false,\n\t\t\t\t\t\t\t\tstate: \"connected\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`MCP: connected \"${serverConfig.name}\" after authorization (${tools.length} tool${tools.length === 1 ? \"\" : \"s\"})`,\n\t\t\t\t\t\t\t\t\"info\",\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t\t\t`MCP: failed to connect \"${serverConfig.name}\" after authorization: ${String(err)}`,\n\t\t\t\t\t\t\t\t\"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\t.catch((err: unknown) => {\n\t\t\t\t\t\tmcpAuthPending.delete(serverConfig.name);\n\t\t\t\t\t\tctx.ui.notify(`MCP: authorization for \"${serverConfig.name}\" failed: ${String(err)}`, \"error\");\n\t\t\t\t\t});\n\t\t\t},\n\t\t});\n\n\t\t// 3. Connect to all servers and register (or defer) 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\tmcpRemoteOptions.set(serverConfig.name, buildRemoteOptions(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\tif (defer) {\n\t\t\t\t\t\tdeferredCatalog.push({ toolName, server: serverConfig.name, description: tool.description });\n\t\t\t\t\t\tdeferredByName.set(toolName, { serverConfig, tool });\n\t\t\t\t\t} else {\n\t\t\t\t\t\tpi.registerTool(buildMcpToolDefinition(serverConfig, tool));\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// A successful connect is reported by the startup resource summary (one\n\t\t\t\t// `mcp` cell plus a details row), not as a transient line above it.\n\t\t\t\tsetMcpServerStatus({\n\t\t\t\t\tname: serverConfig.name,\n\t\t\t\t\ttoolCount: tools.length,\n\t\t\t\t\tbackground: serverConfig.background !== false,\n\t\t\t\t\tdeferred: defer,\n\t\t\t\t\tstate: \"connected\",\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\tif (mcpAuthPending.has(serverConfig.name)) {\n\t\t\t\t\tsetMcpServerStatus({\n\t\t\t\t\t\tname: serverConfig.name,\n\t\t\t\t\t\ttoolCount: 0,\n\t\t\t\t\t\tbackground: serverConfig.background !== false,\n\t\t\t\t\t\tdeferred: defer,\n\t\t\t\t\t\tstate: \"authorizing\",\n\t\t\t\t\t});\n\t\t\t\t\tctx.ui.notify(\n\t\t\t\t\t\t`MCP: \"${serverConfig.name}\" is waiting for browser authorization; tools will register once it completes`,\n\t\t\t\t\t\t\"info\",\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tctx.ui.notify(`MCP: failed to connect \"${serverConfig.name}\": ${String(err)}`, \"error\");\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// 4. In deferred mode, register the single resolver that materializes schemas on demand.\n\t\tif (defer && deferredCatalog.length > 0) {\n\t\t\tconst resolveParams = Type.Object(\n\t\t\t\t{\n\t\t\t\t\tnames: Type.Array(Type.String(), {\n\t\t\t\t\t\tdescription: \"MCP tool names to make callable (e.g. 'mcp_github_create_pr' or 'create_pr').\",\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t\t{ additionalProperties: false },\n\t\t\t);\n\t\t\tpi.registerTool({\n\t\t\t\tname: RESOLVE_MCP_TOOLS_NAME,\n\t\t\t\tlabel: RESOLVE_MCP_TOOLS_NAME,\n\t\t\t\tdescription:\n\t\t\t\t\t\"MCP tools are connected but their schemas are loaded on demand to keep context small. Call this with the tool name(s) you need to make them callable, then call the tool(s). Available MCP tools:\\n\" +\n\t\t\t\t\tformatDeferredCatalog(deferredCatalog),\n\t\t\t\tpromptSnippet: \"Resolve deferred MCP tool schemas by name before calling them.\",\n\t\t\t\tparameters: resolveParams,\n\t\t\t\tasync execute(_toolCallId: string, params: Static<typeof resolveParams>) {\n\t\t\t\t\tconst matched = selectResolvable(deferredCatalog, params.names ?? []);\n\t\t\t\t\tconst newlyResolved: string[] = [];\n\t\t\t\t\tfor (const entry of matched) {\n\t\t\t\t\t\tif (resolvedNames.has(entry.toolName)) continue;\n\t\t\t\t\t\tconst raw = deferredByName.get(entry.toolName);\n\t\t\t\t\t\tif (!raw) continue;\n\t\t\t\t\t\tpi.registerTool(buildMcpToolDefinition(raw.serverConfig, raw.tool));\n\t\t\t\t\t\tresolvedNames.add(entry.toolName);\n\t\t\t\t\t\tnewlyResolved.push(entry.toolName);\n\t\t\t\t\t}\n\t\t\t\t\tconst text = matched.length\n\t\t\t\t\t\t? `Resolved ${newlyResolved.length} MCP tool(s): ${matched.map((m) => m.toolName).join(\", \")}. They are now callable.`\n\t\t\t\t\t\t: `No MCP tools matched: ${(params.names ?? []).join(\", \") || \"(none)\"}. See ResolveMcpTools for the catalog.`;\n\t\t\t\t\treturn { content: [{ type: \"text\" as const, text }], details: undefined };\n\t\t\t\t},\n\t\t\t} as ToolDefinition);\n\t\t}\n\t});\n}\n"]}
|