@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,260 @@
1
+ import { createHash } from "node:crypto"
2
+ import { join, posix, resolve } from "node:path"
3
+ import { versionOf } from "../../fsutil.ts"
4
+ import type { Session, ToolKind, Turn } from "../../session/types.ts"
5
+ import { asText, clip, iso, lossCounter, toolKindOf } from "../../session/util.ts"
6
+ import { CONTINUED_NOTE } from "../claude-code/records.ts"
7
+
8
+ // What Kiro's V3 sessions look like, shared by the Kiro CLI and Kiro IDE adapters: the V3 engine (Kiro's
9
+ // agent server, @kiro/agent) keeps both products' sessions in one store. Checked against @kiro/agent 0.66.91
10
+ // as Kiro CLI 3.1.0 ships it: SessionPersistence (saveSession, loadSession, listSessions, the bucket list
11
+ // index), the record schemas (session.json and the messages.jsonl line union), messagesToContextMessages
12
+ // (what the model is sent) and convertMessage (what the chat shows). Catalogue:
13
+ // docs/sessions/2026-11-08-session-formats.md, "Kiro CLI" and "Kiro IDE".
14
+ //
15
+ // A session is ~/.kiro/sessions/<bucket>/<id>/: session.json (metadata, rewritten whole) and messages.jsonl
16
+ // (one {id, timestamp, payload} record per line, appended). The bucket is the first 16 hex of the SHA-256 of
17
+ // the session's sorted, normalized workspace folders; the id is sess_<uuid>. Kiro lists a workspace's
18
+ // sessions from its bucket, through an index (<bucket>/.index/index.json) that re-walks the bucket when the
19
+ // folder's mtime changes, so a session folder added from outside is listed like one of Kiro's own.
20
+
21
+ export const TESTED_VERSIONS = ["3.1.0"]
22
+ /** The agent engine those versions ship; the record shapes below are its own. */
23
+ export const ENGINE = "@kiro/agent 0.66.91"
24
+
25
+ export const sessionsDir = (home: string) => join(home, ".kiro", "sessions")
26
+
27
+ /** "3.1.0" from `kiro-cli --version` ("kiro-cli 3.1.0"); undefined when kiro-cli is missing. */
28
+ export const kiroCliVersion = (bin = "kiro-cli") => versionOf(bin)?.match(/\d+\.\d+\.\d+/)?.[0]
29
+
30
+ /** Kiro's normalizePath: absolute, forward slashes, POSIX-normalized, no trailing slash. */
31
+ function normalizePath(p: string): string {
32
+ let n = posix.normalize(resolve(p).replace(/\\/g, "/"))
33
+ if (n.length > 1) n = n.replace(/\/+$/, "") || n
34
+ return n
35
+ }
36
+
37
+ /** Kiro's computeWorkspaceHash: the bucket a session with these workspace folders lives in. */
38
+ export function workspaceHash(paths: string[]): string {
39
+ if (!paths.length) return "_global"
40
+ const key = paths.map(normalizePath).sort().join("\0")
41
+ return createHash("sha256").update(key).digest("hex").slice(0, 16)
42
+ }
43
+
44
+ /** Kiro's session id rule (isValidSessionId): it refuses to load anything else. */
45
+ export const validSessionId = (id: string) => id.length > 0 && id.length <= 128 && /^[A-Za-z0-9_-]+$/.test(id)
46
+
47
+ /** session.json: the fields Kiro requires, and the optional ones this adapter reads or writes. */
48
+ export interface KiroMetadata {
49
+ schemaVersion: string
50
+ dataModelVersion?: number
51
+ id: string
52
+ title: string
53
+ agentMode: string
54
+ workspacePaths: string[]
55
+ createdAt: string
56
+ lastModifiedAt: string
57
+ modelId?: string
58
+ parentSessionId?: string
59
+ status?: string
60
+ titleSetByUser?: boolean
61
+ [k: string]: unknown
62
+ }
63
+
64
+ /** One messages.jsonl line. */
65
+ export interface KiroRow {
66
+ id: string
67
+ timestamp: string
68
+ payload: Record<string, any> & { type: string }
69
+ }
70
+
71
+ /** Kiro's placeholder for reasoning it did not keep. */
72
+ const ELIDED_REASONING = "..."
73
+
74
+ /**
75
+ * What a Kiro tool does. The V3 built-ins are read_file, fs_write, fs_append, str_replace, delete_file,
76
+ * list_directory, execute_bash (execute_pwsh on Windows), grep_search, file_search, web_fetch and
77
+ * invoke_sub_agent; MCP tools are named after their server.
78
+ */
79
+ export function kiroToolKind(name: string): ToolKind {
80
+ switch (name) {
81
+ case "read_file":
82
+ return "read"
83
+ case "fs_write":
84
+ case "fs_append":
85
+ return "write"
86
+ case "str_replace":
87
+ case "delete_file":
88
+ return "edit"
89
+ case "list_directory":
90
+ return "list"
91
+ case "execute_bash":
92
+ case "execute_pwsh":
93
+ return "shell"
94
+ case "grep_search":
95
+ case "file_search":
96
+ return "search"
97
+ case "web_fetch":
98
+ return "web"
99
+ case "invoke_sub_agent":
100
+ return "task"
101
+ }
102
+ return toolKindOf(name)
103
+ }
104
+
105
+ const str = (v: unknown) => (typeof v === "string" ? v : undefined)
106
+
107
+ /** A call as Kiro's own built-in tool would have been called, when its arguments say what that tool needs. */
108
+ export function kiroCall(c: Extract<Turn, { kind: "tool_call" }>): { name: string; args: Record<string, unknown> } | undefined {
109
+ const i = (c.input && typeof c.input === "object" && !Array.isArray(c.input) ? c.input : {}) as Record<string, unknown>
110
+ const path = str(i.file_path) ?? str(i.path) ?? str(i.filePath) ?? str(i.target_file) ?? str(i.absolute_path)
111
+ switch (c.toolKind) {
112
+ case "shell": {
113
+ const command = str(i.command) ?? str(i.cmd) ?? (Array.isArray(i.command) ? (i.command as unknown[]).map(String).join(" ") : undefined)
114
+ return command ? { name: "execute_bash", args: { command } } : undefined
115
+ }
116
+ case "write": {
117
+ const text = str(i.content) ?? str(i.file_text) ?? str(i.contents) ?? str(i.text)
118
+ return path && text !== undefined ? { name: "fs_write", args: { path, text } } : undefined
119
+ }
120
+ case "edit": {
121
+ const oldStr = str(i.old_string) ?? str(i.old_str) ?? str(i.oldText) ?? str(i.oldStr)
122
+ const newStr = str(i.new_string) ?? str(i.new_str) ?? str(i.newText) ?? str(i.newStr)
123
+ return path && oldStr !== undefined && newStr !== undefined ? { name: "str_replace", args: { path, oldStr, newStr } } : undefined
124
+ }
125
+ case "read":
126
+ return path ? { name: "read_file", args: { path } } : undefined
127
+ }
128
+ return undefined
129
+ }
130
+
131
+ /** The chat's icon for a call (the tool_call record's `kind`). */
132
+ export function uiKind(k: ToolKind): string {
133
+ switch (k) {
134
+ case "read":
135
+ case "list":
136
+ return "read"
137
+ case "edit":
138
+ case "write":
139
+ return "edit"
140
+ case "shell":
141
+ return "execute"
142
+ case "search":
143
+ return "search"
144
+ case "web":
145
+ return "fetch"
146
+ }
147
+ return "other"
148
+ }
149
+
150
+ /**
151
+ * Applies checkpoint reverts the way Kiro resolves tombstones: a tombstone removes the rows from the one it
152
+ * names up to itself. Rows before a summarization tombstone are kept, since they stay on screen.
153
+ */
154
+ function revertCheckpoints(rows: KiroRow[]): KiroRow[] {
155
+ const out: KiroRow[] = []
156
+ for (const r of rows) {
157
+ if (r.payload.type !== "tombstone" || r.payload.kind !== "checkpoint_revert") {
158
+ out.push(r)
159
+ continue
160
+ }
161
+ // A tombstone whose target is gone is skipped, as Kiro skips it.
162
+ for (let at = out.length - 1; at >= 0; at--)
163
+ if (out[at]!.id === r.payload.effectiveFromMessageId) {
164
+ out.length = at
165
+ break
166
+ }
167
+ }
168
+ return out
169
+ }
170
+
171
+ /**
172
+ * The portable turns of a Kiro session. Everything the chat shows is history, including turns a compaction
173
+ * hid from the model; the compaction itself is its summary row. Returns the id of the session a brain-written
174
+ * session continues, when its note is there.
175
+ */
176
+ export function turnsOf(rows: KiroRow[], h: Session["header"]): { turns: Turn[]; continued?: string } {
177
+ const turns: Turn[] = []
178
+ const lost = lossCounter()
179
+ let continued: string | undefined
180
+ const calls = new Map<string, Extract<Turn, { kind: "tool_call" }>>()
181
+ const answered = new Set<string>()
182
+ for (const r of revertCheckpoints(rows)) {
183
+ const p = r.payload
184
+ const at = iso(r.timestamp)
185
+ const stamp = at ? { at } : {}
186
+ switch (p.type) {
187
+ case "user": {
188
+ // Prompts Kiro makes up itself (continuations, wake-ups) are not the person's words.
189
+ if (p._meta?.kiro && Object.hasOwn(p._meta.kiro, "syntheticUserMessageReason")) {
190
+ lost.add("Kiro's own follow-up prompts (not carried)")
191
+ break
192
+ }
193
+ if (Array.isArray(p.images) && p.images.length) lost.add("images", p.images.length)
194
+ if (Array.isArray(p.documents) && p.documents.length) lost.add("documents", p.documents.length)
195
+ const text = String(p.content ?? "")
196
+ if (text.trim()) turns.push({ kind: "user", ...stamp, text })
197
+ break
198
+ }
199
+ case "assistant": {
200
+ const text = String(p.content ?? "")
201
+ if (p.operationType === "Summary") {
202
+ if (text.trim()) turns.push({ kind: "compaction", ...stamp, summary: text })
203
+ } else if (p.operationType === "Reasoning") {
204
+ if (p.reasoningRedactedContent) lost.add("redacted reasoning")
205
+ else if (text.trim() && text !== ELIDED_REASONING) turns.push({ kind: "reasoning", ...stamp, text })
206
+ } else if (text.trim()) turns.push({ kind: "assistant", ...stamp, text, ...(h.models[0] ? { model: h.models[0] } : {}) })
207
+ break
208
+ }
209
+ case "tool_call": {
210
+ const id = String(p.toolCallId)
211
+ const args = p.args ?? {}
212
+ const seen = calls.get(id)
213
+ // Kiro writes a call again as its approval and status change; the last arguments are the ones that ran.
214
+ if (seen) seen.input = args
215
+ else {
216
+ const call = { kind: "tool_call" as const, ...stamp, id, name: String(p.toolName), toolKind: kiroToolKind(String(p.toolName)), input: args }
217
+ calls.set(id, call)
218
+ turns.push(call)
219
+ }
220
+ break
221
+ }
222
+ case "tool_result": {
223
+ const id = String(p.toolCallId)
224
+ if (answered.has(id) || p.metadata?.reverted === true) break
225
+ answered.add(id)
226
+ if (Array.isArray(p.images) && p.images.length) lost.add("images in tool results", p.images.length)
227
+ const c = clip(asText(p.content))
228
+ turns.push({ kind: "tool_result", ...stamp, callId: id, ok: p.success !== false, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
229
+ break
230
+ }
231
+ case "sub_agent_start": {
232
+ // The model sees a subagent as an invoke_sub_agent call answered by its final response.
233
+ const id = String(p.subSessionId)
234
+ if (calls.has(id)) break
235
+ const call = { kind: "tool_call" as const, ...stamp, id, name: "invoke_sub_agent", toolKind: "task" as const, input: { name: p.subAgentName, prompt: p.prompt } }
236
+ calls.set(id, call)
237
+ turns.push(call)
238
+ lost.add("subagent transcripts (their results stay in the parent's tool results)")
239
+ break
240
+ }
241
+ case "sub_agent_complete": {
242
+ const id = String(p.subSessionId)
243
+ if (answered.has(id) || !calls.has(id)) break
244
+ answered.add(id)
245
+ const c = clip(asText(p.response ?? p.errorMessage ?? ""))
246
+ turns.push({ kind: "tool_result", ...stamp, callId: id, ok: p.status === "success", output: c.text, ...(c.truncated ? { truncated: true } : {}) })
247
+ break
248
+ }
249
+ case "agent_note": {
250
+ const note = String(p.content ?? "").match(CONTINUED_NOTE)
251
+ if (note) continued = note[1]
252
+ else lost.add("Kiro agent notes (not carried)")
253
+ break
254
+ }
255
+ // Turn boundaries, usage, hooks, steering, approvals and session events are not conversation.
256
+ }
257
+ }
258
+ lost.flush(h)
259
+ return { turns, ...(continued ? { continued } : {}) }
260
+ }
@@ -0,0 +1,305 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import { mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs"
3
+ import { extname, join, resolve } from "node:path"
4
+ import { toolLabel } from "../../brief.ts"
5
+ import { exists, isDir } from "../../fsutil.ts"
6
+ import type { Session, Turn } from "../../session/types.ts"
7
+ import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
8
+ import { continuedNote } from "../claude-code/records.ts"
9
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
10
+ import { readKiroDir } from "./reader.ts"
11
+ import { type KiroMetadata, type KiroRow, TESTED_VERSIONS, kiroCall, kiroCliVersion, kiroToolKind, sessionsDir, uiKind, workspaceHash } from "./records.ts"
12
+
13
+ // Continues a portable session as a new Kiro V3 session: a folder in ~/.kiro/sessions/<bucket>/ holding the
14
+ // messages.jsonl and session.json that SessionPersistence.saveSession writes, with the records Kiro's own
15
+ // turns produce. Each user turn is a turn_start, the user record, the replies (assistant "Say" text, plain
16
+ // "Reasoning", tool_call and tool_result records paired by toolCallId) and a turn_end. Calls are mapped onto
17
+ // Kiro's built-ins (execute_bash, fs_write, str_replace, read_file) when their arguments carry what those
18
+ // need; others keep their names. A compaction is written as Kiro compacts: a summarization tombstone, which
19
+ // hides the turns before it from the model while the chat still shows them, then the summary row. The note
20
+ // saying where the session came from is an agent_note, which Kiro sends the model and never shows.
21
+ //
22
+ // Kiro writes nothing about a session into a shared database: it lists a workspace from its bucket folder,
23
+ // and its list index re-walks a bucket whose folder changed. So adding a folder needs Kiro neither closed nor
24
+ // told; only the new session's own folder is written. Readable reasoning is kept as Kiro keeps it, shown in
25
+ // the chat; Kiro sends the model only signed reasoning, so it never reaches the model.
26
+
27
+ const IMAGE_TYPES: Record<string, string> = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" }
28
+ const IMAGE_LIMIT = 5 * 1024 * 1024
29
+ const NO_RESULT = "No result was recorded for this call; the session moved before it finished."
30
+
31
+ type Call = Extract<Turn, { kind: "tool_call" }>
32
+
33
+ export interface KiroPlan {
34
+ id: string
35
+ bucket: string
36
+ metadata: KiroMetadata
37
+ rows: KiroRow[]
38
+ lost: string[]
39
+ /** What a read-back must find. */
40
+ expect: { users: number; assistants: number; calls: number; compactions: number }
41
+ }
42
+
43
+ /** The session folder's contents for `session` continued in `cwd`. Pure apart from reading attached images. */
44
+ export function planKiro(session: Session, opts: { cwd: string; now: Date; id?: string }): KiroPlan {
45
+ const id = opts.id ?? `sess_${randomUUID()}`
46
+ const label = toolLabel(session.header.tool)
47
+ const drops = new Map<string, number>()
48
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
49
+ const turns = session.turns
50
+ const lastCompaction = turns.map((t) => t.kind).lastIndexOf("compaction")
51
+ const expect = { users: 0, assistants: 0, calls: 0, compactions: 0 }
52
+ const results = new Map<string, boolean>()
53
+ for (const t of turns) if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t.ok)
54
+
55
+ // Tool call ids: Kiro's request builder takes [A-Za-z0-9_-], up to 64; others are mapped once.
56
+ const ids = new Map<string, string>()
57
+ const used = new Set<string>()
58
+ let remapped = 0
59
+ const callId = (raw: string) => {
60
+ let v = ids.get(raw)
61
+ if (v) return v
62
+ const base = /^[A-Za-z0-9_-]{1,64}$/.test(raw) ? raw : `call_${raw.replace(/[^A-Za-z0-9]/g, "").slice(-40) || "x"}`
63
+ v = base
64
+ for (let n = 2; used.has(v); n++) v = `${base}_${n}`
65
+ ids.set(raw, v)
66
+ used.add(v)
67
+ if (v !== raw) remapped++
68
+ return v
69
+ }
70
+
71
+ // Record times never go backwards; turns without one take the last one seen.
72
+ const known = turns.map((t) => (t.at ? Date.parse(t.at) : Number.NaN)).find(Number.isFinite)
73
+ let clock = known ?? opts.now.getTime()
74
+ const time = (at?: string) => {
75
+ const t = at ? Date.parse(at) : Number.NaN
76
+ if (Number.isFinite(t) && t > clock) clock = t
77
+ return new Date(clock).toISOString()
78
+ }
79
+
80
+ const rows: KiroRow[] = []
81
+ const push = (payload: KiroRow["payload"], at?: string, rowId: string = randomUUID()) => {
82
+ rows.push({ id: rowId, timestamp: time(at), payload })
83
+ return rowId
84
+ }
85
+ let turn: { id: string; replied: boolean } | undefined
86
+ const close = (at?: string) => {
87
+ if (!turn) return
88
+ push({ type: "turn_end", stopReason: turn.replied ? "end_turn" : "cancelled", executionId: turn.id, turnId: turn.id, boundarySource: "observed" }, at, `${turn.id}-turn-end`)
89
+ turn = undefined
90
+ }
91
+ const open = (at?: string) => {
92
+ turn = { id: randomUUID(), replied: false }
93
+ push({ type: "turn_start", executionId: turn.id, turnId: turn.id, boundarySource: "observed" }, at, `${turn.id}-turn-start`)
94
+ }
95
+ /** The current turn's identity, opening one for replies that come before any user turn. */
96
+ const inTurn = (at?: string) => {
97
+ if (!turn) open(at)
98
+ turn!.replied = true
99
+ return { executionId: turn!.id, turnId: turn!.id }
100
+ }
101
+
102
+ push({ type: "agent_note", content: continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)) }, turns[0]?.at, `note_${randomUUID()}`)
103
+ /** The first record after the note: where a compaction's tombstone starts hiding turns from the model. */
104
+ const firstAfterNote = () => rows[1]?.id
105
+ const written = new Set<string>()
106
+ const done = new Set<string>()
107
+ const result = (raw: string, ok: boolean, output: string, at?: string) =>
108
+ push({ type: "tool_result", toolCallId: callId(raw), content: output || "(no output)", success: ok, ...inTurn(at) }, at)
109
+
110
+ for (const [i, t] of turns.entries()) {
111
+ switch (t.kind) {
112
+ case "user": {
113
+ let text = t.text
114
+ const images: Array<{ data: string; mimeType: string }> = []
115
+ for (const a of t.attachments ?? []) {
116
+ if (!a.path) continue // bytes the reader could not keep; it already counted them
117
+ if (a.kind === "file") {
118
+ text += `\n\n[Attached file: ${a.path}]`
119
+ continue
120
+ }
121
+ const mime = a.mime ?? IMAGE_TYPES[extname(a.path).toLowerCase()]
122
+ let size = Number.POSITIVE_INFINITY
123
+ try {
124
+ size = statSync(a.path).size
125
+ } catch {}
126
+ if (mime && Object.values(IMAGE_TYPES).includes(mime) && size <= IMAGE_LIMIT) images.push({ data: readFileSync(a.path).toString("base64"), mimeType: mime })
127
+ else drop("images that could not be attached")
128
+ }
129
+ if (!text.trim() && !images.length) break
130
+ close(t.at)
131
+ open(t.at)
132
+ push({ type: "user", content: text.trim() ? text : "(image)", source: "chat", ...(images.length ? { images } : {}) }, t.at)
133
+ expect.users++
134
+ break
135
+ }
136
+ case "assistant":
137
+ if (!t.text.trim()) break
138
+ push({ type: "assistant", content: t.text, operationType: "Say", ...inTurn(t.at) }, t.at)
139
+ expect.assistants++
140
+ break
141
+ case "reasoning":
142
+ if (!t.text.trim() || t.text === "...") break
143
+ push({ type: "assistant", content: t.text, operationType: "Reasoning", ...inTurn(t.at) }, t.at)
144
+ break
145
+ case "tool_call": {
146
+ if (written.has(t.id)) {
147
+ drop("repeated tool call ids")
148
+ break
149
+ }
150
+ written.add(t.id)
151
+ const mapped = kiroCall(t as Call)
152
+ const name = mapped?.name ?? t.name
153
+ const args = mapped?.args ?? (t.input && typeof t.input === "object" && !Array.isArray(t.input) ? (t.input as Record<string, unknown>) : { input: t.input ?? null })
154
+ const ok = results.get(t.id)
155
+ push(
156
+ { type: "tool_call", toolCallId: callId(t.id), toolName: name, args, status: ok === false || ok === undefined ? "failed" : "completed", kind: uiKind(mapped ? kiroToolKind(name) : t.toolKind), ...inTurn(t.at) },
157
+ t.at,
158
+ )
159
+ expect.calls++
160
+ if (ok === undefined) {
161
+ result(t.id, false, NO_RESULT, t.at)
162
+ done.add(t.id)
163
+ drop("tool calls without a result (closed as errors)")
164
+ }
165
+ break
166
+ }
167
+ case "tool_result": {
168
+ if (!written.has(t.callId)) {
169
+ drop("tool results without a call")
170
+ break
171
+ }
172
+ if (done.has(t.callId)) {
173
+ drop("repeated tool results")
174
+ break
175
+ }
176
+ done.add(t.callId)
177
+ result(t.callId, t.ok, t.fullPath ? `${t.output}\n\n[Full output: ${t.fullPath}]` : t.output, t.at)
178
+ break
179
+ }
180
+ case "compaction": {
181
+ close(t.at)
182
+ const summary = `[Summary of the earlier conversation in ${label}, which replaced the turns before it]\n\n${t.summary}`
183
+ const from = firstAfterNote()
184
+ // Only the last compaction hides turns: Kiro resolves a tombstone against the rows still in effect.
185
+ if (i === lastCompaction && from)
186
+ push({ type: "tombstone", kind: "summarization", effectiveFromMessageId: from, metadata: { truncatedMessageCount: rows.length - 1, truncatedAt: time(t.at) } }, t.at, `tombstone_summarize_${randomUUID()}`)
187
+ push({ type: "assistant", content: summary, operationType: "Summary" }, t.at, `summary_${randomUUID()}`)
188
+ expect.compactions++
189
+ break
190
+ }
191
+ case "file_edit":
192
+ if (!t.callId || !written.has(t.callId)) drop("file edits outside a tool call")
193
+ break
194
+ case "subagent":
195
+ drop("subagent transcripts (their results stay in the parent's tool results)")
196
+ break
197
+ }
198
+ }
199
+ close()
200
+
201
+ const first = rows[0]?.timestamp ?? opts.now.toISOString()
202
+ const title = session.header.title ?? firstUserText(session)?.replace(/\s+/g, " ").trim().slice(0, 60) ?? "Session"
203
+ const metadata: KiroMetadata = {
204
+ schemaVersion: "1.0.0",
205
+ dataModelVersion: 1,
206
+ id,
207
+ title: `${title} (from ${label})`,
208
+ agentMode: "vibe",
209
+ workspacePaths: [opts.cwd],
210
+ createdAt: first,
211
+ lastModifiedAt: opts.now.toISOString(),
212
+ status: "idle",
213
+ titleSetByUser: true,
214
+ }
215
+ const lost = [...drops].map(([what, n]) => `${what} (${n})`)
216
+ if (remapped) lost.push(`tool call ids, remapped to ids Kiro accepts (${remapped})`)
217
+ return { id, bucket: workspaceHash([opts.cwd]), metadata, rows, lost, expect }
218
+ }
219
+
220
+ /** The kiro-cli binary: the installer's ~/.local/bin/kiro-cli first, else whatever is on PATH. */
221
+ function resolveBin(home: string): { path: string; command: string } {
222
+ const own = join(home, ".local", "bin", "kiro-cli")
223
+ if (exists(own)) return { path: own, command: Bun.which("kiro-cli") === own ? "kiro-cli" : own }
224
+ return { path: "kiro-cli", command: "kiro-cli" }
225
+ }
226
+
227
+ /**
228
+ * The folder as Kiro CLI reports it to its engine (the process's current directory, so with symlinks
229
+ * resolved: /tmp is /private/tmp on macOS); the session's bucket is keyed by it.
230
+ */
231
+ export function realCwd(cwd: string): string {
232
+ try {
233
+ return realpathSync(cwd)
234
+ } catch {
235
+ return resolve(cwd)
236
+ }
237
+ }
238
+
239
+ export const sh = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
240
+
241
+ export interface KiroTarget {
242
+ tool: string
243
+ label: string
244
+ ready(w: WriteOptions): Promise<Readiness>
245
+ /** How the person opens the new session: a command, and what to do after it. */
246
+ open(w: { home: string; cwd: string; id: string; title: string }): { open: string; note?: string }
247
+ }
248
+
249
+ /** A writer into the shared store for one Kiro product. */
250
+ export function kiroWriter(target: KiroTarget): Writer {
251
+ return {
252
+ tool: target.tool,
253
+ label: target.label,
254
+ way: "native",
255
+ ready: (w) => target.ready(w),
256
+
257
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
258
+ const cwd = realCwd(w.cwd)
259
+ const plan = planKiro(session, { cwd, now: new Date() })
260
+ const dir = join(sessionsDir(w.home), plan.bucket, plan.id)
261
+ const files = [join(dir, "messages.jsonl"), join(dir, "session.json")]
262
+ const how = target.open({ home: w.home, cwd, id: plan.id, title: plan.metadata.title })
263
+ const result = { tool: target.tool, way: "native" as const, sessionId: plan.id, written: files, ...how, lost: plan.lost }
264
+ if (w.dryRun) return { ...result, dryRun: true }
265
+
266
+ // Kiro's own modes: the folders private to the person, the files too. The transcript goes first, as
267
+ // Kiro saves it, so a folder with a session.json is always complete.
268
+ mkdirSync(dir, { recursive: true, mode: 0o700 })
269
+ writeFileSync(files[0]!, `${plan.rows.map((r) => JSON.stringify(r)).join("\n")}\n`, { flag: "wx", mode: 0o600 })
270
+ writeFileSync(files[1]!, JSON.stringify(plan.metadata, null, 2), { flag: "wx", mode: 0o600 })
271
+
272
+ // Read it back as the reader reads Kiro; a session that does not round-trip is removed again.
273
+ const back = readKiroDir(dir, w.home, target.tool)
274
+ const n = countTurns(back)
275
+ const pairs = unpaired(back)
276
+ const e = plan.expect
277
+ if (n.user !== e.users || n.assistant !== e.assistants || n.tool_call !== e.calls || n.compaction !== e.compactions || pairs.calls.length || pairs.results.length) {
278
+ rmSync(dir, { recursive: true, force: true })
279
+ throw new Error(
280
+ `the new Kiro session did not read back as written (users ${n.user}/${e.users}, replies ${n.assistant}/${e.assistants}, tool calls ${n.tool_call}/${e.calls}, summaries ${n.compaction}/${e.compactions}, unpaired ${pairs.calls.length + pairs.results.length}); it was removed`,
281
+ )
282
+ }
283
+ return { ...result, dryRun: false }
284
+ },
285
+ }
286
+ }
287
+
288
+ /** Kiro CLI: its V3 engine (the default since 3.0) resumes the session by id from the folder it ran in. */
289
+ export function kiroCliWriter(opts: { bin?: string; version?: string } = {}): Writer {
290
+ const binFor = (home: string) => (opts.bin ? { path: opts.bin, command: opts.bin } : resolveBin(home))
291
+ return kiroWriter({
292
+ tool: "kiro-cli",
293
+ label: "Kiro CLI",
294
+ async ready(w) {
295
+ const v = opts.version ?? kiroCliVersion(binFor(w.home).path)
296
+ if (!v) return { ok: false, why: "Kiro CLI is not installed (`kiro-cli --version` did not run)" }
297
+ if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Kiro CLI ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
298
+ if (!isDir(w.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${w.cwd}` }
299
+ return { ok: true }
300
+ },
301
+ open: ({ home, cwd, id }) => ({ open: `cd ${sh(cwd)} && ${sh(binFor(home).command)} chat --resume-id ${id}` }),
302
+ })
303
+ }
304
+
305
+ export const kiroCliNativeWriter = kiroCliWriter()
@@ -0,0 +1,8 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { kiroIdeNativeWriter } from "./writer.ts"
3
+
4
+ // Kiro IDE: continues any session as a new chat in Kiro's V3 session store. The IDE shares that store with
5
+ // Kiro CLI, whose reader (kiro-cli) lists and reads both products' sessions; a second reader here would list
6
+ // every session twice, since a session does not record which product made it.
7
+ export const readers: Reader[] = []
8
+ export const writers: Writer[] = [kiroIdeNativeWriter]
@@ -0,0 +1,52 @@
1
+ import { join } from "node:path"
2
+ import { exists, isDir } from "../../fsutil.ts"
3
+ import type { Writer } from "../types.ts"
4
+ import { kiroWriter, sh } from "../kiro-cli/writer.ts"
5
+ import { ENGINE } from "../kiro-cli/records.ts"
6
+
7
+ // Continues a portable session in Kiro IDE. Since 1.0 the IDE keeps its chats in the same V3 store as Kiro
8
+ // CLI (~/.kiro/sessions/<bucket>/<id>/, the bucket keyed by the workspace's folders), so the session is
9
+ // written exactly as for the CLI and appears in the IDE's chat history for that folder.
10
+ //
11
+ // The shape was checked against the agent engine Kiro CLI 3.1.0 ships (see ENGINE), not against an IDE: no
12
+ // Kiro IDE was available to check which engine it bundles. So no IDE version is listed as tested yet, and
13
+ // ready() says so; add a version here once a session written by this writer has been opened in it.
14
+
15
+ export const TESTED_IDE_VERSIONS: string[] = []
16
+
17
+ /** Where Kiro IDE is installed, if it is: the system Applications folder or the person's own. */
18
+ export function kiroApp(home: string): string | undefined {
19
+ return ["/Applications/Kiro.app", join(home, "Applications", "Kiro.app")].find((p) => isDir(p))
20
+ }
21
+
22
+ /** The app's CFBundleShortVersionString, e.g. "1.4.121". */
23
+ export function appVersion(app: string): string | undefined {
24
+ const plist = join(app, "Contents", "Info.plist")
25
+ if (!exists(plist)) return undefined
26
+ const p = Bun.spawnSync(["plutil", "-extract", "CFBundleShortVersionString", "raw", plist], { stdout: "pipe", stderr: "pipe" })
27
+ return p.exitCode === 0 ? p.stdout.toString().trim() || undefined : undefined
28
+ }
29
+
30
+ /** The Kiro IDE writer; `app` and `version` are for tests, which must not depend on an installed Kiro. */
31
+ export function kiroIdeWriter(opts: { app?: string; version?: string; tested?: string[] } = {}): Writer {
32
+ const tested = opts.tested ?? TESTED_IDE_VERSIONS
33
+ return kiroWriter({
34
+ tool: "kiro-ide",
35
+ label: "Kiro IDE",
36
+ async ready(w) {
37
+ const app = opts.app ?? kiroApp(w.home)
38
+ if (!app || !isDir(app)) return { ok: false, why: "Kiro IDE is not installed (no Kiro.app in /Applications or ~/Applications)" }
39
+ const v = opts.version ?? appVersion(app)
40
+ if (!v || !tested.includes(v))
41
+ return {
42
+ ok: false,
43
+ why: `Kiro IDE ${v ?? "(unknown version)"} is not a version this writer was checked against (${tested.join(", ") || `none yet; the format was checked against ${ENGINE} in Kiro CLI 3.1.0`})`,
44
+ }
45
+ if (!isDir(w.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${w.cwd}` }
46
+ return { ok: true }
47
+ },
48
+ open: ({ cwd, title }) => ({ open: `open -a Kiro ${sh(cwd)}`, note: `In Kiro's chat, open the session history and pick "${title}".` }),
49
+ })
50
+ }
51
+
52
+ export const kiroIdeNativeWriter = kiroIdeWriter()
@@ -0,0 +1,8 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { lettaReader } from "./reader.ts"
3
+ import { lettaNativeWriter } from "./writer.ts"
4
+
5
+ // Letta Code: reads and writes conversations of the local backend's agents (~/.letta/lc-local-backend).
6
+ // Server ("cloud") agents keep their history on the Letta server and are not on disk.
7
+ export const readers: Reader[] = [lettaReader]
8
+ export const writers: Writer[] = [lettaNativeWriter]