@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,215 @@
1
+ import { closeSync, openSync, readSync, statSync } from "node:fs"
2
+ import { basename, join } from "node:path"
3
+ import { isDir, ls, mtime, readJson } from "../../fsutil.ts"
4
+ import type { Attachment, Session, Turn } from "../../session/types.ts"
5
+ import { asText, clip, header, lossCounter, noteModel } from "../../session/util.ts"
6
+ import type { Reader, SessionRef } from "../types.ts"
7
+ import { CHAT_MESSAGES, CHAT_META, CONTINUED_NOTE, LABEL, RUN_STATE, SYSTEM_WRAPPED, TOOL, chatTime, codebuffKind, projectsDir, resultText, summaryOf } from "./records.ts"
8
+
9
+ // Codebuff / Freebuff chats (see records.ts). A chat is read from what the model is sent, run-state.json's
10
+ // messageHistory: user prompts (tagged USER_PROMPT), assistant text, reasoning and tool-call parts, tool
11
+ // messages with JSON results, and the runtime's <conversation_summary> message after a compaction. The
12
+ // runtime's own scaffolding (instructions, step prompts, <system> notes) is skipped. Without a run state the
13
+ // on-screen chat-messages.json is read instead.
14
+
15
+ type Json = Record<string, any>
16
+
17
+ const textOf = (content: unknown) =>
18
+ typeof content === "string"
19
+ ? content
20
+ : (Array.isArray(content) ? content : [])
21
+ .filter((p: Json) => p?.type === "text" && typeof p.text === "string")
22
+ .map((p: Json) => p.text as string)
23
+ .join("")
24
+
25
+ /** A prompt the runtime re-sends to resume a turn after a compaction (conversation-summary.ts). */
26
+ const CONTINUATION_TAG = "CONTINUATION_PROMPT"
27
+ /** A message sent while the agent was working, as the runtime frames it (util/messages.ts frameSteeringText). */
28
+ const STEERING = /^<user_message_sent_while_working>\n([\s\S]*?)\n<\/user_message_sent_while_working>[\s\S]*$/
29
+
30
+ /** The first prompt, for titles: chat-meta.json when it still matches the messages file, as /history uses it. */
31
+ function firstPrompt(dir: string): string | undefined {
32
+ const meta = readJson<Json>(join(dir, CHAT_META))
33
+ try {
34
+ const st = statSync(join(dir, CHAT_MESSAGES))
35
+ if (meta && typeof meta.firstPrompt === "string" && meta.messagesSize === st.size && meta.messagesMtimeMs === st.mtimeMs) return meta.firstPrompt
36
+ } catch {
37
+ return undefined
38
+ }
39
+ const msgs = readJson<Json[]>(join(dir, CHAT_MESSAGES))
40
+ const first = Array.isArray(msgs) ? msgs.find((m) => m?.variant === "user" && typeof m.content === "string" && m.content.trim()) : undefined
41
+ return first?.content.trim()
42
+ }
43
+
44
+ /** The project folder a chat ran in, read from the start of run-state.json's fileContext without parsing it all. */
45
+ function projectRoot(dir: string): string | undefined {
46
+ const path = join(dir, RUN_STATE)
47
+ let size = 0
48
+ try {
49
+ size = statSync(path).size
50
+ } catch {
51
+ return undefined
52
+ }
53
+ // fileContext follows messageHistory, so search a window of the file rather than the whole of a long chat.
54
+ const fd = openSync(path, "r")
55
+ try {
56
+ const chunk = 1 << 20
57
+ const buf = Buffer.alloc(chunk + 4096)
58
+ for (let pos = 0; pos < size; pos += chunk) {
59
+ const n = readSync(fd, buf, 0, buf.length, pos)
60
+ const m = buf.subarray(0, n).toString("utf8").match(/"fileContext":\{"projectRoot":("(?:[^"\\]|\\.)*")/)
61
+ if (m) return JSON.parse(m[1]!) as string
62
+ }
63
+ } finally {
64
+ closeSync(fd)
65
+ }
66
+ return undefined
67
+ }
68
+
69
+ export function readChat(dir: string, id: string): Session {
70
+ const run = readJson<Json>(join(dir, RUN_STATE))
71
+ const state = run?.sessionState as Json | undefined
72
+ const history = state?.mainAgentState?.messageHistory as Json[] | undefined
73
+ const h = header(TOOL, id, { sourcePath: dir, project: { cwd: state?.fileContext?.projectRoot ?? state?.fileContext?.cwd }, startedAt: chatTime(id), updatedAt: mtime(join(dir, CHAT_MESSAGES)) ?? mtime(dir) })
74
+ noteModel(h, state?.mainAgentState?.contextTokenBaseline?.model)
75
+ const lost = lossCounter()
76
+ const turns: Turn[] = []
77
+ if (Array.isArray(history)) readHistory(history, turns, h, lost)
78
+ else if (!readScreen(dir, turns, lost)) throw new Error(`no readable chat in ${dir}`)
79
+ lost.flush(h)
80
+ const first = turns.find((t) => t.kind === "user") as Extract<Turn, { kind: "user" }> | undefined
81
+ if (first) h.title = first.text.replace(/\s+/g, " ").trim().slice(0, 80)
82
+ return { header: h, turns }
83
+ }
84
+
85
+ function readHistory(history: Json[], turns: Turn[], h: Session["header"], lost: ReturnType<typeof lossCounter>) {
86
+ const calls = new Map<string, string>()
87
+ const answered = new Set<string>()
88
+ const at = (m: Json) => (typeof m.sentAt === "number" ? new Date(m.sentAt).toISOString() : undefined)
89
+ for (const m of history) {
90
+ const when = at(m)
91
+ const stamp = when ? { at: when } : {}
92
+ if (m.role === "user") {
93
+ const text = textOf(m.content)
94
+ const tags: string[] = Array.isArray(m.tags) ? m.tags : []
95
+ const summary = summaryOf(text)
96
+ if (summary !== undefined) {
97
+ turns.push({ kind: "compaction", ...stamp, summary })
98
+ continue
99
+ }
100
+ // The person's prompts carry USER_PROMPT; every other tag marks a note the runtime added itself.
101
+ if (tags.length ? !tags.includes("USER_PROMPT") || tags.includes(CONTINUATION_TAG) : SYSTEM_WRAPPED.test(text.trim())) {
102
+ const note = text.match(CONTINUED_NOTE)
103
+ if (note && !turns.length) h.parent = { id: note[1]!, kind: "continued" }
104
+ continue
105
+ }
106
+ const parts: Json[] = Array.isArray(m.content) ? m.content : []
107
+ const images = parts.filter((p) => p?.type === "image" || p?.type === "file").length
108
+ if (images) lost.add("images (kept inline by Codebuff, not as files)", images)
109
+ const attachments: Attachment[] = Array.from({ length: images }, () => ({ kind: "image" }))
110
+ const said = text.replace(STEERING, "$1")
111
+ if (said.trim() || images) turns.push({ kind: "user", ...stamp, text: said, ...(images ? { attachments } : {}) })
112
+ continue
113
+ }
114
+ if (m.role === "assistant") {
115
+ for (const p of (Array.isArray(m.content) ? m.content : [{ type: "text", text: m.content }]) as Json[]) {
116
+ if (p?.type === "text" && typeof p.text === "string" && p.text.trim()) turns.push({ kind: "assistant", ...stamp, text: p.text, ...(h.models[0] ? { model: h.models[0] } : {}) })
117
+ else if (p?.type === "reasoning") {
118
+ if (typeof p.text === "string" && p.text.trim()) turns.push({ kind: "reasoning", ...stamp, text: p.text })
119
+ if (p.providerOptions) lost.add("signed reasoning")
120
+ } else if (p?.type === "tool-call") {
121
+ const id = String(p.toolCallId ?? "")
122
+ if (!id || calls.has(id)) {
123
+ lost.add("tool calls without a usable id")
124
+ continue
125
+ }
126
+ const name = String(p.toolName ?? "tool")
127
+ calls.set(id, name)
128
+ turns.push({ kind: "tool_call", ...stamp, id, name, toolKind: codebuffKind(name), input: p.input ?? {} })
129
+ if (name === "spawn_agents" || name === "spawn_agent_inline") lost.add("subagent transcripts (only their results are kept)")
130
+ if ((name === "write_file" || name === "str_replace") && typeof p.input?.path === "string") turns.push({ kind: "file_edit", path: p.input.path, callId: id })
131
+ }
132
+ }
133
+ continue
134
+ }
135
+ if (m.role === "tool") {
136
+ const id = String(m.toolCallId ?? "")
137
+ if (!calls.has(id)) {
138
+ lost.add("results without a call")
139
+ continue
140
+ }
141
+ if (answered.has(id)) {
142
+ lost.add("repeated results")
143
+ continue
144
+ }
145
+ answered.add(id)
146
+ const r = resultText(String(m.toolName ?? calls.get(id)), m.content)
147
+ if (r.media) lost.add("images in tool results", r.media)
148
+ const c = clip(r.text)
149
+ turns.push({ kind: "tool_result", ...stamp, callId: id, ok: r.ok, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
150
+ continue
151
+ }
152
+ if (m.role !== "system") lost.add(`unknown message roles: ${String(m.role)}`)
153
+ }
154
+ }
155
+
156
+ /** The on-screen messages, for a chat whose run state is missing. */
157
+ function readScreen(dir: string, turns: Turn[], lost: ReturnType<typeof lossCounter>): boolean {
158
+ const msgs = readJson<Json[]>(join(dir, CHAT_MESSAGES))
159
+ if (!Array.isArray(msgs)) return false
160
+ lost.add("the model's history (read from the on-screen chat, run-state.json is missing)")
161
+ const walk = (blocks: Json[]) => {
162
+ for (const b of blocks) {
163
+ if (b?.type === "text" && typeof b.content === "string" && b.content.trim()) turns.push(b.textType === "reasoning" ? { kind: "reasoning", text: b.content } : { kind: "assistant", text: b.content })
164
+ else if (b?.type === "tool" && b.toolCallId) {
165
+ turns.push({ kind: "tool_call", id: String(b.toolCallId), name: String(b.toolName), toolKind: codebuffKind(String(b.toolName)), input: b.input ?? {} })
166
+ turns.push({ kind: "tool_result", callId: String(b.toolCallId), ok: true, output: clip(asText(b.output ?? "")).text })
167
+ } else if (b?.type === "agent" && Array.isArray(b.blocks)) {
168
+ lost.add("subagent transcripts (only their results are kept)")
169
+ }
170
+ }
171
+ }
172
+ for (const m of msgs) {
173
+ if (m?.variant === "user" && typeof m.content === "string" && m.content.trim()) turns.push({ kind: "user", text: m.content })
174
+ else if (m?.variant === "ai" && String(m.id ?? "").startsWith("ai-")) {
175
+ if (m.content) turns.push({ kind: "assistant", text: String(m.content) })
176
+ walk(Array.isArray(m.blocks) ? m.blocks : [])
177
+ }
178
+ }
179
+ return true
180
+ }
181
+
182
+ export const codebuffReader: Reader = {
183
+ tool: TOOL,
184
+ label: LABEL,
185
+
186
+ async list(home: string): Promise<SessionRef[]> {
187
+ const refs: SessionRef[] = []
188
+ for (const project of ls(projectsDir(home))) {
189
+ const chats = join(projectsDir(home), project, "chats")
190
+ for (const id of ls(chats)) {
191
+ const dir = join(chats, id)
192
+ if (!isDir(dir)) continue
193
+ // As /history lists: chats with messages, newest folder first.
194
+ const title = firstPrompt(dir)
195
+ if (!title) continue
196
+ const cwd = projectRoot(dir)
197
+ refs.push({
198
+ tool: TOOL,
199
+ sourceId: id,
200
+ path: dir,
201
+ title: title.replace(/\s+/g, " ").trim().slice(0, 80),
202
+ ...(cwd ? { cwd } : {}),
203
+ updatedAt: mtime(dir),
204
+ extra: { project },
205
+ })
206
+ }
207
+ }
208
+ return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
209
+ },
210
+
211
+ async read(ref: SessionRef): Promise<Session> {
212
+ if (!isDir(ref.path)) throw new Error(`no Codebuff chat at ${ref.path}`)
213
+ return readChat(ref.path, ref.sourceId || basename(ref.path))
214
+ },
215
+ }
@@ -0,0 +1,173 @@
1
+ import { readFileSync, realpathSync } from "node:fs"
2
+ import { basename, dirname, join } from "node:path"
3
+ import type { ToolKind } from "../../session/types.ts"
4
+ import { asText, toolKindOf } from "../../session/util.ts"
5
+
6
+ // What Codebuff / Freebuff chats look like, as far as this adapter relies on it. Checked against the source of
7
+ // CodebuffAI/freebuff at 0f5dfd6 (2026-11-08), whose release packages are freebuff 0.2.36 and codebuff 1.0.688:
8
+ // cli/src/utils/{run-state-storage,chat-meta,chat-history,config-dir,message-history,send-message-helpers}.ts,
9
+ // cli/src/project-files.ts, sdk/src/run-state.ts, common/src/types/{session-state,messages/*}.ts,
10
+ // common/src/util/messages.ts, packages/agent-runtime/src/{run-agent-step,compact-history}.ts and
11
+ // tools/stream-parser.ts. Catalogue entry 19.
12
+ //
13
+ // Both CLIs keep their chats in ~/.config/manicode/projects/<basename of the project folder>/chats/<chat id>/,
14
+ // the chat id being the start time as an ISO string with ':' replaced by '-'. A chat is three files:
15
+ // run-state.json (the SDK RunState; sessionState.mainAgentState.messageHistory is what the model is sent,
16
+ // fileContext is the project snapshot the system prompt is built from), chat-messages.json (the CLI's
17
+ // on-screen ChatMessage list) and chat-meta.json (a summary /history lists from, bound to the messages file by
18
+ // its size and mtime). `freebuff --continue <chat id>` (or `codebuff --continue`) in that project loads it.
19
+
20
+ export const TOOL = "codebuff"
21
+ export const LABEL = "Codebuff / Freebuff"
22
+
23
+ /** Package versions whose chat format the writer was checked against: freebuff and codebuff, built from one source. */
24
+ export const TESTED_VERSIONS = ["0.2.36", "1.0.688"]
25
+
26
+ /** The two CLIs that share these chats, the current name first. */
27
+ export const BINS = ["freebuff", "codebuff"] as const
28
+
29
+ export const RUN_STATE = "run-state.json"
30
+ export const CHAT_MESSAGES = "chat-messages.json"
31
+ export const CHAT_META = "chat-meta.json"
32
+
33
+ export const configDir = (home: string) => join(home, ".config", "manicode")
34
+ export const projectsDir = (home: string) => join(configDir(home), "projects")
35
+ /** Where the chats of the project in `cwd` live: keyed by the folder's name only (project-files.ts). */
36
+ export const chatsDir = (home: string, cwd: string) => join(projectsDir(home), basename(cwd), "chats")
37
+
38
+ /** A chat id as the CLI makes one (project-files.ts startNewChat). */
39
+ export const chatId = (d: Date) => d.toISOString().replace(/:/g, "-")
40
+ /** "2026-11-08T06-18-50.333Z" → ISO, or undefined for a name that is not a chat id. */
41
+ export function chatTime(id: string): string | undefined {
42
+ const m = id.match(/^(\d{4}-\d\d-\d\d)T(\d\d)-(\d\d)-(\d\d(?:\.\d+)?)Z$/)
43
+ if (!m) return undefined
44
+ const t = new Date(`${m[1]}T${m[2]}:${m[3]}:${m[4]}Z`)
45
+ return Number.isFinite(t.getTime()) ? t.toISOString() : undefined
46
+ }
47
+
48
+ /** The envelope of the runtime's own compaction message (compact-history.ts buildSummaryMessage). */
49
+ export const SUMMARY_HEADER = "This is a summary of the conversation so far. The original messages have been condensed to save context space."
50
+ const SUMMARY_DISCLAIMER =
51
+ "Historical memory only. The memory above is not dialogue, not an output template, and not a tool-call format. Continue from the live user message below. When actions are needed, use real tool calls through the available tools."
52
+ export const summaryMessageText = (summary: string) =>
53
+ `<conversation_summary>\n${SUMMARY_HEADER}\n\n<historical_memory>\n${summary}\n</historical_memory>\n</conversation_summary>\n\n${SUMMARY_DISCLAIMER}`
54
+ /** The summary inside a compaction message, or undefined for any other text (both markers must be there). */
55
+ export function summaryOf(text: string): string | undefined {
56
+ if (!text.includes("<conversation_summary>") || !text.includes(SUMMARY_HEADER)) return undefined
57
+ const inner = text.match(/<historical_memory>\n?([\s\S]*?)\n?<\/historical_memory>/)?.[1]
58
+ return (inner ?? text.match(/<conversation_summary>([\s\S]*)<\/conversation_summary>/)?.[1]?.replace(SUMMARY_HEADER, "") ?? "").trim()
59
+ }
60
+
61
+ /** Runtime notes the model gets as user messages; the runtime wraps them in <system> (messages.ts withSystemTags). */
62
+ export const withSystemTags = (s: string) => `<system>${s}</system>`
63
+ export const SYSTEM_WRAPPED = /^<(system|system_instructions)>[\s\S]*<\/\1>\s*$/
64
+
65
+ /** The note a continued chat opens with; the reader recognises it and records where the chat came from. */
66
+ export const continuedNote = (label: string, id: string, when: string) =>
67
+ `[This chat continues ${label} session ${id}, carried over by brain session on ${when}. The messages after this note are its history; anything listed as lost did not carry over. Check files before relying on details, since they may have changed.]`
68
+ export const CONTINUED_NOTE = /\[This chat continues .+? session (\S+), carried over by brain session/
69
+
70
+ /**
71
+ * The installed CLI and its version, read from the npm package the command resolves to (its package.json sits
72
+ * next to the launcher script). Running `freebuff --version` would let the launcher download or update its binary.
73
+ */
74
+ export function installed(bins: readonly string[] = BINS): { bin: string; version: string } | undefined {
75
+ for (const bin of bins) {
76
+ const path = Bun.which(bin)
77
+ if (!path) continue
78
+ let dir: string
79
+ try {
80
+ dir = dirname(realpathSync(path))
81
+ } catch {
82
+ continue
83
+ }
84
+ for (let i = 0; i < 3; i++, dir = dirname(dir)) {
85
+ try {
86
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")) as { name?: string; version?: string }
87
+ if (pkg.name === bin && pkg.version) return { bin, version: pkg.version }
88
+ } catch {}
89
+ }
90
+ }
91
+ return undefined
92
+ }
93
+
94
+ /** Codebuff's own tools in portable terms; others by name. */
95
+ export function codebuffKind(name: string): ToolKind {
96
+ switch (name) {
97
+ case "run_terminal_command":
98
+ return "shell"
99
+ case "read_files":
100
+ case "read_subtree":
101
+ case "read_docs":
102
+ return "read"
103
+ case "write_file":
104
+ case "propose_write_file":
105
+ return "write"
106
+ case "str_replace":
107
+ case "propose_str_replace":
108
+ case "apply_patch":
109
+ return "edit"
110
+ case "code_search":
111
+ case "find_files":
112
+ case "glob":
113
+ return "search"
114
+ case "list_directory":
115
+ return "list"
116
+ case "web_search":
117
+ case "read_url":
118
+ return "web"
119
+ case "spawn_agents":
120
+ case "spawn_agent_inline":
121
+ return "task"
122
+ case "write_todos":
123
+ return "todo"
124
+ case "ask_user":
125
+ return "ask"
126
+ case "think_deeply":
127
+ case "end_turn":
128
+ case "set_output":
129
+ case "task_completed":
130
+ return "other"
131
+ }
132
+ return toolKindOf(name)
133
+ }
134
+
135
+ type Json = Record<string, any>
136
+
137
+ /** A tool message's content (ToolResultOutput[]) as the text a person would read, and whether it failed. */
138
+ export function resultText(toolName: string, content: unknown): { ok: boolean; text: string; media: number } {
139
+ const parts = Array.isArray(content) ? (content as Json[]) : []
140
+ let ok = true
141
+ let media = 0
142
+ const texts: string[] = []
143
+ for (const p of parts) {
144
+ if (p?.type === "media") {
145
+ media++
146
+ continue
147
+ }
148
+ if (p?.type !== "json") continue
149
+ const v = p.value as unknown
150
+ if (v && typeof v === "object" && !Array.isArray(v) && typeof (v as Json).errorMessage === "string") {
151
+ ok = false
152
+ texts.push((v as Json).errorMessage)
153
+ continue
154
+ }
155
+ if (toolName === "run_terminal_command" && v && typeof v === "object" && !Array.isArray(v)) {
156
+ const r = v as Json
157
+ if (typeof r.exitCode === "number" && r.exitCode !== 0) ok = false
158
+ const out = [r.stdout, r.stderr].filter((s) => typeof s === "string" && s).join("")
159
+ texts.push(out || (typeof r.message === "string" ? r.message : asText(v)))
160
+ continue
161
+ }
162
+ if (toolName === "read_files" && Array.isArray(v)) {
163
+ texts.push(v.map((f: Json) => (typeof f?.content === "string" ? (v.length > 1 ? `${f.path}:\n${f.content}` : f.content) : asText(f))).join("\n\n"))
164
+ continue
165
+ }
166
+ if (v && typeof v === "object" && !Array.isArray(v) && typeof (v as Json).message === "string" && Object.keys(v).every((k) => k === "message" || k === "file")) {
167
+ texts.push((v as Json).message)
168
+ continue
169
+ }
170
+ texts.push(asText(v))
171
+ }
172
+ return { ok, text: texts.join("\n"), media }
173
+ }