@100xprompt/brain 0.0.0-stage → 0.1.0

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 (213) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +172 -2
  3. package/package.json +48 -4
  4. package/src/adapters/aider/index.ts +8 -0
  5. package/src/adapters/aider/reader.ts +129 -0
  6. package/src/adapters/aider/records.ts +220 -0
  7. package/src/adapters/aider/writer.ts +269 -0
  8. package/src/adapters/amazon-q-cli/index.ts +8 -0
  9. package/src/adapters/amazon-q-cli/reader.ts +84 -0
  10. package/src/adapters/amazon-q-cli/records.ts +191 -0
  11. package/src/adapters/amazon-q-cli/writer.ts +370 -0
  12. package/src/adapters/amazon-q-ide/index.ts +9 -0
  13. package/src/adapters/amazon-q-ide/reader.ts +131 -0
  14. package/src/adapters/amazon-q-ide/records.ts +141 -0
  15. package/src/adapters/amazon-q-ide/writer.ts +325 -0
  16. package/src/adapters/antigravity/index.ts +9 -0
  17. package/src/adapters/antigravity/proto.ts +194 -0
  18. package/src/adapters/antigravity/reader.ts +424 -0
  19. package/src/adapters/antigravity/records.ts +236 -0
  20. package/src/adapters/antigravity/writer.ts +463 -0
  21. package/src/adapters/auggie/index.ts +36 -0
  22. package/src/adapters/claude-code/index.ts +8 -0
  23. package/src/adapters/claude-code/reader.ts +639 -0
  24. package/src/adapters/claude-code/records.ts +88 -0
  25. package/src/adapters/claude-code/writer.ts +329 -0
  26. package/src/adapters/cline/index.ts +12 -0
  27. package/src/adapters/cline/reader.ts +432 -0
  28. package/src/adapters/cline/records.ts +151 -0
  29. package/src/adapters/cline/vscdb.ts +56 -0
  30. package/src/adapters/cline/writer.ts +656 -0
  31. package/src/adapters/cline/xml.ts +177 -0
  32. package/src/adapters/cline4/index.ts +9 -0
  33. package/src/adapters/cline4/reader.ts +179 -0
  34. package/src/adapters/cline4/records.ts +92 -0
  35. package/src/adapters/cline4/writer.ts +361 -0
  36. package/src/adapters/codebuff/index.ts +8 -0
  37. package/src/adapters/codebuff/project.ts +207 -0
  38. package/src/adapters/codebuff/reader.ts +215 -0
  39. package/src/adapters/codebuff/records.ts +173 -0
  40. package/src/adapters/codebuff/writer.ts +383 -0
  41. package/src/adapters/codex/index.ts +8 -0
  42. package/src/adapters/codex/native.ts +239 -0
  43. package/src/adapters/codex/reader.ts +747 -0
  44. package/src/adapters/codex/records.ts +114 -0
  45. package/src/adapters/codex/writer.ts +479 -0
  46. package/src/adapters/continue/index.ts +9 -0
  47. package/src/adapters/continue/reader.ts +68 -0
  48. package/src/adapters/continue/records.ts +418 -0
  49. package/src/adapters/continue/writer.ts +162 -0
  50. package/src/adapters/copilot-cli/index.ts +9 -0
  51. package/src/adapters/copilot-cli/reader.ts +186 -0
  52. package/src/adapters/copilot-cli/records.ts +241 -0
  53. package/src/adapters/copilot-cli/writer.ts +353 -0
  54. package/src/adapters/crush/index.ts +8 -0
  55. package/src/adapters/crush/reader.ts +180 -0
  56. package/src/adapters/crush/records.ts +169 -0
  57. package/src/adapters/crush/writer.ts +333 -0
  58. package/src/adapters/cursor-cli/index.ts +405 -0
  59. package/src/adapters/cursor-ide/agentkv.ts +95 -0
  60. package/src/adapters/cursor-ide/index.ts +501 -0
  61. package/src/adapters/cursor-ide/transcripts.ts +122 -0
  62. package/src/adapters/cursor-ide/workspaces.ts +119 -0
  63. package/src/adapters/cursor-ide/writer.ts +170 -0
  64. package/src/adapters/devin/index.ts +8 -0
  65. package/src/adapters/devin/reader.ts +305 -0
  66. package/src/adapters/devin/records.ts +277 -0
  67. package/src/adapters/devin/writer.ts +523 -0
  68. package/src/adapters/factory-droid/index.ts +12 -0
  69. package/src/adapters/factory-droid/reader.ts +184 -0
  70. package/src/adapters/factory-droid/records.ts +79 -0
  71. package/src/adapters/factory-droid/writer.ts +350 -0
  72. package/src/adapters/gemini-cli/exchanges.ts +228 -0
  73. package/src/adapters/gemini-cli/index.ts +396 -0
  74. package/src/adapters/gemini-cli/records.ts +521 -0
  75. package/src/adapters/gemini-cli/writer.ts +354 -0
  76. package/src/adapters/goose/index.ts +8 -0
  77. package/src/adapters/goose/reader.ts +138 -0
  78. package/src/adapters/goose/records.ts +199 -0
  79. package/src/adapters/goose/writer.ts +340 -0
  80. package/src/adapters/grok-cli/image.ts +44 -0
  81. package/src/adapters/grok-cli/index.ts +8 -0
  82. package/src/adapters/grok-cli/reader.ts +302 -0
  83. package/src/adapters/grok-cli/records.ts +172 -0
  84. package/src/adapters/grok-cli/writer.ts +329 -0
  85. package/src/adapters/hermes/index.ts +8 -0
  86. package/src/adapters/hermes/reader.ts +249 -0
  87. package/src/adapters/hermes/records.ts +285 -0
  88. package/src/adapters/hermes/writer.ts +452 -0
  89. package/src/adapters/iflow-cli/index.ts +72 -0
  90. package/src/adapters/iflow-cli/records.ts +215 -0
  91. package/src/adapters/iflow-cli/writer.ts +204 -0
  92. package/src/adapters/index.ts +112 -0
  93. package/src/adapters/kilo-cli/index.ts +21 -0
  94. package/src/adapters/kilo-code/index.ts +11 -0
  95. package/src/adapters/kilo-code/writer.ts +92 -0
  96. package/src/adapters/kimi/index.ts +9 -0
  97. package/src/adapters/kimi/reader.ts +92 -0
  98. package/src/adapters/kimi/records.ts +221 -0
  99. package/src/adapters/kimi/writer.ts +367 -0
  100. package/src/adapters/kiro-cli/index.ts +8 -0
  101. package/src/adapters/kiro-cli/reader.ts +87 -0
  102. package/src/adapters/kiro-cli/records.ts +260 -0
  103. package/src/adapters/kiro-cli/writer.ts +305 -0
  104. package/src/adapters/kiro-ide/index.ts +8 -0
  105. package/src/adapters/kiro-ide/writer.ts +52 -0
  106. package/src/adapters/letta/index.ts +8 -0
  107. package/src/adapters/letta/reader.ts +177 -0
  108. package/src/adapters/letta/records.ts +119 -0
  109. package/src/adapters/letta/writer.ts +413 -0
  110. package/src/adapters/mastracode/index.ts +8 -0
  111. package/src/adapters/mastracode/reader.ts +323 -0
  112. package/src/adapters/mastracode/records.ts +336 -0
  113. package/src/adapters/mastracode/writer.ts +385 -0
  114. package/src/adapters/mistral-vibe/index.ts +9 -0
  115. package/src/adapters/mistral-vibe/reader.ts +62 -0
  116. package/src/adapters/mistral-vibe/records.ts +104 -0
  117. package/src/adapters/mistral-vibe/writer.ts +359 -0
  118. package/src/adapters/muse/index.ts +10 -0
  119. package/src/adapters/muse/reader.ts +332 -0
  120. package/src/adapters/muse/records.ts +104 -0
  121. package/src/adapters/muse/writer.ts +387 -0
  122. package/src/adapters/omp/index.ts +13 -0
  123. package/src/adapters/omp/reader.ts +314 -0
  124. package/src/adapters/omp/records.ts +153 -0
  125. package/src/adapters/omp/writer.ts +216 -0
  126. package/src/adapters/opencode/ids.ts +52 -0
  127. package/src/adapters/opencode/index.ts +31 -0
  128. package/src/adapters/opencode/note.ts +8 -0
  129. package/src/adapters/opencode/reader.ts +452 -0
  130. package/src/adapters/opencode/tools.ts +174 -0
  131. package/src/adapters/opencode/v2.ts +367 -0
  132. package/src/adapters/opencode/writer.ts +379 -0
  133. package/src/adapters/openhands/index.ts +8 -0
  134. package/src/adapters/openhands/reader.ts +222 -0
  135. package/src/adapters/openhands/records.ts +136 -0
  136. package/src/adapters/openhands/writer.ts +404 -0
  137. package/src/adapters/pearai/index.ts +10 -0
  138. package/src/adapters/pi/index.ts +28 -0
  139. package/src/adapters/pi/reader.ts +261 -0
  140. package/src/adapters/pi/records.ts +99 -0
  141. package/src/adapters/pi/writer.ts +362 -0
  142. package/src/adapters/qoder-cli/index.ts +8 -0
  143. package/src/adapters/qoder-cli/reader.ts +83 -0
  144. package/src/adapters/qoder-cli/records.ts +45 -0
  145. package/src/adapters/qoder-cli/writer.ts +72 -0
  146. package/src/adapters/qwen-code/index.ts +84 -0
  147. package/src/adapters/qwen-code/records.ts +194 -0
  148. package/src/adapters/qwen-code/writer.ts +224 -0
  149. package/src/adapters/tabnine-cli/index.ts +8 -0
  150. package/src/adapters/tabnine-cli/reader.ts +106 -0
  151. package/src/adapters/tabnine-cli/records.ts +33 -0
  152. package/src/adapters/tabnine-cli/writer.ts +156 -0
  153. package/src/adapters/types.ts +73 -0
  154. package/src/adapters/void/index.ts +9 -0
  155. package/src/adapters/void/reader.ts +48 -0
  156. package/src/adapters/void/records.ts +285 -0
  157. package/src/adapters/void/writer.ts +93 -0
  158. package/src/adapters/vscode-copilot/index.ts +9 -0
  159. package/src/adapters/vscode-copilot/reader.ts +212 -0
  160. package/src/adapters/vscode-copilot/records.ts +388 -0
  161. package/src/adapters/vscode-copilot/shared.ts +68 -0
  162. package/src/adapters/vscode-copilot/writer.ts +546 -0
  163. package/src/adapters/zed/index.ts +8 -0
  164. package/src/adapters/zed/reader.ts +198 -0
  165. package/src/adapters/zed/records.ts +87 -0
  166. package/src/adapters/zed/writer.ts +341 -0
  167. package/src/brief-writers.ts +71 -0
  168. package/src/brief.ts +238 -0
  169. package/src/cli.ts +326 -0
  170. package/src/fsutil.ts +133 -0
  171. package/src/gateway/connect.ts +845 -0
  172. package/src/gateway/signin.ts +323 -0
  173. package/src/handoff.ts +104 -0
  174. package/src/hooks.ts +455 -0
  175. package/src/memory/audit.ts +514 -0
  176. package/src/memory/blobs.ts +94 -0
  177. package/src/memory/cli.ts +117 -0
  178. package/src/memory/distill.ts +381 -0
  179. package/src/memory/externalize.ts +435 -0
  180. package/src/memory/lookups.ts +70 -0
  181. package/src/memory/mcp-install.ts +401 -0
  182. package/src/memory/mcp-server.ts +296 -0
  183. package/src/memory/prepare.ts +179 -0
  184. package/src/memory/preview.ts +154 -0
  185. package/src/memory/project.ts +35 -0
  186. package/src/memory/redact.ts +72 -0
  187. package/src/memory/relevance.ts +187 -0
  188. package/src/memory/repomap.ts +516 -0
  189. package/src/memory/store.ts +270 -0
  190. package/src/memory/sync.ts +166 -0
  191. package/src/memory/tidy.ts +193 -0
  192. package/src/memory/types.ts +118 -0
  193. package/src/org/admin-ops.ts +96 -0
  194. package/src/org/auth.ts +112 -0
  195. package/src/org/backup.ts +115 -0
  196. package/src/org/cli.ts +438 -0
  197. package/src/org/client.ts +404 -0
  198. package/src/org/config.ts +44 -0
  199. package/src/org/crypto.ts +537 -0
  200. package/src/org/db.ts +435 -0
  201. package/src/org/graph.ts +318 -0
  202. package/src/org/oidc.ts +435 -0
  203. package/src/org/policy.ts +398 -0
  204. package/src/org/server.ts +920 -0
  205. package/src/org/sso.ts +316 -0
  206. package/src/org/tls.ts +66 -0
  207. package/src/org/types.ts +193 -0
  208. package/src/self.ts +68 -0
  209. package/src/session/attachments.ts +53 -0
  210. package/src/session/peekcache.ts +50 -0
  211. package/src/session/types.ts +104 -0
  212. package/src/session/util.ts +130 -0
  213. package/src/sqlite.ts +48 -0
@@ -0,0 +1,516 @@
1
+ // A compact, persistent map of a code folder: what is where and what each important file exports, so an agent does
2
+ // not rediscover the same files every session. Built from the git working tree (`git ls-files`, so .gitignore is
3
+ // respected) with regex-level symbol extraction, no model, no network, deterministic.
4
+ //
5
+ // Freshness: a stale map is worse than none, so the cache key is HEAD + `git status` + the size and mtime of every
6
+ // file git reports as modified or untracked (an edit that leaves the status letter unchanged still changes the key).
7
+ // Cached at <home>/.brain/cache/repomap-<hash of cwd>.json; BRAIN_NO_CACHE=1 bypasses it.
8
+ import { createHash } from "node:crypto"
9
+ import { existsSync, mkdirSync, openSync, readFileSync, readSync, closeSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs"
10
+ import { basename, extname, join, resolve } from "node:path"
11
+ import { approxTokens } from "./types.ts"
12
+
13
+ export const REPOMAP_TOKENS = 1_500
14
+ /** Files over this many bytes are never read. */
15
+ export const MAX_FILE_BYTES = 200 * 1024
16
+ const MAX_FILES_READ = 4_000
17
+ const MAX_TOTAL_BYTES = 48 * 1024 * 1024
18
+ const MAX_WALK_FILES = 6_000
19
+ const MAX_SYMBOLS_PER_FILE = 14
20
+ const MAX_LINE = 260
21
+ /** Bumped when the output format changes, so old caches are not served. */
22
+ const FORMAT = 2
23
+
24
+ export interface RepoMap {
25
+ text: string
26
+ tokens: number
27
+ /** The freshness key the map was built for (HEAD plus a hash of the working-tree state), when known. */
28
+ head?: string
29
+ }
30
+
31
+ // ---------------------------------------------------------------- files
32
+
33
+ interface Listing {
34
+ files: string[] // relative to cwd, '/' separated, sorted
35
+ git: boolean
36
+ /** Recently changed files, newest first (git only). */
37
+ recent: string[]
38
+ /** Files with uncommitted changes (git only). */
39
+ dirty: Set<string>
40
+ }
41
+
42
+ const SKIP_DIRS = new Set(["node_modules", "dist", "build", "out", "target", "vendor", "coverage", "venv", "env", "__pycache__", "site-packages", "bower_components"])
43
+
44
+ function git(cwd: string, args: string[]): string | undefined {
45
+ try {
46
+ const p = Bun.spawnSync(["git", "-c", "core.quotepath=off", ...args], { cwd, stdout: "pipe", stderr: "ignore", env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } })
47
+ return p.exitCode === 0 ? p.stdout.toString() : undefined
48
+ } catch {
49
+ return undefined
50
+ }
51
+ }
52
+
53
+ function walk(cwd: string): string[] {
54
+ const out: string[] = []
55
+ const rec = (rel: string, depth: number) => {
56
+ if (out.length >= MAX_WALK_FILES || depth > 8) return
57
+ let names: string[]
58
+ try {
59
+ names = readdirSync(join(cwd, rel)).sort()
60
+ } catch {
61
+ return
62
+ }
63
+ for (const name of names) {
64
+ if (out.length >= MAX_WALK_FILES) return
65
+ if (name.startsWith(".")) continue
66
+ const r = rel ? `${rel}/${name}` : name
67
+ let st
68
+ try {
69
+ st = statSync(join(cwd, r))
70
+ } catch {
71
+ continue
72
+ }
73
+ if (st.isDirectory()) {
74
+ if (!SKIP_DIRS.has(name)) rec(r, depth + 1)
75
+ } else if (st.isFile()) out.push(r)
76
+ }
77
+ }
78
+ rec("", 0)
79
+ return out
80
+ }
81
+
82
+ function list(cwd: string): Listing {
83
+ const inside = git(cwd, ["rev-parse", "--is-inside-work-tree"])?.trim() === "true"
84
+ if (!inside) return { files: walk(cwd), git: false, recent: [], dirty: new Set() }
85
+ const tracked = (git(cwd, ["ls-files", "-co", "--exclude-standard", "-z"]) ?? "").split("\0").filter(Boolean)
86
+ // A tracked file that was deleted in the working tree is still listed by ls-files.
87
+ const files = [...new Set(tracked)].filter((f) => !f.split("/").includes("node_modules") && existsSync(join(cwd, f))).sort()
88
+ const recent: string[] = []
89
+ const log = git(cwd, ["log", "-n", "300", "--name-only", "--pretty=format:", "--relative", "--", "."]) ?? ""
90
+ const have = new Set(files)
91
+ for (const l of log.split("\n")) if (l && have.has(l) && !recent.includes(l)) recent.push(l)
92
+ return { files, git: true, recent, dirty: new Set(dirtyFiles(cwd)) }
93
+ }
94
+
95
+ /** Files with uncommitted changes or not yet tracked, relative to cwd. */
96
+ function dirtyFiles(cwd: string): string[] {
97
+ const out = git(cwd, ["ls-files", "-m", "-o", "--exclude-standard", "-z"]) ?? ""
98
+ return [...new Set(out.split("\0").filter(Boolean))].sort()
99
+ }
100
+
101
+ /** The freshness key of the folder's current state. Cheap: a few git calls and a stat per dirty file. */
102
+ export function repoKey(cwd: string, maxTokens = REPOMAP_TOKENS): string {
103
+ const h = createHash("sha256")
104
+ h.update(`v${FORMAT}\0${maxTokens}\0`)
105
+ const inside = git(cwd, ["rev-parse", "--is-inside-work-tree"])?.trim() === "true"
106
+ let head = "nogit"
107
+ if (inside) {
108
+ head = git(cwd, ["rev-parse", "HEAD"])?.trim() || "nohead"
109
+ h.update(`${head}\0`)
110
+ h.update(git(cwd, ["status", "--porcelain", "-uall", "--", "."]) ?? "")
111
+ for (const f of dirtyFiles(cwd)) h.update(`\0${f}:${statKey(join(cwd, f))}`)
112
+ // A staged-only or committed-away change shows in status/HEAD; the stats above catch content edits.
113
+ } else {
114
+ for (const f of walk(cwd)) h.update(`\0${f}:${statKey(join(cwd, f))}`)
115
+ }
116
+ return `${head.slice(0, 12)}-${h.digest("hex").slice(0, 16)}`
117
+ }
118
+
119
+ function statKey(p: string): string {
120
+ try {
121
+ const s = statSync(p)
122
+ return `${s.size}:${Math.round(s.mtimeMs)}`
123
+ } catch {
124
+ return "gone"
125
+ }
126
+ }
127
+
128
+ // ---------------------------------------------------------------- symbols
129
+
130
+ const LANG: Record<string, string> = {
131
+ ".ts": "ts", ".tsx": "ts", ".mts": "ts", ".cts": "ts", ".js": "ts", ".jsx": "ts", ".mjs": "ts", ".cjs": "ts",
132
+ ".py": "py", ".go": "go", ".rs": "rs", ".java": "java", ".kt": "kt", ".kts": "kt", ".rb": "rb",
133
+ ".h": "c", ".hpp": "c", ".hh": "c", ".hxx": "c",
134
+ }
135
+ /** Source files we read for mentions even though they yield no symbols (so references from them still count). */
136
+ const MENTION_ONLY = new Set([".c", ".cc", ".cpp", ".cxx", ".swift", ".cs", ".php", ".scala", ".sh", ".vue", ".svelte", ".md", ".json", ".toml", ".yaml", ".yml"])
137
+
138
+ type Kind = "fn" | "class" | "type" | "const" | "mod"
139
+ interface Sym {
140
+ kind: Kind
141
+ name: string
142
+ }
143
+
144
+ const KIND_LABEL: Record<Kind, string> = { fn: "fn", class: "class", type: "type", const: "const", mod: "mod" }
145
+
146
+ /** Exported / public symbols of one file, in order of appearance, without duplicates. Regex-level: good enough to orient, not a parser. */
147
+ export function extractSymbols(lang: string, text: string): Sym[] {
148
+ const out: Sym[] = []
149
+ const seen = new Set<string>()
150
+ const add = (kind: Kind, name: string | undefined) => {
151
+ if (!name || seen.has(name)) return
152
+ seen.add(name)
153
+ out.push({ kind, name })
154
+ }
155
+ const lines = text.split("\n")
156
+ switch (lang) {
157
+ case "ts": {
158
+ for (const l of lines) {
159
+ if (!l.startsWith("export")) continue
160
+ let m = l.match(/^export\s+(?:default\s+)?(?:declare\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/)
161
+ if (m) {
162
+ add("fn", m[1])
163
+ continue
164
+ }
165
+ m = l.match(/^export\s+(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/)
166
+ if (m) {
167
+ add("class", m[1])
168
+ continue
169
+ }
170
+ m = l.match(/^export\s+(?:declare\s+)?(?:interface|type|(?:const\s+)?enum)\s+([A-Za-z_$][\w$]*)/)
171
+ if (m) {
172
+ add("type", m[1])
173
+ continue
174
+ }
175
+ m = l.match(/^export\s+(?:declare\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]*)?=\s*(async\s+)?(\(|[A-Za-z_$][\w$]*\s*=>|function)/)
176
+ if (m) {
177
+ add("fn", m[1])
178
+ continue
179
+ }
180
+ m = l.match(/^export\s+(?:declare\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)/)
181
+ if (m) {
182
+ add("const", m[1])
183
+ continue
184
+ }
185
+ m = l.match(/^export\s+(?:type\s+)?\{([^}]*)\}/)
186
+ if (m) {
187
+ for (const part of m[1]!.split(",")) {
188
+ const n = part.trim().split(/\s+as\s+/).pop()?.replace(/^type\s+/, "").trim()
189
+ if (n && /^[A-Za-z_$][\w$]*$/.test(n)) add("const", n)
190
+ }
191
+ }
192
+ }
193
+ break
194
+ }
195
+ case "py": {
196
+ for (const l of lines) {
197
+ let m = l.match(/^(?:async\s+)?def\s+([A-Za-z][\w]*)\s*\(/)
198
+ if (m) {
199
+ add("fn", m[1])
200
+ continue
201
+ }
202
+ m = l.match(/^class\s+([A-Za-z][\w]*)/)
203
+ if (m) {
204
+ add("class", m[1])
205
+ continue
206
+ }
207
+ m = l.match(/^([A-Z][A-Z0-9_]{2,})\s*(?::[^=]+)?=/)
208
+ if (m) add("const", m[1])
209
+ }
210
+ break
211
+ }
212
+ case "go": {
213
+ for (const l of lines) {
214
+ let m = l.match(/^func\s+(?:\([^)]*\)\s*)?([A-Z]\w*)\s*[(\[]/)
215
+ if (m) {
216
+ add("fn", m[1])
217
+ continue
218
+ }
219
+ m = l.match(/^type\s+([A-Z]\w*)/)
220
+ if (m) {
221
+ add("type", m[1])
222
+ continue
223
+ }
224
+ m = l.match(/^(?:const|var)\s+([A-Z]\w*)/)
225
+ if (m) add("const", m[1])
226
+ }
227
+ break
228
+ }
229
+ case "rs": {
230
+ for (const l of lines) {
231
+ const m = l.match(/^\s*pub\s+(?:async\s+)?(?:unsafe\s+)?(?:extern\s+"[^"]*"\s+)?(fn|struct|enum|trait|type|const|static|mod|union)\s+(?:mut\s+)?([A-Za-z_]\w*)/)
232
+ if (!m) continue
233
+ const k = m[1]!
234
+ add(k === "fn" ? "fn" : k === "const" || k === "static" ? "const" : k === "mod" ? "mod" : k === "type" ? "type" : "class", m[2])
235
+ }
236
+ break
237
+ }
238
+ case "java": {
239
+ for (const l of lines) {
240
+ let m = l.match(/^\s*public\s+(?:(?:static|final|abstract|sealed|non-sealed|strictfp)\s+)*(class|interface|enum|record|@interface)\s+(\w+)/)
241
+ if (m) {
242
+ add("class", m[2])
243
+ continue
244
+ }
245
+ m = l.match(/^\s+public\s+(?:(?:static|final|abstract|synchronized|default)\s+)*(?:<[^>]+>\s+)?[\w<>\[\],.? ]+?\s+(\w+)\s*\(/)
246
+ if (m && !["if", "for", "while", "switch", "return", "new"].includes(m[1]!)) add("fn", m[1])
247
+ }
248
+ break
249
+ }
250
+ case "kt": {
251
+ for (const l of lines) {
252
+ if (/\b(private|protected|internal)\b/.test(l.split(/[({=]/)[0]!)) continue
253
+ let m = l.match(/^(?:(?:public|open|abstract|sealed|data|enum|annotation|inline|value|fun)\s+)*(class|interface|object)\s+(\w+)/)
254
+ if (m) {
255
+ add("class", m[2])
256
+ continue
257
+ }
258
+ m = l.match(/^(?:(?:public|inline|suspend|tailrec|operator|infix|external|actual|expect)\s+)*fun\s+(?:<[^>]+>\s+)?(?:[\w.<>?]+\.)?(\w+)\s*\(/)
259
+ if (m) {
260
+ add("fn", m[1])
261
+ continue
262
+ }
263
+ m = l.match(/^(?:public\s+)?(?:const\s+)?(?:val|var)\s+(\w+)/)
264
+ if (m) add("const", m[1])
265
+ }
266
+ break
267
+ }
268
+ case "rb": {
269
+ let priv = false
270
+ for (const l of lines) {
271
+ let m = l.match(/^\s*(class|module)\s+([A-Z][\w:]*)/)
272
+ if (m) {
273
+ priv = false
274
+ add(m[1] === "module" ? "mod" : "class", m[2])
275
+ continue
276
+ }
277
+ if (/^\s*(private|protected)\s*$/.test(l)) priv = true
278
+ else if (/^\s*public\s*$/.test(l)) priv = false
279
+ m = l.match(/^(\s{0,2})def\s+(?:self\.)?([a-z_][\w?!=]*)/)
280
+ if (m && !priv && !m[2]!.startsWith("_")) add("fn", m[2])
281
+ }
282
+ break
283
+ }
284
+ case "c": {
285
+ for (const l of lines) {
286
+ if (/^\s/.test(l) || l.startsWith("//") || l.startsWith("static") || l.startsWith("return")) continue
287
+ let m = l.match(/^#define\s+([A-Za-z_]\w*)/)
288
+ if (m) {
289
+ if (!/^_*[A-Z0-9_]+_H(PP|XX)?_*$/.test(m[1]!)) add("const", m[1])
290
+ continue
291
+ }
292
+ m = l.match(/^(?:typedef\s+)?(struct|class|enum|union)\s+([A-Za-z_]\w*)/)
293
+ if (m && !l.includes("(")) {
294
+ add("class", m[2])
295
+ continue
296
+ }
297
+ m = l.match(/^typedef\s+.*?\b([A-Za-z_]\w*)\s*;/)
298
+ if (m) {
299
+ add("type", m[1])
300
+ continue
301
+ }
302
+ m = l.match(/^(?:extern\s+(?:"C"\s+)?)?(?:inline\s+)?[A-Za-z_][\w\s*:<>,&]*?[\s*&]+([A-Za-z_]\w*)\s*\([^;{]*\)\s*(?:const)?\s*;/)
303
+ if (m && !["if", "while", "for", "return", "sizeof"].includes(m[1]!)) add("fn", m[1])
304
+ }
305
+ break
306
+ }
307
+ }
308
+ return out
309
+ }
310
+
311
+ function formatSymbols(syms: Sym[]): string {
312
+ const shown = syms.slice(0, MAX_SYMBOLS_PER_FILE)
313
+ const byKind = new Map<Kind, string[]>()
314
+ for (const s of shown) byKind.set(s.kind, [...(byKind.get(s.kind) ?? []), s.name])
315
+ const parts = (["class", "fn", "type", "const", "mod"] as Kind[]).filter((k) => byKind.has(k)).map((k) => `${KIND_LABEL[k]} ${byKind.get(k)!.join(", ")}`)
316
+ const extra = syms.length - shown.length
317
+ return parts.join("; ") + (extra > 0 ? `; +${extra} more` : "")
318
+ }
319
+
320
+ // ---------------------------------------------------------------- reading
321
+
322
+ function readSource(abs: string): string | undefined {
323
+ try {
324
+ const st = statSync(abs)
325
+ if (!st.isFile() || st.size > MAX_FILE_BYTES) return undefined
326
+ const fd = openSync(abs, "r")
327
+ try {
328
+ const probe = Buffer.alloc(Math.min(8192, st.size))
329
+ readSync(fd, probe, 0, probe.length, 0)
330
+ if (probe.includes(0)) return undefined // binary
331
+ } finally {
332
+ closeSync(fd)
333
+ }
334
+ return readFileSync(abs, "utf8")
335
+ } catch {
336
+ return undefined
337
+ }
338
+ }
339
+
340
+ const MANIFESTS = new Set(["package.json", "cargo.toml", "go.mod", "pyproject.toml", "setup.py", "requirements.txt", "pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", "gemfile", "cmakelists.txt", "makefile", "dockerfile", "tsconfig.json", "deno.json", "composer.json", "mix.exs", "pubspec.yaml", "package.swift"])
341
+ const ENTRYPOINT = /^(?:index|main|cli|app|server|__main__|manage|mod|lib|root)\.(?:[mc]?[jt]sx?|py|go|rs|java|kt|rb|c|cc|cpp)$/i
342
+ const README = /^readme(?:\.[a-z]+)?$/i
343
+
344
+ const ID = /[A-Za-z_$][\w$]{2,}/g
345
+
346
+ function dirSummary(files: string[]): string {
347
+ const top = new Map<string, number>()
348
+ const second = new Map<string, Map<string, number>>()
349
+ for (const f of files) {
350
+ const parts = f.split("/")
351
+ const d = parts.length > 1 ? parts[0]! : "."
352
+ top.set(d, (top.get(d) ?? 0) + 1)
353
+ if (parts.length > 2) {
354
+ const m = second.get(d) ?? new Map<string, number>()
355
+ m.set(parts[1]!, (m.get(parts[1]!) ?? 0) + 1)
356
+ second.set(d, m)
357
+ }
358
+ }
359
+ const dirs = [...top.entries()].filter(([d]) => d !== ".").sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1))
360
+ const rootFiles = top.get(".") ?? 0
361
+ const out: string[] = []
362
+ for (const [d, n] of dirs.slice(0, 14)) {
363
+ const subs = [...(second.get(d) ?? [])].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1))
364
+ const sub = n >= 20 && subs.length > 1 ? ` (${subs.slice(0, 4).map(([s, c]) => `${s}/ ${c}`).join(", ")}${subs.length > 4 ? ", ..." : ""})` : ""
365
+ out.push(`${d}/ ${n}${sub}`)
366
+ }
367
+ if (dirs.length > 14) out.push(`+${dirs.length - 14} more dirs`)
368
+ if (rootFiles) out.push(`root files ${rootFiles}`)
369
+ return out.join("; ")
370
+ }
371
+
372
+ // ---------------------------------------------------------------- build
373
+
374
+ /** Builds the map for `cwd` from scratch (no cache). */
375
+ export function buildRepoMap(cwd: string, opts: { maxTokens?: number } = {}): RepoMap {
376
+ const maxTokens = opts.maxTokens ?? REPOMAP_TOKENS
377
+ const root = resolve(cwd)
378
+ const listing = list(root)
379
+ const { files } = listing
380
+ const head = repoKey(root, maxTokens)
381
+ if (!files.length) return { text: "", tokens: 0, head }
382
+
383
+ // Pass 1: read each source file once, extract its symbols.
384
+ interface Src {
385
+ path: string
386
+ text: string
387
+ size: number
388
+ syms: Sym[]
389
+ }
390
+ const sources: Src[] = []
391
+ let total = 0
392
+ for (const f of files) {
393
+ if (sources.length >= MAX_FILES_READ || total >= MAX_TOTAL_BYTES) break
394
+ const ext = extname(f).toLowerCase()
395
+ const lang = LANG[ext]
396
+ if (!lang && !MENTION_ONLY.has(ext)) continue
397
+ const text = readSource(join(root, f))
398
+ if (text === undefined) continue
399
+ total += text.length
400
+ sources.push({ path: f, text, size: text.length, syms: lang ? extractSymbols(lang, text) : [] })
401
+ }
402
+
403
+ // Candidate names: symbols defined in at most 3 files (a name defined everywhere says nothing) and file basenames.
404
+ const definedIn = new Map<string, number>()
405
+ for (const s of sources) for (const y of new Set(s.syms.map((x) => x.name))) definedIn.set(y, (definedIn.get(y) ?? 0) + 1)
406
+ const stem = (p: string) => basename(p).replace(/\.[^.]+$/, "")
407
+ const owners = new Map<string, Set<number>>() // candidate name -> files that define it / carry that stem
408
+ sources.forEach((s, i) => {
409
+ const names = new Set<string>(s.syms.filter((y) => y.name.length >= 3 && (definedIn.get(y.name) ?? 0) <= 3).map((y) => y.name))
410
+ const st = stem(s.path)
411
+ if (st.length >= 3 && st !== "index" && st !== "main") names.add(st)
412
+ for (const n of names) owners.set(n, (owners.get(n) ?? new Set()).add(i))
413
+ })
414
+
415
+ // Pass 2: which other files mention each file's names (distinct files, so one noisy file counts once).
416
+ const refs = new Array<number>(sources.length).fill(0)
417
+ const referrers: Array<Set<number>> = sources.map(() => new Set())
418
+ sources.forEach((s, j) => {
419
+ const ids = new Set(s.text.match(ID) ?? [])
420
+ for (const id of ids) {
421
+ const own = owners.get(id)
422
+ if (!own) continue
423
+ for (const i of own) if (i !== j) referrers[i]!.add(j)
424
+ }
425
+ })
426
+ referrers.forEach((r, i) => (refs[i] = r.size))
427
+
428
+ // Rank: references dominate, then recency, then size. Ties by path, so the output is deterministic.
429
+ const recentRank = new Map(listing.recent.map((f, i) => [f, i] as const))
430
+ const score = (s: Src, i: number): number => {
431
+ const r = recentRank.get(s.path)
432
+ const recent = r === undefined ? 0 : 20 * (1 - r / 300)
433
+ const dirty = listing.dirty.has(s.path) ? 25 : 0
434
+ const size = Math.min(5, Math.log2(s.size / 1024 + 1)) * 2
435
+ return refs[i]! * 10 + recent + dirty + size
436
+ }
437
+ const ranked = sources
438
+ .map((s, i) => ({ s, i, score: score(s, i) }))
439
+ .filter((x) => x.s.syms.length > 0)
440
+ .sort((a, b) => b.score - a.score || (a.s.path < b.s.path ? -1 : a.s.path > b.s.path ? 1 : 0))
441
+
442
+ // Header lines.
443
+ const exts = new Map<string, number>()
444
+ for (const f of files) {
445
+ const e = extname(f).toLowerCase()
446
+ if (LANG[e]) exts.set(LANG[e]!, (exts.get(LANG[e]!) ?? 0) + 1)
447
+ }
448
+ const langs = [...exts.entries()].sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).map(([l, n]) => `${l} ${n}`)
449
+ const lines: string[] = [`# repo map: ${basename(root)}, ${files.length} files${langs.length ? ` (${langs.join(", ")})` : ""}${listing.git ? "" : ", not a git repo"}`]
450
+ lines.push(`dirs: ${dirSummary(files)}`)
451
+ const key: string[] = []
452
+ for (const f of files) {
453
+ const depth = f.split("/").length
454
+ const b = basename(f)
455
+ if ((MANIFESTS.has(b.toLowerCase()) && depth <= 2) || (README.test(b) && depth <= 2) || (ENTRYPOINT.test(b) && depth <= 3)) key.push(f)
456
+ }
457
+ key.sort((a, b) => a.split("/").length - b.split("/").length || (a < b ? -1 : 1))
458
+ if (key.length) lines.push(`key files: ${key.slice(0, 16).join(", ")}${key.length > 16 ? `, +${key.length - 16} more` : ""}`)
459
+ lines.push("exports by importance (references, recency, size):")
460
+
461
+ const fit = (all: string[]) => approxTokens(all.join("\n"))
462
+ // Never the header alone over budget: it is cut to the budget in the last step.
463
+ const body: string[] = []
464
+ let omitted = 0
465
+ for (const { s } of ranked) {
466
+ let line = `${s.path}: ${formatSymbols(s.syms)}`
467
+ if (line.length > MAX_LINE) line = `${line.slice(0, MAX_LINE - 1)}…`
468
+ if (fit([...lines, ...body, line, `(+${ranked.length} more files)`]) > maxTokens) {
469
+ omitted++
470
+ continue
471
+ }
472
+ body.push(line)
473
+ }
474
+ omitted = ranked.length - body.length
475
+ let text = [...lines, ...body].join("\n")
476
+ if (omitted > 0) text += `\n(+${omitted} more files with exports; read directories narrowly)`
477
+ if (approxTokens(text) > maxTokens) text = `${text.slice(0, Math.max(0, maxTokens * 4 - 1)).trimEnd()}…`
478
+ return { text, tokens: approxTokens(text), head }
479
+ }
480
+
481
+ // ---------------------------------------------------------------- cache
482
+
483
+ const cachePath = (home: string, cwd: string) => join(home, ".brain", "cache", `repomap-${createHash("sha256").update(resolve(cwd)).digest("hex").slice(0, 16)}.json`)
484
+
485
+ export interface CachedMap extends RepoMap {
486
+ /** true when served from the cache, false when it was just built. */
487
+ cached: boolean
488
+ }
489
+
490
+ /** The map for `cwd`, from the cache while the code is unchanged, else rebuilt (and cached unless `write` is false). */
491
+ export function repoMap(cwd: string, home: string, opts: { maxTokens?: number; write?: boolean } = {}): CachedMap {
492
+ const maxTokens = opts.maxTokens ?? REPOMAP_TOKENS
493
+ if (process.env.BRAIN_NO_CACHE === "1") return { ...buildRepoMap(cwd, { maxTokens }), cached: false }
494
+ const key = repoKey(resolve(cwd), maxTokens)
495
+ const file = cachePath(home, cwd)
496
+ try {
497
+ const c = JSON.parse(readFileSync(file, "utf8")) as { key?: string; text?: string; tokens?: number; head?: string }
498
+ if (c.key === key && typeof c.text === "string" && typeof c.tokens === "number") return { text: c.text, tokens: c.tokens, head: key, cached: true }
499
+ } catch {
500
+ // no cache, or unreadable: rebuild
501
+ }
502
+ const built = buildRepoMap(cwd, { maxTokens })
503
+ // The key was computed before the build; if the files moved in between, the next call sees a different key and rebuilds.
504
+ const map: RepoMap = { ...built, head: key }
505
+ if (opts.write !== false && map.text) {
506
+ try {
507
+ mkdirSync(join(home, ".brain", "cache"), { recursive: true, mode: 0o700 })
508
+ const tmp = `${file}.${process.pid}.tmp`
509
+ writeFileSync(tmp, JSON.stringify({ key, text: map.text, tokens: map.tokens, head: key }), { mode: 0o600 })
510
+ renameSync(tmp, file)
511
+ } catch {
512
+ // A cache that cannot be written only costs a rebuild.
513
+ }
514
+ }
515
+ return { ...map, cached: false }
516
+ }