headlesscode 1.0.2

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 (232) hide show
  1. package/ATTRIBUTION.md +53 -0
  2. package/CODE_OF_CONDUCT.md +130 -0
  3. package/CONTRIBUTING.md +107 -0
  4. package/LICENSE +202 -0
  5. package/README.md +486 -0
  6. package/SECURITY.md +211 -0
  7. package/bin/headlesscode.mjs +83 -0
  8. package/package.json +63 -0
  9. package/shared/prompts/review-mode-prompt-short.md +93 -0
  10. package/shared/prompts/review-mode-prompt.md +281 -0
  11. package/shared/rules-code/rules.md +22 -0
  12. package/shared/stacks/cpp/rules.md +30 -0
  13. package/shared/stacks/fastapi/rules.md +30 -0
  14. package/shared/stacks/javascript/rules.md +37 -0
  15. package/shared/stacks/postgresql/rules.md +31 -0
  16. package/shared/stacks/python/rules.md +35 -0
  17. package/shared/stacks/react/rules.md +11 -0
  18. package/shared/stacks/typescript/rules.md +10 -0
  19. package/src/budget/budget.ts +221 -0
  20. package/src/budget/concurrency.ts +126 -0
  21. package/src/budget/cost.ts +309 -0
  22. package/src/budget/index.ts +8 -0
  23. package/src/checkpoints/cli.ts +256 -0
  24. package/src/checkpoints/service.ts +227 -0
  25. package/src/cli.ts +1535 -0
  26. package/src/cloud/docker-provider.ts +334 -0
  27. package/src/cloud/provider.ts +300 -0
  28. package/src/codeintel/call-graph.ts +78 -0
  29. package/src/codeintel/find-references.ts +123 -0
  30. package/src/codeintel/go-to-definition.ts +193 -0
  31. package/src/codeintel/handlers.ts +190 -0
  32. package/src/codeintel/import-graph.ts +173 -0
  33. package/src/codeintel/outline.ts +180 -0
  34. package/src/codeintel/position.ts +77 -0
  35. package/src/codeintel/program.ts +350 -0
  36. package/src/codeintel/rename-symbol.ts +213 -0
  37. package/src/codeintel/tools.ts +280 -0
  38. package/src/codemap/build.ts +135 -0
  39. package/src/codemap/cli.ts +190 -0
  40. package/src/codemap/extract.ts +339 -0
  41. package/src/codemap/files.ts +236 -0
  42. package/src/codemap/fingerprint.ts +65 -0
  43. package/src/codemap/flows.ts +62 -0
  44. package/src/codemap/html.ts +451 -0
  45. package/src/codemap/lock.ts +80 -0
  46. package/src/codemap/types.ts +101 -0
  47. package/src/codesearch/airunner-embedder.ts +185 -0
  48. package/src/codesearch/chunk.ts +339 -0
  49. package/src/codesearch/cli.ts +223 -0
  50. package/src/codesearch/embedder.ts +332 -0
  51. package/src/codesearch/files.ts +280 -0
  52. package/src/codesearch/index.ts +469 -0
  53. package/src/codesearch/ollama-embedder.ts +205 -0
  54. package/src/codesearch/search.ts +141 -0
  55. package/src/codesearch/types.ts +100 -0
  56. package/src/config/mode-models.ts +218 -0
  57. package/src/dashboard/aggregate.ts +364 -0
  58. package/src/dashboard/chat-thread.ts +141 -0
  59. package/src/dashboard/checkpoints.ts +124 -0
  60. package/src/dashboard/cli.ts +193 -0
  61. package/src/dashboard/codemap.ts +44 -0
  62. package/src/dashboard/files.ts +121 -0
  63. package/src/dashboard/page.ts +2803 -0
  64. package/src/dashboard/self-improvement-metrics.ts +282 -0
  65. package/src/dashboard/server.ts +1103 -0
  66. package/src/dashboard/session-launch.ts +310 -0
  67. package/src/dashboard/timeline.ts +273 -0
  68. package/src/dashboard/tool-exec.ts +107 -0
  69. package/src/dashboard/trend-cli.ts +141 -0
  70. package/src/dashboard/trend.ts +413 -0
  71. package/src/decision-proxy/cli.ts +261 -0
  72. package/src/decision-proxy/proxy.ts +569 -0
  73. package/src/deploy/gate-cli.ts +147 -0
  74. package/src/deploy/gate.ts +254 -0
  75. package/src/engine/condense.ts +512 -0
  76. package/src/engine/events.ts +428 -0
  77. package/src/engine/handoff.ts +71 -0
  78. package/src/engine/lazy-tools.ts +160 -0
  79. package/src/engine/local-explore.ts +653 -0
  80. package/src/engine/logger.ts +96 -0
  81. package/src/engine/loop.ts +5517 -0
  82. package/src/engine/parser.ts +347 -0
  83. package/src/engine/prompt.ts +860 -0
  84. package/src/engine/reports.ts +47 -0
  85. package/src/engine/stacks.ts +448 -0
  86. package/src/engine/types.ts +291 -0
  87. package/src/engine/usage.ts +186 -0
  88. package/src/github/app-auth.ts +161 -0
  89. package/src/github/cli.ts +448 -0
  90. package/src/github/installations.ts +133 -0
  91. package/src/github/pr.ts +321 -0
  92. package/src/github/provision.ts +118 -0
  93. package/src/github/push.ts +122 -0
  94. package/src/index-util.ts +50 -0
  95. package/src/index.ts +81 -0
  96. package/src/init/cli.ts +248 -0
  97. package/src/init/gitignore.ts +74 -0
  98. package/src/llm/ollama.ts +308 -0
  99. package/src/llm/openrouter.ts +868 -0
  100. package/src/llm/preflight.ts +367 -0
  101. package/src/llm/transcript-capture.ts +84 -0
  102. package/src/memory/embed.ts +110 -0
  103. package/src/memory/index.ts +22 -0
  104. package/src/memory/local.ts +259 -0
  105. package/src/memory/summarizer.ts +283 -0
  106. package/src/memory/types.ts +153 -0
  107. package/src/memory/uwuchat.ts +157 -0
  108. package/src/migrate/cli.ts +115 -0
  109. package/src/orchestrator/analyze-cli.ts +104 -0
  110. package/src/orchestrator/auto-split.ts +206 -0
  111. package/src/orchestrator/cleanup.ts +1003 -0
  112. package/src/orchestrator/cli.ts +3571 -0
  113. package/src/orchestrator/cost-estimate.ts +564 -0
  114. package/src/orchestrator/cost-history-cli.ts +242 -0
  115. package/src/orchestrator/cost-history.ts +397 -0
  116. package/src/orchestrator/git-sync.ts +250 -0
  117. package/src/orchestrator/index.ts +153 -0
  118. package/src/orchestrator/log-analysis.ts +0 -0
  119. package/src/orchestrator/merge-check.ts +108 -0
  120. package/src/orchestrator/pipeline.ts +411 -0
  121. package/src/orchestrator/resume.ts +1940 -0
  122. package/src/orchestrator/reviewer.ts +503 -0
  123. package/src/orchestrator/split.ts +296 -0
  124. package/src/orchestrator/state.ts +542 -0
  125. package/src/orchestrator/status.ts +697 -0
  126. package/src/orchestrator/verification-gate.ts +134 -0
  127. package/src/orchestrator/watch.ts +898 -0
  128. package/src/permissions/commands.ts +1083 -0
  129. package/src/permissions/config.ts +241 -0
  130. package/src/permissions/index.ts +12 -0
  131. package/src/permissions/protected-files.ts +96 -0
  132. package/src/permissions/store-protection.ts +272 -0
  133. package/src/project-store.ts +648 -0
  134. package/src/projects/cli.ts +382 -0
  135. package/src/qa/qa.ts +487 -0
  136. package/src/tools/browser/handler.ts +346 -0
  137. package/src/tools/browser/service.ts +406 -0
  138. package/src/tools/browser/smoke.ts +78 -0
  139. package/src/tools/browser/tool.ts +99 -0
  140. package/src/tools/executor.ts +2575 -0
  141. package/src/tools/language-detect.ts +183 -0
  142. package/src/tools/output-summarizer.ts +369 -0
  143. package/src/tools/run-tests.ts +302 -0
  144. package/src/tools/set-indentation-tool.ts +49 -0
  145. package/src/tools/test-selection.ts +160 -0
  146. package/src/vendor/tests/smoke.ts +103 -0
  147. package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
  148. package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
  149. package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
  150. package/src/vendor/zoo-code/shim/os-name.ts +18 -0
  151. package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
  152. package/src/vendor/zoo-code/shim/vscode.ts +76 -0
  153. package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
  154. package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
  155. package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
  156. package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
  157. package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
  158. package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
  159. package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
  160. package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
  161. package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
  162. package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
  163. package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
  164. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
  165. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
  166. package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
  167. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
  168. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
  169. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
  170. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
  171. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
  172. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
  173. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
  174. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
  175. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
  176. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
  177. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
  178. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
  179. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
  180. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
  181. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
  182. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
  183. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
  184. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
  185. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
  186. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
  187. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
  188. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
  189. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
  190. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
  191. package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
  192. package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
  193. package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
  194. package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
  195. package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
  196. package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
  197. package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
  198. package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
  199. package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
  200. package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
  201. package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
  202. package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
  203. package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
  204. package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
  205. package/src/vendor/zoo-code/src/shared/language.ts +43 -0
  206. package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
  207. package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
  208. package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
  209. package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
  210. package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
  211. package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
  212. package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
  213. package/src/vendor/zoo-code/src/utils/object.ts +18 -0
  214. package/src/vendor/zoo-code/src/utils/path.ts +94 -0
  215. package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
  216. package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
  217. package/src/vendor/zoo-code/types/global-settings.ts +19 -0
  218. package/src/vendor/zoo-code/types/index.ts +22 -0
  219. package/src/vendor/zoo-code/types/message.ts +375 -0
  220. package/src/vendor/zoo-code/types/mode.ts +241 -0
  221. package/src/vendor/zoo-code/types/todo.ts +19 -0
  222. package/src/vendor/zoo-code/types/tool-params.ts +116 -0
  223. package/src/vendor/zoo-code/types/tool.ts +67 -0
  224. package/src/vendor/zoo-code/types/vscode.ts +84 -0
  225. package/src/vision/describe.ts +242 -0
  226. package/src/vision/tool.ts +91 -0
  227. package/src/watcher/cli.ts +369 -0
  228. package/src/watcher/github.ts +304 -0
  229. package/src/watcher/index.ts +59 -0
  230. package/src/watcher/state.ts +254 -0
  231. package/src/watcher/watch.ts +562 -0
  232. package/tsconfig.json +18 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Tool parameter type definitions for native protocol
3
+ */
4
+
5
+ /**
6
+ * Read mode for the read_file tool.
7
+ * - "slice": Simple offset/limit reading (default)
8
+ * - "indentation": Semantic block extraction based on code structure
9
+ */
10
+ export type ReadFileMode = "slice" | "indentation"
11
+
12
+ /**
13
+ * Indentation-mode configuration for the read_file tool.
14
+ */
15
+ export interface IndentationParams {
16
+ /** 1-based line number to anchor indentation extraction (defaults to offset) */
17
+ anchor_line?: number
18
+ /** Maximum indentation levels to include above anchor (0 = unlimited) */
19
+ max_levels?: number
20
+ /** Include sibling blocks at the same indentation level */
21
+ include_siblings?: boolean
22
+ /** Include file header (imports, comments at top) */
23
+ include_header?: boolean
24
+ /** Hard cap on lines returned for indentation mode */
25
+ max_lines?: number
26
+ }
27
+
28
+ /**
29
+ * Parameters for the read_file tool (new format).
30
+ *
31
+ * NOTE: This is the canonical, single-file-per-call shape.
32
+ */
33
+ export interface ReadFileParams {
34
+ /** Path to the file, relative to workspace */
35
+ path: string
36
+ /** Reading mode: "slice" (default) or "indentation" */
37
+ mode?: ReadFileMode
38
+ /** 1-based line number to start reading from (slice mode, default: 1) */
39
+ offset?: number
40
+ /** Maximum number of lines to read (default: 2000) */
41
+ limit?: number
42
+ /** Indentation-mode configuration (only used when mode === "indentation") */
43
+ indentation?: IndentationParams
44
+ }
45
+
46
+ // ─── Legacy Format Types (Backward Compatibility) ─────────────────────────────
47
+
48
+ /**
49
+ * Line range specification for legacy read_file format.
50
+ * Represents a contiguous range of lines [start, end] (1-based, inclusive).
51
+ */
52
+ export interface LineRange {
53
+ start: number
54
+ end: number
55
+ }
56
+
57
+ /**
58
+ * File entry for legacy read_file format.
59
+ * Supports reading multiple disjoint line ranges from a single file.
60
+ */
61
+ export interface FileEntry {
62
+ /** Path to the file, relative to workspace */
63
+ path: string
64
+ /** Optional list of line ranges to read (if omitted, reads entire file) */
65
+ lineRanges?: LineRange[]
66
+ }
67
+
68
+ /**
69
+ * Legacy parameters for the read_file tool (pre-refactor format).
70
+ * Supports reading multiple files in a single call with optional line ranges.
71
+ *
72
+ * @deprecated Use ReadFileParams instead. This format is maintained for
73
+ * backward compatibility with existing chat histories.
74
+ */
75
+ export interface LegacyReadFileParams {
76
+ /** Array of file entries to read */
77
+ files: FileEntry[]
78
+ /** Discriminant flag for type narrowing */
79
+ _legacyFormat: true
80
+ }
81
+
82
+ /**
83
+ * Union type for read_file tool parameters.
84
+ * Supports both new single-file format and legacy multi-file format.
85
+ */
86
+ export type ReadFileToolParams = ReadFileParams | LegacyReadFileParams
87
+
88
+ /**
89
+ * Type guard to check if params are in legacy format.
90
+ */
91
+ export function isLegacyReadFileParams(params: ReadFileToolParams): params is LegacyReadFileParams {
92
+ // `NativeToolCallParser` always tags freshly parsed legacy calls with `_legacyFormat: true`.
93
+ // The bare-`files` fallback only matters for chat history persisted before that flag was
94
+ // introduced (commit cc86049f1) and re-hydrated on a later run. Note that params matched via
95
+ // that fallback narrow to `LegacyReadFileParams` but leave `_legacyFormat` `undefined`, so
96
+ // callers should branch on the presence of `files`, not on `_legacyFormat === true`.
97
+ const hasLegacyFlag = "_legacyFormat" in params && params._legacyFormat === true
98
+ const hasFilesArray = "files" in params && Array.isArray((params as unknown as Record<string, unknown>).files)
99
+ return hasLegacyFlag || hasFilesArray
100
+ }
101
+
102
+ export interface Coordinate {
103
+ x: number
104
+ y: number
105
+ }
106
+
107
+ export interface Size {
108
+ width: number
109
+ height: number
110
+ }
111
+
112
+ export interface GenerateImageParams {
113
+ prompt: string
114
+ path: string
115
+ image?: string
116
+ }
@@ -0,0 +1,67 @@
1
+ import { z } from "zod"
2
+
3
+ /**
4
+ * ToolGroup
5
+ */
6
+
7
+ export const toolGroups = ["read", "edit", "command", "mcp", "modes"] as const
8
+
9
+ export const toolGroupsSchema = z.enum(toolGroups)
10
+
11
+ /**
12
+ * Tool groups that have been removed but may still exist in user config files.
13
+ * Used by schema preprocessing to silently strip these before validation,
14
+ * preventing errors for users with older configs.
15
+ */
16
+ export const deprecatedToolGroups: readonly string[] = ["browser"]
17
+
18
+ export type ToolGroup = z.infer<typeof toolGroupsSchema>
19
+
20
+ /**
21
+ * ToolName
22
+ */
23
+
24
+ export const toolNames = [
25
+ "execute_command",
26
+ "read_file",
27
+ "read_command_output",
28
+ "write_to_file",
29
+ "apply_diff",
30
+ "edit",
31
+ "search_and_replace",
32
+ "search_replace",
33
+ "edit_file",
34
+ "apply_patch",
35
+ "search_files",
36
+ "list_files",
37
+ "use_mcp_tool",
38
+ "access_mcp_resource",
39
+ "ask_followup_question",
40
+ "attempt_completion",
41
+ "switch_mode",
42
+ "new_task",
43
+ "codebase_search",
44
+ "update_todo_list",
45
+ "run_slash_command",
46
+ "skill",
47
+ "generate_image",
48
+ "custom_tool",
49
+ ] as const
50
+
51
+ export const toolNamesSchema = z.enum(toolNames)
52
+
53
+ export type ToolName = z.infer<typeof toolNamesSchema>
54
+
55
+ /**
56
+ * ToolUsage
57
+ */
58
+
59
+ export const toolUsageSchema = z.record(
60
+ toolNamesSchema,
61
+ z.object({
62
+ attempts: z.number(),
63
+ failures: z.number(),
64
+ }),
65
+ )
66
+
67
+ export type ToolUsage = z.infer<typeof toolUsageSchema>
@@ -0,0 +1,84 @@
1
+ import { z } from "zod"
2
+
3
+ /**
4
+ * CodeAction
5
+ */
6
+
7
+ export const codeActionIds = ["explainCode", "fixCode", "improveCode", "addToContext", "newTask"] as const
8
+
9
+ export type CodeActionId = (typeof codeActionIds)[number]
10
+
11
+ export type CodeActionName = "EXPLAIN" | "FIX" | "IMPROVE" | "ADD_TO_CONTEXT" | "NEW_TASK"
12
+
13
+ /**
14
+ * TerminalAction
15
+ */
16
+
17
+ export const terminalActionIds = ["terminalAddToContext", "terminalFixCommand", "terminalExplainCommand"] as const
18
+
19
+ export type TerminalActionId = (typeof terminalActionIds)[number]
20
+
21
+ export type TerminalActionName = "ADD_TO_CONTEXT" | "FIX" | "EXPLAIN"
22
+
23
+ export type TerminalActionPromptType = `TERMINAL_${TerminalActionName}`
24
+
25
+ /**
26
+ * Command
27
+ */
28
+
29
+ export const commandIds = [
30
+ "activationCompleted",
31
+
32
+ "plusButtonClicked",
33
+ "historyButtonClicked",
34
+ "marketplaceButtonClicked",
35
+ "popoutButtonClicked",
36
+ "settingsButtonClicked",
37
+
38
+ "openInNewTab",
39
+
40
+ "newTask",
41
+
42
+ "setCustomStoragePath",
43
+ "importSettings",
44
+
45
+ "focusInput",
46
+ "acceptInput",
47
+ "focusPanel",
48
+ "toggleAutoApprove",
49
+
50
+ "showRipgrepDiagnostic",
51
+ ] as const
52
+
53
+ export type CommandId = (typeof commandIds)[number]
54
+
55
+ /**
56
+ * Language
57
+ */
58
+
59
+ export const languages = [
60
+ "ca",
61
+ "de",
62
+ "en",
63
+ "es",
64
+ "fr",
65
+ "hi",
66
+ "id",
67
+ "it",
68
+ "ja",
69
+ "ko",
70
+ "nl",
71
+ "pl",
72
+ "pt-BR",
73
+ "ru",
74
+ "tr",
75
+ "vi",
76
+ "zh-CN",
77
+ "zh-TW",
78
+ ] as const
79
+
80
+ export const languagesSchema = z.enum(languages)
81
+
82
+ export type Language = z.infer<typeof languagesSchema>
83
+
84
+ export const isLanguage = (value: string): value is Language => languages.includes(value as Language)
@@ -0,0 +1,242 @@
1
+ import { readFile } from "node:fs/promises"
2
+ import path from "node:path"
3
+ import type { AuxLlmUsage } from "../engine/types.js"
4
+
5
+ /**
6
+ * Cloud vision captioning for images — the ONLY image path through this
7
+ * harness. This is a deliberate, narrow, standalone client (same pattern as
8
+ * OllamaLocalChatClient in src/engine/local-explore.ts): the main agent loop
9
+ * and every LlmClient are typed around the text-only ChatMessage shape, so a
10
+ * multimodal request cannot flow through them. Instead this module makes its
11
+ * OWN OpenAI-compatible chat-completions call against OpenRouter with an
12
+ * `image_url` content part, gets back a text description, and that text is
13
+ * all that ever touches the rest of the system (ToolResult content is
14
+ * string-only).
15
+ *
16
+ * Cost is real and tracked: each call returns the provider's token counts in
17
+ * AuxLlmUsage, which the executor's onAuxLlmUsage hook forwards to the
18
+ * session's BudgetTracker + running totals (see recordAuxLlmUsage in
19
+ * src/engine/loop.ts) — captioning a screenshot shows up in the session's
20
+ * budget/usage accounting exactly like a regular LLM call.
21
+ *
22
+ * Model default (google/gemma-3-12b-it) is the result of the real model
23
+ * evaluation documented in the imgsupport plan: the cheapest candidate that
24
+ * produced descriptions with genuinely useful detail (correctly diagnosed a
25
+ * broken-page screenshot) and no material quality regression vs. the
26
+ * gemma-3-27b-it quality anchor — which hallucinated detail on the same
27
+ * image. Override via HEADLESSCODE_VISION_MODEL.
28
+ */
29
+
30
+ export const DEFAULT_VISION_MODEL = "google/gemma-3-12b-it"
31
+
32
+ export const DEFAULT_VISION_TIMEOUT_MS = 60_000
33
+
34
+ /** Default resolution of the base URL: same OpenRouter endpoint the main
35
+ * client uses, overridable via OPENROUTER_BASE_URL like the main client. */
36
+ export function visionBaseUrl(): string {
37
+ return (process.env.OPENROUTER_BASE_URL ?? "https://openrouter.ai").replace(/\/+$/, "")
38
+ }
39
+
40
+ export const VISION_SYSTEM_PROMPT = [
41
+ "You are an image captioning system for a coding agent. Describe the image in concrete, factual",
42
+ "detail a software engineer can act on. Include:",
43
+ "- All visible text verbatim where readable: error messages, button labels, headings, URLs, console output.",
44
+ "- The overall layout and every UI element present (buttons, inputs, dialogs, tables, toggles, images) and their state (disabled, checked, highlighted, empty).",
45
+ "- Anything that looks wrong: error states, missing content, layout breakage, misalignment, blank areas where content should be.",
46
+ "- Colors only when they carry meaning (red error text, green success, amber warnings).",
47
+ "Do not write a vague generic caption, do not speculate about what the image is 'probably' from, and do not invent text you cannot actually read",
48
+ "- if text is illegible, say it is illegible rather than guessing.",
49
+ ].join("\n")
50
+
51
+ /** Failure from the vision captioning call. Mirrors OpenRouterError's shape
52
+ * (message + optional HTTP status + raw body excerpt) so callers can
53
+ * distinguish an auth/rate-limit failure from a malformed response. */
54
+ export class VisionError extends Error {
55
+ constructor(
56
+ message: string,
57
+ readonly status?: number,
58
+ readonly body?: string,
59
+ ) {
60
+ super(message)
61
+ this.name = "VisionError"
62
+ }
63
+ }
64
+
65
+ export interface DescribeImageResult {
66
+ description: string
67
+ /** Real token counts from the provider, for budget/usage accounting. */
68
+ usage: AuxLlmUsage
69
+ }
70
+
71
+ export interface DescribeImageOptions {
72
+ /** Model id. Default: HEADLESSCODE_VISION_MODEL, then DEFAULT_VISION_MODEL. */
73
+ model?: string
74
+ /** Reuses HEADLESSCODE_OPENROUTER_API_KEY when omitted. */
75
+ apiKey?: string
76
+ /** Reuses OPENROUTER_BASE_URL when omitted. */
77
+ baseUrl?: string
78
+ /** Injectable fetch (tests). Defaults to global fetch. */
79
+ fetchImpl?: typeof fetch
80
+ timeoutMs?: number
81
+ /** External abort (e.g. a tool-level deadline). */
82
+ signal?: AbortSignal
83
+ systemPrompt?: string
84
+ maxTokens?: number
85
+ }
86
+
87
+ function mimeTypeFor(filePath: string): string {
88
+ const ext = path.extname(filePath).toLowerCase()
89
+ switch (ext) {
90
+ case ".jpg":
91
+ case ".jpeg":
92
+ return "image/jpeg"
93
+ case ".webp":
94
+ return "image/webp"
95
+ case ".gif":
96
+ return "image/gif"
97
+ default:
98
+ return "image/png"
99
+ }
100
+ }
101
+
102
+ /** Caption one image file via OpenRouter's multimodal chat-completions
103
+ * endpoint. Reads the file, base64-encodes it into a standard OpenAI-
104
+ * compatible `image_url` content part, and returns the description plus the
105
+ * real usage. Throws VisionError on any failure (missing file, network,
106
+ * HTTP error, malformed response). */
107
+ export async function describeImage(
108
+ imagePath: string,
109
+ options: DescribeImageOptions = {},
110
+ ): Promise<DescribeImageResult> {
111
+ const apiKey = options.apiKey ?? process.env.HEADLESSCODE_OPENROUTER_API_KEY
112
+ if (!apiKey) {
113
+ throw new VisionError(
114
+ "HEADLESSCODE_OPENROUTER_API_KEY is not set. Set the environment variable HEADLESSCODE_OPENROUTER_API_KEY to use describeImage (vision captioning).",
115
+ )
116
+ }
117
+ const model = options.model ?? process.env.HEADLESSCODE_VISION_MODEL ?? DEFAULT_VISION_MODEL
118
+ const baseUrl = (options.baseUrl ?? visionBaseUrl()).replace(/\/+$/, "")
119
+ const timeoutMs = options.timeoutMs ?? DEFAULT_VISION_TIMEOUT_MS
120
+ const fetchImpl = options.fetchImpl ?? ((...args: Parameters<typeof fetch>) => fetch(...args))
121
+ const maxTokens = options.maxTokens ?? 1024
122
+
123
+ let imageBuffer: Buffer
124
+ try {
125
+ imageBuffer = await readFile(imagePath)
126
+ } catch (err) {
127
+ throw new VisionError(
128
+ `describeImage: cannot read image file ${imagePath}: ${err instanceof Error ? err.message : String(err)}`,
129
+ )
130
+ }
131
+ if (imageBuffer.length === 0) {
132
+ throw new VisionError(`describeImage: image file ${imagePath} is empty (0 bytes)`)
133
+ }
134
+
135
+ const imageUrl = `data:${mimeTypeFor(imagePath)};base64,${imageBuffer.toString("base64")}`
136
+ const body = {
137
+ model,
138
+ max_tokens: maxTokens,
139
+ temperature: 0.2,
140
+ messages: [
141
+ { role: "system", content: options.systemPrompt ?? VISION_SYSTEM_PROMPT },
142
+ {
143
+ role: "user",
144
+ content: [
145
+ { type: "text", text: "Describe this image in the detail requested." },
146
+ { type: "image_url", image_url: { url: imageUrl } },
147
+ ],
148
+ },
149
+ ],
150
+ }
151
+
152
+ const controller = new AbortController()
153
+ let timedOut = false
154
+ const timer = setTimeout(() => {
155
+ timedOut = true
156
+ controller.abort()
157
+ }, timeoutMs)
158
+ const onExternalAbort = () => controller.abort()
159
+ options.signal?.addEventListener("abort", onExternalAbort, { once: true })
160
+
161
+ let response: Response
162
+ try {
163
+ response = await fetchImpl(`${baseUrl}/api/v1/chat/completions`, {
164
+ method: "POST",
165
+ headers: {
166
+ authorization: `Bearer ${apiKey}`,
167
+ "content-type": "application/json",
168
+ },
169
+ signal: controller.signal,
170
+ body: JSON.stringify(body),
171
+ })
172
+ } catch (err) {
173
+ if (timedOut) {
174
+ throw new VisionError(`Vision request to OpenRouter timed out after ${timeoutMs}ms`)
175
+ }
176
+ if (err instanceof Error && err.name === "AbortError") {
177
+ throw err // caller-managed external abort
178
+ }
179
+ throw new VisionError(
180
+ `Network error calling OpenRouter vision endpoint: ${err instanceof Error ? err.message : String(err)}`,
181
+ )
182
+ } finally {
183
+ clearTimeout(timer)
184
+ options.signal?.removeEventListener("abort", onExternalAbort)
185
+ }
186
+
187
+ if (!response.ok) {
188
+ const rawBody = await response.text().catch(() => "")
189
+ const excerpt = rawBody.length > 500 ? `${rawBody.slice(0, 500)}…` : rawBody
190
+ throw new VisionError(
191
+ `OpenRouter vision request returned HTTP ${response.status}: ${excerpt || "(empty)"}`,
192
+ response.status,
193
+ excerpt,
194
+ )
195
+ }
196
+
197
+ // Same raw-body-first discipline as the main OpenRouter client: 200 does
198
+ // not guarantee a well-formed choices[] payload.
199
+ const rawBody = await response.text()
200
+ let data:
201
+ | {
202
+ model?: string
203
+ choices?: Array<{ message?: { content?: string | null }; finish_reason?: string }>
204
+ usage?: {
205
+ prompt_tokens?: number
206
+ completion_tokens?: number
207
+ prompt_tokens_details?: { cached_tokens?: number }
208
+ }
209
+ error?: { message?: string; code?: unknown }
210
+ }
211
+ | undefined
212
+ try {
213
+ data = JSON.parse(rawBody)
214
+ } catch {
215
+ const excerpt = rawBody.length > 500 ? `${rawBody.slice(0, 500)}…` : rawBody
216
+ throw new VisionError(`OpenRouter vision request returned HTTP 200 with a non-JSON/unparseable body: ${excerpt || "(empty)"}`)
217
+ }
218
+ if (data?.error) {
219
+ throw new VisionError(
220
+ `OpenRouter vision request returned HTTP 200 with an error envelope: ${data.error.message ?? JSON.stringify(data.error)}`,
221
+ )
222
+ }
223
+ const description = data?.choices?.[0]?.message?.content
224
+ if (!data || typeof description !== "string" || description.trim() === "") {
225
+ const finishReason = data?.choices?.[0]?.finish_reason
226
+ const excerpt = rawBody.length > 500 ? `${rawBody.slice(0, 500)}…` : rawBody
227
+ throw new VisionError(
228
+ `OpenRouter vision response contained no choices[0].message.content` +
229
+ (finishReason ? ` (finish_reason: ${finishReason})` : "") +
230
+ `. Raw body: ${excerpt || "(empty)"}`,
231
+ )
232
+ }
233
+
234
+ const usage: AuxLlmUsage = {
235
+ model: data.model ?? model,
236
+ inputTokens: data.usage?.prompt_tokens ?? 0,
237
+ outputTokens: data.usage?.completion_tokens ?? 0,
238
+ cachedTokens: data.usage?.prompt_tokens_details?.cached_tokens,
239
+ }
240
+
241
+ return { description, usage }
242
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * `describe_image` — a general-purpose captioning tool for ANY image already
3
+ * in the workspace (not just fresh browser screenshots): screenshots the
4
+ * harness or a repo already contains, dashboard-uploaded images (a follow-up
5
+ * feature), diagrams, design mockups. Schema is a single `path` relative to
6
+ * the workspace root; path safety reuses `resolveWithinWorkspace`'s existing
7
+ * guard (src/tools/executor.ts) rather than re-deriving it.
8
+ *
9
+ * The description is generated by the same cloud vision call as the
10
+ * screenshot auto-captioning (src/vision/describe.ts) and the real token/cost
11
+ * is reported back through `ToolContext.onAuxLlmUsage` so the session's
12
+ * BudgetTracker + usage accounting sees it (see recordAuxLlmUsage in
13
+ * src/engine/loop.ts) — an explicit describe_image call is never invisible
14
+ * spend.
15
+ */
16
+
17
+ import { constants } from "node:fs"
18
+ import { access } from "node:fs/promises"
19
+
20
+ import type OpenAI from "openai"
21
+
22
+ import type { ToolContext, ToolResult } from "../engine/types.js"
23
+ import { resolveWithinWorkspace } from "../tools/executor.js"
24
+ import { describeImage } from "./describe.js"
25
+
26
+ export const DESCRIBE_IMAGE_NAME = "describe_image"
27
+
28
+ const DESCRIBE_IMAGE_DESCRIPTION = `Request to describe an image file that already exists in the workspace (PNG/JPEG/WebP/GIF): reads the file and returns a detailed, factual text description generated by a cloud vision model — visible text verbatim, layout, UI element states, colors that carry meaning, and anything that looks visually wrong. This is a one-way image -> text conversion: the description is what reaches the coding model, never the raw image bytes.
29
+
30
+ Use this when a task references an image already present in the repo/workspace and you need to know what it actually shows (e.g. a screenshot checked into the repo, a diagram, a broken-page capture, a design mockup). Do NOT use it for screenshots you took yourself with browser_action — those are already auto-described in the screenshot result. One describe_image call costs a small real amount of money (an LLM API call); use it deliberately, not speculatively.`
31
+
32
+ const PATH_PARAMETER_DESCRIPTION = `Path to the image file, relative to the workspace root (e.g. "docs/diagram.png" or ".headlesscode/browser-screenshots/screenshot-2026-08-02T00-00-00-000Z.png").`
33
+
34
+ export const describeImageTool = {
35
+ type: "function",
36
+ function: {
37
+ name: DESCRIBE_IMAGE_NAME,
38
+ description: DESCRIBE_IMAGE_DESCRIPTION,
39
+ strict: true,
40
+ parameters: {
41
+ type: "object",
42
+ properties: {
43
+ path: {
44
+ type: "string",
45
+ description: PATH_PARAMETER_DESCRIPTION,
46
+ },
47
+ },
48
+ required: ["path"],
49
+ additionalProperties: false,
50
+ },
51
+ },
52
+ } satisfies OpenAI.Chat.ChatCompletionTool
53
+
54
+ function ok(content: string): ToolResult {
55
+ return { content, isError: false }
56
+ }
57
+
58
+ function err(content: string): ToolResult {
59
+ return { content: `[Error] ${content}`, isError: true }
60
+ }
61
+
62
+ export async function describeImageHandler(
63
+ args: Record<string, unknown>,
64
+ ctx: ToolContext,
65
+ ): Promise<ToolResult> {
66
+ const p = args.path
67
+ if (typeof p !== "string" || p.trim() === "") {
68
+ return err(`describe_image: missing or invalid string argument 'path' (got ${JSON.stringify(p)})`)
69
+ }
70
+
71
+ let abs: string
72
+ try {
73
+ abs = resolveWithinWorkspace(ctx.workspaceRoot, p)
74
+ } catch (e) {
75
+ return err(`describe_image: ${e instanceof Error ? e.message : String(e)}`)
76
+ }
77
+
78
+ try {
79
+ await access(abs, constants.F_OK)
80
+ } catch {
81
+ return err(`describe_image: no such file: ${p} (resolved to ${abs})`)
82
+ }
83
+
84
+ try {
85
+ const { description, usage } = await describeImage(abs)
86
+ ctx.onAuxLlmUsage?.(usage)
87
+ return ok(`describe_image: ${p}\n${description}`)
88
+ } catch (e) {
89
+ return err(`describe_image: ${e instanceof Error ? e.message : String(e)}`)
90
+ }
91
+ }