@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,387 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import { mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs"
3
+ import { basename, dirname } from "node:path"
4
+ import { toolLabel } from "../../brief.ts"
5
+ import { exists, isDir, readJsonl, versionOf } from "../../fsutil.ts"
6
+ import type { Session, Turn } from "../../session/types.ts"
7
+ import { countTurns, 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 { parseSession, sessionFiles } from "./reader.ts"
11
+ import { BIN, BUILD_SHA, FAILED_PREFIX, isObj, LABEL, type Rec, sessionPath, TOOL } from "./records.ts"
12
+
13
+ // Continues a portable session in Muse Code as a new session of its own: one durable event stream at
14
+ // sessions/YYYY/MM/DD/<uuid>/session.jsonl under Muse's data home, which `muse exec --session-id <uuid>`
15
+ // resumes. The record lifecycle is the one session-migrate (xhluca/session-migrate, MIT) found Muse needs to
16
+ // rebuild a model's context from a history it did not write itself (docs/muse-qwen-kimi-formats.md), with the
17
+ // field values Muse 0.2.1 writes in the real capture:
18
+ //
19
+ // runtime.session.metadata, session.opened.observed, then per prompt runtime.user_intent.accepted (the
20
+ // prompt as model_messages and refill_blocks), a run `started` event, runtime.user_intent.materialized
21
+ // naming both, the model's committed messages, tool-call batches and their result batch, a `terminal` run
22
+ // event; session.end last.
23
+ //
24
+ // Envelopes carry contiguous sequences, fresh UUIDs and strictly increasing recorded_at microseconds. Each run
25
+ // event other than a result batch has its own source_run_record_id and per-run sequence, a result batch's
26
+ // batch_id is its call batch's message_id, and a call keeps its id as call_id with `fc_<id>` as its item id,
27
+ // as Muse writes them. Muse keeps no error flag on a result: a failure is worded "tool failed: ...", as Muse
28
+ // words its own. Read calls become Muse's read_file; other calls keep their names.
29
+ //
30
+ // The note on where the session came from is a first text block of the first prompt's model_messages (what
31
+ // the model gets); refill_blocks and the started event hold the person's words alone. Muse keeps no images,
32
+ // and replays no reasoning or compaction from outside, so those are counted as lost.
33
+
34
+ /** Muse Code versions whose session format this writer was checked against. */
35
+ export const TESTED_VERSIONS = ["0.2.1"]
36
+
37
+ /** The model and provider Muse 0.2.1 recorded in the capture, and session-migrate's documented Muse target. */
38
+ export const DEFAULT_MODEL = "meta/muse-glimmer-30b"
39
+ export const DEFAULT_PROVIDER = "meta"
40
+
41
+ const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
42
+
43
+ /**
44
+ * The installed version ("Muse Code 0.2.1 (0.2.1-R1215.1)" gives "0.2.1"), or undefined when it is missing.
45
+ * The binary is looked up on `PATH` as it is now: Bun.which without one does not see a PATH changed after start.
46
+ */
47
+ export function museVersion(bin = BIN, PATH = process.env.PATH ?? ""): string | undefined {
48
+ const path = bin.includes("/") ? bin : Bun.which(bin, { PATH })
49
+ if (!path) return undefined
50
+ return versionOf(path)?.match(/(\d+\.\d+\.\d+)/)?.[1]
51
+ }
52
+
53
+ /** Muse records the workspace as the process sees it: the resolved path (/private/tmp, not /tmp). */
54
+ function realCwd(cwd: string): string {
55
+ try {
56
+ return realpathSync(cwd)
57
+ } catch {
58
+ return cwd
59
+ }
60
+ }
61
+
62
+ type Call = Extract<Turn, { kind: "tool_call" }>
63
+
64
+ const hex = () => randomUUID().replace(/-/g, "").slice(0, 24)
65
+
66
+ /** A read from another tool as Muse's own read_file, when it names a file; undefined keeps the call as it was. */
67
+ function asMuseTool(c: Call): { name: string; args: Rec } | undefined {
68
+ if (c.toolKind !== "read" || !isObj(c.input)) return undefined
69
+ const path = ["path", "file_path", "filePath", "absolute_path", "target_file", "file"].map((k) => (c.input as Rec)[k]).find((v) => typeof v === "string" && v)
70
+ return path ? { name: "read_file", args: { path } } : undefined
71
+ }
72
+
73
+ export interface MusePlan {
74
+ /** Every record of the new session file, in order. */
75
+ records: Rec[]
76
+ lost: string[]
77
+ /** What a read-back must find. */
78
+ expect: { users: number; assistants: number; calls: number }
79
+ }
80
+
81
+ /** The records of a new Muse session `id` continuing `session`. Pure apart from random ids. */
82
+ export function planMuse(session: Session, o: { id: string; cwd: string; now: Date; model: string; provider: string; version: string }): MusePlan {
83
+ const drops = new Map<string, number>()
84
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
85
+ const label = toolLabel(session.header.tool)
86
+ const expect = { users: 0, assistants: 0, calls: 0 }
87
+
88
+ // recorded_at: microseconds, strictly increasing; turn times where they move forward, never past now.
89
+ const nowUs = o.now.getTime() * 1000
90
+ const known = session.turns.map((t) => (t.at ? Date.parse(t.at) : Number.NaN)).find(Number.isFinite)
91
+ let clock = Math.min(known ?? o.now.getTime(), o.now.getTime()) * 1000 - 1
92
+ const stamp = (at?: string) => {
93
+ const t = at ? Date.parse(at) * 1000 : Number.NaN
94
+ clock = Number.isFinite(t) && t > clock && t <= nowUs ? t : clock + 1
95
+ return clock
96
+ }
97
+
98
+ const records: Rec[] = []
99
+ const append = (payloadType: string, payload: Rec, at?: string, causation: string | null = null) => {
100
+ const r: Rec = {
101
+ schema_version: 1,
102
+ id: randomUUID(),
103
+ stream: { kind: "session", id: o.id },
104
+ sequence: records.length + 1,
105
+ recorded_at: stamp(at),
106
+ record_type: "event",
107
+ durability: "durable",
108
+ causation_id: causation,
109
+ payload_type: payloadType,
110
+ payload_schema_version: 1,
111
+ payload,
112
+ }
113
+ records.push(r)
114
+ return r
115
+ }
116
+
117
+ append("runtime.session.metadata", {
118
+ kind: "metadata",
119
+ record: { build: { semver: o.version, sha: BUILD_SHA }, model_id: o.model, provider_id: o.provider, tool_surface_version: "2", web_search_mode: "client", workspace_root: o.cwd },
120
+ })
121
+ append("session.opened.observed", {
122
+ kind: "session_opened",
123
+ record: { data_dir_free_bytes: 0, resume: false, schema_version: 1, security_mode: "normal", session_id: o.id, workspace_free_bytes: 0 },
124
+ })
125
+
126
+ // A run is one prompt and everything the model did for it.
127
+ let run: { id: string; seq: number } | undefined
128
+ let turn = 0
129
+ let noted = false
130
+ const note = continuedNote(label, session.header.id, o.now.toISOString().slice(0, 10))
131
+ const runEvent = (event: Rec, at?: string, source = true) =>
132
+ append("runtime.session", { event, kind: "run", run_id: run!.id, ...(source ? { source_run_record_id: randomUUID(), source_run_record_sequence: ++run!.seq } : {}) }, at)
133
+ const endRun = () => {
134
+ if (!run) return
135
+ runEvent({ eot_gate_ms: 0, kind: "terminal", reason: null, terminal: "completed", time_to_first_token_ms: 0, turn_duration_ms: 0 })
136
+ run = undefined
137
+ }
138
+ /** Opens a run for `prompt`; without one (the session opens with the model), the note is the prompt. */
139
+ const startRun = (prompt: string | undefined, at?: string) => {
140
+ endRun()
141
+ const text = prompt ?? note
142
+ const blocks = noted || prompt === undefined ? [{ kind: "text", text }] : [{ kind: "text", text: note }, { kind: "text", text }]
143
+ noted = true
144
+ // Muse uses one id for the command, the intent and its run.
145
+ const id = randomUUID()
146
+ const accepted = append(
147
+ "runtime.user_intent.accepted",
148
+ { bindings: [], delivery_policy: "session_current", intent_id: id, model_messages: [{ content: blocks }], refill_blocks: [{ kind: "text", text }], semantic_kind: { kind: "chat" }, source_session_id: o.id, surface: "main", wake_policy: "start_once" },
149
+ at,
150
+ id,
151
+ )
152
+ run = { id, seq: 0 }
153
+ const started = runEvent({ kind: "started", prompt: text }, at)
154
+ append(
155
+ "runtime.user_intent.materialized",
156
+ {
157
+ envelope_record_id: accepted.id,
158
+ envelope_sequence: accepted.sequence,
159
+ intent_id: id,
160
+ outcome: { cut_before: `session:${o.id}:turn:${turn}`, kind: "top_level_turn_started", run_id: id, run_started_session_record_id: started.id, run_started_source_record_id: started.payload.source_run_record_id },
161
+ source_session_id: o.id,
162
+ },
163
+ at,
164
+ )
165
+ turn++
166
+ }
167
+ const ensureRun = (at?: string) => {
168
+ if (!run) startRun(undefined, at)
169
+ }
170
+
171
+ // Call ids stay as they were when Muse can carry them, else get a call_ id; each is used once.
172
+ const ids = new Map<string, string>()
173
+ const used = new Set<string>()
174
+ let remapped = 0
175
+ let renamed = 0
176
+ const idFor = (src: string) => {
177
+ let v = ids.get(src)
178
+ if (v) return v
179
+ v = /^[A-Za-z0-9_.:-]{1,64}$/.test(src) && !used.has(src) ? src : `call_${hex()}`
180
+ while (used.has(v)) v = `call_${hex()}`
181
+ if (v !== src) remapped++
182
+ ids.set(src, v)
183
+ used.add(v)
184
+ return v
185
+ }
186
+
187
+ // One model response: its text and its tool-call batch share a response id; results come back as one batch.
188
+ let response: string | undefined
189
+ type Batch = { messageId: string; calls: Array<{ src: string; id: string; name: string; args: string }>; results: Map<string, string>; written: boolean; at?: string }
190
+ let batch: Batch | undefined
191
+ const given = new Set<string>()
192
+ const writeCalls = () => {
193
+ if (!batch || batch.written) return
194
+ response ??= `resp_${hex()}`
195
+ runEvent({ kind: "assistant_tool_calls_committed", message_id: batch.messageId, response_id: response, tool_calls: batch.calls.map((c) => ({ args: c.args, call_id: c.id, id: `fc_${c.id}`, name: c.name })) }, batch.at)
196
+ batch.written = true
197
+ }
198
+ const closeBatch = () => {
199
+ if (!batch) return
200
+ writeCalls()
201
+ const b = batch
202
+ const results = b.calls.map((c, i) => {
203
+ let text = b.results.get(c.src)
204
+ if (text === undefined) {
205
+ drop("tool calls without a result (closed as failed)")
206
+ text = `${FAILED_PREFIX}no result was recorded for this call; the session moved on before it finished.`
207
+ }
208
+ return { text, tool_call_id: c.id, tool_call_index: i }
209
+ })
210
+ // A result batch carries no source run record, as Muse writes it.
211
+ runEvent({ batch_id: b.messageId, kind: "tool_result_batch_committed", results }, b.at, false)
212
+ batch = undefined
213
+ response = undefined
214
+ }
215
+
216
+ for (const t of session.turns) {
217
+ switch (t.kind) {
218
+ case "user": {
219
+ closeBatch()
220
+ response = undefined
221
+ let text = t.text
222
+ for (const a of t.attachments ?? []) {
223
+ if (a.kind === "file" && a.path) text += `\n\n[Attached file: ${a.path}]`
224
+ else drop(`${a.kind === "image" ? "images" : "attachments"} (a Muse session keeps no image)`)
225
+ }
226
+ if (!text.trim()) break
227
+ startRun(text, t.at)
228
+ expect.users++
229
+ break
230
+ }
231
+ case "assistant": {
232
+ closeBatch()
233
+ if (!t.text.trim()) break
234
+ ensureRun(t.at)
235
+ response = `resp_${hex()}`
236
+ runEvent({ kind: "assistant_message_committed", message_id: randomUUID(), provider_item_id: `msg_${response}`, response_id: response, text: t.text }, t.at)
237
+ expect.assistants++
238
+ break
239
+ }
240
+ case "reasoning":
241
+ if (t.text.trim()) drop("reasoning (Muse replays no reasoning written from outside)")
242
+ break
243
+ case "tool_call": {
244
+ if (ids.has(t.id)) {
245
+ drop("repeated tool call ids")
246
+ break
247
+ }
248
+ // A call after results is the next response.
249
+ if (batch?.results.size) closeBatch()
250
+ ensureRun(t.at)
251
+ const own = asMuseTool(t)
252
+ const name = own?.name ?? (t.name.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 64) || "tool")
253
+ if (name !== t.name) renamed++
254
+ let args: Rec
255
+ if (own) args = own.args
256
+ else if (isObj(t.input)) args = t.input
257
+ else {
258
+ args = { input: t.input ?? null }
259
+ drop("tool inputs that were not objects (wrapped)")
260
+ }
261
+ batch ??= { messageId: randomUUID(), calls: [], results: new Map(), written: false, ...(t.at ? { at: t.at } : {}) }
262
+ batch.calls.push({ src: t.id, id: idFor(t.id), name, args: JSON.stringify(args) })
263
+ expect.calls++
264
+ break
265
+ }
266
+ case "tool_result": {
267
+ if (!ids.has(t.callId)) {
268
+ drop("tool results without a call")
269
+ break
270
+ }
271
+ if (given.has(t.callId)) {
272
+ drop("repeated tool results")
273
+ break
274
+ }
275
+ // Its call's batch was already closed (with a failure) when something else came between them.
276
+ if (!batch || !batch.calls.some((c) => c.src === t.callId)) {
277
+ drop("tool results recorded after the session moved on")
278
+ break
279
+ }
280
+ given.add(t.callId)
281
+ writeCalls()
282
+ let out = t.fullPath ? `${t.output}\n\n[Full output: ${t.fullPath}]` : t.output
283
+ if (!t.ok && !out.startsWith(FAILED_PREFIX.trimEnd())) out = `${FAILED_PREFIX}${out}`
284
+ batch.results.set(t.callId, out)
285
+ if (t.at) batch.at = t.at
286
+ if (batch.results.size === batch.calls.length) closeBatch()
287
+ break
288
+ }
289
+ case "compaction":
290
+ closeBatch()
291
+ drop("compaction summaries (the turns they replaced are all kept)")
292
+ break
293
+ case "file_edit":
294
+ if (!t.callId || !ids.has(t.callId)) drop("file edits outside a tool call")
295
+ break
296
+ case "subagent":
297
+ drop("subagent transcripts (their results stay in the parent's tool results)")
298
+ break
299
+ }
300
+ }
301
+ closeBatch()
302
+ endRun()
303
+ if (!expect.users && !expect.assistants && !expect.calls) throw new Error("nothing to continue: the session has no messages or tool calls")
304
+ append("session.end", { kind: "session_end", record: { exit_reason: "clean", resource_usage: {}, schema_version: 1, session_id: o.id, uptime_ms: 0 } })
305
+
306
+ const lost = [...drops].map(([what, n]) => `${what} (${n})`)
307
+ if (renamed) lost.push(`original tool names (${renamed} calls renamed to Muse's read_file or to fit its rules)`)
308
+ if (remapped) lost.push(`tool call ids remapped (${remapped})`)
309
+ return { records, lost, expect }
310
+ }
311
+
312
+ /** Options for tests and for people who run Muse with another model; the defaults suit an installed Muse 0.2.1. */
313
+ export interface MuseWriterOptions {
314
+ /** The installed version, instead of asking `muse --version`. */
315
+ version?: string
316
+ bin?: string
317
+ /** The PATH to look the binary up on (default: process.env.PATH when ready() runs). */
318
+ PATH?: string
319
+ model?: string
320
+ provider?: string
321
+ /** New session ids (default: random UUIDs). */
322
+ newId?: () => string
323
+ }
324
+
325
+ export function museWriter(opts: MuseWriterOptions = {}): Writer {
326
+ const installed = () => opts.version ?? museVersion(opts.bin ?? BIN, opts.PATH ?? process.env.PATH ?? "")
327
+ const newId = opts.newId ?? (() => randomUUID())
328
+ return {
329
+ tool: TOOL,
330
+ label: LABEL,
331
+ way: "native",
332
+
333
+ async ready(w: WriteOptions): Promise<Readiness> {
334
+ const v = installed()
335
+ if (!v) return { ok: false, why: `${LABEL} is not installed (\`${opts.bin ?? BIN} --version\` did not run)` }
336
+ if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `${LABEL} ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
337
+ if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; ${LABEL} records the workspace a session runs in` }
338
+ return { ok: true }
339
+ },
340
+
341
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
342
+ const cwd = realCwd(w.cwd)
343
+ const now = new Date()
344
+ // Muse finds a session by its id in any date folder, so the id must be new in all of them.
345
+ const taken = new Set(sessionFiles(w.home).map((p) => basename(dirname(p))))
346
+ let id = newId()
347
+ for (let tries = 0; taken.has(id) || exists(dirname(sessionPath(w.home, id, now))); tries++) {
348
+ if (tries >= 20) throw new Error(`could not find an unused ${LABEL} session id; nothing was written`)
349
+ id = newId()
350
+ }
351
+ const path = sessionPath(w.home, id, now)
352
+ const plan = planMuse(session, { id, cwd, now, model: opts.model ?? DEFAULT_MODEL, provider: opts.provider ?? DEFAULT_PROVIDER, version: TESTED_VERSIONS[TESTED_VERSIONS.length - 1]! })
353
+ if (!w.dryRun) {
354
+ const dir = dirname(path)
355
+ mkdirSync(dirname(dir), { recursive: true, mode: 0o700 })
356
+ // mkdir without recursive on the session folder itself: an existing session is never touched.
357
+ mkdirSync(dir, { mode: 0o700 })
358
+ try {
359
+ writeFileSync(path, `${plan.records.map((r) => JSON.stringify(r)).join("\n")}\n`, { flag: "wx", mode: 0o600 })
360
+ const { rows, bad } = readJsonl(path)
361
+ const back = parseSession(rows, bad, path, w.home)
362
+ const n = countTurns(back)
363
+ const pairs = unpaired(back)
364
+ if (n.user !== plan.expect.users || n.assistant !== plan.expect.assistants || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length)
365
+ throw new Error(
366
+ `the new ${LABEL} session did not read back as written (users ${n.user}/${plan.expect.users}, replies ${n.assistant}/${plan.expect.assistants}, tool calls ${n.tool_call}/${plan.expect.calls}, unpaired ${pairs.calls.length + pairs.results.length}); nothing was kept`,
367
+ )
368
+ } catch (e) {
369
+ rmSync(dir, { recursive: true, force: true })
370
+ throw e
371
+ }
372
+ }
373
+ return {
374
+ tool: TOOL,
375
+ way: "native",
376
+ sessionId: id,
377
+ written: [path],
378
+ open: `cd ${shellQuote(cwd)} && ${opts.bin ?? BIN} exec --session-id ${id} --workspace ${shellQuote(cwd)} "<your next message>"`,
379
+ note: `${LABEL} resumes a session by id with \`muse exec --session-id\` (the path session-migrate checked with Muse 0.2.1); it answers with your own Muse provider and model settings.`,
380
+ lost: plan.lost,
381
+ dryRun: !!w.dryRun,
382
+ }
383
+ },
384
+ }
385
+ }
386
+
387
+ export const museNativeWriter = museWriter()
@@ -0,0 +1,13 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { ompReader } from "./reader.ts"
3
+ import { ompWriter } from "./writer.ts"
4
+
5
+ // Oh My Pi (can1357/oh-my-pi, `omp`), a fork of Pi: reads its sessions from ~/.omp/agent and each profile's
6
+ // ~/.omp/profiles/<name>/agent, and continues any session as a new native OMP session, `omp --resume <id>`.
7
+ // Format and checks ported from xhluca/session-migrate (MIT), docs/omp-format.md and formats/omp.py; see records.ts.
8
+
9
+ export { OMP_TESTED_VERSIONS } from "./records.ts"
10
+ export { ompReader, ompWriter }
11
+
12
+ export const readers: Reader[] = [ompReader]
13
+ export const writers: Writer[] = [ompWriter()]