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,141 @@
1
+ /**
2
+ * Query-time search over the persisted codebase index.
3
+ *
4
+ * Embed the query (one call), brute-force cosine against every stored chunk,
5
+ * return the top-K as file:startLine-endLine citations + a snippet — matching
6
+ * the vendored codebase_search tool's expected result shape (see
7
+ * src/vendor/zoo-code/src/core/tools/CodebaseSearchTool.ts and the native
8
+ * tool's description).
9
+ *
10
+ * The index stores only { file, startLine, endLine, embedding, hash } — the
11
+ * chunk TEXT is deliberately NOT duplicated into the index (it would roughly
12
+ * double the file). The snippet is re-read from the file at query time from
13
+ * the cited line range; if the file is gone or unreadable the citation is
14
+ * still returned with an empty snippet.
15
+ *
16
+ * SCALING LIMIT (documented, accepted): brute-force cosine is O(N) per query
17
+ * over all chunks in the index. Fine for this project's scale; a vector DB or
18
+ * ANN index is the known future path for very large repos (see
19
+ * src/codesearch/index.ts).
20
+ *
21
+ * LOAD CACHING: parsing the index JSONL is the dominant per-query cost — a
22
+ * 37k-chunk index is ~860MB of JSONL that takes ~5s to stream-parse into
23
+ * ~1.4GB of JS objects. A codebase_search query therefore pays that once per
24
+ * SESSION (not per call): the parsed entries are cached keyed by
25
+ * (path, mtimeMs, size), so a rebuild elsewhere invalidates the cache
26
+ * automatically and repeated queries are ~instant.
27
+ */
28
+
29
+ import * as fs from "node:fs"
30
+ import * as path from "node:path"
31
+
32
+ import { cosineSimilarity } from "./embedder.js"
33
+ import { indexFilePath, loadIndex } from "./index.js"
34
+ import { DEFAULT_TOP_K, type IndexEntry } from "./types.js"
35
+
36
+ /** One search result, matching the vendored CodebaseSearchTool's payload shape. */
37
+ export interface SearchResult {
38
+ filePath: string
39
+ score: number
40
+ startLine: number
41
+ endLine: number
42
+ codeChunk: string
43
+ }
44
+
45
+ /** Best-effort read of the cited line range from the file on disk. */
46
+ function readSnippet(workspaceRoot: string, rel: string, startLine: number, endLine: number): string {
47
+ try {
48
+ const abs = path.join(path.resolve(workspaceRoot), rel)
49
+ const content = fs.readFileSync(abs, "utf-8")
50
+ const lines = content.split(/\r?\n/)
51
+ const slice = lines.slice(startLine - 1, endLine)
52
+ return slice.join("\n").trim()
53
+ } catch {
54
+ return ""
55
+ }
56
+ }
57
+
58
+ // Module-level parsed-index cache (one entry per process — a session runs one
59
+ // workspace; keying by path+mtime+size also makes it correct for several).
60
+ let cachedIndexPath: string | undefined
61
+ let cachedIndexMtimeMs = -1
62
+ let cachedIndexSize = -1
63
+ let cachedEntries: IndexEntry[] = []
64
+
65
+ /** loadIndex with a session-scoped parse cache; invalidated by file change. */
66
+ function loadIndexCached(workspaceRoot: string): IndexEntry[] {
67
+ const file = indexFilePath(workspaceRoot)
68
+ let st: fs.Stats
69
+ try {
70
+ st = fs.statSync(file)
71
+ } catch {
72
+ // Index disappeared (or never existed) — drop the cache entry so a
73
+ // later build of the same path is picked up fresh.
74
+ cachedIndexPath = undefined
75
+ return loadIndex(workspaceRoot)
76
+ }
77
+ if (cachedIndexPath === file && cachedIndexMtimeMs === st.mtimeMs && cachedIndexSize === st.size) {
78
+ return cachedEntries
79
+ }
80
+ const entries = loadIndex(workspaceRoot)
81
+ cachedIndexPath = file
82
+ cachedIndexMtimeMs = st.mtimeMs
83
+ cachedIndexSize = st.size
84
+ cachedEntries = entries
85
+ return entries
86
+ }
87
+
88
+ /** Search the persisted index for the chunks most similar to `query`. */
89
+ export function searchIndex(
90
+ workspaceRoot: string,
91
+ queryEmbedding: number[],
92
+ pathPrefix?: string,
93
+ topK: number = DEFAULT_TOP_K,
94
+ ): SearchResult[] {
95
+ const entries = loadIndexCached(workspaceRoot)
96
+ const scored: Array<{ entry: IndexEntry; score: number }> = []
97
+
98
+ for (const entry of entries) {
99
+ if (pathPrefix) {
100
+ const normalized = pathPrefix.replace(/\\/g, "/").replace(/\/+$/, "")
101
+ if (!entry.file.startsWith(normalized + "/") && entry.file !== normalized) {
102
+ continue
103
+ }
104
+ }
105
+ const score = cosineSimilarity(queryEmbedding, entry.embedding)
106
+ if (score > 0) {
107
+ scored.push({ entry, score })
108
+ }
109
+ }
110
+
111
+ scored.sort((a, b) => b.score - a.score)
112
+ return scored.slice(0, Math.max(1, topK)).map(({ entry, score }) => ({
113
+ filePath: entry.file,
114
+ score,
115
+ startLine: entry.startLine,
116
+ endLine: entry.endLine,
117
+ codeChunk: readSnippet(workspaceRoot, entry.file, entry.startLine, entry.endLine),
118
+ }))
119
+ }
120
+
121
+ /**
122
+ * Format search results for the tool result string, matching the vendored
123
+ * CodebaseSearchTool's output layout (Query / Results / File path / Score /
124
+ * Lines / Code Chunk).
125
+ */
126
+ export function formatSearchResults(query: string, results: SearchResult[]): string {
127
+ if (results.length === 0) {
128
+ return `No relevant code snippets found for the query: "${query}"`
129
+ }
130
+ const blocks = results.map(
131
+ (r) => `File path: ${r.filePath}
132
+ Score: ${r.score.toFixed(4)}
133
+ Lines: ${r.startLine}-${r.endLine}
134
+ Code Chunk: ${r.codeChunk}
135
+ `,
136
+ )
137
+ return `Query: ${query}
138
+ Results:
139
+
140
+ ${blocks.join("\n")}`
141
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Shared types for the codebase semantic-search subsystem
3
+ * (src/codesearch/ — chunker, cloud embedder, persisted index, and the
4
+ * `codebase_search` tool handler that consumes it).
5
+ *
6
+ * This is a deliberately separate subsystem from the Phase 3 memory embedder
7
+ * (src/memory/embed.ts): that one is a cheap deterministic hash stand-in for
8
+ * handful-of-records session/fact recall, while this one embeds an entire
9
+ * repo's source via a real cloud model and persists the vectors to disk.
10
+ */
11
+
12
+ /** One indexed code chunk, persisted as one JSON line in the index file. */
13
+ export interface IndexEntry {
14
+ /** Workspace-relative POSIX path (e.g. "src/engine/loop.ts"). */
15
+ file: string
16
+ /** 1-based inclusive line range of this chunk within the file. */
17
+ startLine: number
18
+ endLine: number
19
+ /** Embedding vector (dimension depends on the model). */
20
+ embedding: number[]
21
+ /**
22
+ * Content hash of the chunk text (sha256 hex of the raw chunk lines). The
23
+ * index build compares this against the previous build's entries to skip
24
+ * re-embedding unchanged chunks — the cost-control mechanism that stops a
25
+ * re-index of an untouched repo from spending money.
26
+ */
27
+ hash: string
28
+ /**
29
+ * Backend that produced this entry's embedding
30
+ * (`openrouter` | `ollama` | `airunner`). Added by the build for every
31
+ * entry; absent on pre-existing entries from before the Ollama backend
32
+ * existed, which the build treats as `openrouter` (the only backend that
33
+ * existed then). The query handler refuses to search with a mismatched
34
+ * backend — local and cloud embedding models produce different vector
35
+ * dimensions, so mixing them would silently yield garbage similarity
36
+ * scores.
37
+ */
38
+ backend?: "openrouter" | "ollama" | "airunner"
39
+ }
40
+
41
+ /**
42
+ * The on-disk index, relative to the CENTRAL project data dir
43
+ * (`<dataRoot>/projects/<project-key>/codesearch/index.jsonl` — see
44
+ * src/project-store.ts). No longer workspace-relative: the index lives in the
45
+ * central store so every worktree of a repo shares it.
46
+ */
47
+ export const INDEX_RELATIVE_PATH = "codesearch/index.jsonl"
48
+
49
+ /** Default top-K results returned by codebase_search. */
50
+ export const DEFAULT_TOP_K = 10
51
+
52
+ /** Supported source-file extensions (this repo's language mix + docs/config). */
53
+ export const CODE_EXTENSIONS = new Set([
54
+ ".ts",
55
+ ".tsx",
56
+ ".js",
57
+ ".jsx",
58
+ ".mjs",
59
+ ".cjs",
60
+ ".py",
61
+ ".rs",
62
+ ".go",
63
+ ".c",
64
+ ".h",
65
+ ".cpp",
66
+ ".hpp",
67
+ ".cs",
68
+ ".rb",
69
+ ".java",
70
+ ".php",
71
+ ".swift",
72
+ ".kt",
73
+ ".kts",
74
+ ".ex",
75
+ ".exs",
76
+ ".el",
77
+ ".html",
78
+ ".htm",
79
+ ".md",
80
+ ".markdown",
81
+ ".txt",
82
+ ".json",
83
+ ".css",
84
+ ".ml",
85
+ ".mli",
86
+ ".lua",
87
+ ".scala",
88
+ ".toml",
89
+ ".zig",
90
+ ".elm",
91
+ ".ejs",
92
+ ".erb",
93
+ ".vb",
94
+ ".dart",
95
+ ".sh",
96
+ ".bash",
97
+ ".zsh",
98
+ ".yml",
99
+ ".yaml",
100
+ ])
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Per-mode model assignment — the CENTRAL project store's `mode-models.json`
3
+ * (`~/.local/share/headlesscode/projects/<key>/mode-models.json`, see
4
+ * src/project-store.ts). Keyed by the repo's git-common-dir, so every worktree
5
+ * of a repo resolves the same per-mode models with zero per-directory setup.
6
+ *
7
+ * Maps a mode slug to an OpenRouter model id, with an optional `_default`
8
+ * fallback key. Kept deliberately simple (a plain local config file — no
9
+ * complexity-based routing, which is an explicit future idea, not this):
10
+ *
11
+ * { "code": "deepseek/deepseek-v4-flash", "deepseek-reviewer": "deepseek/deepseek-reasoner", "_default": "deepseek/deepseek-v4-flash" }
12
+ *
13
+ * Resolution precedence for `resolveModelForMode`:
14
+ * 1. `explicitModel` (an actual `--model` flag value) — always wins, a
15
+ * deliberate manual override.
16
+ * 2. `mode-models.json[mode]`.
17
+ * 3. `mode-models.json["_default"]`.
18
+ * 4. `env.OPENROUTER_MODEL`.
19
+ * 5. `undefined` — the caller's OpenRouter client then falls back to its
20
+ * own built-in default (`DEFAULT_MODEL` in src/llm/openrouter.ts),
21
+ * which stays the single source of truth for the ultimate default.
22
+ *
23
+ * Non-model `_`-prefixed keys extend the same file: `_condensation` assigns
24
+ * a cheaper model for the context-condensation call (consulted via extraKeys
25
+ * before the mode entry), and `_reasoning_effort` sets the graded reasoning
26
+ * effort for deepseek/* models (see resolveReasoningEffortForMode).
27
+ *
28
+ * Fail-loudly on a malformed file (invalid JSON, non-object, or a non-string
29
+ * value) — the same idiom as the central permissions.json (see
30
+ * src/permissions/config.ts): a broken config file throws with a clear
31
+ * message, it never silently falls back. A missing file is NOT an error —
32
+ * zero config means zero behavior change.
33
+ */
34
+
35
+ import * as fs from "node:fs"
36
+ import * as path from "node:path"
37
+
38
+ import { resolveProjectDataDir } from "../project-store.js"
39
+
40
+ /** Config file basename inside the central project data dir. */
41
+ export const MODE_MODELS_CONFIG_FILE = "mode-models.json"
42
+
43
+ /** Absolute path of the central mode-models.json for a workspace root. */
44
+ export function modeModelsFilePath(workspaceRoot: string): string {
45
+ return path.join(resolveProjectDataDir(workspaceRoot), MODE_MODELS_CONFIG_FILE)
46
+ }
47
+
48
+ /** The fallback key inside the config file (not a mode slug itself). */
49
+ export const DEFAULT_KEY = "_default"
50
+
51
+ /** The parsed config file: mode slug (or `_default`) -> model id. */
52
+ export type ModeModelsFile = Record<string, string>
53
+
54
+ export interface ResolveModelOptions {
55
+ workspaceRoot: string
56
+ /** Mode slug to resolve a model for (e.g. "code", "deepseek-reviewer", "qa-agent"). */
57
+ mode: string
58
+ /** An explicit `--model` flag value, if the caller passed one. Always wins. */
59
+ explicitModel?: string
60
+ /** Env to read OPENROUTER_MODEL from (default: process.env). */
61
+ env?: NodeJS.ProcessEnv
62
+ /**
63
+ * Optional EXTRA config keys consulted BEFORE the mode entry (e.g.
64
+ * `_condensation` — a cheaper model id used for the context-condensation
65
+ * LLM call; see src/engine/condense.ts). Each key is read directly from
66
+ * the config file, so an entry like `"_condensation": "qwen/qwen3-8b"`
67
+ * works without being a real mode slug. Consulted first because the
68
+ * whole point of such a key is to override whatever the session model
69
+ * resolves to — if the mode entry shadowed it, a file that sets both
70
+ * `"code"` and `"_condensation"` (the canonical example) would silently
71
+ * keep running condensation on the full-price session model. When none
72
+ * of the keys resolve, precedence falls through to `mode` → `_default`
73
+ * → `OPENROUTER_MODEL` → undefined exactly as before.
74
+ */
75
+ extraKeys?: string[]
76
+ }
77
+
78
+ /**
79
+ * Load the central `mode-models.json` for a workspace when present.
80
+ * Returns `null` when the file does not exist (the common case). A
81
+ * present-but-malformed file throws — a broken mapping must fail loudly,
82
+ * never silently fall back to a different model than the one configured.
83
+ * Falls back to the legacy `<workspaceRoot>/.headlesscode/mode-models.json`
84
+ * only when the central file is absent AND the legacy one still exists
85
+ * (pre-migration grace — the migration in resolveProjectDataDir normally
86
+ * moves it before this is reached).
87
+ */
88
+ export function loadModeModelsFile(workspaceRoot: string): ModeModelsFile | null {
89
+ const filePath = modeModelsFilePath(workspaceRoot)
90
+ let raw: string
91
+ try {
92
+ raw = fs.readFileSync(filePath, "utf-8")
93
+ } catch (err) {
94
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
95
+ const legacy = path.resolve(workspaceRoot, ".headlesscode", MODE_MODELS_CONFIG_FILE)
96
+ try {
97
+ raw = fs.readFileSync(legacy, "utf-8")
98
+ } catch {
99
+ return null
100
+ }
101
+ } else {
102
+ throw new Error(
103
+ `mode-models: cannot read mode-models file '${filePath}': ${err instanceof Error ? err.message : String(err)}`,
104
+ )
105
+ }
106
+ }
107
+
108
+ let parsed: unknown
109
+ try {
110
+ parsed = JSON.parse(raw)
111
+ } catch (err) {
112
+ throw new Error(
113
+ `mode-models: invalid JSON in '${filePath}': ${err instanceof Error ? err.message : String(err)}`,
114
+ )
115
+ }
116
+
117
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
118
+ throw new Error(`mode-models: '${filePath}' must be a JSON object of mode slug -> model id strings`)
119
+ }
120
+
121
+ const out: ModeModelsFile = {}
122
+ const obj = parsed as Record<string, unknown>
123
+ for (const [key, value] of Object.entries(obj)) {
124
+ if (typeof value !== "string") {
125
+ throw new Error(
126
+ `mode-models: '${filePath}' entry '${key}' must be a string model id, got ${JSON.stringify(value)}`,
127
+ )
128
+ }
129
+ out[key] = value
130
+ }
131
+ return out
132
+ }
133
+
134
+ /**
135
+ * Resolve the model id to use for one session, per the precedence rules in
136
+ * the file header. Returns `undefined` when nothing more specific is
137
+ * configured — the caller's OpenRouter client then applies its own built-in
138
+ * default, keeping `DEFAULT_MODEL` in src/llm/openrouter.ts the single
139
+ * source of truth for the ultimate fallback.
140
+ */
141
+ export function resolveModelForMode(options: ResolveModelOptions): string | undefined {
142
+ const { workspaceRoot, mode, explicitModel, extraKeys, env = process.env } = options
143
+
144
+ if (explicitModel !== undefined && explicitModel !== "") {
145
+ return explicitModel
146
+ }
147
+
148
+ const file = loadModeModelsFile(workspaceRoot)
149
+ if (file !== null) {
150
+ for (const key of extraKeys ?? []) {
151
+ if (typeof file[key] === "string") {
152
+ return file[key]
153
+ }
154
+ }
155
+ if (typeof file[mode] === "string") {
156
+ return file[mode]
157
+ }
158
+ if (typeof file[DEFAULT_KEY] === "string") {
159
+ return file[DEFAULT_KEY]
160
+ }
161
+ }
162
+
163
+ const envModel = env.OPENROUTER_MODEL
164
+ return envModel === undefined || envModel === "" ? undefined : envModel
165
+ }
166
+
167
+ /** The optional `_reasoning_effort` key inside mode-models.json (not a mode slug). */
168
+ export const REASONING_EFFORT_KEY = "_reasoning_effort"
169
+
170
+ export interface ResolveReasoningEffortOptions {
171
+ workspaceRoot: string
172
+ /** Env to read HEADLESSCODE_REASONING_EFFORT from (default: process.env). */
173
+ env?: NodeJS.ProcessEnv
174
+ }
175
+
176
+ /**
177
+ * Resolve the session's graded reasoning effort (issue #30 experiment) from
178
+ * the central `mode-models.json` `_reasoning_effort` key (beats env), then
179
+ * `$HEADLESSCODE_REASONING_EFFORT`, then `undefined` — no effort sent, the
180
+ * endpoint's undeclared default applies (pre-existing behavior). The value is
181
+ * a plain string in the same `Record<string, string>` format as model ids, so
182
+ * no format change is needed. The caller validates/normalizes the value
183
+ * (parseReasoningEffort in src/llm/openrouter.ts); a missing file/key/env is
184
+ * NOT an error — zero config means zero behavior change.
185
+ */
186
+ export function resolveReasoningEffortForMode(options: ResolveReasoningEffortOptions): string | undefined {
187
+ const { workspaceRoot, env = process.env } = options
188
+ const file = loadModeModelsFile(workspaceRoot)
189
+ if (file !== null && typeof file[REASONING_EFFORT_KEY] === "string") {
190
+ return file[REASONING_EFFORT_KEY]
191
+ }
192
+ const envValue = env.HEADLESSCODE_REASONING_EFFORT
193
+ return envValue === undefined || envValue === "" ? undefined : envValue
194
+ }
195
+
196
+ /**
197
+ * Validate a prospective config-file body (used by the dashboard settings
198
+ * endpoint so a malformed save is rejected server-side instead of writing
199
+ * garbage that later throws when a worker reads it). Throws on invalid input;
200
+ * a `{...}` object of string values always passes.
201
+ */
202
+ export function validateModeModelsBody(body: unknown): asserts body is ModeModelsFile {
203
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
204
+ throw new Error("mode-models: body must be a JSON object of mode slug -> model id strings")
205
+ }
206
+ for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
207
+ if (typeof value !== "string") {
208
+ throw new Error(
209
+ `mode-models: entry '${key}' must be a string model id, got ${JSON.stringify(value)}`,
210
+ )
211
+ }
212
+ }
213
+ }
214
+
215
+ /** Pretty-printed canonical serialization for the settings file. */
216
+ export function stringifyModeModelsFile(file: ModeModelsFile): string {
217
+ return JSON.stringify(file, null, 2) + "\n"
218
+ }