@100xprompt/brain 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +172 -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 +326 -0
  170. package/src/fsutil.ts +133 -0
  171. package/src/gateway/connect.ts +845 -0
  172. package/src/gateway/signin.ts +323 -0
  173. package/src/handoff.ts +104 -0
  174. package/src/hooks.ts +455 -0
  175. package/src/memory/audit.ts +514 -0
  176. package/src/memory/blobs.ts +94 -0
  177. package/src/memory/cli.ts +117 -0
  178. package/src/memory/distill.ts +381 -0
  179. package/src/memory/externalize.ts +435 -0
  180. package/src/memory/lookups.ts +70 -0
  181. package/src/memory/mcp-install.ts +401 -0
  182. package/src/memory/mcp-server.ts +296 -0
  183. package/src/memory/prepare.ts +179 -0
  184. package/src/memory/preview.ts +154 -0
  185. package/src/memory/project.ts +35 -0
  186. package/src/memory/redact.ts +72 -0
  187. package/src/memory/relevance.ts +187 -0
  188. package/src/memory/repomap.ts +516 -0
  189. package/src/memory/store.ts +270 -0
  190. package/src/memory/sync.ts +166 -0
  191. package/src/memory/tidy.ts +193 -0
  192. package/src/memory/types.ts +118 -0
  193. package/src/org/admin-ops.ts +96 -0
  194. package/src/org/auth.ts +112 -0
  195. package/src/org/backup.ts +115 -0
  196. package/src/org/cli.ts +438 -0
  197. package/src/org/client.ts +404 -0
  198. package/src/org/config.ts +44 -0
  199. package/src/org/crypto.ts +537 -0
  200. package/src/org/db.ts +435 -0
  201. package/src/org/graph.ts +318 -0
  202. package/src/org/oidc.ts +435 -0
  203. package/src/org/policy.ts +398 -0
  204. package/src/org/server.ts +920 -0
  205. package/src/org/sso.ts +316 -0
  206. package/src/org/tls.ts +66 -0
  207. package/src/org/types.ts +193 -0
  208. package/src/self.ts +68 -0
  209. package/src/session/attachments.ts +53 -0
  210. package/src/session/peekcache.ts +50 -0
  211. package/src/session/types.ts +104 -0
  212. package/src/session/util.ts +130 -0
  213. package/src/sqlite.ts +48 -0
@@ -0,0 +1,177 @@
1
+ // Cline's original tool protocol (Cline 3.x, Roo Code before "native" tools): the model writes each tool
2
+ // call as XML inside its text, `<read_file><path>src/a.ts</path></read_file>`, and the extension answers in
3
+ // the next user message with a text block headed `[read_file for 'src/a.ts'] Result:`. Neither side has an
4
+ // id, so a result is paired with a call in the assistant message just before it. `<thinking>` blocks in the
5
+ // same text are the model's readable reasoning.
6
+
7
+ /**
8
+ * Tool names of the XML protocol in Cline and Roo Code, from the tool enums in the installed bundles (Cline
9
+ * 3.56.2 and 4.1.26, Roo 3.54.0) plus names older versions used. A tag is read as a call only when it is one
10
+ * of these, or when the next message carries a result for it, so the HTML in a file the model wrote stays text.
11
+ */
12
+ export const XML_TOOLS = new Set([
13
+ // Cline
14
+ "execute_command",
15
+ "read_file",
16
+ "write_to_file",
17
+ "replace_in_file",
18
+ "search_files",
19
+ "list_files",
20
+ "list_code_definition_names",
21
+ "browser_action",
22
+ "use_mcp_tool",
23
+ "access_mcp_resource",
24
+ "load_mcp_documentation",
25
+ "ask_followup_question",
26
+ "attempt_completion",
27
+ "plan_mode_respond",
28
+ "act_mode_respond",
29
+ "new_task",
30
+ "focus_chain",
31
+ "web_fetch",
32
+ "web_search",
33
+ "condense",
34
+ "summarize_task",
35
+ "report_bug",
36
+ "new_rule",
37
+ "apply_patch",
38
+ "generate_explanation",
39
+ "use_skill",
40
+ // Roo Code
41
+ "read_command_output",
42
+ "apply_diff",
43
+ "edit",
44
+ "search_and_replace",
45
+ "search_replace",
46
+ "edit_file",
47
+ "insert_content",
48
+ "codebase_search",
49
+ "switch_mode",
50
+ "update_todo_list",
51
+ "run_slash_command",
52
+ "skill",
53
+ "generate_image",
54
+ "custom_tool",
55
+ "fetch_instructions",
56
+ ])
57
+
58
+ /** Parameters whose value can itself contain tags (file contents, diffs, a final answer): closed at the last close tag. */
59
+ const LONG_PARAMS = new Set(["content", "diff", "result", "response", "question", "arguments", "todos"])
60
+
61
+ export type Piece =
62
+ | { kind: "text"; text: string }
63
+ | { kind: "thinking"; text: string }
64
+ | { kind: "call"; name: string; input: Record<string, unknown> }
65
+
66
+ const OPEN_TAG = /<([A-Za-z_][A-Za-z0-9_]*)>/g
67
+
68
+ /** Assistant text split into plain text, `<thinking>` reasoning and XML tool calls, in order. */
69
+ export function splitAssistantText(text: string, isTool: (name: string) => boolean): Piece[] {
70
+ const out: Piece[] = []
71
+ const pushText = (t: string) => {
72
+ // A </thinking> left after a call that ended the reasoning early (below) is not text.
73
+ const s = t.replace(/<\/thinking>/g, "").trim()
74
+ if (s) out.push({ kind: "text", text: s })
75
+ }
76
+ let pos = 0
77
+ const re = new RegExp(OPEN_TAG.source, "g")
78
+ let m: RegExpExecArray | null
79
+ while ((m = re.exec(text))) {
80
+ const name = m[1]!
81
+ if (name !== "thinking" && !isTool(name)) continue
82
+ const bodyStart = m.index + m[0].length
83
+ const close = `</${name}>`
84
+ let end = text.indexOf(close, bodyStart)
85
+ // An unclosed tag is a call the model was still writing when the stream stopped: take the rest.
86
+ let bodyEnd = end < 0 ? text.length : end
87
+ // The extension parses calls wherever they are, <thinking> or not, and some models never close <thinking>
88
+ // and go straight on to the call: reasoning ends at a call inside it (a tool tag closed within it, or any
89
+ // tool tag when it is never closed), and the call is read next.
90
+ const call = name === "thinking" ? callInside(text, bodyStart, end, isTool) : -1
91
+ if (call >= 0) end = bodyEnd = call
92
+ pushText(text.slice(pos, m.index))
93
+ const body = text.slice(bodyStart, bodyEnd)
94
+ if (name === "thinking") {
95
+ if (body.trim()) out.push({ kind: "thinking", text: body.trim() })
96
+ } else out.push({ kind: "call", name, input: parseParams(body) })
97
+ pos = call >= 0 ? call : end < 0 ? text.length : end + close.length
98
+ re.lastIndex = pos
99
+ }
100
+ pushText(text.slice(pos))
101
+ return out
102
+ }
103
+
104
+ /** Where a tool call starts inside a <thinking> block that runs from `from` to `end` (-1: never closed), or -1. */
105
+ function callInside(text: string, from: number, end: number, isTool: (name: string) => boolean): number {
106
+ const limit = end < 0 ? text.length : end
107
+ const re = new RegExp(OPEN_TAG.source, "g")
108
+ re.lastIndex = from
109
+ let m: RegExpExecArray | null
110
+ while ((m = re.exec(text)) && m.index < limit) {
111
+ const name = m[1]!
112
+ if (name === "thinking" || !isTool(name)) continue
113
+ // In a closed block, a tag the model only mentioned has no close tag of its own before the block ends.
114
+ if (end < 0) return m.index
115
+ const close = text.indexOf(`</${name}>`, m.index + m[0].length)
116
+ if (close >= 0 && close < end) return m.index
117
+ }
118
+ return -1
119
+ }
120
+
121
+ /** `<path>a</path><content>b</content>` as { path: "a", content: "b" }; repeated tags become arrays, nested tags objects. */
122
+ export function parseParams(body: string): Record<string, unknown> {
123
+ const out: Record<string, unknown> = {}
124
+ const re = new RegExp(OPEN_TAG.source, "g")
125
+ let m: RegExpExecArray | null
126
+ while ((m = re.exec(body))) {
127
+ const name = m[1]!
128
+ const start = m.index + m[0].length
129
+ const close = `</${name}>`
130
+ const end = LONG_PARAMS.has(name) ? body.lastIndexOf(close) : body.indexOf(close, start)
131
+ const raw = end < start ? body.slice(start) : body.slice(start, end)
132
+ const value = paramValue(name, raw)
133
+ const prev = out[name]
134
+ out[name] = prev === undefined ? value : Array.isArray(prev) ? [...prev, value] : [prev, value]
135
+ if (end < start) break
136
+ re.lastIndex = end + close.length
137
+ }
138
+ return out
139
+ }
140
+
141
+ function paramValue(name: string, raw: string): unknown {
142
+ // Cline keeps file contents and diffs as written, less the newline after the open tag and before the close.
143
+ if (name === "content" || name === "diff") return raw.replace(/^\r?\n/, "").replace(/\r?\n$/, "")
144
+ const t = raw.trim()
145
+ // Roo's read_file nests its arguments: <args><file><path>a</path></file></args>.
146
+ if (!LONG_PARAMS.has(name) && t.startsWith("<") && t.endsWith(">")) {
147
+ const nested = parseParams(t)
148
+ if (Object.keys(nested).length) return nested
149
+ }
150
+ return t
151
+ }
152
+
153
+ /**
154
+ * A result header, `[read_file for 'src/a.ts'] Result:`: the tool name and the text after the header. The
155
+ * quoted part is the call's main argument as written, so it can run over several lines (a multi-line
156
+ * ask_followup_question): that form is tried when the header does not close on its first line.
157
+ */
158
+ export function resultHeader(text: string): { label: string; name: string; rest: string } | undefined {
159
+ const m = text.match(/^\s*\[([^\n]*?)\] Result:[^\S\n]*\n?/) ?? text.match(/^\s*\[([A-Za-z0-9_]+ for '[\s\S]{0,4000}?'[^\n\]]*)\] Result:[^\S\n]*\n?/)
160
+ if (!m) return undefined
161
+ const label = m[1]!
162
+ return { label, name: label.match(/^[A-Za-z0-9_]+/)?.[0] ?? label, rest: text.slice(m[0].length) }
163
+ }
164
+
165
+ /**
166
+ * The tool named by a notice that a call never ran: the extension runs one XML call per message, or none after
167
+ * the person rejects one, and answers each call it skipped with a text block of its own, e.g. `Tool [x] was not
168
+ * executed because a tool has already been used in this message.`, Roo's `Skipping tool [x for 'a'] due to user
169
+ * rejecting a previous tool.`, Cline's `Skipping tool due to user rejecting a previous tool. [x for 'a']`.
170
+ */
171
+ export function skipNotice(text: string): string | undefined {
172
+ const t = text.trimStart()
173
+ return (
174
+ t.match(/^(?:Tool|Skipping tool) \[([A-Za-z0-9_]+)[\s\S]{0,4000}?\] (?:was not executed|was interrupted and not executed|due to user rejecting)/)?.[1] ??
175
+ t.match(/^(?:Skipping tool|Tool was interrupted and not executed) due to user rejecting a previous tool\.\s*\[([A-Za-z0-9_]+)/)?.[1]
176
+ )
177
+ }
@@ -0,0 +1,9 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { cline4Reader } from "./reader.ts"
3
+ import { cline4NativeWriter } from "./writer.ts"
4
+
5
+ // The Cline 4.x SDK runtime (the `cline` CLI 3.x, Cline Desktop, the 4.x extension): reads its sessions from
6
+ // ~/.cline/data, and continues any session as a new one there (written only while Cline is closed, after a
7
+ // backup of sessions.db). Cline's classic task store is the `cline` adapter's.
8
+ export const readers: Reader[] = [cline4Reader]
9
+ export const writers: Writer[] = [cline4NativeWriter]
@@ -0,0 +1,179 @@
1
+ import { readdirSync } from "node:fs"
2
+ import { exists, gitInfo, readJson } from "../../fsutil.ts"
3
+ import type { Attachment, Session, Turn, Usage } from "../../session/types.ts"
4
+ import { clip, header, iso, lossCounter, noteModel } from "../../session/util.ts"
5
+ import { withSnapshot } from "../../sqlite.ts"
6
+ import type { Reader, SessionRef } from "../types.ts"
7
+ import { CONTINUED_NOTE, LABEL, NOTE_KIND, TOOL, clineToolKind, manifestPath, messagesPath, sessionsDb, sessionsDir } from "./records.ts"
8
+
9
+ // Cline 4.x SDK sessions (records.ts has the layout): the root sessions in sessions.db, newest first, or
10
+ // the manifests when there is no database. Subagent and teammate sessions belong to their root session and
11
+ // are not listed. Cline's classic task store (~/.cline/data/tasks, globalStorage) is the `cline` reader's.
12
+
13
+ type Json = Record<string, any>
14
+
15
+ /** What the person typed: Cline wraps it in <user_input mode="..."> and adds <mode_notice> elements. */
16
+ const displayText = (text: string) =>
17
+ text
18
+ .replace(/<mode_notice>[\s\S]*?<\/mode_notice>/g, "")
19
+ .replace(/<\/?(user_input|user_command)\b[^>]*>/g, "")
20
+ .trim()
21
+
22
+ const usageOf = (m: Json | undefined): Usage | undefined => {
23
+ if (!m || typeof m !== "object") return undefined
24
+ const out: Usage = {}
25
+ if (m.inputTokens) out.input = m.inputTokens
26
+ if (m.outputTokens) out.output = m.outputTokens
27
+ if (m.cacheReadTokens) out.cacheRead = m.cacheReadTokens
28
+ if (m.cacheWriteTokens) out.cacheWrite = m.cacheWriteTokens
29
+ return Object.keys(out).length ? out : undefined
30
+ }
31
+
32
+ const resultText = (content: unknown, images: () => void): string => {
33
+ if (typeof content === "string") return content
34
+ const parts: string[] = []
35
+ for (const b of Array.isArray(content) ? content : []) {
36
+ if (b?.type === "text" && typeof b.text === "string") parts.push(b.text)
37
+ else if (b?.type === "file" && typeof b.path === "string") parts.push(`[File: ${b.path}]`)
38
+ else images()
39
+ }
40
+ return parts.join("\n")
41
+ }
42
+
43
+ export const cline4Reader: Reader = {
44
+ tool: TOOL,
45
+ label: LABEL,
46
+ async list(home) {
47
+ const db = sessionsDb(home)
48
+ const rows: Json[] = exists(db)
49
+ ? withSnapshot(db, (d) =>
50
+ d
51
+ .query(
52
+ "SELECT session_id, cwd, metadata_json, prompt, started_at, ended_at, updated_at, messages_path FROM sessions WHERE is_subagent = 0 AND (parent_session_id IS NULL OR parent_session_id = '') ORDER BY started_at DESC",
53
+ )
54
+ .all() as Json[],
55
+ )
56
+ : manifestRows(home)
57
+ const refs: SessionRef[] = []
58
+ for (const r of rows) {
59
+ let meta: Json = {}
60
+ try {
61
+ meta = typeof r.metadata_json === "string" ? JSON.parse(r.metadata_json) : (r.metadata_json ?? {})
62
+ } catch {}
63
+ const title = typeof meta.title === "string" && meta.title ? meta.title : typeof r.prompt === "string" ? r.prompt.split("\n")[0]?.slice(0, 80) : undefined
64
+ const updated = iso(r.ended_at) ?? iso(r.updated_at) ?? iso(r.started_at)
65
+ refs.push({
66
+ tool: TOOL,
67
+ sourceId: String(r.session_id),
68
+ path: typeof r.messages_path === "string" && r.messages_path ? r.messages_path : messagesPath(home, String(r.session_id)),
69
+ ...(title ? { title } : {}),
70
+ ...(typeof r.cwd === "string" && r.cwd ? { cwd: r.cwd } : {}),
71
+ ...(updated ? { updatedAt: updated } : {}),
72
+ })
73
+ }
74
+ return refs
75
+ },
76
+ async read(ref, home) {
77
+ const path = exists(ref.path) ? ref.path : messagesPath(home, ref.sourceId)
78
+ const file = readJson<Json>(path)
79
+ if (!file || !Array.isArray(file.messages)) throw new Error(`${path} is not a Cline messages file`)
80
+ const manifest = readJson<Json>(manifestPath(home, ref.sourceId)) ?? {}
81
+ return parseMessages(file, manifest, { id: ref.sourceId, path, home })
82
+ },
83
+ }
84
+
85
+ /** Manifests stand in for the database's rows when Cline has not created sessions.db. */
86
+ function manifestRows(home: string): Json[] {
87
+ let names: string[]
88
+ try {
89
+ names = readdirSync(sessionsDir(home))
90
+ } catch {
91
+ return []
92
+ }
93
+ const rows: Json[] = []
94
+ for (const id of names) {
95
+ const m = readJson<Json>(manifestPath(home, id))
96
+ if (m?.version !== 1 || m.session_id !== id) continue
97
+ rows.push({ session_id: id, cwd: m.cwd, metadata_json: m.metadata, prompt: m.prompt, started_at: m.started_at, ended_at: m.ended_at, messages_path: m.messages_path })
98
+ }
99
+ return rows.sort((a, b) => String(b.started_at).localeCompare(String(a.started_at)))
100
+ }
101
+
102
+ /** A messages file (with its manifest) as a portable session. */
103
+ export function parseMessages(file: Json, manifest: Json, ctx: { id: string; path: string; home: string }): Session {
104
+ const cwd = typeof manifest.cwd === "string" && manifest.cwd ? manifest.cwd : undefined
105
+ const title = typeof manifest.metadata?.title === "string" ? manifest.metadata.title : undefined
106
+ const h = header(TOOL, ctx.id, {
107
+ sourcePath: ctx.path,
108
+ ...(title ? { title } : {}),
109
+ project: cwd ? { cwd, ...gitInfo(cwd, ctx.home) } : {},
110
+ ...(iso(manifest.started_at) ? { startedAt: iso(manifest.started_at) } : {}),
111
+ ...(iso(file.updated_at) ?? iso(manifest.ended_at) ? { updatedAt: iso(file.updated_at) ?? iso(manifest.ended_at) } : {}),
112
+ })
113
+ const lost = lossCounter()
114
+ const turns: Turn[] = []
115
+
116
+ for (const m of file.messages as Json[]) {
117
+ if (!m || (m.role !== "user" && m.role !== "assistant")) continue
118
+ const meta = (m.metadata ?? {}) as Json
119
+ const at = typeof m.ts === "number" ? iso(m.ts) : undefined
120
+ const when = at ? { at } : {}
121
+ const blocks: Json[] = typeof m.content === "string" ? [{ type: "text", text: m.content }] : Array.isArray(m.content) ? m.content : []
122
+ if (meta.displayOnly === true) continue // shown in the panel, never sent to the model
123
+ if (meta.kind === "compaction_summary" || meta.kind === "compaction") {
124
+ const summary = typeof meta.summary === "string" && meta.summary ? meta.summary : blocks.map((b) => (b?.type === "text" ? b.text : "")).join("\n")
125
+ if (summary.trim()) turns.push({ kind: "compaction", ...when, summary })
126
+ continue
127
+ }
128
+ if (meta.kind === NOTE_KIND) {
129
+ const continued = blocks.map((b) => (b?.type === "text" ? String(b.text) : "")).join("\n").match(CONTINUED_NOTE)
130
+ if (continued?.[1] && !h.parent) h.parent = { id: continued[1], kind: "continued" }
131
+ continue
132
+ }
133
+ if (m.role === "user") {
134
+ if (meta.displayRole === "system" || meta.displayRole === "status") {
135
+ lost.add("runtime notices")
136
+ continue
137
+ }
138
+ const parts: string[] = []
139
+ const attachments: Attachment[] = []
140
+ for (const b of blocks) {
141
+ if (b?.type === "tool_result") {
142
+ let images = 0
143
+ const out = clip(resultText(b.content, () => images++))
144
+ if (images) lost.add("images in tool results", images)
145
+ turns.push({ kind: "tool_result", ...when, callId: String(b.tool_use_id), ok: b.is_error !== true, output: out.text, ...(out.truncated ? { truncated: true } : {}) })
146
+ } else if (b?.type === "text" && typeof b.text === "string") {
147
+ const t = displayText(b.text)
148
+ if (t) parts.push(t)
149
+ } else if (b?.type === "file" && typeof b.path === "string") attachments.push({ kind: "file", path: b.path })
150
+ else if (b?.type === "image" || b?.type === "media") lost.add("images")
151
+ else lost.add("unknown user content")
152
+ }
153
+ const text = parts.join("\n\n")
154
+ if (text || attachments.length) turns.push({ kind: "user", ...when, text, ...(attachments.length ? { attachments } : {}) })
155
+ continue
156
+ }
157
+ const model = typeof m.modelInfo?.id === "string" && m.modelInfo.id ? m.modelInfo.id : undefined
158
+ noteModel(h, model)
159
+ const usage = usageOf(m.metrics)
160
+ let usageGiven = false
161
+ for (const b of blocks) {
162
+ if (b?.type === "text" && typeof b.text === "string") {
163
+ if (!b.text.trim()) continue
164
+ turns.push({ kind: "assistant", ...when, text: b.text, ...(model ? { model } : {}), ...(usage && !usageGiven ? { usage } : {}) })
165
+ usageGiven = true
166
+ } else if (b?.type === "thinking") {
167
+ if (typeof b.thinking === "string" && b.thinking.trim()) turns.push({ kind: "reasoning", ...when, text: b.thinking })
168
+ else if (b.signature) lost.add("signed reasoning")
169
+ } else if (b?.type === "redacted_thinking") lost.add("redacted reasoning")
170
+ else if (b?.type === "tool_use" && typeof b.id === "string") {
171
+ const name = String(b.name ?? "tool")
172
+ turns.push({ kind: "tool_call", ...when, id: b.id, name, toolKind: clineToolKind(name), input: b.input ?? {} })
173
+ } else if (b?.type === "image" || b?.type === "media") lost.add("generated media")
174
+ else lost.add("unknown assistant content")
175
+ }
176
+ }
177
+ lost.flush(h)
178
+ return { header: h, turns }
179
+ }
@@ -0,0 +1,92 @@
1
+ import { join } from "node:path"
2
+ import type { ToolKind } from "../../session/types.ts"
3
+ import { toolKindOf } from "../../session/util.ts"
4
+
5
+ // What the Cline 4.x SDK runtime (the `cline` CLI 3.x, Cline Desktop, the 4.x extension) stores, as far as
6
+ // this adapter relies on it. Checked against cline/cline at 94eecbb (2026-11-06, CLI 3.0.74):
7
+ // sdk/packages/shared/src/db/sqlite-db.ts (the sessions table), shared/src/storage/paths.ts (the folders),
8
+ // core/src/session/services/persistence-service.ts (createRootSessionWithArtifacts, persistSessionMessages),
9
+ // core/src/services/session-data.ts (the messages file), core/src/session/models/session-manifest.ts,
10
+ // core/src/runtime/host/history.ts (`cline history`), core/src/services/session-import/ (Cline's own
11
+ // Claude Code / Codex / OpenCode importer, whose output this writer matches), shared/src/llms/messages.ts
12
+ // and apps/cli/src/tui/utils/hydrate-messages.ts (what the CLI shows on resume). Catalogue entry 38.
13
+ //
14
+ // ~/.cline/data/db/sessions.db SQLite (WAL): one `sessions` row per session, with
15
+ // messages_path pointing at the messages file
16
+ // ~/.cline/data/sessions/<id>/<id>.json the manifest (version 1), what the row says
17
+ // ~/.cline/data/sessions/<id>/<id>.messages.json {version 1, updated_at, agent, sessionId, origin,
18
+ // messages: MessageWithMetadata[]}
19
+ //
20
+ // Messages are Anthropic-shaped: an assistant message holds text, thinking and tool_use blocks, and the
21
+ // user message after it holds the tool_result blocks. A user message whose metadata.displayRole is "system"
22
+ // goes to the model but is not shown (compaction summaries use it). Ids are `<ms>_<5 nanoid chars>`.
23
+
24
+ export const TOOL = "cline-sdk"
25
+ export const LABEL = "Cline (CLI, Desktop)"
26
+
27
+ /** Cline CLI releases whose store was checked in source; the writer was run against 3.0.74. */
28
+ export const TESTED_VERSIONS = ["3.0.74"]
29
+
30
+ export const dataDir = (home: string) => join(home, ".cline", "data")
31
+ export const sessionsDb = (home: string) => join(dataDir(home), "db", "sessions.db")
32
+ export const sessionsDir = (home: string) => join(dataDir(home), "sessions")
33
+ export const sessionDir = (home: string, id: string) => join(sessionsDir(home), id)
34
+ export const manifestPath = (home: string, id: string) => join(sessionDir(home, id), `${id}.json`)
35
+ export const messagesPath = (home: string, id: string) => join(sessionDir(home, id), `${id}.messages.json`)
36
+
37
+ /** The sessions table as Cline 3.0.74 creates it (SQLITE_STATEMENTS in sqlite-db.ts). */
38
+ export const SESSIONS_TABLE = `CREATE TABLE IF NOT EXISTS sessions (
39
+ session_id TEXT PRIMARY KEY,
40
+ source TEXT NOT NULL,
41
+ pid INTEGER NOT NULL,
42
+ started_at TEXT NOT NULL,
43
+ ended_at TEXT,
44
+ exit_code INTEGER,
45
+ status TEXT NOT NULL,
46
+ status_lock INTEGER NOT NULL DEFAULT 0,
47
+ interactive INTEGER NOT NULL,
48
+ provider TEXT NOT NULL,
49
+ model TEXT NOT NULL,
50
+ cwd TEXT NOT NULL,
51
+ workspace_root TEXT NOT NULL,
52
+ team_name TEXT,
53
+ enable_tools INTEGER NOT NULL,
54
+ enable_spawn INTEGER NOT NULL,
55
+ enable_teams INTEGER NOT NULL,
56
+ parent_session_id TEXT,
57
+ parent_agent_id TEXT,
58
+ agent_id TEXT,
59
+ conversation_id TEXT,
60
+ is_subagent INTEGER NOT NULL DEFAULT 0,
61
+ prompt TEXT,
62
+ metadata_json TEXT,
63
+ transcript_path TEXT NOT NULL DEFAULT '',
64
+ hook_path TEXT NOT NULL,
65
+ messages_path TEXT,
66
+ updated_at TEXT NOT NULL
67
+ );`
68
+
69
+ /** Process names of a running Cline that holds the store open: the CLI and its hub daemon, Cline Desktop. */
70
+ export const PROCESSES = ["cline", "Cline", "Cline Desktop"]
71
+
72
+ /** The metadata kind of the note saying where a session came from (a system-role message, kept off screen). */
73
+ export const NOTE_KIND = "brain_continued_session"
74
+
75
+ /** Cline's own tools (sdk/packages/core/src/extensions/tools), where the name alone does not say what they do. */
76
+ const KINDS: Record<string, ToolKind> = {
77
+ read_file: "read",
78
+ read_files: "read",
79
+ run_commands: "shell",
80
+ editor: "edit",
81
+ apply_patch: "edit",
82
+ search_codebase: "search",
83
+ fetch_web_content: "web",
84
+ ask_question: "ask",
85
+ skills: "other",
86
+ spawn_agent: "task",
87
+ }
88
+ export const clineToolKind = (name: string): ToolKind => KINDS[name] ?? (name.startsWith("team_") ? "task" : toolKindOf(name))
89
+
90
+ export const continuedNote = (label: string, id: string, when: string) =>
91
+ `This session continues ${label} session ${id}, carried over by brain session on ${when}. The messages that follow are its history; reasoning and anything listed as lost did not carry over. Check files before relying on details, since they may have changed.`
92
+ export const CONTINUED_NOTE = /^This session continues .+? session (\S+), carried over by brain session/