@100xprompt/brain 0.0.0-stage → 0.1.1

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 (260) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +235 -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 +351 -0
  170. package/src/dashboard/cli.ts +148 -0
  171. package/src/dashboard/conversions.ts +170 -0
  172. package/src/dashboard/report.ts +261 -0
  173. package/src/dashboard/send.ts +213 -0
  174. package/src/dashboard/state.ts +95 -0
  175. package/src/fsutil.ts +133 -0
  176. package/src/gateway/common.ts +232 -0
  177. package/src/gateway/connect.ts +847 -0
  178. package/src/gateway/connectors/aider.ts +356 -0
  179. package/src/gateway/connectors/amazon-q-cli.ts +6 -0
  180. package/src/gateway/connectors/amazon-q-ide.ts +6 -0
  181. package/src/gateway/connectors/antigravity.ts +6 -0
  182. package/src/gateway/connectors/auggie.ts +6 -0
  183. package/src/gateway/connectors/cline-sdk.ts +85 -0
  184. package/src/gateway/connectors/cline.ts +34 -0
  185. package/src/gateway/connectors/codebuff.ts +6 -0
  186. package/src/gateway/connectors/continue.ts +579 -0
  187. package/src/gateway/connectors/copilot-cli.ts +138 -0
  188. package/src/gateway/connectors/crush.ts +313 -0
  189. package/src/gateway/connectors/cursor-cli.ts +6 -0
  190. package/src/gateway/connectors/cursor.ts +6 -0
  191. package/src/gateway/connectors/devin.ts +6 -0
  192. package/src/gateway/connectors/factory-droid.ts +308 -0
  193. package/src/gateway/connectors/goose.ts +79 -0
  194. package/src/gateway/connectors/grok-cli.ts +82 -0
  195. package/src/gateway/connectors/hermes.ts +230 -0
  196. package/src/gateway/connectors/iflow-cli.ts +50 -0
  197. package/src/gateway/connectors/index.ts +86 -0
  198. package/src/gateway/connectors/kilo-cli.ts +434 -0
  199. package/src/gateway/connectors/kilo-code.ts +32 -0
  200. package/src/gateway/connectors/kimi-code.ts +241 -0
  201. package/src/gateway/connectors/kiro-cli.ts +6 -0
  202. package/src/gateway/connectors/kiro-ide.ts +6 -0
  203. package/src/gateway/connectors/letta-code.ts +170 -0
  204. package/src/gateway/connectors/mastracode.ts +91 -0
  205. package/src/gateway/connectors/mistral-vibe.ts +495 -0
  206. package/src/gateway/connectors/muse.ts +27 -0
  207. package/src/gateway/connectors/omp.ts +279 -0
  208. package/src/gateway/connectors/openhands.ts +296 -0
  209. package/src/gateway/connectors/pearai.ts +52 -0
  210. package/src/gateway/connectors/pi.ts +357 -0
  211. package/src/gateway/connectors/qoder-cli.ts +6 -0
  212. package/src/gateway/connectors/qwen-code.ts +411 -0
  213. package/src/gateway/connectors/roo.ts +43 -0
  214. package/src/gateway/connectors/tabnine-cli.ts +6 -0
  215. package/src/gateway/connectors/void.ts +6 -0
  216. package/src/gateway/connectors/vscode-copilot.ts +286 -0
  217. package/src/gateway/connectors/windsurf.ts +6 -0
  218. package/src/gateway/connectors/zed.ts +29 -0
  219. package/src/gateway/signin.ts +323 -0
  220. package/src/handoff.ts +104 -0
  221. package/src/hooks.ts +455 -0
  222. package/src/memory/audit.ts +514 -0
  223. package/src/memory/blobs.ts +94 -0
  224. package/src/memory/cli.ts +117 -0
  225. package/src/memory/distill.ts +381 -0
  226. package/src/memory/externalize.ts +435 -0
  227. package/src/memory/lookups.ts +70 -0
  228. package/src/memory/mcp-install.ts +401 -0
  229. package/src/memory/mcp-server.ts +296 -0
  230. package/src/memory/prepare.ts +179 -0
  231. package/src/memory/preview.ts +154 -0
  232. package/src/memory/project.ts +35 -0
  233. package/src/memory/redact.ts +72 -0
  234. package/src/memory/relevance.ts +187 -0
  235. package/src/memory/repomap.ts +516 -0
  236. package/src/memory/store.ts +270 -0
  237. package/src/memory/sync.ts +166 -0
  238. package/src/memory/tidy.ts +193 -0
  239. package/src/memory/types.ts +118 -0
  240. package/src/org/admin-ops.ts +96 -0
  241. package/src/org/auth.ts +112 -0
  242. package/src/org/backup.ts +115 -0
  243. package/src/org/cli.ts +438 -0
  244. package/src/org/client.ts +404 -0
  245. package/src/org/config.ts +44 -0
  246. package/src/org/crypto.ts +537 -0
  247. package/src/org/db.ts +435 -0
  248. package/src/org/graph.ts +318 -0
  249. package/src/org/oidc.ts +435 -0
  250. package/src/org/policy.ts +398 -0
  251. package/src/org/server.ts +920 -0
  252. package/src/org/sso.ts +316 -0
  253. package/src/org/tls.ts +66 -0
  254. package/src/org/types.ts +193 -0
  255. package/src/self.ts +68 -0
  256. package/src/session/attachments.ts +53 -0
  257. package/src/session/peekcache.ts +50 -0
  258. package/src/session/types.ts +104 -0
  259. package/src/session/util.ts +130 -0
  260. package/src/sqlite.ts +48 -0
@@ -0,0 +1,177 @@
1
+ import { join } from "node:path"
2
+ import { exists, gitInfo, readJson, readJsonl } from "../../fsutil.ts"
3
+ import type { Session, Turn, Usage } from "../../session/types.ts"
4
+ import { clip, header, iso, lossCounter, noteModel } from "../../session/util.ts"
5
+ import type { Reader, SessionRef } from "../types.ts"
6
+ import { CONTINUED_NOTE, LABEL, MANIFEST, REMINDER, TOOL, conversationDirs, lettaToolKind } from "./records.ts"
7
+
8
+ // Letta Code's local-backend conversations (records.ts has the layout). Every conversation that has a
9
+ // transcript is listed, the agents' default conversations included; hidden ones (subagents, workflow
10
+ // workers) are not. A message Letta rewrote is read as its last snapshot, where it first appeared. A
11
+ // compaction turn goes where the model's context starts over (before the first message it kept), with the
12
+ // turns it replaced kept in front of it. Server-side ("cloud") Letta conversations are not on disk.
13
+
14
+ type Json = Record<string, any>
15
+ type Lost = ReturnType<typeof lossCounter>
16
+
17
+ /** A user message's words: its text parts without the system reminders Letta (or brain) put there. */
18
+ function userText(content: unknown, lost: Lost): { text: string; reminders: string[] } {
19
+ const parts: string[] = []
20
+ const reminders: string[] = []
21
+ for (const c of typeof content === "string" ? [{ type: "text", text: content }] : Array.isArray(content) ? content : []) {
22
+ if (c?.type === "text" && typeof c.text === "string") {
23
+ reminders.push(...(c.text.match(REMINDER) ?? []))
24
+ const t = c.text.replace(REMINDER, "").trim()
25
+ if (t) parts.push(t)
26
+ } else if (c?.type === "image") lost.add("images")
27
+ }
28
+ return { text: parts.join("\n\n"), reminders }
29
+ }
30
+
31
+ const usageOf = (u: Json | undefined): Usage | undefined => {
32
+ if (!u || typeof u !== "object") return undefined
33
+ const out: Usage = {}
34
+ if (u.input) out.input = u.input
35
+ if (u.output) out.output = u.output
36
+ if (u.cacheRead) out.cacheRead = u.cacheRead
37
+ if (u.cacheWrite) out.cacheWrite = u.cacheWrite
38
+ return Object.keys(out).length ? out : undefined
39
+ }
40
+
41
+ /** A packed summary's words: Letta stores the summary itself on the compaction entry and in metadata. */
42
+ const summaryOf = (entry: Json) =>
43
+ typeof entry.summary === "string" ? entry.summary : typeof entry.message?.metadata?.compaction?.summary === "string" ? entry.message.metadata.compaction.summary : ""
44
+
45
+ export const lettaReader: Reader = {
46
+ tool: TOOL,
47
+ label: LABEL,
48
+ async list(home) {
49
+ const refs: SessionRef[] = []
50
+ for (const { dir, key, record } of conversationDirs(home)) {
51
+ if (!record?.id || record.hidden === true || record.agent_free === true || record.is_subagent === true) continue
52
+ const transcript = join(dir, "messages.jsonl")
53
+ if (!exists(transcript)) continue
54
+ const head = readJsonl(transcript).rows[0]
55
+ const cwd = head?.type === "session" && typeof head.cwd === "string" ? head.cwd : undefined
56
+ const updated = iso(record.last_message_at) ?? iso(record.updated_at)
57
+ refs.push({
58
+ tool: TOOL,
59
+ sourceId: key,
60
+ path: dir,
61
+ title: typeof record.summary === "string" && record.summary ? record.summary : record.id === "default" ? `Default conversation of ${record.agent_id}` : String(record.id),
62
+ ...(cwd ? { cwd } : {}),
63
+ ...(updated ? { updatedAt: updated } : {}),
64
+ extra: { agentId: record.agent_id, conversationId: record.id },
65
+ })
66
+ }
67
+ return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
68
+ },
69
+ async read(ref, home) {
70
+ const dir = ref.path
71
+ const record = readJson<Json>(join(dir, "conversation.json"))
72
+ if (!record) throw new Error(`${dir} has no conversation.json`)
73
+ const manifest = readJson<Json>(join(dir, "manifest.json"))
74
+ if (manifest?.message_format !== MANIFEST.message_format)
75
+ throw new Error(`${dir} is in Letta's transcript format ${String(manifest?.message_format ?? "without a manifest")}; run \`letta local-backend migrate-transcripts\` first`)
76
+ const { rows, bad } = readJsonl(join(dir, "messages.jsonl"))
77
+ return parseTranscript(rows, { key: ref.sourceId, dir, record, home, bad })
78
+ },
79
+ }
80
+
81
+ /** A conversation's transcript rows as a portable session. */
82
+ export function parseTranscript(rows: Json[], ctx: { key: string; dir: string; record: Json; home: string; bad?: number }): Session {
83
+ const head = rows[0]?.type === "session" ? rows[0] : undefined
84
+ const cwd = typeof head?.cwd === "string" && head.cwd ? head.cwd : undefined
85
+ const h = header(TOOL, ctx.key, {
86
+ sourcePath: join(ctx.dir, "messages.jsonl"),
87
+ ...(typeof ctx.record.summary === "string" && ctx.record.summary ? { title: ctx.record.summary } : {}),
88
+ project: cwd ? { cwd, ...gitInfo(cwd, ctx.home) } : {},
89
+ ...(iso(ctx.record.created_at) ?? iso(head?.timestamp) ? { startedAt: iso(ctx.record.created_at) ?? iso(head?.timestamp) } : {}),
90
+ ...(iso(ctx.record.last_message_at) ?? iso(ctx.record.updated_at) ? { updatedAt: iso(ctx.record.last_message_at) ?? iso(ctx.record.updated_at) } : {}),
91
+ })
92
+ const lost = lossCounter()
93
+ if (ctx.bad) lost.add("unreadable transcript lines", ctx.bad)
94
+
95
+ // The last snapshot of each message, kept where it first appeared; compactions placed before what they kept.
96
+ const latest = new Map<string, Json>()
97
+ const order: Json[] = []
98
+ const entryIndex = new Map<string, number>()
99
+ for (const r of rows) {
100
+ if ((r.type !== "message" && r.type !== "compaction") || typeof r.id !== "string" || !r.message || typeof r.message.id !== "string") continue
101
+ if (r.type === "message") {
102
+ if (!latest.has(r.message.id)) {
103
+ entryIndex.set(r.id, order.length)
104
+ order.push(r)
105
+ } else entryIndex.set(r.id, order.findIndex((o) => o.type === "message" && o.message.id === r.message.id))
106
+ latest.set(r.message.id, r.message)
107
+ } else order.push(r)
108
+ }
109
+ const before = new Map<number, Json[]>()
110
+ const placed = new Set<Json>()
111
+ for (const r of order) {
112
+ if (r.type !== "compaction" || typeof r.firstKeptEntryId !== "string") continue
113
+ const k = entryIndex.get(r.firstKeptEntryId)
114
+ if (k === undefined || order[k] === r) continue
115
+ before.set(k, [...(before.get(k) ?? []), r])
116
+ placed.add(r)
117
+ }
118
+
119
+ const turns: Turn[] = []
120
+ const compaction = (r: Json) => {
121
+ const summary = summaryOf(r)
122
+ if (summary.trim()) turns.push({ kind: "compaction", ...(iso(r.timestamp) ? { at: iso(r.timestamp)! } : {}), summary })
123
+ }
124
+ const summaries = new Set(order.filter((r) => r.type === "compaction").map((r) => r.message.id as string))
125
+ for (const [i, r] of order.entries()) {
126
+ for (const c of before.get(i) ?? []) compaction(c)
127
+ if (r.type === "compaction") {
128
+ if (!placed.has(r)) compaction(r)
129
+ continue
130
+ }
131
+ const m = latest.get(r.message.id)!
132
+ if (summaries.has(m.id)) continue // the compaction entry carries it
133
+ const at = iso(m.metadata?.created_at) ?? iso(r.timestamp)
134
+ const when = at ? { at } : {}
135
+ if (m.role === "user") {
136
+ if (m.metadata?.compaction) {
137
+ compaction({ summary: m.metadata.compaction.summary, timestamp: at })
138
+ continue
139
+ }
140
+ const { text, reminders } = userText(m.content, lost)
141
+ for (const rem of reminders) {
142
+ const continued = rem.match(CONTINUED_NOTE)
143
+ if (continued?.[1] && !h.parent) h.parent = { id: continued[1], kind: "continued" }
144
+ }
145
+ if (text) turns.push({ kind: "user", ...when, text })
146
+ } else if (m.role === "assistant") {
147
+ const model = typeof m.model === "string" && m.model ? m.model : undefined
148
+ noteModel(h, model)
149
+ const usage = usageOf(m.usage)
150
+ let usageGiven = false
151
+ for (const c of Array.isArray(m.content) ? m.content : []) {
152
+ if (c?.type === "text" && typeof c.text === "string") {
153
+ if (!c.text.trim()) continue
154
+ turns.push({ kind: "assistant", ...when, text: c.text, ...(model ? { model } : {}), ...(usage && !usageGiven ? { usage } : {}) })
155
+ usageGiven = true
156
+ } else if (c?.type === "thinking") {
157
+ if (c.redacted) lost.add("redacted reasoning")
158
+ else if (typeof c.thinking === "string" && c.thinking.trim()) turns.push({ kind: "reasoning", ...when, text: c.thinking })
159
+ } else if (c?.type === "toolCall" && typeof c.id === "string") {
160
+ const name = String(c.name ?? "tool")
161
+ turns.push({ kind: "tool_call", ...when, id: c.id, name, toolKind: lettaToolKind(name), input: c.arguments ?? {} })
162
+ } else lost.add("unknown assistant content")
163
+ }
164
+ if (m.stopReason === "error" || m.stopReason === "aborted") lost.add("failed or aborted model responses")
165
+ } else if (m.role === "toolResult") {
166
+ const parts: string[] = []
167
+ for (const c of Array.isArray(m.content) ? m.content : []) {
168
+ if (c?.type === "text" && typeof c.text === "string") parts.push(c.text)
169
+ else if (c?.type === "image") lost.add("images in tool results")
170
+ }
171
+ const out = clip(parts.join("\n"))
172
+ turns.push({ kind: "tool_result", ...when, callId: String(m.toolCallId), ok: m.isError !== true, output: out.text, ...(out.truncated ? { truncated: true } : {}) })
173
+ } else lost.add("unknown message roles")
174
+ }
175
+ lost.flush(h)
176
+ return { header: h, turns }
177
+ }
@@ -0,0 +1,119 @@
1
+ import { readdirSync, statSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { readJson } from "../../fsutil.ts"
4
+ import type { ToolKind } from "../../session/types.ts"
5
+ import { toolKindOf } from "../../session/util.ts"
6
+
7
+ // What Letta Code's local backend stores, as far as this adapter relies on it. Checked against letta-code's
8
+ // source at 0.34.44 (letta-ai/letta-code 9176d8c, 2026-11-06): src/backend/local/local-store.ts (LocalStore:
9
+ // conversation records, ids, fork, compaction), local-transcript.ts (the session-entry JSONL and its manifest),
10
+ // local-message.ts (pi-ai messages with an id and metadata), local-conversation-record.ts,
11
+ // local-conversation-list.ts and src/utils/local-backend-paths.ts. Catalogue entry 16.
12
+ //
13
+ // Letta keeps its default ("cloud") agents and conversations on a Letta server, never on disk. Only the
14
+ // local backend (`letta --backend local`, agent ids `agent-local-*`) is a store a writer can reach:
15
+ //
16
+ // ~/.letta/lc-local-backend/agents/<base64url(agentId)>.json the agent (system prompt, model, tags)
17
+ // ~/.letta/lc-local-backend/memfs/<agentId>/memory/ its memory, a git repo
18
+ // ~/.letta/lc-local-backend/conversations/<base64url(key)>/ one conversation, key below
19
+ // conversation.json {id, agent_id, summary (the title /resume shows), in_context_message_ids, times}
20
+ // manifest.json {schema_version 2, message_format "pi-session-entry-jsonl", provider_stack "pi-ai"}
21
+ // messages.jsonl a {type:"session", version:3} header, then message and compaction entries on one
22
+ // parentId chain; each holds a pi-ai message (user, assistant, toolResult) with an id
23
+ // system-prompt.json the compiled system prompt, written by Letta on the first turn
24
+ //
25
+ // The key is "default:<agentId>" for an agent's default conversation and "conversation:<id>" for the others;
26
+ // new conversation ids are local-conv-<n> and message ids ui-msg-<n>. Only the messages whose ids are in
27
+ // in_context_message_ids go to the model; a compaction entry's message is the summary that replaces the rest.
28
+
29
+ export const TOOL = "letta-code"
30
+ export const LABEL = "Letta Code"
31
+
32
+ /** Letta Code releases whose local store was checked in source; the writer was run against 0.34.44. */
33
+ export const TESTED_VERSIONS = ["0.34.44"]
34
+
35
+ export const MANIFEST = { schema_version: 2, message_format: "pi-session-entry-jsonl", provider_stack: "pi-ai" } as const
36
+ export const SESSION_HEADER_VERSION = 3
37
+
38
+ export const storageDir = (home: string) => join(home, ".letta", "lc-local-backend")
39
+ export const conversationsDir = (home: string) => join(storageDir(home), "conversations")
40
+ export const agentsDir = (home: string) => join(storageDir(home), "agents")
41
+
42
+ /** LocalStore's encodePathSegment. */
43
+ export const encodeSegment = (v: string) => Buffer.from(v).toString("base64url")
44
+ export const decodeSegment = (v: string) => Buffer.from(v, "base64url").toString("utf8")
45
+
46
+ /** localConversationKey. */
47
+ export const conversationKey = (conversationId: string, agentId: string) =>
48
+ conversationId === "default" ? `default:${agentId}` : `conversation:${conversationId}`
49
+
50
+ export const isLocalAgentId = (id: unknown): id is string => typeof id === "string" && id.startsWith("agent-local-")
51
+
52
+ /** Letta's own tools (src/tools/toolset-catalog.ts), where the name alone does not say what they do. */
53
+ const KINDS: Record<string, ToolKind> = {
54
+ Bash: "shell",
55
+ exec_command: "shell",
56
+ write_stdin: "shell",
57
+ Monitor: "shell",
58
+ Read: "read",
59
+ ViewImage: "read",
60
+ Edit: "edit",
61
+ ApplyPatch: "edit",
62
+ Write: "write",
63
+ Task: "task",
64
+ SendAgentMessage: "task",
65
+ TaskCreate: "todo",
66
+ TaskGet: "todo",
67
+ TaskList: "todo",
68
+ TaskUpdate: "todo",
69
+ UpdatePlan: "todo",
70
+ }
71
+ export const lettaToolKind = (name: string): ToolKind => KINDS[name] ?? toolKindOf(name)
72
+
73
+ /** Letta hides these from the person (backfill.ts removeSystemContextBlocks) but sends them to the model. */
74
+ export const REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>|<system-alert>[\s\S]*?<\/system-alert>/g
75
+
76
+ /** The note saying where a conversation came from, carried in a system reminder so it is not the person's words. */
77
+ export const continuedNote = (label: string, id: string, when: string) =>
78
+ `<system-reminder>\nThis conversation continues ${label} session ${id}, carried over by brain session on ${when}. The messages before this one are its history; reasoning and anything listed as lost did not carry over. Check files before relying on details, since they may have changed.\n</system-reminder>`
79
+ export const CONTINUED_NOTE = /This conversation continues .+? session (\S+), carried over by brain session/
80
+
81
+ /** The local agents on disk, newest first: id, name, and whether Letta hides it (subagents, workers). */
82
+ export function localAgents(home: string): Array<{ id: string; name: string; hidden: boolean; mtimeMs: number }> {
83
+ const dir = agentsDir(home)
84
+ let names: string[]
85
+ try {
86
+ names = readdirSync(dir).filter((n) => n.endsWith(".json") && !n.startsWith("._"))
87
+ } catch {
88
+ return []
89
+ }
90
+ const out: Array<{ id: string; name: string; hidden: boolean; mtimeMs: number }> = []
91
+ for (const n of names) {
92
+ const rec = readJson<Record<string, unknown>>(join(dir, n))
93
+ if (!rec || !isLocalAgentId(rec.id)) continue
94
+ let mtimeMs = 0
95
+ try {
96
+ mtimeMs = statSync(join(dir, n)).mtimeMs
97
+ } catch {}
98
+ out.push({ id: rec.id, name: typeof rec.name === "string" ? rec.name : rec.id, hidden: rec.hidden === true, mtimeMs })
99
+ }
100
+ return out.sort((a, b) => b.mtimeMs - a.mtimeMs)
101
+ }
102
+
103
+ /** Every conversation folder: its decoded key and its record, when the record parses. */
104
+ export function conversationDirs(home: string): Array<{ dir: string; key: string; record?: Record<string, any> }> {
105
+ const root = conversationsDir(home)
106
+ let names: string[]
107
+ try {
108
+ names = readdirSync(root)
109
+ } catch {
110
+ return []
111
+ }
112
+ return names
113
+ .filter((n) => !n.startsWith("."))
114
+ .map((n) => ({ dir: join(root, n), key: decodeSegment(n), record: readJson<Record<string, any>>(join(root, n, "conversation.json")) }))
115
+ }
116
+
117
+ /** The number after `prefix` in `id`, 0 when there is none (LocalStore's numericSuffix). */
118
+ export const numericSuffix = (id: unknown, prefix: string) =>
119
+ typeof id === "string" && id.startsWith(prefix) ? Number.parseInt(id.slice(prefix.length), 10) || 0 : 0