@kolisachint/hoocode-agent 0.4.56 → 0.4.58
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 +4 -0
- package/dist/cli/args.d.ts +1 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +5 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1 -1
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/defaults.d.ts.map +1 -1
- package/dist/core/defaults.js +1 -1
- package/dist/core/defaults.js.map +1 -1
- package/dist/core/settings-defaults.d.ts +1 -0
- package/dist/core/settings-defaults.d.ts.map +1 -1
- package/dist/core/settings-defaults.js +1 -0
- package/dist/core/settings-defaults.js.map +1 -1
- package/dist/core/settings-manager.d.ts +3 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +8 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +16 -6
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +5 -0
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +37 -0
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +1 -0
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +6 -2
- 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 +1 -1
- 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 +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +6 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +1 -0
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +14 -2
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/todo.d.ts +30 -0
- package/dist/core/tools/todo.d.ts.map +1 -0
- package/dist/core/tools/todo.js +126 -0
- package/dist/core/tools/todo.js.map +1 -0
- package/dist/extensions/core/hoo-core.d.ts +44 -0
- package/dist/extensions/core/hoo-core.d.ts.map +1 -1
- package/dist/extensions/core/hoo-core.js +74 -0
- package/dist/extensions/core/hoo-core.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +7 -0
- package/dist/main.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
package/dist/core/tools/ls.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Text } from "@kolisachint/hoocode-tui";
|
|
2
2
|
import { existsSync, readdirSync, statSync } from "fs";
|
|
3
|
+
import { minimatch } from "minimatch";
|
|
3
4
|
import nodePath from "path";
|
|
4
5
|
import { Type } from "typebox";
|
|
5
6
|
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
|
@@ -10,6 +11,9 @@ import { DEFAULT_MAX_BYTES, formatSize, truncateHead } from "./truncate.js";
|
|
|
10
11
|
const lsSchema = Type.Object({
|
|
11
12
|
path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
|
|
12
13
|
limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })),
|
|
14
|
+
ignore: Type.Optional(Type.Array(Type.String(), {
|
|
15
|
+
description: "Glob patterns matched against entry names to exclude, e.g. ['node_modules', '*.log', '.git']. Matching is on the entry name only (ls is non-recursive); dotfiles are matched.",
|
|
16
|
+
})),
|
|
13
17
|
});
|
|
14
18
|
const DEFAULT_LIMIT = 500;
|
|
15
19
|
const defaultLsOperations = {
|
|
@@ -58,10 +62,10 @@ export function createLsToolDefinition(cwd, options) {
|
|
|
58
62
|
return {
|
|
59
63
|
name: "ls",
|
|
60
64
|
label: "ls",
|
|
61
|
-
description: `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).`,
|
|
65
|
+
description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Lists a single directory (not recursive) and shows everything on disk; pass 'ignore' glob patterns to skip entries like node_modules or .git. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
|
|
62
66
|
promptSnippet: "List directory contents",
|
|
63
67
|
parameters: lsSchema,
|
|
64
|
-
async execute(_toolCallId, { path, limit }, signal, _onUpdate, _ctx) {
|
|
68
|
+
async execute(_toolCallId, { path, limit, ignore }, signal, _onUpdate, _ctx) {
|
|
65
69
|
return new Promise((resolve, reject) => {
|
|
66
70
|
if (signal?.aborted) {
|
|
67
71
|
reject(new Error("Operation aborted"));
|
|
@@ -98,6 +102,14 @@ export function createLsToolDefinition(cwd, options) {
|
|
|
98
102
|
}
|
|
99
103
|
return;
|
|
100
104
|
}
|
|
105
|
+
// Exclude entries matching any ignore glob. Match on the bare entry
|
|
106
|
+
// name (ls is non-recursive) with dot:true so patterns can target
|
|
107
|
+
// dotfiles like '.git'. Default behavior (no patterns) still shows
|
|
108
|
+
// everything on disk.
|
|
109
|
+
const ignorePatterns = (ignore ?? []).filter((p) => typeof p === "string" && p.length > 0);
|
|
110
|
+
if (ignorePatterns.length > 0) {
|
|
111
|
+
entries = entries.filter((entry) => !ignorePatterns.some((pattern) => minimatch(entry, pattern, { dot: true })));
|
|
112
|
+
}
|
|
101
113
|
// Sort alphabetically, case-insensitive.
|
|
102
114
|
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
103
115
|
// Format entries with directory indicators.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ls.js","sourceRoot":"","sources":["../../../src/core/tools/ls.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { existsSync, readdirSync, statSync } from \"fs\";\nimport nodePath from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\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\n/**\n * Pluggable operations for the ls tool.\n * Override these to delegate directory listing to remote systems (for example SSH).\n */\nexport interface LsOperations {\n\t/** Check if path exists */\n\texists: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Get file or directory stats. Throws if not found. */\n\tstat: (absolutePath: string) => Promise<{ isDirectory: () => boolean }> | { isDirectory: () => boolean };\n\t/** Read directory entries */\n\treaddir: (absolutePath: string) => Promise<string[]> | string[];\n}\n\nconst defaultLsOperations: LsOperations = {\n\texists: existsSync,\n\tstat: statSync,\n\treaddir: readdirSync,\n};\n\nexport interface LsToolOptions {\n\t/** Custom operations for directory listing. Default: local filesystem */\n\toperations?: LsOperations;\n}\n\nfunction formatLsCall(\n\targs: { path?: string; limit?: number } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").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.js\").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\tcwd: string,\n\toptions?: LsToolOptions,\n): ToolDefinition<typeof lsSchema, LsToolDetails | undefined> {\n\tconst ops = options?.operations ?? defaultLsOperations;\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\tif (!(await ops.exists(dirPath))) {\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) {\n\t\t\t\t\t\t\tif (e instanceof Error) {\n\t\t\t\t\t\t\t\treject(new Error(`Cannot read directory: ${e.message}`));\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treject(new Error(`Cannot read directory: ${String(e)}`));\n\t\t\t\t\t\t\t}\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) {\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(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema> {\n\treturn wrapToolDefinition(createLsToolDefinition(cwd, 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 \"@kolisachint/hoocode-agent-core\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { existsSync, readdirSync, statSync } from \"fs\";\nimport { minimatch } from \"minimatch\";\nimport nodePath from \"path\";\nimport { type Static, Type } from \"typebox\";\nimport { keyHint } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { resolveToCwd } from \"./path-utils.js\";\nimport { getTextOutput, invalidArgText, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\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\tignore: Type.Optional(\n\t\tType.Array(Type.String(), {\n\t\t\tdescription:\n\t\t\t\t\"Glob patterns matched against entry names to exclude, e.g. ['node_modules', '*.log', '.git']. Matching is on the entry name only (ls is non-recursive); dotfiles are matched.\",\n\t\t}),\n\t),\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\n/**\n * Pluggable operations for the ls tool.\n * Override these to delegate directory listing to remote systems (for example SSH).\n */\nexport interface LsOperations {\n\t/** Check if path exists */\n\texists: (absolutePath: string) => Promise<boolean> | boolean;\n\t/** Get file or directory stats. Throws if not found. */\n\tstat: (absolutePath: string) => Promise<{ isDirectory: () => boolean }> | { isDirectory: () => boolean };\n\t/** Read directory entries */\n\treaddir: (absolutePath: string) => Promise<string[]> | string[];\n}\n\nconst defaultLsOperations: LsOperations = {\n\texists: existsSync,\n\tstat: statSync,\n\treaddir: readdirSync,\n};\n\nexport interface LsToolOptions {\n\t/** Custom operations for directory listing. Default: local filesystem */\n\toperations?: LsOperations;\n}\n\nfunction formatLsCall(\n\targs: { path?: string; limit?: number } | undefined,\n\ttheme: typeof import(\"../../modes/interactive/theme/theme.js\").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.js\").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\tcwd: string,\n\toptions?: LsToolOptions,\n): ToolDefinition<typeof lsSchema, LsToolDetails | undefined> {\n\tconst ops = options?.operations ?? defaultLsOperations;\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. Lists a single directory (not recursive) and shows everything on disk; pass 'ignore' glob patterns to skip entries like node_modules or .git. 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, ignore }: { path?: string; limit?: number; ignore?: string[] },\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\tif (!(await ops.exists(dirPath))) {\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) {\n\t\t\t\t\t\t\tif (e instanceof Error) {\n\t\t\t\t\t\t\t\treject(new Error(`Cannot read directory: ${e.message}`));\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\treject(new Error(`Cannot read directory: ${String(e)}`));\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Exclude entries matching any ignore glob. Match on the bare entry\n\t\t\t\t\t\t// name (ls is non-recursive) with dot:true so patterns can target\n\t\t\t\t\t\t// dotfiles like '.git'. Default behavior (no patterns) still shows\n\t\t\t\t\t\t// everything on disk.\n\t\t\t\t\t\tconst ignorePatterns = (ignore ?? []).filter((p) => typeof p === \"string\" && p.length > 0);\n\t\t\t\t\t\tif (ignorePatterns.length > 0) {\n\t\t\t\t\t\t\tentries = entries.filter(\n\t\t\t\t\t\t\t\t(entry) => !ignorePatterns.some((pattern) => minimatch(entry, pattern, { dot: true })),\n\t\t\t\t\t\t\t);\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) {\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(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema> {\n\treturn wrapToolDefinition(createLsToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TodoWrite tool: let the main agent maintain a visible todo list for the
|
|
3
|
+
* current task.
|
|
4
|
+
*
|
|
5
|
+
* hoocode already has all the infrastructure this needs — the task store models
|
|
6
|
+
* `{ title, status }` items with a per-turn lifecycle, and the TUI task panel
|
|
7
|
+
* renders them. The only missing piece was a tool the model can call; this is
|
|
8
|
+
* that thin adapter over `taskStore`.
|
|
9
|
+
*
|
|
10
|
+
* Semantics mirror Claude Code's TodoWrite: each call sends the FULL list and
|
|
11
|
+
* REPLACES the previous one. Because the store is incremental (numeric ids), we
|
|
12
|
+
* reconcile the incoming list against the existing main-agent tasks by position:
|
|
13
|
+
* update items that are still there, create new ones, and drop the tail that was
|
|
14
|
+
* removed. Reconciling (rather than clear-and-recreate) keeps ids stable so the
|
|
15
|
+
* panel does not flicker and in-progress rows stay put.
|
|
16
|
+
*
|
|
17
|
+
* It is an optional, opt-in tool (enabled via the `enableTodoWrite` setting) and
|
|
18
|
+
* is never registered inside a spawned subagent, so a subagent's todos cannot
|
|
19
|
+
* leak into the parent's "main" task group.
|
|
20
|
+
*/
|
|
21
|
+
import { type ToolDefinition } from "../extensions/types.js";
|
|
22
|
+
export interface TodoWriteDetails {
|
|
23
|
+
total: number;
|
|
24
|
+
pending: number;
|
|
25
|
+
inProgress: number;
|
|
26
|
+
completed: number;
|
|
27
|
+
}
|
|
28
|
+
/** Create the TodoWrite tool definition. Registered as a customTool when enabled. */
|
|
29
|
+
export declare function createTodoWriteToolDefinition(): ToolDefinition;
|
|
30
|
+
//# sourceMappingURL=todo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"todo.d.ts","sourceRoot":"","sources":["../../../src/core/tools/todo.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAoCzE,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CAClB;AAyBD,qFAAqF;AACrF,wBAAgB,6BAA6B,IAAI,cAAc,CAoE9D","sourcesContent":["/**\n * TodoWrite tool: let the main agent maintain a visible todo list for the\n * current task.\n *\n * hoocode already has all the infrastructure this needs — the task store models\n * `{ title, status }` items with a per-turn lifecycle, and the TUI task panel\n * renders them. The only missing piece was a tool the model can call; this is\n * that thin adapter over `taskStore`.\n *\n * Semantics mirror Claude Code's TodoWrite: each call sends the FULL list and\n * REPLACES the previous one. Because the store is incremental (numeric ids), we\n * reconcile the incoming list against the existing main-agent tasks by position:\n * update items that are still there, create new ones, and drop the tail that was\n * removed. Reconciling (rather than clear-and-recreate) keeps ids stable so the\n * panel does not flicker and in-progress rows stay put.\n *\n * It is an optional, opt-in tool (enabled via the `enableTodoWrite` setting) and\n * is never registered inside a spawned subagent, so a subagent's todos cannot\n * leak into the parent's \"main\" task group.\n */\n\nimport { type Static, Type } from \"typebox\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport { type Task, type TaskStatus, taskOwnerId, taskStore } from \"../task-store.js\";\n\nconst todoStatusSchema = Type.Union([Type.Literal(\"pending\"), Type.Literal(\"in_progress\"), Type.Literal(\"completed\")], {\n\tdescription: \"pending = not started, in_progress = actively being worked on, completed = finished.\",\n});\n\nconst todoItemSchema = Type.Object(\n\t{\n\t\tcontent: Type.String({\n\t\t\tdescription: \"The task, in imperative form (e.g. 'Add tests for the parser').\",\n\t\t}),\n\t\tstatus: todoStatusSchema,\n\t\tactiveForm: Type.Optional(\n\t\t\tType.String({\n\t\t\t\tdescription:\n\t\t\t\t\t\"Optional present-tense form shown while the item is in_progress (e.g. 'Adding tests for the parser').\",\n\t\t\t}),\n\t\t),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst todoWriteParams = Type.Object(\n\t{\n\t\ttodos: Type.Array(todoItemSchema, {\n\t\t\tdescription:\n\t\t\t\t\"The complete todo list. This REPLACES the previous list on every call, so always send every item with its current status — omitting an item removes it.\",\n\t\t}),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype TodoWriteParams = Static<typeof todoWriteParams>;\ntype IncomingStatus = TodoWriteParams[\"todos\"][number][\"status\"];\n\nexport interface TodoWriteDetails {\n\ttotal: number;\n\tpending: number;\n\tinProgress: number;\n\tcompleted: number;\n}\n\n/** Map the model-facing status vocabulary onto the task store's. */\nfunction toTaskStatus(status: IncomingStatus): TaskStatus {\n\treturn status === \"completed\" ? \"done\" : status;\n}\n\nconst STATUS_GLYPH: Record<TaskStatus, string> = {\n\tpending: \"[ ]\",\n\tin_progress: \"[~]\",\n\tdone: \"[x]\",\n\tfailed: \"[!]\",\n};\n\n/** Title to display: the active-form while in progress, otherwise the content. */\nfunction displayTitle(item: TodoWriteParams[\"todos\"][number]): string {\n\tif (item.status === \"in_progress\" && item.activeForm?.trim()) return item.activeForm.trim();\n\treturn item.content.trim();\n}\n\n/** Current main-agent tasks (source unset), in stable creation order. */\nfunction mainTasks(): Task[] {\n\treturn taskStore.list().filter((t) => taskOwnerId(t) === \"main\");\n}\n\n/** Create the TodoWrite tool definition. Registered as a customTool when enabled. */\nexport function createTodoWriteToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof todoWriteParams, TodoWriteDetails>({\n\t\tname: \"TodoWrite\",\n\t\tlabel: \"TodoWrite\",\n\t\tdescription: [\n\t\t\t\"Maintain a structured todo list for the current task, shown live in the task panel.\",\n\t\t\t\"Use it PROACTIVELY: at the start of any multi-step or non-trivial task, write the full plan as todos before you begin, then keep it current as you work.\",\n\t\t\t\"Mark exactly ONE item in_progress at a time, and flip an item to completed immediately after finishing it — do not batch completions or leave finished work marked in_progress.\",\n\t\t\t\"Each call sends the FULL list and REPLACES the previous one — always include every item with its current status; omitting an item removes it.\",\n\t\t\t\"Skip it only for trivial, single-step tasks where a list adds no value. When in doubt on multi-step work, use it — it keeps you from losing track of steps.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet:\n\t\t\t\"Plan and track multi-step work as a live todo list (use proactively; replaces the whole list each call)\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use TodoWrite proactively for any multi-step or non-trivial task: write the plan as todos up front, keep exactly one item in_progress, and mark items completed immediately as you finish them.\",\n\t\t\t\"TodoWrite replaces the entire list each call — always send all items with their current status.\",\n\t\t\t\"Skip TodoWrite for trivial single-step tasks where a checklist adds no value.\",\n\t\t],\n\t\tparameters: todoWriteParams,\n\t\tasync execute(_toolCallId, params: TodoWriteParams) {\n\t\t\tconst todos = params.todos ?? [];\n\t\t\tconst existing = mainTasks();\n\n\t\t\t// Reconcile by position: update kept items, create new ones, remove the tail.\n\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\tconst item = todos[i]!;\n\t\t\t\tconst status = toTaskStatus(item.status);\n\t\t\t\tconst title = displayTitle(item);\n\t\t\t\tconst current = existing[i];\n\t\t\t\tif (current) {\n\t\t\t\t\ttaskStore.update(current.id, { title, status });\n\t\t\t\t} else {\n\t\t\t\t\tconst created = taskStore.create(title);\n\t\t\t\t\ttaskStore.update(created.id, { status });\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (let i = todos.length; i < existing.length; i++) {\n\t\t\t\ttaskStore.remove(existing[i]!.id);\n\t\t\t}\n\n\t\t\tconst counts = todos.reduce(\n\t\t\t\t(acc, t) => {\n\t\t\t\t\tif (t.status === \"in_progress\") acc.inProgress++;\n\t\t\t\t\telse if (t.status === \"completed\") acc.completed++;\n\t\t\t\t\telse acc.pending++;\n\t\t\t\t\treturn acc;\n\t\t\t\t},\n\t\t\t\t{ pending: 0, inProgress: 0, completed: 0 },\n\t\t\t);\n\n\t\t\tconst lines = todos.map((t) => `${STATUS_GLYPH[toTaskStatus(t.status)]} ${displayTitle(t)}`);\n\t\t\tconst header =\n\t\t\t\ttodos.length === 0\n\t\t\t\t\t? \"Todo list cleared.\"\n\t\t\t\t\t: `Todos updated (${counts.inProgress} in progress, ${counts.pending} pending, ${counts.completed} completed):`;\n\t\t\tconst text = todos.length === 0 ? header : `${header}\\n${lines.join(\"\\n\")}`;\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\tdetails: {\n\t\t\t\t\ttotal: todos.length,\n\t\t\t\t\tpending: counts.pending,\n\t\t\t\t\tinProgress: counts.inProgress,\n\t\t\t\t\tcompleted: counts.completed,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TodoWrite tool: let the main agent maintain a visible todo list for the
|
|
3
|
+
* current task.
|
|
4
|
+
*
|
|
5
|
+
* hoocode already has all the infrastructure this needs — the task store models
|
|
6
|
+
* `{ title, status }` items with a per-turn lifecycle, and the TUI task panel
|
|
7
|
+
* renders them. The only missing piece was a tool the model can call; this is
|
|
8
|
+
* that thin adapter over `taskStore`.
|
|
9
|
+
*
|
|
10
|
+
* Semantics mirror Claude Code's TodoWrite: each call sends the FULL list and
|
|
11
|
+
* REPLACES the previous one. Because the store is incremental (numeric ids), we
|
|
12
|
+
* reconcile the incoming list against the existing main-agent tasks by position:
|
|
13
|
+
* update items that are still there, create new ones, and drop the tail that was
|
|
14
|
+
* removed. Reconciling (rather than clear-and-recreate) keeps ids stable so the
|
|
15
|
+
* panel does not flicker and in-progress rows stay put.
|
|
16
|
+
*
|
|
17
|
+
* It is an optional, opt-in tool (enabled via the `enableTodoWrite` setting) and
|
|
18
|
+
* is never registered inside a spawned subagent, so a subagent's todos cannot
|
|
19
|
+
* leak into the parent's "main" task group.
|
|
20
|
+
*/
|
|
21
|
+
import { Type } from "typebox";
|
|
22
|
+
import { defineTool } from "../extensions/types.js";
|
|
23
|
+
import { taskOwnerId, taskStore } from "../task-store.js";
|
|
24
|
+
const todoStatusSchema = Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")], {
|
|
25
|
+
description: "pending = not started, in_progress = actively being worked on, completed = finished.",
|
|
26
|
+
});
|
|
27
|
+
const todoItemSchema = Type.Object({
|
|
28
|
+
content: Type.String({
|
|
29
|
+
description: "The task, in imperative form (e.g. 'Add tests for the parser').",
|
|
30
|
+
}),
|
|
31
|
+
status: todoStatusSchema,
|
|
32
|
+
activeForm: Type.Optional(Type.String({
|
|
33
|
+
description: "Optional present-tense form shown while the item is in_progress (e.g. 'Adding tests for the parser').",
|
|
34
|
+
})),
|
|
35
|
+
}, { additionalProperties: false });
|
|
36
|
+
const todoWriteParams = Type.Object({
|
|
37
|
+
todos: Type.Array(todoItemSchema, {
|
|
38
|
+
description: "The complete todo list. This REPLACES the previous list on every call, so always send every item with its current status — omitting an item removes it.",
|
|
39
|
+
}),
|
|
40
|
+
}, { additionalProperties: false });
|
|
41
|
+
/** Map the model-facing status vocabulary onto the task store's. */
|
|
42
|
+
function toTaskStatus(status) {
|
|
43
|
+
return status === "completed" ? "done" : status;
|
|
44
|
+
}
|
|
45
|
+
const STATUS_GLYPH = {
|
|
46
|
+
pending: "[ ]",
|
|
47
|
+
in_progress: "[~]",
|
|
48
|
+
done: "[x]",
|
|
49
|
+
failed: "[!]",
|
|
50
|
+
};
|
|
51
|
+
/** Title to display: the active-form while in progress, otherwise the content. */
|
|
52
|
+
function displayTitle(item) {
|
|
53
|
+
if (item.status === "in_progress" && item.activeForm?.trim())
|
|
54
|
+
return item.activeForm.trim();
|
|
55
|
+
return item.content.trim();
|
|
56
|
+
}
|
|
57
|
+
/** Current main-agent tasks (source unset), in stable creation order. */
|
|
58
|
+
function mainTasks() {
|
|
59
|
+
return taskStore.list().filter((t) => taskOwnerId(t) === "main");
|
|
60
|
+
}
|
|
61
|
+
/** Create the TodoWrite tool definition. Registered as a customTool when enabled. */
|
|
62
|
+
export function createTodoWriteToolDefinition() {
|
|
63
|
+
return defineTool({
|
|
64
|
+
name: "TodoWrite",
|
|
65
|
+
label: "TodoWrite",
|
|
66
|
+
description: [
|
|
67
|
+
"Maintain a structured todo list for the current task, shown live in the task panel.",
|
|
68
|
+
"Use it PROACTIVELY: at the start of any multi-step or non-trivial task, write the full plan as todos before you begin, then keep it current as you work.",
|
|
69
|
+
"Mark exactly ONE item in_progress at a time, and flip an item to completed immediately after finishing it — do not batch completions or leave finished work marked in_progress.",
|
|
70
|
+
"Each call sends the FULL list and REPLACES the previous one — always include every item with its current status; omitting an item removes it.",
|
|
71
|
+
"Skip it only for trivial, single-step tasks where a list adds no value. When in doubt on multi-step work, use it — it keeps you from losing track of steps.",
|
|
72
|
+
].join("\n"),
|
|
73
|
+
promptSnippet: "Plan and track multi-step work as a live todo list (use proactively; replaces the whole list each call)",
|
|
74
|
+
promptGuidelines: [
|
|
75
|
+
"Use TodoWrite proactively for any multi-step or non-trivial task: write the plan as todos up front, keep exactly one item in_progress, and mark items completed immediately as you finish them.",
|
|
76
|
+
"TodoWrite replaces the entire list each call — always send all items with their current status.",
|
|
77
|
+
"Skip TodoWrite for trivial single-step tasks where a checklist adds no value.",
|
|
78
|
+
],
|
|
79
|
+
parameters: todoWriteParams,
|
|
80
|
+
async execute(_toolCallId, params) {
|
|
81
|
+
const todos = params.todos ?? [];
|
|
82
|
+
const existing = mainTasks();
|
|
83
|
+
// Reconcile by position: update kept items, create new ones, remove the tail.
|
|
84
|
+
for (let i = 0; i < todos.length; i++) {
|
|
85
|
+
const item = todos[i];
|
|
86
|
+
const status = toTaskStatus(item.status);
|
|
87
|
+
const title = displayTitle(item);
|
|
88
|
+
const current = existing[i];
|
|
89
|
+
if (current) {
|
|
90
|
+
taskStore.update(current.id, { title, status });
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const created = taskStore.create(title);
|
|
94
|
+
taskStore.update(created.id, { status });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (let i = todos.length; i < existing.length; i++) {
|
|
98
|
+
taskStore.remove(existing[i].id);
|
|
99
|
+
}
|
|
100
|
+
const counts = todos.reduce((acc, t) => {
|
|
101
|
+
if (t.status === "in_progress")
|
|
102
|
+
acc.inProgress++;
|
|
103
|
+
else if (t.status === "completed")
|
|
104
|
+
acc.completed++;
|
|
105
|
+
else
|
|
106
|
+
acc.pending++;
|
|
107
|
+
return acc;
|
|
108
|
+
}, { pending: 0, inProgress: 0, completed: 0 });
|
|
109
|
+
const lines = todos.map((t) => `${STATUS_GLYPH[toTaskStatus(t.status)]} ${displayTitle(t)}`);
|
|
110
|
+
const header = todos.length === 0
|
|
111
|
+
? "Todo list cleared."
|
|
112
|
+
: `Todos updated (${counts.inProgress} in progress, ${counts.pending} pending, ${counts.completed} completed):`;
|
|
113
|
+
const text = todos.length === 0 ? header : `${header}\n${lines.join("\n")}`;
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text }],
|
|
116
|
+
details: {
|
|
117
|
+
total: todos.length,
|
|
118
|
+
pending: counts.pending,
|
|
119
|
+
inProgress: counts.inProgress,
|
|
120
|
+
completed: counts.completed,
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=todo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"todo.js","sourceRoot":"","sources":["../../../src/core/tools/todo.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * TodoWrite tool: let the main agent maintain a visible todo list for the\n * current task.\n *\n * hoocode already has all the infrastructure this needs — the task store models\n * `{ title, status }` items with a per-turn lifecycle, and the TUI task panel\n * renders them. The only missing piece was a tool the model can call; this is\n * that thin adapter over `taskStore`.\n *\n * Semantics mirror Claude Code's TodoWrite: each call sends the FULL list and\n * REPLACES the previous one. Because the store is incremental (numeric ids), we\n * reconcile the incoming list against the existing main-agent tasks by position:\n * update items that are still there, create new ones, and drop the tail that was\n * removed. Reconciling (rather than clear-and-recreate) keeps ids stable so the\n * panel does not flicker and in-progress rows stay put.\n *\n * It is an optional, opt-in tool (enabled via the `enableTodoWrite` setting) and\n * is never registered inside a spawned subagent, so a subagent's todos cannot\n * leak into the parent's \"main\" task group.\n */\n\nimport { type Static, Type } from \"typebox\";\nimport { defineTool, type ToolDefinition } from \"../extensions/types.js\";\nimport { type Task, type TaskStatus, taskOwnerId, taskStore } from \"../task-store.js\";\n\nconst todoStatusSchema = Type.Union([Type.Literal(\"pending\"), Type.Literal(\"in_progress\"), Type.Literal(\"completed\")], {\n\tdescription: \"pending = not started, in_progress = actively being worked on, completed = finished.\",\n});\n\nconst todoItemSchema = Type.Object(\n\t{\n\t\tcontent: Type.String({\n\t\t\tdescription: \"The task, in imperative form (e.g. 'Add tests for the parser').\",\n\t\t}),\n\t\tstatus: todoStatusSchema,\n\t\tactiveForm: Type.Optional(\n\t\t\tType.String({\n\t\t\t\tdescription:\n\t\t\t\t\t\"Optional present-tense form shown while the item is in_progress (e.g. 'Adding tests for the parser').\",\n\t\t\t}),\n\t\t),\n\t},\n\t{ additionalProperties: false },\n);\n\nconst todoWriteParams = Type.Object(\n\t{\n\t\ttodos: Type.Array(todoItemSchema, {\n\t\t\tdescription:\n\t\t\t\t\"The complete todo list. This REPLACES the previous list on every call, so always send every item with its current status — omitting an item removes it.\",\n\t\t}),\n\t},\n\t{ additionalProperties: false },\n);\n\ntype TodoWriteParams = Static<typeof todoWriteParams>;\ntype IncomingStatus = TodoWriteParams[\"todos\"][number][\"status\"];\n\nexport interface TodoWriteDetails {\n\ttotal: number;\n\tpending: number;\n\tinProgress: number;\n\tcompleted: number;\n}\n\n/** Map the model-facing status vocabulary onto the task store's. */\nfunction toTaskStatus(status: IncomingStatus): TaskStatus {\n\treturn status === \"completed\" ? \"done\" : status;\n}\n\nconst STATUS_GLYPH: Record<TaskStatus, string> = {\n\tpending: \"[ ]\",\n\tin_progress: \"[~]\",\n\tdone: \"[x]\",\n\tfailed: \"[!]\",\n};\n\n/** Title to display: the active-form while in progress, otherwise the content. */\nfunction displayTitle(item: TodoWriteParams[\"todos\"][number]): string {\n\tif (item.status === \"in_progress\" && item.activeForm?.trim()) return item.activeForm.trim();\n\treturn item.content.trim();\n}\n\n/** Current main-agent tasks (source unset), in stable creation order. */\nfunction mainTasks(): Task[] {\n\treturn taskStore.list().filter((t) => taskOwnerId(t) === \"main\");\n}\n\n/** Create the TodoWrite tool definition. Registered as a customTool when enabled. */\nexport function createTodoWriteToolDefinition(): ToolDefinition {\n\treturn defineTool<typeof todoWriteParams, TodoWriteDetails>({\n\t\tname: \"TodoWrite\",\n\t\tlabel: \"TodoWrite\",\n\t\tdescription: [\n\t\t\t\"Maintain a structured todo list for the current task, shown live in the task panel.\",\n\t\t\t\"Use it PROACTIVELY: at the start of any multi-step or non-trivial task, write the full plan as todos before you begin, then keep it current as you work.\",\n\t\t\t\"Mark exactly ONE item in_progress at a time, and flip an item to completed immediately after finishing it — do not batch completions or leave finished work marked in_progress.\",\n\t\t\t\"Each call sends the FULL list and REPLACES the previous one — always include every item with its current status; omitting an item removes it.\",\n\t\t\t\"Skip it only for trivial, single-step tasks where a list adds no value. When in doubt on multi-step work, use it — it keeps you from losing track of steps.\",\n\t\t].join(\"\\n\"),\n\t\tpromptSnippet:\n\t\t\t\"Plan and track multi-step work as a live todo list (use proactively; replaces the whole list each call)\",\n\t\tpromptGuidelines: [\n\t\t\t\"Use TodoWrite proactively for any multi-step or non-trivial task: write the plan as todos up front, keep exactly one item in_progress, and mark items completed immediately as you finish them.\",\n\t\t\t\"TodoWrite replaces the entire list each call — always send all items with their current status.\",\n\t\t\t\"Skip TodoWrite for trivial single-step tasks where a checklist adds no value.\",\n\t\t],\n\t\tparameters: todoWriteParams,\n\t\tasync execute(_toolCallId, params: TodoWriteParams) {\n\t\t\tconst todos = params.todos ?? [];\n\t\t\tconst existing = mainTasks();\n\n\t\t\t// Reconcile by position: update kept items, create new ones, remove the tail.\n\t\t\tfor (let i = 0; i < todos.length; i++) {\n\t\t\t\tconst item = todos[i]!;\n\t\t\t\tconst status = toTaskStatus(item.status);\n\t\t\t\tconst title = displayTitle(item);\n\t\t\t\tconst current = existing[i];\n\t\t\t\tif (current) {\n\t\t\t\t\ttaskStore.update(current.id, { title, status });\n\t\t\t\t} else {\n\t\t\t\t\tconst created = taskStore.create(title);\n\t\t\t\t\ttaskStore.update(created.id, { status });\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (let i = todos.length; i < existing.length; i++) {\n\t\t\t\ttaskStore.remove(existing[i]!.id);\n\t\t\t}\n\n\t\t\tconst counts = todos.reduce(\n\t\t\t\t(acc, t) => {\n\t\t\t\t\tif (t.status === \"in_progress\") acc.inProgress++;\n\t\t\t\t\telse if (t.status === \"completed\") acc.completed++;\n\t\t\t\t\telse acc.pending++;\n\t\t\t\t\treturn acc;\n\t\t\t\t},\n\t\t\t\t{ pending: 0, inProgress: 0, completed: 0 },\n\t\t\t);\n\n\t\t\tconst lines = todos.map((t) => `${STATUS_GLYPH[toTaskStatus(t.status)]} ${displayTitle(t)}`);\n\t\t\tconst header =\n\t\t\t\ttodos.length === 0\n\t\t\t\t\t? \"Todo list cleared.\"\n\t\t\t\t\t: `Todos updated (${counts.inProgress} in progress, ${counts.pending} pending, ${counts.completed} completed):`;\n\t\t\tconst text = todos.length === 0 ? header : `${header}\\n${lines.join(\"\\n\")}`;\n\n\t\t\treturn {\n\t\t\t\tcontent: [{ type: \"text\" as const, text }],\n\t\t\t\tdetails: {\n\t\t\t\t\ttotal: todos.length,\n\t\t\t\t\tpending: counts.pending,\n\t\t\t\t\tinProgress: counts.inProgress,\n\t\t\t\t\tcompleted: counts.completed,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t});\n}\n"]}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* 1. ~/.hoocode/hoo-config.json (global defaults)
|
|
15
15
|
* 2. ./.hoocode/hoo-config.json (project overrides — scalars win; arrays union)
|
|
16
16
|
*/
|
|
17
|
+
import type { ThinkingLevel } from "@kolisachint/hoocode-agent-core";
|
|
17
18
|
import type { ExtensionAPI } from "../../core/extensions/types.js";
|
|
18
19
|
interface ModeConfig {
|
|
19
20
|
/** Tool names that bypass the permission gate in this mode */
|
|
@@ -29,6 +30,32 @@ interface ModeConfig {
|
|
|
29
30
|
/** Regex patterns for denied bash commands. A command matching any pattern is blocked. */
|
|
30
31
|
denied_bash_commands?: string[];
|
|
31
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Tool-outcome-driven thinking escalation (on by default).
|
|
35
|
+
*
|
|
36
|
+
* The default (fast) path keeps thinking low so mechanical tool turns — reads,
|
|
37
|
+
* greps, successful edits — don't pay the extended-thinking prefill tax. When a
|
|
38
|
+
* tool *fails*, the next turn(s) escalate to a higher thinking level so the model
|
|
39
|
+
* reasons through the failure, then the level is restored. This buys low latency
|
|
40
|
+
* on the happy path while preserving deep reasoning exactly when something breaks.
|
|
41
|
+
*
|
|
42
|
+
* Enabled by default; set `thinking_escalation.enabled` to `false` to turn it off.
|
|
43
|
+
*
|
|
44
|
+
* Note: escalation uses the same setter as manual thinking control, so the
|
|
45
|
+
* escalated level is briefly written to settings and restored when the window
|
|
46
|
+
* ends. If a run is interrupted mid-window, the escalated level may persist until
|
|
47
|
+
* the next change.
|
|
48
|
+
*/
|
|
49
|
+
interface ThinkingEscalationConfig {
|
|
50
|
+
/** Master switch. Default: true (set to false to disable). */
|
|
51
|
+
enabled?: boolean;
|
|
52
|
+
/** Level to escalate to after a tool error. Default: "high". */
|
|
53
|
+
on_error?: ThinkingLevel;
|
|
54
|
+
/** Restrict escalation to errors from these tool names. Default: any tool. */
|
|
55
|
+
tools?: string[];
|
|
56
|
+
/** Number of subsequent turns to stay escalated after an error. Default: 1. */
|
|
57
|
+
cooldown_turns?: number;
|
|
58
|
+
}
|
|
32
59
|
export interface HooConfig {
|
|
33
60
|
/** Manually-pinned active mode (overrides default "build") */
|
|
34
61
|
active_mode?: string;
|
|
@@ -36,6 +63,8 @@ export interface HooConfig {
|
|
|
36
63
|
modes?: Record<string, ModeConfig>;
|
|
37
64
|
/** Extra directories to search for `{name}/system.md` mode files (after project + user). */
|
|
38
65
|
mode_paths?: string[];
|
|
66
|
+
/** Raise thinking after tool failures, restore on success. On by default; set `enabled: false` to disable. */
|
|
67
|
+
thinking_escalation?: ThinkingEscalationConfig;
|
|
39
68
|
}
|
|
40
69
|
/**
|
|
41
70
|
* Deep-merges a project-local config on top of the global config.
|
|
@@ -46,6 +75,7 @@ export interface HooConfig {
|
|
|
46
75
|
* - modes[x].allowed_write_paths: union of global + project arrays
|
|
47
76
|
* - modes[x].enabled_tools: project wins if set, else falls back to global
|
|
48
77
|
* - mode_paths: project list is prepended so project paths are searched first
|
|
78
|
+
* - thinking_escalation: project wins as a whole if set, else inherit global
|
|
49
79
|
*/
|
|
50
80
|
export declare function mergeConfigs(global: HooConfig, project: HooConfig): HooConfig;
|
|
51
81
|
/**
|
|
@@ -112,6 +142,20 @@ export declare function parsePlanSections(planContent: string): PlanSections;
|
|
|
112
142
|
export declare function buildApproveMessage(sections: PlanSections): string;
|
|
113
143
|
export declare function setupMode(pi: ExtensionAPI): void;
|
|
114
144
|
export declare function setupAskOptions(pi: ExtensionAPI): void;
|
|
145
|
+
/**
|
|
146
|
+
* Raise the thinking level for the turn(s) following a tool error, then restore
|
|
147
|
+
* it. Opt-in via `thinking_escalation.enabled` in hoo-config.json.
|
|
148
|
+
*
|
|
149
|
+
* Timing model (per agent run):
|
|
150
|
+
* turn N: assistant calls tools → tool_execution_end(s) → turn_end
|
|
151
|
+
* a failing tool here escalates the level used by turn N+1
|
|
152
|
+
* turn N+1: runs escalated; its turn_end decrements the cooldown and, at zero,
|
|
153
|
+
* restores the captured baseline so turn N+2 is fast again
|
|
154
|
+
*
|
|
155
|
+
* `escalatedThisTurn` ensures the error turn's own `turn_end` does not consume a
|
|
156
|
+
* cooldown step — the cooldown counts the turns *after* the failure.
|
|
157
|
+
*/
|
|
158
|
+
export declare function setupThinkingEscalation(pi: ExtensionAPI): void;
|
|
115
159
|
declare function hooCore(pi: ExtensionAPI): void;
|
|
116
160
|
declare namespace hooCore {
|
|
117
161
|
var displayName: string;
|