@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,117 @@
1
+ // `brain memory ingest <session-id> | search <query> [--project p] [--limit n] | status | forget [--project p] [--source id]`.
2
+ // Plain output, counts first. `resolve` turns a session id into a Session (the coordinator wires the real readers).
3
+ import { realpathSync } from "node:fs"
4
+ import type { Session } from "../session/types.ts"
5
+ import { gitInfo } from "../fsutil.ts"
6
+ import { distill } from "./distill.ts"
7
+ import { projectKey } from "./project.ts"
8
+ import { redact } from "./redact.ts"
9
+ import { openMemory, storeBytes } from "./store.ts"
10
+ import type { ProjectKey } from "./types.ts"
11
+
12
+ function flag(args: string[], name: string): string | undefined {
13
+ const i = args.indexOf(`--${name}`)
14
+ return i >= 0 ? args[i + 1] : undefined
15
+ }
16
+
17
+ function positional(args: string[]): string[] {
18
+ const out: string[] = []
19
+ for (let i = 0; i < args.length; i++) {
20
+ if (args[i]!.startsWith("--")) i++
21
+ else out.push(args[i]!)
22
+ }
23
+ return out
24
+ }
25
+
26
+ /** The project to act on: `--project` as an id or a label of a stored project, else the current folder's. */
27
+ async function pickProject(args: string[], mem: ReturnType<typeof openMemory>): Promise<ProjectKey | { error: string }> {
28
+ const wanted = flag(args, "project")
29
+ if (!wanted) {
30
+ const cwd = (() => {
31
+ try {
32
+ return realpathSync(process.cwd())
33
+ } catch {
34
+ return process.cwd()
35
+ }
36
+ })()
37
+ return projectKey({ cwd, ...gitInfo(cwd) })
38
+ }
39
+ const known = await mem.projects()
40
+ const hit = known.find((p) => p.id === wanted.toLowerCase() || p.id === wanted) ?? known.filter((p) => p.label === wanted)[0]
41
+ if (hit) return { id: hit.id, label: hit.label }
42
+ const labelMatches = known.filter((p) => p.label === wanted)
43
+ if (labelMatches.length > 1) return { error: `"${wanted}" matches several projects; use the id` }
44
+ return { error: `no stored project "${wanted}" (known: ${known.map((p) => p.label).join(", ") || "none"})` }
45
+ }
46
+
47
+ export async function memoryCommand(args: string[], home: string, resolve: (id: string) => Promise<Session>, out: (line: string) => void = console.log): Promise<number> {
48
+ const [sub, ...rest] = args
49
+ const mem = openMemory(home)
50
+ try {
51
+ if (sub === "status" || !sub) {
52
+ const s = await mem.status()
53
+ if (!s.available) {
54
+ out("memory: not available")
55
+ out(`why: ${s.why}`)
56
+ return 0
57
+ }
58
+ const c = await mem.counts()
59
+ out(`memory: available, ${c.total} items in ${(await mem.projects()).length} projects`)
60
+ out(`store: ${s.store} (${(storeBytes(home) / 1024).toFixed(0)} KB)`)
61
+ out(`kinds: ${Object.entries(c.byKind).map(([k, n]) => `${k} ${n}`).join(", ") || "none"}`)
62
+ for (const p of await mem.projects()) out(` ${p.label} ${p.items} items (${p.id})`)
63
+ return 0
64
+ }
65
+
66
+ if (sub === "ingest") {
67
+ const id = positional(rest)[0]
68
+ if (!id) return out("usage: brain memory ingest <session-id>"), 2
69
+ let session: Session
70
+ try {
71
+ session = await resolve(id)
72
+ } catch (e) {
73
+ return out(`cannot read session ${id}: ${(e as Error).message}`), 1
74
+ }
75
+ const s = await mem.status()
76
+ if (!s.available) return out(`memory is not available: ${s.why}`), 1
77
+ const items = distill(session)
78
+ const project = projectKey(session.header.project)
79
+ const { stored } = await mem.ingest(project, items)
80
+ const byKind: Record<string, number> = {}
81
+ for (const i of items) byKind[i.kind] = (byKind[i.kind] ?? 0) + 1
82
+ out(`stored ${stored} items from ${session.header.id}`)
83
+ out(`kinds: ${Object.entries(byKind).map(([k, n]) => `${k} ${n}`).join(", ") || "none"}`)
84
+ out(`project: ${project.label} (${project.id})`)
85
+ return 0
86
+ }
87
+
88
+ if (sub === "search") {
89
+ const query = positional(rest).join(" ").trim()
90
+ if (!query) return out("usage: brain memory search <query> [--project p] [--limit n]"), 2
91
+ const s = await mem.status()
92
+ if (!s.available) return out(`memory is not available: ${s.why}`), 1
93
+ const project = await pickProject(rest, mem)
94
+ if ("error" in project) return out(project.error), 1
95
+ const limit = Number(flag(rest, "limit")) || 8
96
+ const hits = await mem.recall(project, query, { limit })
97
+ out(`${hits.length} items for "${redact(query)}" in ${project.label}`)
98
+ for (const h of hits) out(`[${h.kind ?? "item"}] ${h.score.toFixed(3)} ${h.text}${h.source ? ` (from ${h.source})` : ""}`)
99
+ return 0
100
+ }
101
+
102
+ if (sub === "forget") {
103
+ const s = await mem.status()
104
+ if (!s.available) return out(`memory is not available: ${s.why}`), 1
105
+ const project = await pickProject(rest, mem)
106
+ if ("error" in project) return out(project.error), 1
107
+ const n = await mem.forget(project, { source: flag(rest, "source") })
108
+ out(`forgot ${n} items in ${project.label}${flag(rest, "source") ? ` from ${flag(rest, "source")}` : ""}`)
109
+ return 0
110
+ }
111
+
112
+ out("usage: brain memory ingest <session-id> | search <query> [--project p] [--limit n] | status | forget [--project p] [--source id]")
113
+ return 2
114
+ } finally {
115
+ mem.close()
116
+ }
117
+ }
@@ -0,0 +1,381 @@
1
+ // Zero-model distillation of a portable session into small memory items: what was decided, what must not be done, which
2
+ // approaches failed, which files changed and why, which commands are reusable, how it ended. Precision over recall: a wrong
3
+ // memory costs tokens in every future session, a missed one costs a re-discovery. Everything passes through `redact`.
4
+ import { createHash } from "node:crypto"
5
+ import { isAbsolute, relative } from "node:path"
6
+ import type { Session, Turn } from "../session/types.ts"
7
+ import { isInjected, withoutInjected } from "../session/util.ts"
8
+ import { projectKey } from "./project.ts"
9
+ import { redact } from "./redact.ts"
10
+ import type { MemoryItem, MemoryKind } from "./types.ts"
11
+
12
+ export interface DistillOptions {
13
+ /** Cap on items per session; the best are kept. Default 40. */
14
+ maxItems?: number
15
+ }
16
+
17
+ type Scored = MemoryItem & { score: number }
18
+
19
+ const sha = (s: string, n = 12): string => createHash("sha1").update(s).digest("hex").slice(0, n)
20
+ const oneLine = (s: string): string => s.replace(/\s+/g, " ").trim()
21
+
22
+ /** Whitespace-collapsed text clipped to `n` characters at a word edge, with an ellipsis. */
23
+ export function clipText(s: string, n: number): string {
24
+ const t = oneLine(s)
25
+ if (t.length <= n) return t
26
+ const cut = t.slice(0, n - 1)
27
+ const sp = cut.lastIndexOf(" ")
28
+ return `${(sp > n * 0.6 ? cut.slice(0, sp) : cut).trimEnd()}…`
29
+ }
30
+
31
+ // ---- user instructions --------------------------------------------------------------------------------------------
32
+
33
+ const STRONG = /\b(always|never|from now on|going forward|in the future|we use|we always|we never|we decided|we('re| are) (using|going with)|the rule is|the convention is|prefer|i prefer|i like|i don'?t like|i hate|whenever|every time|should always|should never|must never|must not|mustn'?t|don'?t ever|under no circumstances|no matter what|default to)\b/i
34
+ const MEDIUM = /\b(must|don'?t|do not|dont|make sure|ensure|instead of|instead|only use|only|avoid|stop (?:using|doing|adding|writing)|no need to|keep (?:it|them|the|using)|rather than|not (?:use|using)|use [\w./-]+ not [\w./-]+)\b/i
35
+ const ONE_OFF = /\b(right now|this time|for now|just this|for this (?:task|one|case|session)|today|tonight|at the moment|in this (?:file|case)|the above|the following|above error|this error|that error|this bug)\b/i
36
+ const QUESTION_START = /^(?:what|why|how|when|where|who|which|can|could|would|should|is|are|do|does|did|will|have|has|any idea|is there|are there)\b/i
37
+ const FILLER_START = /^(?:and|also|but|so|ok|okay|yes|yeah|yep|no|now|well|then|please|pls|plz|btw|oh|hey|hi|hmm|alright|right|cool|nice|great)[\s,:.!-]+/i
38
+ const DEICTIC_START = /^(?:it|this|that|these|those|they|them|he|she|its|there|here|the same|same|such)\b/i
39
+
40
+ function splitInstructionCandidates(text: string): string[] {
41
+ const noCode = text.replace(/```[\s\S]*?(?:```|$)/g, " ").replace(/`([^`\n]{1,80})`/g, "$1")
42
+ const out: string[] = []
43
+ for (const line of noCode.split(/\n+/)) {
44
+ const l = line.replace(/^\s*(?:[-*•]|\d+[.)])\s+/, "").trim()
45
+ if (!l) continue
46
+ if (l.length > 600) continue // a pasted paragraph, log line or stack trace, not an instruction
47
+ if (/^\s*(?:at |\{|\[|<|\/\/|#|\$ |>>|[A-Za-z]+Error:)/.test(l) && !/\s/.test(l.slice(0, 3))) continue
48
+ for (const s of l.split(/(?<=[.!?])\s+(?=[A-Z0-9"'`])/)) {
49
+ const t = s.trim()
50
+ if (t) out.push(t)
51
+ }
52
+ }
53
+ return out
54
+ }
55
+
56
+ function classify(sentence: string): { kind: MemoryKind; strong: boolean } | undefined {
57
+ if (sentence.endsWith("?")) return undefined
58
+ let s = sentence
59
+ for (let i = 0; i < 2; i++) s = s.replace(FILLER_START, "")
60
+ if (s.length < 20 || s.split(/\s+/).length < 4) return undefined
61
+ if (QUESTION_START.test(s) && !/^(?:do not|don'?t|do it)\b/i.test(s)) return undefined
62
+ if (DEICTIC_START.test(s)) return undefined
63
+ if (/^(?:thanks|thank you|sure|great|perfect|good|looks good|lgtm)\b/i.test(s)) return undefined
64
+ const strong = STRONG.test(s)
65
+ const medium = MEDIUM.test(s)
66
+ if (!strong && !medium) return undefined
67
+ if (!strong && ONE_OFF.test(s)) return undefined
68
+ // medium markers inside an imperative about "this/it/the failing test" are one-offs: require a durable-sounding object
69
+ if (!strong && /\b(?:make sure|ensure)\b/i.test(s) && !/\b(?:always|every|all|each|any)\b/i.test(s) && /\b(?:this|it|that|the (?:tests?|build|app|page|server|thing))\b/i.test(s)) return undefined
70
+ const kind: MemoryKind = /\b(never|must not|mustn'?t|don'?t|do not|dont|avoid|stop|no need to|only|no matter what|under no circumstances)\b/i.test(s)
71
+ ? "constraint"
72
+ : /\b(prefer|rather than|i like|i don'?t like|i hate|instead|default to)\b/i.test(s)
73
+ ? "preference"
74
+ : "decision"
75
+ return { kind, strong }
76
+ }
77
+
78
+ function instructions(session: Session, at: (t: Turn) => string | undefined): Scored[] {
79
+ const out: Scored[] = []
80
+ const seen = new Set<string>()
81
+ const source = session.header.id
82
+ for (const t of session.turns) {
83
+ if (t.kind !== "user") continue
84
+ if (isInjected(t.text)) continue
85
+ const text = withoutInjected(t.text)
86
+ if (/^This session is being continued from a previous conversation/i.test(text)) continue
87
+ if (text.length > 4000) continue // a pasted blob, not a message
88
+ for (const sentence of splitInstructionCandidates(text)) {
89
+ const c = classify(sentence)
90
+ if (!c) continue
91
+ const clean = redact(clipText(sentence.replace(FILLER_START, ""), 300))
92
+ const key = clean.toLowerCase()
93
+ if (seen.has(key)) continue
94
+ seen.add(key)
95
+ out.push({ id: `${c.kind}-${sha(source + key)}`, kind: c.kind, name: clipText(clean, 70), text: clean, source, occurredAt: at(t), score: c.strong ? 88 : 72 })
96
+ }
97
+ }
98
+ return out
99
+ }
100
+
101
+ // ---- tool turns ---------------------------------------------------------------------------------------------------
102
+
103
+ type Call = Extract<Turn, { kind: "tool_call" }>
104
+ type Result = Extract<Turn, { kind: "tool_result" }>
105
+
106
+ function inputObject(input: unknown): Record<string, any> {
107
+ if (input && typeof input === "object") return input as Record<string, any>
108
+ if (typeof input === "string") {
109
+ try {
110
+ const v = JSON.parse(input)
111
+ if (v && typeof v === "object") return v
112
+ } catch {}
113
+ return { command: input }
114
+ }
115
+ return {}
116
+ }
117
+
118
+ function commandOf(call: Call): string | undefined {
119
+ const o = inputObject(call.input)
120
+ const c = o.command ?? o.cmd ?? o.script
121
+ if (typeof c === "string") return c
122
+ if (Array.isArray(c)) return c.map(String).join(" ")
123
+ return undefined
124
+ }
125
+
126
+ function pathOf(call: Call): string | undefined {
127
+ const o = inputObject(call.input)
128
+ for (const k of ["file_path", "path", "filePath", "file", "filename", "target_file", "notebook_path"]) if (typeof o[k] === "string" && o[k]) return o[k] as string
129
+ return undefined
130
+ }
131
+
132
+ const ERROR_LINE = /\b(error|fail(?:ed|ure)?|not found|denied|cannot|can't|no such|exception|panic|fatal|unable|invalid|unexpected|refused|timed? ?out|ENOENT|EACCES|ERR_)\b/i
133
+ const REJECTION = /(user (?:doesn'?t|does not|didn'?t) want|was rejected|rejected|interrupted by user|request was aborted|tool use was|permission to use|has not been read yet|hook (?:blocked|denied)|aborted)/i
134
+
135
+ function firstErrorLine(output: string): string {
136
+ const lines = output.split("\n").map((l) => l.trim()).filter(Boolean)
137
+ const hit = lines.find((l) => ERROR_LINE.test(l) && l.length < 400) ?? lines[0] ?? ""
138
+ return clipText(hit, 160)
139
+ }
140
+
141
+ /** `cd x && FOO=1 cmd args` -> the part that says what the command is. */
142
+ function core(command: string): string {
143
+ const first = command.split("\n").find((l) => l.trim() && !l.trim().startsWith("#")) ?? ""
144
+ return first.replace(/^\s*(?:cd\s+[^&;]+(?:&&|;)\s*)+/, "").replace(/^\s*(?:[A-Z_][A-Z0-9_]*=\S*\s+)+/, "").trim()
145
+ }
146
+
147
+ const failureKey = (c: string): string => core(c).split(/\s+/).slice(0, 2).join(" ")
148
+
149
+ function failures(session: Session, at: (t: Turn) => string | undefined): Scored[] {
150
+ const calls = new Map<string, { call: Call; index: number }>()
151
+ session.turns.forEach((t, i) => t.kind === "tool_call" && calls.set(t.id, { call: t, index: i }))
152
+ const attempts: Array<{ command: string; ok: boolean; index: number }> = []
153
+ const failed: Array<{ command: string; error: string; index: number; at?: string; kind: string }> = []
154
+ session.turns.forEach((t, i) => {
155
+ if (t.kind !== "tool_result") return
156
+ const entry = calls.get(t.callId)
157
+ if (!entry) return
158
+ const { call } = entry
159
+ if (!["shell", "edit", "write", "mcp", "other", "web"].includes(call.toolKind)) return
160
+ const command = commandOf(call) ?? `${call.name}${pathOf(call) ? ` ${pathOf(call)}` : ""}`
161
+ attempts.push({ command, ok: t.ok, index: i })
162
+ if (t.ok || REJECTION.test(t.output.slice(0, 400)) || !t.output.trim()) return
163
+ failed.push({ command, error: firstErrorLine(t.output), index: i, at: t.at, kind: call.toolKind })
164
+ })
165
+ const source = session.header.id
166
+ const out: Scored[] = []
167
+ const seen = new Set<string>()
168
+ for (const f of failed) {
169
+ const key = failureKey(f.command)
170
+ const fixed = attempts.find((a) => a.ok && a.index > f.index && failureKey(a.command) === key)
171
+ const clean = redact(clipText(core(f.command) || f.command, 140))
172
+ if (!clean || seen.has(`${clean}|${f.error}`)) continue
173
+ seen.add(`${clean}|${f.error}`)
174
+ const fix = fixed && core(fixed.command) !== core(f.command) ? `; worked after changing it to \`${redact(clipText(core(fixed.command), 100))}\`` : fixed ? "; worked on retry" : ""
175
+ const text = `Tried \`${clean}\`: failed with "${redact(f.error)}"${fix}`
176
+ out.push({ id: `failed-${sha(source + f.command + f.error)}`, kind: "failed", name: clipText(`failed: ${clean}`, 70), text, source, occurredAt: f.at, score: fixed ? 60 : 30 })
177
+ }
178
+ return out.sort((a, b) => b.score - a.score).slice(0, 6)
179
+ }
180
+
181
+ const CMD_KEYWORD = /\b(test|tests|build|lint|typecheck|tsc|check|dev|start|serve|deploy|format|fmt|compile|bench|migrate)\b/i
182
+ const CMD_TOOL = /^(?:bun|bunx|npm|npx|pnpm|yarn|cargo|go|pytest|python3?|make|tsc|eslint|prettier|ruff|gradle|\.\/gradlew|mvn|dotnet|deno|vitest|jest|gcloud|docker|terraform|swift|xcodebuild|next|vite|node|uv|poetry|rake|bundle|mix|flutter|gh)\b/
183
+
184
+ function commandLabel(c: string): string {
185
+ if (/\b(test|tests|pytest|vitest|jest)\b/i.test(c)) return "to run the tests"
186
+ if (/\b(typecheck|tsc)\b/i.test(c)) return "to typecheck"
187
+ if (/\b(lint|eslint|ruff|check)\b/i.test(c)) return "to lint"
188
+ if (/\b(deploy)\b/i.test(c)) return "to deploy"
189
+ if (/\b(build|compile)\b/i.test(c)) return "to build"
190
+ if (/\b(format|fmt|prettier)\b/i.test(c)) return "to format"
191
+ if (/\b(dev|start|serve|run)\b/i.test(c)) return "to run the app"
192
+ if (/\bbench\b/i.test(c)) return "to benchmark"
193
+ return `to run ${c.split(/\s+/).slice(0, 2).join(" ")}`
194
+ }
195
+
196
+ function commands(session: Session, at: (t: Turn) => string | undefined): Scored[] {
197
+ const calls = new Map<string, Call>()
198
+ const stats = new Map<string, { n: number; at?: string; command: string }>()
199
+ for (const t of session.turns) {
200
+ if (t.kind === "tool_call") calls.set(t.id, t)
201
+ if (t.kind !== "tool_result" || !t.ok) continue
202
+ const call = calls.get(t.callId)
203
+ if (!call || call.toolKind !== "shell") continue
204
+ const raw = commandOf(call)
205
+ if (!raw || /<<|\|\s*(?:sh|bash)\b/.test(raw)) continue
206
+ const c = core(raw)
207
+ if (c.length < 4 || c.length > 200 || !CMD_TOOL.test(c) || !CMD_KEYWORD.test(c.split(/\s+/).slice(0, 4).join(" "))) continue
208
+ if (/^(?:gh|docker|gcloud)\b/.test(c) && !/\b(deploy|build|test)\b/.test(c)) continue
209
+ const shown = redact(clipText(raw.split("\n")[0]!.replace(/^\s*#.*$/, ""), 160))
210
+ if (shown !== clipText(raw.split("\n")[0]!, 160)) continue // contained a secret: never keep it
211
+ const cur = stats.get(c) ?? { n: 0, command: shown }
212
+ cur.n++
213
+ cur.at = at(t)
214
+ stats.set(c, cur)
215
+ }
216
+ const source = session.header.id
217
+ return [...stats.values()]
218
+ .sort((a, b) => b.n - a.n)
219
+ .slice(0, 8)
220
+ .map((s) => {
221
+ const label = commandLabel(s.command)
222
+ return {
223
+ id: `command-${sha(source + s.command)}`,
224
+ kind: "command" as const,
225
+ name: label,
226
+ text: `${label[0]!.toUpperCase()}${label.slice(1)}: \`${s.command}\``,
227
+ source,
228
+ occurredAt: s.at,
229
+ procedures: [{ trigger: label, action: `\`${s.command}\`` }],
230
+ score: 55 + Math.min(s.n, 5),
231
+ }
232
+ })
233
+ }
234
+
235
+ const SKIP_PATH = /(?:^|\/)(?:node_modules|\.git|dist|build|\.next|coverage|\.brain)\/|(?:package-lock\.json|bun\.lockb?|yarn\.lock|pnpm-lock\.yaml|\.DS_Store)$/
236
+
237
+ function files(session: Session, at: (t: Turn) => string | undefined): Scored[] {
238
+ const cwd = session.header.project.cwd
239
+ const rel = (p: string): string => {
240
+ if (cwd && isAbsolute(p)) {
241
+ const r = relative(cwd, p)
242
+ if (r && !r.startsWith("..")) return r
243
+ }
244
+ return p
245
+ }
246
+ const project = projectKey(session.header.project)
247
+ const byPath = new Map<string, { keys: Set<string>; created: boolean; why?: string; plus: number; minus: number; first?: string; at?: string; written: number }>()
248
+ const touch = (path: string, key: string, t: Turn, extra: { created?: boolean; patch?: string; written?: number }, index: number) => {
249
+ if (SKIP_PATH.test(path)) return
250
+ const p = rel(path)
251
+ const cur = byPath.get(p) ?? { keys: new Set<string>(), created: false, plus: 0, minus: 0, written: 0 }
252
+ if (cur.keys.has(key)) return
253
+ cur.keys.add(key)
254
+ if (extra.created) cur.created = true
255
+ if (extra.patch) {
256
+ for (const l of extra.patch.split("\n")) {
257
+ if (l.startsWith("+") && !l.startsWith("+++")) {
258
+ cur.plus++
259
+ if (!cur.first && l.slice(1).trim().length > 6) cur.first = l.slice(1).trim()
260
+ } else if (l.startsWith("-") && !l.startsWith("---")) cur.minus++
261
+ }
262
+ }
263
+ if (extra.written) cur.written += extra.written
264
+ const why = nearbyWords(session.turns, index)
265
+ if (why) cur.why = why
266
+ cur.at = at(t)
267
+ byPath.set(p, cur)
268
+ }
269
+ session.turns.forEach((t, i) => {
270
+ if (t.kind === "file_edit") touch(t.path, t.callId ?? `edit${i}`, t, { patch: t.patch }, i)
271
+ else if (t.kind === "tool_call" && (t.toolKind === "edit" || t.toolKind === "write")) {
272
+ const p = pathOf(t)
273
+ if (!p) return
274
+ const o = inputObject(t.input)
275
+ const body = typeof o.content === "string" ? o.content : typeof o.file_text === "string" ? o.file_text : typeof o.new_string === "string" ? o.new_string : typeof o.patch === "string" ? o.patch : undefined
276
+ touch(p, t.id, t, { created: t.toolKind === "write", written: body?.length, patch: typeof o.patch === "string" ? o.patch : undefined }, i)
277
+ }
278
+ })
279
+ const source = session.header.id
280
+ const out: Scored[] = []
281
+ for (const [path, f] of byPath) {
282
+ const stat = f.plus || f.minus ? `+${f.plus}/-${f.minus} lines` : f.written ? `${f.written} characters written` : ""
283
+ const what = f.why ? clipText(f.why, 160) : f.first ? `first changed line: ${clipText(f.first, 100)}` : stat
284
+ const n = f.keys.size
285
+ const text = redact(`${path}: ${f.created && n === 1 ? "created" : n > 1 ? `edited ${n} times` : "edited"}${what ? `; ${what}` : ""}${f.why && stat ? ` (${stat})` : ""}`)
286
+ out.push({
287
+ id: `file-${sha(source + path)}`,
288
+ kind: "file",
289
+ name: path,
290
+ text,
291
+ source,
292
+ occurredAt: f.at,
293
+ entities: [{ name: path, type: "file" }, { name: project.label, type: "project" }],
294
+ relations: [{ from: path, to: project.label, type: "edited_in" }],
295
+ score: 40 + Math.min(n, 8) * 2 + (f.why ? 6 : 0),
296
+ })
297
+ }
298
+ return out.sort((a, b) => b.score - a.score).slice(0, 12)
299
+ }
300
+
301
+ /** The assistant's own words just before an edit, when they are short enough to be a reason ("Fixed the TTL unit"). */
302
+ function nearbyWords(turns: Turn[], index: number): string | undefined {
303
+ for (let j = index - 1; j >= Math.max(0, index - 4); j--) {
304
+ const t = turns[j]!
305
+ if (t.kind === "assistant" && t.text.trim()) {
306
+ const first = oneLine(t.text).split(/(?<=[.!?:])\s/)[0] ?? ""
307
+ if (first.length >= 15 && first.length <= 180 && !/^(?:let me|i'll|i will|now|first|next|ok|sure)\b/i.test(first)) return first.replace(/[:.]$/, "")
308
+ if (first.length >= 15 && first.length <= 180) return first.replace(/^(?:let me|i'll|i will)\s+/i, "").replace(/[:.]$/, "")
309
+ return undefined
310
+ }
311
+ if (t.kind === "user" || t.kind === "compaction") return undefined
312
+ }
313
+ return undefined
314
+ }
315
+
316
+ // ---- summaries and state ------------------------------------------------------------------------------------------
317
+
318
+ const DONE = /\b(summary|done|fixed|implemented|changed|added|completed|shipped|now (?:works|passes|returns|uses)|all \d+ (?:tests )?pass|pass(?:es|ing)?|verified|resolved|created|updated|refactored|removed|wired)\b/i
319
+
320
+ function realUserTurns(session: Session): Array<Extract<Turn, { kind: "user" }>> {
321
+ return session.turns.filter((t): t is Extract<Turn, { kind: "user" }> => t.kind === "user" && !isInjected(t.text) && !/^This session is being continued/i.test(withoutInjected(t.text)) && withoutInjected(t.text).length < 6000)
322
+ }
323
+
324
+ function summaries(session: Session, at: (t: Turn) => string | undefined): Scored[] {
325
+ const source = session.header.id
326
+ const out: Scored[] = []
327
+ const compactions = session.turns.filter((t): t is Extract<Turn, { kind: "compaction" }> => t.kind === "compaction" && t.summary.trim().length >= 40)
328
+ for (const c of compactions.slice(-2)) {
329
+ const text = redact(clipText(c.summary, 1500))
330
+ out.push({ id: `summary-${sha(source + text)}`, kind: "summary", name: "summary of earlier work", text, source, occurredAt: at(c), score: 90 })
331
+ }
332
+ let last: Extract<Turn, { kind: "assistant" }> | undefined
333
+ let userAfter = false
334
+ for (let i = session.turns.length - 1; i >= 0; i--) {
335
+ const t = session.turns[i]!
336
+ if (t.kind === "user" && !isInjected(t.text)) userAfter = true
337
+ if (t.kind === "assistant" && t.text.trim().length >= 60) {
338
+ last = t
339
+ break
340
+ }
341
+ }
342
+ if (last && !userAfter && last.text.length >= 200 && DONE.test(last.text) && !last.text.trim().endsWith("?")) {
343
+ const text = redact(clipText(last.text, 1200))
344
+ out.push({ id: `summary-${sha(source + "last")}`, kind: "summary", name: "how the session ended", text, source, occurredAt: at(last), score: 70 })
345
+ }
346
+ return out
347
+ }
348
+
349
+ function state(session: Session, at: (t: Turn) => string | undefined, hasEndSummary: boolean): Scored[] {
350
+ const users = realUserTurns(session)
351
+ const goal = users[0]
352
+ if (!goal) return []
353
+ const lastUser = users[users.length - 1]!
354
+ const lastAssistant = [...session.turns].reverse().find((t): t is Extract<Turn, { kind: "assistant" }> => t.kind === "assistant" && t.text.trim().length >= 20)
355
+ const parts = [`Goal: ${clipText(withoutInjected(goal.text), 300)}`]
356
+ if (lastUser !== goal) parts.push(`Last request: ${clipText(withoutInjected(lastUser.text), 300)}`)
357
+ if (lastAssistant) parts.push(`Where it stands: ${clipText(lastAssistant.text, hasEndSummary ? 160 : 400)}`)
358
+ const text = redact(parts.join("\n"))
359
+ const source = session.header.id
360
+ return [{ id: `state-${sha(source)}`, kind: "state", name: clipText(`state: ${session.header.title ?? withoutInjected(goal.text)}`, 70), text, source, occurredAt: at(session.turns[session.turns.length - 1]!), score: 100 }]
361
+ }
362
+
363
+ // ---- entry point --------------------------------------------------------------------------------------------------
364
+
365
+ export function distill(session: Session, opts: DistillOptions = {}): MemoryItem[] {
366
+ const max = opts.maxItems ?? 40
367
+ const fallback = session.header.updatedAt ?? session.header.startedAt
368
+ const at = (t: Turn): string | undefined => ("at" in t && t.at) || fallback
369
+ const sums = summaries(session, at)
370
+ const hasEnd = sums.some((s) => s.name === "how the session ended")
371
+ const all: Scored[] = [...state(session, at, hasEnd), ...sums, ...instructions(session, at), ...failures(session, at), ...files(session, at), ...commands(session, at)]
372
+ const kept = all
373
+ .filter((i) => i.text.replace(/\s+/g, " ").trim().length >= 20)
374
+ .filter((i) => !(i.kind === "file" && /^[\w./-]+:\s*$/.test(i.text)))
375
+ .sort((a, b) => b.score - a.score)
376
+ .slice(0, max)
377
+ const seen = new Set<string>()
378
+ return kept
379
+ .filter((i) => (seen.has(i.id) ? false : (seen.add(i.id), true)))
380
+ .map(({ score: _s, ...item }) => item)
381
+ }