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,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
|
+
}
|