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,193 @@
1
+ /**
2
+ * `headlesscode dashboard` subcommand — a local, auto-refreshing cost/token
3
+ * dashboard (workstream 3) + live per-session event feed + pause/resume
4
+ * control (live worker monitoring).
5
+ *
6
+ * npx tsx src/cli.ts dashboard [--port 4390] [--repo <path>]
7
+ *
8
+ * Thin dispatch to src/dashboard/server.ts — no logic duplicated here. When
9
+ * `--repo` is omitted the page still starts but shows no sessions/round data
10
+ * (the aggregation gracefully degrades — see src/dashboard/aggregate.ts).
11
+ *
12
+ * NOTE (Phase 3 scope change): the dashboard is no longer purely read-only —
13
+ * the page's pause/resume buttons issue state-changing POSTs to the running
14
+ * worker (see src/dashboard/server.ts's header note). Still local-only +
15
+ * no-auth (binds 127.0.0.1), but that assumption needs revisiting if this is
16
+ * ever exposed beyond localhost.
17
+ */
18
+
19
+ import { DEFAULT_DASHBOARD_PORT, startDashboardServer } from "./server.js"
20
+
21
+ const DASHBOARD_USAGE = `headlesscode dashboard — local cost/token dashboard + live worker control
22
+
23
+ Usage:
24
+ headlesscode dashboard [--port 4390] [--repo <path>]
25
+
26
+ Options:
27
+ --port <n> Port to listen on (default: 4390, or $HEADLESSCODE_DASHBOARD_PORT)
28
+ --repo <path> Repo root to scan for .headlesscode/usage/*.jsonl (own +
29
+ .worktrees/*) and .worktrees/.orchestrator-state.json.
30
+ Omit to start the page with no data (still useful to confirm
31
+ the server runs).
32
+ --help Show this help and exit
33
+
34
+ Environment:
35
+ HEADLESSCODE_DASHBOARD_TOKEN Optional bearer token for the control-plane
36
+ POST routes (/api/session/start|pause|resume|answer) and
37
+ the destructive /api/checkpoints/restore. Unset (default) =
38
+ no auth. Local-only tool; see the notes below.
39
+ HEADLESSCODE_DASHBOARD_SESSION_CLI Override the CLI invocation
40
+ POST /api/session/start spawns (default: "npx tsx
41
+ src/cli.ts", resolved from HEADLESSCODE_DASHBOARD_SESSION_REPO_ROOT).
42
+ Not a CLI flag deliberately (DashboardServerOptions.cli is a
43
+ test-only injection point) — this exists for container
44
+ deployments where the dashboard runs from a live bind-mounted
45
+ source tree but "npx tsx" has no node_modules there to
46
+ resolve against (see docker-compose.headlesscode-local.yml).
47
+ HEADLESSCODE_DASHBOARD_SESSION_REPO_ROOT Override the directory a launched
48
+ session's CLI runs in (default: process.cwd()). Same
49
+ container rationale as above — the dashboard's own cwd may
50
+ be a baked image copy while the live source lives elsewhere.
51
+ HEADLESSCODE_DASHBOARD_WORKTREE_ROOT Root directory for POST
52
+ /api/projects/ensure-worktree's disposable per-project
53
+ worktrees. Unset (default) = that endpoint responds 501.
54
+ HEADLESSCODE_DASHBOARD_WORKTREE_OWNER chown target (e.g. "1000:1000")
55
+ for a newly-created worktree and the ref/admin files
56
+ 'git worktree add' writes into the source repo's own
57
+ .git — read directly by session-launch.ts, not a
58
+ startDashboardServer option. Only matters when this
59
+ process runs as root (a container's usual default);
60
+ unset (default) = skip, leaving those files owned by
61
+ whoever ran the dashboard.
62
+
63
+ Notes:
64
+ Binds to 127.0.0.1 only — no auth unless HEADLESSCODE_DASHBOARD_TOKEN is
65
+ set. Never expose this port beyond localhost.
66
+ IMPORTANT (browser control plane): the "start a session" endpoint spawns
67
+ the CLI as a detached child that INHERITS this process's environment — it
68
+ does NOT re-source .env. Export HEADLESSCODE_OPENROUTER_API_KEY (e.g. 'set -a; source
69
+ .env; set +a') before starting the dashboard, or launched sessions will
70
+ fail immediately with an OpenRouter auth error.
71
+ `
72
+
73
+ interface DashboardCliOptions {
74
+ port: number
75
+ repo?: string
76
+ help: boolean
77
+ }
78
+
79
+ export function parseDashboardArgs(argv: string[]): { options: DashboardCliOptions; error?: string } {
80
+ const options: DashboardCliOptions = {
81
+ port: Number(process.env.HEADLESSCODE_DASHBOARD_PORT ?? DEFAULT_DASHBOARD_PORT),
82
+ help: false,
83
+ }
84
+
85
+ for (let i = 0; i < argv.length; i++) {
86
+ const arg = argv[i]
87
+ const eq = arg.indexOf("=")
88
+ const flag = eq === -1 ? arg : arg.slice(0, eq)
89
+ const inlineValue = eq === -1 ? undefined : arg.slice(eq + 1)
90
+ const next = (): string | undefined => {
91
+ if (inlineValue !== undefined) {
92
+ return inlineValue
93
+ }
94
+ const v = argv[i + 1]
95
+ if (v === undefined || v.startsWith("--")) {
96
+ return undefined
97
+ }
98
+ i++
99
+ return v
100
+ }
101
+
102
+ switch (flag) {
103
+ case "--port": {
104
+ const v = next()
105
+ const n = v === undefined ? Number.NaN : Number(v)
106
+ if (!Number.isInteger(n) || n <= 0 || n > 65535) {
107
+ return { options, error: "--port requires an integer between 1 and 65535" }
108
+ }
109
+ options.port = n
110
+ break
111
+ }
112
+ case "--repo": {
113
+ const v = next()
114
+ if (v === undefined) {
115
+ return { options, error: "Missing value for --repo" }
116
+ }
117
+ options.repo = v
118
+ break
119
+ }
120
+ case "--help":
121
+ case "-h":
122
+ options.help = true
123
+ break
124
+ default:
125
+ return { options, error: `Unknown argument: ${arg}` }
126
+ }
127
+ }
128
+
129
+ return { options }
130
+ }
131
+
132
+ export async function dashboardMain(argv: string[]): Promise<number> {
133
+ const { options, error } = parseDashboardArgs(argv)
134
+ if (error) {
135
+ process.stderr.write(`headlesscode dashboard: ${error}\n\n${DASHBOARD_USAGE}`)
136
+ return 2
137
+ }
138
+ if (options.help) {
139
+ process.stdout.write(DASHBOARD_USAGE)
140
+ return 0
141
+ }
142
+
143
+ let server: Awaited<ReturnType<typeof startDashboardServer>>
144
+ try {
145
+ server = await startDashboardServer({
146
+ port: options.port,
147
+ repo: options.repo,
148
+ token: process.env.HEADLESSCODE_DASHBOARD_TOKEN || undefined,
149
+ cli: process.env.HEADLESSCODE_DASHBOARD_SESSION_CLI || undefined,
150
+ repoRoot: process.env.HEADLESSCODE_DASHBOARD_SESSION_REPO_ROOT || undefined,
151
+ worktreeRoot: process.env.HEADLESSCODE_DASHBOARD_WORKTREE_ROOT || undefined,
152
+ })
153
+ } catch (err) {
154
+ const message = err instanceof Error ? err.message : String(err)
155
+ if ((err as NodeJS.ErrnoException)?.code === "EADDRINUSE") {
156
+ process.stderr.write(
157
+ `headlesscode dashboard: port ${options.port} is already in use. Pick another with --port <n>.\n`,
158
+ )
159
+ return 2
160
+ }
161
+ process.stderr.write(`headlesscode dashboard: failed to start: ${message}\n`)
162
+ return 2
163
+ }
164
+
165
+ process.stdout.write(
166
+ `[dashboard] listening on http://127.0.0.1:${options.port}${options.repo ? ` (repo: ${options.repo})` : " (no --repo given; page will show no data)"}\n`,
167
+ )
168
+
169
+ // Keep the process alive until interrupted; clean shutdown on SIGINT/SIGTERM.
170
+ return new Promise<number>((resolve) => {
171
+ let shuttingDown = false
172
+ const onSignal = (): void => {
173
+ // Only the first signal shuts down; a second Ctrl-C while the
174
+ // server is mid-close is ignored (the process exits on its own
175
+ // once the listening handle is gone).
176
+ if (shuttingDown) {
177
+ return
178
+ }
179
+ shuttingDown = true
180
+ process.stdout.write("\n[dashboard] shutting down\n")
181
+ process.removeListener("SIGINT", onSignal)
182
+ process.removeListener("SIGTERM", onSignal)
183
+ // Release the port: stop accepting new connections and drop
184
+ // keep-alive sockets so the process can exit. Without close the
185
+ // listening handle keeps the event loop alive and the port bound.
186
+ server.close()
187
+ server.closeAllConnections?.()
188
+ resolve(0)
189
+ }
190
+ process.on("SIGINT", onSignal)
191
+ process.on("SIGTERM", onSignal)
192
+ })
193
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Dashboard read-side for the codemap: serves the stored codemap.json /
3
+ * codemap.html from the CENTRAL per-project store (same resolution the
4
+ * `headlesscode codemap` CLI writes to — see src/codemap/lock.ts). The
5
+ * dashboard deliberately does NOT generate the map on request (it's a
6
+ * deterministic script job, run by the CLI/watcher); it just serves what's
7
+ * there and gives a clear pointer when nothing has been generated yet.
8
+ */
9
+
10
+ import * as fs from "node:fs"
11
+
12
+ import { codemapHtmlPath, codemapJsonPath } from "../codemap/lock.js"
13
+
14
+ export interface CodemapFileResult {
15
+ content: string
16
+ contentType: string
17
+ }
18
+
19
+ /** The stored codemap.json content; undefined when not generated yet. */
20
+ export function readCodemapJson(workspaceRoot: string): CodemapFileResult | undefined {
21
+ return readFileIfPresent(codemapJsonPath(workspaceRoot), "application/json; charset=utf-8")
22
+ }
23
+
24
+ /** The stored codemap.html content; undefined when not generated yet. */
25
+ export function readCodemapHtml(workspaceRoot: string): CodemapFileResult | undefined {
26
+ return readFileIfPresent(codemapHtmlPath(workspaceRoot), "text/html; charset=utf-8")
27
+ }
28
+
29
+ /** The error message + exit hint a route returns when the map doesn't exist yet. */
30
+ export function codemapMissingError(workspaceRoot: string): string {
31
+ return (
32
+ `no codemap generated for ${workspaceRoot} yet — run ` +
33
+ `\`headlesscode codemap --workspace ${workspaceRoot}\` (or start it in --watch mode) to build it.`
34
+ )
35
+ }
36
+
37
+ function readFileIfPresent(file: string, contentType: string): CodemapFileResult | undefined {
38
+ try {
39
+ const content = fs.readFileSync(file, "utf-8")
40
+ return { content, contentType }
41
+ } catch {
42
+ return undefined
43
+ }
44
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Dashboard file-browser backend — thin adapters over the tool executor's
3
+ * existing, already-tested directory-listing logic.
4
+ *
5
+ * The non-negotiable rule here is path safety: every path that crosses the
6
+ * HTTP surface goes through `resolveWithinWorkspace` / `PathTraversalError`
7
+ * (src/tools/executor.ts) — the SAME guard the model-facing tools use. A bug
8
+ * here would be a real directory-traversal vulnerability on a locally-running
9
+ * HTTP server, so this module deliberately reuses that guard instead of
10
+ * reimplementing containment checking.
11
+ *
12
+ * Listing semantics mirror `list_files` (`listFilesHandler` in
13
+ * src/tools/executor.ts): a raw `readdir` of the requested directory, no
14
+ * `.gitignore` filtering (the model-facing tool doesn't filter either — the
15
+ * gitignore-aware walker lives in src/codesearch/files.ts for the codebase
16
+ * index, a different surface). Each entry carries its name, type, and size so
17
+ * the browser can render a tree without extra round-trips.
18
+ *
19
+ * Content retrieval mirrors `read_file`'s size discipline: files over
20
+ * `MAX_RESULT_CHARS` (30k chars, the same cap tool results use to keep model
21
+ * context bounded) or containing NUL bytes (a strong binary signal) return a
22
+ * clear "not previewable" response instead of dumping raw bytes into a JSON
23
+ * response.
24
+ */
25
+
26
+ import * as fsp from "node:fs/promises"
27
+ import * as path from "node:path"
28
+
29
+ import { resolveWithinWorkspace, PathTraversalError, MAX_RESULT_CHARS } from "../tools/executor.js"
30
+ import { DEFAULT_PROTECTED_FILES, isProtectedPath } from "../permissions/protected-files.js"
31
+
32
+ /** One directory entry, ready for JSON. */
33
+ export interface FileEntry {
34
+ /** Entry name (basename) — NOT a path, safe to render directly. */
35
+ name: string
36
+ type: "file" | "dir"
37
+ /** Size in bytes (0 for directories). */
38
+ size: number
39
+ }
40
+
41
+ /**
42
+ * List a directory's entries for a workspace. `dir` is a workspace-relative
43
+ * path (default "."). Throws `PathTraversalError` when the requested path
44
+ * escapes the workspace — callers (the HTTP layer) translate that to a 400.
45
+ */
46
+ export async function listWorkspaceDir(workspaceRoot: string, dir: string): Promise<FileEntry[]> {
47
+ const root = path.resolve(workspaceRoot)
48
+ const target = resolveWithinWorkspace(root, dir)
49
+
50
+ const dirents = await fsp.readdir(target, { withFileTypes: true })
51
+ const out: FileEntry[] = []
52
+ for (const ent of dirents) {
53
+ const abs = path.join(target, ent.name)
54
+ let size = 0
55
+ if (ent.isFile()) {
56
+ try {
57
+ const st = await fsp.stat(abs)
58
+ size = st.size
59
+ } catch {
60
+ // File vanished between readdir and stat — report size 0.
61
+ }
62
+ }
63
+ out.push({ name: ent.name, type: ent.isDirectory() ? "dir" : "file", size })
64
+ }
65
+ // Dirs first, then alphabetical — same ordering list_files uses.
66
+ out.sort((a, b) => {
67
+ if (a.type !== b.type) {
68
+ return a.type === "dir" ? -1 : 1
69
+ }
70
+ return a.name.localeCompare(b.name)
71
+ })
72
+ return out
73
+ }
74
+
75
+ /** Result of a file-content read: previewable text, or a clear refusal. */
76
+ export type FileContentResult =
77
+ | { previewable: true; content: string }
78
+ | { previewable: false; reason: string }
79
+
80
+ /**
81
+ * Read a single file's content for browser preview. Only text files are
82
+ * previewable: content over `MAX_RESULT_CHARS` or containing NUL bytes (the
83
+ * strongest cheap binary signal) returns a "not previewable" reason instead
84
+ * of raw bytes. Throws `PathTraversalError` for escaping paths.
85
+ *
86
+ * SEC-7: files matching the protected-file patterns (`.env`, `*.pem`, `*.key`,
87
+ * `id_rsa*`, etc. — the same defaults `src/permissions/protected-files.ts`
88
+ * uses to stop the WRITE tools from touching secrets) are refused here too,
89
+ * unconditionally — the dashboard's GET file-content route must not become a
90
+ * read side-channel for the exact files the write-side guard protects. This
91
+ * check is independent of the optional bearer-token gate in server.ts (it
92
+ * applies even when no token is configured).
93
+ */
94
+ export async function readWorkspaceFile(workspaceRoot: string, file: string): Promise<FileContentResult> {
95
+ const root = path.resolve(workspaceRoot)
96
+ const target = resolveWithinWorkspace(root, file)
97
+
98
+ const relFromRoot = path.relative(root, target).replace(/\\/g, "/")
99
+ if (isProtectedPath(relFromRoot, DEFAULT_PROTECTED_FILES)) {
100
+ return { previewable: false, reason: "protected file — not previewable via the dashboard (matches a secrets pattern like .env/*.key/*.pem)" }
101
+ }
102
+
103
+ const st = await fsp.stat(target)
104
+ if (!st.isFile()) {
105
+ return { previewable: false, reason: "not a file" }
106
+ }
107
+ if (st.size > MAX_RESULT_CHARS) {
108
+ return {
109
+ previewable: false,
110
+ reason: `file is ${st.size.toLocaleString()} bytes — over the ${MAX_RESULT_CHARS.toLocaleString()}-char preview cap (same convention as read_file's MAX_RESULT_CHARS)`,
111
+ }
112
+ }
113
+
114
+ const buf = await fsp.readFile(target)
115
+ if (buf.includes(0)) {
116
+ return { previewable: false, reason: "binary file — contains NUL bytes, not text-previewable" }
117
+ }
118
+ // UTF-8 decode; replacement chars for invalid sequences are acceptable
119
+ // here (the NUL check above already caught the common binary case).
120
+ return { previewable: true, content: buf.toString("utf-8") }
121
+ }