@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,216 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import { mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs"
3
+ import { dirname, extname, join } from "node:path"
4
+ import { exists, isDir, ls, versionOf } from "../../fsutil.ts"
5
+ import { IMAGE_LIMIT, IMAGE_TYPES } from "../../session/attachments.ts"
6
+ import type { Attachment, Session, Turn } from "../../session/types.ts"
7
+ import { countTurns, unpaired } from "../../session/util.ts"
8
+ import type { Entry } from "../pi/records.ts"
9
+ import { planPiSession } from "../pi/writer.ts"
10
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
11
+ import { parseOmpSession } from "./reader.ts"
12
+ import { BLOB_LIMIT, OMP_TESTED_VERSIONS, blobData, blobPath, bucketName, ompFlavor, sha256, titleSlot } from "./records.ts"
13
+
14
+ // Continues a portable session as a new Oh My Pi session: <home>/.omp/agent/sessions/<bucket>/<time>_<uuid>.jsonl,
15
+ // which `omp --resume <uuid>` opens. Ported from session-migrate's omp.serialize (MIT, xhluca), which builds
16
+ // on its Pi writer the same way: the entries are what our Pi writer plans (see ../pi/writer.ts for how turns
17
+ // map, the hidden "continued" note, compactions that keep nothing before them, calls closed as errors), then
18
+ // reshaped the way OMP 18.0.5 writes them:
19
+ // - the 256-byte title slot comes first and the header carries the title, instead of Pi's session_info entry;
20
+ // - prompts carry attribution: "user", and the usage block has OMP's fields (no reasoning count);
21
+ // - pasted media go to the content-addressed blob store, <agent>/blobs/<sha256>, and the message's image block
22
+ // says "blob:sha256:<hex>", as OMP stores what it is given: images, and PDFs, audio and video, which OMP
23
+ // takes through the same field. Other files stay a line naming their path, as for Pi.
24
+ // Blobs are written only when no file of that name exists (same name, same bytes); the session file is new.
25
+
26
+ type User = Extract<Turn, { kind: "user" }>
27
+
28
+ /** Media types OMP 18.0.5 accepted through its image field and stored as blobs (session-migrate's capture). */
29
+ const MEDIA = /^(application\/pdf|audio\/[\w.+-]+|video\/[\w.+-]+)$/
30
+
31
+ const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
32
+
33
+ const sizeOf = (p: string) => {
34
+ try {
35
+ return statSync(p).size
36
+ } catch {
37
+ return Number.POSITIVE_INFINITY
38
+ }
39
+ }
40
+
41
+ /**
42
+ * The attachment's type when OMP gets it as a blob: an image Pi's writer would embed (same types and size
43
+ * bound, so whatever is left is something Pi's writer would drop too), or a PDF, audio or video file.
44
+ */
45
+ function asBlob(a: Attachment): string | undefined {
46
+ if (!a.path) return undefined
47
+ if (a.kind === "image") {
48
+ const mime = a.mime ?? IMAGE_TYPES[extname(a.path).toLowerCase()]
49
+ return mime && Object.values(IMAGE_TYPES).includes(mime) && sizeOf(a.path) <= IMAGE_LIMIT ? mime : undefined
50
+ }
51
+ return a.mime && MEDIA.test(a.mime) && sizeOf(a.path) <= BLOB_LIMIT ? a.mime : undefined
52
+ }
53
+
54
+ export interface OmpPlan {
55
+ /** The whole file: title slot, header, entries. */
56
+ text: string
57
+ /** The header line and every entry, in order (the slot is not among them). */
58
+ lines: Entry[]
59
+ /** Blobs the session refers to, by hash, and where their bytes are. */
60
+ blobs: Map<string, string>
61
+ lost: string[]
62
+ expect: { users: number; assistants: number; calls: number; attachments: number }
63
+ }
64
+
65
+ /** The file of a new OMP session `id` continuing `session`. Pure apart from reading attached files. */
66
+ export function planOmpSession(session: Session, opts: { id: string; now: Date; cwd: string }): OmpPlan {
67
+ // Media OMP stores as blobs leave the turns before Pi's planner sees them and return as blocks afterwards,
68
+ // on the same prompt: k-th user message for the k-th user turn that makes one.
69
+ const blobs = new Map<string, string>()
70
+ const pending: Entry[][] = []
71
+ let attachments = 0
72
+ const turns = session.turns.map((t): Turn => {
73
+ if (t.kind !== "user") return t
74
+ const media: Entry[] = []
75
+ const rest: Attachment[] = []
76
+ for (const a of t.attachments ?? []) {
77
+ const mime = asBlob(a)
78
+ if (!mime) {
79
+ rest.push(a)
80
+ continue
81
+ }
82
+ const hex = sha256(readFileSync(a.path!))
83
+ blobs.set(hex, a.path!)
84
+ media.push({ type: "image", data: blobData(hex), mimeType: mime })
85
+ }
86
+ const text = t.text.trim() || !media.length ? t.text : media.every((b) => b.mimeType.startsWith("image/")) ? "(image)" : "(attachment)"
87
+ const { attachments: _, ...base } = t
88
+ const user: User = { ...base, text, ...(rest.length ? { attachments: rest } : {}) }
89
+ // Pi's planner writes a prompt when it has text or a file line; images left in `rest` it drops.
90
+ if (text.trim() || rest.some((a) => a.kind === "file" && a.path)) {
91
+ pending.push(media)
92
+ attachments += media.length
93
+ }
94
+ return user
95
+ })
96
+ const pi = planPiSession({ ...session, turns }, { flavor: ompFlavor, id: opts.id, now: opts.now, cwd: opts.cwd })
97
+
98
+ // Pi names the session with a session_info entry; OMP keeps the name in the slot and the header instead.
99
+ const info = pi.lines.find((e) => e.type === "session_info")
100
+ const name = typeof info?.name === "string" ? info.name : ""
101
+ const lines = pi.lines.filter((e) => e !== info)
102
+ for (const e of lines) if (info && e.parentId === info.id) e.parentId = info.parentId ?? null
103
+ const head = lines[0]!
104
+ const slot = titleSlot(name, head.timestamp)
105
+ if (slot.title) Object.assign(head, { title: slot.title, titleSource: "user" })
106
+
107
+ let k = 0
108
+ for (const e of lines) {
109
+ const m = e.message as Entry | undefined
110
+ if (e.type !== "message" || !m) continue
111
+ if (m.role === "user") {
112
+ const content = [...(m.content as Entry[]), ...(pending[k++] ?? [])]
113
+ e.message = { role: "user", content, attribution: "user", timestamp: m.timestamp }
114
+ } else if (m.role === "assistant" && m.usage) {
115
+ const { reasoning: _, ...usage } = m.usage as Entry
116
+ m.usage = usage
117
+ }
118
+ }
119
+ if (k !== pending.length) throw new Error(`internal: ${k} prompts written for ${pending.length} expected; nothing was written`)
120
+
121
+ const text = slot.line + lines.map((l) => `${JSON.stringify(l)}\n`).join("")
122
+ return { text, lines, blobs, lost: pi.lost, expect: { ...pi.expect, attachments } }
123
+ }
124
+
125
+ /** The version from `omp --version` ("omp/18.0.5"), found on the PATH as it is now, or undefined when missing. */
126
+ export function ompVersion(bin = "omp"): string | undefined {
127
+ const path = bin.includes("/") ? bin : Bun.which(bin, { PATH: process.env.PATH ?? "" })
128
+ if (!path) return undefined
129
+ return versionOf(path)?.match(/(\d+\.\d+\.\d+(?:-[\w.]+)?)/)?.[1]
130
+ }
131
+
132
+ /** OMP records the folder it ran in as the process saw it: the resolved path. */
133
+ function realCwd(cwd: string): string {
134
+ try {
135
+ return realpathSync(cwd)
136
+ } catch {
137
+ return cwd
138
+ }
139
+ }
140
+
141
+ /** The OMP writer; `version` and `newId` are for tests that must not depend on an installed binary or chance. */
142
+ export function ompWriter(version?: string, o: { newId?: () => string } = {}): Writer {
143
+ const newId = o.newId ?? randomUUID
144
+ return {
145
+ tool: "omp",
146
+ label: ompFlavor.label,
147
+ way: "native",
148
+
149
+ async ready(w: WriteOptions): Promise<Readiness> {
150
+ const v = version ?? ompVersion()
151
+ if (!v) return { ok: false, why: "Oh My Pi is not installed here (`omp --version` failed)" }
152
+ if (!OMP_TESTED_VERSIONS.includes(v)) return { ok: false, why: `Oh My Pi ${v} is not a version this writer was checked against (${OMP_TESTED_VERSIONS.join(", ")})` }
153
+ if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Oh My Pi keeps sessions by the folder they belong to` }
154
+ return { ok: true }
155
+ },
156
+
157
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
158
+ const cwd = realCwd(w.cwd)
159
+ const now = new Date()
160
+ const agent = join(w.home, ".omp", "agent")
161
+ const sessions = join(agent, "sessions")
162
+ const dir = join(sessions, bucketName(cwd, w.home))
163
+ // `omp --resume <id>` finds a session by its id, so the id must be new to the whole store, not just the folder.
164
+ const used = new Set(ls(sessions).flatMap((b) => ls(join(sessions, b)).map((n) => /_([^_]+)\.jsonl$/.exec(n)?.[1])))
165
+ let id = newId()
166
+ for (let tries = 1; used.has(id); tries++) {
167
+ if (tries >= 10) throw new Error(`Oh My Pi already has a session with every new id tried (last ${id}); nothing was written`)
168
+ id = newId()
169
+ }
170
+ const path = join(dir, ompFlavor.fileName(id, now.toISOString()))
171
+ const plan = planOmpSession(session, { id, now, cwd })
172
+ const newBlobs = [...plan.blobs].filter(([hex]) => !exists(blobPath(agent, hex)))
173
+ if (!w.dryRun) {
174
+ const made: string[] = []
175
+ try {
176
+ for (const [hex, from] of newBlobs) {
177
+ const to = blobPath(agent, hex)
178
+ mkdirSync(dirname(to), { recursive: true })
179
+ // "wx": content-addressed, so a blob that appeared meanwhile holds these bytes and is left as it is.
180
+ try {
181
+ writeFileSync(to, readFileSync(from), { flag: "wx", mode: 0o600 })
182
+ made.push(to)
183
+ } catch (e) {
184
+ if ((e as NodeJS.ErrnoException).code !== "EEXIST") throw e
185
+ }
186
+ }
187
+ mkdirSync(dirname(path), { recursive: true })
188
+ writeFileSync(path, plan.text, { flag: "wx", mode: 0o600 })
189
+ made.push(path)
190
+ } catch (e) {
191
+ for (const p of made) rmSync(p, { force: true })
192
+ throw e
193
+ }
194
+ const back = parseOmpSession(readFileSync(path, "utf8"), path, w.home)
195
+ const n = countTurns(back)
196
+ const pairs = unpaired(back)
197
+ const kept = back.turns.reduce((s, t) => s + (t.kind === "user" ? (t.attachments?.filter((a) => a.path?.startsWith(join(agent, "blobs"))).length ?? 0) : 0), 0)
198
+ if (n.user !== plan.expect.users || n.assistant !== plan.expect.assistants || n.tool_call !== plan.expect.calls || kept !== plan.expect.attachments || pairs.calls.length || pairs.results.length) {
199
+ for (const p of made) rmSync(p, { force: true })
200
+ throw new Error(
201
+ `the Oh My Pi 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}, attachments ${kept}/${plan.expect.attachments}, unpaired ${pairs.calls.length + pairs.results.length}); nothing was kept`,
202
+ )
203
+ }
204
+ }
205
+ return {
206
+ tool: "omp",
207
+ way: "native",
208
+ sessionId: id,
209
+ written: [path, ...newBlobs.map(([hex]) => blobPath(agent, hex))],
210
+ open: `cd ${shellQuote(cwd)} && omp --resume ${id}`,
211
+ lost: plan.lost,
212
+ dryRun: !!w.dryRun,
213
+ }
214
+ },
215
+ }
216
+ }
@@ -0,0 +1,52 @@
1
+ // OpenCode's ids, which 100xprompt kept: a prefix ("ses", "msg", "prt"), then 12 hex characters holding the
2
+ // low 48 bits of `ms * 0x1000 + counter` (bit-inverted for sessions, so newer sessions sort first), then 14
3
+ // random base62 characters. Messages and parts are read back in id order, so new ids must ascend.
4
+
5
+ const BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
6
+
7
+ export function random62(n: number): string {
8
+ return Array.from(crypto.getRandomValues(new Uint8Array(n)), (b) => BASE62[b % 62]).join("")
9
+ }
10
+
11
+ export function mintId(prefix: string, ms: number, counter: number, descending = false): string {
12
+ let v = BigInt(ms) * 0x1000n + BigInt(counter)
13
+ if (descending) v = ~v
14
+ const hex = Array.from({ length: 6 }, (_, i) => Number((v >> BigInt(40 - 8 * i)) & 0xffn).toString(16).padStart(2, "0")).join("")
15
+ return `${prefix}_${hex}${random62(14)}`
16
+ }
17
+
18
+ /** Ascending ids from `start`: 4,095 per millisecond, then the next millisecond, as OpenCode mints them. */
19
+ export function idMinter(start: number) {
20
+ let ms = start
21
+ let n = 0
22
+ return (prefix: string) => {
23
+ if (++n > 0xfff) {
24
+ ms++
25
+ n = 1
26
+ }
27
+ return mintId(prefix, ms, n)
28
+ }
29
+ }
30
+
31
+ // The 48-bit field wraps every 2^36 ms (about 795 days); it last did on 2026-08-14, so a plain string sort
32
+ // puts ids minted after that before older ones. As 100xprompt does (id-order.ts), each id is placed in the
33
+ // latest period that is not in the future.
34
+ const SPAN = 2 ** 36
35
+ const SKEW = 24 * 60 * 60 * 1000
36
+
37
+ function decode(id: string, now: number): { ms: number; counter: number } | undefined {
38
+ const sep = id.indexOf("_")
39
+ const hex = sep < 0 ? "" : id.slice(sep + 1, sep + 13)
40
+ if (!/^[0-9a-f]{12}$/.test(hex)) return undefined
41
+ const field = parseInt(hex, 16)
42
+ const wrapped = Math.floor(field / 0x1000)
43
+ return { ms: wrapped + SPAN * Math.floor((now + SKEW - wrapped) / SPAN), counter: field % 0x1000 }
44
+ }
45
+
46
+ /** Chronological order of two ascending ids. */
47
+ export function compareIds(a: string, b: string, now = Date.now()): number {
48
+ const x = decode(a, now)
49
+ const y = decode(b, now)
50
+ if (x && y && (x.ms !== y.ms || x.counter !== y.counter)) return x.ms - y.ms || x.counter - y.counter
51
+ return a < b ? -1 : a > b ? 1 : 0
52
+ }
@@ -0,0 +1,31 @@
1
+ import { versionOf } from "../../fsutil.ts"
2
+ import type { Reader, Writer } from "../types.ts"
3
+ import { familyReader } from "./reader.ts"
4
+ import { v2Writer } from "./v2.ts"
5
+ import { HUNDREDX_TARGET, OPENCODE_TARGET, importWriter, resolveBin, versionNumber } from "./writer.ts"
6
+
7
+ // OpenCode (1.x and 2.x) and 100xprompt, which share the session/message/part layout and the export/import
8
+ // JSON (catalogue entries 8, 9 and 71); OpenCode 2.x has its own store and transfer format (v2.ts). Every
9
+ // one reads, and continues a session through the tool's own `import` command.
10
+ export const readers: Reader[] = [
11
+ familyReader({ tool: "opencode", label: "OpenCode", app: "opencode", sqlite: true }),
12
+ familyReader({ tool: "100xprompt", label: "100xprompt", app: "100xprompt", sqlite: false }),
13
+ ]
14
+
15
+ const v1Writer = importWriter(OPENCODE_TARGET)
16
+
17
+ /** OpenCode's writer for whichever line is installed: 1.x takes the v1 export, 2.x the v2 transfer. */
18
+ const opencodeWriter: Writer = {
19
+ tool: "opencode",
20
+ label: "OpenCode",
21
+ way: "import",
22
+ ready: (opts) => pick(opts.home).ready(opts),
23
+ write: (session, opts) => pick(opts.home).write(session, opts),
24
+ }
25
+
26
+ function pick(home: string): Writer {
27
+ const v = versionNumber(versionOf(resolveBin(OPENCODE_TARGET, home).path))
28
+ return v && Number(v.split(".")[0]) >= 2 ? v2Writer : v1Writer
29
+ }
30
+
31
+ export const writers: Writer[] = [importWriter(HUNDREDX_TARGET), opencodeWriter]
@@ -0,0 +1,8 @@
1
+ // The note that opens every session this family's writers continue: the model reads it as the tool's own
2
+ // hidden message (a synthetic part, a synthetic message, a hidden user text), and the readers recognise it as
3
+ // the session's origin instead of a request.
4
+
5
+ export const continuedNote = (label: string, id: string, when: string) =>
6
+ `[This session continues ${label} session ${id}, carried over by brain session on ${when}. The turns below are its history. Check files before relying on details, since they may have changed.]`
7
+
8
+ export const CONTINUED_NOTE = /^\[This session continues .+? session (\S+), carried over by brain session/