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.
- package/ATTRIBUTION.md +53 -0
- package/CODE_OF_CONDUCT.md +130 -0
- package/CONTRIBUTING.md +107 -0
- package/LICENSE +202 -0
- package/README.md +486 -0
- package/SECURITY.md +211 -0
- package/bin/headlesscode.mjs +83 -0
- package/package.json +63 -0
- package/shared/prompts/review-mode-prompt-short.md +93 -0
- package/shared/prompts/review-mode-prompt.md +281 -0
- package/shared/rules-code/rules.md +22 -0
- package/shared/stacks/cpp/rules.md +30 -0
- package/shared/stacks/fastapi/rules.md +30 -0
- package/shared/stacks/javascript/rules.md +37 -0
- package/shared/stacks/postgresql/rules.md +31 -0
- package/shared/stacks/python/rules.md +35 -0
- package/shared/stacks/react/rules.md +11 -0
- package/shared/stacks/typescript/rules.md +10 -0
- package/src/budget/budget.ts +221 -0
- package/src/budget/concurrency.ts +126 -0
- package/src/budget/cost.ts +309 -0
- package/src/budget/index.ts +8 -0
- package/src/checkpoints/cli.ts +256 -0
- package/src/checkpoints/service.ts +227 -0
- package/src/cli.ts +1535 -0
- package/src/cloud/docker-provider.ts +334 -0
- package/src/cloud/provider.ts +300 -0
- package/src/codeintel/call-graph.ts +78 -0
- package/src/codeintel/find-references.ts +123 -0
- package/src/codeintel/go-to-definition.ts +193 -0
- package/src/codeintel/handlers.ts +190 -0
- package/src/codeintel/import-graph.ts +173 -0
- package/src/codeintel/outline.ts +180 -0
- package/src/codeintel/position.ts +77 -0
- package/src/codeintel/program.ts +350 -0
- package/src/codeintel/rename-symbol.ts +213 -0
- package/src/codeintel/tools.ts +280 -0
- package/src/codemap/build.ts +135 -0
- package/src/codemap/cli.ts +190 -0
- package/src/codemap/extract.ts +339 -0
- package/src/codemap/files.ts +236 -0
- package/src/codemap/fingerprint.ts +65 -0
- package/src/codemap/flows.ts +62 -0
- package/src/codemap/html.ts +451 -0
- package/src/codemap/lock.ts +80 -0
- package/src/codemap/types.ts +101 -0
- package/src/codesearch/airunner-embedder.ts +185 -0
- package/src/codesearch/chunk.ts +339 -0
- package/src/codesearch/cli.ts +223 -0
- package/src/codesearch/embedder.ts +332 -0
- package/src/codesearch/files.ts +280 -0
- package/src/codesearch/index.ts +469 -0
- package/src/codesearch/ollama-embedder.ts +205 -0
- package/src/codesearch/search.ts +141 -0
- package/src/codesearch/types.ts +100 -0
- package/src/config/mode-models.ts +218 -0
- package/src/dashboard/aggregate.ts +364 -0
- package/src/dashboard/chat-thread.ts +141 -0
- package/src/dashboard/checkpoints.ts +124 -0
- package/src/dashboard/cli.ts +193 -0
- package/src/dashboard/codemap.ts +44 -0
- package/src/dashboard/files.ts +121 -0
- package/src/dashboard/page.ts +2803 -0
- package/src/dashboard/self-improvement-metrics.ts +282 -0
- package/src/dashboard/server.ts +1103 -0
- package/src/dashboard/session-launch.ts +310 -0
- package/src/dashboard/timeline.ts +273 -0
- package/src/dashboard/tool-exec.ts +107 -0
- package/src/dashboard/trend-cli.ts +141 -0
- package/src/dashboard/trend.ts +413 -0
- package/src/decision-proxy/cli.ts +261 -0
- package/src/decision-proxy/proxy.ts +569 -0
- package/src/deploy/gate-cli.ts +147 -0
- package/src/deploy/gate.ts +254 -0
- package/src/engine/condense.ts +512 -0
- package/src/engine/events.ts +428 -0
- package/src/engine/handoff.ts +71 -0
- package/src/engine/lazy-tools.ts +160 -0
- package/src/engine/local-explore.ts +653 -0
- package/src/engine/logger.ts +96 -0
- package/src/engine/loop.ts +5517 -0
- package/src/engine/parser.ts +347 -0
- package/src/engine/prompt.ts +860 -0
- package/src/engine/reports.ts +47 -0
- package/src/engine/stacks.ts +448 -0
- package/src/engine/types.ts +291 -0
- package/src/engine/usage.ts +186 -0
- package/src/github/app-auth.ts +161 -0
- package/src/github/cli.ts +448 -0
- package/src/github/installations.ts +133 -0
- package/src/github/pr.ts +321 -0
- package/src/github/provision.ts +118 -0
- package/src/github/push.ts +122 -0
- package/src/index-util.ts +50 -0
- package/src/index.ts +81 -0
- package/src/init/cli.ts +248 -0
- package/src/init/gitignore.ts +74 -0
- package/src/llm/ollama.ts +308 -0
- package/src/llm/openrouter.ts +868 -0
- package/src/llm/preflight.ts +367 -0
- package/src/llm/transcript-capture.ts +84 -0
- package/src/memory/embed.ts +110 -0
- package/src/memory/index.ts +22 -0
- package/src/memory/local.ts +259 -0
- package/src/memory/summarizer.ts +283 -0
- package/src/memory/types.ts +153 -0
- package/src/memory/uwuchat.ts +157 -0
- package/src/migrate/cli.ts +115 -0
- package/src/orchestrator/analyze-cli.ts +104 -0
- package/src/orchestrator/auto-split.ts +206 -0
- package/src/orchestrator/cleanup.ts +1003 -0
- package/src/orchestrator/cli.ts +3571 -0
- package/src/orchestrator/cost-estimate.ts +564 -0
- package/src/orchestrator/cost-history-cli.ts +242 -0
- package/src/orchestrator/cost-history.ts +397 -0
- package/src/orchestrator/git-sync.ts +250 -0
- package/src/orchestrator/index.ts +153 -0
- package/src/orchestrator/log-analysis.ts +0 -0
- package/src/orchestrator/merge-check.ts +108 -0
- package/src/orchestrator/pipeline.ts +411 -0
- package/src/orchestrator/resume.ts +1940 -0
- package/src/orchestrator/reviewer.ts +503 -0
- package/src/orchestrator/split.ts +296 -0
- package/src/orchestrator/state.ts +542 -0
- package/src/orchestrator/status.ts +697 -0
- package/src/orchestrator/verification-gate.ts +134 -0
- package/src/orchestrator/watch.ts +898 -0
- package/src/permissions/commands.ts +1083 -0
- package/src/permissions/config.ts +241 -0
- package/src/permissions/index.ts +12 -0
- package/src/permissions/protected-files.ts +96 -0
- package/src/permissions/store-protection.ts +272 -0
- package/src/project-store.ts +648 -0
- package/src/projects/cli.ts +382 -0
- package/src/qa/qa.ts +487 -0
- package/src/tools/browser/handler.ts +346 -0
- package/src/tools/browser/service.ts +406 -0
- package/src/tools/browser/smoke.ts +78 -0
- package/src/tools/browser/tool.ts +99 -0
- package/src/tools/executor.ts +2575 -0
- package/src/tools/language-detect.ts +183 -0
- package/src/tools/output-summarizer.ts +369 -0
- package/src/tools/run-tests.ts +302 -0
- package/src/tools/set-indentation-tool.ts +49 -0
- package/src/tools/test-selection.ts +160 -0
- package/src/vendor/tests/smoke.ts +103 -0
- package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
- package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
- package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
- package/src/vendor/zoo-code/shim/os-name.ts +18 -0
- package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
- package/src/vendor/zoo-code/shim/vscode.ts +76 -0
- package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
- package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
- package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
- package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
- package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
- package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
- package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
- package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
- package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
- package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
- package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
- package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
- package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
- package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
- package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
- package/src/vendor/zoo-code/src/shared/language.ts +43 -0
- package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
- package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
- package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
- package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
- package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
- package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
- package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
- package/src/vendor/zoo-code/src/utils/object.ts +18 -0
- package/src/vendor/zoo-code/src/utils/path.ts +94 -0
- package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
- package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
- package/src/vendor/zoo-code/types/global-settings.ts +19 -0
- package/src/vendor/zoo-code/types/index.ts +22 -0
- package/src/vendor/zoo-code/types/message.ts +375 -0
- package/src/vendor/zoo-code/types/mode.ts +241 -0
- package/src/vendor/zoo-code/types/todo.ts +19 -0
- package/src/vendor/zoo-code/types/tool-params.ts +116 -0
- package/src/vendor/zoo-code/types/tool.ts +67 -0
- package/src/vendor/zoo-code/types/vscode.ts +84 -0
- package/src/vision/describe.ts +242 -0
- package/src/vision/tool.ts +91 -0
- package/src/watcher/cli.ts +369 -0
- package/src/watcher/github.ts +304 -0
- package/src/watcher/index.ts +59 -0
- package/src/watcher/state.ts +254 -0
- package/src/watcher/watch.ts +562 -0
- 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
|
+
}
|