@kolisachint/hoocode-agent 0.4.57 → 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.
Files changed (53) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/cli/args.d.ts +1 -0
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +5 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +1 -1
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/settings-defaults.d.ts +1 -0
  10. package/dist/core/settings-defaults.d.ts.map +1 -1
  11. package/dist/core/settings-defaults.js +1 -0
  12. package/dist/core/settings-defaults.js.map +1 -1
  13. package/dist/core/settings-manager.d.ts +3 -0
  14. package/dist/core/settings-manager.d.ts.map +1 -1
  15. package/dist/core/settings-manager.js +8 -0
  16. package/dist/core/settings-manager.js.map +1 -1
  17. package/dist/core/system-prompt.d.ts +1 -1
  18. package/dist/core/system-prompt.d.ts.map +1 -1
  19. package/dist/core/system-prompt.js +16 -6
  20. package/dist/core/system-prompt.js.map +1 -1
  21. package/dist/core/tools/edit-diff.d.ts +5 -0
  22. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  23. package/dist/core/tools/edit-diff.js +37 -0
  24. package/dist/core/tools/edit-diff.js.map +1 -1
  25. package/dist/core/tools/edit.d.ts +1 -0
  26. package/dist/core/tools/edit.d.ts.map +1 -1
  27. package/dist/core/tools/edit.js +6 -2
  28. package/dist/core/tools/edit.js.map +1 -1
  29. package/dist/core/tools/find.d.ts.map +1 -1
  30. package/dist/core/tools/find.js +1 -1
  31. package/dist/core/tools/find.js.map +1 -1
  32. package/dist/core/tools/grep.d.ts.map +1 -1
  33. package/dist/core/tools/grep.js +1 -1
  34. package/dist/core/tools/grep.js.map +1 -1
  35. package/dist/core/tools/index.d.ts.map +1 -1
  36. package/dist/core/tools/index.js +6 -0
  37. package/dist/core/tools/index.js.map +1 -1
  38. package/dist/core/tools/ls.d.ts +1 -0
  39. package/dist/core/tools/ls.d.ts.map +1 -1
  40. package/dist/core/tools/ls.js +14 -2
  41. package/dist/core/tools/ls.js.map +1 -1
  42. package/dist/core/tools/todo.d.ts +30 -0
  43. package/dist/core/tools/todo.d.ts.map +1 -0
  44. package/dist/core/tools/todo.js +126 -0
  45. package/dist/core/tools/todo.js.map +1 -0
  46. package/dist/main.d.ts.map +1 -1
  47. package/dist/main.js +7 -0
  48. package/dist/main.js.map +1 -1
  49. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  50. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  51. package/examples/extensions/sandbox/package.json +1 -1
  52. package/examples/extensions/with-deps/package.json +1 -1
  53. package/package.json +4 -4
@@ -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"]}
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuBH,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AA0ZjF,MAAM,WAAW,WAAW;IAC3B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACxC;AAED,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,iBA6Y/D","sourcesContent":["/**\n * Main entry point for the coding agent CLI.\n *\n * This file handles CLI argument parsing and translates them into\n * createAgentSession() options. The SDK does the heavy lifting.\n */\n\nimport { resolve } from \"node:path\";\nimport { createInterface } from \"node:readline\";\nimport { type ImageContent, modelsAreEqual } from \"@kolisachint/hoocode-ai\";\nimport { ProcessTerminal, setKeybindings, TUI } from \"@kolisachint/hoocode-tui\";\nimport chalk from \"chalk\";\nimport { type Args, type Mode, parseArgs, printHelp } from \"./cli/args.js\";\nimport { processFileArguments } from \"./cli/file-processor.js\";\nimport { buildInitialMessage } from \"./cli/initial-message.js\";\nimport { listModels } from \"./cli/list-models.js\";\nimport { selectSession } from \"./cli/session-picker.js\";\nimport { ENV_SESSION_DIR, expandTildePath, getAgentDir, VERSION } from \"./config.js\";\nimport { setAgentCliPaths } from \"./core/agent-manifest-paths.js\";\nimport { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from \"./core/agent-session-runtime.js\";\nimport {\n\ttype AgentSessionRuntimeDiagnostic,\n\tcreateAgentSessionFromServices,\n\tcreateAgentSessionServices,\n} from \"./core/agent-session-services.js\";\nimport { formatNoModelsAvailableMessage } from \"./core/auth-guidance.js\";\nimport { AuthStorage } from \"./core/auth-storage.js\";\nimport { exportFromFile } from \"./core/export-html/index.js\";\nimport type { ExtensionAPI, ExtensionFactory } from \"./core/extensions/types.js\";\nimport { KeybindingsManager } from \"./core/keybindings.js\";\nimport type { ModelRegistry } from \"./core/model-registry.js\";\nimport { resolveCliModel, resolveModelScope, type ScopedModel } from \"./core/model-resolver.js\";\nimport { restoreStdout, takeOverStdout } from \"./core/output-guard.js\";\nimport type { CreateAgentSessionOptions } from \"./core/sdk.js\";\nimport {\n\tformatMissingSessionCwdPrompt,\n\tgetMissingSessionCwdIssue,\n\tMissingSessionCwdError,\n\ttype SessionCwdIssue,\n} from \"./core/session-cwd.js\";\nimport { SessionManager } from \"./core/session-manager.js\";\nimport { SettingsManager } from \"./core/settings-manager.js\";\nimport { printTimings, resetTimings, time } from \"./core/timings.js\";\nimport {\n\tbuildTaskMainPrompt,\n\tcreateTaskOutputToolDefinition,\n\tcreateTaskToolDefinition,\n} from \"./core/tools/subagent.js\";\nimport { runMigrations, showDeprecationWarnings } from \"./migrations.js\";\nimport { InteractiveMode, runPrintMode, runRpcMode } from \"./modes/index.js\";\nimport { ExtensionSelectorComponent } from \"./modes/interactive/components/extension-selector.js\";\nimport { initTheme, stopThemeWatcher } from \"./modes/interactive/theme/theme.js\";\nimport { handleConfigCommand, handlePackageCommand } from \"./package-manager-cli.js\";\nimport { handleResourcesCommand } from \"./resources-cli.js\";\nimport { isLocalPath } from \"./utils/paths.js\";\n\n/**\n * Read all content from piped stdin.\n * Returns undefined if stdin is a TTY (interactive terminal).\n */\nasync function readPipedStdin(): Promise<string | undefined> {\n\t// If stdin is a TTY, we're running interactively - don't read stdin\n\tif (process.stdin.isTTY) {\n\t\treturn undefined;\n\t}\n\n\treturn new Promise((resolve) => {\n\t\tlet data = \"\";\n\t\tprocess.stdin.setEncoding(\"utf8\");\n\t\tprocess.stdin.on(\"data\", (chunk) => {\n\t\t\tdata += chunk;\n\t\t});\n\t\tprocess.stdin.on(\"end\", () => {\n\t\t\tresolve(data.trim() || undefined);\n\t\t});\n\t\tprocess.stdin.resume();\n\t});\n}\n\nfunction collectSettingsDiagnostics(\n\tsettingsManager: SettingsManager,\n\tcontext: string,\n): AgentSessionRuntimeDiagnostic[] {\n\treturn settingsManager.drainErrors().map(({ scope, error }) => ({\n\t\ttype: \"warning\",\n\t\tmessage: `(${context}, ${scope} settings) ${error.message}`,\n\t}));\n}\n\nfunction reportDiagnostics(diagnostics: readonly AgentSessionRuntimeDiagnostic[]): void {\n\tfor (const diagnostic of diagnostics) {\n\t\tconst color = diagnostic.type === \"error\" ? chalk.red : diagnostic.type === \"warning\" ? chalk.yellow : chalk.dim;\n\t\tconst prefix = diagnostic.type === \"error\" ? \"Error: \" : diagnostic.type === \"warning\" ? \"Warning: \" : \"\";\n\t\tconsole.error(color(`${prefix}${diagnostic.message}`));\n\t}\n}\n\nfunction isTruthyEnvFlag(value: string | undefined): boolean {\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\ntype AppMode = \"interactive\" | \"print\" | \"json\" | \"rpc\";\n\nfunction resolveAppMode(parsed: Args, stdinIsTTY: boolean): AppMode {\n\tif (parsed.mode === \"rpc\") {\n\t\treturn \"rpc\";\n\t}\n\tif (parsed.mode === \"json\") {\n\t\treturn \"json\";\n\t}\n\tif (parsed.print || !stdinIsTTY) {\n\t\treturn \"print\";\n\t}\n\treturn \"interactive\";\n}\n\nfunction toPrintOutputMode(appMode: AppMode): Exclude<Mode, \"rpc\"> {\n\treturn appMode === \"json\" ? \"json\" : \"text\";\n}\n\nasync function prepareInitialMessage(\n\tparsed: Args,\n\tautoResizeImages: boolean,\n\tstdinContent?: string,\n): Promise<{\n\tinitialMessage?: string;\n\tinitialImages?: ImageContent[];\n}> {\n\tif (parsed.fileArgs.length === 0) {\n\t\treturn buildInitialMessage({ parsed, stdinContent });\n\t}\n\n\tconst { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages });\n\treturn buildInitialMessage({\n\t\tparsed,\n\t\tfileText: text,\n\t\tfileImages: images,\n\t\tstdinContent,\n\t});\n}\n\n/** Result from resolving a session argument */\ntype ResolvedSession =\n\t| { type: \"path\"; path: string } // Direct file path\n\t| { type: \"local\"; path: string } // Found in current project\n\t| { type: \"global\"; path: string; cwd: string } // Found in different project\n\t| { type: \"not_found\"; arg: string }; // Not found anywhere\n\n/**\n * Resolve a session argument to a file path.\n * If it looks like a path, use as-is. Otherwise try to match as session ID prefix.\n */\nasync function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): Promise<ResolvedSession> {\n\t// If it looks like a file path, use as-is\n\tif (sessionArg.includes(\"/\") || sessionArg.includes(\"\\\\\") || sessionArg.endsWith(\".jsonl\")) {\n\t\treturn { type: \"path\", path: sessionArg };\n\t}\n\n\t// Try to match as session ID in current project first\n\tconst localSessions = await SessionManager.list(cwd, sessionDir);\n\tconst localMatches = localSessions.filter((s) => s.id.startsWith(sessionArg));\n\n\tif (localMatches.length >= 1) {\n\t\treturn { type: \"local\", path: localMatches[0].path };\n\t}\n\n\t// Try global search across all projects\n\tconst allSessions = await SessionManager.listAll();\n\tconst globalMatches = allSessions.filter((s) => s.id.startsWith(sessionArg));\n\n\tif (globalMatches.length >= 1) {\n\t\tconst match = globalMatches[0];\n\t\treturn { type: \"global\", path: match.path, cwd: match.cwd };\n\t}\n\n\t// Not found anywhere\n\treturn { type: \"not_found\", arg: sessionArg };\n}\n\n/** Prompt user for yes/no confirmation */\nasync function promptConfirm(message: string): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tconst rl = createInterface({\n\t\t\tinput: process.stdin,\n\t\t\toutput: process.stdout,\n\t\t});\n\t\trl.question(`${message} [y/N] `, (answer) => {\n\t\t\trl.close();\n\t\t\tresolve(answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\");\n\t\t});\n\t});\n}\n\nfunction validateForkFlags(parsed: Args): void {\n\tif (!parsed.fork) return;\n\n\tconst conflictingFlags = [\n\t\tparsed.session ? \"--session\" : undefined,\n\t\tparsed.continue ? \"--continue\" : undefined,\n\t\tparsed.resume ? \"--resume\" : undefined,\n\t\tparsed.noSession ? \"--no-session\" : undefined,\n\t].filter((flag): flag is string => flag !== undefined);\n\n\tif (conflictingFlags.length > 0) {\n\t\tconsole.error(chalk.red(`Error: --fork cannot be combined with ${conflictingFlags.join(\", \")}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction forkSessionOrExit(sourcePath: string, cwd: string, sessionDir?: string): SessionManager {\n\ttry {\n\t\treturn SessionManager.forkFrom(sourcePath, cwd, sessionDir);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nasync function createSessionManager(\n\tparsed: Args,\n\tcwd: string,\n\tsessionDir: string | undefined,\n\tsettingsManager: SettingsManager,\n): Promise<SessionManager> {\n\tif (parsed.noSession) {\n\t\treturn SessionManager.inMemory();\n\t}\n\n\tif (parsed.fork) {\n\t\tconst resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\tcase \"global\":\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir);\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.session) {\n\t\tconst resolved = await resolveSessionPath(parsed.session, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\t\treturn SessionManager.open(resolved.path, sessionDir);\n\n\t\t\tcase \"global\": {\n\t\t\t\tconsole.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));\n\t\t\t\tconst shouldFork = await promptConfirm(\"Fork this session into current directory?\");\n\t\t\t\tif (!shouldFork) {\n\t\t\t\t\tconsole.log(chalk.dim(\"Aborted.\"));\n\t\t\t\t\tprocess.exit(0);\n\t\t\t\t}\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir);\n\t\t\t}\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.resume) {\n\t\tinitTheme(settingsManager.getTheme(), true);\n\t\ttry {\n\t\t\tconst selectedPath = await selectSession(\n\t\t\t\t(onProgress) => SessionManager.list(cwd, sessionDir, onProgress),\n\t\t\t\tSessionManager.listAll,\n\t\t\t);\n\t\t\tif (!selectedPath) {\n\t\t\t\tconsole.log(chalk.dim(\"No session selected\"));\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\treturn SessionManager.open(selectedPath, sessionDir);\n\t\t} finally {\n\t\t\tstopThemeWatcher();\n\t\t}\n\t}\n\n\tif (parsed.continue) {\n\t\treturn SessionManager.continueRecent(cwd, sessionDir);\n\t}\n\n\treturn SessionManager.create(cwd, sessionDir);\n}\n\nfunction buildSessionOptions(\n\tparsed: Args,\n\tscopedModels: ScopedModel[],\n\thasExistingSession: boolean,\n\tmodelRegistry: ModelRegistry,\n\tsettingsManager: SettingsManager,\n): {\n\toptions: CreateAgentSessionOptions;\n\tcliThinkingFromModel: boolean;\n\tdiagnostics: AgentSessionRuntimeDiagnostic[];\n} {\n\tconst options: CreateAgentSessionOptions = {};\n\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\tlet cliThinkingFromModel = false;\n\n\t// Model from CLI\n\t// - supports --provider <name> --model <pattern>\n\t// - supports --model <provider>/<pattern>\n\tif (parsed.model) {\n\t\tconst resolved = resolveCliModel({\n\t\t\tcliProvider: parsed.provider,\n\t\t\tcliModel: parsed.model,\n\t\t\tmodelRegistry,\n\t\t});\n\t\tif (resolved.warning) {\n\t\t\tdiagnostics.push({ type: \"warning\", message: resolved.warning });\n\t\t}\n\t\tif (resolved.error) {\n\t\t\tdiagnostics.push({ type: \"error\", message: resolved.error });\n\t\t}\n\t\tif (resolved.model) {\n\t\t\toptions.model = resolved.model;\n\t\t\t// Allow \"--model <pattern>:<thinking>\" as a shorthand.\n\t\t\t// Explicit --thinking still takes precedence (applied later).\n\t\t\tif (!parsed.thinking && resolved.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = resolved.thinkingLevel;\n\t\t\t\tcliThinkingFromModel = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!options.model && scopedModels.length > 0 && !hasExistingSession) {\n\t\t// Check if saved default is in scoped models - use it if so, otherwise first scoped model\n\t\tconst savedProvider = settingsManager.getDefaultProvider();\n\t\tconst savedModelId = settingsManager.getDefaultModel();\n\t\tconst savedModel = savedProvider && savedModelId ? modelRegistry.find(savedProvider, savedModelId) : undefined;\n\t\tconst savedInScope = savedModel ? scopedModels.find((sm) => modelsAreEqual(sm.model, savedModel)) : undefined;\n\n\t\tif (savedInScope) {\n\t\t\toptions.model = savedInScope.model;\n\t\t\t// Use thinking level from scoped model config if explicitly set\n\t\t\tif (!parsed.thinking && savedInScope.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = savedInScope.thinkingLevel;\n\t\t\t}\n\t\t} else {\n\t\t\toptions.model = scopedModels[0].model;\n\t\t\t// Use thinking level from first scoped model if explicitly set\n\t\t\tif (!parsed.thinking && scopedModels[0].thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = scopedModels[0].thinkingLevel;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Thinking level from CLI (takes precedence over scoped model thinking levels set above)\n\tif (parsed.thinking) {\n\t\toptions.thinkingLevel = parsed.thinking;\n\t}\n\n\t// Scoped models for Ctrl+P cycling\n\t// Keep thinking level undefined when not explicitly set in the model pattern.\n\t// Undefined means \"inherit current session thinking level\" during cycling.\n\tif (scopedModels.length > 0) {\n\t\toptions.scopedModels = scopedModels.map((sm) => ({\n\t\t\tmodel: sm.model,\n\t\t\tthinkingLevel: sm.thinkingLevel,\n\t\t}));\n\t}\n\n\t// API key from CLI - set in authStorage\n\t// (handled by caller before createAgentSession)\n\n\t// Tools\n\tif (parsed.noTools) {\n\t\toptions.noTools = \"all\";\n\t} else if (parsed.noBuiltinTools) {\n\t\toptions.noTools = \"builtin\";\n\t}\n\tif (parsed.tools) {\n\t\toptions.tools = [...parsed.tools];\n\t}\n\n\t// Optional Task (subagent) tool: opt-in via --enable-subagents flag or the enableSubagent setting.\n\t// Registered as a custom tool; respects --tools/--no-tools allowlists like any other tool.\n\t// Never register it inside a spawned subagent (--task-id present): subagents must not\n\t// recursively dispatch, even if a project's enableSubagent setting is on.\n\tconst isSubagentChild = parsed.taskId !== undefined;\n\tif (!isSubagentChild && (parsed.subagent ?? settingsManager.getEnableSubagent())) {\n\t\toptions.customTools = [\n\t\t\t...(options.customTools ?? []),\n\t\t\tcreateTaskToolDefinition(),\n\t\t\tcreateTaskOutputToolDefinition(),\n\t\t];\n\t}\n\n\treturn { options, cliThinkingFromModel, diagnostics };\n}\n\nfunction resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | undefined {\n\treturn paths?.map((value) => (isLocalPath(value) ? resolve(cwd, value) : value));\n}\n\nasync function promptForMissingSessionCwd(\n\tissue: SessionCwdIssue,\n\tsettingsManager: SettingsManager,\n): Promise<string | undefined> {\n\tinitTheme(settingsManager.getTheme());\n\tsetKeybindings(KeybindingsManager.create());\n\n\treturn new Promise((resolve) => {\n\t\tconst ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor());\n\t\tui.setClearOnShrink(settingsManager.getClearOnShrink());\n\n\t\tlet settled = false;\n\t\tconst finish = (result: string | undefined) => {\n\t\t\tif (settled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettled = true;\n\t\t\tui.stop();\n\t\t\tresolve(result);\n\t\t};\n\n\t\tconst selector = new ExtensionSelectorComponent(\n\t\t\tformatMissingSessionCwdPrompt(issue),\n\t\t\t[\"Continue\", \"Cancel\"],\n\t\t\t(option) => finish(option === \"Continue\" ? issue.fallbackCwd : undefined),\n\t\t\t() => finish(undefined),\n\t\t\t{ tui: ui },\n\t\t);\n\t\tui.addChild(selector);\n\t\tui.setFocus(selector);\n\t\tui.start();\n\t});\n}\n\nexport interface MainOptions {\n\textensionFactories?: ExtensionFactory[];\n}\n\nexport async function main(args: string[], options?: MainOptions) {\n\tresetTimings();\n\tconst offlineMode =\n\t\targs.includes(\"--offline\") || isTruthyEnvFlag(process.env.HOOCODE_OFFLINE ?? process.env.PI_OFFLINE);\n\tif (offlineMode) {\n\t\tprocess.env.HOOCODE_OFFLINE = \"1\";\n\t\tprocess.env.HOOCODE_SKIP_VERSION_CHECK = \"1\";\n\t}\n\n\tif (await handlePackageCommand(args)) {\n\t\treturn;\n\t}\n\n\tif (await handleConfigCommand(args)) {\n\t\treturn;\n\t}\n\n\tif (await handleResourcesCommand(args)) {\n\t\treturn;\n\t}\n\n\tconst parsed = parseArgs(args);\n\tif (parsed.diagnostics.length > 0) {\n\t\tfor (const d of parsed.diagnostics) {\n\t\t\tconst color = d.type === \"error\" ? chalk.red : chalk.yellow;\n\t\t\tconsole.error(color(`${d.type === \"error\" ? \"Error\" : \"Warning\"}: ${d.message}`));\n\t\t}\n\t\tif (parsed.diagnostics.some((d) => d.type === \"error\")) {\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\ttime(\"parseArgs\");\n\tlet appMode = resolveAppMode(parsed, process.stdin.isTTY);\n\tconst shouldTakeOverStdout = appMode !== \"interactive\";\n\tif (shouldTakeOverStdout) {\n\t\ttakeOverStdout();\n\t}\n\n\tif (parsed.version) {\n\t\tconsole.log(VERSION);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.export) {\n\t\tlet result: string;\n\t\ttry {\n\t\t\tconst outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;\n\t\t\tresult = await exportFromFile(parsed.export, outputPath);\n\t\t} catch (error: unknown) {\n\t\t\tconst message = error instanceof Error ? error.message : \"Failed to export session\";\n\t\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconsole.log(`Exported to: ${result}`);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.mode === \"rpc\" && parsed.fileArgs.length > 0) {\n\t\tconsole.error(chalk.red(\"Error: @file arguments are not supported in RPC mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tvalidateForkFlags(parsed);\n\n\t// Run migrations (pass cwd for project-local migrations)\n\tconst { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd());\n\ttime(\"runMigrations\");\n\n\tconst cwd = process.cwd();\n\tconst agentDir = getAgentDir();\n\tconst startupSettingsManager = SettingsManager.create(cwd, agentDir);\n\treportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, \"startup session lookup\"));\n\n\t// Decide the final runtime cwd before creating cwd-bound runtime services.\n\t// --session and --resume may select a session from another project, so project-local\n\t// settings, resources, provider registrations, and models must be resolved only after\n\t// the target session cwd is known. The startup-cwd settings manager is used only for\n\t// sessionDir lookup during session selection.\n\tconst envSessionDir = process.env[ENV_SESSION_DIR];\n\tconst sessionDir =\n\t\tparsed.sessionDir ??\n\t\t(envSessionDir ? expandTildePath(envSessionDir) : undefined) ??\n\t\tstartupSettingsManager.getSessionDir();\n\tlet sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager);\n\tconst missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd);\n\tif (missingSessionCwdIssue) {\n\t\tif (appMode === \"interactive\") {\n\t\t\tconst selectedCwd = await promptForMissingSessionCwd(missingSessionCwdIssue, startupSettingsManager);\n\t\t\tif (!selectedCwd) {\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\tsessionManager = SessionManager.open(missingSessionCwdIssue.sessionFile!, sessionDir, selectedCwd);\n\t\t} else {\n\t\t\tconsole.error(chalk.red(new MissingSessionCwdError(missingSessionCwdIssue).message));\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\ttime(\"createSessionManager\");\n\n\tconst resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);\n\tconst resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);\n\tconst resolvedAgentPaths = resolveCliPaths(cwd, parsed.agents);\n\tconst resolvedPromptTemplatePaths = resolveCliPaths(cwd, parsed.promptTemplates);\n\tconst resolvedSlashCommandPaths = resolveCliPaths(cwd, parsed.slashCommands);\n\tconst resolvedThemePaths = resolveCliPaths(cwd, parsed.themes);\n\n\t// Populate the module-level CLI agent store before any session is created.\n\tsetAgentCliPaths(resolvedAgentPaths ?? []);\n\n\t// Synthetic factory: feed CLI --mode-path values into the extension runtime\n\t// so hoo-core (and any other extension that reads pi.getModeSearchPaths)\n\t// sees them alongside extension-registered dirs.\n\tconst cliModePaths = parsed.modePaths ?? [];\n\tconst cliResourcePathFactories: ExtensionFactory[] =\n\t\tcliModePaths.length === 0\n\t\t\t? []\n\t\t\t: [\n\t\t\t\t\tObject.assign(\n\t\t\t\t\t\t(pi: ExtensionAPI) => {\n\t\t\t\t\t\t\tfor (const p of cliModePaths) pi.addModeSearchPath(p);\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ internal: true },\n\t\t\t\t\t),\n\t\t\t\t];\n\tconst allExtensionFactories: ExtensionFactory[] = [\n\t\t...cliResourcePathFactories,\n\t\t...(options?.extensionFactories ?? []),\n\t];\n\tconst authStorage = AuthStorage.create();\n\tconst createRuntime: CreateAgentSessionRuntimeFactory = async ({\n\t\tcwd,\n\t\tagentDir,\n\t\tsessionManager,\n\t\tsessionStartEvent,\n\t}) => {\n\t\tconst services = await createAgentSessionServices({\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tauthStorage,\n\t\t\textensionFlagValues: parsed.unknownFlags,\n\t\t\tresourceLoaderOptions: {\n\t\t\t\tadditionalExtensionPaths: resolvedExtensionPaths,\n\t\t\t\tadditionalSkillPaths: resolvedSkillPaths,\n\t\t\t\tadditionalPromptTemplatePaths: resolvedPromptTemplatePaths,\n\t\t\t\tadditionalSlashCommandPaths: resolvedSlashCommandPaths,\n\t\t\t\tadditionalThemePaths: resolvedThemePaths,\n\t\t\t\tnoExtensions: parsed.noExtensions,\n\t\t\t\tnoSkills: parsed.noSkills,\n\t\t\t\tnoPromptTemplates: parsed.noPromptTemplates,\n\t\t\t\tnoSlashCommands: parsed.noSlashCommands,\n\t\t\t\tnoThemes: parsed.noThemes,\n\t\t\t\tnoContextFiles: parsed.noContextFiles,\n\t\t\t\tsystemPrompt: parsed.systemPrompt,\n\t\t\t\textensionFactories: allExtensionFactories,\n\t\t\t},\n\t\t});\n\t\tconst { settingsManager, modelRegistry, resourceLoader } = services;\n\t\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [\n\t\t\t...services.diagnostics,\n\t\t\t...collectSettingsDiagnostics(settingsManager, \"runtime creation\"),\n\t\t\t...resourceLoader.getExtensions().errors.map(({ path, error }) => ({\n\t\t\t\ttype: \"error\" as const,\n\t\t\t\tmessage: `Failed to load extension \"${path}\": ${error}`,\n\t\t\t})),\n\t\t];\n\n\t\t// When subagent tooling is enabled, append the main session subagent instructions.\n\t\tif (parsed.subagent ?? settingsManager.getEnableSubagent()) {\n\t\t\tresourceLoader.addAppendSystemPrompt(buildTaskMainPrompt());\n\t\t}\n\n\t\tconst modelPatterns = parsed.models ?? settingsManager.getEnabledModels();\n\t\tconst scopedModels =\n\t\t\tmodelPatterns && modelPatterns.length > 0 ? await resolveModelScope(modelPatterns, modelRegistry) : [];\n\t\tconst {\n\t\t\toptions: sessionOptions,\n\t\t\tcliThinkingFromModel,\n\t\t\tdiagnostics: sessionOptionDiagnostics,\n\t\t} = buildSessionOptions(\n\t\t\tparsed,\n\t\t\tscopedModels,\n\t\t\tsessionManager.buildSessionContext().messages.length > 0,\n\t\t\tmodelRegistry,\n\t\t\tsettingsManager,\n\t\t);\n\t\tdiagnostics.push(...sessionOptionDiagnostics);\n\n\t\tif (parsed.apiKey) {\n\t\t\tif (!sessionOptions.model) {\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\tmessage: \"--api-key requires a model to be specified via --model, --provider/--model, or --models\",\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tauthStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);\n\t\t\t}\n\t\t}\n\n\t\tconst created = await createAgentSessionFromServices({\n\t\t\tservices,\n\t\t\tsessionManager,\n\t\t\tsessionStartEvent,\n\t\t\tmodel: sessionOptions.model,\n\t\t\tthinkingLevel: sessionOptions.thinkingLevel,\n\t\t\tscopedModels: sessionOptions.scopedModels,\n\t\t\ttools: sessionOptions.tools,\n\t\t\tnoTools: sessionOptions.noTools,\n\t\t\tcustomTools: sessionOptions.customTools,\n\t\t});\n\t\tconst cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;\n\t\tif (created.session.model && cliThinkingOverride) {\n\t\t\tcreated.session.setThinkingLevel(created.session.thinkingLevel);\n\t\t}\n\n\t\treturn {\n\t\t\t...created,\n\t\t\tservices,\n\t\t\tdiagnostics,\n\t\t};\n\t};\n\ttime(\"createRuntime\");\n\tconst runtime = await createAgentSessionRuntime(createRuntime, {\n\t\tcwd: sessionManager.getCwd(),\n\t\tagentDir,\n\t\tsessionManager,\n\t});\n\tconst { services, session, modelFallbackMessage } = runtime;\n\tconst { settingsManager, modelRegistry, resourceLoader } = services;\n\n\tif (parsed.help) {\n\t\tconst extensionFlags = resourceLoader\n\t\t\t.getExtensions()\n\t\t\t.extensions.flatMap((extension) => Array.from(extension.flags.values()));\n\t\tprintHelp(extensionFlags);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.listModels !== undefined) {\n\t\tconst searchPattern = typeof parsed.listModels === \"string\" ? parsed.listModels : undefined;\n\t\tawait listModels(modelRegistry, searchPattern);\n\t\tprocess.exit(0);\n\t}\n\n\t// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC\n\tlet stdinContent: string | undefined;\n\tif (appMode !== \"rpc\") {\n\t\tstdinContent = await readPipedStdin();\n\t\tif (stdinContent !== undefined && appMode === \"interactive\") {\n\t\t\tappMode = \"print\";\n\t\t}\n\t}\n\ttime(\"readPipedStdin\");\n\n\tconst { initialMessage, initialImages } = await prepareInitialMessage(\n\t\tparsed,\n\t\tsettingsManager.getImageAutoResize(),\n\t\tstdinContent,\n\t);\n\ttime(\"prepareInitialMessage\");\n\tinitTheme(settingsManager.getTheme(), appMode === \"interactive\");\n\ttime(\"initTheme\");\n\n\t// Show deprecation warnings in interactive mode\n\tif (appMode === \"interactive\" && deprecationWarnings.length > 0) {\n\t\tawait showDeprecationWarnings(deprecationWarnings);\n\t}\n\n\tconst scopedModels = [...session.scopedModels];\n\ttime(\"resolveModelScope\");\n\treportDiagnostics(runtime.diagnostics);\n\tif (runtime.diagnostics.some((diagnostic) => diagnostic.type === \"error\")) {\n\t\tprocess.exit(1);\n\t}\n\ttime(\"createAgentSession\");\n\n\tif (appMode !== \"interactive\" && !session.model) {\n\t\tconsole.error(chalk.red(formatNoModelsAvailableMessage()));\n\t\tprocess.exit(1);\n\t}\n\n\tconst startupBenchmark = isTruthyEnvFlag(process.env.HOOCODE_STARTUP_BENCHMARK ?? process.env.PI_STARTUP_BENCHMARK);\n\tif (startupBenchmark && appMode !== \"interactive\") {\n\t\tconsole.error(chalk.red(\"Error: HOOCODE_STARTUP_BENCHMARK only supports interactive mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tif (appMode === \"rpc\") {\n\t\tprintTimings();\n\t\tawait runRpcMode(runtime);\n\t} else if (appMode === \"interactive\") {\n\t\tif (scopedModels.length > 0 && (parsed.verbose || !settingsManager.getQuietStartup())) {\n\t\t\tconst modelList = scopedModels\n\t\t\t\t.map((sm) => {\n\t\t\t\t\tconst thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : \"\";\n\t\t\t\t\treturn `${sm.model.id}${thinkingStr}`;\n\t\t\t\t})\n\t\t\t\t.join(\", \");\n\t\t\tconsole.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray(\"(Ctrl+P to cycle)\")}`));\n\t\t}\n\n\t\tconst interactiveMode = new InteractiveMode(runtime, {\n\t\t\tmigratedProviders,\n\t\t\tmodelFallbackMessage,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\tinitialMessages: parsed.messages,\n\t\t\tverbose: parsed.verbose,\n\t\t});\n\n\t\t// Optional hooteams bridge. `--team auto` discovers a config, spawns a\n\t\t// local hooteams child on a free port, and proceeds as if its URL had\n\t\t// been passed; the child is reaped on exit (clean or signal) via a\n\t\t// process \"exit\" hook plus the explicit stop below.\n\t\tlet autoTeam: { url: string; stop(): Promise<void> } | undefined;\n\t\tlet teamUrl = parsed.team;\n\t\tif (teamUrl === \"auto\") {\n\t\t\tconst { startAutoTeam } = await import(\"./core/team-auto.js\");\n\t\t\ttry {\n\t\t\t\tautoTeam = await startAutoTeam(process.cwd(), { log: (message) => console.log(chalk.dim(message)) });\n\t\t\t\tteamUrl = autoTeam.url;\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(chalk.red(error instanceof Error ? error.message : String(error)));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\n\t\t// Team mirror + client. Fire-and-forget: connect failures and drops warn\n\t\t// in the background and never block the main agent. Warnings go through\n\t\t// the chat, not console.error: a raw stderr write while the TUI owns the\n\t\t// screen scribbles over the render and can leave the editor looking\n\t\t// frozen. The same connection powers team focus / nudge / attach.\n\t\tlet teamView: { stop(): void } | undefined;\n\t\tif (teamUrl) {\n\t\t\tconst { connectTeamView } = await import(\"./core/team-view.js\");\n\t\t\tconst teamClient = connectTeamView(teamUrl, {\n\t\t\t\twarn: (message) => interactiveMode.showWarning(message),\n\t\t\t});\n\t\t\tteamView = teamClient;\n\t\t\tinteractiveMode.attachTeamClient(teamClient);\n\t\t}\n\t\tif (startupBenchmark) {\n\t\t\tawait interactiveMode.init();\n\t\t\ttime(\"interactiveMode.init\");\n\t\t\tprintTimings();\n\t\t\tteamView?.stop();\n\t\t\tawait autoTeam?.stop();\n\t\t\tinteractiveMode.stop();\n\t\t\tstopThemeWatcher();\n\t\t\tif (process.stdout.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tif (process.stderr.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stderr.once(\"drain\", resolve));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tprintTimings();\n\t\ttry {\n\t\t\tawait interactiveMode.run();\n\t\t} finally {\n\t\t\tteamView?.stop();\n\t\t\tawait autoTeam?.stop();\n\t\t}\n\t} else {\n\t\tprintTimings();\n\t\tconst exitCode = await runPrintMode(runtime, {\n\t\t\tmode: toPrintOutputMode(appMode),\n\t\t\tmessages: parsed.messages,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\ttaskId: parsed.taskId,\n\t\t\tmaxTurns: parsed.maxTurns,\n\t\t});\n\t\tstopThemeWatcher();\n\t\trestoreStdout();\n\t\tif (exitCode !== 0) {\n\t\t\tprocess.exitCode = exitCode;\n\t\t}\n\t\t// Spawned subagents (run with a task id) must exit promptly once their work is\n\t\t// done and result.json is written. The child's runtime can leave handles open\n\t\t// (e.g. MCP client connections) that keep the event loop alive, so a natural\n\t\t// exit may never happen. When that occurs the parent lifeguard SIGKILLs the idle\n\t\t// child at the 60s heartbeat threshold and misreports an already-completed task\n\t\t// as \"stalled\". Force a clean exit after draining output to avoid that false stall.\n\t\tconst ranAsSubagent = typeof parsed.taskId === \"string\" && parsed.taskId.length > 0;\n\t\tif (ranAsSubagent) {\n\t\t\tif (process.stdout.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tif (process.stderr.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stderr.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tprocess.exit(exitCode);\n\t\t}\n\t\treturn;\n\t}\n}\n"]}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuBH,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAkajF,MAAM,WAAW,WAAW;IAC3B,kBAAkB,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACxC;AAED,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,WAAW,iBA6Y/D","sourcesContent":["/**\n * Main entry point for the coding agent CLI.\n *\n * This file handles CLI argument parsing and translates them into\n * createAgentSession() options. The SDK does the heavy lifting.\n */\n\nimport { resolve } from \"node:path\";\nimport { createInterface } from \"node:readline\";\nimport { type ImageContent, modelsAreEqual } from \"@kolisachint/hoocode-ai\";\nimport { ProcessTerminal, setKeybindings, TUI } from \"@kolisachint/hoocode-tui\";\nimport chalk from \"chalk\";\nimport { type Args, type Mode, parseArgs, printHelp } from \"./cli/args.js\";\nimport { processFileArguments } from \"./cli/file-processor.js\";\nimport { buildInitialMessage } from \"./cli/initial-message.js\";\nimport { listModels } from \"./cli/list-models.js\";\nimport { selectSession } from \"./cli/session-picker.js\";\nimport { ENV_SESSION_DIR, expandTildePath, getAgentDir, VERSION } from \"./config.js\";\nimport { setAgentCliPaths } from \"./core/agent-manifest-paths.js\";\nimport { type CreateAgentSessionRuntimeFactory, createAgentSessionRuntime } from \"./core/agent-session-runtime.js\";\nimport {\n\ttype AgentSessionRuntimeDiagnostic,\n\tcreateAgentSessionFromServices,\n\tcreateAgentSessionServices,\n} from \"./core/agent-session-services.js\";\nimport { formatNoModelsAvailableMessage } from \"./core/auth-guidance.js\";\nimport { AuthStorage } from \"./core/auth-storage.js\";\nimport { exportFromFile } from \"./core/export-html/index.js\";\nimport type { ExtensionAPI, ExtensionFactory } from \"./core/extensions/types.js\";\nimport { KeybindingsManager } from \"./core/keybindings.js\";\nimport type { ModelRegistry } from \"./core/model-registry.js\";\nimport { resolveCliModel, resolveModelScope, type ScopedModel } from \"./core/model-resolver.js\";\nimport { restoreStdout, takeOverStdout } from \"./core/output-guard.js\";\nimport type { CreateAgentSessionOptions } from \"./core/sdk.js\";\nimport {\n\tformatMissingSessionCwdPrompt,\n\tgetMissingSessionCwdIssue,\n\tMissingSessionCwdError,\n\ttype SessionCwdIssue,\n} from \"./core/session-cwd.js\";\nimport { SessionManager } from \"./core/session-manager.js\";\nimport { SettingsManager } from \"./core/settings-manager.js\";\nimport { printTimings, resetTimings, time } from \"./core/timings.js\";\nimport {\n\tbuildTaskMainPrompt,\n\tcreateTaskOutputToolDefinition,\n\tcreateTaskToolDefinition,\n} from \"./core/tools/subagent.js\";\nimport { createTodoWriteToolDefinition } from \"./core/tools/todo.js\";\nimport { runMigrations, showDeprecationWarnings } from \"./migrations.js\";\nimport { InteractiveMode, runPrintMode, runRpcMode } from \"./modes/index.js\";\nimport { ExtensionSelectorComponent } from \"./modes/interactive/components/extension-selector.js\";\nimport { initTheme, stopThemeWatcher } from \"./modes/interactive/theme/theme.js\";\nimport { handleConfigCommand, handlePackageCommand } from \"./package-manager-cli.js\";\nimport { handleResourcesCommand } from \"./resources-cli.js\";\nimport { isLocalPath } from \"./utils/paths.js\";\n\n/**\n * Read all content from piped stdin.\n * Returns undefined if stdin is a TTY (interactive terminal).\n */\nasync function readPipedStdin(): Promise<string | undefined> {\n\t// If stdin is a TTY, we're running interactively - don't read stdin\n\tif (process.stdin.isTTY) {\n\t\treturn undefined;\n\t}\n\n\treturn new Promise((resolve) => {\n\t\tlet data = \"\";\n\t\tprocess.stdin.setEncoding(\"utf8\");\n\t\tprocess.stdin.on(\"data\", (chunk) => {\n\t\t\tdata += chunk;\n\t\t});\n\t\tprocess.stdin.on(\"end\", () => {\n\t\t\tresolve(data.trim() || undefined);\n\t\t});\n\t\tprocess.stdin.resume();\n\t});\n}\n\nfunction collectSettingsDiagnostics(\n\tsettingsManager: SettingsManager,\n\tcontext: string,\n): AgentSessionRuntimeDiagnostic[] {\n\treturn settingsManager.drainErrors().map(({ scope, error }) => ({\n\t\ttype: \"warning\",\n\t\tmessage: `(${context}, ${scope} settings) ${error.message}`,\n\t}));\n}\n\nfunction reportDiagnostics(diagnostics: readonly AgentSessionRuntimeDiagnostic[]): void {\n\tfor (const diagnostic of diagnostics) {\n\t\tconst color = diagnostic.type === \"error\" ? chalk.red : diagnostic.type === \"warning\" ? chalk.yellow : chalk.dim;\n\t\tconst prefix = diagnostic.type === \"error\" ? \"Error: \" : diagnostic.type === \"warning\" ? \"Warning: \" : \"\";\n\t\tconsole.error(color(`${prefix}${diagnostic.message}`));\n\t}\n}\n\nfunction isTruthyEnvFlag(value: string | undefined): boolean {\n\tif (!value) return false;\n\treturn value === \"1\" || value.toLowerCase() === \"true\" || value.toLowerCase() === \"yes\";\n}\n\ntype AppMode = \"interactive\" | \"print\" | \"json\" | \"rpc\";\n\nfunction resolveAppMode(parsed: Args, stdinIsTTY: boolean): AppMode {\n\tif (parsed.mode === \"rpc\") {\n\t\treturn \"rpc\";\n\t}\n\tif (parsed.mode === \"json\") {\n\t\treturn \"json\";\n\t}\n\tif (parsed.print || !stdinIsTTY) {\n\t\treturn \"print\";\n\t}\n\treturn \"interactive\";\n}\n\nfunction toPrintOutputMode(appMode: AppMode): Exclude<Mode, \"rpc\"> {\n\treturn appMode === \"json\" ? \"json\" : \"text\";\n}\n\nasync function prepareInitialMessage(\n\tparsed: Args,\n\tautoResizeImages: boolean,\n\tstdinContent?: string,\n): Promise<{\n\tinitialMessage?: string;\n\tinitialImages?: ImageContent[];\n}> {\n\tif (parsed.fileArgs.length === 0) {\n\t\treturn buildInitialMessage({ parsed, stdinContent });\n\t}\n\n\tconst { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages });\n\treturn buildInitialMessage({\n\t\tparsed,\n\t\tfileText: text,\n\t\tfileImages: images,\n\t\tstdinContent,\n\t});\n}\n\n/** Result from resolving a session argument */\ntype ResolvedSession =\n\t| { type: \"path\"; path: string } // Direct file path\n\t| { type: \"local\"; path: string } // Found in current project\n\t| { type: \"global\"; path: string; cwd: string } // Found in different project\n\t| { type: \"not_found\"; arg: string }; // Not found anywhere\n\n/**\n * Resolve a session argument to a file path.\n * If it looks like a path, use as-is. Otherwise try to match as session ID prefix.\n */\nasync function resolveSessionPath(sessionArg: string, cwd: string, sessionDir?: string): Promise<ResolvedSession> {\n\t// If it looks like a file path, use as-is\n\tif (sessionArg.includes(\"/\") || sessionArg.includes(\"\\\\\") || sessionArg.endsWith(\".jsonl\")) {\n\t\treturn { type: \"path\", path: sessionArg };\n\t}\n\n\t// Try to match as session ID in current project first\n\tconst localSessions = await SessionManager.list(cwd, sessionDir);\n\tconst localMatches = localSessions.filter((s) => s.id.startsWith(sessionArg));\n\n\tif (localMatches.length >= 1) {\n\t\treturn { type: \"local\", path: localMatches[0].path };\n\t}\n\n\t// Try global search across all projects\n\tconst allSessions = await SessionManager.listAll();\n\tconst globalMatches = allSessions.filter((s) => s.id.startsWith(sessionArg));\n\n\tif (globalMatches.length >= 1) {\n\t\tconst match = globalMatches[0];\n\t\treturn { type: \"global\", path: match.path, cwd: match.cwd };\n\t}\n\n\t// Not found anywhere\n\treturn { type: \"not_found\", arg: sessionArg };\n}\n\n/** Prompt user for yes/no confirmation */\nasync function promptConfirm(message: string): Promise<boolean> {\n\treturn new Promise((resolve) => {\n\t\tconst rl = createInterface({\n\t\t\tinput: process.stdin,\n\t\t\toutput: process.stdout,\n\t\t});\n\t\trl.question(`${message} [y/N] `, (answer) => {\n\t\t\trl.close();\n\t\t\tresolve(answer.toLowerCase() === \"y\" || answer.toLowerCase() === \"yes\");\n\t\t});\n\t});\n}\n\nfunction validateForkFlags(parsed: Args): void {\n\tif (!parsed.fork) return;\n\n\tconst conflictingFlags = [\n\t\tparsed.session ? \"--session\" : undefined,\n\t\tparsed.continue ? \"--continue\" : undefined,\n\t\tparsed.resume ? \"--resume\" : undefined,\n\t\tparsed.noSession ? \"--no-session\" : undefined,\n\t].filter((flag): flag is string => flag !== undefined);\n\n\tif (conflictingFlags.length > 0) {\n\t\tconsole.error(chalk.red(`Error: --fork cannot be combined with ${conflictingFlags.join(\", \")}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nfunction forkSessionOrExit(sourcePath: string, cwd: string, sessionDir?: string): SessionManager {\n\ttry {\n\t\treturn SessionManager.forkFrom(sourcePath, cwd, sessionDir);\n\t} catch (error: unknown) {\n\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\tprocess.exit(1);\n\t}\n}\n\nasync function createSessionManager(\n\tparsed: Args,\n\tcwd: string,\n\tsessionDir: string | undefined,\n\tsettingsManager: SettingsManager,\n): Promise<SessionManager> {\n\tif (parsed.noSession) {\n\t\treturn SessionManager.inMemory();\n\t}\n\n\tif (parsed.fork) {\n\t\tconst resolved = await resolveSessionPath(parsed.fork, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\tcase \"global\":\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir);\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.session) {\n\t\tconst resolved = await resolveSessionPath(parsed.session, cwd, sessionDir);\n\n\t\tswitch (resolved.type) {\n\t\t\tcase \"path\":\n\t\t\tcase \"local\":\n\t\t\t\treturn SessionManager.open(resolved.path, sessionDir);\n\n\t\t\tcase \"global\": {\n\t\t\t\tconsole.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));\n\t\t\t\tconst shouldFork = await promptConfirm(\"Fork this session into current directory?\");\n\t\t\t\tif (!shouldFork) {\n\t\t\t\t\tconsole.log(chalk.dim(\"Aborted.\"));\n\t\t\t\t\tprocess.exit(0);\n\t\t\t\t}\n\t\t\t\treturn forkSessionOrExit(resolved.path, cwd, sessionDir);\n\t\t\t}\n\n\t\t\tcase \"not_found\":\n\t\t\t\tconsole.error(chalk.red(`No session found matching '${resolved.arg}'`));\n\t\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\tif (parsed.resume) {\n\t\tinitTheme(settingsManager.getTheme(), true);\n\t\ttry {\n\t\t\tconst selectedPath = await selectSession(\n\t\t\t\t(onProgress) => SessionManager.list(cwd, sessionDir, onProgress),\n\t\t\t\tSessionManager.listAll,\n\t\t\t);\n\t\t\tif (!selectedPath) {\n\t\t\t\tconsole.log(chalk.dim(\"No session selected\"));\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\treturn SessionManager.open(selectedPath, sessionDir);\n\t\t} finally {\n\t\t\tstopThemeWatcher();\n\t\t}\n\t}\n\n\tif (parsed.continue) {\n\t\treturn SessionManager.continueRecent(cwd, sessionDir);\n\t}\n\n\treturn SessionManager.create(cwd, sessionDir);\n}\n\nfunction buildSessionOptions(\n\tparsed: Args,\n\tscopedModels: ScopedModel[],\n\thasExistingSession: boolean,\n\tmodelRegistry: ModelRegistry,\n\tsettingsManager: SettingsManager,\n): {\n\toptions: CreateAgentSessionOptions;\n\tcliThinkingFromModel: boolean;\n\tdiagnostics: AgentSessionRuntimeDiagnostic[];\n} {\n\tconst options: CreateAgentSessionOptions = {};\n\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [];\n\tlet cliThinkingFromModel = false;\n\n\t// Model from CLI\n\t// - supports --provider <name> --model <pattern>\n\t// - supports --model <provider>/<pattern>\n\tif (parsed.model) {\n\t\tconst resolved = resolveCliModel({\n\t\t\tcliProvider: parsed.provider,\n\t\t\tcliModel: parsed.model,\n\t\t\tmodelRegistry,\n\t\t});\n\t\tif (resolved.warning) {\n\t\t\tdiagnostics.push({ type: \"warning\", message: resolved.warning });\n\t\t}\n\t\tif (resolved.error) {\n\t\t\tdiagnostics.push({ type: \"error\", message: resolved.error });\n\t\t}\n\t\tif (resolved.model) {\n\t\t\toptions.model = resolved.model;\n\t\t\t// Allow \"--model <pattern>:<thinking>\" as a shorthand.\n\t\t\t// Explicit --thinking still takes precedence (applied later).\n\t\t\tif (!parsed.thinking && resolved.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = resolved.thinkingLevel;\n\t\t\t\tcliThinkingFromModel = true;\n\t\t\t}\n\t\t}\n\t}\n\n\tif (!options.model && scopedModels.length > 0 && !hasExistingSession) {\n\t\t// Check if saved default is in scoped models - use it if so, otherwise first scoped model\n\t\tconst savedProvider = settingsManager.getDefaultProvider();\n\t\tconst savedModelId = settingsManager.getDefaultModel();\n\t\tconst savedModel = savedProvider && savedModelId ? modelRegistry.find(savedProvider, savedModelId) : undefined;\n\t\tconst savedInScope = savedModel ? scopedModels.find((sm) => modelsAreEqual(sm.model, savedModel)) : undefined;\n\n\t\tif (savedInScope) {\n\t\t\toptions.model = savedInScope.model;\n\t\t\t// Use thinking level from scoped model config if explicitly set\n\t\t\tif (!parsed.thinking && savedInScope.thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = savedInScope.thinkingLevel;\n\t\t\t}\n\t\t} else {\n\t\t\toptions.model = scopedModels[0].model;\n\t\t\t// Use thinking level from first scoped model if explicitly set\n\t\t\tif (!parsed.thinking && scopedModels[0].thinkingLevel) {\n\t\t\t\toptions.thinkingLevel = scopedModels[0].thinkingLevel;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Thinking level from CLI (takes precedence over scoped model thinking levels set above)\n\tif (parsed.thinking) {\n\t\toptions.thinkingLevel = parsed.thinking;\n\t}\n\n\t// Scoped models for Ctrl+P cycling\n\t// Keep thinking level undefined when not explicitly set in the model pattern.\n\t// Undefined means \"inherit current session thinking level\" during cycling.\n\tif (scopedModels.length > 0) {\n\t\toptions.scopedModels = scopedModels.map((sm) => ({\n\t\t\tmodel: sm.model,\n\t\t\tthinkingLevel: sm.thinkingLevel,\n\t\t}));\n\t}\n\n\t// API key from CLI - set in authStorage\n\t// (handled by caller before createAgentSession)\n\n\t// Tools\n\tif (parsed.noTools) {\n\t\toptions.noTools = \"all\";\n\t} else if (parsed.noBuiltinTools) {\n\t\toptions.noTools = \"builtin\";\n\t}\n\tif (parsed.tools) {\n\t\toptions.tools = [...parsed.tools];\n\t}\n\n\t// Optional Task (subagent) tool: opt-in via --enable-subagents flag or the enableSubagent setting.\n\t// Registered as a custom tool; respects --tools/--no-tools allowlists like any other tool.\n\t// Never register it inside a spawned subagent (--task-id present): subagents must not\n\t// recursively dispatch, even if a project's enableSubagent setting is on.\n\tconst isSubagentChild = parsed.taskId !== undefined;\n\tif (!isSubagentChild && (parsed.subagent ?? settingsManager.getEnableSubagent())) {\n\t\toptions.customTools = [\n\t\t\t...(options.customTools ?? []),\n\t\t\tcreateTaskToolDefinition(),\n\t\t\tcreateTaskOutputToolDefinition(),\n\t\t];\n\t}\n\n\t// Optional TodoWrite tool: opt-in via --enable-todowrite flag or the\n\t// enableTodoWrite setting. Never registered inside a spawned subagent child —\n\t// its todos would otherwise leak into the parent's \"main\" task group in the pane.\n\tif (!isSubagentChild && (parsed.todoWrite ?? settingsManager.getEnableTodoWrite())) {\n\t\toptions.customTools = [...(options.customTools ?? []), createTodoWriteToolDefinition()];\n\t}\n\n\treturn { options, cliThinkingFromModel, diagnostics };\n}\n\nfunction resolveCliPaths(cwd: string, paths: string[] | undefined): string[] | undefined {\n\treturn paths?.map((value) => (isLocalPath(value) ? resolve(cwd, value) : value));\n}\n\nasync function promptForMissingSessionCwd(\n\tissue: SessionCwdIssue,\n\tsettingsManager: SettingsManager,\n): Promise<string | undefined> {\n\tinitTheme(settingsManager.getTheme());\n\tsetKeybindings(KeybindingsManager.create());\n\n\treturn new Promise((resolve) => {\n\t\tconst ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor());\n\t\tui.setClearOnShrink(settingsManager.getClearOnShrink());\n\n\t\tlet settled = false;\n\t\tconst finish = (result: string | undefined) => {\n\t\t\tif (settled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tsettled = true;\n\t\t\tui.stop();\n\t\t\tresolve(result);\n\t\t};\n\n\t\tconst selector = new ExtensionSelectorComponent(\n\t\t\tformatMissingSessionCwdPrompt(issue),\n\t\t\t[\"Continue\", \"Cancel\"],\n\t\t\t(option) => finish(option === \"Continue\" ? issue.fallbackCwd : undefined),\n\t\t\t() => finish(undefined),\n\t\t\t{ tui: ui },\n\t\t);\n\t\tui.addChild(selector);\n\t\tui.setFocus(selector);\n\t\tui.start();\n\t});\n}\n\nexport interface MainOptions {\n\textensionFactories?: ExtensionFactory[];\n}\n\nexport async function main(args: string[], options?: MainOptions) {\n\tresetTimings();\n\tconst offlineMode =\n\t\targs.includes(\"--offline\") || isTruthyEnvFlag(process.env.HOOCODE_OFFLINE ?? process.env.PI_OFFLINE);\n\tif (offlineMode) {\n\t\tprocess.env.HOOCODE_OFFLINE = \"1\";\n\t\tprocess.env.HOOCODE_SKIP_VERSION_CHECK = \"1\";\n\t}\n\n\tif (await handlePackageCommand(args)) {\n\t\treturn;\n\t}\n\n\tif (await handleConfigCommand(args)) {\n\t\treturn;\n\t}\n\n\tif (await handleResourcesCommand(args)) {\n\t\treturn;\n\t}\n\n\tconst parsed = parseArgs(args);\n\tif (parsed.diagnostics.length > 0) {\n\t\tfor (const d of parsed.diagnostics) {\n\t\t\tconst color = d.type === \"error\" ? chalk.red : chalk.yellow;\n\t\t\tconsole.error(color(`${d.type === \"error\" ? \"Error\" : \"Warning\"}: ${d.message}`));\n\t\t}\n\t\tif (parsed.diagnostics.some((d) => d.type === \"error\")) {\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\ttime(\"parseArgs\");\n\tlet appMode = resolveAppMode(parsed, process.stdin.isTTY);\n\tconst shouldTakeOverStdout = appMode !== \"interactive\";\n\tif (shouldTakeOverStdout) {\n\t\ttakeOverStdout();\n\t}\n\n\tif (parsed.version) {\n\t\tconsole.log(VERSION);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.export) {\n\t\tlet result: string;\n\t\ttry {\n\t\t\tconst outputPath = parsed.messages.length > 0 ? parsed.messages[0] : undefined;\n\t\t\tresult = await exportFromFile(parsed.export, outputPath);\n\t\t} catch (error: unknown) {\n\t\t\tconst message = error instanceof Error ? error.message : \"Failed to export session\";\n\t\t\tconsole.error(chalk.red(`Error: ${message}`));\n\t\t\tprocess.exit(1);\n\t\t}\n\t\tconsole.log(`Exported to: ${result}`);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.mode === \"rpc\" && parsed.fileArgs.length > 0) {\n\t\tconsole.error(chalk.red(\"Error: @file arguments are not supported in RPC mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tvalidateForkFlags(parsed);\n\n\t// Run migrations (pass cwd for project-local migrations)\n\tconst { migratedAuthProviders: migratedProviders, deprecationWarnings } = runMigrations(process.cwd());\n\ttime(\"runMigrations\");\n\n\tconst cwd = process.cwd();\n\tconst agentDir = getAgentDir();\n\tconst startupSettingsManager = SettingsManager.create(cwd, agentDir);\n\treportDiagnostics(collectSettingsDiagnostics(startupSettingsManager, \"startup session lookup\"));\n\n\t// Decide the final runtime cwd before creating cwd-bound runtime services.\n\t// --session and --resume may select a session from another project, so project-local\n\t// settings, resources, provider registrations, and models must be resolved only after\n\t// the target session cwd is known. The startup-cwd settings manager is used only for\n\t// sessionDir lookup during session selection.\n\tconst envSessionDir = process.env[ENV_SESSION_DIR];\n\tconst sessionDir =\n\t\tparsed.sessionDir ??\n\t\t(envSessionDir ? expandTildePath(envSessionDir) : undefined) ??\n\t\tstartupSettingsManager.getSessionDir();\n\tlet sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager);\n\tconst missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd);\n\tif (missingSessionCwdIssue) {\n\t\tif (appMode === \"interactive\") {\n\t\t\tconst selectedCwd = await promptForMissingSessionCwd(missingSessionCwdIssue, startupSettingsManager);\n\t\t\tif (!selectedCwd) {\n\t\t\t\tprocess.exit(0);\n\t\t\t}\n\t\t\tsessionManager = SessionManager.open(missingSessionCwdIssue.sessionFile!, sessionDir, selectedCwd);\n\t\t} else {\n\t\t\tconsole.error(chalk.red(new MissingSessionCwdError(missingSessionCwdIssue).message));\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\ttime(\"createSessionManager\");\n\n\tconst resolvedExtensionPaths = resolveCliPaths(cwd, parsed.extensions);\n\tconst resolvedSkillPaths = resolveCliPaths(cwd, parsed.skills);\n\tconst resolvedAgentPaths = resolveCliPaths(cwd, parsed.agents);\n\tconst resolvedPromptTemplatePaths = resolveCliPaths(cwd, parsed.promptTemplates);\n\tconst resolvedSlashCommandPaths = resolveCliPaths(cwd, parsed.slashCommands);\n\tconst resolvedThemePaths = resolveCliPaths(cwd, parsed.themes);\n\n\t// Populate the module-level CLI agent store before any session is created.\n\tsetAgentCliPaths(resolvedAgentPaths ?? []);\n\n\t// Synthetic factory: feed CLI --mode-path values into the extension runtime\n\t// so hoo-core (and any other extension that reads pi.getModeSearchPaths)\n\t// sees them alongside extension-registered dirs.\n\tconst cliModePaths = parsed.modePaths ?? [];\n\tconst cliResourcePathFactories: ExtensionFactory[] =\n\t\tcliModePaths.length === 0\n\t\t\t? []\n\t\t\t: [\n\t\t\t\t\tObject.assign(\n\t\t\t\t\t\t(pi: ExtensionAPI) => {\n\t\t\t\t\t\t\tfor (const p of cliModePaths) pi.addModeSearchPath(p);\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ internal: true },\n\t\t\t\t\t),\n\t\t\t\t];\n\tconst allExtensionFactories: ExtensionFactory[] = [\n\t\t...cliResourcePathFactories,\n\t\t...(options?.extensionFactories ?? []),\n\t];\n\tconst authStorage = AuthStorage.create();\n\tconst createRuntime: CreateAgentSessionRuntimeFactory = async ({\n\t\tcwd,\n\t\tagentDir,\n\t\tsessionManager,\n\t\tsessionStartEvent,\n\t}) => {\n\t\tconst services = await createAgentSessionServices({\n\t\t\tcwd,\n\t\t\tagentDir,\n\t\t\tauthStorage,\n\t\t\textensionFlagValues: parsed.unknownFlags,\n\t\t\tresourceLoaderOptions: {\n\t\t\t\tadditionalExtensionPaths: resolvedExtensionPaths,\n\t\t\t\tadditionalSkillPaths: resolvedSkillPaths,\n\t\t\t\tadditionalPromptTemplatePaths: resolvedPromptTemplatePaths,\n\t\t\t\tadditionalSlashCommandPaths: resolvedSlashCommandPaths,\n\t\t\t\tadditionalThemePaths: resolvedThemePaths,\n\t\t\t\tnoExtensions: parsed.noExtensions,\n\t\t\t\tnoSkills: parsed.noSkills,\n\t\t\t\tnoPromptTemplates: parsed.noPromptTemplates,\n\t\t\t\tnoSlashCommands: parsed.noSlashCommands,\n\t\t\t\tnoThemes: parsed.noThemes,\n\t\t\t\tnoContextFiles: parsed.noContextFiles,\n\t\t\t\tsystemPrompt: parsed.systemPrompt,\n\t\t\t\textensionFactories: allExtensionFactories,\n\t\t\t},\n\t\t});\n\t\tconst { settingsManager, modelRegistry, resourceLoader } = services;\n\t\tconst diagnostics: AgentSessionRuntimeDiagnostic[] = [\n\t\t\t...services.diagnostics,\n\t\t\t...collectSettingsDiagnostics(settingsManager, \"runtime creation\"),\n\t\t\t...resourceLoader.getExtensions().errors.map(({ path, error }) => ({\n\t\t\t\ttype: \"error\" as const,\n\t\t\t\tmessage: `Failed to load extension \"${path}\": ${error}`,\n\t\t\t})),\n\t\t];\n\n\t\t// When subagent tooling is enabled, append the main session subagent instructions.\n\t\tif (parsed.subagent ?? settingsManager.getEnableSubagent()) {\n\t\t\tresourceLoader.addAppendSystemPrompt(buildTaskMainPrompt());\n\t\t}\n\n\t\tconst modelPatterns = parsed.models ?? settingsManager.getEnabledModels();\n\t\tconst scopedModels =\n\t\t\tmodelPatterns && modelPatterns.length > 0 ? await resolveModelScope(modelPatterns, modelRegistry) : [];\n\t\tconst {\n\t\t\toptions: sessionOptions,\n\t\t\tcliThinkingFromModel,\n\t\t\tdiagnostics: sessionOptionDiagnostics,\n\t\t} = buildSessionOptions(\n\t\t\tparsed,\n\t\t\tscopedModels,\n\t\t\tsessionManager.buildSessionContext().messages.length > 0,\n\t\t\tmodelRegistry,\n\t\t\tsettingsManager,\n\t\t);\n\t\tdiagnostics.push(...sessionOptionDiagnostics);\n\n\t\tif (parsed.apiKey) {\n\t\t\tif (!sessionOptions.model) {\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\ttype: \"error\",\n\t\t\t\t\tmessage: \"--api-key requires a model to be specified via --model, --provider/--model, or --models\",\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tauthStorage.setRuntimeApiKey(sessionOptions.model.provider, parsed.apiKey);\n\t\t\t}\n\t\t}\n\n\t\tconst created = await createAgentSessionFromServices({\n\t\t\tservices,\n\t\t\tsessionManager,\n\t\t\tsessionStartEvent,\n\t\t\tmodel: sessionOptions.model,\n\t\t\tthinkingLevel: sessionOptions.thinkingLevel,\n\t\t\tscopedModels: sessionOptions.scopedModels,\n\t\t\ttools: sessionOptions.tools,\n\t\t\tnoTools: sessionOptions.noTools,\n\t\t\tcustomTools: sessionOptions.customTools,\n\t\t});\n\t\tconst cliThinkingOverride = parsed.thinking !== undefined || cliThinkingFromModel;\n\t\tif (created.session.model && cliThinkingOverride) {\n\t\t\tcreated.session.setThinkingLevel(created.session.thinkingLevel);\n\t\t}\n\n\t\treturn {\n\t\t\t...created,\n\t\t\tservices,\n\t\t\tdiagnostics,\n\t\t};\n\t};\n\ttime(\"createRuntime\");\n\tconst runtime = await createAgentSessionRuntime(createRuntime, {\n\t\tcwd: sessionManager.getCwd(),\n\t\tagentDir,\n\t\tsessionManager,\n\t});\n\tconst { services, session, modelFallbackMessage } = runtime;\n\tconst { settingsManager, modelRegistry, resourceLoader } = services;\n\n\tif (parsed.help) {\n\t\tconst extensionFlags = resourceLoader\n\t\t\t.getExtensions()\n\t\t\t.extensions.flatMap((extension) => Array.from(extension.flags.values()));\n\t\tprintHelp(extensionFlags);\n\t\tprocess.exit(0);\n\t}\n\n\tif (parsed.listModels !== undefined) {\n\t\tconst searchPattern = typeof parsed.listModels === \"string\" ? parsed.listModels : undefined;\n\t\tawait listModels(modelRegistry, searchPattern);\n\t\tprocess.exit(0);\n\t}\n\n\t// Read piped stdin content (if any) - skip for RPC mode which uses stdin for JSON-RPC\n\tlet stdinContent: string | undefined;\n\tif (appMode !== \"rpc\") {\n\t\tstdinContent = await readPipedStdin();\n\t\tif (stdinContent !== undefined && appMode === \"interactive\") {\n\t\t\tappMode = \"print\";\n\t\t}\n\t}\n\ttime(\"readPipedStdin\");\n\n\tconst { initialMessage, initialImages } = await prepareInitialMessage(\n\t\tparsed,\n\t\tsettingsManager.getImageAutoResize(),\n\t\tstdinContent,\n\t);\n\ttime(\"prepareInitialMessage\");\n\tinitTheme(settingsManager.getTheme(), appMode === \"interactive\");\n\ttime(\"initTheme\");\n\n\t// Show deprecation warnings in interactive mode\n\tif (appMode === \"interactive\" && deprecationWarnings.length > 0) {\n\t\tawait showDeprecationWarnings(deprecationWarnings);\n\t}\n\n\tconst scopedModels = [...session.scopedModels];\n\ttime(\"resolveModelScope\");\n\treportDiagnostics(runtime.diagnostics);\n\tif (runtime.diagnostics.some((diagnostic) => diagnostic.type === \"error\")) {\n\t\tprocess.exit(1);\n\t}\n\ttime(\"createAgentSession\");\n\n\tif (appMode !== \"interactive\" && !session.model) {\n\t\tconsole.error(chalk.red(formatNoModelsAvailableMessage()));\n\t\tprocess.exit(1);\n\t}\n\n\tconst startupBenchmark = isTruthyEnvFlag(process.env.HOOCODE_STARTUP_BENCHMARK ?? process.env.PI_STARTUP_BENCHMARK);\n\tif (startupBenchmark && appMode !== \"interactive\") {\n\t\tconsole.error(chalk.red(\"Error: HOOCODE_STARTUP_BENCHMARK only supports interactive mode\"));\n\t\tprocess.exit(1);\n\t}\n\n\tif (appMode === \"rpc\") {\n\t\tprintTimings();\n\t\tawait runRpcMode(runtime);\n\t} else if (appMode === \"interactive\") {\n\t\tif (scopedModels.length > 0 && (parsed.verbose || !settingsManager.getQuietStartup())) {\n\t\t\tconst modelList = scopedModels\n\t\t\t\t.map((sm) => {\n\t\t\t\t\tconst thinkingStr = sm.thinkingLevel ? `:${sm.thinkingLevel}` : \"\";\n\t\t\t\t\treturn `${sm.model.id}${thinkingStr}`;\n\t\t\t\t})\n\t\t\t\t.join(\", \");\n\t\t\tconsole.log(chalk.dim(`Model scope: ${modelList} ${chalk.gray(\"(Ctrl+P to cycle)\")}`));\n\t\t}\n\n\t\tconst interactiveMode = new InteractiveMode(runtime, {\n\t\t\tmigratedProviders,\n\t\t\tmodelFallbackMessage,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\tinitialMessages: parsed.messages,\n\t\t\tverbose: parsed.verbose,\n\t\t});\n\n\t\t// Optional hooteams bridge. `--team auto` discovers a config, spawns a\n\t\t// local hooteams child on a free port, and proceeds as if its URL had\n\t\t// been passed; the child is reaped on exit (clean or signal) via a\n\t\t// process \"exit\" hook plus the explicit stop below.\n\t\tlet autoTeam: { url: string; stop(): Promise<void> } | undefined;\n\t\tlet teamUrl = parsed.team;\n\t\tif (teamUrl === \"auto\") {\n\t\t\tconst { startAutoTeam } = await import(\"./core/team-auto.js\");\n\t\t\ttry {\n\t\t\t\tautoTeam = await startAutoTeam(process.cwd(), { log: (message) => console.log(chalk.dim(message)) });\n\t\t\t\tteamUrl = autoTeam.url;\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(chalk.red(error instanceof Error ? error.message : String(error)));\n\t\t\t\tprocess.exit(1);\n\t\t\t}\n\t\t}\n\n\t\t// Team mirror + client. Fire-and-forget: connect failures and drops warn\n\t\t// in the background and never block the main agent. Warnings go through\n\t\t// the chat, not console.error: a raw stderr write while the TUI owns the\n\t\t// screen scribbles over the render and can leave the editor looking\n\t\t// frozen. The same connection powers team focus / nudge / attach.\n\t\tlet teamView: { stop(): void } | undefined;\n\t\tif (teamUrl) {\n\t\t\tconst { connectTeamView } = await import(\"./core/team-view.js\");\n\t\t\tconst teamClient = connectTeamView(teamUrl, {\n\t\t\t\twarn: (message) => interactiveMode.showWarning(message),\n\t\t\t});\n\t\t\tteamView = teamClient;\n\t\t\tinteractiveMode.attachTeamClient(teamClient);\n\t\t}\n\t\tif (startupBenchmark) {\n\t\t\tawait interactiveMode.init();\n\t\t\ttime(\"interactiveMode.init\");\n\t\t\tprintTimings();\n\t\t\tteamView?.stop();\n\t\t\tawait autoTeam?.stop();\n\t\t\tinteractiveMode.stop();\n\t\t\tstopThemeWatcher();\n\t\t\tif (process.stdout.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tif (process.stderr.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stderr.once(\"drain\", resolve));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tprintTimings();\n\t\ttry {\n\t\t\tawait interactiveMode.run();\n\t\t} finally {\n\t\t\tteamView?.stop();\n\t\t\tawait autoTeam?.stop();\n\t\t}\n\t} else {\n\t\tprintTimings();\n\t\tconst exitCode = await runPrintMode(runtime, {\n\t\t\tmode: toPrintOutputMode(appMode),\n\t\t\tmessages: parsed.messages,\n\t\t\tinitialMessage,\n\t\t\tinitialImages,\n\t\t\ttaskId: parsed.taskId,\n\t\t\tmaxTurns: parsed.maxTurns,\n\t\t});\n\t\tstopThemeWatcher();\n\t\trestoreStdout();\n\t\tif (exitCode !== 0) {\n\t\t\tprocess.exitCode = exitCode;\n\t\t}\n\t\t// Spawned subagents (run with a task id) must exit promptly once their work is\n\t\t// done and result.json is written. The child's runtime can leave handles open\n\t\t// (e.g. MCP client connections) that keep the event loop alive, so a natural\n\t\t// exit may never happen. When that occurs the parent lifeguard SIGKILLs the idle\n\t\t// child at the 60s heartbeat threshold and misreports an already-completed task\n\t\t// as \"stalled\". Force a clean exit after draining output to avoid that false stall.\n\t\tconst ranAsSubagent = typeof parsed.taskId === \"string\" && parsed.taskId.length > 0;\n\t\tif (ranAsSubagent) {\n\t\t\tif (process.stdout.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tif (process.stderr.writableLength > 0) {\n\t\t\t\tawait new Promise<void>((resolve) => process.stderr.once(\"drain\", resolve));\n\t\t\t}\n\t\t\tprocess.exit(exitCode);\n\t\t}\n\t\treturn;\n\t}\n}\n"]}
package/dist/main.js CHANGED
@@ -29,6 +29,7 @@ import { SessionManager } from "./core/session-manager.js";
29
29
  import { SettingsManager } from "./core/settings-manager.js";
30
30
  import { printTimings, resetTimings, time } from "./core/timings.js";
31
31
  import { buildTaskMainPrompt, createTaskOutputToolDefinition, createTaskToolDefinition, } from "./core/tools/subagent.js";
32
+ import { createTodoWriteToolDefinition } from "./core/tools/todo.js";
32
33
  import { runMigrations, showDeprecationWarnings } from "./migrations.js";
33
34
  import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
34
35
  import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.js";
@@ -306,6 +307,12 @@ function buildSessionOptions(parsed, scopedModels, hasExistingSession, modelRegi
306
307
  createTaskOutputToolDefinition(),
307
308
  ];
308
309
  }
310
+ // Optional TodoWrite tool: opt-in via --enable-todowrite flag or the
311
+ // enableTodoWrite setting. Never registered inside a spawned subagent child —
312
+ // its todos would otherwise leak into the parent's "main" task group in the pane.
313
+ if (!isSubagentChild && (parsed.todoWrite ?? settingsManager.getEnableTodoWrite())) {
314
+ options.customTools = [...(options.customTools ?? []), createTodoWriteToolDefinition()];
315
+ }
309
316
  return { options, cliThinkingFromModel, diagnostics };
310
317
  }
311
318
  function resolveCliPaths(cwd, paths) {