@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 { readFileSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+ import type { SessionHeader, Turn } from "../../session/types.ts"
4
+ import { asText, clip, iso, noteModel, toolKindOf, type lossCounter } from "../../session/util.ts"
5
+ import { CONTINUED_NOTE, idMaker, type PartList, resultText, snapshotText } from "../gemini-cli/records.ts"
6
+
7
+ // Facts about iFlow CLI's session records, shared by its reader and writer. iFlow CLI is a Gemini CLI fork
8
+ // that records sessions in Claude Code's shape. Checked against its last release, 0.5.19 (bundle/iflow.js:
9
+ // the session message builder, the JSONL storage, and the resume path that turns records back into the
10
+ // screen's history and the model's).
11
+ //
12
+ // A session is ~/.iflow/projects/<project name>/session-<uuid>.jsonl (IFLOW_HOME moves ~/.iflow), one record
13
+ // per line, linked by uuid/parentUuid. The loader sorts records by timestamp and starts from the last
14
+ // compression record. type "user" holds the person's text (a string, or text and image blocks), a tool
15
+ // result (a tool_result block whose content is the tool's response object, plus toolUseResult), a
16
+ // compression summary (isCompactSummary) or a hidden note (isMeta); type "assistant" holds a Claude-style
17
+ // message whose content is text and tool_use blocks. iFlow keeps no reasoning in its records.
18
+
19
+ export const TOOL = "iflow-cli"
20
+ export const LABEL = "iFlow CLI"
21
+
22
+ /** What iFlow CLI writes in every record's `version` (process.env.IFLOW_CLI_VERSION, which it never sets). */
23
+ export const RECORD_VERSION = "1.0.0"
24
+
25
+ /** iFlow CLI's project folder name (ProjectNameGenerator.fromPath). */
26
+ export function projectName(cwd: string): string {
27
+ let n = resolve(cwd)
28
+ .replace(/^\//, "")
29
+ .replace(/\\/g, "-")
30
+ .replace(/\//g, "-")
31
+ .replace(/:/g, "-")
32
+ .replace(/\s+/g, "-")
33
+ .replace(/[^\w\-_.]/g, "-")
34
+ if (!n.startsWith("-")) n = `-${n}`
35
+ n = n.replace(/-+/g, "-")
36
+ return n || "-unnamed-project"
37
+ }
38
+
39
+ export type Block =
40
+ | { type: "text"; text: string }
41
+ | { type: "image"; source?: { type?: string; media_type?: string; data?: string } }
42
+ | { type: "tool_use"; id: string; name: string; input: unknown }
43
+ | { type: "tool_result"; tool_use_id: string; content: unknown }
44
+ | { type: "thinking"; thinking?: string }
45
+
46
+ export interface IflowRecord {
47
+ uuid: string
48
+ parentUuid: string | null
49
+ sessionId: string
50
+ timestamp: string
51
+ type: "user" | "assistant"
52
+ isSidechain?: boolean
53
+ userType?: string
54
+ message: { role: string; content: string | Block[]; id?: string; type?: string; model?: string; stop_reason?: null | string; stop_sequence?: null | string; usage?: { input_tokens?: number; output_tokens?: number } }
55
+ cwd?: string
56
+ gitBranch?: string | null
57
+ version?: string
58
+ toolUseResult?: { toolName?: string; status?: string; timestamp?: number }
59
+ isCompactSummary?: boolean
60
+ compressionInfo?: { originalTokenCount?: number; newTokenCount?: number; summary?: string }
61
+ isMeta?: boolean
62
+ }
63
+
64
+ /** The note that ends the first message of a session brain continued, with the blank line before it. */
65
+ const NOTE_SUFFIX = /(?:^|\n\n)(\[This session continues [^\n]*\])$/
66
+
67
+ /** Text without brain's note, and the source session it names. */
68
+ export function stripNote(text: string): { text: string; source?: string } {
69
+ const m = text.match(NOTE_SUFFIX)
70
+ const source = m?.[1]!.match(CONTINUED_NOTE)?.[1]
71
+ return source ? { text: text.slice(0, m!.index), source } : { text }
72
+ }
73
+
74
+ const isObj = (v: unknown): v is Record<string, any> => typeof v === "object" && v !== null && !Array.isArray(v)
75
+
76
+ /** A session file's records in the order iFlow loads them: by timestamp, file order breaking ties. */
77
+ export function readRecords(path: string): { records: IflowRecord[]; bad: number } {
78
+ let text: string
79
+ try {
80
+ text = readFileSync(path, "utf8")
81
+ } catch {
82
+ return { records: [], bad: 0 }
83
+ }
84
+ const rows: Array<{ r: IflowRecord; i: number }> = []
85
+ let bad = 0
86
+ text.split("\n").forEach((line, i) => {
87
+ if (!line.trim()) return
88
+ try {
89
+ const r = JSON.parse(line)
90
+ if (isObj(r) && (r.type === "user" || r.type === "assistant") && isObj(r.message)) rows.push({ r: r as IflowRecord, i })
91
+ else bad++
92
+ } catch {
93
+ bad++
94
+ }
95
+ })
96
+ const time = (r: IflowRecord) => {
97
+ const t = Date.parse(r.timestamp)
98
+ return Number.isFinite(t) ? t : 0
99
+ }
100
+ rows.sort((a, b) => time(a.r) - time(b.r) || a.i - b.i)
101
+ return { records: rows.map((x) => x.r), bad }
102
+ }
103
+
104
+ /** iFlow's own names for tools whose kind toolKindOf cannot tell from the name. */
105
+ const KIND: Record<string, Extract<Turn, { kind: "tool_call" }>["toolKind"]> = { replace: "edit" }
106
+
107
+ /** A tool result's text and whether it failed: the tool's response object (responseParts), or plain text. */
108
+ function resultOf(content: unknown, status: string | undefined): { text: string; ok: boolean } {
109
+ const failed = status !== undefined && status !== "success"
110
+ if (isObj(content)) {
111
+ if (content.responseParts !== undefined) {
112
+ const r = resultText(content.responseParts as PartList)
113
+ const error = content.error ? asText(isObj(content.error) ? content.error.message ?? content.error : content.error) : ""
114
+ return { text: r.text || error || (typeof content.resultDisplay === "string" ? content.resultDisplay : ""), ok: r.ok && !error && !failed }
115
+ }
116
+ if (typeof content.resultDisplay === "string") return { text: content.resultDisplay, ok: !failed }
117
+ }
118
+ return { text: asText(content), ok: !failed }
119
+ }
120
+
121
+ /** The portable turns of a session's records; `h` gets its models, version and source note. */
122
+ export function recordsToTurns(records: IflowRecord[], h: SessionHeader, lost: ReturnType<typeof lossCounter>): Turn[] {
123
+ const turns: Turn[] = []
124
+ const newId = idMaker()
125
+ // iFlow's own call ids, to the turn ids given to them, while a call waits for its result.
126
+ const waiting = new Map<string, Extract<Turn, { kind: "tool_call" }>>()
127
+ for (const r of records) {
128
+ const at = iso(r.timestamp)
129
+ const content = r.message.content
130
+ const blocks: Block[] = typeof content === "string" ? [{ type: "text", text: content }] : Array.isArray(content) ? content : []
131
+ if (r.version && r.version !== RECORD_VERSION && !h.toolVersion) h.toolVersion = r.version
132
+ if (r.isSidechain) {
133
+ lost.add("subagent records")
134
+ continue
135
+ }
136
+ if (r.type === "assistant") {
137
+ noteModel(h, r.message.model)
138
+ let text = ""
139
+ const flush = () => {
140
+ if (text.trim()) turns.push({ kind: "assistant", ...(at ? { at } : {}), text, ...(r.message.model ? { model: r.message.model } : {}) })
141
+ text = ""
142
+ }
143
+ for (const b of blocks) {
144
+ if (b.type === "text") text += text ? `\n${b.text}` : b.text
145
+ else if (b.type === "thinking") {
146
+ flush()
147
+ if (b.thinking?.trim()) turns.push({ kind: "reasoning", ...(at ? { at } : {}), text: b.thinking })
148
+ } else if (b.type === "tool_use") {
149
+ flush()
150
+ const call = { kind: "tool_call" as const, ...(at ? { at } : {}), id: newId(b.id), name: b.name, toolKind: KIND[b.name] ?? toolKindOf(b.name), input: b.input ?? {} }
151
+ waiting.set(b.id, call)
152
+ turns.push(call)
153
+ }
154
+ }
155
+ flush()
156
+ const u = r.message.usage
157
+ const last = [...turns].reverse().find((t) => t.kind === "assistant") as Extract<Turn, { kind: "assistant" }> | undefined
158
+ if (u && last && last.at === at && (u.input_tokens || u.output_tokens)) last.usage = { input: u.input_tokens ?? 0, output: u.output_tokens ?? 0 }
159
+ continue
160
+ }
161
+ if (r.isCompactSummary) {
162
+ const text = r.compressionInfo?.summary ?? stripNote(blocks.map((b) => (b.type === "text" ? b.text : "")).join("")).text
163
+ const note = stripNote(typeof content === "string" ? content : "")
164
+ if (note.source) h.parent = { id: note.source, kind: "continued" }
165
+ turns.push({ kind: "compaction", ...(at ? { at } : {}), summary: text })
166
+ continue
167
+ }
168
+ if (r.isMeta) {
169
+ lost.add("iFlow CLI meta messages")
170
+ continue
171
+ }
172
+ const results = blocks.filter((b): b is Extract<Block, { type: "tool_result" }> => b.type === "tool_result")
173
+ if (results.length) {
174
+ for (const b of results) {
175
+ const call = waiting.get(b.tool_use_id)
176
+ if (!call) {
177
+ lost.add("tool results without a call")
178
+ continue
179
+ }
180
+ waiting.delete(b.tool_use_id)
181
+ const res = resultOf(b.content, r.toolUseResult?.status)
182
+ const c = clip(res.text)
183
+ turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId: call.id, ok: res.ok, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
184
+ }
185
+ continue
186
+ }
187
+ let text = blocks.map((b) => (b.type === "text" ? b.text : "")).join("")
188
+ const note = stripNote(text)
189
+ if (note.source) {
190
+ h.parent = { id: note.source, kind: "continued" }
191
+ text = note.text
192
+ }
193
+ // A summary brain wrote for a compacted session, in the form Gemini CLI's compression writes.
194
+ const summary = snapshotText(text)
195
+ if (summary !== undefined) {
196
+ turns.push({ kind: "compaction", ...(at ? { at } : {}), summary })
197
+ continue
198
+ }
199
+ const images = blocks.filter((b) => b.type === "image").length
200
+ if (images) lost.add("inline images", images)
201
+ if (text.trim()) turns.push({ kind: "user", ...(at ? { at } : {}), text })
202
+ }
203
+ // Calls that never got a result (iFlow records only successful ones) are closed as failed, after the call.
204
+ if (waiting.size) {
205
+ lost.add("tool results not recorded", waiting.size)
206
+ const open = new Set(waiting.values())
207
+ const out: Turn[] = []
208
+ for (const t of turns) {
209
+ out.push(t)
210
+ if (t.kind === "tool_call" && open.has(t)) out.push({ kind: "tool_result", ...(t.at ? { at: t.at } : {}), callId: t.id, ok: false, output: "(no result recorded: iFlow CLI keeps only successful results)" })
211
+ }
212
+ return out
213
+ }
214
+ return turns
215
+ }
@@ -0,0 +1,204 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs"
3
+ import { join } from "node:path"
4
+ import { toolLabel } from "../../brief.ts"
5
+ import { exists, gitInfo, isDir, readJson, versionOf } from "../../fsutil.ts"
6
+ import type { Session } from "../../session/types.ts"
7
+ import { countTurns, header, lossCounter, unpaired } from "../../session/util.ts"
8
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
9
+ import { ALIASES, type Call, clock, drops, imageData, mapCall, realCwd, regroup, resultOutput, shellQuote, type ToolSpec } from "../gemini-cli/exchanges.ts"
10
+ import { continuedNote, snapshotOf } from "../gemini-cli/records.ts"
11
+ import { type Block, type IflowRecord, LABEL, projectName, RECORD_VERSION, readRecords, recordsToTurns, TOOL } from "./records.ts"
12
+
13
+ // Continues a portable session in iFlow CLI as a new session of its own: projects/<project name>/
14
+ // session-<uuid>.jsonl under ~/.iflow, with the session added to config/projects.json as iFlow's
15
+ // createSession adds it. Records are built the way its session message builder builds them, so
16
+ // `iflow --resume <id>` loads the file (checked by running 0.5.19's own loader code, taken from its bundle).
17
+ //
18
+ // The person's messages are string content; a model response is one assistant record of text and tool_use
19
+ // blocks; each result is a user record holding a tool_result block with the tool's response object
20
+ // (responseParts, resultDisplay) and toolUseResult, which is what iFlow's resume turns into a tool group.
21
+ // A compacted session starts from its last summary, as iFlow loads its own from the last compression record
22
+ // on. The summary is a <state_snapshot> user message, not iFlow's isCompactSummary record: resuming from that
23
+ // record (rQ) gives the model the calls after it without their results, which the API rejects, while a plain
24
+ // message goes through the resume that pairs every call with its result. iFlow keeps no reasoning.
25
+ // iFlow hides isMeta messages from the model as well as the screen, so the note on where the session came
26
+ // from ends the first message the model reads (the first user message, or the summary); at the end, the
27
+ // session picker, which shows each session's first message, still shows the person's request.
28
+
29
+ /** Versions this writer was checked against; others get the brief. 0.5.19 is iFlow CLI's last release. */
30
+ export const TESTED_VERSIONS = ["0.5.19"]
31
+
32
+ /** The installed iFlow CLI version ("0.5.19"), or undefined when it is not installed. */
33
+ export const iflowVersion = (bin = "iflow") => versionOf(bin)?.match(/(\d+\.\d+\.\d+)/)?.[1]
34
+
35
+ /** iFlow CLI's own tool for a portable call, by what the call does. */
36
+ function iflowTool(call: Call): ToolSpec | undefined {
37
+ const input = (call.input ?? {}) as Record<string, unknown>
38
+ switch (call.toolKind) {
39
+ case "read":
40
+ return { name: "read_file", args: { absolute_path: ALIASES.file } }
41
+ case "edit":
42
+ return { name: "replace", args: { file_path: ALIASES.file, old_string: ALIASES.oldText, new_string: ALIASES.newText } }
43
+ case "write":
44
+ return { name: "write_file", args: { file_path: ALIASES.file, content: ALIASES.content } }
45
+ case "shell":
46
+ return { name: "run_shell_command", args: { command: ALIASES.command } }
47
+ case "search":
48
+ return { name: /glob/i.test(call.name) ? "glob" : "search_file_content", args: { pattern: ALIASES.pattern, path: ALIASES.dir } }
49
+ case "list":
50
+ return { name: "list_directory", args: { path: ALIASES.dir } }
51
+ case "web":
52
+ return ALIASES.url.some((k) => input[k] !== undefined) ? { name: "web_fetch", args: { url: ALIASES.url, prompt: ["prompt"] } } : { name: "web_search", args: { query: ALIASES.query } }
53
+ case "todo":
54
+ return { name: "todo_write", args: { todos: ["todos"] } }
55
+ default:
56
+ return undefined
57
+ }
58
+ }
59
+
60
+ export interface IflowPlan {
61
+ records: IflowRecord[]
62
+ lost: string[]
63
+ /** What a read-back must find. */
64
+ expect: { users: number; assistants: number; calls: number }
65
+ }
66
+
67
+ /** The records of the new session `sessionId` continuing `session`. Pure apart from reading attached images. */
68
+ export function planRecords(session: Session, opts: { sessionId: string; cwd: string; gitBranch?: string; now: Date }): IflowPlan {
69
+ const { drop, list } = drops()
70
+ const exchanges = regroup(session, { fromLastCompaction: true, keepReasoning: false, drop })
71
+ const label = toolLabel(session.header.tool)
72
+ const stamp = clock(session, opts.now)
73
+ const note = continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10))
74
+ const expect = { users: 0, assistants: 0, calls: 0 }
75
+ const records: IflowRecord[] = []
76
+ let parent: string | null = null
77
+ let lastModel = session.header.models[session.header.models.length - 1] ?? "unknown"
78
+ let noted = false
79
+ const withNote = (text: string) => {
80
+ if (noted) return text
81
+ noted = true
82
+ return text ? `${text}\n\n${note}` : note
83
+ }
84
+ const where = { cwd: opts.cwd, gitBranch: opts.gitBranch ?? null, version: RECORD_VERSION }
85
+ const add = (at: string | undefined, r: Omit<IflowRecord, "uuid" | "parentUuid" | "sessionId" | "timestamp" | "isSidechain" | "userType">, withPlace = true) => {
86
+ const out = { uuid: randomUUID(), parentUuid: parent, sessionId: opts.sessionId, timestamp: stamp(at), isSidechain: false, userType: "external", ...r, ...(withPlace ? where : {}) } as IflowRecord
87
+ records.push(out)
88
+ parent = out.uuid
89
+ return out
90
+ }
91
+
92
+ // A session that opens with the model gets the note on its own first.
93
+ if (exchanges[0]?.kind === "model") add(exchanges[0].at, { type: "user", message: { role: "user", content: withNote("") } })
94
+ for (const x of exchanges) {
95
+ if (x.kind === "user") {
96
+ let text = x.text
97
+ const images: Block[] = []
98
+ for (const a of x.attachments) {
99
+ if (a.kind === "file") {
100
+ if (a.path) text += `\n\n[Attached file: ${a.path}]`
101
+ continue
102
+ }
103
+ const img = imageData(a)
104
+ if (img) images.push({ type: "image", source: { type: "base64", media_type: img.mimeType, data: img.data } })
105
+ else if (a.path) drop("images that could not be attached")
106
+ }
107
+ if (!text.trim() && !images.length) continue
108
+ const body = withNote(text)
109
+ add(x.at, { type: "user", message: { role: "user", content: images.length ? [{ type: "text", text: body }, ...images] : body } })
110
+ if (text.trim()) expect.users++
111
+ } else if (x.kind === "compaction") {
112
+ add(x.at, { type: "user", message: { role: "user", content: withNote(snapshotOf(label, x.summary)) } })
113
+ } else {
114
+ if (x.model) lastModel = x.model
115
+ const calls = x.calls.map((a) => ({ a, id: `call_${randomUUID().replace(/-/g, "").slice(0, 24)}`, ...mapCall(a.call, iflowTool(a.call)) }))
116
+ const content: Block[] = [...(x.text.trim() ? [{ type: "text" as const, text: x.text }] : []), ...calls.map((c) => ({ type: "tool_use" as const, id: c.id, name: c.name, input: c.args }))]
117
+ if (!content.length) continue
118
+ const usage = { input_tokens: (x.usage?.input ?? 0) + (x.usage?.cacheRead ?? 0), output_tokens: x.usage?.output ?? 0 }
119
+ // Assistant records carry no cwd, branch or version, as iFlow writes them.
120
+ add(x.at, { type: "assistant", message: { id: `msg-${randomUUID()}`, type: "message", role: "assistant", content, model: lastModel, stop_reason: null, stop_sequence: null, usage } }, false)
121
+ if (x.text.trim()) expect.assistants++
122
+ for (const c of calls) {
123
+ if (!c.a.result) drop("tool calls without a result (closed as errors)")
124
+ if (c.a.edits.length) drop("file edit diffs", c.a.edits.length)
125
+ const ok = c.a.result?.ok ?? false
126
+ const output = resultOutput(c.a.result)
127
+ const response = { callId: c.id, responseParts: [{ functionResponse: { id: c.id, name: c.name, response: ok ? { output } : { error: output } } }], resultDisplay: output }
128
+ const r = add(c.a.result?.at ?? c.a.call.at, { type: "user", message: { role: "user", content: [{ tool_use_id: c.id, type: "tool_result", content: response }] }, toolUseResult: { toolName: c.name, status: ok ? "success" : "error" } })
129
+ r.toolUseResult!.timestamp = Date.parse(r.timestamp)
130
+ expect.calls++
131
+ }
132
+ }
133
+ }
134
+ if (!records.length) throw new Error("nothing iFlow CLI can resume: the session has no messages")
135
+ return { records, lost: list(), expect }
136
+ }
137
+
138
+ /** Adds the session to config/projects.json as createSession does, replacing the file in one step. */
139
+ function register(home: string, project: string, sessionId: string, now: Date) {
140
+ const file = join(home, ".iflow", "config", "projects.json")
141
+ mkdirSync(join(home, ".iflow", "config"), { recursive: true })
142
+ const data = readJson<Record<string, { name?: string; path?: string; sessions?: string[]; createdAt?: string; lastActivity?: string }>>(file) ?? {}
143
+ const entry = data[project] ?? { name: project, path: project, sessions: [], createdAt: now.toISOString() }
144
+ data[project] = { ...entry, sessions: [...(entry.sessions ?? []), sessionId], lastActivity: now.toISOString() }
145
+ const tmp = `${file}.${randomUUID()}.tmp`
146
+ writeFileSync(tmp, JSON.stringify(data, null, 2))
147
+ renameSync(tmp, file)
148
+ }
149
+
150
+ /** The iFlow CLI writer; `version` and `bin` are for tests, which must not depend on the installed binary. */
151
+ export function iflowWriter(opts: { bin?: string; version?: string } = {}): Writer {
152
+ const installed = () => opts.version ?? iflowVersion(opts.bin)
153
+ return {
154
+ tool: TOOL,
155
+ label: LABEL,
156
+ way: "native",
157
+
158
+ async ready(w: WriteOptions): Promise<Readiness> {
159
+ const v = installed()
160
+ if (!v) return { ok: false, why: "iFlow CLI is not installed (`iflow --version` did not run)" }
161
+ if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `iFlow CLI ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
162
+ if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; iFlow CLI lists and resumes sessions per project folder` }
163
+ return { ok: true }
164
+ },
165
+
166
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
167
+ const cwd = realCwd(w.cwd)
168
+ const project = projectName(cwd)
169
+ const dir = join(w.home, ".iflow", "projects", project)
170
+ let sessionId = `session-${randomUUID()}`
171
+ while (exists(join(dir, `${sessionId}.jsonl`))) sessionId = `session-${randomUUID()}`
172
+ const path = join(dir, `${sessionId}.jsonl`)
173
+ const now = new Date()
174
+ const branch = gitInfo(cwd, w.home).gitBranch
175
+ const plan = planRecords(session, { sessionId, cwd, now, ...(branch ? { gitBranch: branch } : {}) })
176
+ if (!w.dryRun) {
177
+ mkdirSync(dir, { recursive: true })
178
+ // "wx": a new file only; an existing session is never touched.
179
+ writeFileSync(path, `${plan.records.map((r) => JSON.stringify(r)).join("\n")}\n`, { flag: "wx" })
180
+ const h = header(TOOL, sessionId)
181
+ const turns = recordsToTurns(readRecords(path).records, h, lossCounter())
182
+ const got = countTurns({ header: h, turns })
183
+ const pairs = unpaired({ header: h, turns })
184
+ if (got.user !== plan.expect.users || got.assistant !== plan.expect.assistants || got.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length) {
185
+ rmSync(path, { force: true })
186
+ throw new Error(`the new iFlow CLI session did not read back as written (users ${got.user}/${plan.expect.users}, replies ${got.assistant}/${plan.expect.assistants}, tool calls ${got.tool_call}/${plan.expect.calls}); nothing was kept`)
187
+ }
188
+ register(w.home, project, sessionId, now)
189
+ }
190
+ return {
191
+ tool: TOOL,
192
+ way: "native",
193
+ sessionId,
194
+ written: [path, join(w.home, ".iflow", "config", "projects.json")],
195
+ open: `cd ${shellQuote(cwd)} && iflow --resume ${sessionId}`,
196
+ note: "iFlow CLI's service shut down on 2026-04-17, so the session opens but its models may no longer answer.",
197
+ lost: plan.lost,
198
+ dryRun: !!w.dryRun,
199
+ }
200
+ },
201
+ }
202
+ }
203
+
204
+ export const iflowNativeWriter = iflowWriter()
@@ -0,0 +1,112 @@
1
+ import type { Reader, Writer } from "./types.ts";
2
+ import * as claudeCode from "./claude-code/index.ts";
3
+ import * as codex from "./codex/index.ts";
4
+ import * as cursorIde from "./cursor-ide/index.ts";
5
+ import * as cursorCli from "./cursor-cli/index.ts";
6
+ import * as geminiCli from "./gemini-cli/index.ts";
7
+ import * as vscodeCopilot from "./vscode-copilot/index.ts";
8
+ import * as copilotCli from "./copilot-cli/index.ts";
9
+ import * as cline from "./cline/index.ts";
10
+ import * as opencode from "./opencode/index.ts";
11
+ import * as aider from "./aider/index.ts";
12
+ import * as amazonQCli from "./amazon-q-cli/index.ts";
13
+ import * as amazonQIde from "./amazon-q-ide/index.ts";
14
+ import * as auggie from "./auggie/index.ts";
15
+ import * as cline4 from "./cline4/index.ts";
16
+ import * as codebuff from "./codebuff/index.ts";
17
+ import * as continueAdapter from "./continue/index.ts";
18
+ import * as crush from "./crush/index.ts";
19
+ import * as factoryDroid from "./factory-droid/index.ts";
20
+ import * as goose from "./goose/index.ts";
21
+ import * as grokCli from "./grok-cli/index.ts";
22
+ import * as iflowCli from "./iflow-cli/index.ts";
23
+ import * as kiloCli from "./kilo-cli/index.ts";
24
+ import * as kiloCode from "./kilo-code/index.ts";
25
+ import * as kimi from "./kimi/index.ts";
26
+ import * as kiroCli from "./kiro-cli/index.ts";
27
+ import * as kiroIde from "./kiro-ide/index.ts";
28
+ import * as letta from "./letta/index.ts";
29
+ import * as mistralVibe from "./mistral-vibe/index.ts";
30
+ import * as openhands from "./openhands/index.ts";
31
+ import * as pearai from "./pearai/index.ts";
32
+ import * as pi from "./pi/index.ts";
33
+ import * as qoderCli from "./qoder-cli/index.ts";
34
+ import * as qwenCode from "./qwen-code/index.ts";
35
+ import * as tabnineCli from "./tabnine-cli/index.ts";
36
+ import * as voidAdapter from "./void/index.ts";
37
+ import * as zed from "./zed/index.ts";
38
+ import * as antigravity from "./antigravity/index.ts";
39
+ import * as devin from "./devin/index.ts";
40
+ import * as hermes from "./hermes/index.ts";
41
+ import * as mastracode from "./mastracode/index.ts";
42
+ import * as muse from "./muse/index.ts";
43
+ import * as omp from "./omp/index.ts";
44
+ import { briefWriter, briefWriters } from "../brief-writers.ts";
45
+
46
+ // Every adapter. Each module exports the readers and writers for one tool or format family; a tool with
47
+ // no native or import writer is still a target through the continuation brief (brief-writers.ts).
48
+ const MODULES = [
49
+ claudeCode,
50
+ codex,
51
+ cursorIde,
52
+ cursorCli,
53
+ geminiCli,
54
+ vscodeCopilot,
55
+ copilotCli,
56
+ cline,
57
+ opencode,
58
+ aider,
59
+ amazonQCli,
60
+ amazonQIde,
61
+ auggie,
62
+ cline4,
63
+ codebuff,
64
+ continueAdapter,
65
+ crush,
66
+ factoryDroid,
67
+ goose,
68
+ grokCli,
69
+ iflowCli,
70
+ kiloCli,
71
+ kiloCode,
72
+ kimi,
73
+ kiroCli,
74
+ kiroIde,
75
+ letta,
76
+ mistralVibe,
77
+ openhands,
78
+ pearai,
79
+ pi,
80
+ qoderCli,
81
+ qwenCode,
82
+ tabnineCli,
83
+ voidAdapter,
84
+ zed,
85
+ antigravity,
86
+ devin,
87
+ hermes,
88
+ mastracode,
89
+ muse,
90
+ omp,
91
+ ];
92
+
93
+ export const READERS: Reader[] = MODULES.flatMap((m) => m.readers);
94
+
95
+ const OWN: Writer[] = MODULES.flatMap((m) => m.writers);
96
+ const BRIEFED = new Set(briefWriters.map((w) => w.tool));
97
+ /**
98
+ * Every other target with its own writer gets the brief too, as the last resort: when its native or import writer is not
99
+ * ready (the tool missing, or a version that format was never checked on), the session still goes there as a brief.
100
+ */
101
+ const FALLBACK_BRIEFS: Writer[] = [...new Map(OWN.filter((w) => !BRIEFED.has(w.tool)).map((w) => [w.tool, w.label] as const))].map(([tool, label]) => briefWriter(tool, label));
102
+
103
+ /** Native and import writers first, so a target that has one prefers it over the brief. */
104
+ export const WRITERS: Writer[] = [...OWN, ...briefWriters, ...FALLBACK_BRIEFS];
105
+
106
+ export const readerFor = (tool: string) => READERS.find((r) => r.tool === tool);
107
+
108
+ /** The writers that can continue a session in `target`, best way first: native, then import, then brief. */
109
+ export function writersFor(target: string): Writer[] {
110
+ const order = { native: 0, import: 1, brief: 2 } as const;
111
+ return WRITERS.filter((w) => w.tool === target).sort((a, b) => order[a.way] - order[b.way]);
112
+ }
@@ -0,0 +1,21 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { familyReader } from "../opencode/reader.ts"
3
+ import { KILO_V1 } from "../opencode/tools.ts"
4
+ import { importWriter, type Target } from "../opencode/writer.ts"
5
+
6
+ // Kilo CLI 7.x (Kilo-Org/kilocode), an OpenCode fork: the OpenCode 1.x session, message and part tables in
7
+ // ~/.local/share/kilo/kilo.db (kilo-<channel>.db on other channels), and OpenCode's export/import JSON with
8
+ // the same schema (packages/schema/src/v1/session.ts; packages/opencode/src/cli/cmd/import.ts). So it reads
9
+ // with OpenCode's reader and continues a session through `kilo import`, verified live on 7.8.27 (temp home,
10
+ // no network): import, then `kilo session list` and `kilo export`. On 7.8.28 the wire harness (wire/run.ts kilo-cli) imports
11
+ // and continues the session with the real binary against a fake model server, plain and compacted.
12
+ //
13
+ // Checked against a real Kilo 7.5.0 export (session-migrate's capture, test/native-corpus-stores.test.ts): its default agent is
14
+ // `code` (OpenCode's is `build`), so the session is written with that agent.
15
+ //
16
+ // A signed-in Kilo also syncs every new session, an imported one included, to Kilo's cloud (KiloSessions).
17
+
18
+ export const KILO_TARGET: Target = { tool: "kilo-cli", label: "Kilo CLI", bin: "kilo", installDir: ".kilo", tested: ["7.8.27", "7.8.28"], slug: true, dialect: KILO_V1 }
19
+
20
+ export const readers: Reader[] = [familyReader({ tool: "kilo-cli", label: "Kilo CLI", app: "kilo", sqlite: true })]
21
+ export const writers: Writer[] = [importWriter(KILO_TARGET)]
@@ -0,0 +1,11 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { taskReader } from "../cline/reader.ts"
3
+ import { kiloCodeWriters } from "./writer.ts"
4
+
5
+ // The Kilo Code extension. Its 7.x sessions live in the Kilo CLI's database, which the kilo-cli reader already
6
+ // lists (listing them here too would show each twice), so this reader covers what only the extension has: the
7
+ // Cline-style task folders of Kilo Code before 7.0 (a Roo Code fork), in <app>/User/globalStorage/kilocode.kilo-code/
8
+ // tasks, which 7.x still reads for its legacy migration (packages/kilo-vscode/src/legacy-migration). The writer
9
+ // continues a session as a 7.x session through the extension's bundled CLI (writer.ts).
10
+ export const readers: Reader[] = [taskReader({ tool: "kilo-code", label: "Kilo Code", extension: "kilocode.kilo-code" })]
11
+ export const writers: Writer[] = kiloCodeWriters
@@ -0,0 +1,92 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs"
2
+ import { dirname, join } from "node:path"
3
+ import { exists, isDir, ls, readJson } from "../../fsutil.ts"
4
+ import type { Session } from "../../session/types.ts"
5
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
6
+ import { HOSTS, compareVersions } from "../cline/records.ts"
7
+ import { buildExport, continuedTitle, projectIdOf, sh } from "../opencode/writer.ts"
8
+ import { OPENCODE_V1 } from "../opencode/tools.ts"
9
+
10
+ // Continuing a session in the Kilo Code extension (kilocode.kilo-code 7.x). The extension no longer keeps
11
+ // Cline-style task folders: it runs the Kilo CLI it bundles (<extension>/bin/kilo serve, with
12
+ // KILO_DISABLE_CHANNEL_DB=true; packages/kilo-vscode/src/services/cli-backend/server-manager.ts), so its sessions
13
+ // are the CLI's, in ~/.local/share/kilo/kilo.db, and its panel lists them with session.list({directory, roots}).
14
+ // So the session is written as the export bundle Kilo's own importer takes (the OpenCode 1.x format the Kilo CLI
15
+ // writer also uses, ../opencode/writer.ts), and `open` runs the bundled binary's `import` from the folder, with
16
+ // the same environment the extension gives it, so the session lands in the store the panel reads.
17
+ //
18
+ // Verified live on the 7.8.27 and 7.8.28 binaries (the extension and the CLI are released together, from one
19
+ // monorepo at one version): import, then the extension's own listing and message routes on `kilo serve`, in a
20
+ // temp home with outbound network denied.
21
+
22
+ const EXTENSION = "kilocode.kilo-code"
23
+ export const TESTED_VERSIONS = ["7.8.27", "7.8.28"]
24
+
25
+ /** The installed extension's folder and version in an app: extensions.json names the active copy; else the newest folder. */
26
+ export function installedExtension(home: string, app: string): { dir: string; version: string } | undefined {
27
+ const host = HOSTS.find((h) => h.app === app)
28
+ if (!host) return undefined
29
+ const root = join(home, host.extensions)
30
+ const manifest = readJson<Array<{ identifier?: { id?: string }; version?: string; relativeLocation?: string; location?: { path?: string } }>>(join(root, "extensions.json"))
31
+ const entry = Array.isArray(manifest) ? manifest.find((e) => e?.identifier?.id?.toLowerCase() === EXTENSION && typeof e.version === "string") : undefined
32
+ if (entry?.version) {
33
+ const dir = entry.relativeLocation ? join(root, entry.relativeLocation) : entry.location?.path
34
+ if (dir && isDir(dir)) return { dir, version: entry.version }
35
+ }
36
+ const found = ls(root)
37
+ .map((d) => ({ d, v: d.match(/^kilocode\.kilo-code-(\d+\.\d+\.\d+)/i)?.[1] }))
38
+ .filter((x): x is { d: string; v: string } => !!x.v)
39
+ .sort((a, b) => compareVersions(a.v, b.v))
40
+ .at(-1)
41
+ return found ? { dir: join(root, found.d), version: found.v } : undefined
42
+ }
43
+
44
+ export function kiloCodeWriter(app: string): Writer {
45
+ const host = HOSTS.find((h) => h.app === app)!
46
+ const tool = app === "Code" ? "kilo-code" : `kilo-code-${app.toLowerCase()}`
47
+ const label = app === "Code" ? "Kilo Code" : `Kilo Code (${app})`
48
+ const where = app === "Code" ? "VS Code" : app
49
+ const binOf = (dir: string) => join(dir, "bin", process.platform === "win32" ? "kilo.exe" : "kilo")
50
+
51
+ return {
52
+ tool,
53
+ label,
54
+ way: "import",
55
+
56
+ async ready(w: WriteOptions): Promise<Readiness> {
57
+ const ext = installedExtension(w.home, app)
58
+ if (!ext) return { ok: false, why: `Kilo Code is not installed in ${where}` }
59
+ if (compareVersions(ext.version, "7.0.0") < 0) return { ok: false, why: `Kilo Code ${ext.version} keeps Cline-style tasks; this writer targets the 7.x extension, which runs the Kilo CLI` }
60
+ if (!TESTED_VERSIONS.includes(ext.version)) return { ok: false, why: `Kilo Code ${ext.version} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
61
+ if (!exists(binOf(ext.dir))) return { ok: false, why: `Kilo Code ${ext.version} has no bundled CLI at ${binOf(ext.dir)}` }
62
+ if (!isDir(w.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${w.cwd}` }
63
+ return { ok: true }
64
+ },
65
+
66
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
67
+ const ext = installedExtension(w.home, app)
68
+ const version = ext?.version ?? TESTED_VERSIONS.at(-1)!
69
+ const bin = ext ? binOf(ext.dir) : `<${where} extensions>/kilocode.kilo-code-${version}/bin/kilo`
70
+ const { bundle, sessionId, lost } = buildExport(session, { label: "Kilo Code", dialect: OPENCODE_V1, cwd: w.cwd, version, projectID: projectIdOf(w.cwd, "kilo"), slug: true })
71
+ const file = join(w.home, ".brain", "exports", `${tool}-${sessionId}.json`)
72
+ if (!w.dryRun) {
73
+ mkdirSync(dirname(file), { recursive: true })
74
+ writeFileSync(file, `${JSON.stringify(bundle, null, 2)}\n`)
75
+ }
76
+ return {
77
+ tool,
78
+ way: "import",
79
+ sessionId,
80
+ written: [file],
81
+ // The importer files the session under the project of the folder it runs in; the channel setting is the
82
+ // extension's own, so the session goes to the database its panel reads.
83
+ open: `cd ${sh(w.cwd)} && KILO_DISABLE_CHANNEL_DB=true ${sh(bin)} import ${sh(file)} && ${host.cli} ${sh(w.cwd)}`,
84
+ note: `In ${where}, open Kilo Code's session history and pick "${continuedTitle(session).slice(0, 80)}".`,
85
+ lost,
86
+ dryRun: !!w.dryRun,
87
+ }
88
+ },
89
+ }
90
+ }
91
+
92
+ export const kiloCodeWriters: Writer[] = ["Code", "Cursor", "Windsurf"].map(kiloCodeWriter)