@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,186 @@
1
+ import { closeSync, openSync, readFileSync, readSync, statSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { exists, gitInfo, isDir, ls, mtime } from "../../fsutil.ts"
4
+ import type { Project, Session, Turn } from "../../session/types.ts"
5
+ import { header, iso, lossCounter } from "../../session/util.ts"
6
+ import type { Reader, SessionRef } from "../types.ts"
7
+ import { applyRewinds, type Event, eventsToTurns, isPrompt, LABEL, readJournal, TESTED_EVENT_VERSION, TOOL, workspaceYaml } from "./records.ts"
8
+
9
+ // The Copilot CLI keeps each session in ~/.copilot/session-state/<id>/: events.jsonl (the journal every
10
+ // resume replays; rotated archives beside it as events.N.jsonl) and workspace.yaml (cwd, git, summary,
11
+ // timestamps). Early versions wrote a flat session-state/<id>.jsonl. The format comes from the Copilot SDK's
12
+ // event schema and from sessions Copilot CLI 1.0.107 wrote itself (records.ts).
13
+
14
+ const oneLine = (s: string) => s.replace(/\s+/g, " ").trim()
15
+ const titleFrom = (s: unknown) => (typeof s === "string" && oneLine(s) ? oneLine(s).slice(0, 120) : undefined)
16
+
17
+ function readText(p: string): string | undefined {
18
+ try {
19
+ return readFileSync(p, "utf8")
20
+ } catch {
21
+ return undefined
22
+ }
23
+ }
24
+
25
+ /** The whole JSON lines among bytes [start, start+length) of a journal; a line cut by the window is skipped. */
26
+ function linesAt(path: string, start: number, length: number): Event[] {
27
+ const fd = openSync(path, "r")
28
+ try {
29
+ const buf = Buffer.alloc(length)
30
+ const n = readSync(fd, buf, 0, length, Math.max(0, start))
31
+ const out: Event[] = []
32
+ for (const line of buf.subarray(0, n).toString("utf8").split("\n")) {
33
+ try {
34
+ const v = JSON.parse(line)
35
+ if (v && typeof v === "object") out.push(v)
36
+ } catch {
37
+ // cut by the window, or half-written
38
+ }
39
+ }
40
+ return out
41
+ } finally {
42
+ closeSync(fd)
43
+ }
44
+ }
45
+
46
+ interface Found {
47
+ journal: string
48
+ /** The session folder (current layout), where workspace.yaml lives. */
49
+ dir?: string
50
+ stem: string
51
+ }
52
+
53
+ /** Every session under session-state: <id>/events.jsonl, or a flat <id>.jsonl. */
54
+ function sessions(home: string): Found[] {
55
+ const root = join(home, ".copilot", "session-state")
56
+ const out: Found[] = []
57
+ for (const name of ls(root)) {
58
+ const p = join(root, name)
59
+ if (isDir(p)) {
60
+ if (exists(join(p, "events.jsonl"))) out.push({ journal: join(p, "events.jsonl"), dir: p, stem: name })
61
+ } else if (name.endsWith(".jsonl")) out.push({ journal: p, stem: name.slice(0, -6) })
62
+ }
63
+ return out
64
+ }
65
+
66
+ const WINDOW = 64 * 1024
67
+
68
+ export const reader: Reader = {
69
+ tool: TOOL,
70
+ label: LABEL,
71
+
72
+ async list(home) {
73
+ const refs: SessionRef[] = []
74
+ for (const f of sessions(home)) {
75
+ let size = 0
76
+ try {
77
+ size = statSync(f.journal).size
78
+ } catch {
79
+ continue
80
+ }
81
+ // The start of the journal has session.start and the first message; the end has the latest title.
82
+ const head = linesAt(f.journal, 0, WINDOW)
83
+ const tail = size > WINDOW ? linesAt(f.journal, size - WINDOW, WINDOW) : head
84
+ const ws = f.dir ? workspaceYaml(readText(join(f.dir, "workspace.yaml"))) : {}
85
+ const start = head.find((e) => e.type === "session.start")?.data
86
+ const firstUser = head.find((e) => e.type === "user.message" && !e.agentId && isPrompt(e.data?.source))?.data
87
+ // A session that never got a message is not worth continuing; the CLI makes one on every launch.
88
+ if (!firstUser && size <= WINDOW) continue
89
+ const title = titleFrom([...tail].reverse().find((e) => e.type === "session.title_changed")?.data?.title) ?? titleFrom(ws.summary) ?? titleFrom(ws.name) ?? titleFrom(firstUser?.content)
90
+ const cwd = ws.cwd || (typeof start?.context?.cwd === "string" ? start.context.cwd : undefined)
91
+ const updatedAt = iso(ws.updated_at) ?? mtime(f.journal)
92
+ const sourceId = typeof start?.sessionId === "string" ? start.sessionId : ws.id || f.stem
93
+ refs.push({ tool: TOOL, sourceId, path: f.journal, ...(title ? { title } : {}), ...(cwd ? { cwd } : {}), ...(updatedAt ? { updatedAt } : {}), ...(f.dir ? { extra: { dir: f.dir } } : {}) })
94
+ }
95
+ return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
96
+ },
97
+
98
+ async read(ref) {
99
+ if (!exists(ref.path)) throw new Error(`${ref.path} does not exist`)
100
+ const journal = readJournal(ref.path)
101
+ const { events, removed } = applyRewinds(journal.events.filter((e) => !e.ephemeral))
102
+ if (!events.length) throw new Error(`${ref.path} has no Copilot CLI events`)
103
+ const lost = lossCounter()
104
+ if (journal.bad) lost.add("unreadable lines", journal.bad)
105
+ if (journal.missingArchives) lost.add("rotated journal archives missing (older history)", journal.missingArchives)
106
+ if (removed) lost.add("events removed by a rewind", removed)
107
+
108
+ const start = events.find((e) => e.type === "session.start")?.data
109
+ if (start && start.version !== TESTED_EVENT_VERSION) lost.add(`unverified event schema version ${String(start.version)}`)
110
+ const sessionId = typeof start?.sessionId === "string" ? start.sessionId : ref.sourceId
111
+ // The working folder can change (session.resume, session.context_changed); the latest is where work goes on.
112
+ const contexts = events.flatMap((e) => (e.type === "session.start" || e.type === "session.resume" ? [e.data?.context] : e.type === "session.context_changed" ? [e.data] : [])).filter((c) => c && typeof c.cwd === "string")
113
+ const context = contexts[contexts.length - 1]
114
+ const ws = ref.extra?.dir ? workspaceYaml(readText(join(String(ref.extra.dir), "workspace.yaml"))) : {}
115
+ const cwd: string | undefined = context?.cwd ?? (ws.cwd || ref.cwd)
116
+ const local = gitInfo(cwd)
117
+ const project: Project = {
118
+ ...(cwd ? { cwd } : {}),
119
+ ...local,
120
+ ...(typeof context?.branch === "string" ? { gitBranch: context.branch } : {}),
121
+ ...(typeof context?.headCommit === "string" ? { gitCommit: context.headCommit } : {}),
122
+ }
123
+ if (!project.gitRemote && typeof context?.repository === "string") project.gitRemote = `https://${typeof context.repositoryHost === "string" ? context.repositoryHost : "github.com"}/${context.repository}`
124
+ const titled = events.filter((e) => e.type === "session.title_changed").pop()?.data?.title
125
+ const firstUser = events.find((e) => e.type === "user.message" && !e.agentId && isPrompt(e.data?.source))?.data?.content
126
+ const title = titleFrom(titled) ?? titleFrom(ws.summary) ?? titleFrom(ws.name) ?? titleFrom(firstUser)
127
+ const startedAt = iso(start?.startTime) ?? iso(events[0]?.timestamp)
128
+ const updatedAt = iso(events[events.length - 1]?.timestamp) ?? mtime(ref.path)
129
+ const h = header(TOOL, sessionId, {
130
+ ...(typeof start?.copilotVersion === "string" ? { toolVersion: start.copilotVersion } : {}),
131
+ sourcePath: ref.path,
132
+ ...(title ? { title } : {}),
133
+ project,
134
+ ...(startedAt ? { startedAt } : {}),
135
+ ...(updatedAt ? { updatedAt } : {}),
136
+ })
137
+
138
+ // Subagents write into the same journal under their own agentId; each becomes a child session, shown
139
+ // in the parent after the tool call that started it and that call's result.
140
+ // Older events name only the spawning call (parentToolCallId, now deprecated), not the agent.
141
+ const agentOf = (e: Event) => (typeof e.agentId === "string" && e.agentId ? e.agentId : typeof e.data?.parentToolCallId === "string" ? `task-${e.data.parentToolCallId}` : undefined)
142
+ const byAgent = new Map<string, Event[]>()
143
+ for (const e of events) {
144
+ const a = agentOf(e)
145
+ if (!a) continue
146
+ if (!byAgent.has(a)) byAgent.set(a, [])
147
+ byAgent.get(a)!.push(e)
148
+ }
149
+ const started = events.filter((e) => e.type === "subagent.started" && typeof e.data?.toolCallId === "string")
150
+ const turns: Turn[] = eventsToTurns(events.filter((e) => !agentOf(e)), h, lost)
151
+ const children: Session[] = []
152
+ for (const [agentId, own] of byAgent) {
153
+ const callId =
154
+ started.find((e) => e.agentId === agentId)?.data?.toolCallId ??
155
+ own.map((e) => e.data?.parentToolCallId).find((v) => typeof v === "string")
156
+ const info = started.find((e) => e.data?.toolCallId === callId)?.data
157
+ const name = titleFrom(info?.agentDisplayName)
158
+ const from = iso(own[0]?.timestamp)
159
+ const to = iso(own[own.length - 1]?.timestamp)
160
+ const ch = header(TOOL, `${sessionId}/${agentId}`, {
161
+ ...(h.toolVersion ? { toolVersion: h.toolVersion } : {}),
162
+ sourcePath: ref.path,
163
+ ...(name ? { title: name } : {}),
164
+ project,
165
+ ...(from ? { startedAt: from } : {}),
166
+ ...(to ? { updatedAt: to } : {}),
167
+ parent: { id: h.id, kind: "subagent" },
168
+ })
169
+ const childLost = lossCounter()
170
+ const child: Session = { header: ch, turns: eventsToTurns(own, ch, childLost) }
171
+ childLost.flush(ch)
172
+ children.push(child)
173
+ const description = titleFrom(info?.agentDescription) ?? name
174
+ const t: Turn = { kind: "subagent", ...(ch.startedAt ? { at: ch.startedAt } : {}), sessionId: ch.id, ...(typeof callId === "string" ? { callId } : {}), ...(description ? { description } : {}) }
175
+ let i = -1
176
+ turns.forEach((x, j) => {
177
+ if ((x.kind === "tool_call" && x.id === callId) || (x.kind === "tool_result" && x.callId === callId)) i = j
178
+ })
179
+ if (i >= 0) turns.splice(i + 1, 0, t)
180
+ else turns.push(t)
181
+ }
182
+ lost.flush(h)
183
+ return children.length ? { header: h, turns, children } : { header: h, turns }
184
+ },
185
+ }
186
+
@@ -0,0 +1,241 @@
1
+ import { existsSync } from "node:fs"
2
+ import { dirname, join } from "node:path"
3
+ import { readJsonl } from "../../fsutil.ts"
4
+ import type { Attachment, SessionHeader, Turn } from "../../session/types.ts"
5
+ import { asText, clip, iso, noteModel, toolKindOf, type lossCounter } from "../../session/util.ts"
6
+
7
+ // Facts about the Copilot CLI's session journal, shared by its reader and writer. They come from the
8
+ // Copilot SDK's generated event schema (github/copilot-sdk, nodejs/src/generated/session-events.ts, and
9
+ // docs/features/session-persistence.md) and from journals Copilot CLI 1.0.107 wrote itself.
10
+ // See docs/sessions/2026-11-08-session-formats.md, entry 34.
11
+
12
+ export const TOOL = "copilot-cli"
13
+ export const LABEL = "Copilot CLI"
14
+
15
+ /** session.start's `version`, the event schema version, as the SDK and Copilot CLI 1.0.107 write it. */
16
+ export const TESTED_EVENT_VERSION = 1
17
+
18
+ type Lost = ReturnType<typeof lossCounter>
19
+ type Json = Record<string, any>
20
+
21
+ /** One journal line: {id, parentId, timestamp, type, data}, plus agentId for subagent events. */
22
+ export interface Event {
23
+ id?: string
24
+ parentId?: string | null
25
+ timestamp?: string
26
+ type?: string
27
+ data?: Json
28
+ /** The subagent instance that emitted it; absent for the main agent. */
29
+ agentId?: string
30
+ /** Streaming deltas; the CLI does not persist them, but a reader skips them if it meets one. */
31
+ ephemeral?: boolean
32
+ /** Part of a rotated journal's replay prefix rather than original history. */
33
+ checkpoint?: boolean
34
+ }
35
+
36
+ const isObj = (v: unknown): v is Json => typeof v === "object" && v !== null && !Array.isArray(v)
37
+
38
+ /**
39
+ * A session's events in order, archives included. With size rotation on, the CLI moves the journal to
40
+ * events.N.jsonl and starts a new events.jsonl with a session.journal_link to it, then `checkpoint: true`
41
+ * events that replay the state so far. The full history is the predecessor's events followed by this
42
+ * file's events after that replay prefix; when the predecessor is gone, the prefix is all that is left.
43
+ */
44
+ export function readJournal(path: string): { events: Event[]; bad: number; missingArchives: number } {
45
+ let bad = 0
46
+ let missingArchives = 0
47
+ const seen = new Set<string>()
48
+ const read = (file: string): Event[] => {
49
+ seen.add(file)
50
+ const r = readJsonl(file)
51
+ bad += r.bad
52
+ const rows = r.rows.filter(isObj) as Event[]
53
+ const link = rows[0]?.type === "session.journal_link" ? rows[0].data : undefined
54
+ if (!link) return rows
55
+ const prev = typeof link.previousFile === "string" && !link.previousFile.includes("/") ? join(dirname(file), link.previousFile) : undefined
56
+ if (!prev || seen.has(prev) || !existsSync(prev)) {
57
+ missingArchives++
58
+ return rows.slice(1)
59
+ }
60
+ let i = 1
61
+ while (rows[i]?.checkpoint === true) i++
62
+ return [...read(prev), ...rows.slice(i)]
63
+ }
64
+ return { events: read(path), bad, missingArchives }
65
+ }
66
+
67
+ /**
68
+ * Events with rewinds applied: session.snapshot_rewind removes `eventIds` when it lists them, otherwise
69
+ * `upToEventId` and every event after it.
70
+ */
71
+ export function applyRewinds(events: Event[]): { events: Event[]; removed: number } {
72
+ let out: Event[] = []
73
+ let removed = 0
74
+ for (const e of events) {
75
+ if (e.type !== "session.snapshot_rewind") {
76
+ out.push(e)
77
+ continue
78
+ }
79
+ const before = out.length
80
+ const ids = e.data?.eventIds
81
+ if (Array.isArray(ids) && ids.length) {
82
+ const drop = new Set(ids)
83
+ out = out.filter((x) => !drop.has(x.id))
84
+ } else {
85
+ const i = out.findIndex((x) => x.id === e.data?.upToEventId)
86
+ if (i >= 0) out.length = i
87
+ }
88
+ removed += before - out.length
89
+ }
90
+ return { events: out, removed }
91
+ }
92
+
93
+ // Event families that carry no conversation: session bookkeeping, streaming, hooks, permissions, MCP,
94
+ // billing and UI. They are dropped without being counted; any other unhandled type is counted as unknown.
95
+ const NOT_CONVERSATION =
96
+ /^(session\.|hook\.|permission\.|mcp[._]|model\.|skill\.|elicitation\.|sampling\.|command\.|commands\.|capabilities\.|auto_mode|exit_plan_mode\.|user_input\.|workflow\.|tool_search\.|ui\.|pending_messages\.|instruction|system\.|external_tool\.|human_response\.|session_limits|new_inbox|abort$|agent_(idle|completed)$|subagent\.|assistant\.(turn_start|turn_end|turn_retry|idle|intent|message_start|message_delta|reasoning_delta|streaming_delta|tool_call_delta|server_tool_progress|fusion_)|tool\.(execution_progress|execution_partial_result|user_requested|shell_output))/
97
+
98
+ type CallTurn = Extract<Turn, { kind: "tool_call" }>
99
+
100
+ /**
101
+ * Whether a user.message is a prompt rather than injected context. Copilot CLI 1.0.107's timeline shows a
102
+ * message with no source, "user", or a command-, schedule- or autopilot- source; "agent-" is another
103
+ * agent's prompt (a subagent's task). Others ("system" context, "skill-" injections) are not anyone's words.
104
+ */
105
+ export const isPrompt = (source: unknown) => typeof source !== "string" || source === "user" || /^(command|schedule|autopilot|agent)-/.test(source)
106
+
107
+ function attachmentsOf(list: unknown, lost: Lost): Attachment[] {
108
+ const out: Attachment[] = []
109
+ for (const a of Array.isArray(list) ? list : []) {
110
+ if (!isObj(a)) continue
111
+ const name = typeof a.displayName === "string" ? { name: a.displayName } : {}
112
+ if ((a.type === "file" || a.type === "directory") && typeof a.path === "string")
113
+ // Copilot 1.0.70 records a pasted or @mentioned image as a "file" attachment with an image mimeType (plus assetId, byteLength).
114
+ out.push({ kind: typeof a.mimeType === "string" && a.mimeType.startsWith("image/") ? "image" : "file", path: a.path, ...name, ...(typeof a.mimeType === "string" ? { mime: a.mimeType } : {}) })
115
+ else if (a.type === "selection" && typeof a.filePath === "string") out.push({ kind: "file", path: a.filePath, ...name })
116
+ else if (a.type === "blob") {
117
+ out.push({ kind: typeof a.mimeType === "string" && a.mimeType.startsWith("image/") ? "image" : "file", ...name, ...(typeof a.mimeType === "string" ? { mime: a.mimeType } : {}) })
118
+ lost.add("inline attachment bytes")
119
+ } else if (typeof a.type === "string" && a.type.startsWith("github")) lost.add("GitHub reference attachments")
120
+ }
121
+ return out
122
+ }
123
+
124
+ /** A tool result's text as the model got it, and whether the tool succeeded. */
125
+ function resultOf(d: Json, lost: Lost): { text: string; ok: boolean } {
126
+ const r = isObj(d.result) ? d.result : {}
127
+ const text = typeof r.content === "string" ? r.content : typeof r.detailedContent === "string" ? r.detailedContent : asText(r.content ?? "")
128
+ const binary = (Array.isArray(r.binaryResultsForLlm) ? r.binaryResultsForLlm.length : 0) + (Array.isArray(r.contents) ? r.contents.filter((c: Json) => isObj(c) && c.type !== "text" && c.type !== "terminal").length : 0)
129
+ if (binary) lost.add("binary tool output", binary)
130
+ const error = isObj(d.error) && typeof d.error.message === "string" ? d.error.message : undefined
131
+ return { text: [error, text].filter(Boolean).join("\n"), ok: d.success !== false && !error }
132
+ }
133
+
134
+ /**
135
+ * The turns in one agent's events (the main agent's, or one subagent's). Tool calls come from the
136
+ * assistant message's toolRequests, or from tool.execution_start for calls the model did not make (a
137
+ * command the person ran); results come from tool.execution_complete, both keyed by toolCallId.
138
+ */
139
+ export function eventsToTurns(events: Event[], h: SessionHeader, lost: Lost): Turn[] {
140
+ const turns: Turn[] = []
141
+ const calls = new Map<string, CallTurn>()
142
+ const started = new Map<string, Json>()
143
+ const answered = new Set<string>()
144
+ let reasoning = new Set<string>()
145
+ let last: Extract<Turn, { kind: "assistant" }> | undefined
146
+ const call = (id: unknown, name: unknown, input: unknown, at: string | undefined) => {
147
+ if (typeof id !== "string" || !id || calls.has(id)) return
148
+ const n = typeof name === "string" && name ? name : "unknown"
149
+ const t: CallTurn = { kind: "tool_call", ...(at ? { at } : {}), id, name: n, toolKind: toolKindOf(n), input: input ?? {} }
150
+ calls.set(id, t)
151
+ turns.push(t)
152
+ }
153
+ for (const e of events) {
154
+ if (e.ephemeral) continue
155
+ const d = isObj(e.data) ? e.data : {}
156
+ const at = iso(e.timestamp)
157
+ switch (e.type) {
158
+ case "user.message": {
159
+ if (!isPrompt(d.source)) break
160
+ const text = typeof d.content === "string" ? d.content : typeof d.transformedContent === "string" ? d.transformedContent : ""
161
+ const attachments = attachmentsOf(d.attachments, lost)
162
+ if (text.trim() || attachments.length) turns.push({ kind: "user", ...(at ? { at } : {}), text, ...(attachments.length ? { attachments } : {}) })
163
+ break
164
+ }
165
+ case "assistant.reasoning":
166
+ if (typeof d.content === "string" && d.content.trim()) {
167
+ turns.push({ kind: "reasoning", ...(at ? { at } : {}), text: d.content })
168
+ reasoning.add(d.content)
169
+ }
170
+ break
171
+ case "assistant.message": {
172
+ const model = typeof d.model === "string" ? d.model : undefined
173
+ noteModel(h, model)
174
+ // reasoningOpaque (Anthropic) and encryptedContent (OpenAI) are bound to the provider that made them.
175
+ if (d.reasoningOpaque || d.encryptedContent) lost.add("signed or encrypted reasoning")
176
+ // reasoningText repeats the assistant.reasoning event of the same call, when there was one.
177
+ if (typeof d.reasoningText === "string" && d.reasoningText.trim() && !reasoning.has(d.reasoningText)) turns.push({ kind: "reasoning", ...(at ? { at } : {}), text: d.reasoningText })
178
+ reasoning = new Set()
179
+ if (typeof d.content === "string" && d.content.trim()) {
180
+ last = { kind: "assistant", ...(at ? { at } : {}), text: d.content, ...(model ? { model } : {}) }
181
+ turns.push(last)
182
+ }
183
+ for (const r of Array.isArray(d.toolRequests) ? d.toolRequests : []) if (isObj(r)) call(r.toolCallId, r.name, r.arguments, at)
184
+ break
185
+ }
186
+ case "tool.execution_start":
187
+ if (typeof d.toolCallId === "string") started.set(d.toolCallId, d)
188
+ call(d.toolCallId, d.toolName, d.arguments, at)
189
+ break
190
+ case "tool.execution_complete": {
191
+ if (typeof d.toolCallId !== "string" || answered.has(d.toolCallId)) break
192
+ const s = started.get(d.toolCallId)
193
+ call(d.toolCallId, s?.toolName, s?.arguments, at)
194
+ const r = resultOf(d, lost)
195
+ const c = clip(r.text)
196
+ turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId: d.toolCallId, ok: r.ok, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
197
+ answered.add(d.toolCallId)
198
+ break
199
+ }
200
+ case "assistant.usage":
201
+ if (last && !last.usage) {
202
+ const u = { input: d.inputTokens, output: d.outputTokens, cacheRead: d.cacheReadTokens, cacheWrite: d.cacheWriteTokens }
203
+ const usage = Object.fromEntries(Object.entries(u).filter(([, v]) => typeof v === "number"))
204
+ if (Object.keys(usage).length) last.usage = usage
205
+ }
206
+ noteModel(h, typeof d.model === "string" ? d.model : undefined)
207
+ break
208
+ case "session.compaction_complete":
209
+ if (d.success !== false && typeof d.summaryContent === "string" && d.summaryContent.trim()) turns.push({ kind: "compaction", ...(at ? { at } : {}), summary: d.summaryContent })
210
+ break
211
+ default:
212
+ if (typeof e.type !== "string" || !NOT_CONVERSATION.test(e.type)) lost.add(`unknown record types: ${String(e.type)}`)
213
+ }
214
+ }
215
+ // Calls the journal never answered (interrupted, or still running when it was copied) get a result
216
+ // saying so, right after the call, so every call stays paired.
217
+ const open = [...calls.values()].filter((c) => !answered.has(c.id))
218
+ if (!open.length) return turns
219
+ lost.add("tool results not recorded", open.length)
220
+ const waiting = new Set(open)
221
+ return turns.flatMap((t) => (t.kind === "tool_call" && waiting.has(t) ? [t, { kind: "tool_result" as const, ...(t.at ? { at: t.at } : {}), callId: t.id, ok: false, output: "(no result recorded)" }] : [t]))
222
+ }
223
+
224
+ /** Top-level `key: value` lines of workspace.yaml (id, cwd, branch, summary, created_at, updated_at…). */
225
+ export function workspaceYaml(text: string | undefined): Record<string, string> {
226
+ const out: Record<string, string> = {}
227
+ for (const line of (text ?? "").split("\n")) {
228
+ const m = line.match(/^([A-Za-z_][\w-]*):\s*(.*?)\s*$/)
229
+ if (!m || !m[2] || m[2] === "|" || m[2] === ">") continue
230
+ let v = m[2]
231
+ if (v.startsWith('"') && v.endsWith('"')) {
232
+ try {
233
+ v = JSON.parse(v)
234
+ } catch {
235
+ v = v.slice(1, -1)
236
+ }
237
+ } else if (v.startsWith("'") && v.endsWith("'")) v = v.slice(1, -1).replace(/''/g, "'")
238
+ out[m[1]!] = v
239
+ }
240
+ return out
241
+ }