@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.
- package/LICENSE +48 -0
- package/README.md +172 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +326 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/connect.ts +845 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- 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
|
+
}
|