@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,523 @@
1
+ import { Database } from "bun:sqlite"
2
+ import { randomBytes, randomUUID } from "node:crypto"
3
+ import { chmodSync, linkSync, lstatSync, mkdirSync, rmSync } from "node:fs"
4
+ import { dirname, join, resolve } from "node:path"
5
+ import { toolLabel } from "../../brief.ts"
6
+ import { exists, isDir } from "../../fsutil.ts"
7
+ import { withSnapshot } from "../../sqlite.ts"
8
+ import { imageDataUrl, parseDataUrl } from "../../session/attachments.ts"
9
+ import type { Session, ToolKind, Turn, Usage } from "../../session/types.ts"
10
+ import { countTurns, unpaired } from "../../session/util.ts"
11
+ import { continuedNote } from "../opencode/note.ts"
12
+ import { asRecord, safeToolName } from "../opencode/tools.ts"
13
+ import { callTitle, continuedTitle, sh } from "../opencode/writer.ts"
14
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
15
+ import { readDevin } from "./reader.ts"
16
+ import {
17
+ COLUMNS,
18
+ COMPACTION_EXT,
19
+ DEFAULT_MODEL,
20
+ LABEL,
21
+ MIGRATIONS,
22
+ SCHEMA,
23
+ SUMMARY_PREFIX,
24
+ TESTED_VERSIONS,
25
+ TOOL,
26
+ acpKindOf,
27
+ devinBin,
28
+ devinVersion,
29
+ imageSize,
30
+ schemaProblem,
31
+ storePath,
32
+ type DevinImage,
33
+ type DevinMessage,
34
+ type DevinMessageMeta,
35
+ type DevinToolCall,
36
+ } from "./records.ts"
37
+
38
+ // Continues a portable session in Devin CLI as a new session of its shared sessions.db, written as the rows
39
+ // Devin 3000.6.7 writes itself (the native-corpus capture): a sessions row and one message_nodes chain from the
40
+ // root to sessions.main_chain_id. The chain opens with brain's note as a system message; then a user node per
41
+ // prompt (images as base64 `images`), an assistant node per model response (its text, its readable reasoning as
42
+ // `thinking`, its `tool_calls`), and a tool node per result, with the ACP tool_call_state Devin keeps for each
43
+ // call. Prompts go to prompt_history, as session-migrate's writer does. A compaction becomes a system node
44
+ // marked "brain/compaction" (Devin's own summary form is not known), and reads back as a compaction.
45
+ //
46
+ // The write is one BEGIN IMMEDIATE transaction that refuses an id the store already has and reads the session
47
+ // back before committing; the store is backed up first (VACUUM INTO, a consistent copy even while Devin runs).
48
+ // The doc does not say Devin holds the store open, and SQLite's locking covers a concurrent writer, so a running
49
+ // Devin is not refused. A missing store is built with the exact schema-16 text and migrations Devin makes, under
50
+ // a temporary name, and linked into place only if no store appeared meanwhile.
51
+
52
+ /** The message_nodes.metadata Devin writes for a node that is not part of its system prefix. */
53
+ const NODE_META = JSON.stringify({ summarized_from: null, num_tokens_preceding: null, is_system_prefix: null })
54
+
55
+ export interface NodeRow {
56
+ node_id: number
57
+ parent_node_id: number | null
58
+ chat_message: DevinMessage
59
+ created_at: number
60
+ metadata: string
61
+ }
62
+
63
+ export interface DevinPlan {
64
+ id: string
65
+ /** The sessions row, by column. */
66
+ session: Record<string, string | number | null>
67
+ nodes: NodeRow[]
68
+ prompts: Array<{ content: string; timestamp: number }>
69
+ states: Array<{ tool_call_id: string; tool_call_json: string; tool_call_update_json: string }>
70
+ lost: string[]
71
+ /** What a read-back must find. */
72
+ expect: { users: number; assistants: number; calls: number; reasoning: number; compactions: number }
73
+ }
74
+
75
+ /** A new session id in Devin's slug style that says where the session came from, e.g. "from-claude-code-3f9a1c". */
76
+ export function newSessionId(session: Session, taken: Set<string>): string {
77
+ const from = session.header.tool.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^[-_]+/, "").slice(0, 40) || "session"
78
+ for (;;) {
79
+ const id = `from-${from}-${randomBytes(3).toString("hex")}`
80
+ if (!taken.has(id)) return id
81
+ }
82
+ }
83
+
84
+ function metaOf(
85
+ source: string,
86
+ operation: string,
87
+ at: string,
88
+ o: { userInput?: boolean; finish?: string; tokens?: number; extensions?: Record<string, unknown>; model?: string } = {},
89
+ ): DevinMessageMeta {
90
+ return {
91
+ num_tokens: o.tokens ?? null,
92
+ is_user_input: o.userInput ?? null,
93
+ request_id: null,
94
+ metrics: null,
95
+ finish_reason: o.finish ?? null,
96
+ ...(o.extensions ? { extensions: o.extensions } : {}),
97
+ ...(o.model !== undefined ? { started_generation_at: at } : {}),
98
+ created_at: at,
99
+ ...(o.model !== undefined ? { generation_model: o.model } : {}),
100
+ telemetry: { source, operation },
101
+ }
102
+ }
103
+
104
+ const FILE_KEYS = ["file_path", "filePath", "path", "target_file", "absolute_path", "file"]
105
+
106
+ /** A portable call as Devin draws it: an ACP title, kind and the file it touches. */
107
+ function acpView(name: string, kind: ToolKind, input: Record<string, unknown>) {
108
+ const file = ["read", "edit", "write"].includes(kind) ? FILE_KEYS.map((k) => input[k]).find((v): v is string => typeof v === "string" && !!v) : undefined
109
+ const title = kind === "read" ? "Read file" : kind === "edit" || kind === "write" ? "Edit file" : callTitle(name, input)
110
+ return { title, kind: acpKindOf(kind), ...(file ? { locations: [{ path: file }] } : {}) }
111
+ }
112
+
113
+ /** The rows of a new Devin session continuing `session`. Pure: writes nothing. */
114
+ export function planDevin(session: Session, opts: { id: string; cwd: string; now: Date }): DevinPlan {
115
+ const drops = new Map<string, number>()
116
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
117
+ const nowSec = Math.floor(opts.now.getTime() / 1000)
118
+ const nodes: NodeRow[] = []
119
+ const prompts: DevinPlan["prompts"] = []
120
+ const states: DevinPlan["states"] = []
121
+ const expect = { users: 0, assistants: 0, calls: 0, reasoning: 0, compactions: 0 }
122
+
123
+ // Node times are whole seconds that never go back and never pass now; the JSON keeps the turn's own time when it fits.
124
+ let clock = Math.min(nowSec, Math.floor((Date.parse(session.header.startedAt ?? "") || opts.now.getTime()) / 1000))
125
+ const tick = (at?: string): string => {
126
+ const ms = at ? Date.parse(at) : NaN
127
+ const t = Number.isFinite(ms) ? Math.floor(ms / 1000) : NaN
128
+ if (t > clock) clock = Math.min(t, nowSec)
129
+ return t === clock ? new Date(ms).toISOString() : new Date(clock * 1000).toISOString()
130
+ }
131
+ const push = (chat_message: DevinMessage): NodeRow => {
132
+ const node: NodeRow = { node_id: nodes.length + 1, parent_node_id: nodes.length || null, chat_message, created_at: clock, metadata: NODE_META }
133
+ nodes.push(node)
134
+ return node
135
+ }
136
+
137
+ const sourceModel = session.header.models.at(-1)
138
+ const devinModel = sourceModel && /^swe-/i.test(sourceModel) ? sourceModel : DEFAULT_MODEL
139
+ /** The model of the response being written: the first reply's to begin with. */
140
+ let model = session.turns.find((t): t is Extract<Turn, { kind: "assistant" }> => t.kind === "assistant" && !!t.model)?.model ?? sourceModel ?? devinModel
141
+
142
+ interface Call {
143
+ id: string
144
+ name: string
145
+ kind: string
146
+ view: ReturnType<typeof acpView>
147
+ input: Record<string, unknown>
148
+ }
149
+ const calls = new Map<string, Call>()
150
+ const answered = new Set<string>()
151
+ const meta = (c: Call) => ({ "cognition.ai/inferenceToolName": c.name })
152
+
153
+ const result = (src: string, ok: boolean, output: string, at?: string) => {
154
+ const c = calls.get(src)!
155
+ answered.add(src)
156
+ const when = tick(at)
157
+ push({
158
+ message_id: randomUUID(),
159
+ role: "tool",
160
+ content: output,
161
+ tool_call_id: c.id,
162
+ metadata: metaOf("tool_result", c.name, when, {
163
+ extensions: {
164
+ "chisel/tool_result_meta": ok ? { success: true, kind: c.kind } : { success: false, failure_reason: "ToolError", kind: c.kind },
165
+ ...(ok ? {} : { "chisel/tool_failure": { reason: "ToolError" } }),
166
+ },
167
+ }),
168
+ })
169
+ states.push({
170
+ tool_call_id: c.id,
171
+ tool_call_json: JSON.stringify({ toolCallId: c.id, title: c.view.title, kind: c.view.kind, ...(c.view.locations ? { locations: c.view.locations } : {}), rawInput: c.input, _meta: meta(c) }),
172
+ tool_call_update_json: JSON.stringify({
173
+ toolCallId: c.id,
174
+ status: ok ? "completed" : "failed",
175
+ ...(ok ? {} : { content: [{ type: "content", content: { type: "text", text: output } }] }),
176
+ _meta: meta(c),
177
+ }),
178
+ })
179
+ }
180
+
181
+ interface Step {
182
+ node: NodeRow
183
+ at: string
184
+ model: string
185
+ text: string[]
186
+ thinking: string[]
187
+ calls: DevinToolCall[]
188
+ sources: string[]
189
+ shown: Record<string, unknown>
190
+ usage?: Usage
191
+ results: boolean
192
+ }
193
+ let step: Step | undefined
194
+ const pending: Array<() => void> = []
195
+
196
+ const openStep = (at?: string): Step => {
197
+ if (step && !step.results) return step
198
+ closeStep()
199
+ const when = tick(at)
200
+ // Placeholders in Devin's key order; closeStep fills them.
201
+ const node = push({ message_id: randomUUID(), role: "assistant", content: "", tool_calls: [], thinking: null, metadata: metaOf("assistant", "inference", when) })
202
+ step = { node, at: when, model, text: [], thinking: [], calls: [], sources: [], shown: {}, results: false }
203
+ return step
204
+ }
205
+ function closeStep() {
206
+ if (!step) return
207
+ const s = step
208
+ step = undefined
209
+ const m = s.node.chat_message
210
+ m.content = s.text.join("\n\n")
211
+ m.tool_calls = s.calls
212
+ m.thinking = s.thinking.length ? { thinking: s.thinking.join("\n\n"), signature: "" } : null
213
+ m.metadata = metaOf("assistant", "inference", s.at, {
214
+ finish: s.calls.length ? "tool_calls" : "stop",
215
+ ...(s.usage?.output !== undefined ? { tokens: s.usage.output } : {}),
216
+ model: s.model,
217
+ ...(s.calls.length ? { extensions: { "chisel/tool_call_content": s.shown } } : {}),
218
+ })
219
+ if (s.text.length) expect.assistants++
220
+ if (s.thinking.length) expect.reasoning++
221
+ for (const r of pending.splice(0)) r()
222
+ // Every call needs its result before the next message, as the model's API requires.
223
+ for (const src of s.sources)
224
+ if (!answered.has(src)) {
225
+ result(src, false, "No result was recorded for this call.")
226
+ drop("calls with no result, marked as failed")
227
+ }
228
+ }
229
+
230
+ push({
231
+ message_id: randomUUID(),
232
+ role: "system",
233
+ content: continuedNote(toolLabel(session.header.tool), session.header.id, opts.now.toISOString().slice(0, 10)),
234
+ metadata: metaOf("system", "unknown", tick(session.header.startedAt)),
235
+ })
236
+
237
+ for (const t of session.turns as Turn[]) {
238
+ switch (t.kind) {
239
+ case "user": {
240
+ const images: DevinImage[] = []
241
+ let skipped = 0
242
+ for (const a of t.attachments ?? []) {
243
+ const url = a.kind === "image" ? parseDataUrl(imageDataUrl(a)) : undefined
244
+ if (!url) {
245
+ skipped++
246
+ drop("attachments (not images, or their files are gone)")
247
+ continue
248
+ }
249
+ images.push({ ...imageSize(Buffer.from(url.base64, "base64")), base64_data: url.base64, mime_type: url.mime, source_path: a.path ?? a.name ?? "", caption: "" })
250
+ }
251
+ const text = t.text.trim() ? t.text : skipped ? `(${skipped} attachment${skipped > 1 ? "s" : ""} not carried over)` : ""
252
+ if (!text && !images.length) break
253
+ closeStep()
254
+ const when = tick(t.at)
255
+ push({
256
+ message_id: randomUUID(),
257
+ role: "user",
258
+ content: text,
259
+ ...(images.length ? { images } : {}),
260
+ metadata: metaOf("user", "unknown", when, { userInput: true, ...(text ? { extensions: { "chisel/acp-content-blocks": [{ type: "text", text }] } } : {}) }),
261
+ })
262
+ if (text.trim()) prompts.push({ content: text, timestamp: clock })
263
+ expect.users++
264
+ break
265
+ }
266
+ case "assistant": {
267
+ if (!t.text.trim()) break
268
+ if (t.model && t.model !== model) {
269
+ // A response belongs to one model: text from another starts a new one (its reasoning stays with it).
270
+ if (step && (step.text.length || step.calls.length)) closeStep()
271
+ model = t.model
272
+ if (step) step.model = model
273
+ }
274
+ const s = openStep(t.at)
275
+ s.text.push(t.text)
276
+ if (t.usage) s.usage = t.usage
277
+ // Devin's metrics block holds timings this session does not have; only the output count is kept (num_tokens).
278
+ if (t.usage && (t.usage.input !== undefined || t.usage.cacheRead !== undefined || t.usage.cacheWrite !== undefined)) drop("token usage of replies, other than the output count")
279
+ break
280
+ }
281
+ case "reasoning":
282
+ if (!t.text.trim()) break
283
+ openStep(t.at).thinking.push(t.text)
284
+ drop("reasoning: kept as text, without provider signatures")
285
+ break
286
+ case "tool_call": {
287
+ if (calls.has(t.id)) {
288
+ drop("repeated tool call ids")
289
+ break
290
+ }
291
+ const s = openStep(t.at)
292
+ const raw = t.input
293
+ if (raw !== undefined && raw !== null && (typeof raw !== "object" || Array.isArray(raw))) drop("tool inputs that were not objects (wrapped)")
294
+ const input = asRecord(raw)
295
+ const name = safeToolName(t.name)
296
+ if (name !== t.name) drop("tool names changed to fit Devin")
297
+ const view = acpView(name, t.toolKind, input)
298
+ const id = `call_${randomBytes(12).toString("hex")}`
299
+ calls.set(t.id, { id, name, kind: view.kind, view, input })
300
+ s.calls.push({ id, name, arguments: input, index: s.calls.length, kind: "function" })
301
+ s.sources.push(t.id)
302
+ s.shown[id] = { toolCallId: id, title: view.title, status: "pending", ...(view.locations ? { locations: view.locations } : {}), kind: view.kind, rawInput: input }
303
+ expect.calls++
304
+ break
305
+ }
306
+ case "tool_result": {
307
+ if (!calls.has(t.callId)) {
308
+ drop("tool results without a call")
309
+ break
310
+ }
311
+ // A result belongs right after the response that made its call; one that comes later (after the next
312
+ // message, when its call was already closed as failed) has nowhere to go.
313
+ if (answered.has(t.callId) || !step?.sources.includes(t.callId)) {
314
+ drop("repeated tool results, or results that came after the conversation moved on")
315
+ break
316
+ }
317
+ const src = t.callId
318
+ answered.add(src)
319
+ step.results = true
320
+ pending.push(() => result(src, t.ok, t.output, t.at))
321
+ break
322
+ }
323
+ case "compaction": {
324
+ if (!t.summary.trim()) break
325
+ closeStep()
326
+ const when = tick(t.at)
327
+ push({ message_id: randomUUID(), role: "system", content: `${SUMMARY_PREFIX}${t.summary}`, metadata: metaOf("system", "unknown", when, { extensions: { [COMPACTION_EXT]: true } }) })
328
+ expect.compactions++
329
+ drop("compactions: kept as a marked system message, the turns before them stay in the history")
330
+ break
331
+ }
332
+ case "file_edit":
333
+ if (!t.callId) drop("file edits not made by a tool call")
334
+ break
335
+ case "subagent":
336
+ drop("subagent sessions (their call and result are kept)")
337
+ break
338
+ }
339
+ }
340
+ closeStep()
341
+
342
+ const lost = [...drops].map(([what, n]) => `${what} (${n})`)
343
+ if (calls.size) lost.push(`tool-call ids replaced (${calls.size})`)
344
+ if (sourceModel && devinModel !== sourceModel) lost.push(`the model: Devin continues on ${devinModel} (the session used ${sourceModel})`)
345
+ const first = nodes[0]!
346
+ const last = nodes.at(-1)!
347
+ return {
348
+ id: opts.id,
349
+ session: {
350
+ id: opts.id,
351
+ working_directory: opts.cwd,
352
+ backend_type: "windsurf",
353
+ model: devinModel,
354
+ agent_mode: "accept-edits",
355
+ created_at: first.created_at,
356
+ last_activity_at: last.created_at,
357
+ title: continuedTitle(session),
358
+ main_chain_id: last.node_id,
359
+ shell_last_seen_index: 0,
360
+ cogs_json: "[]",
361
+ workspace_dirs: "[]",
362
+ hidden: 0,
363
+ metadata: JSON.stringify({ total_credit_cost: 0, total_acu_cost: 0, response_dimensions: [] }),
364
+ },
365
+ nodes,
366
+ prompts,
367
+ states,
368
+ lost,
369
+ expect,
370
+ }
371
+ }
372
+
373
+ /** Inserts the plan in one BEGIN IMMEDIATE transaction and reads it back before committing; any failure rolls it back. */
374
+ export function insertPlan(db: Database, plan: DevinPlan, path: string) {
375
+ const cols = COLUMNS.sessions!
376
+ // Prepared statements are finalized here: one left to the garbage collector keeps the connection open.
377
+ const stmts = [
378
+ db.prepare(`INSERT INTO sessions (${cols.join(", ")}) VALUES (${cols.map(() => "?").join(", ")})`),
379
+ db.prepare("INSERT INTO message_nodes (session_id, node_id, parent_node_id, chat_message, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?)"),
380
+ db.prepare("INSERT INTO prompt_history (content, timestamp, session_id, is_shell) VALUES (?, ?, ?, 0)"),
381
+ db.prepare("INSERT INTO tool_call_state (session_id, tool_call_id, tool_call_json, tool_call_update_json) VALUES (?, ?, ?, ?)"),
382
+ ] as const
383
+ const [session, node, prompt, state] = stmts
384
+ db.exec("BEGIN IMMEDIATE")
385
+ try {
386
+ if (db.query("SELECT 1 FROM sessions WHERE id = ?").get(plan.id)) throw new Error(`refusing to overwrite Devin session ${plan.id}: the store already has it`)
387
+ session.run(...cols.map((c) => plan.session[c] ?? null))
388
+ for (const n of plan.nodes) node.run(plan.id, n.node_id, n.parent_node_id, JSON.stringify(n.chat_message), n.created_at, n.metadata)
389
+ for (const p of plan.prompts) prompt.run(p.content, p.timestamp, plan.id)
390
+ for (const s of plan.states) state.run(plan.id, s.tool_call_id, s.tool_call_json, s.tool_call_update_json)
391
+ // Read back as Devin's sessions are read.
392
+ const back = readDevin(db, plan.id, path)
393
+ const n = countTurns(back)
394
+ const pairs = unpaired(back)
395
+ const e = plan.expect
396
+ if (n.user !== e.users || n.assistant !== e.assistants || n.tool_call !== e.calls || n.reasoning !== e.reasoning || n.compaction !== e.compactions || pairs.calls.length || pairs.results.length)
397
+ throw new Error(
398
+ `the Devin session did not read back as written (users ${n.user}/${e.users}, replies ${n.assistant}/${e.assistants}, tool calls ${n.tool_call}/${e.calls}, reasoning ${n.reasoning}/${e.reasoning}, compactions ${n.compaction}/${e.compactions}, unpaired ${pairs.calls.length + pairs.results.length}); nothing was kept`,
399
+ )
400
+ db.exec("COMMIT")
401
+ } catch (err) {
402
+ db.exec("ROLLBACK")
403
+ throw err
404
+ } finally {
405
+ for (const st of stmts) st.finalize()
406
+ }
407
+ }
408
+
409
+ /** Adds the session to an existing store after a consistent backup. */
410
+ export function addToStore(dbPath: string, plan: DevinPlan, backupFile: string) {
411
+ if (lstatSync(dbPath).isSymbolicLink()) throw new Error(`the Devin store ${dbPath} is a symbolic link; brain does not write through one`)
412
+ // readwrite without create: a store removed meanwhile is an error, not a new empty store.
413
+ const db = new Database(dbPath, { readwrite: true })
414
+ try {
415
+ db.exec("PRAGMA busy_timeout = 5000")
416
+ db.exec("PRAGMA foreign_keys = ON")
417
+ const problem = schemaProblem(db)
418
+ if (problem) throw new Error(problem)
419
+ mkdirSync(dirname(backupFile), { recursive: true, mode: 0o700 })
420
+ db.run("VACUUM INTO ?", [backupFile])
421
+ chmodSync(backupFile, 0o600)
422
+ insertPlan(db, plan, dbPath)
423
+ try {
424
+ db.exec("PRAGMA wal_checkpoint(PASSIVE)")
425
+ } catch {
426
+ // Devin checkpoints the log itself.
427
+ }
428
+ } finally {
429
+ db.close(true)
430
+ }
431
+ }
432
+
433
+ /** Builds a missing store as Devin 3000.6.7 does, with the session in it, and puts it in place atomically. */
434
+ export function createStore(dbPath: string, plan: DevinPlan, now: Date) {
435
+ const dir = dirname(dbPath)
436
+ mkdirSync(dir, { recursive: true, mode: 0o700 })
437
+ const tmp = join(dir, `.sessions.${randomBytes(6).toString("hex")}.db.tmp`)
438
+ try {
439
+ const db = new Database(tmp)
440
+ try {
441
+ db.exec("PRAGMA journal_mode = WAL")
442
+ for (const sql of SCHEMA) db.exec(sql)
443
+ // refinery's applied_on: RFC 3339 with nanoseconds, a moment apart per migration.
444
+ const base = now.toISOString().slice(0, 23)
445
+ const add = db.prepare("INSERT INTO refinery_schema_history (version, name, applied_on, checksum) VALUES (?, ?, ?, ?)")
446
+ MIGRATIONS.forEach(([version, name, checksum], i) => add.run(version, name, `${base}${String(i * 1000).padStart(6, "0")}Z`, checksum))
447
+ add.finalize()
448
+ insertPlan(db, plan, dbPath)
449
+ // Everything into the main file: the -wal of the temporary name cannot follow the link.
450
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)")
451
+ } finally {
452
+ // Released now, before the file is linked into place: SQLite tracks a WAL store by its inode, and a
453
+ // connection left open on the temporary name would tie the store's shared memory to that name.
454
+ db.close(true)
455
+ }
456
+ chmodSync(tmp, 0o600)
457
+ try {
458
+ linkSync(tmp, dbPath)
459
+ } catch (err) {
460
+ if ((err as NodeJS.ErrnoException).code === "EEXIST") throw new Error(`a Devin store appeared at ${dbPath} while brain was writing; it was left as it is`)
461
+ throw err
462
+ }
463
+ } finally {
464
+ for (const side of ["", "-wal", "-shm", "-journal"]) rmSync(tmp + side, { force: true })
465
+ }
466
+ }
467
+
468
+ const stamp = (d: Date) => d.toISOString().replace(/[:.]/g, "-")
469
+
470
+ export const devinWriter: Writer = {
471
+ tool: TOOL,
472
+ label: LABEL,
473
+ way: "native",
474
+
475
+ async ready(opts: WriteOptions): Promise<Readiness> {
476
+ const bin = devinBin()
477
+ if (!bin) return { ok: false, why: "Devin CLI is not installed (no devin on PATH)" }
478
+ const { raw, version } = devinVersion(bin)
479
+ if (!version || !TESTED_VERSIONS.includes(version))
480
+ return { ok: false, why: `Devin CLI ${version ?? raw ?? "(version unknown)"} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
481
+ if (!isDir(opts.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${opts.cwd}` }
482
+ const db = storePath(opts.home)
483
+ if (exists(db)) {
484
+ let problem: string | undefined
485
+ try {
486
+ problem = withSnapshot(db, schemaProblem)
487
+ } catch (e) {
488
+ problem = `the Devin store at ${db} cannot be read: ${(e as Error).message}`
489
+ }
490
+ if (problem) return { ok: false, why: problem }
491
+ }
492
+ return { ok: true }
493
+ },
494
+
495
+ async write(session: Session, opts: WriteOptions): Promise<WriteResult> {
496
+ const cwd = resolve(opts.cwd)
497
+ const dbPath = storePath(opts.home)
498
+ const now = new Date()
499
+ const had = exists(dbPath)
500
+ let taken = new Set<string>()
501
+ if (had) {
502
+ const seen = withSnapshot(dbPath, (d) => ({ problem: schemaProblem(d), ids: (d.query("SELECT id FROM sessions").all() as Array<{ id: string }>).map((r) => r.id) }))
503
+ if (seen.problem) throw new Error(seen.problem)
504
+ taken = new Set(seen.ids)
505
+ }
506
+ const plan = planDevin(session, { id: newSessionId(session, taken), cwd, now })
507
+ const backup = join(opts.home, ".brain", "backups", TOOL, `${stamp(now)}-${plan.id}`, "sessions.db")
508
+ if (!opts.dryRun) {
509
+ if (had) addToStore(dbPath, plan, backup)
510
+ else createStore(dbPath, plan, now)
511
+ }
512
+ return {
513
+ tool: TOOL,
514
+ way: "native",
515
+ sessionId: plan.id,
516
+ written: had ? [dbPath, backup] : [dbPath],
517
+ // `devin list` shows the sessions of the folder it runs in, so the session is resumed from its own.
518
+ open: `cd ${sh(cwd)} && devin --resume ${plan.id}`,
519
+ lost: plan.lost,
520
+ dryRun: !!opts.dryRun,
521
+ }
522
+ },
523
+ }
@@ -0,0 +1,12 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { factoryDroidReader } from "./reader.ts"
3
+ import { factoryDroidWriter } from "./writer.ts"
4
+
5
+ // Factory Droid (CLI): reads its sessions and continues any session as a new native Droid session,
6
+ // `droid --resume <id>`.
7
+
8
+ export { DROID_TESTED_VERSIONS, factoryDroidWriter } from "./writer.ts"
9
+ export { factoryDroidReader }
10
+
11
+ export const readers: Reader[] = [factoryDroidReader]
12
+ export const writers: Writer[] = [factoryDroidWriter()]