@kolisachint/hoocode-agent-core 0.4.47 → 0.4.49
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/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/default-tools.d.ts +27 -0
- package/dist/tools/default-tools.d.ts.map +1 -0
- package/dist/tools/default-tools.js +451 -0
- package/dist/tools/default-tools.js.map +1 -0
- package/dist/tools/mcp-tools.d.ts +30 -0
- package/dist/tools/mcp-tools.d.ts.map +1 -0
- package/dist/tools/mcp-tools.js +214 -0
- package/dist/tools/mcp-tools.js.map +1 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -16,5 +16,7 @@ export * from "./harness/types.js";
|
|
|
16
16
|
export * from "./harness/utils/shell-output.js";
|
|
17
17
|
export * from "./harness/utils/truncate.js";
|
|
18
18
|
export * from "./proxy.js";
|
|
19
|
+
export * from "./tools/default-tools.js";
|
|
20
|
+
export * from "./tools/mcp-tools.js";
|
|
19
21
|
export * from "./types.js";
|
|
20
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,YAAY,CAAC;AAE3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EACN,8BAA8B,EAC9B,qBAAqB,EACrB,oBAAoB,GACpB,MAAM,8CAA8C,CAAC;AACtD,OAAO,EACN,sBAAsB,EACtB,OAAO,EACP,2BAA2B,EAC3B,qBAAqB,EACrB,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,aAAa,GACb,MAAM,oCAAoC,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,kCAAkC,CAAC;AACjD,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAE5C,cAAc,YAAY,CAAC;AAE3B,cAAc,YAAY,CAAC","sourcesContent":["// Core Agent\nexport * from \"./agent.js\";\n// Loop functions\nexport * from \"./agent-loop.js\";\nexport * from \"./harness/agent-harness.js\";\nexport {\n\tcollectEntriesForBranchSummary,\n\tgenerateBranchSummary,\n\tprepareBranchEntries,\n} from \"./harness/compaction/branch-summarization.js\";\nexport {\n\tcalculateContextTokens,\n\tcompact,\n\tDEFAULT_COMPACTION_SETTINGS,\n\testimateContextTokens,\n\testimateTokens,\n\tfindCutPoint,\n\tfindTurnStartIndex,\n\tgenerateSummary,\n\tgetLastAssistantUsage,\n\tprepareCompaction,\n\tserializeConversation,\n\tshouldCompact,\n} from \"./harness/compaction/compaction.js\";\nexport * from \"./harness/execution-env.js\";\nexport * from \"./harness/messages.js\";\nexport * from \"./harness/prompt-templates.js\";\nexport * from \"./harness/session/repo/jsonl.js\";\nexport * from \"./harness/session/repo/memory.js\";\nexport * from \"./harness/session/repo/shared.js\";\nexport * from \"./harness/session/session.js\";\nexport * from \"./harness/skills.js\";\nexport * from \"./harness/system-prompt.js\";\n// Harness\nexport * from \"./harness/types.js\";\nexport * from \"./harness/utils/shell-output.js\";\nexport * from \"./harness/utils/truncate.js\";\n// Proxy utilities\nexport * from \"./proxy.js\";\n// Types\nexport * from \"./types.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,YAAY,CAAC;AAE3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EACN,8BAA8B,EAC9B,qBAAqB,EACrB,oBAAoB,GACpB,MAAM,8CAA8C,CAAC;AACtD,OAAO,EACN,sBAAsB,EACtB,OAAO,EACP,2BAA2B,EAC3B,qBAAqB,EACrB,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,aAAa,GACb,MAAM,oCAAoC,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,kCAAkC,CAAC;AACjD,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAE3C,cAAc,oBAAoB,CAAC;AACnC,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAE5C,cAAc,YAAY,CAAC;AAE3B,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AAErC,cAAc,YAAY,CAAC","sourcesContent":["// Core Agent\nexport * from \"./agent.js\";\n// Loop functions\nexport * from \"./agent-loop.js\";\nexport * from \"./harness/agent-harness.js\";\nexport {\n\tcollectEntriesForBranchSummary,\n\tgenerateBranchSummary,\n\tprepareBranchEntries,\n} from \"./harness/compaction/branch-summarization.js\";\nexport {\n\tcalculateContextTokens,\n\tcompact,\n\tDEFAULT_COMPACTION_SETTINGS,\n\testimateContextTokens,\n\testimateTokens,\n\tfindCutPoint,\n\tfindTurnStartIndex,\n\tgenerateSummary,\n\tgetLastAssistantUsage,\n\tprepareCompaction,\n\tserializeConversation,\n\tshouldCompact,\n} from \"./harness/compaction/compaction.js\";\nexport * from \"./harness/execution-env.js\";\nexport * from \"./harness/messages.js\";\nexport * from \"./harness/prompt-templates.js\";\nexport * from \"./harness/session/repo/jsonl.js\";\nexport * from \"./harness/session/repo/memory.js\";\nexport * from \"./harness/session/repo/shared.js\";\nexport * from \"./harness/session/session.js\";\nexport * from \"./harness/skills.js\";\nexport * from \"./harness/system-prompt.js\";\n// Harness\nexport * from \"./harness/types.js\";\nexport * from \"./harness/utils/shell-output.js\";\nexport * from \"./harness/utils/truncate.js\";\n// Proxy utilities\nexport * from \"./proxy.js\";\n// Headless tool bundles\nexport * from \"./tools/default-tools.js\";\nexport * from \"./tools/mcp-tools.js\";\n// Types\nexport * from \"./types.js\";\n"]}
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,9 @@ export * from "./harness/utils/shell-output.js";
|
|
|
20
20
|
export * from "./harness/utils/truncate.js";
|
|
21
21
|
// Proxy utilities
|
|
22
22
|
export * from "./proxy.js";
|
|
23
|
+
// Headless tool bundles
|
|
24
|
+
export * from "./tools/default-tools.js";
|
|
25
|
+
export * from "./tools/mcp-tools.js";
|
|
23
26
|
// Types
|
|
24
27
|
export * from "./types.js";
|
|
25
28
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,cAAc,YAAY,CAAC;AAC3B,iBAAiB;AACjB,cAAc,iBAAiB,CAAC;AAChC,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EACN,8BAA8B,EAC9B,qBAAqB,EACrB,oBAAoB,GACpB,MAAM,8CAA8C,CAAC;AACtD,OAAO,EACN,sBAAsB,EACtB,OAAO,EACP,2BAA2B,EAC3B,qBAAqB,EACrB,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,aAAa,GACb,MAAM,oCAAoC,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,kCAAkC,CAAC;AACjD,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,UAAU;AACV,cAAc,oBAAoB,CAAC;AACnC,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,kBAAkB;AAClB,cAAc,YAAY,CAAC;AAC3B,QAAQ;AACR,cAAc,YAAY,CAAC","sourcesContent":["// Core Agent\nexport * from \"./agent.js\";\n// Loop functions\nexport * from \"./agent-loop.js\";\nexport * from \"./harness/agent-harness.js\";\nexport {\n\tcollectEntriesForBranchSummary,\n\tgenerateBranchSummary,\n\tprepareBranchEntries,\n} from \"./harness/compaction/branch-summarization.js\";\nexport {\n\tcalculateContextTokens,\n\tcompact,\n\tDEFAULT_COMPACTION_SETTINGS,\n\testimateContextTokens,\n\testimateTokens,\n\tfindCutPoint,\n\tfindTurnStartIndex,\n\tgenerateSummary,\n\tgetLastAssistantUsage,\n\tprepareCompaction,\n\tserializeConversation,\n\tshouldCompact,\n} from \"./harness/compaction/compaction.js\";\nexport * from \"./harness/execution-env.js\";\nexport * from \"./harness/messages.js\";\nexport * from \"./harness/prompt-templates.js\";\nexport * from \"./harness/session/repo/jsonl.js\";\nexport * from \"./harness/session/repo/memory.js\";\nexport * from \"./harness/session/repo/shared.js\";\nexport * from \"./harness/session/session.js\";\nexport * from \"./harness/skills.js\";\nexport * from \"./harness/system-prompt.js\";\n// Harness\nexport * from \"./harness/types.js\";\nexport * from \"./harness/utils/shell-output.js\";\nexport * from \"./harness/utils/truncate.js\";\n// Proxy utilities\nexport * from \"./proxy.js\";\n// Types\nexport * from \"./types.js\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,aAAa;AACb,cAAc,YAAY,CAAC;AAC3B,iBAAiB;AACjB,cAAc,iBAAiB,CAAC;AAChC,cAAc,4BAA4B,CAAC;AAC3C,OAAO,EACN,8BAA8B,EAC9B,qBAAqB,EACrB,oBAAoB,GACpB,MAAM,8CAA8C,CAAC;AACtD,OAAO,EACN,sBAAsB,EACtB,OAAO,EACP,2BAA2B,EAC3B,qBAAqB,EACrB,cAAc,EACd,YAAY,EACZ,kBAAkB,EAClB,eAAe,EACf,qBAAqB,EACrB,iBAAiB,EACjB,qBAAqB,EACrB,aAAa,GACb,MAAM,oCAAoC,CAAC;AAC5C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,iCAAiC,CAAC;AAChD,cAAc,kCAAkC,CAAC;AACjD,cAAc,kCAAkC,CAAC;AACjD,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,4BAA4B,CAAC;AAC3C,UAAU;AACV,cAAc,oBAAoB,CAAC;AACnC,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,kBAAkB;AAClB,cAAc,YAAY,CAAC;AAC3B,wBAAwB;AACxB,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,QAAQ;AACR,cAAc,YAAY,CAAC","sourcesContent":["// Core Agent\nexport * from \"./agent.js\";\n// Loop functions\nexport * from \"./agent-loop.js\";\nexport * from \"./harness/agent-harness.js\";\nexport {\n\tcollectEntriesForBranchSummary,\n\tgenerateBranchSummary,\n\tprepareBranchEntries,\n} from \"./harness/compaction/branch-summarization.js\";\nexport {\n\tcalculateContextTokens,\n\tcompact,\n\tDEFAULT_COMPACTION_SETTINGS,\n\testimateContextTokens,\n\testimateTokens,\n\tfindCutPoint,\n\tfindTurnStartIndex,\n\tgenerateSummary,\n\tgetLastAssistantUsage,\n\tprepareCompaction,\n\tserializeConversation,\n\tshouldCompact,\n} from \"./harness/compaction/compaction.js\";\nexport * from \"./harness/execution-env.js\";\nexport * from \"./harness/messages.js\";\nexport * from \"./harness/prompt-templates.js\";\nexport * from \"./harness/session/repo/jsonl.js\";\nexport * from \"./harness/session/repo/memory.js\";\nexport * from \"./harness/session/repo/shared.js\";\nexport * from \"./harness/session/session.js\";\nexport * from \"./harness/skills.js\";\nexport * from \"./harness/system-prompt.js\";\n// Harness\nexport * from \"./harness/types.js\";\nexport * from \"./harness/utils/shell-output.js\";\nexport * from \"./harness/utils/truncate.js\";\n// Proxy utilities\nexport * from \"./proxy.js\";\n// Headless tool bundles\nexport * from \"./tools/default-tools.js\";\nexport * from \"./tools/mcp-tools.js\";\n// Types\nexport * from \"./types.js\";\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless default tool bundle: the same built-in tools the hoocode CLI
|
|
3
|
+
* registers (bash/read/edit/write/grep/find/ls), implemented without any
|
|
4
|
+
* CLI or TUI dependency so they can run in a separate process (for example
|
|
5
|
+
* a hooteams worker). The CLI keeps its own richer implementations with
|
|
6
|
+
* interactive rendering; these share the tool names and parameter contracts.
|
|
7
|
+
*
|
|
8
|
+
* No singletons, no top-level side effects: every call to getDefaultTools()
|
|
9
|
+
* builds a fresh bundle bound to the given cwd.
|
|
10
|
+
*/
|
|
11
|
+
import type { AgentTool } from "../types.js";
|
|
12
|
+
export interface DefaultToolsOptions {
|
|
13
|
+
/** Working directory the tools operate in. Defaults to process.cwd(). */
|
|
14
|
+
cwd?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Convert a glob pattern to a regular expression over `/`-separated paths. */
|
|
17
|
+
export declare function globToRegExp(pattern: string): RegExp;
|
|
18
|
+
/**
|
|
19
|
+
* Build the default headless tool bundle (bash/read/edit/write/grep/find/ls)
|
|
20
|
+
* bound to the given working directory.
|
|
21
|
+
*
|
|
22
|
+
* The CLI's Task tool is intentionally not part of this bundle: it requires
|
|
23
|
+
* the CLI's subagent runtime (agent registry, subagent pool, session
|
|
24
|
+
* services), which does not exist in a standalone process.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getDefaultTools(opts?: DefaultToolsOptions): AgentTool<any>[];
|
|
27
|
+
//# sourceMappingURL=default-tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-tools.d.ts","sourceRoot":"","sources":["../../src/tools/default-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAgBH,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,aAAa,CAAC;AAE9D,MAAM,WAAW,mBAAmB;IACnC,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC;CACb;AAwMD,+EAA+E;AAC/E,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0BpD;AA+ND;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,SAAS,CAAC,GAAG,CAAC,EAAE,CAY5E","sourcesContent":["/**\n * Headless default tool bundle: the same built-in tools the hoocode CLI\n * registers (bash/read/edit/write/grep/find/ls), implemented without any\n * CLI or TUI dependency so they can run in a separate process (for example\n * a hooteams worker). The CLI keeps its own richer implementations with\n * interactive rendering; these share the tool names and parameter contracts.\n *\n * No singletons, no top-level side effects: every call to getDefaultTools()\n * builds a fresh bundle bound to the given cwd.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { isAbsolute, join, relative, resolve } from \"node:path\";\nimport ignore from \"ignore\";\nimport { type Static, Type } from \"typebox\";\nimport { NodeExecutionEnv } from \"../harness/env/nodejs.js\";\nimport { FileError } from \"../harness/types.js\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tDEFAULT_MAX_LINES,\n\tformatSize,\n\ttruncateHead,\n\ttruncateLine,\n\ttruncateTail,\n} from \"../harness/utils/truncate.js\";\nimport type { AgentTool, AgentToolResult } from \"../types.js\";\n\nexport interface DefaultToolsOptions {\n\t/** Working directory the tools operate in. Defaults to process.cwd(). */\n\tcwd?: string;\n}\n\nfunction textResult(text: string): AgentToolResult<undefined> {\n\treturn { content: [{ type: \"text\", text }], details: undefined };\n}\n\nfunction resolveToCwd(cwd: string, path: string): string {\n\treturn isAbsolute(path) ? path : resolve(cwd, path);\n}\n\n// ---------------------------------------------------------------------------\n// bash\n// ---------------------------------------------------------------------------\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nfunction createBashTool(env: NodeExecutionEnv): AgentTool<typeof bashSchema> {\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Optionally provide a timeout in seconds.`,\n\t\tparameters: bashSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof bashSchema>, signal) => {\n\t\t\tlet combined = \"\";\n\t\t\tlet exitCode: number;\n\t\t\ttry {\n\t\t\t\tconst result = await env.exec(params.command, {\n\t\t\t\t\ttimeout: params.timeout,\n\t\t\t\t\tsignal,\n\t\t\t\t\tonStdout: (chunk) => {\n\t\t\t\t\t\tcombined += chunk;\n\t\t\t\t\t},\n\t\t\t\t\tonStderr: (chunk) => {\n\t\t\t\t\t\tcombined += chunk;\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t\texitCode = result.exitCode;\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tif (message.startsWith(\"timeout:\")) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Command timed out after ${params.timeout}s${combined ? `\\nOutput so far:\\n${combined}` : \"\"}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst truncation = truncateTail(combined);\n\t\t\tlet text = truncation.content;\n\t\t\tif (truncation.truncated) {\n\t\t\t\ttext = `[Output truncated: showing last ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.totalBytes)})]\\n${text}`;\n\t\t\t}\n\t\t\tif (exitCode !== 0) {\n\t\t\t\ttext = text.length > 0 ? `${text}\\nExit code: ${exitCode}` : `Exit code: ${exitCode}`;\n\t\t\t}\n\t\t\treturn textResult(text.length > 0 ? text : \"(no output)\");\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// read\n// ---------------------------------------------------------------------------\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nfunction createReadTool(env: NodeExecutionEnv): AgentTool<typeof readSchema> {\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a text file. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tparameters: readSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof readSchema>) => {\n\t\t\tconst content = await env.readTextFile(params.path);\n\t\t\tlet lines = content.split(\"\\n\");\n\t\t\tconst totalLines = lines.length;\n\t\t\tconst offset = params.offset !== undefined ? Math.max(1, Math.floor(params.offset)) : 1;\n\t\t\tif (offset > totalLines) {\n\t\t\t\tthrow new Error(`Offset ${offset} is past the end of the file (${totalLines} lines)`);\n\t\t\t}\n\t\t\tlines = lines.slice(offset - 1);\n\t\t\tif (params.limit !== undefined) {\n\t\t\t\tlines = lines.slice(0, Math.max(0, Math.floor(params.limit)));\n\t\t\t}\n\t\t\tconst truncation = truncateHead(lines.join(\"\\n\"));\n\t\t\tlet text = truncation.content;\n\t\t\tif (truncation.truncated) {\n\t\t\t\tconst lastShown = offset - 1 + truncation.outputLines;\n\t\t\t\ttext = `${text}\\n[Truncated: showing lines ${offset}-${lastShown} of ${totalLines}. Continue with offset=${lastShown + 1}]`;\n\t\t\t}\n\t\t\treturn textResult(text);\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// edit\n// ---------------------------------------------------------------------------\n\nconst replaceEditSchema = Type.Object(\n\t{\n\t\toldText: Type.String({\n\t\t\tdescription:\n\t\t\t\t\"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call.\",\n\t\t}),\n\t\tnewText: Type.String({ description: \"Replacement text for this targeted edit.\" }),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst editSchema = Type.Object(\n\t{\n\t\tpath: Type.String({ description: \"Path to the file to edit (relative or absolute)\" }),\n\t\tedits: Type.Array(replaceEditSchema, {\n\t\t\tdescription:\n\t\t\t\t\"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits.\",\n\t\t}),\n\t},\n\t{ additionalProperties: false },\n);\n\nfunction countOccurrences(haystack: string, needle: string): number {\n\tif (needle.length === 0) return 0;\n\tlet count = 0;\n\tlet index = haystack.indexOf(needle);\n\twhile (index !== -1) {\n\t\tcount++;\n\t\tindex = haystack.indexOf(needle, index + needle.length);\n\t}\n\treturn count;\n}\n\nfunction createEditTool(env: NodeExecutionEnv): AgentTool<typeof editSchema> {\n\treturn {\n\t\tname: \"edit\",\n\t\tlabel: \"edit\",\n\t\tdescription:\n\t\t\t\"Edit a file by replacing exact text. Each edit's oldText must appear exactly once in the file. Provide multiple edits to make several targeted replacements in one call.\",\n\t\tparameters: editSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof editSchema>) => {\n\t\t\tconst original = await env.readTextFile(params.path);\n\t\t\tif (params.edits.length === 0) {\n\t\t\t\tthrow new Error(\"No edits provided\");\n\t\t\t}\n\t\t\tlet content = original;\n\t\t\tfor (const [index, edit] of params.edits.entries()) {\n\t\t\t\tconst occurrences = countOccurrences(original, edit.oldText);\n\t\t\t\tif (occurrences === 0) {\n\t\t\t\t\tthrow new Error(`edits[${index}].oldText not found in ${params.path}`);\n\t\t\t\t}\n\t\t\t\tif (occurrences > 1) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`edits[${index}].oldText matches ${occurrences} locations in ${params.path}; add surrounding context to make it unique`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!content.includes(edit.oldText)) {\n\t\t\t\t\tthrow new Error(`edits[${index}].oldText overlaps with an earlier edit in the same call`);\n\t\t\t\t}\n\t\t\t\tcontent = content.replace(edit.oldText, edit.newText);\n\t\t\t}\n\t\t\tawait env.writeFile(params.path, content);\n\t\t\treturn textResult(\n\t\t\t\t`Applied ${params.edits.length} edit${params.edits.length === 1 ? \"\" : \"s\"} to ${params.path}`,\n\t\t\t);\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// write\n// ---------------------------------------------------------------------------\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\nfunction createWriteTool(env: NodeExecutionEnv): AgentTool<typeof writeSchema> {\n\treturn {\n\t\tname: \"write\",\n\t\tlabel: \"write\",\n\t\tdescription: \"Write content to a file, creating parent directories as needed. Overwrites existing files.\",\n\t\tparameters: writeSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof writeSchema>) => {\n\t\t\tawait env.writeFile(params.path, params.content);\n\t\t\treturn textResult(`Wrote ${formatSize(Buffer.byteLength(params.content, \"utf-8\"))} to ${params.path}`);\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// shared file walking for grep/find\n// ---------------------------------------------------------------------------\n\n/** Convert a glob pattern to a regular expression over `/`-separated paths. */\nexport function globToRegExp(pattern: string): RegExp {\n\tlet regex = \"\";\n\tfor (let i = 0; i < pattern.length; i++) {\n\t\tconst char = pattern[i];\n\t\tif (char === \"*\") {\n\t\t\tif (pattern[i + 1] === \"*\") {\n\t\t\t\t// `**/` and `**` cross directory boundaries\n\t\t\t\tif (pattern[i + 2] === \"/\") {\n\t\t\t\t\tregex += \"(?:[^/]+/)*\";\n\t\t\t\t\ti += 2;\n\t\t\t\t} else {\n\t\t\t\t\tregex += \".*\";\n\t\t\t\t\ti += 1;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tregex += \"[^/]*\";\n\t\t\t}\n\t\t} else if (char === \"?\") {\n\t\t\tregex += \"[^/]\";\n\t\t} else if (\"\\\\^$.|+()[]{}\".includes(char)) {\n\t\t\tregex += `\\\\${char}`;\n\t\t} else {\n\t\t\tregex += char;\n\t\t}\n\t}\n\treturn new RegExp(`^${regex}$`);\n}\n\n/** Match a relative path against a glob; patterns without `/` match the basename at any depth. */\nfunction matchGlob(relPath: string, pattern: string): boolean {\n\tconst normalized = relPath.split(\"\\\\\").join(\"/\");\n\tif (!pattern.includes(\"/\")) {\n\t\tconst base = normalized.split(\"/\").pop() ?? normalized;\n\t\treturn globToRegExp(pattern).test(base);\n\t}\n\treturn globToRegExp(pattern).test(normalized);\n}\n\nconst ALWAYS_IGNORED = new Set([\".git\", \"node_modules\"]);\n\n/**\n * Walk files under root depth-first, honoring the root .gitignore (nested\n * .gitignore files are not consulted) and always skipping .git/node_modules.\n * Yields paths relative to root with `/` separators.\n */\nasync function collectFiles(env: NodeExecutionEnv, root: string, limit: number): Promise<string[]> {\n\tconst ig = ignore();\n\ttry {\n\t\tig.add(await readFile(join(root, \".gitignore\"), \"utf-8\"));\n\t} catch {\n\t\t// no .gitignore at the search root\n\t}\n\tconst results: string[] = [];\n\tconst stack: string[] = [\"\"];\n\twhile (stack.length > 0 && results.length < limit) {\n\t\tconst dir = stack.pop()!;\n\t\tlet entries: Awaited<ReturnType<NodeExecutionEnv[\"listDir\"]>>;\n\t\ttry {\n\t\t\tentries = await env.listDir(dir === \"\" ? root : join(root, dir));\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tentries.sort((a, b) => a.name.localeCompare(b.name));\n\t\tfor (const entry of entries) {\n\t\t\tif (ALWAYS_IGNORED.has(entry.name)) continue;\n\t\t\tconst relPath = dir === \"\" ? entry.name : `${dir}/${entry.name}`;\n\t\t\tif (entry.kind === \"directory\") {\n\t\t\t\tif (ig.ignores(`${relPath}/`)) continue;\n\t\t\t\tstack.push(relPath);\n\t\t\t} else if (entry.kind === \"file\") {\n\t\t\t\tif (ig.ignores(relPath)) continue;\n\t\t\t\tresults.push(relPath);\n\t\t\t\tif (results.length >= limit) break;\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n}\n\n// ---------------------------------------------------------------------------\n// grep\n// ---------------------------------------------------------------------------\n\nconst grepSchema = Type.Object({\n\tpattern: Type.String({ description: \"Search pattern (regex or literal string)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Directory or file to search (default: current directory)\" })),\n\tglob: Type.Optional(Type.String({ description: \"Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'\" })),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive search (default: false)\" })),\n\tliteral: Type.Optional(\n\t\tType.Boolean({ description: \"Treat pattern as literal string instead of regex (default: false)\" }),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of matches to return (default: 100)\" })),\n});\n\nconst GREP_DEFAULT_LIMIT = 100;\nconst GREP_FILE_SCAN_LIMIT = 50_000;\n\nfunction escapeRegExp(text: string): string {\n\treturn text.replace(/[\\\\^$.|?*+()[\\]{}]/g, \"\\\\$&\");\n}\n\nfunction looksBinary(content: string): boolean {\n\treturn content.includes(\"\\0\");\n}\n\nfunction createGrepTool(env: NodeExecutionEnv, cwd: string): AgentTool<typeof grepSchema> {\n\treturn {\n\t\tname: \"grep\",\n\t\tlabel: \"grep\",\n\t\tdescription: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects the root .gitignore. Output is truncated to ${GREP_DEFAULT_LIMIT} matches by default.`,\n\t\tparameters: grepSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof grepSchema>, signal) => {\n\t\t\tconst source = params.literal ? escapeRegExp(params.pattern) : params.pattern;\n\t\t\tconst regex = new RegExp(source, params.ignoreCase ? \"i\" : \"\");\n\t\t\tconst limit = params.limit ?? GREP_DEFAULT_LIMIT;\n\t\t\tconst searchRoot = resolveToCwd(cwd, params.path ?? \".\");\n\n\t\t\tconst rootInfo = await env.fileInfo(searchRoot);\n\t\t\tconst files =\n\t\t\t\trootInfo.kind === \"file\"\n\t\t\t\t\t? [relative(cwd, searchRoot).split(\"\\\\\").join(\"/\") || rootInfo.name]\n\t\t\t\t\t: await collectFiles(env, searchRoot, GREP_FILE_SCAN_LIMIT);\n\n\t\t\tconst matches: string[] = [];\n\t\t\tlet limitReached = false;\n\t\t\touter: for (const file of files) {\n\t\t\t\tif (signal?.aborted) throw new Error(\"aborted\");\n\t\t\t\tif (params.glob && !matchGlob(file, params.glob)) continue;\n\t\t\t\tconst absolute = rootInfo.kind === \"file\" ? searchRoot : join(searchRoot, file);\n\t\t\t\tlet content: string;\n\t\t\t\ttry {\n\t\t\t\t\tcontent = await env.readTextFile(absolute);\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (looksBinary(content)) continue;\n\t\t\t\tconst displayPath = rootInfo.kind === \"file\" ? file : relative(cwd, absolute).split(\"\\\\\").join(\"/\");\n\t\t\t\tconst lines = content.split(\"\\n\");\n\t\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\t\tif (!regex.test(lines[i])) continue;\n\t\t\t\t\tmatches.push(`${displayPath}:${i + 1}: ${truncateLine(lines[i]).text}`);\n\t\t\t\t\tif (matches.length >= limit) {\n\t\t\t\t\t\tlimitReached = true;\n\t\t\t\t\t\tbreak outer;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (matches.length === 0) {\n\t\t\t\treturn textResult(\"No matches found\");\n\t\t\t}\n\t\t\tconst truncation = truncateHead(matches.join(\"\\n\"));\n\t\t\tlet text = truncation.content;\n\t\t\tif (limitReached) {\n\t\t\t\ttext = `${text}\\n[Match limit of ${limit} reached; refine the pattern or raise limit]`;\n\t\t\t} else if (truncation.truncated) {\n\t\t\t\ttext = `${text}\\n[Output truncated at ${formatSize(truncation.maxBytes)}]`;\n\t\t\t}\n\t\t\treturn textResult(text);\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// find\n// ---------------------------------------------------------------------------\n\nconst findSchema = Type.Object({\n\tpattern: Type.String({\n\t\tdescription: \"Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'\",\n\t}),\n\tpath: Type.Optional(Type.String({ description: \"Directory to search in (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results (default: 1000)\" })),\n});\n\nconst FIND_DEFAULT_LIMIT = 1000;\n\nfunction createFindTool(env: NodeExecutionEnv, cwd: string): AgentTool<typeof findSchema> {\n\treturn {\n\t\tname: \"find\",\n\t\tlabel: \"find\",\n\t\tdescription: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects the root .gitignore. Output is truncated to ${FIND_DEFAULT_LIMIT} results by default.`,\n\t\tparameters: findSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof findSchema>) => {\n\t\t\tconst limit = params.limit ?? FIND_DEFAULT_LIMIT;\n\t\t\tconst searchRoot = resolveToCwd(cwd, params.path ?? \".\");\n\t\t\tconst files = await collectFiles(env, searchRoot, GREP_FILE_SCAN_LIMIT);\n\t\t\tconst matched: string[] = [];\n\t\t\tlet limitReached = false;\n\t\t\tfor (const file of files) {\n\t\t\t\tif (!matchGlob(file, params.pattern)) continue;\n\t\t\t\tmatched.push(file);\n\t\t\t\tif (matched.length >= limit) {\n\t\t\t\t\tlimitReached = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (matched.length === 0) {\n\t\t\t\treturn textResult(\"No files found\");\n\t\t\t}\n\t\t\tlet text = matched.join(\"\\n\");\n\t\t\tif (limitReached) {\n\t\t\t\ttext = `${text}\\n[Result limit of ${limit} reached]`;\n\t\t\t}\n\t\t\treturn textResult(text);\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// ls\n// ---------------------------------------------------------------------------\n\nconst lsSchema = Type.Object({\n\tpath: Type.Optional(Type.String({ description: \"Directory to list (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of entries to return (default: 500)\" })),\n});\n\nconst LS_DEFAULT_LIMIT = 500;\n\nfunction createLsTool(env: NodeExecutionEnv, cwd: string): AgentTool<typeof lsSchema> {\n\treturn {\n\t\tname: \"ls\",\n\t\tlabel: \"ls\",\n\t\tdescription: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${LS_DEFAULT_LIMIT} entries by default.`,\n\t\tparameters: lsSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof lsSchema>) => {\n\t\t\tconst target = resolveToCwd(cwd, params.path ?? \".\");\n\t\t\tconst limit = params.limit ?? LS_DEFAULT_LIMIT;\n\t\t\tconst info = await env.fileInfo(target);\n\t\t\tif (info.kind !== \"directory\") {\n\t\t\t\tthrow new FileError(\"not_directory\", `Not a directory: ${target}`, target);\n\t\t\t}\n\t\t\tconst entries = await env.listDir(target);\n\t\t\tentries.sort((a, b) => a.name.localeCompare(b.name));\n\t\t\tconst shown = entries.slice(0, limit);\n\t\t\tlet text = shown.map((entry) => (entry.kind === \"directory\" ? `${entry.name}/` : entry.name)).join(\"\\n\");\n\t\t\tif (entries.length > limit) {\n\t\t\t\ttext = `${text}\\n[Entry limit of ${limit} reached; ${entries.length - limit} more entries]`;\n\t\t\t}\n\t\t\treturn textResult(text.length > 0 ? text : \"(empty directory)\");\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// bundle\n// ---------------------------------------------------------------------------\n\n/**\n * Build the default headless tool bundle (bash/read/edit/write/grep/find/ls)\n * bound to the given working directory.\n *\n * The CLI's Task tool is intentionally not part of this bundle: it requires\n * the CLI's subagent runtime (agent registry, subagent pool, session\n * services), which does not exist in a standalone process.\n */\nexport function getDefaultTools(opts?: DefaultToolsOptions): AgentTool<any>[] {\n\tconst cwd = resolve(opts?.cwd ?? process.cwd());\n\tconst env = new NodeExecutionEnv({ cwd });\n\treturn [\n\t\tcreateBashTool(env),\n\t\tcreateReadTool(env),\n\t\tcreateEditTool(env),\n\t\tcreateWriteTool(env),\n\t\tcreateGrepTool(env, cwd),\n\t\tcreateFindTool(env, cwd),\n\t\tcreateLsTool(env, cwd),\n\t];\n}\n"]}
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless default tool bundle: the same built-in tools the hoocode CLI
|
|
3
|
+
* registers (bash/read/edit/write/grep/find/ls), implemented without any
|
|
4
|
+
* CLI or TUI dependency so they can run in a separate process (for example
|
|
5
|
+
* a hooteams worker). The CLI keeps its own richer implementations with
|
|
6
|
+
* interactive rendering; these share the tool names and parameter contracts.
|
|
7
|
+
*
|
|
8
|
+
* No singletons, no top-level side effects: every call to getDefaultTools()
|
|
9
|
+
* builds a fresh bundle bound to the given cwd.
|
|
10
|
+
*/
|
|
11
|
+
import { readFile } from "node:fs/promises";
|
|
12
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
13
|
+
import ignore from "ignore";
|
|
14
|
+
import { Type } from "typebox";
|
|
15
|
+
import { NodeExecutionEnv } from "../harness/env/nodejs.js";
|
|
16
|
+
import { FileError } from "../harness/types.js";
|
|
17
|
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, truncateLine, truncateTail, } from "../harness/utils/truncate.js";
|
|
18
|
+
function textResult(text) {
|
|
19
|
+
return { content: [{ type: "text", text }], details: undefined };
|
|
20
|
+
}
|
|
21
|
+
function resolveToCwd(cwd, path) {
|
|
22
|
+
return isAbsolute(path) ? path : resolve(cwd, path);
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// bash
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
const bashSchema = Type.Object({
|
|
28
|
+
command: Type.String({ description: "Bash command to execute" }),
|
|
29
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
|
|
30
|
+
});
|
|
31
|
+
function createBashTool(env) {
|
|
32
|
+
return {
|
|
33
|
+
name: "bash",
|
|
34
|
+
label: "bash",
|
|
35
|
+
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Optionally provide a timeout in seconds.`,
|
|
36
|
+
parameters: bashSchema,
|
|
37
|
+
execute: async (_toolCallId, params, signal) => {
|
|
38
|
+
let combined = "";
|
|
39
|
+
let exitCode;
|
|
40
|
+
try {
|
|
41
|
+
const result = await env.exec(params.command, {
|
|
42
|
+
timeout: params.timeout,
|
|
43
|
+
signal,
|
|
44
|
+
onStdout: (chunk) => {
|
|
45
|
+
combined += chunk;
|
|
46
|
+
},
|
|
47
|
+
onStderr: (chunk) => {
|
|
48
|
+
combined += chunk;
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
exitCode = result.exitCode;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
55
|
+
if (message.startsWith("timeout:")) {
|
|
56
|
+
throw new Error(`Command timed out after ${params.timeout}s${combined ? `\nOutput so far:\n${combined}` : ""}`);
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
const truncation = truncateTail(combined);
|
|
61
|
+
let text = truncation.content;
|
|
62
|
+
if (truncation.truncated) {
|
|
63
|
+
text = `[Output truncated: showing last ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.totalBytes)})]\n${text}`;
|
|
64
|
+
}
|
|
65
|
+
if (exitCode !== 0) {
|
|
66
|
+
text = text.length > 0 ? `${text}\nExit code: ${exitCode}` : `Exit code: ${exitCode}`;
|
|
67
|
+
}
|
|
68
|
+
return textResult(text.length > 0 ? text : "(no output)");
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// read
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
const readSchema = Type.Object({
|
|
76
|
+
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
77
|
+
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
78
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
79
|
+
});
|
|
80
|
+
function createReadTool(env) {
|
|
81
|
+
return {
|
|
82
|
+
name: "read",
|
|
83
|
+
label: "read",
|
|
84
|
+
description: `Read the contents of a text file. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,
|
|
85
|
+
parameters: readSchema,
|
|
86
|
+
execute: async (_toolCallId, params) => {
|
|
87
|
+
const content = await env.readTextFile(params.path);
|
|
88
|
+
let lines = content.split("\n");
|
|
89
|
+
const totalLines = lines.length;
|
|
90
|
+
const offset = params.offset !== undefined ? Math.max(1, Math.floor(params.offset)) : 1;
|
|
91
|
+
if (offset > totalLines) {
|
|
92
|
+
throw new Error(`Offset ${offset} is past the end of the file (${totalLines} lines)`);
|
|
93
|
+
}
|
|
94
|
+
lines = lines.slice(offset - 1);
|
|
95
|
+
if (params.limit !== undefined) {
|
|
96
|
+
lines = lines.slice(0, Math.max(0, Math.floor(params.limit)));
|
|
97
|
+
}
|
|
98
|
+
const truncation = truncateHead(lines.join("\n"));
|
|
99
|
+
let text = truncation.content;
|
|
100
|
+
if (truncation.truncated) {
|
|
101
|
+
const lastShown = offset - 1 + truncation.outputLines;
|
|
102
|
+
text = `${text}\n[Truncated: showing lines ${offset}-${lastShown} of ${totalLines}. Continue with offset=${lastShown + 1}]`;
|
|
103
|
+
}
|
|
104
|
+
return textResult(text);
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// edit
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
const replaceEditSchema = Type.Object({
|
|
112
|
+
oldText: Type.String({
|
|
113
|
+
description: "Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call.",
|
|
114
|
+
}),
|
|
115
|
+
newText: Type.String({ description: "Replacement text for this targeted edit." }),
|
|
116
|
+
}, { additionalProperties: false });
|
|
117
|
+
const editSchema = Type.Object({
|
|
118
|
+
path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
119
|
+
edits: Type.Array(replaceEditSchema, {
|
|
120
|
+
description: "One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits.",
|
|
121
|
+
}),
|
|
122
|
+
}, { additionalProperties: false });
|
|
123
|
+
function countOccurrences(haystack, needle) {
|
|
124
|
+
if (needle.length === 0)
|
|
125
|
+
return 0;
|
|
126
|
+
let count = 0;
|
|
127
|
+
let index = haystack.indexOf(needle);
|
|
128
|
+
while (index !== -1) {
|
|
129
|
+
count++;
|
|
130
|
+
index = haystack.indexOf(needle, index + needle.length);
|
|
131
|
+
}
|
|
132
|
+
return count;
|
|
133
|
+
}
|
|
134
|
+
function createEditTool(env) {
|
|
135
|
+
return {
|
|
136
|
+
name: "edit",
|
|
137
|
+
label: "edit",
|
|
138
|
+
description: "Edit a file by replacing exact text. Each edit's oldText must appear exactly once in the file. Provide multiple edits to make several targeted replacements in one call.",
|
|
139
|
+
parameters: editSchema,
|
|
140
|
+
execute: async (_toolCallId, params) => {
|
|
141
|
+
const original = await env.readTextFile(params.path);
|
|
142
|
+
if (params.edits.length === 0) {
|
|
143
|
+
throw new Error("No edits provided");
|
|
144
|
+
}
|
|
145
|
+
let content = original;
|
|
146
|
+
for (const [index, edit] of params.edits.entries()) {
|
|
147
|
+
const occurrences = countOccurrences(original, edit.oldText);
|
|
148
|
+
if (occurrences === 0) {
|
|
149
|
+
throw new Error(`edits[${index}].oldText not found in ${params.path}`);
|
|
150
|
+
}
|
|
151
|
+
if (occurrences > 1) {
|
|
152
|
+
throw new Error(`edits[${index}].oldText matches ${occurrences} locations in ${params.path}; add surrounding context to make it unique`);
|
|
153
|
+
}
|
|
154
|
+
if (!content.includes(edit.oldText)) {
|
|
155
|
+
throw new Error(`edits[${index}].oldText overlaps with an earlier edit in the same call`);
|
|
156
|
+
}
|
|
157
|
+
content = content.replace(edit.oldText, edit.newText);
|
|
158
|
+
}
|
|
159
|
+
await env.writeFile(params.path, content);
|
|
160
|
+
return textResult(`Applied ${params.edits.length} edit${params.edits.length === 1 ? "" : "s"} to ${params.path}`);
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// write
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
const writeSchema = Type.Object({
|
|
168
|
+
path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
|
|
169
|
+
content: Type.String({ description: "Content to write to the file" }),
|
|
170
|
+
});
|
|
171
|
+
function createWriteTool(env) {
|
|
172
|
+
return {
|
|
173
|
+
name: "write",
|
|
174
|
+
label: "write",
|
|
175
|
+
description: "Write content to a file, creating parent directories as needed. Overwrites existing files.",
|
|
176
|
+
parameters: writeSchema,
|
|
177
|
+
execute: async (_toolCallId, params) => {
|
|
178
|
+
await env.writeFile(params.path, params.content);
|
|
179
|
+
return textResult(`Wrote ${formatSize(Buffer.byteLength(params.content, "utf-8"))} to ${params.path}`);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// shared file walking for grep/find
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
/** Convert a glob pattern to a regular expression over `/`-separated paths. */
|
|
187
|
+
export function globToRegExp(pattern) {
|
|
188
|
+
let regex = "";
|
|
189
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
190
|
+
const char = pattern[i];
|
|
191
|
+
if (char === "*") {
|
|
192
|
+
if (pattern[i + 1] === "*") {
|
|
193
|
+
// `**/` and `**` cross directory boundaries
|
|
194
|
+
if (pattern[i + 2] === "/") {
|
|
195
|
+
regex += "(?:[^/]+/)*";
|
|
196
|
+
i += 2;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
regex += ".*";
|
|
200
|
+
i += 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
regex += "[^/]*";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else if (char === "?") {
|
|
208
|
+
regex += "[^/]";
|
|
209
|
+
}
|
|
210
|
+
else if ("\\^$.|+()[]{}".includes(char)) {
|
|
211
|
+
regex += `\\${char}`;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
regex += char;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return new RegExp(`^${regex}$`);
|
|
218
|
+
}
|
|
219
|
+
/** Match a relative path against a glob; patterns without `/` match the basename at any depth. */
|
|
220
|
+
function matchGlob(relPath, pattern) {
|
|
221
|
+
const normalized = relPath.split("\\").join("/");
|
|
222
|
+
if (!pattern.includes("/")) {
|
|
223
|
+
const base = normalized.split("/").pop() ?? normalized;
|
|
224
|
+
return globToRegExp(pattern).test(base);
|
|
225
|
+
}
|
|
226
|
+
return globToRegExp(pattern).test(normalized);
|
|
227
|
+
}
|
|
228
|
+
const ALWAYS_IGNORED = new Set([".git", "node_modules"]);
|
|
229
|
+
/**
|
|
230
|
+
* Walk files under root depth-first, honoring the root .gitignore (nested
|
|
231
|
+
* .gitignore files are not consulted) and always skipping .git/node_modules.
|
|
232
|
+
* Yields paths relative to root with `/` separators.
|
|
233
|
+
*/
|
|
234
|
+
async function collectFiles(env, root, limit) {
|
|
235
|
+
const ig = ignore();
|
|
236
|
+
try {
|
|
237
|
+
ig.add(await readFile(join(root, ".gitignore"), "utf-8"));
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// no .gitignore at the search root
|
|
241
|
+
}
|
|
242
|
+
const results = [];
|
|
243
|
+
const stack = [""];
|
|
244
|
+
while (stack.length > 0 && results.length < limit) {
|
|
245
|
+
const dir = stack.pop();
|
|
246
|
+
let entries;
|
|
247
|
+
try {
|
|
248
|
+
entries = await env.listDir(dir === "" ? root : join(root, dir));
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
254
|
+
for (const entry of entries) {
|
|
255
|
+
if (ALWAYS_IGNORED.has(entry.name))
|
|
256
|
+
continue;
|
|
257
|
+
const relPath = dir === "" ? entry.name : `${dir}/${entry.name}`;
|
|
258
|
+
if (entry.kind === "directory") {
|
|
259
|
+
if (ig.ignores(`${relPath}/`))
|
|
260
|
+
continue;
|
|
261
|
+
stack.push(relPath);
|
|
262
|
+
}
|
|
263
|
+
else if (entry.kind === "file") {
|
|
264
|
+
if (ig.ignores(relPath))
|
|
265
|
+
continue;
|
|
266
|
+
results.push(relPath);
|
|
267
|
+
if (results.length >= limit)
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return results;
|
|
273
|
+
}
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// grep
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
const grepSchema = Type.Object({
|
|
278
|
+
pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
|
|
279
|
+
path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })),
|
|
280
|
+
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" })),
|
|
281
|
+
ignoreCase: Type.Optional(Type.Boolean({ description: "Case-insensitive search (default: false)" })),
|
|
282
|
+
literal: Type.Optional(Type.Boolean({ description: "Treat pattern as literal string instead of regex (default: false)" })),
|
|
283
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" })),
|
|
284
|
+
});
|
|
285
|
+
const GREP_DEFAULT_LIMIT = 100;
|
|
286
|
+
const GREP_FILE_SCAN_LIMIT = 50_000;
|
|
287
|
+
function escapeRegExp(text) {
|
|
288
|
+
return text.replace(/[\\^$.|?*+()[\]{}]/g, "\\$&");
|
|
289
|
+
}
|
|
290
|
+
function looksBinary(content) {
|
|
291
|
+
return content.includes("\0");
|
|
292
|
+
}
|
|
293
|
+
function createGrepTool(env, cwd) {
|
|
294
|
+
return {
|
|
295
|
+
name: "grep",
|
|
296
|
+
label: "grep",
|
|
297
|
+
description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects the root .gitignore. Output is truncated to ${GREP_DEFAULT_LIMIT} matches by default.`,
|
|
298
|
+
parameters: grepSchema,
|
|
299
|
+
execute: async (_toolCallId, params, signal) => {
|
|
300
|
+
const source = params.literal ? escapeRegExp(params.pattern) : params.pattern;
|
|
301
|
+
const regex = new RegExp(source, params.ignoreCase ? "i" : "");
|
|
302
|
+
const limit = params.limit ?? GREP_DEFAULT_LIMIT;
|
|
303
|
+
const searchRoot = resolveToCwd(cwd, params.path ?? ".");
|
|
304
|
+
const rootInfo = await env.fileInfo(searchRoot);
|
|
305
|
+
const files = rootInfo.kind === "file"
|
|
306
|
+
? [relative(cwd, searchRoot).split("\\").join("/") || rootInfo.name]
|
|
307
|
+
: await collectFiles(env, searchRoot, GREP_FILE_SCAN_LIMIT);
|
|
308
|
+
const matches = [];
|
|
309
|
+
let limitReached = false;
|
|
310
|
+
outer: for (const file of files) {
|
|
311
|
+
if (signal?.aborted)
|
|
312
|
+
throw new Error("aborted");
|
|
313
|
+
if (params.glob && !matchGlob(file, params.glob))
|
|
314
|
+
continue;
|
|
315
|
+
const absolute = rootInfo.kind === "file" ? searchRoot : join(searchRoot, file);
|
|
316
|
+
let content;
|
|
317
|
+
try {
|
|
318
|
+
content = await env.readTextFile(absolute);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (looksBinary(content))
|
|
324
|
+
continue;
|
|
325
|
+
const displayPath = rootInfo.kind === "file" ? file : relative(cwd, absolute).split("\\").join("/");
|
|
326
|
+
const lines = content.split("\n");
|
|
327
|
+
for (let i = 0; i < lines.length; i++) {
|
|
328
|
+
if (!regex.test(lines[i]))
|
|
329
|
+
continue;
|
|
330
|
+
matches.push(`${displayPath}:${i + 1}: ${truncateLine(lines[i]).text}`);
|
|
331
|
+
if (matches.length >= limit) {
|
|
332
|
+
limitReached = true;
|
|
333
|
+
break outer;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (matches.length === 0) {
|
|
338
|
+
return textResult("No matches found");
|
|
339
|
+
}
|
|
340
|
+
const truncation = truncateHead(matches.join("\n"));
|
|
341
|
+
let text = truncation.content;
|
|
342
|
+
if (limitReached) {
|
|
343
|
+
text = `${text}\n[Match limit of ${limit} reached; refine the pattern or raise limit]`;
|
|
344
|
+
}
|
|
345
|
+
else if (truncation.truncated) {
|
|
346
|
+
text = `${text}\n[Output truncated at ${formatSize(truncation.maxBytes)}]`;
|
|
347
|
+
}
|
|
348
|
+
return textResult(text);
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// find
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
const findSchema = Type.Object({
|
|
356
|
+
pattern: Type.String({
|
|
357
|
+
description: "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'",
|
|
358
|
+
}),
|
|
359
|
+
path: Type.Optional(Type.String({ description: "Directory to search in (default: current directory)" })),
|
|
360
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 1000)" })),
|
|
361
|
+
});
|
|
362
|
+
const FIND_DEFAULT_LIMIT = 1000;
|
|
363
|
+
function createFindTool(env, cwd) {
|
|
364
|
+
return {
|
|
365
|
+
name: "find",
|
|
366
|
+
label: "find",
|
|
367
|
+
description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects the root .gitignore. Output is truncated to ${FIND_DEFAULT_LIMIT} results by default.`,
|
|
368
|
+
parameters: findSchema,
|
|
369
|
+
execute: async (_toolCallId, params) => {
|
|
370
|
+
const limit = params.limit ?? FIND_DEFAULT_LIMIT;
|
|
371
|
+
const searchRoot = resolveToCwd(cwd, params.path ?? ".");
|
|
372
|
+
const files = await collectFiles(env, searchRoot, GREP_FILE_SCAN_LIMIT);
|
|
373
|
+
const matched = [];
|
|
374
|
+
let limitReached = false;
|
|
375
|
+
for (const file of files) {
|
|
376
|
+
if (!matchGlob(file, params.pattern))
|
|
377
|
+
continue;
|
|
378
|
+
matched.push(file);
|
|
379
|
+
if (matched.length >= limit) {
|
|
380
|
+
limitReached = true;
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (matched.length === 0) {
|
|
385
|
+
return textResult("No files found");
|
|
386
|
+
}
|
|
387
|
+
let text = matched.join("\n");
|
|
388
|
+
if (limitReached) {
|
|
389
|
+
text = `${text}\n[Result limit of ${limit} reached]`;
|
|
390
|
+
}
|
|
391
|
+
return textResult(text);
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// ls
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
const lsSchema = Type.Object({
|
|
399
|
+
path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
|
|
400
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })),
|
|
401
|
+
});
|
|
402
|
+
const LS_DEFAULT_LIMIT = 500;
|
|
403
|
+
function createLsTool(env, cwd) {
|
|
404
|
+
return {
|
|
405
|
+
name: "ls",
|
|
406
|
+
label: "ls",
|
|
407
|
+
description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${LS_DEFAULT_LIMIT} entries by default.`,
|
|
408
|
+
parameters: lsSchema,
|
|
409
|
+
execute: async (_toolCallId, params) => {
|
|
410
|
+
const target = resolveToCwd(cwd, params.path ?? ".");
|
|
411
|
+
const limit = params.limit ?? LS_DEFAULT_LIMIT;
|
|
412
|
+
const info = await env.fileInfo(target);
|
|
413
|
+
if (info.kind !== "directory") {
|
|
414
|
+
throw new FileError("not_directory", `Not a directory: ${target}`, target);
|
|
415
|
+
}
|
|
416
|
+
const entries = await env.listDir(target);
|
|
417
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
418
|
+
const shown = entries.slice(0, limit);
|
|
419
|
+
let text = shown.map((entry) => (entry.kind === "directory" ? `${entry.name}/` : entry.name)).join("\n");
|
|
420
|
+
if (entries.length > limit) {
|
|
421
|
+
text = `${text}\n[Entry limit of ${limit} reached; ${entries.length - limit} more entries]`;
|
|
422
|
+
}
|
|
423
|
+
return textResult(text.length > 0 ? text : "(empty directory)");
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// bundle
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
/**
|
|
431
|
+
* Build the default headless tool bundle (bash/read/edit/write/grep/find/ls)
|
|
432
|
+
* bound to the given working directory.
|
|
433
|
+
*
|
|
434
|
+
* The CLI's Task tool is intentionally not part of this bundle: it requires
|
|
435
|
+
* the CLI's subagent runtime (agent registry, subagent pool, session
|
|
436
|
+
* services), which does not exist in a standalone process.
|
|
437
|
+
*/
|
|
438
|
+
export function getDefaultTools(opts) {
|
|
439
|
+
const cwd = resolve(opts?.cwd ?? process.cwd());
|
|
440
|
+
const env = new NodeExecutionEnv({ cwd });
|
|
441
|
+
return [
|
|
442
|
+
createBashTool(env),
|
|
443
|
+
createReadTool(env),
|
|
444
|
+
createEditTool(env),
|
|
445
|
+
createWriteTool(env),
|
|
446
|
+
createGrepTool(env, cwd),
|
|
447
|
+
createFindTool(env, cwd),
|
|
448
|
+
createLsTool(env, cwd),
|
|
449
|
+
];
|
|
450
|
+
}
|
|
451
|
+
//# sourceMappingURL=default-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-tools.js","sourceRoot":"","sources":["../../src/tools/default-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Headless default tool bundle: the same built-in tools the hoocode CLI\n * registers (bash/read/edit/write/grep/find/ls), implemented without any\n * CLI or TUI dependency so they can run in a separate process (for example\n * a hooteams worker). The CLI keeps its own richer implementations with\n * interactive rendering; these share the tool names and parameter contracts.\n *\n * No singletons, no top-level side effects: every call to getDefaultTools()\n * builds a fresh bundle bound to the given cwd.\n */\n\nimport { readFile } from \"node:fs/promises\";\nimport { isAbsolute, join, relative, resolve } from \"node:path\";\nimport ignore from \"ignore\";\nimport { type Static, Type } from \"typebox\";\nimport { NodeExecutionEnv } from \"../harness/env/nodejs.js\";\nimport { FileError } from \"../harness/types.js\";\nimport {\n\tDEFAULT_MAX_BYTES,\n\tDEFAULT_MAX_LINES,\n\tformatSize,\n\ttruncateHead,\n\ttruncateLine,\n\ttruncateTail,\n} from \"../harness/utils/truncate.js\";\nimport type { AgentTool, AgentToolResult } from \"../types.js\";\n\nexport interface DefaultToolsOptions {\n\t/** Working directory the tools operate in. Defaults to process.cwd(). */\n\tcwd?: string;\n}\n\nfunction textResult(text: string): AgentToolResult<undefined> {\n\treturn { content: [{ type: \"text\", text }], details: undefined };\n}\n\nfunction resolveToCwd(cwd: string, path: string): string {\n\treturn isAbsolute(path) ? path : resolve(cwd, path);\n}\n\n// ---------------------------------------------------------------------------\n// bash\n// ---------------------------------------------------------------------------\n\nconst bashSchema = Type.Object({\n\tcommand: Type.String({ description: \"Bash command to execute\" }),\n\ttimeout: Type.Optional(Type.Number({ description: \"Timeout in seconds (optional, no default timeout)\" })),\n});\n\nfunction createBashTool(env: NodeExecutionEnv): AgentTool<typeof bashSchema> {\n\treturn {\n\t\tname: \"bash\",\n\t\tlabel: \"bash\",\n\t\tdescription: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Optionally provide a timeout in seconds.`,\n\t\tparameters: bashSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof bashSchema>, signal) => {\n\t\t\tlet combined = \"\";\n\t\t\tlet exitCode: number;\n\t\t\ttry {\n\t\t\t\tconst result = await env.exec(params.command, {\n\t\t\t\t\ttimeout: params.timeout,\n\t\t\t\t\tsignal,\n\t\t\t\t\tonStdout: (chunk) => {\n\t\t\t\t\t\tcombined += chunk;\n\t\t\t\t\t},\n\t\t\t\t\tonStderr: (chunk) => {\n\t\t\t\t\t\tcombined += chunk;\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t\texitCode = result.exitCode;\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tif (message.startsWith(\"timeout:\")) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Command timed out after ${params.timeout}s${combined ? `\\nOutput so far:\\n${combined}` : \"\"}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tconst truncation = truncateTail(combined);\n\t\t\tlet text = truncation.content;\n\t\t\tif (truncation.truncated) {\n\t\t\t\ttext = `[Output truncated: showing last ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.totalBytes)})]\\n${text}`;\n\t\t\t}\n\t\t\tif (exitCode !== 0) {\n\t\t\t\ttext = text.length > 0 ? `${text}\\nExit code: ${exitCode}` : `Exit code: ${exitCode}`;\n\t\t\t}\n\t\t\treturn textResult(text.length > 0 ? text : \"(no output)\");\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// read\n// ---------------------------------------------------------------------------\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nfunction createReadTool(env: NodeExecutionEnv): AgentTool<typeof readSchema> {\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a text file. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tparameters: readSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof readSchema>) => {\n\t\t\tconst content = await env.readTextFile(params.path);\n\t\t\tlet lines = content.split(\"\\n\");\n\t\t\tconst totalLines = lines.length;\n\t\t\tconst offset = params.offset !== undefined ? Math.max(1, Math.floor(params.offset)) : 1;\n\t\t\tif (offset > totalLines) {\n\t\t\t\tthrow new Error(`Offset ${offset} is past the end of the file (${totalLines} lines)`);\n\t\t\t}\n\t\t\tlines = lines.slice(offset - 1);\n\t\t\tif (params.limit !== undefined) {\n\t\t\t\tlines = lines.slice(0, Math.max(0, Math.floor(params.limit)));\n\t\t\t}\n\t\t\tconst truncation = truncateHead(lines.join(\"\\n\"));\n\t\t\tlet text = truncation.content;\n\t\t\tif (truncation.truncated) {\n\t\t\t\tconst lastShown = offset - 1 + truncation.outputLines;\n\t\t\t\ttext = `${text}\\n[Truncated: showing lines ${offset}-${lastShown} of ${totalLines}. Continue with offset=${lastShown + 1}]`;\n\t\t\t}\n\t\t\treturn textResult(text);\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// edit\n// ---------------------------------------------------------------------------\n\nconst replaceEditSchema = Type.Object(\n\t{\n\t\toldText: Type.String({\n\t\t\tdescription:\n\t\t\t\t\"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call.\",\n\t\t}),\n\t\tnewText: Type.String({ description: \"Replacement text for this targeted edit.\" }),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst editSchema = Type.Object(\n\t{\n\t\tpath: Type.String({ description: \"Path to the file to edit (relative or absolute)\" }),\n\t\tedits: Type.Array(replaceEditSchema, {\n\t\t\tdescription:\n\t\t\t\t\"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits.\",\n\t\t}),\n\t},\n\t{ additionalProperties: false },\n);\n\nfunction countOccurrences(haystack: string, needle: string): number {\n\tif (needle.length === 0) return 0;\n\tlet count = 0;\n\tlet index = haystack.indexOf(needle);\n\twhile (index !== -1) {\n\t\tcount++;\n\t\tindex = haystack.indexOf(needle, index + needle.length);\n\t}\n\treturn count;\n}\n\nfunction createEditTool(env: NodeExecutionEnv): AgentTool<typeof editSchema> {\n\treturn {\n\t\tname: \"edit\",\n\t\tlabel: \"edit\",\n\t\tdescription:\n\t\t\t\"Edit a file by replacing exact text. Each edit's oldText must appear exactly once in the file. Provide multiple edits to make several targeted replacements in one call.\",\n\t\tparameters: editSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof editSchema>) => {\n\t\t\tconst original = await env.readTextFile(params.path);\n\t\t\tif (params.edits.length === 0) {\n\t\t\t\tthrow new Error(\"No edits provided\");\n\t\t\t}\n\t\t\tlet content = original;\n\t\t\tfor (const [index, edit] of params.edits.entries()) {\n\t\t\t\tconst occurrences = countOccurrences(original, edit.oldText);\n\t\t\t\tif (occurrences === 0) {\n\t\t\t\t\tthrow new Error(`edits[${index}].oldText not found in ${params.path}`);\n\t\t\t\t}\n\t\t\t\tif (occurrences > 1) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`edits[${index}].oldText matches ${occurrences} locations in ${params.path}; add surrounding context to make it unique`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (!content.includes(edit.oldText)) {\n\t\t\t\t\tthrow new Error(`edits[${index}].oldText overlaps with an earlier edit in the same call`);\n\t\t\t\t}\n\t\t\t\tcontent = content.replace(edit.oldText, edit.newText);\n\t\t\t}\n\t\t\tawait env.writeFile(params.path, content);\n\t\t\treturn textResult(\n\t\t\t\t`Applied ${params.edits.length} edit${params.edits.length === 1 ? \"\" : \"s\"} to ${params.path}`,\n\t\t\t);\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// write\n// ---------------------------------------------------------------------------\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\nfunction createWriteTool(env: NodeExecutionEnv): AgentTool<typeof writeSchema> {\n\treturn {\n\t\tname: \"write\",\n\t\tlabel: \"write\",\n\t\tdescription: \"Write content to a file, creating parent directories as needed. Overwrites existing files.\",\n\t\tparameters: writeSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof writeSchema>) => {\n\t\t\tawait env.writeFile(params.path, params.content);\n\t\t\treturn textResult(`Wrote ${formatSize(Buffer.byteLength(params.content, \"utf-8\"))} to ${params.path}`);\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// shared file walking for grep/find\n// ---------------------------------------------------------------------------\n\n/** Convert a glob pattern to a regular expression over `/`-separated paths. */\nexport function globToRegExp(pattern: string): RegExp {\n\tlet regex = \"\";\n\tfor (let i = 0; i < pattern.length; i++) {\n\t\tconst char = pattern[i];\n\t\tif (char === \"*\") {\n\t\t\tif (pattern[i + 1] === \"*\") {\n\t\t\t\t// `**/` and `**` cross directory boundaries\n\t\t\t\tif (pattern[i + 2] === \"/\") {\n\t\t\t\t\tregex += \"(?:[^/]+/)*\";\n\t\t\t\t\ti += 2;\n\t\t\t\t} else {\n\t\t\t\t\tregex += \".*\";\n\t\t\t\t\ti += 1;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tregex += \"[^/]*\";\n\t\t\t}\n\t\t} else if (char === \"?\") {\n\t\t\tregex += \"[^/]\";\n\t\t} else if (\"\\\\^$.|+()[]{}\".includes(char)) {\n\t\t\tregex += `\\\\${char}`;\n\t\t} else {\n\t\t\tregex += char;\n\t\t}\n\t}\n\treturn new RegExp(`^${regex}$`);\n}\n\n/** Match a relative path against a glob; patterns without `/` match the basename at any depth. */\nfunction matchGlob(relPath: string, pattern: string): boolean {\n\tconst normalized = relPath.split(\"\\\\\").join(\"/\");\n\tif (!pattern.includes(\"/\")) {\n\t\tconst base = normalized.split(\"/\").pop() ?? normalized;\n\t\treturn globToRegExp(pattern).test(base);\n\t}\n\treturn globToRegExp(pattern).test(normalized);\n}\n\nconst ALWAYS_IGNORED = new Set([\".git\", \"node_modules\"]);\n\n/**\n * Walk files under root depth-first, honoring the root .gitignore (nested\n * .gitignore files are not consulted) and always skipping .git/node_modules.\n * Yields paths relative to root with `/` separators.\n */\nasync function collectFiles(env: NodeExecutionEnv, root: string, limit: number): Promise<string[]> {\n\tconst ig = ignore();\n\ttry {\n\t\tig.add(await readFile(join(root, \".gitignore\"), \"utf-8\"));\n\t} catch {\n\t\t// no .gitignore at the search root\n\t}\n\tconst results: string[] = [];\n\tconst stack: string[] = [\"\"];\n\twhile (stack.length > 0 && results.length < limit) {\n\t\tconst dir = stack.pop()!;\n\t\tlet entries: Awaited<ReturnType<NodeExecutionEnv[\"listDir\"]>>;\n\t\ttry {\n\t\t\tentries = await env.listDir(dir === \"\" ? root : join(root, dir));\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\t\tentries.sort((a, b) => a.name.localeCompare(b.name));\n\t\tfor (const entry of entries) {\n\t\t\tif (ALWAYS_IGNORED.has(entry.name)) continue;\n\t\t\tconst relPath = dir === \"\" ? entry.name : `${dir}/${entry.name}`;\n\t\t\tif (entry.kind === \"directory\") {\n\t\t\t\tif (ig.ignores(`${relPath}/`)) continue;\n\t\t\t\tstack.push(relPath);\n\t\t\t} else if (entry.kind === \"file\") {\n\t\t\t\tif (ig.ignores(relPath)) continue;\n\t\t\t\tresults.push(relPath);\n\t\t\t\tif (results.length >= limit) break;\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n}\n\n// ---------------------------------------------------------------------------\n// grep\n// ---------------------------------------------------------------------------\n\nconst grepSchema = Type.Object({\n\tpattern: Type.String({ description: \"Search pattern (regex or literal string)\" }),\n\tpath: Type.Optional(Type.String({ description: \"Directory or file to search (default: current directory)\" })),\n\tglob: Type.Optional(Type.String({ description: \"Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'\" })),\n\tignoreCase: Type.Optional(Type.Boolean({ description: \"Case-insensitive search (default: false)\" })),\n\tliteral: Type.Optional(\n\t\tType.Boolean({ description: \"Treat pattern as literal string instead of regex (default: false)\" }),\n\t),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of matches to return (default: 100)\" })),\n});\n\nconst GREP_DEFAULT_LIMIT = 100;\nconst GREP_FILE_SCAN_LIMIT = 50_000;\n\nfunction escapeRegExp(text: string): string {\n\treturn text.replace(/[\\\\^$.|?*+()[\\]{}]/g, \"\\\\$&\");\n}\n\nfunction looksBinary(content: string): boolean {\n\treturn content.includes(\"\\0\");\n}\n\nfunction createGrepTool(env: NodeExecutionEnv, cwd: string): AgentTool<typeof grepSchema> {\n\treturn {\n\t\tname: \"grep\",\n\t\tlabel: \"grep\",\n\t\tdescription: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects the root .gitignore. Output is truncated to ${GREP_DEFAULT_LIMIT} matches by default.`,\n\t\tparameters: grepSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof grepSchema>, signal) => {\n\t\t\tconst source = params.literal ? escapeRegExp(params.pattern) : params.pattern;\n\t\t\tconst regex = new RegExp(source, params.ignoreCase ? \"i\" : \"\");\n\t\t\tconst limit = params.limit ?? GREP_DEFAULT_LIMIT;\n\t\t\tconst searchRoot = resolveToCwd(cwd, params.path ?? \".\");\n\n\t\t\tconst rootInfo = await env.fileInfo(searchRoot);\n\t\t\tconst files =\n\t\t\t\trootInfo.kind === \"file\"\n\t\t\t\t\t? [relative(cwd, searchRoot).split(\"\\\\\").join(\"/\") || rootInfo.name]\n\t\t\t\t\t: await collectFiles(env, searchRoot, GREP_FILE_SCAN_LIMIT);\n\n\t\t\tconst matches: string[] = [];\n\t\t\tlet limitReached = false;\n\t\t\touter: for (const file of files) {\n\t\t\t\tif (signal?.aborted) throw new Error(\"aborted\");\n\t\t\t\tif (params.glob && !matchGlob(file, params.glob)) continue;\n\t\t\t\tconst absolute = rootInfo.kind === \"file\" ? searchRoot : join(searchRoot, file);\n\t\t\t\tlet content: string;\n\t\t\t\ttry {\n\t\t\t\t\tcontent = await env.readTextFile(absolute);\n\t\t\t\t} catch {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (looksBinary(content)) continue;\n\t\t\t\tconst displayPath = rootInfo.kind === \"file\" ? file : relative(cwd, absolute).split(\"\\\\\").join(\"/\");\n\t\t\t\tconst lines = content.split(\"\\n\");\n\t\t\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\t\t\tif (!regex.test(lines[i])) continue;\n\t\t\t\t\tmatches.push(`${displayPath}:${i + 1}: ${truncateLine(lines[i]).text}`);\n\t\t\t\t\tif (matches.length >= limit) {\n\t\t\t\t\t\tlimitReached = true;\n\t\t\t\t\t\tbreak outer;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (matches.length === 0) {\n\t\t\t\treturn textResult(\"No matches found\");\n\t\t\t}\n\t\t\tconst truncation = truncateHead(matches.join(\"\\n\"));\n\t\t\tlet text = truncation.content;\n\t\t\tif (limitReached) {\n\t\t\t\ttext = `${text}\\n[Match limit of ${limit} reached; refine the pattern or raise limit]`;\n\t\t\t} else if (truncation.truncated) {\n\t\t\t\ttext = `${text}\\n[Output truncated at ${formatSize(truncation.maxBytes)}]`;\n\t\t\t}\n\t\t\treturn textResult(text);\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// find\n// ---------------------------------------------------------------------------\n\nconst findSchema = Type.Object({\n\tpattern: Type.String({\n\t\tdescription: \"Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'\",\n\t}),\n\tpath: Type.Optional(Type.String({ description: \"Directory to search in (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of results (default: 1000)\" })),\n});\n\nconst FIND_DEFAULT_LIMIT = 1000;\n\nfunction createFindTool(env: NodeExecutionEnv, cwd: string): AgentTool<typeof findSchema> {\n\treturn {\n\t\tname: \"find\",\n\t\tlabel: \"find\",\n\t\tdescription: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects the root .gitignore. Output is truncated to ${FIND_DEFAULT_LIMIT} results by default.`,\n\t\tparameters: findSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof findSchema>) => {\n\t\t\tconst limit = params.limit ?? FIND_DEFAULT_LIMIT;\n\t\t\tconst searchRoot = resolveToCwd(cwd, params.path ?? \".\");\n\t\t\tconst files = await collectFiles(env, searchRoot, GREP_FILE_SCAN_LIMIT);\n\t\t\tconst matched: string[] = [];\n\t\t\tlet limitReached = false;\n\t\t\tfor (const file of files) {\n\t\t\t\tif (!matchGlob(file, params.pattern)) continue;\n\t\t\t\tmatched.push(file);\n\t\t\t\tif (matched.length >= limit) {\n\t\t\t\t\tlimitReached = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (matched.length === 0) {\n\t\t\t\treturn textResult(\"No files found\");\n\t\t\t}\n\t\t\tlet text = matched.join(\"\\n\");\n\t\t\tif (limitReached) {\n\t\t\t\ttext = `${text}\\n[Result limit of ${limit} reached]`;\n\t\t\t}\n\t\t\treturn textResult(text);\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// ls\n// ---------------------------------------------------------------------------\n\nconst lsSchema = Type.Object({\n\tpath: Type.Optional(Type.String({ description: \"Directory to list (default: current directory)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of entries to return (default: 500)\" })),\n});\n\nconst LS_DEFAULT_LIMIT = 500;\n\nfunction createLsTool(env: NodeExecutionEnv, cwd: string): AgentTool<typeof lsSchema> {\n\treturn {\n\t\tname: \"ls\",\n\t\tlabel: \"ls\",\n\t\tdescription: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${LS_DEFAULT_LIMIT} entries by default.`,\n\t\tparameters: lsSchema,\n\t\texecute: async (_toolCallId, params: Static<typeof lsSchema>) => {\n\t\t\tconst target = resolveToCwd(cwd, params.path ?? \".\");\n\t\t\tconst limit = params.limit ?? LS_DEFAULT_LIMIT;\n\t\t\tconst info = await env.fileInfo(target);\n\t\t\tif (info.kind !== \"directory\") {\n\t\t\t\tthrow new FileError(\"not_directory\", `Not a directory: ${target}`, target);\n\t\t\t}\n\t\t\tconst entries = await env.listDir(target);\n\t\t\tentries.sort((a, b) => a.name.localeCompare(b.name));\n\t\t\tconst shown = entries.slice(0, limit);\n\t\t\tlet text = shown.map((entry) => (entry.kind === \"directory\" ? `${entry.name}/` : entry.name)).join(\"\\n\");\n\t\t\tif (entries.length > limit) {\n\t\t\t\ttext = `${text}\\n[Entry limit of ${limit} reached; ${entries.length - limit} more entries]`;\n\t\t\t}\n\t\t\treturn textResult(text.length > 0 ? text : \"(empty directory)\");\n\t\t},\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// bundle\n// ---------------------------------------------------------------------------\n\n/**\n * Build the default headless tool bundle (bash/read/edit/write/grep/find/ls)\n * bound to the given working directory.\n *\n * The CLI's Task tool is intentionally not part of this bundle: it requires\n * the CLI's subagent runtime (agent registry, subagent pool, session\n * services), which does not exist in a standalone process.\n */\nexport function getDefaultTools(opts?: DefaultToolsOptions): AgentTool<any>[] {\n\tconst cwd = resolve(opts?.cwd ?? process.cwd());\n\tconst env = new NodeExecutionEnv({ cwd });\n\treturn [\n\t\tcreateBashTool(env),\n\t\tcreateReadTool(env),\n\t\tcreateEditTool(env),\n\t\tcreateWriteTool(env),\n\t\tcreateGrepTool(env, cwd),\n\t\tcreateFindTool(env, cwd),\n\t\tcreateLsTool(env, cwd),\n\t];\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless MCP tool loader: parse a standard mcp.json file (the
|
|
3
|
+
* `{ "mcpServers": { ... } }` format used by Claude Desktop, VS Code, and the
|
|
4
|
+
* hoocode CLI), spawn the declared stdio servers, and expose their tools as
|
|
5
|
+
* AgentTool instances usable by any Agent in any process.
|
|
6
|
+
*
|
|
7
|
+
* Spawned servers are tracked per loader call and reaped on process exit so
|
|
8
|
+
* they never linger as orphans. Call closeMcpTools() to terminate them
|
|
9
|
+
* earlier (tests, graceful shutdown).
|
|
10
|
+
*/
|
|
11
|
+
import type { AgentTool } from "../types.js";
|
|
12
|
+
export interface McpToolsServerConfig {
|
|
13
|
+
/** Unique server identifier used as prefix for tool names. */
|
|
14
|
+
name: string;
|
|
15
|
+
command: string;
|
|
16
|
+
args?: string[];
|
|
17
|
+
env?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
/** Terminate every MCP server spawned by loadMcpTools() in this process. */
|
|
20
|
+
export declare function closeMcpTools(): void;
|
|
21
|
+
/**
|
|
22
|
+
* Parse a standard mcp.json file, start every declared stdio server, and
|
|
23
|
+
* return their tools as AgentTool instances (named `mcp_<server>_<tool>`).
|
|
24
|
+
*
|
|
25
|
+
* An empty or server-less config resolves to []. A missing or malformed file,
|
|
26
|
+
* or a server that fails its handshake, rejects — callers decide whether MCP
|
|
27
|
+
* is optional. Spawned servers are terminated automatically on process exit.
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadMcpTools(mcpJsonPath: string): Promise<AgentTool<any>[]>;
|
|
30
|
+
//# sourceMappingURL=mcp-tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-tools.d.ts","sourceRoot":"","sources":["../../src/tools/mcp-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,OAAO,KAAK,EAAE,SAAS,EAAmB,MAAM,aAAa,CAAC;AAE9D,MAAM,WAAW,oBAAoB;IACpC,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B;AAmCD,4EAA4E;AAC5E,wBAAgB,aAAa,IAAI,IAAI,CASpC;AAuJD;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAwBjF","sourcesContent":["/**\n * Headless MCP tool loader: parse a standard mcp.json file (the\n * `{ \"mcpServers\": { ... } }` format used by Claude Desktop, VS Code, and the\n * hoocode CLI), spawn the declared stdio servers, and expose their tools as\n * AgentTool instances usable by any Agent in any process.\n *\n * Spawned servers are tracked per loader call and reaped on process exit so\n * they never linger as orphans. Call closeMcpTools() to terminate them\n * earlier (tests, graceful shutdown).\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { readFile } from \"node:fs/promises\";\nimport { createInterface } from \"node:readline\";\nimport { type TObject, Type } from \"typebox\";\nimport type { AgentTool, AgentToolResult } from \"../types.js\";\n\nexport interface McpToolsServerConfig {\n\t/** Unique server identifier used as prefix for tool names. */\n\tname: string;\n\tcommand: string;\n\targs?: string[];\n\tenv?: Record<string, string>;\n}\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\ninterface McpConnection {\n\trpc(method: string, params?: unknown, timeoutMs?: number): Promise<unknown>;\n\tnotify(method: string, params?: unknown): void;\n\tterminate(): void;\n}\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\nconst liveConnections = new Set<McpConnection>();\nlet exitCleanupInstalled = false;\n\n/** Kill spawned MCP servers when the host process exits so they don't linger\n * as orphans (their stdin merely goes idle, which doesn't terminate them). */\nfunction installExitCleanup(): void {\n\tif (exitCleanupInstalled) return;\n\texitCleanupInstalled = true;\n\tprocess.once(\"exit\", () => {\n\t\tcloseMcpTools();\n\t});\n}\n\n/** Terminate every MCP server spawned by loadMcpTools() in this process. */\nexport function closeMcpTools(): void {\n\tfor (const conn of liveConnections) {\n\t\ttry {\n\t\t\tconn.terminate();\n\t\t} catch {\n\t\t\t// best-effort cleanup\n\t\t}\n\t}\n\tliveConnections.clear();\n}\n\nfunction spawnMcpServer(config: McpToolsServerConfig): 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 { id?: number; result?: unknown; error?: { message: string } };\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});\n\n\tfunction rpc(method: string, params?: unknown, timeoutMs?: number): Promise<unknown> {\n\t\tconst id = nextId++;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tlet timer: NodeJS.Timeout | undefined;\n\t\t\tif (timeoutMs && timeoutMs > 0) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tif (pending.delete(id)) {\n\t\t\t\t\t\treject(new Error(`MCP server \"${config.name}\" timed out after ${timeoutMs}ms on ${method}`));\n\t\t\t\t\t}\n\t\t\t\t}, timeoutMs);\n\t\t\t\ttimer.unref?.();\n\t\t\t}\n\t\t\tpending.set(id, {\n\t\t\t\tresolve: (r) => {\n\t\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\t\tresolve(r);\n\t\t\t\t},\n\t\t\t\treject: (e) => {\n\t\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\t\treject(e);\n\t\t\t\t},\n\t\t\t});\n\t\t\tproc.stdin!.write(`${JSON.stringify({ jsonrpc: \"2.0\", id, method, params })}\\n`);\n\t\t});\n\t}\n\n\tfunction notify(method: string, params?: unknown): void {\n\t\tproc.stdin!.write(`${JSON.stringify({ jsonrpc: \"2.0\", method, params })}\\n`);\n\t}\n\n\treturn {\n\t\trpc,\n\t\tnotify,\n\t\tterminate: () => {\n\t\t\trl.close();\n\t\t\tproc.kill();\n\t\t},\n\t};\n}\n\nasync function connectMcpServer(config: McpToolsServerConfig): Promise<{ conn: McpConnection; tools: McpToolDef[] }> {\n\tconst conn = spawnMcpServer(config);\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-agent-core\", version: \"1.0.0\" },\n\t\t\t},\n\t\t\tMCP_HANDSHAKE_TIMEOUT_MS,\n\t\t);\n\t\t// Per the MCP spec the client must acknowledge a successful initialize with\n\t\t// the initialized notification before issuing further requests; strict\n\t\t// servers gate tools/call on it.\n\t\tconn.notify(\"notifications/initialized\");\n\t\tconst toolsResult = (await conn.rpc(\"tools/list\", {}, MCP_HANDSHAKE_TIMEOUT_MS)) as { tools?: McpToolDef[] };\n\t\treturn { conn, tools: toolsResult.tools ?? [] };\n\t} catch (error) {\n\t\tconn.terminate();\n\t\tthrow error;\n\t}\n}\n\nfunction buildMcpSchema(tool: McpToolDef): TObject {\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\nfunction createMcpAgentTool(serverName: string, conn: McpConnection, tool: McpToolDef): AgentTool<any> {\n\tconst schema = buildMcpSchema(tool);\n\treturn {\n\t\tname: `mcp_${serverName}_${tool.name}`,\n\t\tlabel: `[MCP] ${serverName} › ${tool.name}`,\n\t\tdescription: tool.description ?? `MCP tool ${tool.name} from server ${serverName}`,\n\t\tparameters: schema,\n\t\texecute: async (_toolCallId, params, signal): Promise<AgentToolResult<undefined>> => {\n\t\t\tconst abortPromise = new Promise<never>((_, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsignal?.addEventListener(\"abort\", () => reject(new Error(\"Aborted\")), { once: true });\n\t\t\t});\n\t\t\tconst result = await Promise.race([\n\t\t\t\tconn.rpc(\"tools/call\", { name: tool.name, arguments: params }),\n\t\t\t\tabortPromise,\n\t\t\t]);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t};\n}\n\ninterface StandardMcpConfig {\n\tmcpServers?: Record<string, { command: string; args?: string[]; env?: Record<string, string> }>;\n}\n\n/**\n * Parse a standard mcp.json file, start every declared stdio server, and\n * return their tools as AgentTool instances (named `mcp_<server>_<tool>`).\n *\n * An empty or server-less config resolves to []. A missing or malformed file,\n * or a server that fails its handshake, rejects — callers decide whether MCP\n * is optional. Spawned servers are terminated automatically on process exit.\n */\nexport async function loadMcpTools(mcpJsonPath: string): Promise<AgentTool<any>[]> {\n\tconst raw = await readFile(mcpJsonPath, \"utf-8\");\n\tconst parsed = JSON.parse(raw) as StandardMcpConfig;\n\tconst servers = Object.entries(parsed.mcpServers ?? {});\n\tif (servers.length === 0) return [];\n\n\tinstallExitCleanup();\n\tconst tools: AgentTool<any>[] = [];\n\tfor (const [name, serverConfig] of servers) {\n\t\tif (!serverConfig || typeof serverConfig.command !== \"string\") {\n\t\t\tthrow new Error(`${mcpJsonPath}: mcpServers[\"${name}\"] is missing a \"command\"`);\n\t\t}\n\t\tconst { conn, tools: toolDefs } = await connectMcpServer({\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});\n\t\tliveConnections.add(conn);\n\t\tfor (const toolDef of toolDefs) {\n\t\t\ttools.push(createMcpAgentTool(name, conn, toolDef));\n\t\t}\n\t}\n\treturn tools;\n}\n"]}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless MCP tool loader: parse a standard mcp.json file (the
|
|
3
|
+
* `{ "mcpServers": { ... } }` format used by Claude Desktop, VS Code, and the
|
|
4
|
+
* hoocode CLI), spawn the declared stdio servers, and expose their tools as
|
|
5
|
+
* AgentTool instances usable by any Agent in any process.
|
|
6
|
+
*
|
|
7
|
+
* Spawned servers are tracked per loader call and reaped on process exit so
|
|
8
|
+
* they never linger as orphans. Call closeMcpTools() to terminate them
|
|
9
|
+
* earlier (tests, graceful shutdown).
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
import { createInterface } from "node:readline";
|
|
14
|
+
import { Type } from "typebox";
|
|
15
|
+
/** Timeout for the connection handshake (initialize / tools/list). Tool calls
|
|
16
|
+
* themselves are left untimed since MCP tools can be long-running. */
|
|
17
|
+
const MCP_HANDSHAKE_TIMEOUT_MS = 15000;
|
|
18
|
+
const liveConnections = new Set();
|
|
19
|
+
let exitCleanupInstalled = false;
|
|
20
|
+
/** Kill spawned MCP servers when the host process exits so they don't linger
|
|
21
|
+
* as orphans (their stdin merely goes idle, which doesn't terminate them). */
|
|
22
|
+
function installExitCleanup() {
|
|
23
|
+
if (exitCleanupInstalled)
|
|
24
|
+
return;
|
|
25
|
+
exitCleanupInstalled = true;
|
|
26
|
+
process.once("exit", () => {
|
|
27
|
+
closeMcpTools();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Terminate every MCP server spawned by loadMcpTools() in this process. */
|
|
31
|
+
export function closeMcpTools() {
|
|
32
|
+
for (const conn of liveConnections) {
|
|
33
|
+
try {
|
|
34
|
+
conn.terminate();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// best-effort cleanup
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
liveConnections.clear();
|
|
41
|
+
}
|
|
42
|
+
function spawnMcpServer(config) {
|
|
43
|
+
const proc = spawn(config.command, config.args ?? [], {
|
|
44
|
+
env: { ...process.env, ...(config.env ?? {}) },
|
|
45
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
46
|
+
});
|
|
47
|
+
let nextId = 1;
|
|
48
|
+
const pending = new Map();
|
|
49
|
+
const rl = createInterface({ input: proc.stdout });
|
|
50
|
+
rl.on("line", (line) => {
|
|
51
|
+
if (!line.trim())
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
const msg = JSON.parse(line);
|
|
55
|
+
if (msg.id === undefined)
|
|
56
|
+
return;
|
|
57
|
+
const cb = pending.get(msg.id);
|
|
58
|
+
if (!cb)
|
|
59
|
+
return;
|
|
60
|
+
pending.delete(msg.id);
|
|
61
|
+
if (msg.error)
|
|
62
|
+
cb.reject(new Error(msg.error.message));
|
|
63
|
+
else
|
|
64
|
+
cb.resolve(msg.result);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// ignore non-JSON server startup output
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
proc.on("exit", () => {
|
|
71
|
+
for (const cb of pending.values())
|
|
72
|
+
cb.reject(new Error(`MCP server "${config.name}" exited unexpectedly`));
|
|
73
|
+
pending.clear();
|
|
74
|
+
});
|
|
75
|
+
function rpc(method, params, timeoutMs) {
|
|
76
|
+
const id = nextId++;
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
let timer;
|
|
79
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
80
|
+
timer = setTimeout(() => {
|
|
81
|
+
if (pending.delete(id)) {
|
|
82
|
+
reject(new Error(`MCP server "${config.name}" timed out after ${timeoutMs}ms on ${method}`));
|
|
83
|
+
}
|
|
84
|
+
}, timeoutMs);
|
|
85
|
+
timer.unref?.();
|
|
86
|
+
}
|
|
87
|
+
pending.set(id, {
|
|
88
|
+
resolve: (r) => {
|
|
89
|
+
if (timer)
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
resolve(r);
|
|
92
|
+
},
|
|
93
|
+
reject: (e) => {
|
|
94
|
+
if (timer)
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
reject(e);
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
proc.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function notify(method, params) {
|
|
103
|
+
proc.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
rpc,
|
|
107
|
+
notify,
|
|
108
|
+
terminate: () => {
|
|
109
|
+
rl.close();
|
|
110
|
+
proc.kill();
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
async function connectMcpServer(config) {
|
|
115
|
+
const conn = spawnMcpServer(config);
|
|
116
|
+
try {
|
|
117
|
+
await conn.rpc("initialize", {
|
|
118
|
+
protocolVersion: "2024-11-05",
|
|
119
|
+
capabilities: { tools: {} },
|
|
120
|
+
clientInfo: { name: "hoocode-agent-core", version: "1.0.0" },
|
|
121
|
+
}, MCP_HANDSHAKE_TIMEOUT_MS);
|
|
122
|
+
// Per the MCP spec the client must acknowledge a successful initialize with
|
|
123
|
+
// the initialized notification before issuing further requests; strict
|
|
124
|
+
// servers gate tools/call on it.
|
|
125
|
+
conn.notify("notifications/initialized");
|
|
126
|
+
const toolsResult = (await conn.rpc("tools/list", {}, MCP_HANDSHAKE_TIMEOUT_MS));
|
|
127
|
+
return { conn, tools: toolsResult.tools ?? [] };
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
conn.terminate();
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function buildMcpSchema(tool) {
|
|
135
|
+
const props = tool.inputSchema?.properties ?? {};
|
|
136
|
+
const required = new Set(tool.inputSchema?.required ?? []);
|
|
137
|
+
const shape = {};
|
|
138
|
+
for (const [key, prop] of Object.entries(props)) {
|
|
139
|
+
let field;
|
|
140
|
+
switch (prop.type) {
|
|
141
|
+
case "number":
|
|
142
|
+
case "integer":
|
|
143
|
+
field = Type.Number({ description: prop.description });
|
|
144
|
+
break;
|
|
145
|
+
case "boolean":
|
|
146
|
+
field = Type.Boolean({ description: prop.description });
|
|
147
|
+
break;
|
|
148
|
+
default:
|
|
149
|
+
field = Type.String({ description: prop.description });
|
|
150
|
+
}
|
|
151
|
+
shape[key] = required.has(key) ? field : Type.Optional(field);
|
|
152
|
+
}
|
|
153
|
+
return Type.Object(shape);
|
|
154
|
+
}
|
|
155
|
+
function createMcpAgentTool(serverName, conn, tool) {
|
|
156
|
+
const schema = buildMcpSchema(tool);
|
|
157
|
+
return {
|
|
158
|
+
name: `mcp_${serverName}_${tool.name}`,
|
|
159
|
+
label: `[MCP] ${serverName} › ${tool.name}`,
|
|
160
|
+
description: tool.description ?? `MCP tool ${tool.name} from server ${serverName}`,
|
|
161
|
+
parameters: schema,
|
|
162
|
+
execute: async (_toolCallId, params, signal) => {
|
|
163
|
+
const abortPromise = new Promise((_, reject) => {
|
|
164
|
+
if (signal?.aborted) {
|
|
165
|
+
reject(new Error("Aborted"));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
signal?.addEventListener("abort", () => reject(new Error("Aborted")), { once: true });
|
|
169
|
+
});
|
|
170
|
+
const result = await Promise.race([
|
|
171
|
+
conn.rpc("tools/call", { name: tool.name, arguments: params }),
|
|
172
|
+
abortPromise,
|
|
173
|
+
]);
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
176
|
+
details: undefined,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Parse a standard mcp.json file, start every declared stdio server, and
|
|
183
|
+
* return their tools as AgentTool instances (named `mcp_<server>_<tool>`).
|
|
184
|
+
*
|
|
185
|
+
* An empty or server-less config resolves to []. A missing or malformed file,
|
|
186
|
+
* or a server that fails its handshake, rejects — callers decide whether MCP
|
|
187
|
+
* is optional. Spawned servers are terminated automatically on process exit.
|
|
188
|
+
*/
|
|
189
|
+
export async function loadMcpTools(mcpJsonPath) {
|
|
190
|
+
const raw = await readFile(mcpJsonPath, "utf-8");
|
|
191
|
+
const parsed = JSON.parse(raw);
|
|
192
|
+
const servers = Object.entries(parsed.mcpServers ?? {});
|
|
193
|
+
if (servers.length === 0)
|
|
194
|
+
return [];
|
|
195
|
+
installExitCleanup();
|
|
196
|
+
const tools = [];
|
|
197
|
+
for (const [name, serverConfig] of servers) {
|
|
198
|
+
if (!serverConfig || typeof serverConfig.command !== "string") {
|
|
199
|
+
throw new Error(`${mcpJsonPath}: mcpServers["${name}"] is missing a "command"`);
|
|
200
|
+
}
|
|
201
|
+
const { conn, tools: toolDefs } = await connectMcpServer({
|
|
202
|
+
name,
|
|
203
|
+
command: serverConfig.command,
|
|
204
|
+
args: serverConfig.args,
|
|
205
|
+
env: serverConfig.env,
|
|
206
|
+
});
|
|
207
|
+
liveConnections.add(conn);
|
|
208
|
+
for (const toolDef of toolDefs) {
|
|
209
|
+
tools.push(createMcpAgentTool(name, conn, toolDef));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return tools;
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=mcp-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-tools.js","sourceRoot":"","sources":["../../src/tools/mcp-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Headless MCP tool loader: parse a standard mcp.json file (the\n * `{ \"mcpServers\": { ... } }` format used by Claude Desktop, VS Code, and the\n * hoocode CLI), spawn the declared stdio servers, and expose their tools as\n * AgentTool instances usable by any Agent in any process.\n *\n * Spawned servers are tracked per loader call and reaped on process exit so\n * they never linger as orphans. Call closeMcpTools() to terminate them\n * earlier (tests, graceful shutdown).\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { readFile } from \"node:fs/promises\";\nimport { createInterface } from \"node:readline\";\nimport { type TObject, Type } from \"typebox\";\nimport type { AgentTool, AgentToolResult } from \"../types.js\";\n\nexport interface McpToolsServerConfig {\n\t/** Unique server identifier used as prefix for tool names. */\n\tname: string;\n\tcommand: string;\n\targs?: string[];\n\tenv?: Record<string, string>;\n}\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\ninterface McpConnection {\n\trpc(method: string, params?: unknown, timeoutMs?: number): Promise<unknown>;\n\tnotify(method: string, params?: unknown): void;\n\tterminate(): void;\n}\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\nconst liveConnections = new Set<McpConnection>();\nlet exitCleanupInstalled = false;\n\n/** Kill spawned MCP servers when the host process exits so they don't linger\n * as orphans (their stdin merely goes idle, which doesn't terminate them). */\nfunction installExitCleanup(): void {\n\tif (exitCleanupInstalled) return;\n\texitCleanupInstalled = true;\n\tprocess.once(\"exit\", () => {\n\t\tcloseMcpTools();\n\t});\n}\n\n/** Terminate every MCP server spawned by loadMcpTools() in this process. */\nexport function closeMcpTools(): void {\n\tfor (const conn of liveConnections) {\n\t\ttry {\n\t\t\tconn.terminate();\n\t\t} catch {\n\t\t\t// best-effort cleanup\n\t\t}\n\t}\n\tliveConnections.clear();\n}\n\nfunction spawnMcpServer(config: McpToolsServerConfig): 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 { id?: number; result?: unknown; error?: { message: string } };\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});\n\n\tfunction rpc(method: string, params?: unknown, timeoutMs?: number): Promise<unknown> {\n\t\tconst id = nextId++;\n\t\treturn new Promise<unknown>((resolve, reject) => {\n\t\t\tlet timer: NodeJS.Timeout | undefined;\n\t\t\tif (timeoutMs && timeoutMs > 0) {\n\t\t\t\ttimer = setTimeout(() => {\n\t\t\t\t\tif (pending.delete(id)) {\n\t\t\t\t\t\treject(new Error(`MCP server \"${config.name}\" timed out after ${timeoutMs}ms on ${method}`));\n\t\t\t\t\t}\n\t\t\t\t}, timeoutMs);\n\t\t\t\ttimer.unref?.();\n\t\t\t}\n\t\t\tpending.set(id, {\n\t\t\t\tresolve: (r) => {\n\t\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\t\tresolve(r);\n\t\t\t\t},\n\t\t\t\treject: (e) => {\n\t\t\t\t\tif (timer) clearTimeout(timer);\n\t\t\t\t\treject(e);\n\t\t\t\t},\n\t\t\t});\n\t\t\tproc.stdin!.write(`${JSON.stringify({ jsonrpc: \"2.0\", id, method, params })}\\n`);\n\t\t});\n\t}\n\n\tfunction notify(method: string, params?: unknown): void {\n\t\tproc.stdin!.write(`${JSON.stringify({ jsonrpc: \"2.0\", method, params })}\\n`);\n\t}\n\n\treturn {\n\t\trpc,\n\t\tnotify,\n\t\tterminate: () => {\n\t\t\trl.close();\n\t\t\tproc.kill();\n\t\t},\n\t};\n}\n\nasync function connectMcpServer(config: McpToolsServerConfig): Promise<{ conn: McpConnection; tools: McpToolDef[] }> {\n\tconst conn = spawnMcpServer(config);\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-agent-core\", version: \"1.0.0\" },\n\t\t\t},\n\t\t\tMCP_HANDSHAKE_TIMEOUT_MS,\n\t\t);\n\t\t// Per the MCP spec the client must acknowledge a successful initialize with\n\t\t// the initialized notification before issuing further requests; strict\n\t\t// servers gate tools/call on it.\n\t\tconn.notify(\"notifications/initialized\");\n\t\tconst toolsResult = (await conn.rpc(\"tools/list\", {}, MCP_HANDSHAKE_TIMEOUT_MS)) as { tools?: McpToolDef[] };\n\t\treturn { conn, tools: toolsResult.tools ?? [] };\n\t} catch (error) {\n\t\tconn.terminate();\n\t\tthrow error;\n\t}\n}\n\nfunction buildMcpSchema(tool: McpToolDef): TObject {\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\nfunction createMcpAgentTool(serverName: string, conn: McpConnection, tool: McpToolDef): AgentTool<any> {\n\tconst schema = buildMcpSchema(tool);\n\treturn {\n\t\tname: `mcp_${serverName}_${tool.name}`,\n\t\tlabel: `[MCP] ${serverName} › ${tool.name}`,\n\t\tdescription: tool.description ?? `MCP tool ${tool.name} from server ${serverName}`,\n\t\tparameters: schema,\n\t\texecute: async (_toolCallId, params, signal): Promise<AgentToolResult<undefined>> => {\n\t\t\tconst abortPromise = new Promise<never>((_, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsignal?.addEventListener(\"abort\", () => reject(new Error(\"Aborted\")), { once: true });\n\t\t\t});\n\t\t\tconst result = await Promise.race([\n\t\t\t\tconn.rpc(\"tools/call\", { name: tool.name, arguments: params }),\n\t\t\t\tabortPromise,\n\t\t\t]);\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\", text: JSON.stringify(result, null, 2) }],\n\t\t\t\tdetails: undefined,\n\t\t\t};\n\t\t},\n\t};\n}\n\ninterface StandardMcpConfig {\n\tmcpServers?: Record<string, { command: string; args?: string[]; env?: Record<string, string> }>;\n}\n\n/**\n * Parse a standard mcp.json file, start every declared stdio server, and\n * return their tools as AgentTool instances (named `mcp_<server>_<tool>`).\n *\n * An empty or server-less config resolves to []. A missing or malformed file,\n * or a server that fails its handshake, rejects — callers decide whether MCP\n * is optional. Spawned servers are terminated automatically on process exit.\n */\nexport async function loadMcpTools(mcpJsonPath: string): Promise<AgentTool<any>[]> {\n\tconst raw = await readFile(mcpJsonPath, \"utf-8\");\n\tconst parsed = JSON.parse(raw) as StandardMcpConfig;\n\tconst servers = Object.entries(parsed.mcpServers ?? {});\n\tif (servers.length === 0) return [];\n\n\tinstallExitCleanup();\n\tconst tools: AgentTool<any>[] = [];\n\tfor (const [name, serverConfig] of servers) {\n\t\tif (!serverConfig || typeof serverConfig.command !== \"string\") {\n\t\t\tthrow new Error(`${mcpJsonPath}: mcpServers[\"${name}\"] is missing a \"command\"`);\n\t\t}\n\t\tconst { conn, tools: toolDefs } = await connectMcpServer({\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});\n\t\tliveConnections.add(conn);\n\t\tfor (const toolDef of toolDefs) {\n\t\t\ttools.push(createMcpAgentTool(name, conn, toolDef));\n\t\t}\n\t}\n\treturn tools;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kolisachint/hoocode-agent-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.49",
|
|
4
4
|
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"prepublishOnly": "npm run clean && npm run build"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@kolisachint/hoocode-ai": "^0.4.
|
|
20
|
+
"@kolisachint/hoocode-ai": "^0.4.49",
|
|
21
21
|
"ignore": "^7.0.5",
|
|
22
22
|
"typebox": "^1.1.24",
|
|
23
23
|
"uuid": "^14.0.0",
|