@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,332 @@
1
+ import { statSync } from "node:fs"
2
+ import { basename, dirname, join } from "node:path"
3
+ import { exists, gitInfo, isDir, ls, mtime, readJsonl } from "../../fsutil.ts"
4
+ import { keepImage, parseDataUrl } from "../../session/attachments.ts"
5
+ import type { Attachment, Session, Turn, Usage } from "../../session/types.ts"
6
+ import { clip, header, lossCounter, noteModel } from "../../session/util.ts"
7
+ import { CONTINUED_NOTE } from "../claude-code/records.ts"
8
+ import type { Reader, SessionRef } from "../types.ts"
9
+ import { FAILED_PREFIX, isObj, isRetainedMarker, KNOWN_TYPES, kindOf, LABEL, QUIET_RUN_EVENTS, type Rec, runEvent, sessionsRoot, str, textBlocks, TOOL, usIso } from "./records.ts"
10
+
11
+ // Reads Muse Code sessions: <home>/.local/share/muse/sessions/YYYY/MM/DD/<uuid>/session.jsonl (records.ts).
12
+ // Semantics ported from session-migrate's Muse adapter (xhluca/session-migrate, MIT).
13
+ //
14
+ // A person's prompt is the text of runtime.user_intent.accepted; a run `started` event repeats it, and is the
15
+ // only record of a prompt Muse took another way (the capture's `--image` turn has no accepted intent), so a
16
+ // started prompt becomes a turn only when no accepted intent already gave it. The model's text, tool calls
17
+ // and tool results are the run's committed events; a failed call is the one whose tool_batch.effect.terminal
18
+ // outcome is not "completed", or whose result text Muse prefixed "tool failed:". Muse keeps no image bytes:
19
+ // when the request the model got carried far more user bytes than the prompt, that is counted as lost.
20
+
21
+ const PEEK = 64 * 1024
22
+ /** Extra bytes in a run's first request beyond its prompt that mean something else was sent with it. */
23
+ const ATTACHED_SLACK = 1024
24
+
25
+ type Lost = ReturnType<typeof lossCounter>
26
+
27
+ /** The prompt of an accepted intent: its text blocks without brain's note, which names the session it continues. */
28
+ function intentPrompt(p: Rec, lost?: Lost): { text: string; attachments: Attachment[]; continued?: string } {
29
+ const texts: string[] = []
30
+ const attachments: Attachment[] = []
31
+ for (const m of Array.isArray(p.model_messages) ? p.model_messages : []) {
32
+ if (!isObj(m) || !Array.isArray(m.content)) continue
33
+ for (const b of m.content) {
34
+ if (!isObj(b)) continue
35
+ if (b.kind === "text" && typeof b.text === "string") texts.push(b.text)
36
+ else if (lost) {
37
+ // Not seen in Muse 0.2.1's log; kept when it is a data URL, counted otherwise.
38
+ const d = parseDataUrl(b.url ?? b.image_url ?? b.data)
39
+ const kept = d ? keepImage(d.base64, d.mime) : undefined
40
+ if (kept) attachments.push(kept)
41
+ else lost.add("prompt attachments Muse did not keep")
42
+ }
43
+ }
44
+ }
45
+ let continued: string | undefined
46
+ const note = texts[0]?.match(CONTINUED_NOTE)
47
+ if (note) {
48
+ continued = note[1]
49
+ texts.shift()
50
+ }
51
+ return { text: texts.join("\n\n"), attachments, ...(continued ? { continued } : {}) }
52
+ }
53
+
54
+ const usageOf = (u: unknown): Usage | undefined => {
55
+ if (!isObj(u)) return undefined
56
+ const out: Usage = {}
57
+ const cached = typeof u.cache_read_tokens === "number" ? u.cache_read_tokens : typeof u.cached_tokens === "number" ? u.cached_tokens : 0
58
+ if (typeof u.input_tokens === "number") out.input = Math.max(0, u.input_tokens - cached)
59
+ if (typeof u.output_tokens === "number") out.output = u.output_tokens
60
+ if (cached) out.cacheRead = cached
61
+ if (typeof u.cache_write_tokens === "number" && u.cache_write_tokens) out.cacheWrite = u.cache_write_tokens
62
+ return Object.keys(out).length ? out : undefined
63
+ }
64
+
65
+ function parseArgs(a: unknown): unknown {
66
+ if (typeof a !== "string") return a ?? {}
67
+ try {
68
+ return JSON.parse(a)
69
+ } catch {
70
+ return { arguments: a }
71
+ }
72
+ }
73
+
74
+ /** One Muse session's records (in file order) as the portable session; `bad` is the count of unreadable lines. */
75
+ export function parseSession(rows: Rec[], bad: number, path: string, home?: string): Session {
76
+ const first = rows.find((r) => !isRetainedMarker(r))
77
+ const meta = first?.payload_type === "runtime.session.metadata" && isObj(first.payload?.record) ? (first.payload.record as Rec) : undefined
78
+ if (!first || !meta) throw new Error(`${path} is not a ${LABEL} session (it does not begin with runtime.session.metadata)`)
79
+ const lost = lossCounter()
80
+ if (bad) lost.add("unreadable lines", bad)
81
+
82
+ const sourceId = str(first.stream?.id) ?? basename(dirname(path))
83
+ let cwd = str(meta.workspace_root)
84
+ if (!cwd) cwd = str(rows.find((r) => r.payload_type === "runtime.session.route_facts")?.payload?.record?.cwd)
85
+ const startedAt = usIso(first.recorded_at)
86
+ const h = header(TOOL, sourceId, { sourcePath: path, project: cwd ? { cwd, ...gitInfo(cwd, home) } : {}, ...(startedAt ? { startedAt } : {}) })
87
+ const version = str(meta.build?.semver)
88
+ if (version) h.toolVersion = version
89
+ let model = str(meta.model_id)
90
+ noteModel(h, model)
91
+
92
+ const turns: Turn[] = []
93
+ /** Accepted prompts whose run has not started yet, by text: the started event repeats them. */
94
+ const pending = new Map<string, number>()
95
+ const calls = new Map<string, Extract<Turn, { kind: "tool_call" }>>()
96
+ const answered = new Set<string>()
97
+ const failed = new Set<string>()
98
+ const prompts = new Map<string, string>() // run id → prompt
99
+ const traced = new Set<string>()
100
+ const reminders = new Set<string>()
101
+ let reply: Extract<Turn, { kind: "assistant" }> | undefined
102
+ let continued: string | undefined
103
+ let last: string | undefined
104
+
105
+ for (const r of rows) {
106
+ if (isRetainedMarker(r)) {
107
+ lost.add("status records Muse left out of its log")
108
+ continue
109
+ }
110
+ const when = usIso(r.recorded_at)
111
+ if (when) last = when
112
+ const at = when ? { at: when } : {}
113
+ const p = isObj(r.payload) ? r.payload : {}
114
+ switch (r.payload_type) {
115
+ case "run.model.configured":
116
+ model = str(p.record?.model_id) ?? model
117
+ noteModel(h, model)
118
+ break
119
+ case "runtime.user_intent.accepted": {
120
+ const { text, attachments, continued: from } = intentPrompt(p, lost)
121
+ if (from) continued = from
122
+ if (!text.trim() && !attachments.length) break
123
+ turns.push({ kind: "user", ...at, text, ...(attachments.length ? { attachments } : {}) })
124
+ pending.set(text, (pending.get(text) ?? 0) + 1)
125
+ break
126
+ }
127
+ case "tool_batch.effect.terminal": {
128
+ const rec = isObj(p.record) ? p.record : {}
129
+ const outcome = isObj(rec.outcome) ? str(rec.outcome.kind) : undefined
130
+ const callId = str(rec.call_id)
131
+ if (callId && outcome && outcome !== "completed") failed.add(callId)
132
+ break
133
+ }
134
+ case "runtime.session":
135
+ run(r, at)
136
+ break
137
+ default:
138
+ if (!KNOWN_TYPES.has(String(r.payload_type))) lost.add(`unknown ${LABEL} record types: ${String(r.payload_type)}`)
139
+ }
140
+ }
141
+
142
+ function run(r: Rec, at: { at?: string }) {
143
+ const e = runEvent(r)
144
+ if (!e) return // security mode, agent tree, task lifecycle: Muse's own bookkeeping
145
+ const runId = String(r.payload.run_id ?? "")
146
+ const kind = String(e.kind)
147
+ switch (kind) {
148
+ case "started": {
149
+ const prompt = typeof e.prompt === "string" ? e.prompt : ""
150
+ prompts.set(runId, prompt)
151
+ const note = prompt.match(CONTINUED_NOTE)
152
+ if (note) {
153
+ continued = note[1]
154
+ return
155
+ }
156
+ const waiting = pending.get(prompt) ?? 0
157
+ if (waiting) pending.set(prompt, waiting - 1)
158
+ else if (prompt.trim()) turns.push({ kind: "user", ...at, text: prompt })
159
+ return
160
+ }
161
+ case "assistant_message_committed": {
162
+ if (typeof e.text !== "string" || !e.text.trim()) return
163
+ reply = { kind: "assistant", ...at, text: e.text, ...(model ? { model } : {}) }
164
+ turns.push(reply)
165
+ return
166
+ }
167
+ case "reasoning_committed":
168
+ if (typeof e.text === "string" && e.text.trim()) turns.push({ kind: "reasoning", ...at, text: e.text })
169
+ else lost.add("encrypted or empty reasoning")
170
+ return
171
+ case "assistant_tool_calls_committed":
172
+ for (const c of Array.isArray(e.tool_calls) ? e.tool_calls : []) {
173
+ if (!isObj(c)) continue
174
+ const id = str(c.call_id) ?? str(c.id)
175
+ if (!id || calls.has(id)) continue
176
+ const name = str(c.name) ?? "unknown_tool"
177
+ const call: Extract<Turn, { kind: "tool_call" }> = { kind: "tool_call", ...at, id, name, toolKind: kindOf(name), input: parseArgs(c.args ?? c.arguments) }
178
+ calls.set(id, call)
179
+ turns.push(call)
180
+ }
181
+ return
182
+ case "tool_result_batch_committed":
183
+ for (const res of Array.isArray(e.results) ? e.results : []) {
184
+ if (!isObj(res)) continue
185
+ const callId = str(res.tool_call_id)
186
+ if (!callId || !calls.has(callId)) {
187
+ lost.add("tool results without a call")
188
+ continue
189
+ }
190
+ if (answered.has(callId)) continue
191
+ answered.add(callId)
192
+ const text = typeof res.text === "string" ? res.text : ""
193
+ const ok = !failed.has(callId) && !text.startsWith(FAILED_PREFIX.trimEnd())
194
+ const c = clip(text)
195
+ turns.push({ kind: "tool_result", ...at, callId, ok, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
196
+ // A file tool that succeeded changed the file it names; Muse keeps no diff beside the call.
197
+ const call = calls.get(callId)!
198
+ const input = isObj(call.input) ? call.input : {}
199
+ const target = str(input.path) ?? str(input.file_path)
200
+ if (ok && target && (call.toolKind === "edit" || call.toolKind === "write")) turns.push({ kind: "file_edit", ...at, path: target, callId })
201
+ }
202
+ return
203
+ case "model_completed": {
204
+ noteModel(h, str(e.model))
205
+ const usage = usageOf(e.usage)
206
+ if (reply && usage && !reply.usage) reply.usage = usage
207
+ reply = undefined
208
+ return
209
+ }
210
+ case "model_input_trace_recorded": {
211
+ // The first request of a run says how many bytes the person's message had; Muse logs only its text.
212
+ if (traced.has(runId) || !prompts.has(runId)) return
213
+ traced.add(runId)
214
+ const lanes = Array.isArray(e.bounded?.aggregates) ? (e.bounded.aggregates as Rec[]) : []
215
+ const sent = lanes.filter((l) => l?.source?.value === "current_user").reduce((n, l) => n + (typeof l.byte_count === "number" ? l.byte_count : 0), 0)
216
+ if (sent > Buffer.byteLength(prompts.get(runId)!) + ATTACHED_SLACK) lost.add(`images or expanded files sent with prompts, not kept by ${LABEL}`)
217
+ return
218
+ }
219
+ case "memory_reminder_child_session_linked":
220
+ reminders.add(String(e.child_session_id ?? e.task_id ?? reminders.size))
221
+ return
222
+ case "context_block_updated":
223
+ lost.add(`context blocks ${LABEL} injected`)
224
+ return
225
+ default:
226
+ if (/compact/i.test(kind)) {
227
+ const summary = str(e.summary) ?? str(e.text)
228
+ if (summary) turns.push({ kind: "compaction", ...at, summary })
229
+ else lost.add("compactions without a readable summary")
230
+ } else if (!QUIET_RUN_EVENTS.has(kind)) lost.add(`unknown ${LABEL} run events: ${kind}`)
231
+ }
232
+ }
233
+
234
+ if (reminders.size) lost.add(`${LABEL} reminder-agent runs`, reminders.size)
235
+ // Calls the log never answered get a result saying so, so every call is paired.
236
+ const open = [...calls.keys()].filter((id) => !answered.has(id))
237
+ if (open.length) lost.add("tool results not recorded", open.length)
238
+ for (const id of open) {
239
+ const i = turns.findIndex((t) => t.kind === "tool_call" && t.id === id)
240
+ turns.splice(i + 1, 0, { kind: "tool_result", callId: id, ok: false, output: "(no result recorded)" })
241
+ }
242
+
243
+ const title = (turns.find((t) => t.kind === "user") as Extract<Turn, { kind: "user" }> | undefined)?.text.replace(/\s+/g, " ").trim().slice(0, 80)
244
+ if (title) h.title = title
245
+ if (last) h.updatedAt = last
246
+ if (continued) h.parent = { id: continued, kind: "continued" }
247
+ lost.flush(h)
248
+ return { header: h, turns }
249
+ }
250
+
251
+ /** The session id, folder and first prompt, from the start of a file only. */
252
+ async function peek(path: string): Promise<Omit<SessionRef, "tool" | "path"> | undefined> {
253
+ const file = Bun.file(path)
254
+ const whole = file.size <= PEEK
255
+ const lines = (whole ? await file.text() : await file.slice(0, PEEK).text()).split("\n")
256
+ if (!whole) lines.pop() // the cut line
257
+ let meta: Rec | undefined
258
+ let prompt: string | undefined
259
+ for (const line of lines) {
260
+ if (!line.trim()) continue
261
+ let r: Rec
262
+ try {
263
+ r = JSON.parse(line)
264
+ } catch {
265
+ continue // a torn last line
266
+ }
267
+ if (!isObj(r) || isRetainedMarker(r)) continue
268
+ if (!meta) {
269
+ if (r.payload_type !== "runtime.session.metadata") return undefined
270
+ meta = r
271
+ continue
272
+ }
273
+ if (r.payload_type === "runtime.user_intent.accepted") prompt = intentPrompt(r.payload ?? {}).text
274
+ else {
275
+ const e = runEvent(r)
276
+ if (e?.kind === "started" && typeof e.prompt === "string" && !CONTINUED_NOTE.test(e.prompt)) prompt = e.prompt
277
+ }
278
+ if (prompt?.trim()) break
279
+ prompt = undefined
280
+ }
281
+ // A session Muse opened but never prompted has nothing to continue.
282
+ if (!meta || (!prompt && whole)) return undefined
283
+ const cwd = str(meta.payload?.record?.workspace_root)
284
+ const title = prompt?.replace(/\s+/g, " ").trim().slice(0, 80)
285
+ const updatedAt = mtime(path)
286
+ return {
287
+ sourceId: str(meta.stream?.id) ?? basename(dirname(path)),
288
+ ...(title ? { title } : {}),
289
+ ...(cwd ? { cwd } : {}),
290
+ ...(updatedAt ? { updatedAt } : {}),
291
+ }
292
+ }
293
+
294
+ /** Every session file: sessions/<year>/<month>/<day>/<id>/session.jsonl (subagent logs below a session are not sessions). */
295
+ export function sessionFiles(home: string): string[] {
296
+ const root = sessionsRoot(home)
297
+ const out: string[] = []
298
+ for (const y of ls(root).filter((n) => /^\d{4}$/.test(n)))
299
+ for (const m of ls(join(root, y)).filter((n) => /^\d{2}$/.test(n)))
300
+ for (const d of ls(join(root, y, m)).filter((n) => /^\d{2}$/.test(n)))
301
+ for (const id of ls(join(root, y, m, d))) {
302
+ const p = join(root, y, m, d, id, "session.jsonl")
303
+ if (isDir(join(root, y, m, d, id)) && exists(p)) out.push(p)
304
+ }
305
+ return out
306
+ }
307
+
308
+ export const reader: Reader = {
309
+ tool: TOOL,
310
+ label: LABEL,
311
+
312
+ async list(home) {
313
+ const refs: SessionRef[] = []
314
+ for (const path of sessionFiles(home)) {
315
+ try {
316
+ if (!statSync(path).isFile()) continue
317
+ const p = await peek(path)
318
+ if (p) refs.push({ tool: TOOL, path, ...p })
319
+ } catch {
320
+ // Deleted or unreadable between listing and reading: not a session to offer.
321
+ }
322
+ }
323
+ return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
324
+ },
325
+
326
+ async read(ref, home) {
327
+ if (!exists(ref.path)) throw new Error(`no ${LABEL} session at ${ref.path}`)
328
+ const { rows, bad } = readJsonl(ref.path)
329
+ if (!rows.length) throw new Error(`${ref.path} holds no ${LABEL} records`)
330
+ return parseSession(rows, bad, ref.path, home)
331
+ },
332
+ }
@@ -0,0 +1,104 @@
1
+ import { join } from "node:path"
2
+ import type { ToolKind } from "../../session/types.ts"
3
+ import { toolKindOf } from "../../session/util.ts"
4
+
5
+ // Facts about Muse Code's session store, shared by its reader and writer. Ported from session-migrate
6
+ // (xhluca/session-migrate, MIT licence: src/session_migrate/formats/muse.py and docs/muse-qwen-kimi-formats.md),
7
+ // checked against the real capture of Muse Code 0.2.1 (0.2.1-R1215.1, build b3170a534f) in
8
+ // test/fixtures/native-corpus/muse.
9
+ //
10
+ // Muse keeps one durable JSONL event stream per session:
11
+ // $XDG_DATA_HOME/muse/sessions/YYYY/MM/DD/<uuid>/session.jsonl (XDG data home defaults to ~/.local/share)
12
+ // Every line is an envelope {schema_version: 1, id, stream: {kind: "session", id}, sequence (1, 2, ...),
13
+ // recorded_at (microseconds, strictly increasing), record_type: "event", durability: "durable", causation_id,
14
+ // payload_type, payload_schema_version, payload}. The first is runtime.session.metadata (workspace_root, build,
15
+ // model). Each turn the person starts is runtime.user_intent.accepted (the prompt as model_messages and
16
+ // refill_blocks), a runtime.session run event `started`, and runtime.user_intent.materialized linking both;
17
+ // the model's work follows as run events (assistant_message_committed, assistant_tool_calls_committed,
18
+ // tool_result_batch_committed) and the run ends with `terminal`. Each process start and exit adds
19
+ // session.opened.observed and session.end; a resume adds session.resumed. Most of the rest is telemetry:
20
+ // tasks, reminder agents (child sessions under <uuid>/subagent/), context diagnostics, usage.
21
+
22
+ export const TOOL = "muse"
23
+ export const LABEL = "Muse Code"
24
+ /** The command (`muse --version` prints "Muse Code 0.2.1 (0.2.1-R1215.1)"). */
25
+ export const BIN = "muse"
26
+
27
+ /** The build Muse Code 0.2.1 records in its session metadata. */
28
+ export const BUILD_SHA = "b3170a534f"
29
+
30
+ export type Rec = Record<string, any>
31
+
32
+ export const isObj = (v: unknown): v is Rec => typeof v === "object" && v !== null && !Array.isArray(v)
33
+ export const str = (v: unknown): string | undefined => (typeof v === "string" && v.trim() ? v : undefined)
34
+
35
+ /** <home>/.local/share/muse, the default XDG data home; $XDG_DATA_HOME is not followed, as other adapters do not follow theirs. */
36
+ export const museHome = (home: string) => join(home, ".local", "share", "muse")
37
+ export const sessionsRoot = (home: string) => join(museHome(home), "sessions")
38
+
39
+ const pad = (n: number) => String(n).padStart(2, "0")
40
+
41
+ /**
42
+ * Where a session started at `at` lives. Muse dates the folder in local time: the capture started at
43
+ * 2026-09-01T00:38Z and session-migrate's reload test puts it under 2026/08/31.
44
+ */
45
+ export const sessionPath = (home: string, id: string, at: Date) =>
46
+ join(sessionsRoot(home), String(at.getFullYear()), pad(at.getMonth() + 1), pad(at.getDate()), id, "session.jsonl")
47
+
48
+ /** Muse's recorded_at (microseconds since the epoch) as ISO time. */
49
+ export function usIso(v: unknown): string | undefined {
50
+ if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) return undefined
51
+ return new Date(Math.floor(v / 1000)).toISOString()
52
+ }
53
+
54
+ /** The durable placeholder Muse writes in place of an ephemeral status record it left out. */
55
+ export function isRetainedMarker(r: Rec): boolean {
56
+ const o = r.omitted_record
57
+ return typeof r.retained_marker === "string" && isObj(r.position) && isObj(r.stream) && isObj(o) && o.durability === "ephemeral"
58
+ }
59
+
60
+ /** The run event of a runtime.session record, when it is one. */
61
+ export const runEvent = (r: Rec): Rec | undefined => (r.payload_type === "runtime.session" && isObj(r.payload) && r.payload.kind === "run" && isObj(r.payload.event) ? r.payload.event : undefined)
62
+
63
+ /** Text blocks of a Muse content list ({kind: "text", text}). */
64
+ export const textBlocks = (content: unknown): string[] =>
65
+ Array.isArray(content) ? content.filter((b) => isObj(b) && b.kind === "text" && typeof b.text === "string").map((b) => (b as Rec).text as string) : []
66
+
67
+ /** What Muse's built-in tools do; read_file is the one the capture shows, others go by name. */
68
+ const MUSE_KINDS: Record<string, ToolKind> = { read_file: "read" }
69
+ export const kindOf = (name: string): ToolKind => MUSE_KINDS[name] ?? toolKindOf(name)
70
+
71
+ /** How Muse words a failed tool's result for the model (it keeps no separate error flag on the result). */
72
+ export const FAILED_PREFIX = "tool failed: "
73
+
74
+ /** Run events that are telemetry, not conversation. */
75
+ export const QUIET_RUN_EVENTS = new Set([
76
+ "started",
77
+ "model_completed",
78
+ "model_response_created",
79
+ "terminal",
80
+ "context_block_diagnostic",
81
+ "model_request_configured",
82
+ "provider_request_options_configured",
83
+ "model_input_trace_recorded",
84
+ "goal_usage_attribution",
85
+ "resource_usage_sampled",
86
+ "task_stream_linked",
87
+ ])
88
+
89
+ /** Record types that carry nothing of the conversation (or that the reader handles itself). */
90
+ export const KNOWN_TYPES = new Set([
91
+ "runtime.session",
92
+ "runtime.session.metadata",
93
+ "runtime.session.route_facts",
94
+ "session.opened.observed",
95
+ "session.resumed",
96
+ "session.end",
97
+ "runtime.command_intake.received",
98
+ "runtime.command_intake.settled",
99
+ "run.model.configured",
100
+ "runtime.user_intent.accepted",
101
+ "runtime.user_intent.materialized",
102
+ "tool_batch.effect.started",
103
+ "tool_batch.effect.terminal",
104
+ ])