@100xprompt/brain 0.0.0-stage → 0.1.1

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 (260) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +235 -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 +351 -0
  170. package/src/dashboard/cli.ts +148 -0
  171. package/src/dashboard/conversions.ts +170 -0
  172. package/src/dashboard/report.ts +261 -0
  173. package/src/dashboard/send.ts +213 -0
  174. package/src/dashboard/state.ts +95 -0
  175. package/src/fsutil.ts +133 -0
  176. package/src/gateway/common.ts +232 -0
  177. package/src/gateway/connect.ts +847 -0
  178. package/src/gateway/connectors/aider.ts +356 -0
  179. package/src/gateway/connectors/amazon-q-cli.ts +6 -0
  180. package/src/gateway/connectors/amazon-q-ide.ts +6 -0
  181. package/src/gateway/connectors/antigravity.ts +6 -0
  182. package/src/gateway/connectors/auggie.ts +6 -0
  183. package/src/gateway/connectors/cline-sdk.ts +85 -0
  184. package/src/gateway/connectors/cline.ts +34 -0
  185. package/src/gateway/connectors/codebuff.ts +6 -0
  186. package/src/gateway/connectors/continue.ts +579 -0
  187. package/src/gateway/connectors/copilot-cli.ts +138 -0
  188. package/src/gateway/connectors/crush.ts +313 -0
  189. package/src/gateway/connectors/cursor-cli.ts +6 -0
  190. package/src/gateway/connectors/cursor.ts +6 -0
  191. package/src/gateway/connectors/devin.ts +6 -0
  192. package/src/gateway/connectors/factory-droid.ts +308 -0
  193. package/src/gateway/connectors/goose.ts +79 -0
  194. package/src/gateway/connectors/grok-cli.ts +82 -0
  195. package/src/gateway/connectors/hermes.ts +230 -0
  196. package/src/gateway/connectors/iflow-cli.ts +50 -0
  197. package/src/gateway/connectors/index.ts +86 -0
  198. package/src/gateway/connectors/kilo-cli.ts +434 -0
  199. package/src/gateway/connectors/kilo-code.ts +32 -0
  200. package/src/gateway/connectors/kimi-code.ts +241 -0
  201. package/src/gateway/connectors/kiro-cli.ts +6 -0
  202. package/src/gateway/connectors/kiro-ide.ts +6 -0
  203. package/src/gateway/connectors/letta-code.ts +170 -0
  204. package/src/gateway/connectors/mastracode.ts +91 -0
  205. package/src/gateway/connectors/mistral-vibe.ts +495 -0
  206. package/src/gateway/connectors/muse.ts +27 -0
  207. package/src/gateway/connectors/omp.ts +279 -0
  208. package/src/gateway/connectors/openhands.ts +296 -0
  209. package/src/gateway/connectors/pearai.ts +52 -0
  210. package/src/gateway/connectors/pi.ts +357 -0
  211. package/src/gateway/connectors/qoder-cli.ts +6 -0
  212. package/src/gateway/connectors/qwen-code.ts +411 -0
  213. package/src/gateway/connectors/roo.ts +43 -0
  214. package/src/gateway/connectors/tabnine-cli.ts +6 -0
  215. package/src/gateway/connectors/void.ts +6 -0
  216. package/src/gateway/connectors/vscode-copilot.ts +286 -0
  217. package/src/gateway/connectors/windsurf.ts +6 -0
  218. package/src/gateway/connectors/zed.ts +29 -0
  219. package/src/gateway/signin.ts +323 -0
  220. package/src/handoff.ts +104 -0
  221. package/src/hooks.ts +455 -0
  222. package/src/memory/audit.ts +514 -0
  223. package/src/memory/blobs.ts +94 -0
  224. package/src/memory/cli.ts +117 -0
  225. package/src/memory/distill.ts +381 -0
  226. package/src/memory/externalize.ts +435 -0
  227. package/src/memory/lookups.ts +70 -0
  228. package/src/memory/mcp-install.ts +401 -0
  229. package/src/memory/mcp-server.ts +296 -0
  230. package/src/memory/prepare.ts +179 -0
  231. package/src/memory/preview.ts +154 -0
  232. package/src/memory/project.ts +35 -0
  233. package/src/memory/redact.ts +72 -0
  234. package/src/memory/relevance.ts +187 -0
  235. package/src/memory/repomap.ts +516 -0
  236. package/src/memory/store.ts +270 -0
  237. package/src/memory/sync.ts +166 -0
  238. package/src/memory/tidy.ts +193 -0
  239. package/src/memory/types.ts +118 -0
  240. package/src/org/admin-ops.ts +96 -0
  241. package/src/org/auth.ts +112 -0
  242. package/src/org/backup.ts +115 -0
  243. package/src/org/cli.ts +438 -0
  244. package/src/org/client.ts +404 -0
  245. package/src/org/config.ts +44 -0
  246. package/src/org/crypto.ts +537 -0
  247. package/src/org/db.ts +435 -0
  248. package/src/org/graph.ts +318 -0
  249. package/src/org/oidc.ts +435 -0
  250. package/src/org/policy.ts +398 -0
  251. package/src/org/server.ts +920 -0
  252. package/src/org/sso.ts +316 -0
  253. package/src/org/tls.ts +66 -0
  254. package/src/org/types.ts +193 -0
  255. package/src/self.ts +68 -0
  256. package/src/session/attachments.ts +53 -0
  257. package/src/session/peekcache.ts +50 -0
  258. package/src/session/types.ts +104 -0
  259. package/src/session/util.ts +130 -0
  260. package/src/sqlite.ts +48 -0
@@ -0,0 +1,72 @@
1
+ // Secret filtering for everything the brain stores in memory or shows through a tool. Deterministic, offline, idempotent:
2
+ // a secret becomes `[redacted]`, anything else is left alone. Deliberately conservative about what counts as a secret
3
+ // (git hashes, UUIDs, file paths and ordinary prose must survive), and aggressive about the shapes that are always secrets
4
+ // (provider key prefixes, PEM blocks, JWTs, bearer headers, `NAME_KEY=value` assignments, credentials inside URLs).
5
+
6
+ const MARK = "[redacted]"
7
+
8
+ // Values that are references to a secret, not the secret: `$API_KEY`, `${TOKEN}`, `process.env.X`, `<your-key>`, `getToken()`.
9
+ const REFERENCE_VALUE = /^(?:\$|\{|<|%|process\.|os\.|env\b|ENV\b|config\.|settings\.|secrets?\.|args\.|opts?\.|options\.|this\.|self\.|null\b|undefined\b|true\b|false\b|none\b|None\b|\[redacted\]|\*+$|x{3,}$|\.{3})/i
10
+
11
+ const PROVIDER_TOKENS: RegExp[] = [
12
+ /sk-(?:ant|proj|live|or-v1|svcacct)?[-_]?[A-Za-z0-9_-]{20,}/g, // OpenAI, Anthropic, OpenRouter and look-alikes
13
+ /\bsk_(?:live|test)_[A-Za-z0-9]{16,}\b/g, // Stripe secret keys
14
+ /\b[rp]k_(?:live|test)_[A-Za-z0-9]{16,}\b/g,
15
+ /\bAIza[0-9A-Za-z_-]{30,}\b/g, // Google API keys
16
+ /\bgh[pousr]_[A-Za-z0-9]{30,}\b/g, // GitHub tokens
17
+ /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g,
18
+ /\bglpat-[A-Za-z0-9_-]{16,}\b/g, // GitLab
19
+ /\bxox[abeprs]-[A-Za-z0-9-]{10,}\b/g, // Slack
20
+ /\b(?:AKIA|ASIA|AGPA|AIDA|AROA)[0-9A-Z]{16}\b/g, // AWS access key ids
21
+ /\bnpm_[A-Za-z0-9]{30,}\b/g,
22
+ /\bhf_[A-Za-z0-9]{30,}\b/g,
23
+ /\bya29\.[0-9A-Za-z_-]{20,}/g, // Google OAuth access tokens
24
+ /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, // SendGrid
25
+ /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, // JWT
26
+ ]
27
+
28
+ const PEM = /-----BEGIN [A-Z0-9 ]*(?:PRIVATE KEY|CERTIFICATE|PGP PRIVATE KEY BLOCK)-----[\s\S]*?(?:-----END [A-Z0-9 ]*-----|$)/g
29
+
30
+ const AUTH_HEADER = /\b(Authorization|Proxy-Authorization|X-Api-Key|X-Auth-Token|Api-Key)(["']?\s*[:=]\s*["']?)(?:(?:Bearer|Basic|Token|Digest)\s+)?[^\s"'`,;]{6,}/gi
31
+
32
+ const URL_CREDENTIALS = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:"']+:[^\s/@"']+@/gi
33
+
34
+ // NAME = value, where NAME ends in key/token/secret/password-like words. A bare `key` is not enough (prose, map keys).
35
+ const ASSIGNMENT =
36
+ /((?:^|[^\w.-])[\w.-]*(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?key|private[_-]?key|auth[_-]?key|client[_-]?secret|secret|token|password|passwd|pwd|credentials?|passphrase))(["']?\s*[:=]\s*["']?)(\[redacted\]|"[^"\n]*"|'[^'\n]*'|[^\s"'`,;)}\]]+)/gi
37
+
38
+ // A long unbroken mixed-case+digit run (base64/hex-ish secrets). Hex-only runs are left alone (git and content hashes).
39
+ const LONG_RUN = /(?<![\w/.+=-])[A-Za-z0-9_+-]{40,}={0,2}(?![\w/.+=-])/g
40
+
41
+ function isMixedSecret(s: string): boolean {
42
+ if (/^[0-9a-f]+$/i.test(s)) return false // git sha, sha256, md5: public identifiers
43
+ if (/^[a-z]+(?:[-_][a-z]+)+$/i.test(s)) return false // kebab/snake words
44
+ const upper = /[A-Z]/.test(s)
45
+ const lower = /[a-z]/.test(s)
46
+ const digit = /\d/.test(s)
47
+ const kinds = [upper, lower, digit].filter(Boolean).length
48
+ if (kinds < 3) return false
49
+ // identifiers like VeryLongCamelCaseFunctionName2 have few digit/letter switches; random strings switch a lot
50
+ let switches = 0
51
+ for (let i = 1; i < s.length; i++) if (/\d/.test(s[i]!) !== /\d/.test(s[i - 1]!)) switches++
52
+ return switches >= 6
53
+ }
54
+
55
+ export function redact(text: string): string {
56
+ if (!text) return text
57
+ let out = text
58
+ out = out.replace(PEM, MARK)
59
+ out = out.replace(AUTH_HEADER, (_m, name: string, sep: string) => `${name}${sep}${MARK}`)
60
+ out = out.replace(URL_CREDENTIALS, (_m, scheme: string) => `${scheme}${MARK}@`)
61
+ for (const re of PROVIDER_TOKENS) out = out.replace(re, MARK)
62
+ out = out.replace(ASSIGNMENT, (m, name: string, sep: string, value: string) => {
63
+ const bare = value.replace(/^["']|["']$/g, "")
64
+ if (bare.length < 4 || REFERENCE_VALUE.test(bare) || bare.includes("(")) return m
65
+ return `${name}${sep}${MARK}`
66
+ })
67
+ out = out.replace(LONG_RUN, (m) => (isMixedSecret(m) ? MARK : m))
68
+ return out
69
+ }
70
+
71
+ /** True when `redact` would change the text. */
72
+ export const hasSecret = (text: string): boolean => redact(text) !== text
@@ -0,0 +1,187 @@
1
+ // Which part of a long stored output to show when the model asks for it without saying where to look.
2
+ //
3
+ // Why not "the first N characters": an agent given a truncated first chunk almost never asks for the next one
4
+ // ("Agents Don't Paginate", arXiv 2608.26130), and the thing that matters in a log (the failure) is at its end.
5
+ // So the default window is the head, the tail and, when the caller says what it is after, the best-matching
6
+ // chunk(s) of the middle, ranked by BM25 over line-aligned chunks. No model, deterministic.
7
+ //
8
+ // Every omission is marked with the exact character range left out (offsets into the SAME text the caller sees,
9
+ // so `offset=` / `grep=` continue from it).
10
+
11
+ /** Target size of one relevance chunk, in characters. */
12
+ export const CHUNK_CHARS = 600
13
+
14
+ export interface Span {
15
+ start: number
16
+ end: number
17
+ }
18
+
19
+ /**
20
+ * Splits text into consecutive spans of about `target` characters that end on a line boundary (a line break is kept
21
+ * at the end of its span). A line longer than `target` is cut into pieces of `target` characters.
22
+ * The spans cover the text exactly: contiguous, no gaps, no overlap.
23
+ */
24
+ export function chunkSpans(text: string, target = CHUNK_CHARS): Span[] {
25
+ const spans: Span[] = []
26
+ let start = 0
27
+ let i = 0
28
+ const n = text.length
29
+ while (i < n) {
30
+ const nl = text.indexOf("\n", i)
31
+ const lineEnd = nl < 0 ? n : nl + 1
32
+ if (lineEnd - i > target) {
33
+ // A long line: close the open chunk first, then cut the line itself.
34
+ if (i > start) spans.push({ start, end: i })
35
+ let p = i
36
+ while (lineEnd - p > target) {
37
+ spans.push({ start: p, end: p + target })
38
+ p += target
39
+ }
40
+ start = p
41
+ i = lineEnd
42
+ if (i - start >= target) {
43
+ spans.push({ start, end: i })
44
+ start = i
45
+ }
46
+ continue
47
+ }
48
+ i = lineEnd
49
+ if (i - start >= target) {
50
+ spans.push({ start, end: i })
51
+ start = i
52
+ }
53
+ }
54
+ if (start < n) spans.push({ start, end: n })
55
+ return spans
56
+ }
57
+
58
+ const STOP = new Set(["the", "a", "an", "and", "or", "of", "to", "in", "on", "is", "it", "for", "at", "by", "as", "be", "this", "that", "with"])
59
+
60
+ /** Lower-cased word tokens (letters, digits, underscore; any script), stop words and one-letter tokens dropped. */
61
+ export function tokenize(s: string): string[] {
62
+ const out: string[] = []
63
+ for (const m of s.toLowerCase().matchAll(/[\p{L}\p{N}_]+/gu)) {
64
+ const t = m[0]
65
+ if (t.length > 1 && !STOP.has(t)) out.push(t)
66
+ }
67
+ return out
68
+ }
69
+
70
+ /**
71
+ * BM25 score of each chunk for the query (k1 1.2, b 0.75, the usual idf with +1 so it is never negative).
72
+ * A chunk with no query term scores 0.
73
+ */
74
+ export function bm25(query: string, chunks: string[]): number[] {
75
+ const q = [...new Set(tokenize(query))]
76
+ const scores = new Array<number>(chunks.length).fill(0)
77
+ if (!q.length || !chunks.length) return scores
78
+ const docs = chunks.map((c) => {
79
+ const tf = new Map<string, number>()
80
+ let len = 0
81
+ for (const t of tokenize(c)) {
82
+ tf.set(t, (tf.get(t) ?? 0) + 1)
83
+ len++
84
+ }
85
+ return { tf, len }
86
+ })
87
+ const avg = docs.reduce((a, d) => a + d.len, 0) / docs.length || 1
88
+ const N = docs.length
89
+ for (const term of q) {
90
+ let df = 0
91
+ for (const d of docs) if (d.tf.has(term)) df++
92
+ if (!df) continue
93
+ const idf = Math.log(1 + (N - df + 0.5) / (df + 0.5))
94
+ docs.forEach((d, i) => {
95
+ const f = d.tf.get(term)
96
+ if (!f) return
97
+ scores[i]! += idf * ((f * 2.2) / (f + 1.2 * (0.25 + 0.75 * (d.len / avg))))
98
+ })
99
+ }
100
+ return scores
101
+ }
102
+
103
+ const count = (s: string, ch: string) => {
104
+ let n = 0
105
+ for (let i = s.indexOf(ch); i >= 0; i = s.indexOf(ch, i + 1)) n++
106
+ return n
107
+ }
108
+
109
+ /** The marker for a skipped range [a, b) of `text`. */
110
+ export function omission(text: string, a: number, b: number): string {
111
+ const lines = count(text.slice(a, b), "\n")
112
+ return `[brain: omitted characters ${a}-${b} (${b - a} chars, ${lines} line break${lines === 1 ? "" : "s"}); get them with offset=${a} limit=${b - a}, or grep=<regex>]`
113
+ }
114
+
115
+ const MARKER_RESERVE = 190
116
+ const MAX_MIDDLE = 3
117
+
118
+ export interface WindowOptions {
119
+ /** What the caller is after: the best-matching chunks of the middle are added. */
120
+ query?: string
121
+ }
122
+
123
+ /**
124
+ * The text itself when it fits in `budget` characters; otherwise a window of about `budget` characters (markers
125
+ * included) made of the head, the tail and (with a query that matches something) up to three middle chunks, in
126
+ * file order, each gap replaced by an omission marker. With no query the room goes to head and tail equally;
127
+ * with one, head 35%, tail 35%, middle 30%.
128
+ */
129
+ export function windowOf(text: string, budget: number, opts: WindowOptions = {}): string {
130
+ budget = Math.max(600, Math.floor(budget))
131
+ if (text.length <= budget) return text
132
+ const n = text.length
133
+ const content = budget - MARKER_RESERVE * (MAX_MIDDLE + 2)
134
+ const room = Math.max(200, content)
135
+ const hasQuery = !!opts.query && tokenize(opts.query).length > 0
136
+
137
+ const spans = hasQuery ? chunkSpans(text) : []
138
+ const scores = hasQuery ? bm25(opts.query!, spans.map((s) => text.slice(s.start, s.end))) : []
139
+ const anyHit = scores.some((s) => s > 0)
140
+ const headB = Math.floor(room * (anyHit ? 0.35 : 0.5))
141
+ const tailB = Math.floor(room * (anyHit ? 0.35 : 0.5))
142
+ const midB = room - headB - tailB
143
+
144
+ // Head ends on a line boundary when one is near; the tail starts on one.
145
+ let headEnd = headB
146
+ if (text[headEnd - 1] !== "\n") {
147
+ const k = text.lastIndexOf("\n", headEnd - 1)
148
+ if (k + 1 >= headB * 0.5) headEnd = k + 1
149
+ }
150
+ let tailStart = n - tailB
151
+ if (text[tailStart - 1] !== "\n") {
152
+ const k = text.indexOf("\n", tailStart)
153
+ if (k >= 0 && k + 1 - tailStart <= tailB * 0.5) tailStart = k + 1
154
+ }
155
+ if (headEnd >= tailStart) return text // cannot happen when text.length > budget; stay safe
156
+
157
+ const picked: Span[] = []
158
+ if (anyHit) {
159
+ const order = spans
160
+ .map((s, i) => ({ s, score: scores[i]! }))
161
+ .filter((x) => x.score > 0 && x.s.start >= headEnd && x.s.end <= tailStart)
162
+ .sort((a, b) => b.score - a.score || a.s.start - b.s.start)
163
+ let used = 0
164
+ for (const x of order) {
165
+ if (picked.length >= MAX_MIDDLE) break
166
+ const size = x.s.end - x.s.start
167
+ if (used + size > midB) continue
168
+ picked.push(x.s)
169
+ used += size
170
+ }
171
+ picked.sort((a, b) => a.start - b.start)
172
+ }
173
+
174
+ const parts: string[] = [text.slice(0, headEnd)]
175
+ let at = headEnd
176
+ const gap = (to: number) => {
177
+ if (to > at) parts.push(`${parts[parts.length - 1]!.endsWith("\n") ? "" : "\n"}${omission(text, at, to)}\n`)
178
+ }
179
+ for (const s of picked) {
180
+ gap(s.start)
181
+ parts.push(text.slice(s.start, s.end))
182
+ at = s.end
183
+ }
184
+ gap(tailStart)
185
+ parts.push(text.slice(tailStart))
186
+ return parts.join("")
187
+ }