@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,432 @@
1
+ import { closeSync, openSync, readSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { appSupport, exists, gitInfo, isDir, ls, mtime, readJson } from "../../fsutil.ts"
4
+ import type { Attachment, Session, Turn } from "../../session/types.ts"
5
+ import { asText, clip, header, iso, lossCounter, noteModel, toolKindOf } from "../../session/util.ts"
6
+ import type { Reader, SessionRef } from "../types.ts"
7
+ import { CONTINUED_NOTE, HOSTS, SHOWN, SUMMARY_LEAD, extensionVersion } from "./records.ts"
8
+ import { XML_TOOLS, resultHeader, skipNotice, splitAssistantText } from "./xml.ts"
9
+
10
+ // Cline's classic task store, and Roo Code's, which forked from it (catalogue entries 37 and 39). Each
11
+ // VS Code-family app keeps its own copy under <app>/User/globalStorage/<extension id>/:
12
+ // tasks/<id>/api_conversation_history.json what the model saw: Anthropic-style {role, content} messages
13
+ // tasks/<id>/ui_messages.json what the panel showed, with timestamps
14
+ // tasks/<id>/task_metadata.json models used, files in context
15
+ // state/taskHistory.json (Cline), tasks/_index.json and tasks/<id>/history_item.json (Roo): the task list
16
+ // Tool calls are either native tool_use/tool_result blocks or XML in the text (see xml.ts).
17
+
18
+ interface Flavor {
19
+ tool: string
20
+ label: string
21
+ /** The extension's id, which names its globalStorage folder. */
22
+ extension: string
23
+ }
24
+
25
+ /** One task's line in the extension's own task list. */
26
+ interface IndexEntry {
27
+ task?: string
28
+ ts?: number
29
+ cwd?: string
30
+ parentId?: string
31
+ native?: boolean
32
+ }
33
+
34
+ type Block = Record<string, any>
35
+ /** Roo also marks its own context management: a condensed summary (isSummary) and a sliding-window note (isTruncationMarker). */
36
+ type Msg = { role?: string; content?: unknown; ts?: number; isSummary?: boolean; isTruncationMarker?: boolean }
37
+
38
+ const API_FILE = "api_conversation_history.json"
39
+ const UI_FILE = "ui_messages.json"
40
+
41
+ export function taskReader(f: Flavor): Reader {
42
+ return {
43
+ tool: f.tool,
44
+ label: f.label,
45
+ async list(home) {
46
+ const refs: SessionRef[] = []
47
+ for (const host of HOSTS) {
48
+ const root = join(appSupport(home), host.app, "User", "globalStorage", f.extension)
49
+ const tasks = join(root, "tasks")
50
+ if (!isDir(tasks)) continue
51
+ const index = loadIndex(root)
52
+ for (const id of ls(tasks)) {
53
+ const dir = join(tasks, id)
54
+ // The index can list tasks whose folder is gone, and folders can lack the index entry: the folder decides.
55
+ if (!exists(join(dir, API_FILE))) continue
56
+ let e = index.get(id) ?? historyItem(dir)
57
+ // Older index entries lack the folder, and some tasks have no entry: the history's first message says both.
58
+ if (!e?.cwd || !e.task) e = { ...peek(join(dir, API_FILE)), ...e }
59
+ const times = [iso(e?.ts), mtime(join(dir, API_FILE)), mtime(join(dir, UI_FILE))].filter((t): t is string => !!t)
60
+ refs.push({
61
+ tool: f.tool,
62
+ sourceId: id,
63
+ path: dir,
64
+ ...(e?.task ? { title: titleOf(e.task) } : {}),
65
+ ...(e?.cwd ? { cwd: e.cwd } : {}),
66
+ ...(times.length ? { updatedAt: times.sort().at(-1)! } : {}),
67
+ extra: { host: host.app },
68
+ })
69
+ }
70
+ }
71
+ return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
72
+ },
73
+ async read(ref, home) {
74
+ return readTask(f, ref, home)
75
+ },
76
+ }
77
+ }
78
+
79
+ const titleOf = (task: string) => {
80
+ const line = task.replace(/\s+/g, " ").trim()
81
+ return line.length > 120 ? `${line.slice(0, 119)}…` : line
82
+ }
83
+
84
+ /** The task list: Cline's state/taskHistory.json or Roo's tasks/_index.json. */
85
+ function loadIndex(root: string): Map<string, IndexEntry> {
86
+ const out = new Map<string, IndexEntry>()
87
+ const add = (items: unknown) => {
88
+ if (!Array.isArray(items)) return
89
+ for (const it of items as Array<Record<string, any>>) if (typeof it?.id === "string") out.set(it.id, entryOf(it))
90
+ }
91
+ add(readJson(join(root, "state", "taskHistory.json")))
92
+ add(readJson<{ entries?: unknown }>(join(root, "tasks", "_index.json"))?.entries)
93
+ return out
94
+ }
95
+
96
+ /**
97
+ * The task text and folder from the start of a task's history, for tasks the index does not list (older Roo
98
+ * kept its list in VS Code's own database): the first 64 KB hold the first message, without parsing the rest.
99
+ */
100
+ function peek(apiFile: string): IndexEntry | undefined {
101
+ let head = ""
102
+ try {
103
+ const fd = openSync(apiFile, "r")
104
+ try {
105
+ const buf = Buffer.alloc(64 * 1024)
106
+ head = buf.toString("utf8", 0, readSync(fd, buf, 0, buf.length, 0))
107
+ } finally {
108
+ closeSync(fd)
109
+ }
110
+ } catch {
111
+ return undefined
112
+ }
113
+ const out: IndexEntry = {}
114
+ const raw = head.match(/"text"\s*:\s*"((?:[^"\\]|\\.)*)"/)?.[1]
115
+ if (raw) {
116
+ try {
117
+ const task = unwrap(JSON.parse(`"${raw}"`) as string)
118
+ if (task) out.task = task
119
+ } catch {}
120
+ }
121
+ const cwd = head.match(/# Current (?:Working|Workspace) Directory \((.+?)\) Files/)?.[1]
122
+ if (cwd) out.cwd = cwd
123
+ return out.task || out.cwd ? out : undefined
124
+ }
125
+
126
+ /** Roo's per-task copy of its index entry (newer versions). */
127
+ function historyItem(dir: string): IndexEntry | undefined {
128
+ const it = readJson<Record<string, any>>(join(dir, "history_item.json"))
129
+ return it && typeof it === "object" ? entryOf(it) : undefined
130
+ }
131
+
132
+ function entryOf(it: Record<string, any>): IndexEntry {
133
+ const cwd = it.cwdOnTaskInitialization ?? it.workspace ?? it.shadowGitConfigWorkTree
134
+ return {
135
+ ...(typeof it.task === "string" ? { task: it.task } : {}),
136
+ ...(typeof it.ts === "number" ? { ts: it.ts } : {}),
137
+ ...(typeof cwd === "string" && cwd ? { cwd } : {}),
138
+ ...(typeof it.parentTaskId === "string" ? { parentId: it.parentTaskId } : {}),
139
+ ...(it.toolProtocol === "native" ? { native: true } : {}),
140
+ }
141
+ }
142
+
143
+ function readTask(f: Flavor, ref: SessionRef, home: string): Session {
144
+ const dir = ref.path
145
+ const api = readJson<Msg[]>(join(dir, API_FILE))
146
+ if (!Array.isArray(api)) throw new Error(`${API_FILE} is missing or not JSON in ${dir}`)
147
+ const ui = readJson<Array<Record<string, any>>>(join(dir, UI_FILE))
148
+ const meta = readJson<Record<string, any>>(join(dir, "task_metadata.json")) ?? {}
149
+ const root = join(dir, "..", "..")
150
+ const entry = loadIndex(root).get(ref.sourceId) ?? historyItem(dir)
151
+ const app = (ref.extra?.host as string | undefined) ?? HOSTS.find((h) => dir.includes(`/${h.app}/User/globalStorage/`))?.app
152
+
153
+ // When each message was sent. Roo stamps its messages; Cline does not, but every panel message records how
154
+ // long the conversation was when it appeared (conversationHistoryIndex), which dates each message closely.
155
+ const uiTimes = new Map<number, number>()
156
+ const uiTs: number[] = []
157
+ for (const u of Array.isArray(ui) ? ui : []) {
158
+ if (typeof u?.ts !== "number") continue
159
+ uiTs.push(u.ts)
160
+ const k = u.conversationHistoryIndex
161
+ if (typeof k === "number" && !uiTimes.has(k)) uiTimes.set(k, u.ts)
162
+ }
163
+ const at = (i: number) => iso(api[i]?.ts) ?? iso(uiTimes.get(i))
164
+
165
+ const usage = (Array.isArray(meta.model_usage) ? meta.model_usage : [])
166
+ .filter((u: any) => typeof u?.model_id === "string")
167
+ .sort((a: any, b: any) => (a.ts ?? 0) - (b.ts ?? 0)) as Array<{ ts?: number; model_id: string }>
168
+ const modelAt = (when: string | undefined): string | undefined => {
169
+ if (!usage.length) return undefined
170
+ if (!when) return usage.at(-1)!.model_id
171
+ const t = Date.parse(when)
172
+ return [...usage].reverse().find((u) => (u.ts ?? 0) <= t)?.model_id ?? usage[0]!.model_id
173
+ }
174
+
175
+ const lost = lossCounter()
176
+ let cwd = ref.cwd ?? entry?.cwd
177
+ /** The session this task continues, when the writer made it. */
178
+ let continued: string | undefined
179
+ const native = !!entry?.native || api.some((m) => Array.isArray(m.content) && (m.content as Block[]).some((b) => b?.type === "tool_use"))
180
+ const turns: Turn[] = []
181
+ const models: string[] = []
182
+ /** XML calls from the last assistant message, waiting for their results. */
183
+ let waiting: Array<{ id: string; name: string; done: boolean }> = []
184
+ // Native call ids are the provider's, and some repeat them across a task (Gemini numbers calls per message:
185
+ // "read_file-0" again and again). Turns need them unique, so a repeat gets a suffix, and a result answers the
186
+ // latest call issued under its id, since results come in the message right after their call.
187
+ const usedIds = new Set<string>()
188
+ const latestId = new Map<string, string>()
189
+ const callIdFor = (raw: string) => {
190
+ let id = raw
191
+ for (let k = 2; usedIds.has(id); k++) id = `${raw}#${k}`
192
+ usedIds.add(id)
193
+ latestId.set(raw, id)
194
+ return id
195
+ }
196
+
197
+ api.forEach((m, i) => {
198
+ const blocks: Block[] = typeof m.content === "string" ? [{ type: "text", text: m.content }] : Array.isArray(m.content) ? (m.content as Block[]) : []
199
+ const when = at(i)
200
+ // Roo condenses the context without deleting anything: the older messages stay (marked condenseParent) and a
201
+ // summary is added, as an assistant message (3.38) or a user one (3.54). It is the tool's own compaction,
202
+ // not something the model or the person said. A sliding-window truncation leaves a marker message the same way.
203
+ if (m.isSummary) {
204
+ // A summary the writer made also repeats the continuation note and carries what the person said next.
205
+ const texts = blocks.filter((b) => b?.type === "text").map((b) => String(b.text ?? ""))
206
+ const said = texts.filter((t) => /^\s*<user_message>/.test(t)).map(unwrap).filter(Boolean)
207
+ const summary = texts
208
+ .filter((t) => !CONTINUED_NOTE.test(t.trimStart()) && !/^\s*<user_message>/.test(t))
209
+ .join("\n\n")
210
+ .replace(/^\s*## Conversation Summary\n/, "")
211
+ .trim()
212
+ if (texts.some((t) => CONTINUED_NOTE.test(t.trimStart()))) continued ??= texts.find((t) => CONTINUED_NOTE.test(t.trimStart()))!.trimStart().match(CONTINUED_NOTE)![1]
213
+ if (summary) turns.push({ kind: "compaction", ...(when ? { at: when } : {}), summary })
214
+ if (said.length) turns.push({ kind: "user", ...(when ? { at: when } : {}), text: said.join("\n\n") })
215
+ return
216
+ }
217
+ if (m.isTruncationMarker) {
218
+ lost.add("context truncation markers")
219
+ return
220
+ }
221
+ if (m.role === "assistant") {
222
+ waiting = []
223
+ const model = modelAt(when)
224
+ if (model && !models.includes(model)) models.push(model)
225
+ const next = api[i + 1]
226
+ const answered = new Set<string>()
227
+ if (next?.role === "user" && Array.isArray(next.content))
228
+ for (const b of next.content as Block[]) if (b?.type === "text") {
229
+ const h = resultHeader(String(b.text ?? ""))
230
+ if (h) answered.add(h.name)
231
+ const skipped = skipNotice(String(b.text ?? ""))
232
+ if (skipped) answered.add(skipped)
233
+ }
234
+ // XML is read only in tasks on the XML protocol: in a native task, tags in the text were never run.
235
+ const isTool = native ? () => false : (name: string) => XML_TOOLS.has(name) || answered.has(name)
236
+ let n = 0
237
+ for (const b of blocks) {
238
+ switch (b?.type) {
239
+ case "text":
240
+ for (const p of splitAssistantText(String(b.text ?? ""), isTool)) {
241
+ if (p.kind === "text") turns.push({ kind: "assistant", ...(when ? { at: when } : {}), text: p.text, ...(model ? { model } : {}) })
242
+ else if (p.kind === "thinking") turns.push({ kind: "reasoning", ...(when ? { at: when } : {}), text: p.text })
243
+ else {
244
+ const id = `xml-${i}-${n++}`
245
+ pushCall(turns, { id, name: p.name, input: p.input, at: when, model })
246
+ waiting.push({ id, name: p.name, done: false })
247
+ }
248
+ }
249
+ break
250
+ case "thinking":
251
+ if (typeof b.thinking === "string" && b.thinking.trim()) {
252
+ turns.push({ kind: "reasoning", ...(when ? { at: when } : {}), text: b.thinking })
253
+ if (b.signature) lost.add("reasoning signatures")
254
+ } else lost.add("signed reasoning")
255
+ break
256
+ case "redacted_thinking":
257
+ lost.add("encrypted reasoning")
258
+ break
259
+ case "thoughtSignature":
260
+ lost.add("Gemini thought signatures")
261
+ break
262
+ case "tool_use":
263
+ pushCall(turns, { id: callIdFor(String(b.id ?? `tool-${i}-${n++}`)), name: String(b.name ?? "unknown"), input: b.input ?? {}, at: when, model })
264
+ break
265
+ case "image":
266
+ lost.add("image data")
267
+ break
268
+ default:
269
+ lost.add(`unknown block types: ${String(b?.type)}`)
270
+ }
271
+ }
272
+ return
273
+ }
274
+ if (m.role !== "user") {
275
+ lost.add(`unknown message roles: ${String(m.role)}`)
276
+ return
277
+ }
278
+
279
+ // A user message: tool results first (they answer the message before), then what the person wrote.
280
+ const texts: string[] = []
281
+ const attachments: Attachment[] = []
282
+ let result: { id: string; body: string[] } | undefined
283
+ const flush = () => {
284
+ if (!result) return
285
+ const { output, feedback } = splitFeedback(result.body.join("\n\n"))
286
+ const c = clip(output || "(no output)")
287
+ turns.push({ kind: "tool_result", ...(when ? { at: when } : {}), callId: result.id, ok: !FAILED.test(output), output: c.text, ...(c.truncated ? { truncated: true } : {}) })
288
+ if (feedback) texts.push(feedback)
289
+ result = undefined
290
+ }
291
+ for (const b of blocks) {
292
+ if (b?.type === "text") {
293
+ const t = String(b.text ?? "")
294
+ const lead = t.trimStart()
295
+ if (lead.startsWith("<environment_details>")) {
296
+ flush()
297
+ cwd ??= t.match(/# Current (?:Working|Workspace) Directory \((.+?)\) Files/)?.[1]
298
+ lost.add("environment details blocks")
299
+ continue
300
+ }
301
+ // A call the extension skipped is answered by a notice of its own, not by the open result's output.
302
+ const skipped = skipNotice(t)
303
+ if (skipped) {
304
+ flush()
305
+ const call = waiting.find((w) => !w.done && w.name === skipped) ?? waiting.find((w) => !w.done)
306
+ if (call) {
307
+ call.done = true
308
+ turns.push({ kind: "tool_result", ...(when ? { at: when } : {}), callId: call.id, ok: false, output: t.trim() })
309
+ } else lost.add("harness notices")
310
+ continue
311
+ }
312
+ const h = resultHeader(t)
313
+ if (h) {
314
+ flush()
315
+ const call = waiting.find((w) => !w.done && w.name === h.name) ?? waiting.find((w) => !w.done)
316
+ if (call) {
317
+ call.done = true
318
+ result = { id: call.id, body: h.rest.trim() ? [h.rest] : [] }
319
+ } else texts.push(t.trim()) // a result for no call we know: keep what the model saw, as text
320
+ continue
321
+ }
322
+ // Resuming a task adds the extension's notice, and Cline 3.5x puts the person's words in a block of their
323
+ // own after a lead-in ("New instructions for task continuation: <user_message>…"): only the words are theirs.
324
+ if (lead.startsWith("[TASK RESUMPTION]") || RESUME_LEAD.test(lead)) {
325
+ flush()
326
+ const said = t.match(/<user_message>([\s\S]*?)<\/user_message>/)?.[1]?.trim()
327
+ if (said) texts.push(said)
328
+ else lost.add("task resumption notices")
329
+ continue
330
+ }
331
+ if (result) {
332
+ result.body.push(t)
333
+ continue
334
+ }
335
+ // The writer's note on where the task came from, and its marker for a summary that replaced earlier turns.
336
+ const note = lead.match(CONTINUED_NOTE)
337
+ if (note) {
338
+ continued ??= note[1]
339
+ continue
340
+ }
341
+ if (lead.startsWith(SUMMARY_LEAD)) {
342
+ flush()
343
+ // What the person said before the summary in the same message comes first.
344
+ if (texts.length) turns.push({ kind: "user", ...(when ? { at: when } : {}), text: texts.splice(0).join("\n\n") })
345
+ const summary = t.slice(t.indexOf("]") + 1).trim()
346
+ if (summary) turns.push({ kind: "compaction", ...(when ? { at: when } : {}), summary })
347
+ continue
348
+ }
349
+ if (lead.startsWith("[ERROR]")) {
350
+ lost.add("harness notices")
351
+ continue
352
+ }
353
+ const own = unwrap(t)
354
+ if (own) texts.push(own)
355
+ } else if (b?.type === "tool_result") {
356
+ flush()
357
+ const c = clip(resultText(b.content, lost) || "(no output)")
358
+ const raw = String(b.tool_use_id)
359
+ turns.push({ kind: "tool_result", ...(when ? { at: when } : {}), callId: latestId.get(raw) ?? raw, ok: !b.is_error, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
360
+ } else if (b?.type === "image") {
361
+ if (result) lost.add("images in tool results")
362
+ else {
363
+ const mime = b.source?.media_type
364
+ attachments.push({ kind: "image", ...(typeof mime === "string" ? { mime } : {}) })
365
+ lost.add("image data")
366
+ }
367
+ } else lost.add(`unknown block types: ${String(b?.type)}`)
368
+ }
369
+ flush()
370
+ if (texts.length || attachments.length)
371
+ turns.push({ kind: "user", ...(when ? { at: when } : {}), text: texts.join("\n\n"), ...(attachments.length ? { attachments } : {}) })
372
+ })
373
+
374
+ const firstAt = /^\d{12,14}$/.test(ref.sourceId) ? iso(Number(ref.sourceId)) : undefined
375
+ const startedAt = firstAt ?? turns.find((t) => t.at)?.at ?? iso(uiTs[0])
376
+ const updatedAt = iso(uiTs.reduce((a, b) => Math.max(a, b), 0)) ?? iso(entry?.ts) ?? mtime(join(dir, API_FILE))
377
+ const toolVersion = extensionVersion(home, app, f.extension)
378
+ const h = header(f.tool, ref.sourceId, {
379
+ ...(toolVersion ? { toolVersion } : {}),
380
+ sourcePath: dir,
381
+ ...(entry?.task ? { title: titleOf(entry.task) } : ref.title ? { title: ref.title } : {}),
382
+ project: { ...(cwd ? { cwd } : {}), ...gitInfo(cwd) },
383
+ ...(startedAt ? { startedAt } : {}),
384
+ ...(updatedAt ? { updatedAt } : {}),
385
+ ...(entry?.parentId
386
+ ? { parent: { id: `${f.tool}:${entry.parentId}`, kind: "subagent" as const } }
387
+ : continued
388
+ ? { parent: { id: continued, kind: "continued" as const } }
389
+ : {}),
390
+ })
391
+ for (const m of models) noteModel(h, m)
392
+ lost.flush(h)
393
+ return { header: h, turns }
394
+ }
395
+
396
+ /** Cline's lead-ins to what the person typed when resuming, and its note when they typed nothing (plan mode). */
397
+ const RESUME_LEAD = /^(?:New instructions for task continuation|New message to respond to with plan_mode_respond tool|\(The user did not provide a new message)/
398
+
399
+ /** Results that say the call did not go through. */
400
+ const FAILED = /The user denied this operation|The tool execution failed|^\s*<error>|^\s*Error[: ]/i
401
+
402
+ function pushCall(turns: Turn[], c: { id: string; name: string; input: unknown; at: string | undefined; model: string | undefined }) {
403
+ const shown = SHOWN[c.name]
404
+ const text = shown ? (c.input as Record<string, unknown> | undefined)?.[shown] : undefined
405
+ if (typeof text === "string" && text.trim()) turns.push({ kind: "assistant", ...(c.at ? { at: c.at } : {}), text: text.trim(), ...(c.model ? { model: c.model } : {}) })
406
+ turns.push({ kind: "tool_call", ...(c.at ? { at: c.at } : {}), id: c.id, name: c.name, toolKind: toolKindOf(c.name), input: c.input })
407
+ }
408
+
409
+ /** What the person typed, out of the tags Cline wraps it in. */
410
+ function unwrap(t: string): string {
411
+ return t.replace(/<(task|feedback|answer|user_message)>\s*([\s\S]*?)\s*<\/\1>/g, "$2").trim()
412
+ }
413
+
414
+ /** Feedback the person gave in reply to a tool (a denied edit, a finished task) is their message, not the tool's output. */
415
+ function splitFeedback(body: string): { output: string; feedback?: string } {
416
+ const m = body.match(/<feedback>\s*([\s\S]*?)\s*<\/feedback>/)
417
+ if (!m) return { output: body.trim() }
418
+ return { output: body.replace(m[0], "").trim(), feedback: m[1]! }
419
+ }
420
+
421
+ /** A native tool_result's content as text; images in it are counted, not kept. */
422
+ function resultText(content: unknown, lost: ReturnType<typeof lossCounter>): string {
423
+ if (typeof content === "string") return content
424
+ if (!Array.isArray(content)) return asText(content)
425
+ const out: string[] = []
426
+ for (const c of content as Block[]) {
427
+ if (c?.type === "text") out.push(String(c.text ?? ""))
428
+ else if (c?.type === "image") lost.add("images in tool results")
429
+ else out.push(asText(c))
430
+ }
431
+ return out.join("\n")
432
+ }
@@ -0,0 +1,151 @@
1
+ import { join } from "node:path"
2
+ import { appSupport, ls, readJson } from "../../fsutil.ts"
3
+ import type { ToolKind } from "../../session/types.ts"
4
+
5
+ // Facts about the Cline-lineage task store shared by the reader and the writers: which editors host the
6
+ // extensions, which versions the writers were checked against, and the tool names each extension's model
7
+ // knows. Checked against the sources at the installed versions (cline v3.56.2 and v4.1.26, Roo-Code v3.38.1
8
+ // and v3.54.0) and the task folders on the author's Mac (structure only).
9
+
10
+ /** VS Code-family apps that host the extensions: their data folder, extensions folder and process name. */
11
+ export interface Host {
12
+ app: string
13
+ extensions: string
14
+ process: string
15
+ /** The command that opens a folder in the app. */
16
+ cli: string
17
+ }
18
+
19
+ export const HOSTS: Host[] = [
20
+ { app: "Code", extensions: ".vscode/extensions", process: "Code", cli: "code" },
21
+ { app: "Code - Insiders", extensions: ".vscode-insiders/extensions", process: "Code - Insiders", cli: "code-insiders" },
22
+ { app: "Cursor", extensions: ".cursor/extensions", process: "Cursor", cli: "cursor" },
23
+ { app: "Windsurf", extensions: ".windsurf/extensions", process: "Windsurf", cli: "windsurf" },
24
+ { app: "VSCodium", extensions: ".vscode-oss/extensions", process: "VSCodium", cli: "codium" },
25
+ ]
26
+
27
+ export const hostByApp = (app: string) => HOSTS.find((h) => h.app === app)
28
+
29
+ /** The extension's folder in a host's global storage. */
30
+ export const storageDir = (home: string, app: string, extension: string) => join(appSupport(home), app, "User", "globalStorage", extension)
31
+
32
+ /** The host's own global state database, where VS Code keeps every extension's globalState. */
33
+ export const stateDb = (home: string, app: string) => join(appSupport(home), app, "User", "globalStorage", "state.vscdb")
34
+
35
+ const num = (v: string) => v.split(".").map(Number)
36
+
37
+ /** Semantic-version order: negative when a is older than b. */
38
+ export function compareVersions(a: string, b: string): number {
39
+ const [x, y] = [num(a), num(b)]
40
+ for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) - (y[i] ?? 0)
41
+ return 0
42
+ }
43
+
44
+ /**
45
+ * The extension's version installed in an app. The host's extensions.json names the active copy (an
46
+ * update can leave the old folder behind until the editor cleans up); without it the newest folder wins.
47
+ */
48
+ export function extensionVersion(home: string, app: string | undefined, extension: string): string | undefined {
49
+ const host = HOSTS.find((h) => h.app === app)
50
+ if (!host) return undefined
51
+ const dir = join(home, host.extensions)
52
+ const manifest = readJson<Array<{ identifier?: { id?: string }; version?: string }>>(join(dir, "extensions.json"))
53
+ const listed = Array.isArray(manifest)
54
+ ? manifest.find((e) => e?.identifier?.id?.toLowerCase() === extension.toLowerCase() && typeof e.version === "string")?.version
55
+ : undefined
56
+ if (listed) return listed
57
+ const versions = ls(dir)
58
+ .map((d) => d.match(new RegExp(`^${extension.replace(/\./g, "\\.")}-(\\d+\\.\\d+\\.\\d+)`, "i"))?.[1])
59
+ .filter((v): v is string => !!v)
60
+ return versions.sort(compareVersions).at(-1)
61
+ }
62
+
63
+ /**
64
+ * The note that opens a task the writer creates, as its own text block in the first API message: the model
65
+ * reads it, the task panel does not show it (the panel shows ui_messages.json, not the API history), and the
66
+ * reader recognises it.
67
+ */
68
+ export const continuedNote = (label: string, id: string, when: string) =>
69
+ `[This task continues ${label} session ${id}, carried over by brain session on ${when}. The turns below are its history; reasoning and anything listed as lost did not carry over. Tool names may differ from yours. Check files before relying on details, since they may have changed.]`
70
+ export const CONTINUED_NOTE = /^\[This task continues .+? session (\S+), carried over by brain session/
71
+
72
+ /** Tools whose input is the text the person saw (the final answer, a plan): that text is also an assistant turn. */
73
+ export const SHOWN: Record<string, string> = { attempt_completion: "result", plan_mode_respond: "response" }
74
+
75
+ /** How the writer marks a compaction summary in Cline's history, where the summary is a plain user text block. */
76
+ export const SUMMARY_LEAD = "[Summary of the earlier conversation"
77
+ export const summaryText = (label: string, summary: string) => `${SUMMARY_LEAD} in ${label}, which replaced the turns before it]\n\n${summary}`
78
+
79
+ type Input = Record<string, unknown>
80
+ const str = (v: unknown) => (typeof v === "string" && v ? v : undefined)
81
+ const pathOf = (i: Input) => str(i.file_path) ?? str(i.path) ?? str(i.filePath) ?? str(i.target_file) ?? str(i.absolute_path) ?? str(i.file)
82
+ const commandOf = (i: Input) => (Array.isArray(i.command) ? i.command.map(String).join(" ") : str(i.command) ?? str(i.cmd))
83
+
84
+ /** A native tool call in the target's own vocabulary, or undefined to keep the call as it was. */
85
+ export type Mapping = (input: Input, name: string) => { name: string; input: Input } | undefined
86
+
87
+ /**
88
+ * Portable tool kinds to the native tools each extension defines (Cline src/core/prompts/system-prompt/tools,
89
+ * Roo src/core/prompts/tools/native-tools). Only shapes that translate cleanly are mapped; anything else keeps
90
+ * its own name and input, which the model still reads as a past call.
91
+ */
92
+ const COMMON: Partial<Record<ToolKind, Mapping>> = {
93
+ shell: (i) => {
94
+ const command = commandOf(i)
95
+ return command ? { name: "execute_command", input: { command } } : undefined
96
+ },
97
+ read: (i) => {
98
+ const path = pathOf(i)
99
+ return path ? { name: "read_file", input: { path } } : undefined
100
+ },
101
+ write: (i) => {
102
+ const path = pathOf(i)
103
+ const content = str(i.content) ?? str(i.file_text) ?? str(i.contents) ?? (typeof i.content === "string" ? "" : undefined)
104
+ return path && content !== undefined ? { name: "write_to_file", input: { path, content } } : undefined
105
+ },
106
+ search: (i, name) => {
107
+ // A glob search (Glob, glob_file_search) matches file names, which search_files cannot express.
108
+ const regex = /glob/i.test(name) ? undefined : str(i.pattern) ?? str(i.regex) ?? str(i.query)
109
+ if (!regex) return undefined
110
+ const pattern = str(i.glob) ?? str(i.include) ?? str(i.file_pattern)
111
+ return { name: "search_files", input: { path: str(i.path) ?? ".", regex, ...(pattern ? { file_pattern: pattern } : {}) } }
112
+ },
113
+ list: (i) => ({ name: "list_files", input: { path: pathOf(i) ?? str(i.dir_path) ?? str(i.target_directory) ?? ".", recursive: i.recursive === true } }),
114
+ ask: (i) => {
115
+ const question = str(i.question) ?? str((i.questions as Input[] | undefined)?.[0]?.question)
116
+ return question ? { name: "ask_followup_question", input: { question } } : undefined
117
+ },
118
+ }
119
+
120
+ const oldNew = (i: Input) => {
121
+ const path = pathOf(i)
122
+ const before = typeof i.old_string === "string" ? i.old_string : typeof i.oldString === "string" ? i.oldString : typeof i.old_str === "string" ? i.old_str : undefined
123
+ const after = typeof i.new_string === "string" ? i.new_string : typeof i.newString === "string" ? i.newString : typeof i.new_str === "string" ? i.new_str : undefined
124
+ return path && before !== undefined && after !== undefined ? { path, before, after } : undefined
125
+ }
126
+
127
+ export const CLINE_TOOLS: Partial<Record<ToolKind, Mapping>> = {
128
+ ...COMMON,
129
+ shell: (i) => {
130
+ const command = commandOf(i)
131
+ return command ? { name: "execute_command", input: { command, requires_approval: false } } : undefined
132
+ },
133
+ // Cline edits with SEARCH/REPLACE blocks in replace_in_file.
134
+ edit: (i) => {
135
+ const e = oldNew(i)
136
+ return e ? { name: "replace_in_file", input: { path: e.path, diff: `------- SEARCH\n${e.before}\n=======\n${e.after}\n+++++++ REPLACE` } } : undefined
137
+ },
138
+ web: (i) => {
139
+ const url = str(i.url)
140
+ return url ? { name: "web_fetch", input: { url } } : undefined
141
+ },
142
+ }
143
+
144
+ export const ROO_TOOLS: Partial<Record<ToolKind, Mapping>> = {
145
+ ...COMMON,
146
+ // search_replace takes Claude-style old and new strings in both 3.38 and 3.54.
147
+ edit: (i) => {
148
+ const e = oldNew(i)
149
+ return e ? { name: "search_replace", input: { file_path: e.path, old_string: e.before, new_string: e.after } } : undefined
150
+ },
151
+ }
@@ -0,0 +1,56 @@
1
+ import { Database } from "bun:sqlite"
2
+ import { copyFileSync, mkdirSync } from "node:fs"
3
+ import { basename, join } from "node:path"
4
+ import { exists } from "../../fsutil.ts"
5
+
6
+ // An extension's globalState in a VS Code-family app's own database (<app>/User/globalStorage/state.vscdb):
7
+ // one ItemTable row per extension, keyed by its id as the publisher wrote it ("RooVeterinaryInc.roo-cline"),
8
+ // holding the whole state as JSON text. VS Code reads the table once at startup and writes its in-memory copy
9
+ // back, so this is only written while the app is closed, after a backup.
10
+
11
+ /** The row's key and JSON value, or undefined when the extension has never stored state there. */
12
+ export function readState(db: Database, extension: string): { key: string; value: Record<string, unknown> } | undefined {
13
+ const row = db.query("SELECT key, value FROM ItemTable WHERE lower(key) = lower(?)").get(extension) as { key: string; value: string | Uint8Array } | null
14
+ if (!row) return undefined
15
+ const text = typeof row.value === "string" ? row.value : new TextDecoder().decode(row.value)
16
+ const value = JSON.parse(text) as unknown
17
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${extension}'s state in ${db.filename} is not a JSON object`)
18
+ return { key: row.key, value: value as Record<string, unknown> }
19
+ }
20
+
21
+ /** Copies the database and its journal files into `dir`, returning the copies. */
22
+ export function backupDb(path: string, dir: string): string[] {
23
+ mkdirSync(dir, { recursive: true })
24
+ const out: string[] = []
25
+ for (const side of ["", "-journal", "-wal", "-shm"]) {
26
+ if (!exists(path + side)) continue
27
+ const copy = join(dir, basename(path) + side)
28
+ copyFileSync(path + side, copy)
29
+ out.push(copy)
30
+ }
31
+ return out
32
+ }
33
+
34
+ /**
35
+ * Applies `change` to an extension's state in one transaction. The key keeps the case the row already has;
36
+ * a new row takes `extension` as written.
37
+ */
38
+ export function updateState(path: string, extension: string, change: (state: Record<string, unknown>) => void) {
39
+ // Never creates the file: an editor that has not made its database yet has no state to add to.
40
+ const db = new Database(path, { readwrite: true, create: false })
41
+ try {
42
+ db.exec("BEGIN IMMEDIATE")
43
+ try {
44
+ const cur = readState(db, extension)
45
+ const state = cur?.value ?? {}
46
+ change(state)
47
+ db.query("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)").run(cur?.key ?? extension, JSON.stringify(state))
48
+ db.exec("COMMIT")
49
+ } catch (e) {
50
+ db.exec("ROLLBACK")
51
+ throw e
52
+ }
53
+ } finally {
54
+ db.close()
55
+ }
56
+ }