@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,361 @@
1
+ import { Database } from "bun:sqlite"
2
+ import { randomBytes } from "node:crypto"
3
+ import { copyFileSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs"
4
+ import { dirname, extname, join } from "node:path"
5
+ import { toolLabel } from "../../brief.ts"
6
+ import { exists, gitInfo, isDir, isRunning, versionOf } from "../../fsutil.ts"
7
+ import type { Session, Turn } from "../../session/types.ts"
8
+ import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
9
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
10
+ import { cline4Reader } from "./reader.ts"
11
+ import { LABEL, NOTE_KIND, PROCESSES, SESSIONS_TABLE, TESTED_VERSIONS, TOOL, continuedNote, manifestPath, messagesPath, sessionDir, sessionsDb } from "./records.ts"
12
+
13
+ // Continues a portable session as a new Cline 4.x SDK session, written the way Cline's own session importer
14
+ // (core/src/services/session-import) writes an imported Claude Code or Codex conversation: a `sessions` row in
15
+ // sessions.db, a manifest and a messages file, a completed root session whose metadata carries the title,
16
+ // sessionHistoryOrigin {mode: "import"} and continuedFrom (Cline's importer writes importedFrom, which makes Cline summarise on resume). `cline history` lists it and `cline --id <id>`
17
+ // resumes it with the earlier turns on screen; Cline Desktop shows it in its history.
18
+ //
19
+ // Messages are Cline's Anthropic-shaped MessageWithMetadata: one assistant message per model response (its
20
+ // readable reasoning as a thinking block without a signature, its text, its tool_use blocks), then one user
21
+ // message with the tool_result blocks, as Cline's importer pairs them; a call without a result is closed as
22
+ // an error. Tool names stay as they were, as the importer leaves them. A compaction is Cline's own: a
23
+ // compaction_summary message replaces the turns before it. The note saying where the session came from is a
24
+ // user message with displayRole "system", which the model reads and the panel does not show.
25
+ //
26
+ // sessions.db is written only while no Cline CLI, hub or Desktop process runs, after a copy of it is kept
27
+ // under ~/.brain/backups/cline-sdk/.
28
+
29
+ const IMAGE_TYPES: Record<string, string> = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" }
30
+ const IMAGE_LIMIT = 5 * 1024 * 1024
31
+ const NANO = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"
32
+
33
+ type Json = Record<string, any>
34
+ type Call = Extract<Turn, { kind: "tool_call" }>
35
+ type Result = Extract<Turn, { kind: "tool_result" }>
36
+
37
+ const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
38
+
39
+ function realCwd(cwd: string): string {
40
+ try {
41
+ return realpathSync(cwd)
42
+ } catch {
43
+ return cwd
44
+ }
45
+ }
46
+
47
+ /** A session id as Cline makes one: `${Date.now()}_${nanoid(5)}`. */
48
+ export const sessionId = (now: Date) => `${now.getTime()}_${[...randomBytes(5)].map((b) => NANO[b & 63]).join("")}`
49
+
50
+ /** Call ids the providers Cline talks to accept (Anthropic's ^[a-zA-Z0-9_-]{1,64}$). */
51
+ const safeId = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "call"
52
+
53
+ const objectInput = (input: unknown): Json => (input && typeof input === "object" && !Array.isArray(input) ? (input as Json) : { input: input ?? null })
54
+
55
+ /** The provider a model id belongs to, for the history list; Cline resumes with the person's own provider. */
56
+ function providerOf(model: string | undefined, tool: string): string {
57
+ if (!model) return tool
58
+ if (/claude|anthropic/i.test(model)) return "anthropic"
59
+ if (/gemini/i.test(model)) return "gemini"
60
+ if (/gpt|^o\d|codex|openai/i.test(model)) return "openai"
61
+ const slash = model.indexOf("/")
62
+ return slash > 0 ? model.slice(0, slash) : tool
63
+ }
64
+
65
+ export interface ClinePlan {
66
+ messages: Json[]
67
+ title: string
68
+ prompt?: string
69
+ lost: string[]
70
+ /** What a read-back must find. */
71
+ expect: { users: number; assistants: number; calls: number }
72
+ }
73
+
74
+ /** The messages of a new Cline session continuing `session`. Pure apart from reading attached images. */
75
+ export function planMessages(session: Session, opts: { now: Date }): ClinePlan {
76
+ const drops = new Map<string, number>()
77
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
78
+ const label = toolLabel(session.header.tool)
79
+
80
+ // Cline's compaction replaces what came before it: start from the last summary.
81
+ const lastCompaction = session.turns.map((t) => t.kind).lastIndexOf("compaction")
82
+ const turns = lastCompaction >= 0 ? session.turns.slice(lastCompaction) : session.turns
83
+ if (lastCompaction > 0) drop("turns before the last compaction, replaced by its summary as Cline compacts", lastCompaction)
84
+ const droppedUsers = session.turns.slice(0, Math.max(0, lastCompaction)).filter((t) => t.kind === "user").length
85
+
86
+ const results = new Map<string, Result>()
87
+ for (const t of turns) if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
88
+ const ids = new Map<string, string>()
89
+ const used = new Set<string>()
90
+ let remapped = 0
91
+ const idFor = (id: string) => {
92
+ let v = ids.get(id)
93
+ if (v) return v
94
+ v = safeId(id)
95
+ for (let n = 2; used.has(v); n++) v = `${safeId(id)}_${n}`
96
+ ids.set(id, v)
97
+ used.add(v)
98
+ if (v !== id) remapped++
99
+ return v
100
+ }
101
+ const ts = (at?: string) => {
102
+ const t = at ? Date.parse(at) : Number.NaN
103
+ return Number.isFinite(t) ? { ts: t } : {}
104
+ }
105
+
106
+ const expect = { users: 0, assistants: 0, calls: 0 }
107
+ const messages: Json[] = []
108
+ const note: Json = {
109
+ role: "user",
110
+ content: [{ type: "text", text: continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)) }],
111
+ metadata: { kind: NOTE_KIND, displayRole: "system", userRunSpan: 0 },
112
+ }
113
+ let cur: { msg: Json; calls: Call[] } | undefined
114
+ const flush = () => {
115
+ if (!cur) return
116
+ const { msg, calls } = cur
117
+ cur = undefined
118
+ if (!msg.content.length) return
119
+ messages.push(msg)
120
+ if (!calls.length) return
121
+ const blocks = calls.map((call) => {
122
+ const r = results.get(call.id)
123
+ if (!r) drop("tool calls without a result (closed as errors)")
124
+ const output = r ? (r.fullPath ? `${r.output}\n\n[Full output: ${r.fullPath}]` : r.output) : "No result was recorded for this call; the session moved on before it finished."
125
+ return { type: "tool_result", tool_use_id: idFor(call.id), name: call.name, content: output.trim() ? output : "(no output)", ...(r?.ok === false || !r ? { is_error: true } : {}) }
126
+ })
127
+ const last = calls.map((c) => results.get(c.id)?.at).filter(Boolean).at(-1)
128
+ messages.push({ role: "user", content: blocks, ...ts(last) })
129
+ }
130
+ const assistant = (at: string | undefined, model: string | undefined) => {
131
+ if (!cur) {
132
+ const id = model ?? session.header.models.at(-1)
133
+ cur = { msg: { role: "assistant", content: [], ...(id ? { modelInfo: { id, provider: providerOf(id, session.header.tool) } } : {}), ...ts(at) }, calls: [] }
134
+ }
135
+ return cur
136
+ }
137
+ const seen = new Set<string>()
138
+
139
+ for (const t of turns) {
140
+ switch (t.kind) {
141
+ case "user": {
142
+ flush()
143
+ const content: Json[] = []
144
+ let text = t.text
145
+ for (const a of t.attachments ?? []) {
146
+ if (!a.path) continue // bytes the reader could not keep; it already counted them
147
+ if (a.kind === "file") {
148
+ text += `\n\n[Attached file: ${a.path}]`
149
+ continue
150
+ }
151
+ const mime = a.mime ?? IMAGE_TYPES[extname(a.path).toLowerCase()]
152
+ let size = Number.POSITIVE_INFINITY
153
+ try {
154
+ size = statSync(a.path).size
155
+ } catch {}
156
+ if (mime && Object.values(IMAGE_TYPES).includes(mime) && size <= IMAGE_LIMIT) content.push({ type: "image", data: readFileSync(a.path).toString("base64"), mediaType: mime })
157
+ else drop("images Cline cannot hold")
158
+ }
159
+ if (!text.trim() && !content.length) break
160
+ if (text.trim()) content.unshift({ type: "text", text })
161
+ messages.push({ role: "user", content, ...ts(t.at) })
162
+ if (text.trim()) expect.users++
163
+ break
164
+ }
165
+ case "assistant":
166
+ // Text after a call is a new response: it was written after the call's result came back.
167
+ if (cur?.calls.length) flush()
168
+ if (!t.text.trim()) break
169
+ assistant(t.at, t.model).msg.content.push({ type: "text", text: t.text })
170
+ expect.assistants++
171
+ break
172
+ case "reasoning":
173
+ if (cur?.calls.length) flush()
174
+ if (t.text.trim()) assistant(t.at, undefined).msg.content.push({ type: "thinking", thinking: t.text })
175
+ break
176
+ case "tool_call": {
177
+ if (seen.has(t.id)) {
178
+ drop("repeated tool call ids")
179
+ break
180
+ }
181
+ seen.add(t.id)
182
+ const a = assistant(t.at, undefined)
183
+ a.msg.content.push({ type: "tool_use", id: idFor(t.id), name: t.name, input: objectInput(t.input) })
184
+ a.calls.push(t)
185
+ expect.calls++
186
+ break
187
+ }
188
+ case "tool_result":
189
+ if (!seen.has(t.callId)) drop("tool results without a call")
190
+ else if (results.get(t.callId) !== t) drop("repeated tool results")
191
+ break
192
+ case "compaction":
193
+ flush()
194
+ // Cline's own compaction summary (compaction-shared.ts): off screen, for the model.
195
+ messages.push({
196
+ role: "user",
197
+ content: [{ type: "text", text: `Context summary:\n\n[Summary of the earlier conversation in ${label}]\n\n${t.summary}` }],
198
+ metadata: { kind: "compaction_summary", displayRole: "system", userRunSpan: droppedUsers, summary: t.summary, details: { readFiles: [], modifiedFiles: [] }, tokensBefore: 0, generatedAt: opts.now.getTime() },
199
+ ...ts(t.at),
200
+ })
201
+ break
202
+ case "file_edit":
203
+ if (!t.callId || !seen.has(t.callId)) drop("file edits outside a tool call")
204
+ break
205
+ case "subagent":
206
+ drop("subagent sessions (their results stay in the parent's tool results)")
207
+ break
208
+ }
209
+ }
210
+ flush()
211
+ if (!messages.length) throw new Error("nothing Cline can show: the session has no messages or tool calls")
212
+ // The note opens the model's context, after a compaction summary when there is one.
213
+ messages.splice(lastCompaction >= 0 ? 1 : 0, 0, note)
214
+
215
+ const firstText = firstUserText(session)?.replace(/\s+/g, " ").trim()
216
+ const title = `${session.header.title ?? firstText?.slice(0, 60) ?? "Session"} (from ${label})`.slice(0, 120)
217
+ const lost = [...drops].map(([what, n]) => `${what} (${n})`)
218
+ if (remapped) lost.push(`tool call ids, changed to fit Cline (${remapped})`)
219
+ return { messages, title, ...(firstText ? { prompt: firstText.slice(0, 2000) } : {}), lost, expect }
220
+ }
221
+
222
+ /** A filesystem-safe timestamp for a backup folder. */
223
+ const stamp = (d: Date) => d.toISOString().replace(/[:.]/g, "-")
224
+
225
+ /** Copies sessions.db and its -wal and -shm files; returns the folder. */
226
+ export function backupDb(home: string, path: string, now: Date): string {
227
+ const dir = join(home, ".brain", "backups", TOOL, stamp(now))
228
+ mkdirSync(dir, { recursive: true })
229
+ for (const side of ["", "-wal", "-shm"]) if (exists(path + side)) copyFileSync(path + side, join(dir, `sessions.db${side}`))
230
+ return dir
231
+ }
232
+
233
+ const semver = (v: string | undefined) => v?.match(/(\d+\.\d+\.\d+)/)?.[1]
234
+
235
+ /** The Cline 4 writer; `version` and `running` are for tests, which must not depend on what is installed or open. */
236
+ export function cline4Writer(opts: { version?: string; running?: () => boolean } = {}): Writer {
237
+ const installed = () => opts.version ?? semver(versionOf("cline"))
238
+ const running = opts.running ?? (() => PROCESSES.some(isRunning))
239
+ return {
240
+ tool: TOOL,
241
+ label: LABEL,
242
+ way: "native",
243
+
244
+ async ready(w: WriteOptions): Promise<Readiness> {
245
+ const v = installed()
246
+ if (!v) return { ok: false, why: "the Cline CLI is not installed (no `cline` on the PATH)" }
247
+ if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Cline CLI ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
248
+ if (running()) return { ok: false, why: "Cline is running (the CLI, its hub or Cline Desktop); quit it first, since it keeps sessions.db open" }
249
+ if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder` }
250
+ return { ok: true }
251
+ },
252
+
253
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
254
+ const now = new Date()
255
+ const cwd = realCwd(w.cwd)
256
+ const id = sessionId(now)
257
+ const plan = planMessages(session, { now })
258
+ const db = sessionsDb(w.home)
259
+ const files = { manifest: manifestPath(w.home, id), messages: messagesPath(w.home, id) }
260
+ const written = [db, files.manifest, files.messages]
261
+ if (!w.dryRun) {
262
+ if (running()) throw new Error("Cline is running; quit it before writing its sessions database")
263
+ if (exists(db)) written.push(backupDb(w.home, db, now))
264
+ const started = new Date(session.header.startedAt && Number.isFinite(Date.parse(session.header.startedAt)) ? Date.parse(session.header.startedAt) : now.getTime()).toISOString()
265
+ const ended = now.toISOString()
266
+ const model = session.header.models.at(-1) ?? "unknown"
267
+ const provider = providerOf(session.header.models.at(-1), session.header.tool)
268
+ const branch = gitInfo(cwd, w.home).gitBranch ?? session.header.project.gitBranch
269
+ const metadata = {
270
+ title: plan.title,
271
+ ...(branch ? { git: { branch } } : {}),
272
+ sessionHistoryOrigin: { mode: "import", trigger: session.header.tool },
273
+ // Not `importedFrom`: Cline 3.0.74 summarises a session that has it with the model the first time it resumes
274
+ // it (the wire harness saw the follow-up sent over a model-written summary), which would drop the turns.
275
+ continuedFrom: { tool: session.header.tool, sourceSessionId: session.header.sourceId, sourcePath: session.header.sourcePath ?? "", importedAt: ended },
276
+ }
277
+ const row = { source: "cli", status: "completed", interactive: true, provider, model, enableTools: true, enableSpawn: false, enableTeams: false }
278
+ mkdirSync(sessionDir(w.home, id), { recursive: true })
279
+ writeFileSync(
280
+ files.messages,
281
+ `${JSON.stringify({ version: 1, updated_at: ended, agent: "lead", sessionId: id, origin: { source: row.source, mode: "import", sessionId: id, trigger: session.header.tool }, messages: plan.messages }, null, 2)}\n`,
282
+ )
283
+ writeFileSync(
284
+ files.manifest,
285
+ `${JSON.stringify(
286
+ {
287
+ version: 1,
288
+ session_id: id,
289
+ source: row.source,
290
+ pid: 0,
291
+ started_at: started,
292
+ ended_at: ended,
293
+ exit_code: 0,
294
+ status: row.status,
295
+ interactive: row.interactive,
296
+ provider,
297
+ model,
298
+ cwd,
299
+ workspace_root: cwd,
300
+ enable_tools: row.enableTools,
301
+ enable_spawn: row.enableSpawn,
302
+ enable_teams: row.enableTeams,
303
+ ...(plan.prompt ? { prompt: plan.prompt } : {}),
304
+ metadata,
305
+ messages_path: files.messages,
306
+ },
307
+ null,
308
+ 2,
309
+ )}\n`,
310
+ )
311
+ mkdirSync(dirname(db), { recursive: true })
312
+ const d = new Database(db)
313
+ try {
314
+ d.exec("PRAGMA journal_mode = WAL")
315
+ d.exec("PRAGMA busy_timeout = 5000")
316
+ d.exec(SESSIONS_TABLE)
317
+ // A plain INSERT, unlike Cline's INSERT OR REPLACE: a new session never replaces a row.
318
+ d.query(
319
+ `INSERT INTO sessions (session_id, source, pid, started_at, ended_at, exit_code, status, status_lock, interactive, provider, model, cwd, workspace_root, team_name,
320
+ enable_tools, enable_spawn, enable_teams, parent_session_id, parent_agent_id, agent_id, conversation_id, is_subagent, prompt, metadata_json, transcript_path, hook_path,
321
+ messages_path, updated_at) VALUES (?, ?, 0, ?, ?, 0, ?, 0, 1, ?, ?, ?, ?, NULL, 1, 0, 0, NULL, NULL, NULL, NULL, 0, ?, ?, '', '', ?, ?)`,
322
+ ).run(id, row.source, started, ended, row.status, provider, model, cwd, cwd, plan.prompt ?? null, JSON.stringify(metadata), files.messages, ended)
323
+ } finally {
324
+ d.close()
325
+ }
326
+ let problem: string | undefined
327
+ try {
328
+ const back = await cline4Reader.read({ tool: TOOL, sourceId: id, path: files.messages }, w.home)
329
+ const n = countTurns(back)
330
+ const pairs = unpaired(back)
331
+ if (n.user !== plan.expect.users || n.assistant !== plan.expect.assistants || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length)
332
+ problem = `users ${n.user}/${plan.expect.users}, replies ${n.assistant}/${plan.expect.assistants}, tool calls ${n.tool_call}/${plan.expect.calls}, unpaired ${pairs.calls.length + pairs.results.length}`
333
+ } catch (e) {
334
+ problem = e instanceof Error ? e.message : String(e)
335
+ }
336
+ if (problem) {
337
+ const d2 = new Database(db)
338
+ try {
339
+ d2.query("DELETE FROM sessions WHERE session_id = ?").run(id)
340
+ } finally {
341
+ d2.close()
342
+ }
343
+ rmSync(sessionDir(w.home, id), { recursive: true, force: true })
344
+ throw new Error(`the new Cline session did not read back as written (${problem}); it was removed`)
345
+ }
346
+ }
347
+ return {
348
+ tool: TOOL,
349
+ way: "native",
350
+ sessionId: id,
351
+ written,
352
+ open: `cd ${shellQuote(cwd)} && cline --id ${id}`,
353
+ note: `Cline Desktop lists it in its history as "${plan.title}".`,
354
+ lost: plan.lost,
355
+ dryRun: !!w.dryRun,
356
+ }
357
+ },
358
+ }
359
+ }
360
+
361
+ export const cline4NativeWriter = cline4Writer()
@@ -0,0 +1,8 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { codebuffReader } from "./reader.ts"
3
+ import { codebuffNativeWriter } from "./writer.ts"
4
+
5
+ // Codebuff and Freebuff (one CLI under two names, sharing ~/.config/manicode): reads their chats, and continues
6
+ // any session as a new chat that `freebuff --continue <id>` (or `codebuff --continue <id>`) opens.
7
+ export const readers: Reader[] = [codebuffReader]
8
+ export const writers: Writer[] = [codebuffNativeWriter]
@@ -0,0 +1,207 @@
1
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs"
2
+ import { cpus } from "node:os"
3
+ import { basename, dirname, join, relative } from "node:path"
4
+
5
+ // The project snapshot a Freebuff chat carries (RunState.sessionState.fileContext), made the way the SDK makes
6
+ // one for a new chat (sdk/src/run-state.ts initialSessionState, common/src/project-file-tree.ts): the folder
7
+ // tree as the system prompt lists it, the project's AGENTS.md / CLAUDE.md, ~/.AGENTS.md, a git summary and
8
+ // the machine. A continued chat keeps the snapshot it started with, so this is what Freebuff itself would
9
+ // have stored had the chat begun here today. Differences from Freebuff's own, kept small on purpose:
10
+ // - ignore rules come from git (`git ls-files --ignored`) where the folder is a git work tree, which covers
11
+ // .gitignore but not .codebuffignore / .manicodeignore; outside git only the default ignores apply;
12
+ // - no symbol index (fileTokenScores is empty): Freebuff skips it for its default agent too;
13
+ // - skills are left for the skill tool to find on disk, and the repository's commit statistics and
14
+ // GitHub visibility (Freebuff asks `gh`) are left out.
15
+
16
+ export interface FileNode {
17
+ name: string
18
+ type: "file" | "directory"
19
+ filePath: string
20
+ children?: FileNode[]
21
+ lastReadTime?: number
22
+ }
23
+
24
+ /** common/src/constants/paths.ts DEFAULT_IGNORED_PATHS, as gitignore patterns on a name. */
25
+ const DEFAULT_IGNORED = [".git", ".env", ".env.*", "*.min.*", "node_modules", "venv", "virtualenv", ".venv", ".virtualenv", "__pycache__", "*.egg-info/", "*.pyc", ".DS_Store", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".next", "package-lock.json", "bun.lockb"]
26
+ const globRe = (p: string) => new RegExp(`^${p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")}$`)
27
+ const IGNORED = DEFAULT_IGNORED.map((p) => ({ re: globRe(p.replace(/\/$/, "")), dirOnly: p.endsWith("/") }))
28
+ const defaultIgnored = (name: string, dir: boolean, shallow: boolean) => (shallow && name.startsWith(".")) || IGNORED.some((i) => (!i.dirOnly || dir) && i.re.test(name))
29
+
30
+ export const DEFAULT_MAX_FILES = 10_000
31
+ const SHALLOW = { depth: 3, files: 2_000, dirs: 500 }
32
+
33
+ const git = (cwd: string, args: string[]) => {
34
+ try {
35
+ const p = Bun.spawnSync(["git", ...args], { cwd, stdout: "pipe", stderr: "ignore", timeout: 20_000 })
36
+ return p.exitCode === 0 ? p.stdout.toString() : undefined
37
+ } catch {
38
+ return undefined
39
+ }
40
+ }
41
+
42
+ /** True when `dir` is `home` or holds it: Freebuff then scans only a few levels (isValidProjectRoot). */
43
+ const holdsHome = (dir: string, home: string) => !relative(dir, home).startsWith("..")
44
+
45
+ /** The folder tree, breadth first and capped, as getProjectFileTree builds it. */
46
+ export function fileTree(cwd: string, home: string): { tree: FileNode[]; paths: string[] } {
47
+ const shallow = holdsHome(cwd, home)
48
+ const maxFiles = shallow ? SHALLOW.files : DEFAULT_MAX_FILES
49
+ const maxDepth = shallow ? SHALLOW.depth : Number.POSITIVE_INFINITY
50
+ const maxDirs = shallow ? SHALLOW.dirs : Number.POSITIVE_INFINITY
51
+ // Untracked paths git ignores (directories end in "/"); git applies every .gitignore on the way down.
52
+ const ignored = new Set((git(cwd, ["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory"]) ?? "").split("\0").filter(Boolean))
53
+ const root: FileNode = { name: basename(cwd), type: "directory", filePath: "", children: [] }
54
+ const queue: Array<{ node: FileNode; full: string; depth: number }> = [{ node: root, full: cwd, depth: 0 }]
55
+ const seen = new Set<string>()
56
+ const paths: string[] = []
57
+ let dirs = 0
58
+ while (queue.length && paths.length < maxFiles && dirs < maxDirs) {
59
+ const { node, full, depth } = queue.shift()!
60
+ dirs++
61
+ try {
62
+ const real = realpathSync(full)
63
+ if (seen.has(real)) continue
64
+ seen.add(real)
65
+ } catch {
66
+ continue
67
+ }
68
+ let names: string[]
69
+ try {
70
+ names = readdirSync(full).sort()
71
+ } catch {
72
+ continue
73
+ }
74
+ for (const name of names) {
75
+ if (paths.length >= maxFiles) break
76
+ const path = join(full, name)
77
+ const rel = relative(cwd, path).split("\\").join("/")
78
+ let st: ReturnType<typeof statSync>
79
+ try {
80
+ st = statSync(path)
81
+ } catch {
82
+ continue
83
+ }
84
+ const dir = st.isDirectory()
85
+ if (defaultIgnored(name, dir, shallow) || ignored.has(dir ? `${rel}/` : rel)) continue
86
+ if (dir) {
87
+ const child: FileNode = { name, type: "directory", children: [], filePath: rel }
88
+ node.children!.push(child)
89
+ if (depth + 1 < maxDepth) queue.push({ node: child, full: path, depth: depth + 1 })
90
+ } else {
91
+ node.children!.push({ name, type: "file", lastReadTime: st.atimeMs, filePath: rel })
92
+ paths.push(rel)
93
+ }
94
+ }
95
+ }
96
+ return { tree: root.children!, paths }
97
+ }
98
+
99
+ const TEST_DIRS = new Set(["__tests__", "__specs__", "test", "tests", "spec", "specs"])
100
+ /** sdk/src/run-state.ts isTestFilePath. */
101
+ export function isTestFilePath(filePath: string): boolean {
102
+ const segments = filePath.split("/")
103
+ const name = segments.pop() ?? ""
104
+ if (segments.some((s) => TEST_DIRS.has(s.toLowerCase()))) return true
105
+ const lower = name.toLowerCase()
106
+ return (
107
+ /\.(?:test|tests|spec|specs|cy)\./.test(lower) ||
108
+ /^(?:test|spec)_.+\.[^.]+$/.test(lower) ||
109
+ /_(?:test|tests|spec|specs)\.[^.]+$/.test(lower) ||
110
+ /^(?:test|tests|spec|specs)\.[^.]+$/.test(lower) ||
111
+ /(?:Test|Tests|TestCase|Spec)\.[^.]+$/.test(name)
112
+ )
113
+ }
114
+
115
+ const KNOWLEDGE_NAMES = ["agents.md", "claude.md"]
116
+ const isKnowledgeFile = (p: string) => {
117
+ const name = basename(p).toLowerCase()
118
+ return KNOWLEDGE_NAMES.includes(name) || name.endsWith(".knowledge.md")
119
+ }
120
+
121
+ /** One instructions file per folder, AGENTS.md before CLAUDE.md (selectKnowledgeFilePaths), with its text. */
122
+ export function knowledgeFiles(cwd: string, paths: string[]): Record<string, string> {
123
+ const byDir = new Map<string, string[]>()
124
+ for (const p of paths.filter(isKnowledgeFile)) byDir.set(dirname(p), [...(byDir.get(dirname(p)) ?? []), p])
125
+ const out: Record<string, string> = {}
126
+ for (const files of byDir.values()) {
127
+ const pick = KNOWLEDGE_NAMES.map((n) => files.find((f) => f.toLowerCase().endsWith(n))).find(Boolean)
128
+ if (!pick) continue
129
+ try {
130
+ out[pick] = readFileSync(join(cwd, pick), "utf8")
131
+ } catch {}
132
+ }
133
+ return out
134
+ }
135
+
136
+ /** ~/.AGENTS.md, else ~/.CLAUDE.md, matched without case (loadUserKnowledgeFiles). */
137
+ export function userKnowledgeFiles(home: string): Record<string, string> {
138
+ let names: string[]
139
+ try {
140
+ names = readdirSync(home)
141
+ } catch {
142
+ return {}
143
+ }
144
+ for (const want of KNOWLEDGE_NAMES) {
145
+ const name = names.find((n) => n.startsWith(".") && n.slice(1).toLowerCase() === want)
146
+ if (!name) continue
147
+ try {
148
+ return { [`~/${name}`]: readFileSync(join(home, name), "utf8") }
149
+ } catch {}
150
+ }
151
+ return {}
152
+ }
153
+
154
+ const CHROME: Record<string, string[]> = {
155
+ darwin: ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta", "/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev", "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", "/Applications/Chromium.app/Contents/MacOS/Chromium"],
156
+ linux: ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/google-chrome-beta", "/usr/bin/google-chrome-unstable", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/snap/bin/chromium"],
157
+ }
158
+
159
+ /** The git summary the system prompt prints (getGitChanges), without commit statistics or visibility. */
160
+ function gitChanges(cwd: string, counts: { fileCount: number; fileCountIsLowerBound: boolean; testFileCount: number }) {
161
+ const lists = [["diff", "--name-only", "--"], ["diff", "--cached", "--name-only", "--"], ["ls-files", "--others", "--exclude-standard"]].map((a) => git(cwd, a))
162
+ const changed = [...new Set(lists.flatMap((o) => (o ?? "").split("\n").map((l) => l.trim()).filter(Boolean)))].sort()
163
+ const count = Number.parseInt(git(cwd, ["rev-list", "--count", "HEAD"]) ?? "", 10)
164
+ const shallow = git(cwd, ["rev-parse", "--is-shallow-repository"])?.trim()
165
+ return {
166
+ gitAvailable: lists.some((o) => o !== undefined),
167
+ ...(git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"])?.trim() ? { branch: git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"])!.trim() } : {}),
168
+ changedFiles: changed.slice(0, 25),
169
+ changedFileCount: changed.length,
170
+ changedFileScanTruncated: false,
171
+ repositoryVisibility: "unknown" as const,
172
+ ...(Number.isFinite(count) ? { commitCount: count } : {}),
173
+ ...(shallow ? { historyIsShallow: shallow === "true" } : {}),
174
+ ...counts,
175
+ }
176
+ }
177
+
178
+ /** The fileContext of a new chat in `cwd`, in the SDK's key order. */
179
+ export function fileContext(cwd: string, home: string) {
180
+ const { tree, paths } = fileTree(cwd, home)
181
+ const counts = { fileCount: paths.length, fileCountIsLowerBound: paths.length >= DEFAULT_MAX_FILES, testFileCount: paths.filter(isTestFilePath).length }
182
+ return {
183
+ projectRoot: cwd,
184
+ cwd,
185
+ fileTree: tree,
186
+ fileTokenScores: {},
187
+ tokenCallers: {},
188
+ knowledgeFiles: knowledgeFiles(cwd, paths),
189
+ userKnowledgeFiles: userKnowledgeFiles(home),
190
+ agentTemplates: {},
191
+ customToolDefinitions: {},
192
+ skills: {},
193
+ includeHomeSkills: false,
194
+ gitChanges: gitChanges(cwd, counts),
195
+ changesSinceLastChat: {},
196
+ shellConfigFiles: {},
197
+ systemInfo: {
198
+ platform: process.platform,
199
+ shell: "bash",
200
+ nodeVersion: process.version,
201
+ arch: process.arch,
202
+ homedir: home,
203
+ cpus: cpus().length,
204
+ chromeAvailable: (CHROME[process.platform] ?? []).some((p) => existsSync(p)),
205
+ },
206
+ }
207
+ }