@fleetagent/pi-coding-agent 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/cli/args.d.ts +3 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +20 -8
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +10 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +75 -14
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts +2 -0
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +1 -0
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +86 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +3 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +27 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +55 -2
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/pi-agent.d.ts.map +1 -1
- package/dist/core/pi-agent.js +1 -0
- package/dist/core/pi-agent.js.map +1 -1
- package/dist/core/prompt-templates.d.ts +5 -0
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +115 -0
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/resource-loader.d.ts +15 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +355 -40
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/rules.d.ts +6 -0
- package/dist/core/rules.d.ts.map +1 -1
- package/dist/core/rules.js +231 -10
- package/dist/core/rules.js.map +1 -1
- package/dist/core/skills.d.ts +6 -0
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +231 -10
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/source-info.d.ts +2 -0
- package/dist/core/source-info.d.ts.map +1 -1
- package/dist/core/source-info.js +6 -0
- package/dist/core/source-info.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +11 -8
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +5 -5
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +2 -2
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +2 -2
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +1 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +1 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +2 -2
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/operations.d.ts +49 -4
- package/dist/core/tools/operations.d.ts.map +1 -1
- package/dist/core/tools/operations.js +340 -4
- package/dist/core/tools/operations.js.map +1 -1
- package/dist/core/tools/read.d.ts +2 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +14 -6
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +9 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +16 -0
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +3 -2
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +36 -19
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +61 -28
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +11 -4
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +6 -6
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +11 -7
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +10 -4
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/extensions.md +83 -4
- package/docs/rpc.md +31 -0
- package/docs/usage.md +5 -0
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAG3D,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AACtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAItD,OAAO,EAAiC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEnG,QAAA,MAAM,QAAQ;;;EAGZ,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC;AAIlD,MAAM,WAAW,aAAa;IAC7B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;CAAG;AAkDjC,wBAAgB,sBAAsB,CACrC,UAAU,EAAE,cAAc,EAC1B,QAAQ,CAAC,EAAE,aAAa,GACtB,cAAc,CAAC,OAAO,QAAQ,EAAE,aAAa,GAAG,SAAS,CAAC,CA8H5D;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC,OAAO,QAAQ,CAAC,CAE5G","sourcesContent":["import type { AgentTool } from \"@fleetagent/pi-agent-core\";\nimport { Text } from \"@fleetagent/pi-tui\";\nimport nodePath from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport type { ToolOperations } from \"./operations.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.ts\";\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\nexport type LsToolInput = Static<typeof lsSchema>;\n\nconst DEFAULT_LIMIT = 500;\n\nexport interface LsToolDetails {\n\ttruncation?: TruncationResult;\n\tentryLimitReached?: number;\n}\n\nexport interface LsToolOptions {}\n\nfunction formatLsCall(\n\targs: { path?: string; limit?: number } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.ts\").theme,\n): string {\n\tconst rawPath = str(args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath || \".\") : null;\n\tconst limit = args?.limit;\n\tconst invalidArg = invalidArgText(theme);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"ls\"))} ${path === null ? invalidArg : theme.fg(\"accent\", path)}`;\n\tif (limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t}\n\treturn text;\n}\n\nfunction formatLsResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: LsToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.ts\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 20;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\n\tconst entryLimit = result.details?.entryLimitReached;\n\tconst truncation = result.details?.truncation;\n\tif (entryLimit || truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (entryLimit) warnings.push(`${entryLimit} entries limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createLsToolDefinition(\n\toperations: ToolOperations,\n\t_options?: LsToolOptions,\n): ToolDefinition<typeof lsSchema, LsToolDetails | undefined> {\n\tconst ops = operations;\n\tconst cwd = operations.cwd;\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 ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\t\tpromptSnippet: \"List directory contents\",\n\t\tparameters: lsSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ path, limit }: { path?: string; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst dirPath = resolveToCwd(path || \".\", cwd);\n\t\t\t\t\t\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t\t// Check if path exists.\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait ops.access(dirPath, \"exists\");\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treject(new Error(`Path not found: ${dirPath}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Check if path is a directory.\n\t\t\t\t\t\tconst stat = await ops.stat(dirPath);\n\t\t\t\t\t\tif (!stat.isDirectory()) {\n\t\t\t\t\t\t\treject(new Error(`Not a directory: ${dirPath}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Read directory entries.\n\t\t\t\t\t\tlet entries: string[];\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tentries = await ops.readdir(dirPath);\n\t\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\t\treject(new Error(`Cannot read directory: ${e.message}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Sort alphabetically, case-insensitive.\n\t\t\t\t\t\tentries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));\n\n\t\t\t\t\t\t// Format entries with directory indicators.\n\t\t\t\t\t\tconst results: string[] = [];\n\t\t\t\t\t\tlet entryLimitReached = false;\n\t\t\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\t\t\tif (results.length >= effectiveLimit) {\n\t\t\t\t\t\t\t\tentryLimitReached = true;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst fullPath = nodePath.join(dirPath, entry);\n\t\t\t\t\t\t\tlet suffix = \"\";\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst entryStat = await ops.stat(fullPath);\n\t\t\t\t\t\t\t\tif (entryStat.isDirectory()) suffix = \"/\";\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Skip entries we cannot stat.\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tresults.push(entry + suffix);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\t\tif (results.length === 0) {\n\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"(empty directory)\" }], details: undefined });\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\t\t\t// Apply byte truncation. There is no separate line limit because entry count is already capped.\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: LsToolDetails = {};\n\t\t\t\t\t\t// Build actionable notices for truncation and entry limits.\n\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\t\t\tnotices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);\n\t\t\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\treject(e);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createLsTool(operations: ToolOperations, options?: LsToolOptions): AgentTool<typeof lsSchema> {\n\treturn wrapToolDefinition(createLsToolDefinition(operations, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ls.d.ts","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAG3D,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AACtF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAItD,OAAO,EAAiC,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEnG,QAAA,MAAM,QAAQ;;;EAGZ,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC;AAIlD,MAAM,WAAW,aAAa;IAC7B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;CAAG;AAkDjC,wBAAgB,sBAAsB,CACrC,UAAU,EAAE,cAAc,EAC1B,QAAQ,CAAC,EAAE,aAAa,GACtB,cAAc,CAAC,OAAO,QAAQ,EAAE,aAAa,GAAG,SAAS,CAAC,CA8H5D;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,SAAS,CAAC,OAAO,QAAQ,CAAC,CAE5G","sourcesContent":["import type { AgentTool } from \"@fleetagent/pi-agent-core\";\nimport { Text } from \"@fleetagent/pi-tui\";\nimport nodePath from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport type { ToolOperations } from \"./operations.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\nimport { formatBackendIcon, getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.ts\";\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\nexport type LsToolInput = Static<typeof lsSchema>;\n\nconst DEFAULT_LIMIT = 500;\n\nexport interface LsToolDetails {\n\ttruncation?: TruncationResult;\n\tentryLimitReached?: number;\n}\n\nexport interface LsToolOptions {}\n\nfunction formatLsCall(\n\targs: { path?: string; limit?: number } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.ts\").theme,\n): string {\n\tconst rawPath = str(args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath || \".\") : null;\n\tconst limit = args?.limit;\n\tconst invalidArg = invalidArgText(theme);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"ls\"))} ${path === null ? invalidArg : theme.fg(\"accent\", path)}`;\n\tif (limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t}\n\treturn text;\n}\n\nfunction formatLsResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: LsToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.ts\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 20;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\n\tconst entryLimit = result.details?.entryLimitReached;\n\tconst truncation = result.details?.truncation;\n\tif (entryLimit || truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (entryLimit) warnings.push(`${entryLimit} entries limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createLsToolDefinition(\n\toperations: ToolOperations,\n\t_options?: LsToolOptions,\n): ToolDefinition<typeof lsSchema, LsToolDetails | undefined> {\n\tconst ops = operations;\n\tconst cwd = operations.cwd;\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 ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\t\tpromptSnippet: \"List directory contents\",\n\t\tparameters: lsSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ path, limit }: { path?: string; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst dirPath = resolveToCwd(path || \".\", cwd);\n\t\t\t\t\t\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t\t// Check if path exists.\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait ops.access(dirPath, \"exists\");\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treject(new Error(`Path not found: ${dirPath}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Check if path is a directory.\n\t\t\t\t\t\tconst stat = await ops.stat(dirPath);\n\t\t\t\t\t\tif (!stat.isDirectory()) {\n\t\t\t\t\t\t\treject(new Error(`Not a directory: ${dirPath}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Read directory entries.\n\t\t\t\t\t\tlet entries: string[];\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tentries = await ops.readdir(dirPath);\n\t\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\t\treject(new Error(`Cannot read directory: ${e.message}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Sort alphabetically, case-insensitive.\n\t\t\t\t\t\tentries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));\n\n\t\t\t\t\t\t// Format entries with directory indicators.\n\t\t\t\t\t\tconst results: string[] = [];\n\t\t\t\t\t\tlet entryLimitReached = false;\n\t\t\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\t\t\tif (results.length >= effectiveLimit) {\n\t\t\t\t\t\t\t\tentryLimitReached = true;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst fullPath = nodePath.join(dirPath, entry);\n\t\t\t\t\t\t\tlet suffix = \"\";\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst entryStat = await ops.stat(fullPath);\n\t\t\t\t\t\t\t\tif (entryStat.isDirectory()) suffix = \"/\";\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Skip entries we cannot stat.\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tresults.push(entry + suffix);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\t\tif (results.length === 0) {\n\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"(empty directory)\" }], details: undefined });\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\t\t\t// Apply byte truncation. There is no separate line limit because entry count is already capped.\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: LsToolDetails = {};\n\t\t\t\t\t\t// Build actionable notices for truncation and entry limits.\n\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\t\t\tnotices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);\n\t\t\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\treject(e);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatBackendIcon(ops.getBackendInfo?.(), theme) + formatLsCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createLsTool(operations: ToolOperations, options?: LsToolOptions): AgentTool<typeof lsSchema> {\n\treturn wrapToolDefinition(createLsToolDefinition(operations, options));\n}\n"]}
|
package/dist/core/tools/ls.js
CHANGED
|
@@ -3,7 +3,7 @@ import nodePath from "path";
|
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
|
5
5
|
import { resolveToCwd } from "./path-utils.js";
|
|
6
|
-
import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
|
|
6
|
+
import { formatBackendIcon, getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
|
|
7
7
|
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
8
8
|
import { DEFAULT_MAX_BYTES, formatSize, truncateHead } from "./truncate.js";
|
|
9
9
|
const lsSchema = Type.Object({
|
|
@@ -151,7 +151,7 @@ export function createLsToolDefinition(operations, _options) {
|
|
|
151
151
|
},
|
|
152
152
|
renderCall(args, theme, context) {
|
|
153
153
|
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
154
|
-
text.setText(formatLsCall(args, theme));
|
|
154
|
+
text.setText(formatBackendIcon(ops.getBackendInfo?.(), theme) + formatLsCall(args, theme));
|
|
155
155
|
return text;
|
|
156
156
|
},
|
|
157
157
|
renderResult(result, options, theme, context) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ls.js","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAC1C,OAAO,QAAQ,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,wDAAwD,CAAC;AAGjF,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAyB,YAAY,EAAE,MAAM,eAAe,CAAC;AAEnG,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC;IAC5B,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,gDAAgD,EAAE,CAAC,CAAC;IACnG,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,oDAAoD,EAAE,CAAC,CAAC;CACxG,CAAC,CAAC;AAIH,MAAM,aAAa,GAAG,GAAG,CAAC;AAS1B,SAAS,YAAY,CACpB,IAAmD,EACnD,KAAoE,EAC3D;IACT,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IAChC,MAAM,IAAI,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACnE,MAAM,KAAK,GAAG,IAAI,EAAE,KAAK,CAAC;IAC1B,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACzC,IAAI,IAAI,GAAG,GAAG,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC;IACjH,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACzB,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,WAAW,KAAK,GAAG,CAAC,CAAC;IACrD,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,SAAS,cAAc,CACtB,MAGC,EACD,OAAgC,EAChC,KAAoE,EACpE,UAAmB,EACV;IACT,MAAM,MAAM,GAAG,aAAa,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;IACxD,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,MAAM,EAAE,CAAC;QACZ,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACjC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,MAAM,YAAY,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,GAAG,QAAQ,CAAC;QAC1C,IAAI,IAAI,KAAK,YAAY,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACnF,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YACnB,IAAI,IAAI,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,SAAS,cAAc,CAAC,IAAI,OAAO,CAAC,kBAAkB,EAAE,WAAW,CAAC,GAAG,CAAC;QAChH,CAAC;IACF,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACrD,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC;IAC9C,IAAI,UAAU,IAAI,UAAU,EAAE,SAAS,EAAE,CAAC;QACzC,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,IAAI,UAAU;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,UAAU,gBAAgB,CAAC,CAAC;QAC7D,IAAI,UAAU,EAAE,SAAS;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,UAAU,CAAC,QAAQ,IAAI,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QAC1G,IAAI,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,eAAe,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IAC3E,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,UAAU,sBAAsB,CACrC,UAA0B,EAC1B,QAAwB,EACqC;IAC7D,MAAM,GAAG,GAAG,UAAU,CAAC;IACvB,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC;IAC3B,OAAO;QACN,IAAI,EAAE,IAAI;QACV,KAAK,EAAE,IAAI;QACX,WAAW,EAAE,8IAA8I,aAAa,eAAe,iBAAiB,GAAG,IAAI,8BAA8B;QAC7O,aAAa,EAAE,yBAAyB;QACxC,UAAU,EAAE,QAAQ;QACpB,KAAK,CAAC,OAAO,CACZ,WAAW,EACX,EAAE,IAAI,EAAE,KAAK,EAAqC,EAClD,MAAoB,EACpB,SAAU,EACV,IAAK,EACJ;YACD,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;gBACvC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;oBACrB,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;oBACvC,OAAO;gBACR,CAAC;gBAED,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC;gBAC7D,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;gBAE3D,CAAC,KAAK,IAAI,EAAE,CAAC;oBACZ,IAAI,CAAC;wBACJ,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,IAAI,GAAG,EAAE,GAAG,CAAC,CAAC;wBAC/C,MAAM,cAAc,GAAG,KAAK,IAAI,aAAa,CAAC;wBAE9C,wBAAwB;wBACxB,IAAI,CAAC;4BACJ,MAAM,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;wBACrC,CAAC;wBAAC,MAAM,CAAC;4BACR,MAAM,CAAC,IAAI,KAAK,CAAC,mBAAmB,OAAO,EAAE,CAAC,CAAC,CAAC;4BAChD,OAAO;wBACR,CAAC;wBAED,gCAAgC;wBAChC,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;wBACrC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;4BACzB,MAAM,CAAC,IAAI,KAAK,CAAC,oBAAoB,OAAO,EAAE,CAAC,CAAC,CAAC;4BACjD,OAAO;wBACR,CAAC;wBAED,0BAA0B;wBAC1B,IAAI,OAAiB,CAAC;wBACtB,IAAI,CAAC;4BACJ,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;wBACtC,CAAC;wBAAC,OAAO,CAAM,EAAE,CAAC;4BACjB,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;4BACzD,OAAO;wBACR,CAAC;wBAED,yCAAyC;wBACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;wBAEvE,4CAA4C;wBAC5C,MAAM,OAAO,GAAa,EAAE,CAAC;wBAC7B,IAAI,iBAAiB,GAAG,KAAK,CAAC;wBAC9B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;4BAC7B,IAAI,OAAO,CAAC,MAAM,IAAI,cAAc,EAAE,CAAC;gCACtC,iBAAiB,GAAG,IAAI,CAAC;gCACzB,MAAM;4BACP,CAAC;4BAED,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;4BAC/C,IAAI,MAAM,GAAG,EAAE,CAAC;4BAChB,IAAI,CAAC;gCACJ,MAAM,SAAS,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;gCAC3C,IAAI,SAAS,CAAC,WAAW,EAAE;oCAAE,MAAM,GAAG,GAAG,CAAC;4BAC3C,CAAC;4BAAC,MAAM,CAAC;gCACR,+BAA+B;gCAC/B,SAAS;4BACV,CAAC;4BACD,OAAO,CAAC,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC;wBAC9B,CAAC;wBAED,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;wBAE9C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;4BAC1B,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,EAAE,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;4BACxF,OAAO;wBACR,CAAC;wBAED,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;wBACrC,gGAAgG;wBAChG,MAAM,UAAU,GAAG,YAAY,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC;wBAClF,IAAI,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC;wBAChC,MAAM,OAAO,GAAkB,EAAE,CAAC;wBAClC,4DAA4D;wBAC5D,MAAM,OAAO,GAAa,EAAE,CAAC;wBAC7B,IAAI,iBAAiB,EAAE,CAAC;4BACvB,OAAO,CAAC,IAAI,CAAC,GAAG,cAAc,qCAAqC,cAAc,GAAG,CAAC,WAAW,CAAC,CAAC;4BAClG,OAAO,CAAC,iBAAiB,GAAG,cAAc,CAAC;wBAC5C,CAAC;wBACD,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;4BAC1B,OAAO,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,CAAC;4BAC/D,OAAO,CAAC,UAAU,GAAG,UAAU,CAAC;wBACjC,CAAC;wBACD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;4BACxB,MAAM,IAAI,QAAQ,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;wBACzC,CAAC;wBAED,OAAO,CAAC;4BACP,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;4BACzC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;yBAC9D,CAAC,CAAC;oBACJ,CAAC;oBAAC,OAAO,CAAM,EAAE,CAAC;wBACjB,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;wBAC9C,MAAM,CAAC,CAAC,CAAC,CAAC;oBACX,CAAC;gBAAA,CACD,CAAC,EAAE,CAAC;YAAA,CACL,CAAC,CAAC;QAAA,CACH;QACD,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE;YAChC,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;YACxC,OAAO,IAAI,CAAC;QAAA,CACZ;QACD,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE;YAC7C,MAAM,IAAI,GAAI,OAAO,CAAC,aAAkC,IAAI,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC/E,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,MAAa,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;YAChF,OAAO,IAAI,CAAC;QAAA,CACZ;KACD,CAAC;AAAA,CACF;AAED,MAAM,UAAU,YAAY,CAAC,UAA0B,EAAE,OAAuB,EAA8B;IAC7G,OAAO,kBAAkB,CAAC,sBAAsB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;AAAA,CACvE","sourcesContent":["import type { AgentTool } from \"@fleetagent/pi-agent-core\";\nimport { Text } from \"@fleetagent/pi-tui\";\nimport nodePath from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport type { ToolOperations } from \"./operations.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.ts\";\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\nexport type LsToolInput = Static<typeof lsSchema>;\n\nconst DEFAULT_LIMIT = 500;\n\nexport interface LsToolDetails {\n\ttruncation?: TruncationResult;\n\tentryLimitReached?: number;\n}\n\nexport interface LsToolOptions {}\n\nfunction formatLsCall(\n\targs: { path?: string; limit?: number } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.ts\").theme,\n): string {\n\tconst rawPath = str(args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath || \".\") : null;\n\tconst limit = args?.limit;\n\tconst invalidArg = invalidArgText(theme);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"ls\"))} ${path === null ? invalidArg : theme.fg(\"accent\", path)}`;\n\tif (limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t}\n\treturn text;\n}\n\nfunction formatLsResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: LsToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.ts\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 20;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\n\tconst entryLimit = result.details?.entryLimitReached;\n\tconst truncation = result.details?.truncation;\n\tif (entryLimit || truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (entryLimit) warnings.push(`${entryLimit} entries limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createLsToolDefinition(\n\toperations: ToolOperations,\n\t_options?: LsToolOptions,\n): ToolDefinition<typeof lsSchema, LsToolDetails | undefined> {\n\tconst ops = operations;\n\tconst cwd = operations.cwd;\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 ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\t\tpromptSnippet: \"List directory contents\",\n\t\tparameters: lsSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ path, limit }: { path?: string; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst dirPath = resolveToCwd(path || \".\", cwd);\n\t\t\t\t\t\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t\t// Check if path exists.\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait ops.access(dirPath, \"exists\");\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treject(new Error(`Path not found: ${dirPath}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Check if path is a directory.\n\t\t\t\t\t\tconst stat = await ops.stat(dirPath);\n\t\t\t\t\t\tif (!stat.isDirectory()) {\n\t\t\t\t\t\t\treject(new Error(`Not a directory: ${dirPath}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Read directory entries.\n\t\t\t\t\t\tlet entries: string[];\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tentries = await ops.readdir(dirPath);\n\t\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\t\treject(new Error(`Cannot read directory: ${e.message}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Sort alphabetically, case-insensitive.\n\t\t\t\t\t\tentries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));\n\n\t\t\t\t\t\t// Format entries with directory indicators.\n\t\t\t\t\t\tconst results: string[] = [];\n\t\t\t\t\t\tlet entryLimitReached = false;\n\t\t\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\t\t\tif (results.length >= effectiveLimit) {\n\t\t\t\t\t\t\t\tentryLimitReached = true;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst fullPath = nodePath.join(dirPath, entry);\n\t\t\t\t\t\t\tlet suffix = \"\";\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst entryStat = await ops.stat(fullPath);\n\t\t\t\t\t\t\t\tif (entryStat.isDirectory()) suffix = \"/\";\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Skip entries we cannot stat.\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tresults.push(entry + suffix);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\t\tif (results.length === 0) {\n\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"(empty directory)\" }], details: undefined });\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\t\t\t// Apply byte truncation. There is no separate line limit because entry count is already capped.\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: LsToolDetails = {};\n\t\t\t\t\t\t// Build actionable notices for truncation and entry limits.\n\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\t\t\tnotices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);\n\t\t\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\treject(e);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createLsTool(operations: ToolOperations, options?: LsToolOptions): AgentTool<typeof lsSchema> {\n\treturn wrapToolDefinition(createLsToolDefinition(operations, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ls.js","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@fleetagent/pi-agent-core\";\nimport { Text } from \"@fleetagent/pi-tui\";\nimport nodePath from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.ts\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.ts\";\nimport type { ToolOperations } from \"./operations.ts\";\nimport { resolveToCwd } from \"./path-utils.ts\";\nimport { formatBackendIcon, getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.ts\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.ts\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.ts\";\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\nexport type LsToolInput = Static<typeof lsSchema>;\n\nconst DEFAULT_LIMIT = 500;\n\nexport interface LsToolDetails {\n\ttruncation?: TruncationResult;\n\tentryLimitReached?: number;\n}\n\nexport interface LsToolOptions {}\n\nfunction formatLsCall(\n\targs: { path?: string; limit?: number } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.ts\").theme,\n): string {\n\tconst rawPath = str(args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath || \".\") : null;\n\tconst limit = args?.limit;\n\tconst invalidArg = invalidArgText(theme);\n\tlet text = `${theme.fg(\"toolTitle\", theme.bold(\"ls\"))} ${path === null ? invalidArg : theme.fg(\"accent\", path)}`;\n\tif (limit !== undefined) {\n\t\ttext += theme.fg(\"toolOutput\", ` (limit ${limit})`);\n\t}\n\treturn text;\n}\n\nfunction formatLsResult(\n\tresult: {\n\t\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\t\tdetails?: LsToolDetails;\n\t},\n\toptions: ToolRenderResultOptions,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.ts\").theme,\n\tshowImages: boolean,\n): string {\n\tconst output = getTextOutput(result, showImages).trim();\n\tlet text = \"\";\n\tif (output) {\n\t\tconst lines = output.split(\"\\n\");\n\t\tconst maxLines = options.expanded ? lines.length : 20;\n\t\tconst displayLines = lines.slice(0, maxLines);\n\t\tconst remaining = lines.length - maxLines;\n\t\ttext += `\\n${displayLines.map((line) => theme.fg(\"toolOutput\", line)).join(\"\\n\")}`;\n\t\tif (remaining > 0) {\n\t\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t\t}\n\t}\n\n\tconst entryLimit = result.details?.entryLimitReached;\n\tconst truncation = result.details?.truncation;\n\tif (entryLimit || truncation?.truncated) {\n\t\tconst warnings: string[] = [];\n\t\tif (entryLimit) warnings.push(`${entryLimit} entries limit`);\n\t\tif (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);\n\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${warnings.join(\", \")}]`)}`;\n\t}\n\treturn text;\n}\n\nexport function createLsToolDefinition(\n\toperations: ToolOperations,\n\t_options?: LsToolOptions,\n): ToolDefinition<typeof lsSchema, LsToolDetails | undefined> {\n\tconst ops = operations;\n\tconst cwd = operations.cwd;\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 ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,\n\t\tpromptSnippet: \"List directory contents\",\n\t\tparameters: lsSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ path, limit }: { path?: string; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\t_ctx?,\n\t\t) {\n\t\t\treturn new Promise((resolve, reject) => {\n\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tconst onAbort = () => reject(new Error(\"Operation aborted\"));\n\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t(async () => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst dirPath = resolveToCwd(path || \".\", cwd);\n\t\t\t\t\t\tconst effectiveLimit = limit ?? DEFAULT_LIMIT;\n\n\t\t\t\t\t\t// Check if path exists.\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tawait ops.access(dirPath, \"exists\");\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\treject(new Error(`Path not found: ${dirPath}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Check if path is a directory.\n\t\t\t\t\t\tconst stat = await ops.stat(dirPath);\n\t\t\t\t\t\tif (!stat.isDirectory()) {\n\t\t\t\t\t\t\treject(new Error(`Not a directory: ${dirPath}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Read directory entries.\n\t\t\t\t\t\tlet entries: string[];\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tentries = await ops.readdir(dirPath);\n\t\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\t\treject(new Error(`Cannot read directory: ${e.message}`));\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Sort alphabetically, case-insensitive.\n\t\t\t\t\t\tentries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));\n\n\t\t\t\t\t\t// Format entries with directory indicators.\n\t\t\t\t\t\tconst results: string[] = [];\n\t\t\t\t\t\tlet entryLimitReached = false;\n\t\t\t\t\t\tfor (const entry of entries) {\n\t\t\t\t\t\t\tif (results.length >= effectiveLimit) {\n\t\t\t\t\t\t\t\tentryLimitReached = true;\n\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst fullPath = nodePath.join(dirPath, entry);\n\t\t\t\t\t\t\tlet suffix = \"\";\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tconst entryStat = await ops.stat(fullPath);\n\t\t\t\t\t\t\t\tif (entryStat.isDirectory()) suffix = \"/\";\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Skip entries we cannot stat.\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tresults.push(entry + suffix);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\n\t\t\t\t\t\tif (results.length === 0) {\n\t\t\t\t\t\t\tresolve({ content: [{ type: \"text\", text: \"(empty directory)\" }], details: undefined });\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst rawOutput = results.join(\"\\n\");\n\t\t\t\t\t\t// Apply byte truncation. There is no separate line limit because entry count is already capped.\n\t\t\t\t\t\tconst truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });\n\t\t\t\t\t\tlet output = truncation.content;\n\t\t\t\t\t\tconst details: LsToolDetails = {};\n\t\t\t\t\t\t// Build actionable notices for truncation and entry limits.\n\t\t\t\t\t\tconst notices: string[] = [];\n\t\t\t\t\t\tif (entryLimitReached) {\n\t\t\t\t\t\t\tnotices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);\n\t\t\t\t\t\t\tdetails.entryLimitReached = effectiveLimit;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (truncation.truncated) {\n\t\t\t\t\t\t\tnotices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);\n\t\t\t\t\t\t\tdetails.truncation = truncation;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (notices.length > 0) {\n\t\t\t\t\t\t\toutput += `\\n\\n[${notices.join(\". \")}]`;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tresolve({\n\t\t\t\t\t\t\tcontent: [{ type: \"text\", text: output }],\n\t\t\t\t\t\t\tdetails: Object.keys(details).length > 0 ? details : undefined,\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (e: any) {\n\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\treject(e);\n\t\t\t\t\t}\n\t\t\t\t})();\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatBackendIcon(ops.getBackendInfo?.(), theme) + formatLsCall(args, theme));\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(formatLsResult(result as any, options, theme, context.showImages));\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createLsTool(operations: ToolOperations, options?: LsToolOptions): AgentTool<typeof lsSchema> {\n\treturn wrapToolDefinition(createLsToolDefinition(operations, options));\n}\n"]}
|
|
@@ -41,9 +41,15 @@ export type ToolBackendInfo = {
|
|
|
41
41
|
remote: string;
|
|
42
42
|
configured: true;
|
|
43
43
|
} | {
|
|
44
|
-
type: "
|
|
44
|
+
type: "remote";
|
|
45
45
|
cwd: string;
|
|
46
46
|
configured: false;
|
|
47
|
+
} | {
|
|
48
|
+
type: "remote";
|
|
49
|
+
cwd: string;
|
|
50
|
+
url: string;
|
|
51
|
+
protocol: "ws";
|
|
52
|
+
configured: true;
|
|
47
53
|
};
|
|
48
54
|
export interface ToolOperations {
|
|
49
55
|
cwd: string;
|
|
@@ -71,7 +77,7 @@ export interface SshToolOperationsOptions {
|
|
|
71
77
|
remote: string;
|
|
72
78
|
cwd: string;
|
|
73
79
|
}
|
|
74
|
-
export interface
|
|
80
|
+
export interface DeferredRemoteToolOperationsConfigureSshOptions {
|
|
75
81
|
remote: string;
|
|
76
82
|
cwd?: string;
|
|
77
83
|
}
|
|
@@ -117,12 +123,14 @@ export declare class SshToolOperations implements ToolOperations {
|
|
|
117
123
|
grep(options: ToolGrepOptions): Promise<ToolGrepResult>;
|
|
118
124
|
detectImageMimeType(path: string): Promise<string | null | undefined>;
|
|
119
125
|
getBackendInfo(): ToolBackendInfo;
|
|
126
|
+
dispose(): Promise<void>;
|
|
120
127
|
}
|
|
121
|
-
export declare class
|
|
128
|
+
export declare class DeferredRemoteToolOperations implements ToolOperations {
|
|
122
129
|
cwd: string;
|
|
123
130
|
private operations;
|
|
124
131
|
constructor(cwd: string);
|
|
125
|
-
configure(options:
|
|
132
|
+
configure(options: DeferredRemoteToolOperationsConfigureSshOptions): Promise<ToolBackendInfo>;
|
|
133
|
+
configureRemote(url: string): Promise<ToolBackendInfo>;
|
|
126
134
|
clear(): void;
|
|
127
135
|
private requireOperations;
|
|
128
136
|
exec(command: string, options: ToolExecOptions): Promise<{
|
|
@@ -141,5 +149,42 @@ export declare class DeferredSshToolOperations implements ToolOperations {
|
|
|
141
149
|
detectImageMimeType(path: string): Promise<string | null | undefined>;
|
|
142
150
|
getBackendInfo(): ToolBackendInfo;
|
|
143
151
|
}
|
|
152
|
+
export declare class RemoteToolOperations implements ToolOperations {
|
|
153
|
+
readonly url: string;
|
|
154
|
+
readonly protocol: "ws";
|
|
155
|
+
cwd: string;
|
|
156
|
+
private socket;
|
|
157
|
+
private nextId;
|
|
158
|
+
private pending;
|
|
159
|
+
private execPending;
|
|
160
|
+
private keepAliveInterval;
|
|
161
|
+
private lastPongAt;
|
|
162
|
+
private constructor();
|
|
163
|
+
static connect(url: string): Promise<RemoteToolOperations>;
|
|
164
|
+
private startKeepAlive;
|
|
165
|
+
private stopKeepAlive;
|
|
166
|
+
private rejectAll;
|
|
167
|
+
private send;
|
|
168
|
+
private request;
|
|
169
|
+
private handleMessage;
|
|
170
|
+
private handleExecEvent;
|
|
171
|
+
exec(command: string, options: ToolExecOptions): Promise<{
|
|
172
|
+
exitCode: number | null;
|
|
173
|
+
}>;
|
|
174
|
+
access(path: string, mode?: ToolAccessMode): Promise<void>;
|
|
175
|
+
readFile(path: string): Promise<Buffer>;
|
|
176
|
+
writeFile(path: string, content: string | Buffer): Promise<void>;
|
|
177
|
+
mkdir(path: string, options?: {
|
|
178
|
+
recursive?: boolean;
|
|
179
|
+
}): Promise<void>;
|
|
180
|
+
stat(path: string): Promise<ToolFileStat>;
|
|
181
|
+
readdir(path: string): Promise<string[]>;
|
|
182
|
+
glob(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]>;
|
|
183
|
+
grep(options: ToolGrepOptions): Promise<ToolGrepResult>;
|
|
184
|
+
detectImageMimeType(path: string): Promise<string | null | undefined>;
|
|
185
|
+
getBackendInfo(): ToolBackendInfo;
|
|
186
|
+
dispose(): Promise<void>;
|
|
187
|
+
}
|
|
188
|
+
export declare function createRemoteToolOperations(url: string): Promise<RemoteToolOperations>;
|
|
144
189
|
export declare function createSshToolOperations(target: string): Promise<SshToolOperations>;
|
|
145
190
|
//# sourceMappingURL=operations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../../../src/core/tools/operations.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport type { Stats } from \"node:fs\";\nimport { constants } from \"node:fs\";\nimport {\n\taccess as fsAccess,\n\tmkdir as fsMkdir,\n\treaddir as fsReaddir,\n\treadFile as fsReadFile,\n\tstat as fsStat,\n\twriteFile as fsWriteFile,\n} from \"node:fs/promises\";\nimport { waitForChildProcess } from \"../../utils/child-process.ts\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.ts\";\nimport {\n\tgetShellConfig,\n\tgetShellEnv,\n\tkillProcessTree,\n\ttrackDetachedChildPid,\n\tuntrackDetachedChildPid,\n} from \"../../utils/shell.ts\";\n\nexport type ToolAccessMode = \"exists\" | \"read\" | \"write\" | \"readwrite\";\n\nexport interface ToolFileStat {\n\tisDirectory: () => boolean;\n\tisFile: () => boolean;\n}\n\nexport interface ToolExecOptions {\n\tcwd?: string;\n\tonData: (data: Buffer) => void;\n\tsignal?: AbortSignal;\n\ttimeout?: number;\n\tenv?: NodeJS.ProcessEnv;\n}\n\nexport interface ToolGlobOptions {\n\tignore: string[];\n\tlimit: number;\n}\n\nexport interface ToolGrepOptions {\n\tpattern: string;\n\tpath: string;\n\tglob?: string;\n\tignoreCase?: boolean;\n\tliteral?: boolean;\n\tlimit: number;\n}\n\nexport interface ToolGrepMatch {\n\tfilePath: string;\n\tlineNumber: number;\n\tlineText?: string;\n}\n\nexport interface ToolGrepResult {\n\tisDirectory: boolean;\n\tmatches: ToolGrepMatch[];\n}\n\nexport type ToolBackendInfo =\n\t| { type: \"local\"; cwd: string }\n\t| { type: \"ssh\"; cwd: string; remote: string; configured: true }\n\t| { type: \"ssh\"; cwd: string; configured: false };\n\nexport interface ToolOperations {\n\tcwd: string;\n\texec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }>;\n\taccess(path: string, mode?: ToolAccessMode): Promise<void>;\n\treadFile(path: string): Promise<Buffer>;\n\twriteFile(path: string, content: string | Buffer): Promise<void>;\n\tmkdir(path: string, options?: { recursive?: boolean }): Promise<void>;\n\tstat(path: string): Promise<ToolFileStat>;\n\treaddir(path: string): Promise<string[]>;\n\tglob?(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]>;\n\tgrep?(options: ToolGrepOptions): Promise<ToolGrepResult>;\n\tdetectImageMimeType?(path: string): Promise<string | null | undefined>;\n\tgetBackendInfo?(): ToolBackendInfo;\n\tdispose?(): Promise<void>;\n}\n\nexport interface LocalToolOperationsOptions {\n\tshellPath?: string;\n}\n\nexport interface SshToolOperationsOptions {\n\tremote: string;\n\tcwd: string;\n}\n\nexport interface DeferredSshToolOperationsConfigureOptions {\n\tremote: string;\n\tcwd?: string;\n}\n\nexport interface ParsedSshTarget {\n\tremote: string;\n\tcwd?: string;\n}\n\nfunction accessModeToFsMode(mode: ToolAccessMode | undefined): number {\n\tswitch (mode) {\n\t\tcase \"read\":\n\t\t\treturn constants.R_OK;\n\t\tcase \"write\":\n\t\t\treturn constants.W_OK;\n\t\tcase \"readwrite\":\n\t\t\treturn constants.R_OK | constants.W_OK;\n\t\tcase \"exists\":\n\t\tcase undefined:\n\t\t\treturn constants.F_OK;\n\t}\n}\n\nfunction shellQuote(value: string): string {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\nfunction parseSshTarget(value: string): ParsedSshTarget {\n\tconst separatorIndex = value.indexOf(\":\");\n\tif (separatorIndex === -1) {\n\t\treturn { remote: value };\n\t}\n\tconst remote = value.slice(0, separatorIndex);\n\tconst cwd = value.slice(separatorIndex + 1);\n\treturn cwd ? { remote, cwd } : { remote };\n}\n\nfunction validateSshRemote(remote: string): void {\n\tif (!remote) {\n\t\tthrow new Error(\"--ssh requires a remote target like user@host or user@host:/path\");\n\t}\n\tif (remote.startsWith(\"-\")) {\n\t\tthrow new Error(\"--ssh remote target must not start with '-'\");\n\t}\n}\n\nfunction sshArgs(remote: string, command: string): string[] {\n\tvalidateSshRemote(remote);\n\treturn [\"--\", remote, command];\n}\n\nfunction buildFdArgs(pattern: string, searchPath: string, limit: number): string[] {\n\tconst args: string[] = [\"--glob\", \"--color=never\", \"--hidden\", \"--no-require-git\", \"--max-results\", String(limit)];\n\tlet effectivePattern = pattern;\n\tif (pattern.includes(\"/\")) {\n\t\targs.push(\"--full-path\");\n\t\tif (!pattern.startsWith(\"/\") && !pattern.startsWith(\"**/\") && pattern !== \"**\") {\n\t\t\teffectivePattern = `**/${pattern}`;\n\t\t}\n\t}\n\targs.push(\"--\", effectivePattern, searchPath);\n\treturn args;\n}\n\nfunction buildRgArgs(options: ToolGrepOptions): string[] {\n\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\tif (options.ignoreCase) args.push(\"--ignore-case\");\n\tif (options.literal) args.push(\"--fixed-strings\");\n\tif (options.glob) args.push(\"--glob\", options.glob);\n\targs.push(\"--\", options.pattern, options.path);\n\treturn args;\n}\n\nfunction commandWithArgs(command: string, args: string[]): string {\n\treturn [command, ...args.map(shellQuote)].join(\" \");\n}\n\nasync function runSshBuffer(\n\tremote: string,\n\tcommand: string,\n\toptions: { input?: Buffer | string; signal?: AbortSignal; timeout?: number } = {},\n): Promise<Buffer> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(\"ssh\", sshArgs(remote, command), { stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n\t\tconst stdout: Buffer[] = [];\n\t\tconst stderr: Buffer[] = [];\n\t\tlet timedOut = false;\n\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\ttimedOut = true;\n\t\t\t\tchild.kill();\n\t\t\t}, options.timeout * 1000);\n\t\t}\n\t\tchild.stdout.on(\"data\", (data: Buffer) => stdout.push(data));\n\t\tchild.stderr.on(\"data\", (data: Buffer) => stderr.push(data));\n\t\tchild.on(\"error\", reject);\n\t\tconst onAbort = () => child.kill();\n\t\toptions.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\tif (options.input !== undefined) {\n\t\t\tchild.stdin.end(options.input);\n\t\t} else {\n\t\t\tchild.stdin.end();\n\t\t}\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\toptions.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\tif (options.signal?.aborted) {\n\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (timedOut) {\n\t\t\t\treject(new Error(`timeout:${options.timeout}`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (code !== 0) {\n\t\t\t\treject(new Error(Buffer.concat(stderr).toString(\"utf-8\").trim() || `ssh exited with code ${code}`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve(Buffer.concat(stdout));\n\t\t});\n\t});\n}\n\nexport class LocalToolOperations implements ToolOperations {\n\tcwd: string;\n\tprivate shellPath: string | undefined;\n\n\tconstructor(cwd: string, options: LocalToolOperationsOptions = {}) {\n\t\tthis.cwd = cwd;\n\t\tthis.shellPath = options.shellPath;\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\tconst cwd = options.cwd ?? this.cwd;\n\t\tconst { shell, args } = getShellConfig(this.shellPath);\n\t\ttry {\n\t\t\tawait fsAccess(cwd, constants.F_OK);\n\t\t} catch {\n\t\t\tthrow new Error(`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`);\n\t\t}\n\t\tif (options.signal?.aborted) {\n\t\t\tthrow new Error(\"aborted\");\n\t\t}\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\tcwd,\n\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\tenv: options.env ?? getShellEnv(),\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\t\t\tlet timedOut = false;\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t}, options.timeout * 1000);\n\t\t\t}\n\t\t\tchild.stdout?.on(\"data\", options.onData);\n\t\t\tchild.stderr?.on(\"data\", options.onData);\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t};\n\t\t\tif (options.signal) {\n\t\t\t\tif (options.signal.aborted) onAbort();\n\t\t\t\telse options.signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\t\t\twaitForChildProcess(child)\n\t\t\t\t.then((code) => {\n\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\tif (options.signal) options.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tif (options.signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (timedOut) {\n\t\t\t\t\t\treject(new Error(`timeout:${options.timeout}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tresolve({ exitCode: code });\n\t\t\t\t})\n\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\tif (options.signal) options.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\treject(error);\n\t\t\t\t});\n\t\t});\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tawait fsAccess(path, accessModeToFsMode(mode));\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\treturn fsReadFile(path);\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait fsWriteFile(path, content, typeof content === \"string\" ? \"utf-8\" : undefined);\n\t}\n\n\tasync mkdir(path: string, options: { recursive?: boolean } = {}): Promise<void> {\n\t\tawait fsMkdir(path, { recursive: options.recursive ?? false });\n\t}\n\n\tasync stat(path: string): Promise<Stats> {\n\t\treturn fsStat(path);\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\treturn fsReaddir(path);\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\treturn detectSupportedImageMimeTypeFromFile(path);\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn { type: \"local\", cwd: this.cwd };\n\t}\n}\n\nexport class SshToolOperations implements ToolOperations {\n\treadonly remote: string;\n\tcwd: string;\n\n\tconstructor(options: SshToolOperationsOptions) {\n\t\tthis.remote = options.remote;\n\t\tthis.cwd = options.cwd;\n\t}\n\n\tstatic async fromTarget(target: string): Promise<SshToolOperations> {\n\t\tconst parsed = parseSshTarget(target);\n\t\tvalidateSshRemote(parsed.remote);\n\t\tconst cwd = parsed.cwd ?? (await runSshBuffer(parsed.remote, \"pwd\")).toString(\"utf-8\").trim();\n\t\treturn new SshToolOperations({ remote: parsed.remote, cwd });\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\tconst cwd = options.cwd ?? this.cwd;\n\t\tconst remoteCommand = `cd ${shellQuote(cwd)} && bash -s`;\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst child = spawn(\"ssh\", sshArgs(this.remote, remoteCommand), {\n\t\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\t});\n\t\t\tlet timedOut = false;\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tchild.kill();\n\t\t\t\t}, options.timeout * 1000);\n\t\t\t}\n\t\t\tchild.stdout?.on(\"data\", options.onData);\n\t\t\tchild.stderr?.on(\"data\", options.onData);\n\t\t\tchild.on(\"error\", reject);\n\t\t\tchild.stdin.end(command);\n\t\t\tconst onAbort = () => child.kill();\n\t\t\toptions.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\toptions.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\tif (options.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\tif (timedOut) {\n\t\t\t\t\treject(new Error(`timeout:${options.timeout}`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tresolve({ exitCode: code });\n\t\t\t});\n\t\t});\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tconst remotePath = shellQuote(path);\n\t\tif (mode === \"readwrite\") {\n\t\t\tawait runSshBuffer(this.remote, `test -r ${remotePath} && test -w ${remotePath}`);\n\t\t\treturn;\n\t\t}\n\t\tconst flag = mode === \"read\" ? \"-r\" : mode === \"write\" ? \"-w\" : \"-e\";\n\t\tawait runSshBuffer(this.remote, `test ${flag} ${remotePath}`);\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\treturn runSshBuffer(this.remote, `cat ${shellQuote(path)}`);\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait runSshBuffer(this.remote, `base64 -d > ${shellQuote(path)}`, {\n\t\t\tinput: Buffer.from(content).toString(\"base64\"),\n\t\t});\n\t}\n\n\tasync mkdir(path: string, options: { recursive?: boolean } = {}): Promise<void> {\n\t\tconst flag = options.recursive ? \"-p \" : \"\";\n\t\tawait runSshBuffer(this.remote, `mkdir ${flag}${shellQuote(path)}`);\n\t}\n\n\tasync stat(path: string): Promise<ToolFileStat> {\n\t\tconst output = await runSshBuffer(\n\t\t\tthis.remote,\n\t\t\t`if test -d ${shellQuote(path)}; then echo d; elif test -f ${shellQuote(path)}; then echo f; else test -e ${shellQuote(path)} && echo o || exit 1; fi`,\n\t\t);\n\t\tconst kind = output.toString(\"utf-8\").trim();\n\t\treturn {\n\t\t\tisDirectory: () => kind === \"d\",\n\t\t\tisFile: () => kind === \"f\",\n\t\t};\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\tconst output = await runSshBuffer(\n\t\t\tthis.remote,\n\t\t\t`find ${shellQuote(path)} -maxdepth 1 -mindepth 1 -printf '%f\\\\n'`,\n\t\t);\n\t\treturn output.toString(\"utf-8\").split(\"\\n\").filter(Boolean);\n\t}\n\n\tasync glob(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]> {\n\t\tconst command = commandWithArgs(\"fd\", buildFdArgs(pattern, cwd, options.limit));\n\t\tconst output = await runSshBuffer(this.remote, command);\n\t\treturn output.toString(\"utf-8\").split(\"\\n\").filter(Boolean);\n\t}\n\n\tasync grep(options: ToolGrepOptions): Promise<ToolGrepResult> {\n\t\tconst isDirectory = (await this.stat(options.path)).isDirectory();\n\t\tconst command = commandWithArgs(\"rg\", buildRgArgs(options));\n\t\tconst output = await runSshBuffer(this.remote, command).catch((error: unknown) => {\n\t\t\tif (error instanceof Error && error.message.includes(\"ssh exited with code 1\")) {\n\t\t\t\treturn Buffer.alloc(0);\n\t\t\t}\n\t\t\tthrow error;\n\t\t});\n\t\tconst matches: ToolGrepMatch[] = [];\n\t\tfor (const line of output.toString(\"utf-8\").split(\"\\n\")) {\n\t\t\tif (!line.trim() || matches.length >= options.limit) continue;\n\t\t\tlet event: unknown;\n\t\t\ttry {\n\t\t\t\tevent = JSON.parse(line);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!event || typeof event !== \"object\" || !(\"type\" in event) || event.type !== \"match\") continue;\n\t\t\tconst data = \"data\" in event && event.data && typeof event.data === \"object\" ? event.data : undefined;\n\t\t\tconst filePath =\n\t\t\t\tdata && \"path\" in data && data.path && typeof data.path === \"object\" && \"text\" in data.path\n\t\t\t\t\t? data.path.text\n\t\t\t\t\t: undefined;\n\t\t\tconst lineNumber = data && \"line_number\" in data ? data.line_number : undefined;\n\t\t\tconst lineText =\n\t\t\t\tdata && \"lines\" in data && data.lines && typeof data.lines === \"object\" && \"text\" in data.lines\n\t\t\t\t\t? data.lines.text\n\t\t\t\t\t: undefined;\n\t\t\tif (typeof filePath === \"string\" && typeof lineNumber === \"number\") {\n\t\t\t\tmatches.push({ filePath, lineNumber, lineText: typeof lineText === \"string\" ? lineText : undefined });\n\t\t\t}\n\t\t}\n\t\treturn { isDirectory, matches };\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\ttry {\n\t\t\tconst output = await runSshBuffer(this.remote, `file --mime-type -b ${shellQuote(path)}`);\n\t\t\tconst mimeType = output.toString(\"utf-8\").trim();\n\t\t\treturn [\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\"].includes(mimeType) ? mimeType : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn { type: \"ssh\", remote: this.remote, cwd: this.cwd, configured: true };\n\t}\n}\n\nexport class DeferredSshToolOperations implements ToolOperations {\n\tcwd: string;\n\tprivate operations: SshToolOperations | undefined;\n\n\tconstructor(cwd: string) {\n\t\tthis.cwd = cwd;\n\t}\n\n\tasync configure(options: DeferredSshToolOperationsConfigureOptions): Promise<ToolBackendInfo> {\n\t\tconst next = new SshToolOperations({ remote: options.remote, cwd: options.cwd ?? this.cwd });\n\t\tconst stat = await next.stat(next.cwd);\n\t\tif (!stat.isDirectory()) {\n\t\t\tthrow new Error(`SSH sandbox cwd is not a directory: ${next.cwd}`);\n\t\t}\n\t\tthis.cwd = next.cwd;\n\t\tthis.operations = next;\n\t\treturn this.getBackendInfo();\n\t}\n\n\tclear(): void {\n\t\tthis.operations = undefined;\n\t}\n\n\tprivate requireOperations(): SshToolOperations {\n\t\tif (!this.operations) {\n\t\t\tthrow new Error(\n\t\t\t\t\"SSH sandbox is not configured. Configure it over RPC or with /ssh-sandbox before using tools.\",\n\t\t\t);\n\t\t}\n\t\treturn this.operations;\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\treturn this.requireOperations().exec(command, options);\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tawait this.requireOperations().access(path, mode);\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\treturn this.requireOperations().readFile(path);\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait this.requireOperations().writeFile(path, content);\n\t}\n\n\tasync mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {\n\t\tawait this.requireOperations().mkdir(path, options);\n\t}\n\n\tasync stat(path: string): Promise<ToolFileStat> {\n\t\treturn this.requireOperations().stat(path);\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\treturn this.requireOperations().readdir(path);\n\t}\n\n\tasync glob(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]> {\n\t\treturn this.requireOperations().glob(pattern, cwd, options);\n\t}\n\n\tasync grep(options: ToolGrepOptions): Promise<ToolGrepResult> {\n\t\treturn this.requireOperations().grep(options);\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\treturn this.requireOperations().detectImageMimeType(path);\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn this.operations?.getBackendInfo() ?? { type: \"ssh\", cwd: this.cwd, configured: false };\n\t}\n}\n\nexport function createSshToolOperations(target: string): Promise<SshToolOperations> {\n\treturn SshToolOperations.fromTarget(target);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../../../src/core/tools/operations.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport type { Stats } from \"node:fs\";\nimport { constants } from \"node:fs\";\nimport {\n\taccess as fsAccess,\n\tmkdir as fsMkdir,\n\treaddir as fsReaddir,\n\treadFile as fsReadFile,\n\tstat as fsStat,\n\twriteFile as fsWriteFile,\n} from \"node:fs/promises\";\nimport { waitForChildProcess } from \"../../utils/child-process.ts\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.ts\";\nimport {\n\tgetShellConfig,\n\tgetShellEnv,\n\tkillProcessTree,\n\ttrackDetachedChildPid,\n\tuntrackDetachedChildPid,\n} from \"../../utils/shell.ts\";\n\nexport type ToolAccessMode = \"exists\" | \"read\" | \"write\" | \"readwrite\";\n\nexport interface ToolFileStat {\n\tisDirectory: () => boolean;\n\tisFile: () => boolean;\n}\n\nexport interface ToolExecOptions {\n\tcwd?: string;\n\tonData: (data: Buffer) => void;\n\tsignal?: AbortSignal;\n\ttimeout?: number;\n\tenv?: NodeJS.ProcessEnv;\n}\n\nexport interface ToolGlobOptions {\n\tignore: string[];\n\tlimit: number;\n}\n\nexport interface ToolGrepOptions {\n\tpattern: string;\n\tpath: string;\n\tglob?: string;\n\tignoreCase?: boolean;\n\tliteral?: boolean;\n\tlimit: number;\n}\n\nexport interface ToolGrepMatch {\n\tfilePath: string;\n\tlineNumber: number;\n\tlineText?: string;\n}\n\nexport interface ToolGrepResult {\n\tisDirectory: boolean;\n\tmatches: ToolGrepMatch[];\n}\n\nexport type ToolBackendInfo =\n\t| { type: \"local\"; cwd: string }\n\t| { type: \"ssh\"; cwd: string; remote: string; configured: true }\n\t| { type: \"remote\"; cwd: string; configured: false }\n\t| { type: \"remote\"; cwd: string; url: string; protocol: \"ws\"; configured: true };\n\nexport interface ToolOperations {\n\tcwd: string;\n\texec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }>;\n\taccess(path: string, mode?: ToolAccessMode): Promise<void>;\n\treadFile(path: string): Promise<Buffer>;\n\twriteFile(path: string, content: string | Buffer): Promise<void>;\n\tmkdir(path: string, options?: { recursive?: boolean }): Promise<void>;\n\tstat(path: string): Promise<ToolFileStat>;\n\treaddir(path: string): Promise<string[]>;\n\tglob?(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]>;\n\tgrep?(options: ToolGrepOptions): Promise<ToolGrepResult>;\n\tdetectImageMimeType?(path: string): Promise<string | null | undefined>;\n\tgetBackendInfo?(): ToolBackendInfo;\n\tdispose?(): Promise<void>;\n}\n\nexport interface LocalToolOperationsOptions {\n\tshellPath?: string;\n}\n\nexport interface SshToolOperationsOptions {\n\tremote: string;\n\tcwd: string;\n}\n\nexport interface DeferredRemoteToolOperationsConfigureSshOptions {\n\tremote: string;\n\tcwd?: string;\n}\n\nexport interface ParsedSshTarget {\n\tremote: string;\n\tcwd?: string;\n}\n\nfunction accessModeToFsMode(mode: ToolAccessMode | undefined): number {\n\tswitch (mode) {\n\t\tcase \"read\":\n\t\t\treturn constants.R_OK;\n\t\tcase \"write\":\n\t\t\treturn constants.W_OK;\n\t\tcase \"readwrite\":\n\t\t\treturn constants.R_OK | constants.W_OK;\n\t\tcase \"exists\":\n\t\tcase undefined:\n\t\t\treturn constants.F_OK;\n\t}\n}\n\nfunction shellQuote(value: string): string {\n\treturn `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\nfunction parseSshTarget(value: string): ParsedSshTarget {\n\tconst separatorIndex = value.indexOf(\":\");\n\tif (separatorIndex === -1) {\n\t\treturn { remote: value };\n\t}\n\tconst remote = value.slice(0, separatorIndex);\n\tconst cwd = value.slice(separatorIndex + 1);\n\treturn cwd ? { remote, cwd } : { remote };\n}\n\nfunction validateSshRemote(remote: string): void {\n\tif (!remote) {\n\t\tthrow new Error(\"--ssh requires a remote target like user@host or user@host:/path\");\n\t}\n\tif (remote.startsWith(\"-\")) {\n\t\tthrow new Error(\"--ssh remote target must not start with '-'\");\n\t}\n}\n\nfunction sshArgs(remote: string, command: string): string[] {\n\tvalidateSshRemote(remote);\n\treturn [\"--\", remote, command];\n}\n\nfunction buildFdArgs(pattern: string, searchPath: string, limit: number): string[] {\n\tconst args: string[] = [\"--glob\", \"--color=never\", \"--hidden\", \"--no-require-git\", \"--max-results\", String(limit)];\n\tlet effectivePattern = pattern;\n\tif (pattern.includes(\"/\")) {\n\t\targs.push(\"--full-path\");\n\t\tif (!pattern.startsWith(\"/\") && !pattern.startsWith(\"**/\") && pattern !== \"**\") {\n\t\t\teffectivePattern = `**/${pattern}`;\n\t\t}\n\t}\n\targs.push(\"--\", effectivePattern, searchPath);\n\treturn args;\n}\n\nfunction buildRgArgs(options: ToolGrepOptions): string[] {\n\tconst args: string[] = [\"--json\", \"--line-number\", \"--color=never\", \"--hidden\"];\n\tif (options.ignoreCase) args.push(\"--ignore-case\");\n\tif (options.literal) args.push(\"--fixed-strings\");\n\tif (options.glob) args.push(\"--glob\", options.glob);\n\targs.push(\"--\", options.pattern, options.path);\n\treturn args;\n}\n\nfunction commandWithArgs(command: string, args: string[]): string {\n\treturn [command, ...args.map(shellQuote)].join(\" \");\n}\n\nasync function runSshBuffer(\n\tremote: string,\n\tcommand: string,\n\toptions: { input?: Buffer | string; signal?: AbortSignal; timeout?: number } = {},\n): Promise<Buffer> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst child = spawn(\"ssh\", sshArgs(remote, command), { stdio: [\"pipe\", \"pipe\", \"pipe\"] });\n\t\tconst stdout: Buffer[] = [];\n\t\tconst stderr: Buffer[] = [];\n\t\tlet timedOut = false;\n\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\ttimedOut = true;\n\t\t\t\tchild.kill();\n\t\t\t}, options.timeout * 1000);\n\t\t}\n\t\tchild.stdout.on(\"data\", (data: Buffer) => stdout.push(data));\n\t\tchild.stderr.on(\"data\", (data: Buffer) => stderr.push(data));\n\t\tchild.on(\"error\", reject);\n\t\tconst onAbort = () => child.kill();\n\t\toptions.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\tif (options.input !== undefined) {\n\t\t\tchild.stdin.end(options.input);\n\t\t} else {\n\t\t\tchild.stdin.end();\n\t\t}\n\t\tchild.on(\"close\", (code) => {\n\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\toptions.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\tif (options.signal?.aborted) {\n\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (timedOut) {\n\t\t\t\treject(new Error(`timeout:${options.timeout}`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (code !== 0) {\n\t\t\t\treject(new Error(Buffer.concat(stderr).toString(\"utf-8\").trim() || `ssh exited with code ${code}`));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tresolve(Buffer.concat(stdout));\n\t\t});\n\t});\n}\n\nexport class LocalToolOperations implements ToolOperations {\n\tcwd: string;\n\tprivate shellPath: string | undefined;\n\n\tconstructor(cwd: string, options: LocalToolOperationsOptions = {}) {\n\t\tthis.cwd = cwd;\n\t\tthis.shellPath = options.shellPath;\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\tconst cwd = options.cwd ?? this.cwd;\n\t\tconst { shell, args } = getShellConfig(this.shellPath);\n\t\ttry {\n\t\t\tawait fsAccess(cwd, constants.F_OK);\n\t\t} catch {\n\t\t\tthrow new Error(`Working directory does not exist: ${cwd}\\nCannot execute bash commands.`);\n\t\t}\n\t\tif (options.signal?.aborted) {\n\t\t\tthrow new Error(\"aborted\");\n\t\t}\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst child = spawn(shell, [...args, command], {\n\t\t\t\tcwd,\n\t\t\t\tdetached: process.platform !== \"win32\",\n\t\t\t\tenv: options.env ?? getShellEnv(),\n\t\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\t\twindowsHide: true,\n\t\t\t});\n\t\t\tif (child.pid) trackDetachedChildPid(child.pid);\n\t\t\tlet timedOut = false;\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t\t}, options.timeout * 1000);\n\t\t\t}\n\t\t\tchild.stdout?.on(\"data\", options.onData);\n\t\t\tchild.stderr?.on(\"data\", options.onData);\n\t\t\tconst onAbort = () => {\n\t\t\t\tif (child.pid) killProcessTree(child.pid);\n\t\t\t};\n\t\t\tif (options.signal) {\n\t\t\t\tif (options.signal.aborted) onAbort();\n\t\t\t\telse options.signal.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\t}\n\t\t\twaitForChildProcess(child)\n\t\t\t\t.then((code) => {\n\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\tif (options.signal) options.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\tif (options.signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tif (timedOut) {\n\t\t\t\t\t\treject(new Error(`timeout:${options.timeout}`));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tresolve({ exitCode: code });\n\t\t\t\t})\n\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\tif (child.pid) untrackDetachedChildPid(child.pid);\n\t\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\t\tif (options.signal) options.signal.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\treject(error);\n\t\t\t\t});\n\t\t});\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tawait fsAccess(path, accessModeToFsMode(mode));\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\treturn fsReadFile(path);\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait fsWriteFile(path, content, typeof content === \"string\" ? \"utf-8\" : undefined);\n\t}\n\n\tasync mkdir(path: string, options: { recursive?: boolean } = {}): Promise<void> {\n\t\tawait fsMkdir(path, { recursive: options.recursive ?? false });\n\t}\n\n\tasync stat(path: string): Promise<Stats> {\n\t\treturn fsStat(path);\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\treturn fsReaddir(path);\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\treturn detectSupportedImageMimeTypeFromFile(path);\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn { type: \"local\", cwd: this.cwd };\n\t}\n}\n\nexport class SshToolOperations implements ToolOperations {\n\treadonly remote: string;\n\tcwd: string;\n\n\tconstructor(options: SshToolOperationsOptions) {\n\t\tthis.remote = options.remote;\n\t\tthis.cwd = options.cwd;\n\t}\n\n\tstatic async fromTarget(target: string): Promise<SshToolOperations> {\n\t\tconst parsed = parseSshTarget(target);\n\t\tvalidateSshRemote(parsed.remote);\n\t\tconst cwd = parsed.cwd ?? (await runSshBuffer(parsed.remote, \"pwd\")).toString(\"utf-8\").trim();\n\t\treturn new SshToolOperations({ remote: parsed.remote, cwd });\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\tconst cwd = options.cwd ?? this.cwd;\n\t\tconst remoteCommand = `cd ${shellQuote(cwd)} && bash -s`;\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst child = spawn(\"ssh\", sshArgs(this.remote, remoteCommand), {\n\t\t\t\tstdio: [\"pipe\", \"pipe\", \"pipe\"],\n\t\t\t});\n\t\t\tlet timedOut = false;\n\t\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttimedOut = true;\n\t\t\t\t\tchild.kill();\n\t\t\t\t}, options.timeout * 1000);\n\t\t\t}\n\t\t\tchild.stdout?.on(\"data\", options.onData);\n\t\t\tchild.stderr?.on(\"data\", options.onData);\n\t\t\tchild.on(\"error\", reject);\n\t\t\tchild.stdin.end(command);\n\t\t\tconst onAbort = () => child.kill();\n\t\t\toptions.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\tchild.on(\"close\", (code) => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\toptions.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\tif (options.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\tif (timedOut) {\n\t\t\t\t\treject(new Error(`timeout:${options.timeout}`));\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tresolve({ exitCode: code });\n\t\t\t});\n\t\t});\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tconst remotePath = shellQuote(path);\n\t\tif (mode === \"readwrite\") {\n\t\t\tawait runSshBuffer(this.remote, `test -r ${remotePath} && test -w ${remotePath}`);\n\t\t\treturn;\n\t\t}\n\t\tconst flag = mode === \"read\" ? \"-r\" : mode === \"write\" ? \"-w\" : \"-e\";\n\t\tawait runSshBuffer(this.remote, `test ${flag} ${remotePath}`);\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\treturn runSshBuffer(this.remote, `cat ${shellQuote(path)}`);\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait runSshBuffer(this.remote, `base64 -d > ${shellQuote(path)}`, {\n\t\t\tinput: Buffer.from(content).toString(\"base64\"),\n\t\t});\n\t}\n\n\tasync mkdir(path: string, options: { recursive?: boolean } = {}): Promise<void> {\n\t\tconst flag = options.recursive ? \"-p \" : \"\";\n\t\tawait runSshBuffer(this.remote, `mkdir ${flag}${shellQuote(path)}`);\n\t}\n\n\tasync stat(path: string): Promise<ToolFileStat> {\n\t\tconst output = await runSshBuffer(\n\t\t\tthis.remote,\n\t\t\t`if test -d ${shellQuote(path)}; then echo d; elif test -f ${shellQuote(path)}; then echo f; else test -e ${shellQuote(path)} && echo o || exit 1; fi`,\n\t\t);\n\t\tconst kind = output.toString(\"utf-8\").trim();\n\t\treturn {\n\t\t\tisDirectory: () => kind === \"d\",\n\t\t\tisFile: () => kind === \"f\",\n\t\t};\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\tconst output = await runSshBuffer(\n\t\t\tthis.remote,\n\t\t\t`find ${shellQuote(path)} -maxdepth 1 -mindepth 1 -printf '%f\\\\n'`,\n\t\t);\n\t\treturn output.toString(\"utf-8\").split(\"\\n\").filter(Boolean);\n\t}\n\n\tasync glob(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]> {\n\t\tconst command = commandWithArgs(\"fd\", buildFdArgs(pattern, cwd, options.limit));\n\t\tconst output = await runSshBuffer(this.remote, command);\n\t\treturn output.toString(\"utf-8\").split(\"\\n\").filter(Boolean);\n\t}\n\n\tasync grep(options: ToolGrepOptions): Promise<ToolGrepResult> {\n\t\tconst isDirectory = (await this.stat(options.path)).isDirectory();\n\t\tconst command = commandWithArgs(\"rg\", buildRgArgs(options));\n\t\tconst output = await runSshBuffer(this.remote, command).catch((error: unknown) => {\n\t\t\tif (error instanceof Error && error.message.includes(\"ssh exited with code 1\")) {\n\t\t\t\treturn Buffer.alloc(0);\n\t\t\t}\n\t\t\tthrow error;\n\t\t});\n\t\tconst matches: ToolGrepMatch[] = [];\n\t\tfor (const line of output.toString(\"utf-8\").split(\"\\n\")) {\n\t\t\tif (!line.trim() || matches.length >= options.limit) continue;\n\t\t\tlet event: unknown;\n\t\t\ttry {\n\t\t\t\tevent = JSON.parse(line);\n\t\t\t} catch {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!event || typeof event !== \"object\" || !(\"type\" in event) || event.type !== \"match\") continue;\n\t\t\tconst data = \"data\" in event && event.data && typeof event.data === \"object\" ? event.data : undefined;\n\t\t\tconst filePath =\n\t\t\t\tdata && \"path\" in data && data.path && typeof data.path === \"object\" && \"text\" in data.path\n\t\t\t\t\t? data.path.text\n\t\t\t\t\t: undefined;\n\t\t\tconst lineNumber = data && \"line_number\" in data ? data.line_number : undefined;\n\t\t\tconst lineText =\n\t\t\t\tdata && \"lines\" in data && data.lines && typeof data.lines === \"object\" && \"text\" in data.lines\n\t\t\t\t\t? data.lines.text\n\t\t\t\t\t: undefined;\n\t\t\tif (typeof filePath === \"string\" && typeof lineNumber === \"number\") {\n\t\t\t\tmatches.push({ filePath, lineNumber, lineText: typeof lineText === \"string\" ? lineText : undefined });\n\t\t\t}\n\t\t}\n\t\treturn { isDirectory, matches };\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\ttry {\n\t\t\tconst output = await runSshBuffer(this.remote, `file --mime-type -b ${shellQuote(path)}`);\n\t\t\tconst mimeType = output.toString(\"utf-8\").trim();\n\t\t\treturn [\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\"].includes(mimeType) ? mimeType : null;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn { type: \"ssh\", remote: this.remote, cwd: this.cwd, configured: true };\n\t}\n\n\tasync dispose(): Promise<void> {}\n}\n\nexport class DeferredRemoteToolOperations implements ToolOperations {\n\tcwd: string;\n\tprivate operations: SshToolOperations | RemoteToolOperations | undefined;\n\n\tconstructor(cwd: string) {\n\t\tthis.cwd = cwd;\n\t}\n\n\tasync configure(options: DeferredRemoteToolOperationsConfigureSshOptions): Promise<ToolBackendInfo> {\n\t\tconst next = new SshToolOperations({ remote: options.remote, cwd: options.cwd ?? this.cwd });\n\t\tconst stat = await next.stat(next.cwd);\n\t\tif (!stat.isDirectory()) {\n\t\t\tthrow new Error(`SSH backend cwd is not a directory: ${next.cwd}`);\n\t\t}\n\t\tawait this.operations?.dispose?.();\n\t\tthis.cwd = next.cwd;\n\t\tthis.operations = next;\n\t\treturn this.getBackendInfo();\n\t}\n\n\tasync configureRemote(url: string): Promise<ToolBackendInfo> {\n\t\tconst next = await createRemoteToolOperations(url);\n\t\tconst stat = await next.stat(next.cwd);\n\t\tif (!stat.isDirectory()) {\n\t\t\tthrow new Error(`Remote daemon cwd is not a directory: ${next.cwd}`);\n\t\t}\n\t\tawait this.operations?.dispose?.();\n\t\tthis.cwd = next.cwd;\n\t\tthis.operations = next;\n\t\treturn this.getBackendInfo();\n\t}\n\n\tclear(): void {\n\t\tvoid this.operations?.dispose?.();\n\t\tthis.operations = undefined;\n\t}\n\n\tprivate requireOperations(): SshToolOperations | RemoteToolOperations {\n\t\tif (!this.operations) {\n\t\t\tthrow new Error(\"Remote backend is not configured. Configure it over RPC or with /remote before using tools.\");\n\t\t}\n\t\treturn this.operations;\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\treturn this.requireOperations().exec(command, options);\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tawait this.requireOperations().access(path, mode);\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\treturn this.requireOperations().readFile(path);\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait this.requireOperations().writeFile(path, content);\n\t}\n\n\tasync mkdir(path: string, options?: { recursive?: boolean }): Promise<void> {\n\t\tawait this.requireOperations().mkdir(path, options);\n\t}\n\n\tasync stat(path: string): Promise<ToolFileStat> {\n\t\treturn this.requireOperations().stat(path);\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\treturn this.requireOperations().readdir(path);\n\t}\n\n\tasync glob(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]> {\n\t\treturn this.requireOperations().glob(pattern, cwd, options);\n\t}\n\n\tasync grep(options: ToolGrepOptions): Promise<ToolGrepResult> {\n\t\treturn this.requireOperations().grep(options);\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\treturn this.requireOperations().detectImageMimeType(path);\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn this.operations?.getBackendInfo() ?? { type: \"remote\", cwd: this.cwd, configured: false };\n\t}\n}\n\ntype RemoteResponse = { id: string; result: unknown } | { id: string; error: { message?: unknown } | string };\n\ntype RemoteExecEvent =\n\t| { id: string; event: \"data\"; dataBase64?: unknown; data?: unknown; stream?: unknown }\n\t| { id: string; event: \"exit\"; exitCode?: unknown; cancelled?: unknown }\n\t| { id: string; event: \"error\"; error?: { message?: unknown } | string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction remoteErrorMessage(error: unknown): string {\n\tif (typeof error === \"string\") return error;\n\tif (isRecord(error) && typeof error.message === \"string\") return error.message;\n\treturn \"remote operation failed\";\n}\n\nfunction requireString(value: unknown, name: string): string {\n\tif (typeof value !== \"string\") throw new Error(`Remote response missing string ${name}`);\n\treturn value;\n}\n\nfunction optionalString(value: unknown): string | undefined {\n\treturn typeof value === \"string\" ? value : undefined;\n}\n\nfunction optionalNumber(value: unknown): number | undefined {\n\treturn typeof value === \"number\" ? value : undefined;\n}\n\nfunction normalizeRemoteUrl(url: string): { url: string; protocol: \"ws\" } {\n\tconst parsed = new URL(url);\n\tif (parsed.protocol !== \"ws:\" && parsed.protocol !== \"wss:\") {\n\t\tthrow new Error(`--remote currently supports ws:// and wss:// URLs, got ${parsed.protocol}`);\n\t}\n\treturn { url, protocol: \"ws\" };\n}\n\nexport class RemoteToolOperations implements ToolOperations {\n\treadonly url: string;\n\treadonly protocol: \"ws\";\n\tcwd: string;\n\tprivate socket: WebSocket;\n\tprivate nextId = 1;\n\tprivate pending = new Map<\n\t\tstring,\n\t\t{\n\t\t\tresolve: (value: unknown) => void;\n\t\t\treject: (error: Error) => void;\n\t\t}\n\t>();\n\tprivate execPending = new Map<\n\t\tstring,\n\t\t{\n\t\t\tonData: (data: Buffer) => void;\n\t\t\tresolve: (value: { exitCode: number | null }) => void;\n\t\t\treject: (error: Error) => void;\n\t\t}\n\t>();\n\tprivate keepAliveInterval: NodeJS.Timeout | undefined;\n\tprivate lastPongAt = Date.now();\n\n\tprivate constructor(url: string, protocol: \"ws\", socket: WebSocket, cwd: string) {\n\t\tthis.url = url;\n\t\tthis.protocol = protocol;\n\t\tthis.socket = socket;\n\t\tthis.cwd = cwd;\n\t\tthis.socket.addEventListener(\"message\", (event) => this.handleMessage(event.data));\n\t\tthis.socket.addEventListener(\"close\", () => {\n\t\t\tthis.stopKeepAlive();\n\t\t\tthis.rejectAll(new Error(\"remote connection closed\"));\n\t\t});\n\t\tthis.socket.addEventListener(\"error\", () => {\n\t\t\tthis.stopKeepAlive();\n\t\t\tthis.rejectAll(new Error(\"remote connection error\"));\n\t\t});\n\t\tthis.startKeepAlive();\n\t}\n\n\tstatic async connect(url: string): Promise<RemoteToolOperations> {\n\t\tconst normalized = normalizeRemoteUrl(url);\n\t\tconst socket = await new Promise<WebSocket>((resolveSocket, rejectSocket) => {\n\t\t\tconst ws = new WebSocket(normalized.url);\n\t\t\tconst cleanup = () => {\n\t\t\t\tws.removeEventListener(\"open\", onOpen);\n\t\t\t\tws.removeEventListener(\"error\", onError);\n\t\t\t};\n\t\t\tconst onOpen = () => {\n\t\t\t\tcleanup();\n\t\t\t\tresolveSocket(ws);\n\t\t\t};\n\t\t\tconst onError = () => {\n\t\t\t\tcleanup();\n\t\t\t\trejectSocket(new Error(`failed to connect remote commander: ${normalized.url}`));\n\t\t\t};\n\t\t\tws.addEventListener(\"open\", onOpen, { once: true });\n\t\t\tws.addEventListener(\"error\", onError, { once: true });\n\t\t});\n\t\tconst operations = new RemoteToolOperations(normalized.url, normalized.protocol, socket, \"/\");\n\t\tconst capabilities = await operations.request(\"capabilities\", {});\n\t\tif (isRecord(capabilities) && typeof capabilities.cwd === \"string\") {\n\t\t\toperations.cwd = capabilities.cwd;\n\t\t}\n\t\treturn operations;\n\t}\n\n\tprivate startKeepAlive(): void {\n\t\tthis.keepAliveInterval = setInterval(() => {\n\t\t\tif (Date.now() - this.lastPongAt > 90_000) {\n\t\t\t\tthis.stopKeepAlive();\n\t\t\t\tthis.rejectAll(new Error(\"remote connection heartbeat timed out\"));\n\t\t\t\tthis.socket.close();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.send({ type: \"ping\", timestamp: Date.now() });\n\t\t\t} catch (error) {\n\t\t\t\tthis.stopKeepAlive();\n\t\t\t\tthis.rejectAll(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t}, 30_000);\n\t\tthis.keepAliveInterval.unref?.();\n\t}\n\n\tprivate stopKeepAlive(): void {\n\t\tif (this.keepAliveInterval) {\n\t\t\tclearInterval(this.keepAliveInterval);\n\t\t\tthis.keepAliveInterval = undefined;\n\t\t}\n\t}\n\n\tprivate rejectAll(error: Error): void {\n\t\tfor (const pending of this.pending.values()) pending.reject(error);\n\t\tthis.pending.clear();\n\t\tfor (const pending of this.execPending.values()) pending.reject(error);\n\t\tthis.execPending.clear();\n\t}\n\n\tprivate send(message: Record<string, unknown>): void {\n\t\tif (this.socket.readyState !== WebSocket.OPEN) {\n\t\t\tthrow new Error(\"remote connection is not open\");\n\t\t}\n\t\tthis.socket.send(JSON.stringify(message));\n\t}\n\n\tprivate request(method: string, params: Record<string, unknown>): Promise<unknown> {\n\t\tconst id = `remote-${this.nextId++}`;\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tthis.pending.set(id, { resolve, reject });\n\t\t\ttry {\n\t\t\t\tthis.send({ id, method, params });\n\t\t\t} catch (error) {\n\t\t\t\tthis.pending.delete(id);\n\t\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t});\n\t}\n\n\tprivate handleMessage(data: unknown): void {\n\t\tif (typeof data !== \"string\") return;\n\t\tlet parsed: unknown;\n\t\ttry {\n\t\t\tparsed = JSON.parse(data);\n\t\t} catch {\n\t\t\treturn;\n\t\t}\n\t\tif (!isRecord(parsed)) return;\n\t\tif (parsed.type === \"pong\") {\n\t\t\tthis.lastPongAt = Date.now();\n\t\t\treturn;\n\t\t}\n\t\tif (typeof parsed.id !== \"string\") return;\n\t\tif (typeof parsed.event === \"string\") {\n\t\t\tthis.handleExecEvent(parsed as RemoteExecEvent);\n\t\t\treturn;\n\t\t}\n\t\tconst pending = this.pending.get(parsed.id);\n\t\tif (!pending) return;\n\t\tthis.pending.delete(parsed.id);\n\t\tconst response = parsed as RemoteResponse;\n\t\tif (\"error\" in response) {\n\t\t\tpending.reject(new Error(remoteErrorMessage(response.error)));\n\t\t\treturn;\n\t\t}\n\t\tpending.resolve(response.result);\n\t}\n\n\tprivate handleExecEvent(event: RemoteExecEvent): void {\n\t\tconst pending = this.execPending.get(event.id);\n\t\tif (!pending) return;\n\t\tif (event.event === \"data\") {\n\t\t\tconst encoded = optionalString(event.dataBase64);\n\t\t\tconst text = optionalString(event.data);\n\t\t\tif (encoded !== undefined) pending.onData(Buffer.from(encoded, \"base64\"));\n\t\t\telse if (text !== undefined) pending.onData(Buffer.from(text));\n\t\t\treturn;\n\t\t}\n\t\tthis.execPending.delete(event.id);\n\t\tif (event.event === \"error\") {\n\t\t\tpending.reject(new Error(remoteErrorMessage(event.error)));\n\t\t\treturn;\n\t\t}\n\t\tconst exitCode = optionalNumber(event.exitCode) ?? null;\n\t\tpending.resolve({ exitCode });\n\t}\n\n\tasync exec(command: string, options: ToolExecOptions): Promise<{ exitCode: number | null }> {\n\t\tconst id = `remote-${this.nextId++}`;\n\t\tlet timeoutHandle: NodeJS.Timeout | undefined;\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tconst cleanup = () => {\n\t\t\t\tif (timeoutHandle) clearTimeout(timeoutHandle);\n\t\t\t\toptions.signal?.removeEventListener(\"abort\", onAbort);\n\t\t\t};\n\t\t\tconst onAbort = () => {\n\t\t\t\ttry {\n\t\t\t\t\tthis.send({ id, method: \"cancel\" });\n\t\t\t\t} catch {}\n\t\t\t\tconst pending = this.execPending.get(id);\n\t\t\t\tif (pending) {\n\t\t\t\t\tthis.execPending.delete(id);\n\t\t\t\t\tcleanup();\n\t\t\t\t\tpending.reject(new Error(\"aborted\"));\n\t\t\t\t}\n\t\t\t};\n\t\t\tthis.execPending.set(id, {\n\t\t\t\tonData: options.onData,\n\t\t\t\tresolve: (value) => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\tresolve(value);\n\t\t\t\t},\n\t\t\t\treject: (error) => {\n\t\t\t\t\tcleanup();\n\t\t\t\t\treject(error);\n\t\t\t\t},\n\t\t\t});\n\t\t\toptions.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\t\t\tif (options.timeout !== undefined && options.timeout > 0) {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tthis.send({ id, method: \"cancel\" });\n\t\t\t\t\t} catch {}\n\t\t\t\t\tconst pending = this.execPending.get(id);\n\t\t\t\t\tif (pending) {\n\t\t\t\t\t\tthis.execPending.delete(id);\n\t\t\t\t\t\tcleanup();\n\t\t\t\t\t\tpending.reject(new Error(`timeout:${options.timeout}`));\n\t\t\t\t\t}\n\t\t\t\t}, options.timeout * 1000);\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.send({\n\t\t\t\t\tid,\n\t\t\t\t\tmethod: \"exec\",\n\t\t\t\t\tparams: { command, cwd: options.cwd ?? this.cwd, env: options.env, timeout: options.timeout },\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tthis.execPending.delete(id);\n\t\t\t\tcleanup();\n\t\t\t\treject(error instanceof Error ? error : new Error(String(error)));\n\t\t\t}\n\t\t});\n\t}\n\n\tasync access(path: string, mode?: ToolAccessMode): Promise<void> {\n\t\tawait this.request(\"access\", { path, mode });\n\t}\n\n\tasync readFile(path: string): Promise<Buffer> {\n\t\tconst result = await this.request(\"readFile\", { path });\n\t\tif (!isRecord(result)) throw new Error(\"Invalid remote readFile response\");\n\t\treturn Buffer.from(requireString(result.contentBase64, \"contentBase64\"), \"base64\");\n\t}\n\n\tasync writeFile(path: string, content: string | Buffer): Promise<void> {\n\t\tawait this.request(\"writeFile\", { path, contentBase64: Buffer.from(content).toString(\"base64\") });\n\t}\n\n\tasync mkdir(path: string, options: { recursive?: boolean } = {}): Promise<void> {\n\t\tawait this.request(\"mkdir\", { path, recursive: options.recursive ?? false });\n\t}\n\n\tasync stat(path: string): Promise<ToolFileStat> {\n\t\tconst result = await this.request(\"stat\", { path });\n\t\tif (!isRecord(result)) throw new Error(\"Invalid remote stat response\");\n\t\tconst kind = optionalString(result.kind) ?? optionalString(result.type);\n\t\tconst isDirectory = result.isDirectory === true || kind === \"directory\" || kind === \"dir\";\n\t\tconst isFile = result.isFile === true || kind === \"file\";\n\t\treturn { isDirectory: () => isDirectory, isFile: () => isFile };\n\t}\n\n\tasync readdir(path: string): Promise<string[]> {\n\t\tconst result = await this.request(\"readdir\", { path });\n\t\tif (Array.isArray(result)) return result.filter((entry): entry is string => typeof entry === \"string\");\n\t\tif (!isRecord(result) || !Array.isArray(result.entries)) throw new Error(\"Invalid remote readdir response\");\n\t\treturn result.entries.filter((entry): entry is string => typeof entry === \"string\");\n\t}\n\n\tasync glob(pattern: string, cwd: string, options: ToolGlobOptions): Promise<string[]> {\n\t\tconst result = await this.request(\"glob\", { pattern, cwd, ignore: options.ignore, limit: options.limit });\n\t\tif (Array.isArray(result)) return result.filter((entry): entry is string => typeof entry === \"string\");\n\t\tif (!isRecord(result) || !Array.isArray(result.matches)) throw new Error(\"Invalid remote glob response\");\n\t\treturn result.matches.filter((entry): entry is string => typeof entry === \"string\");\n\t}\n\n\tasync grep(options: ToolGrepOptions): Promise<ToolGrepResult> {\n\t\tconst result = await this.request(\"grep\", options as unknown as Record<string, unknown>);\n\t\tif (!isRecord(result)) throw new Error(\"Invalid remote grep response\");\n\t\tconst matches = Array.isArray(result.matches) ? result.matches : [];\n\t\treturn {\n\t\t\tisDirectory: result.isDirectory === true,\n\t\t\tmatches: matches.flatMap((entry): ToolGrepMatch[] => {\n\t\t\t\tif (!isRecord(entry)) return [];\n\t\t\t\tconst filePath = optionalString(entry.filePath);\n\t\t\t\tconst lineNumber = optionalNumber(entry.lineNumber);\n\t\t\t\tif (!filePath || lineNumber === undefined) return [];\n\t\t\t\treturn [{ filePath, lineNumber, lineText: optionalString(entry.lineText) }];\n\t\t\t}),\n\t\t};\n\t}\n\n\tasync detectImageMimeType(path: string): Promise<string | null | undefined> {\n\t\tconst result = await this.request(\"detectImageMimeType\", { path });\n\t\tif (!isRecord(result)) return undefined;\n\t\tconst mimeType = optionalString(result.mimeType);\n\t\treturn mimeType && [\"image/jpeg\", \"image/png\", \"image/gif\", \"image/webp\"].includes(mimeType) ? mimeType : null;\n\t}\n\n\tgetBackendInfo(): ToolBackendInfo {\n\t\treturn { type: \"remote\", cwd: this.cwd, url: this.url, protocol: this.protocol, configured: true };\n\t}\n\n\tasync dispose(): Promise<void> {\n\t\tthis.stopKeepAlive();\n\t\tthis.socket.close();\n\t}\n}\n\nexport function createRemoteToolOperations(url: string): Promise<RemoteToolOperations> {\n\treturn RemoteToolOperations.connect(url);\n}\n\nexport function createSshToolOperations(target: string): Promise<SshToolOperations> {\n\treturn SshToolOperations.fromTarget(target);\n}\n"]}
|