@100xprompt/brain 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +172 -2
  3. package/package.json +48 -4
  4. package/src/adapters/aider/index.ts +8 -0
  5. package/src/adapters/aider/reader.ts +129 -0
  6. package/src/adapters/aider/records.ts +220 -0
  7. package/src/adapters/aider/writer.ts +269 -0
  8. package/src/adapters/amazon-q-cli/index.ts +8 -0
  9. package/src/adapters/amazon-q-cli/reader.ts +84 -0
  10. package/src/adapters/amazon-q-cli/records.ts +191 -0
  11. package/src/adapters/amazon-q-cli/writer.ts +370 -0
  12. package/src/adapters/amazon-q-ide/index.ts +9 -0
  13. package/src/adapters/amazon-q-ide/reader.ts +131 -0
  14. package/src/adapters/amazon-q-ide/records.ts +141 -0
  15. package/src/adapters/amazon-q-ide/writer.ts +325 -0
  16. package/src/adapters/antigravity/index.ts +9 -0
  17. package/src/adapters/antigravity/proto.ts +194 -0
  18. package/src/adapters/antigravity/reader.ts +424 -0
  19. package/src/adapters/antigravity/records.ts +236 -0
  20. package/src/adapters/antigravity/writer.ts +463 -0
  21. package/src/adapters/auggie/index.ts +36 -0
  22. package/src/adapters/claude-code/index.ts +8 -0
  23. package/src/adapters/claude-code/reader.ts +639 -0
  24. package/src/adapters/claude-code/records.ts +88 -0
  25. package/src/adapters/claude-code/writer.ts +329 -0
  26. package/src/adapters/cline/index.ts +12 -0
  27. package/src/adapters/cline/reader.ts +432 -0
  28. package/src/adapters/cline/records.ts +151 -0
  29. package/src/adapters/cline/vscdb.ts +56 -0
  30. package/src/adapters/cline/writer.ts +656 -0
  31. package/src/adapters/cline/xml.ts +177 -0
  32. package/src/adapters/cline4/index.ts +9 -0
  33. package/src/adapters/cline4/reader.ts +179 -0
  34. package/src/adapters/cline4/records.ts +92 -0
  35. package/src/adapters/cline4/writer.ts +361 -0
  36. package/src/adapters/codebuff/index.ts +8 -0
  37. package/src/adapters/codebuff/project.ts +207 -0
  38. package/src/adapters/codebuff/reader.ts +215 -0
  39. package/src/adapters/codebuff/records.ts +173 -0
  40. package/src/adapters/codebuff/writer.ts +383 -0
  41. package/src/adapters/codex/index.ts +8 -0
  42. package/src/adapters/codex/native.ts +239 -0
  43. package/src/adapters/codex/reader.ts +747 -0
  44. package/src/adapters/codex/records.ts +114 -0
  45. package/src/adapters/codex/writer.ts +479 -0
  46. package/src/adapters/continue/index.ts +9 -0
  47. package/src/adapters/continue/reader.ts +68 -0
  48. package/src/adapters/continue/records.ts +418 -0
  49. package/src/adapters/continue/writer.ts +162 -0
  50. package/src/adapters/copilot-cli/index.ts +9 -0
  51. package/src/adapters/copilot-cli/reader.ts +186 -0
  52. package/src/adapters/copilot-cli/records.ts +241 -0
  53. package/src/adapters/copilot-cli/writer.ts +353 -0
  54. package/src/adapters/crush/index.ts +8 -0
  55. package/src/adapters/crush/reader.ts +180 -0
  56. package/src/adapters/crush/records.ts +169 -0
  57. package/src/adapters/crush/writer.ts +333 -0
  58. package/src/adapters/cursor-cli/index.ts +405 -0
  59. package/src/adapters/cursor-ide/agentkv.ts +95 -0
  60. package/src/adapters/cursor-ide/index.ts +501 -0
  61. package/src/adapters/cursor-ide/transcripts.ts +122 -0
  62. package/src/adapters/cursor-ide/workspaces.ts +119 -0
  63. package/src/adapters/cursor-ide/writer.ts +170 -0
  64. package/src/adapters/devin/index.ts +8 -0
  65. package/src/adapters/devin/reader.ts +305 -0
  66. package/src/adapters/devin/records.ts +277 -0
  67. package/src/adapters/devin/writer.ts +523 -0
  68. package/src/adapters/factory-droid/index.ts +12 -0
  69. package/src/adapters/factory-droid/reader.ts +184 -0
  70. package/src/adapters/factory-droid/records.ts +79 -0
  71. package/src/adapters/factory-droid/writer.ts +350 -0
  72. package/src/adapters/gemini-cli/exchanges.ts +228 -0
  73. package/src/adapters/gemini-cli/index.ts +396 -0
  74. package/src/adapters/gemini-cli/records.ts +521 -0
  75. package/src/adapters/gemini-cli/writer.ts +354 -0
  76. package/src/adapters/goose/index.ts +8 -0
  77. package/src/adapters/goose/reader.ts +138 -0
  78. package/src/adapters/goose/records.ts +199 -0
  79. package/src/adapters/goose/writer.ts +340 -0
  80. package/src/adapters/grok-cli/image.ts +44 -0
  81. package/src/adapters/grok-cli/index.ts +8 -0
  82. package/src/adapters/grok-cli/reader.ts +302 -0
  83. package/src/adapters/grok-cli/records.ts +172 -0
  84. package/src/adapters/grok-cli/writer.ts +329 -0
  85. package/src/adapters/hermes/index.ts +8 -0
  86. package/src/adapters/hermes/reader.ts +249 -0
  87. package/src/adapters/hermes/records.ts +285 -0
  88. package/src/adapters/hermes/writer.ts +452 -0
  89. package/src/adapters/iflow-cli/index.ts +72 -0
  90. package/src/adapters/iflow-cli/records.ts +215 -0
  91. package/src/adapters/iflow-cli/writer.ts +204 -0
  92. package/src/adapters/index.ts +112 -0
  93. package/src/adapters/kilo-cli/index.ts +21 -0
  94. package/src/adapters/kilo-code/index.ts +11 -0
  95. package/src/adapters/kilo-code/writer.ts +92 -0
  96. package/src/adapters/kimi/index.ts +9 -0
  97. package/src/adapters/kimi/reader.ts +92 -0
  98. package/src/adapters/kimi/records.ts +221 -0
  99. package/src/adapters/kimi/writer.ts +367 -0
  100. package/src/adapters/kiro-cli/index.ts +8 -0
  101. package/src/adapters/kiro-cli/reader.ts +87 -0
  102. package/src/adapters/kiro-cli/records.ts +260 -0
  103. package/src/adapters/kiro-cli/writer.ts +305 -0
  104. package/src/adapters/kiro-ide/index.ts +8 -0
  105. package/src/adapters/kiro-ide/writer.ts +52 -0
  106. package/src/adapters/letta/index.ts +8 -0
  107. package/src/adapters/letta/reader.ts +177 -0
  108. package/src/adapters/letta/records.ts +119 -0
  109. package/src/adapters/letta/writer.ts +413 -0
  110. package/src/adapters/mastracode/index.ts +8 -0
  111. package/src/adapters/mastracode/reader.ts +323 -0
  112. package/src/adapters/mastracode/records.ts +336 -0
  113. package/src/adapters/mastracode/writer.ts +385 -0
  114. package/src/adapters/mistral-vibe/index.ts +9 -0
  115. package/src/adapters/mistral-vibe/reader.ts +62 -0
  116. package/src/adapters/mistral-vibe/records.ts +104 -0
  117. package/src/adapters/mistral-vibe/writer.ts +359 -0
  118. package/src/adapters/muse/index.ts +10 -0
  119. package/src/adapters/muse/reader.ts +332 -0
  120. package/src/adapters/muse/records.ts +104 -0
  121. package/src/adapters/muse/writer.ts +387 -0
  122. package/src/adapters/omp/index.ts +13 -0
  123. package/src/adapters/omp/reader.ts +314 -0
  124. package/src/adapters/omp/records.ts +153 -0
  125. package/src/adapters/omp/writer.ts +216 -0
  126. package/src/adapters/opencode/ids.ts +52 -0
  127. package/src/adapters/opencode/index.ts +31 -0
  128. package/src/adapters/opencode/note.ts +8 -0
  129. package/src/adapters/opencode/reader.ts +452 -0
  130. package/src/adapters/opencode/tools.ts +174 -0
  131. package/src/adapters/opencode/v2.ts +367 -0
  132. package/src/adapters/opencode/writer.ts +379 -0
  133. package/src/adapters/openhands/index.ts +8 -0
  134. package/src/adapters/openhands/reader.ts +222 -0
  135. package/src/adapters/openhands/records.ts +136 -0
  136. package/src/adapters/openhands/writer.ts +404 -0
  137. package/src/adapters/pearai/index.ts +10 -0
  138. package/src/adapters/pi/index.ts +28 -0
  139. package/src/adapters/pi/reader.ts +261 -0
  140. package/src/adapters/pi/records.ts +99 -0
  141. package/src/adapters/pi/writer.ts +362 -0
  142. package/src/adapters/qoder-cli/index.ts +8 -0
  143. package/src/adapters/qoder-cli/reader.ts +83 -0
  144. package/src/adapters/qoder-cli/records.ts +45 -0
  145. package/src/adapters/qoder-cli/writer.ts +72 -0
  146. package/src/adapters/qwen-code/index.ts +84 -0
  147. package/src/adapters/qwen-code/records.ts +194 -0
  148. package/src/adapters/qwen-code/writer.ts +224 -0
  149. package/src/adapters/tabnine-cli/index.ts +8 -0
  150. package/src/adapters/tabnine-cli/reader.ts +106 -0
  151. package/src/adapters/tabnine-cli/records.ts +33 -0
  152. package/src/adapters/tabnine-cli/writer.ts +156 -0
  153. package/src/adapters/types.ts +73 -0
  154. package/src/adapters/void/index.ts +9 -0
  155. package/src/adapters/void/reader.ts +48 -0
  156. package/src/adapters/void/records.ts +285 -0
  157. package/src/adapters/void/writer.ts +93 -0
  158. package/src/adapters/vscode-copilot/index.ts +9 -0
  159. package/src/adapters/vscode-copilot/reader.ts +212 -0
  160. package/src/adapters/vscode-copilot/records.ts +388 -0
  161. package/src/adapters/vscode-copilot/shared.ts +68 -0
  162. package/src/adapters/vscode-copilot/writer.ts +546 -0
  163. package/src/adapters/zed/index.ts +8 -0
  164. package/src/adapters/zed/reader.ts +198 -0
  165. package/src/adapters/zed/records.ts +87 -0
  166. package/src/adapters/zed/writer.ts +341 -0
  167. package/src/brief-writers.ts +71 -0
  168. package/src/brief.ts +238 -0
  169. package/src/cli.ts +326 -0
  170. package/src/fsutil.ts +133 -0
  171. package/src/gateway/connect.ts +845 -0
  172. package/src/gateway/signin.ts +323 -0
  173. package/src/handoff.ts +104 -0
  174. package/src/hooks.ts +455 -0
  175. package/src/memory/audit.ts +514 -0
  176. package/src/memory/blobs.ts +94 -0
  177. package/src/memory/cli.ts +117 -0
  178. package/src/memory/distill.ts +381 -0
  179. package/src/memory/externalize.ts +435 -0
  180. package/src/memory/lookups.ts +70 -0
  181. package/src/memory/mcp-install.ts +401 -0
  182. package/src/memory/mcp-server.ts +296 -0
  183. package/src/memory/prepare.ts +179 -0
  184. package/src/memory/preview.ts +154 -0
  185. package/src/memory/project.ts +35 -0
  186. package/src/memory/redact.ts +72 -0
  187. package/src/memory/relevance.ts +187 -0
  188. package/src/memory/repomap.ts +516 -0
  189. package/src/memory/store.ts +270 -0
  190. package/src/memory/sync.ts +166 -0
  191. package/src/memory/tidy.ts +193 -0
  192. package/src/memory/types.ts +118 -0
  193. package/src/org/admin-ops.ts +96 -0
  194. package/src/org/auth.ts +112 -0
  195. package/src/org/backup.ts +115 -0
  196. package/src/org/cli.ts +438 -0
  197. package/src/org/client.ts +404 -0
  198. package/src/org/config.ts +44 -0
  199. package/src/org/crypto.ts +537 -0
  200. package/src/org/db.ts +435 -0
  201. package/src/org/graph.ts +318 -0
  202. package/src/org/oidc.ts +435 -0
  203. package/src/org/policy.ts +398 -0
  204. package/src/org/server.ts +920 -0
  205. package/src/org/sso.ts +316 -0
  206. package/src/org/tls.ts +66 -0
  207. package/src/org/types.ts +193 -0
  208. package/src/self.ts +68 -0
  209. package/src/session/attachments.ts +53 -0
  210. package/src/session/peekcache.ts +50 -0
  211. package/src/session/types.ts +104 -0
  212. package/src/session/util.ts +130 -0
  213. package/src/sqlite.ts +48 -0
@@ -0,0 +1,68 @@
1
+ import { copyFileSync, mkdirSync } from "node:fs"
2
+ import { join } from "node:path"
3
+ import { exists } from "../../fsutil.ts"
4
+ import type { ToolKind } from "../../session/types.ts"
5
+
6
+ // Helpers the VS Code chat family's writers share (Copilot Chat, Continue, PearAI, Void): picking the
7
+ // arguments a tool call names in another tool's vocabulary, shell quoting, and the backup taken before
8
+ // a writer changes a database another program owns.
9
+
10
+ type Json = Record<string, unknown>
11
+
12
+ const isObj = (v: unknown): v is Json => typeof v === "object" && v !== null && !Array.isArray(v)
13
+
14
+ /** The first non-empty string among `keys` in a tool call's input (arrays of strings are joined with spaces). */
15
+ export function pick(input: unknown, keys: string[]): string | undefined {
16
+ if (!isObj(input)) return undefined
17
+ for (const k of keys) {
18
+ const v = input[k]
19
+ if (typeof v === "string" && v) return v
20
+ if (Array.isArray(v) && v.length && v.every((x) => typeof x === "string")) return v.join(" ")
21
+ }
22
+ return undefined
23
+ }
24
+
25
+ /** Argument names other tools use for the same thing. */
26
+ export const ARG = {
27
+ path: ["file_path", "filePath", "filepath", "path", "absolute_path", "target_file", "file", "uri"],
28
+ dir: ["dir_path", "dirPath", "path", "directory", "target_directory", "relative_workspace_path", "dir"],
29
+ content: ["content", "contents", "file_text", "text", "code"],
30
+ old: ["old_string", "oldString", "old_str", "old_text"],
31
+ new: ["new_string", "newString", "new_str", "new_text"],
32
+ command: ["command", "cmd", "commandLine"],
33
+ query: ["query", "pattern", "regex", "search", "q"],
34
+ url: ["url", "uri", "href"],
35
+ }
36
+
37
+ /** A tool call in the target's own terms, when its kind has an equivalent there; undefined keeps it as it was. */
38
+ export type ToolMap = Partial<Record<ToolKind, (input: unknown) => { name: string; args: Json } | undefined>>
39
+
40
+ export function mapTool(map: ToolMap, kind: ToolKind, input: unknown): { name: string; args: Json } | undefined {
41
+ return map[kind]?.(input)
42
+ }
43
+
44
+ /** Tool names every provider accepts: letters, digits, _ and -, at most 64. */
45
+ export const safeToolName = (name: string) => name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "tool"
46
+
47
+ /** A tool's input as an object, as the targets' tool-call records require. */
48
+ export const asArgs = (input: unknown): Json => (isObj(input) ? input : input === undefined || input === null ? {} : { input })
49
+
50
+ export const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
51
+
52
+ /**
53
+ * Copies a database another program owns (with its -wal and -shm files) to ~/.brain/backups/<tool>/<stamp>/
54
+ * before a writer changes it. Returns the folder; nothing is copied for files that do not exist yet.
55
+ */
56
+ export function backup(home: string, tool: string, files: string[], now = new Date()): string {
57
+ const dir = join(home, ".brain", "backups", tool, now.toISOString().replace(/[:.]/g, "-"))
58
+ for (const f of files) {
59
+ for (const side of ["", "-wal", "-shm"]) {
60
+ if (!exists(f + side)) continue
61
+ // Files from different folders can share a name (two state.vscdb); keep their folder's name too.
62
+ const name = `${f.split("/").slice(-2).join("__")}${side}`
63
+ mkdirSync(dir, { recursive: true })
64
+ copyFileSync(f + side, join(dir, name))
65
+ }
66
+ }
67
+ return dir
68
+ }
@@ -0,0 +1,546 @@
1
+ import { Database } from "bun:sqlite"
2
+ import { createHash, randomUUID } from "node:crypto"
3
+ import { mkdirSync, rmSync, statSync, writeFileSync } from "node:fs"
4
+ import { join } from "node:path"
5
+ import { toolLabel } from "../../brief.ts"
6
+ import { appSupport, exists, isDir, isRunning, readJson } from "../../fsutil.ts"
7
+ import { imageDataUrl, parseDataUrl } from "../../session/attachments.ts"
8
+ import type { Attachment, Session, Turn } from "../../session/types.ts"
9
+ import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
10
+ import { continuedNote } from "../claude-code/records.ts"
11
+ import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
12
+ import { reader } from "./reader.ts"
13
+ import { LABEL, TESTED_VERSION, TOOL } from "./records.ts"
14
+ import { ARG, type ToolMap, asArgs, backup, mapTool, pick, safeToolName, shellQuote } from "./shared.ts"
15
+
16
+ // Continues a portable session in Copilot Chat in VS Code as a new local chat: the session is written the
17
+ // way VS Code 1.144 stores its own (chatSessionStore.ts and chatSessionOperationLog.ts at the 1.144 commit
18
+ // 2c33b50b), and an entry is added to the chat index so the Chat view's history lists it:
19
+ //
20
+ // - User/workspaceStorage/<workspace id>/chatSessions/<uuid>.jsonl: a mutation log holding one Initial
21
+ // entry, {kind: 0, v: ISerializableChatData v3}, which is what VS Code writes for a new session.
22
+ // - The same folder's state.vscdb, ItemTable key 'chat.ChatSessionStore.index': {version: 1, entries}.
23
+ // VS Code lists history from this index, not from the folder, and throws the whole index away if any
24
+ // entry is malformed, so the entry is built as getSessionMetadata builds it and merged, never replaced.
25
+ //
26
+ // Each request carries two records of its reply, as Copilot's agent does. The response parts are what
27
+ // the chat shows (markdown, tool invocations, thinking). result.metadata.toolCallRounds and
28
+ // toolCallResults are what Copilot Chat 0.72 replays to the model on the next request
29
+ // (AgentConversationHistory, ChatToolCalls): each round is one model call with its text and tool calls,
30
+ // and each result is a LanguageModelToolResult marshalled with $mid 20/21. The request's agent is
31
+ // github.copilot.editsAgent: Copilot keeps result metadata in history only for its own participants.
32
+ //
33
+ // The note saying where the session came from goes in the first request's renderedUserMessage, which
34
+ // Copilot sends to the model in place of the request text while the chat shows only the person's words.
35
+ // A compaction becomes a system-initiated request whose single round carries the summary
36
+ // (metadata.summaries), which Copilot renders as <conversation-summary> in place of everything before it.
37
+ //
38
+ // state.vscdb is a database VS Code keeps open and rewrites from memory, so this writer runs only with
39
+ // VS Code closed and backs the file up first.
40
+
41
+ export const TESTED_VSCODE = ["1.144"]
42
+ export const TESTED_COPILOT = ["0.72"]
43
+
44
+ const APP = "Visual Studio Code.app"
45
+ const INDEX_KEY = "chat.ChatSessionStore.index"
46
+ const AGENT_ID = "github.copilot.editsAgent"
47
+
48
+ /** The installed VS Code and its built-in Copilot Chat, read from the app bundle. */
49
+ export function installedVSCode(home: string): { vscode?: string; copilot?: string } {
50
+ for (const root of ["/Applications", join(home, "Applications")]) {
51
+ const app = join(root, APP, "Contents", "Resources", "app")
52
+ const v = readJson<{ version?: string }>(join(app, "package.json"))?.version
53
+ if (!v) continue
54
+ const c = readJson<{ version?: string }>(join(app, "extensions", "copilot", "package.json"))?.version
55
+ return { vscode: v, ...(c ? { copilot: c } : {}) }
56
+ }
57
+ return {}
58
+ }
59
+
60
+ const line = (v: string | undefined) => v?.split(".").slice(0, 2).join(".")
61
+
62
+ /**
63
+ * VS Code's id for a single-folder workspace (workspaces.ts getSingleFolderWorkspaceIdentifier): the md5 of
64
+ * the folder path and, on macOS, its birth time in milliseconds. Node rounds the birth time to whole
65
+ * milliseconds where Bun truncates, so it is rounded here as VS Code (on Node) sees it.
66
+ */
67
+ export function workspaceId(folder: string): string {
68
+ const birth = Math.round(statSync(folder).birthtimeMs)
69
+ return createHash("md5").update(folder).update(String(birth)).digest("hex")
70
+ }
71
+
72
+ /** A file:// URI as VS Code's URI.toString() writes it: everything but unreserved characters and / is escaped. */
73
+ export function fileUri(path: string): string {
74
+ let out = ""
75
+ for (const ch of path) out += /^[A-Za-z0-9\-._~/]$/.test(ch) ? ch : encodeURIComponent(ch)
76
+ return `file://${out}`
77
+ }
78
+
79
+ /** VS Code's user folder under a home. */
80
+ export const userDir = (home: string) => join(appSupport(home), "Code", "User")
81
+
82
+ /** The agent descriptor VS Code serializes with each request (IChatAgentData of Copilot's agent mode). */
83
+ function agentData(extensionVersion: string) {
84
+ return {
85
+ extensionId: { value: "GitHub.copilot-chat", _lower: "github.copilot-chat" },
86
+ extensionVersion,
87
+ publisherDisplayName: "GitHub",
88
+ extensionPublisherId: "GitHub",
89
+ extensionDisplayName: "GitHub Copilot Chat",
90
+ id: AGENT_ID,
91
+ description: "Edit files in your workspace in agent mode",
92
+ when: "config.chat.agent.enabled",
93
+ metadata: { themeIcon: { id: "tools" }, hasFollowups: false, supportIssueReporting: false },
94
+ name: "agent",
95
+ fullName: "GitHub Copilot",
96
+ isDefault: true,
97
+ locations: ["panel"],
98
+ modes: ["agent"],
99
+ slashCommands: [],
100
+ disambiguation: [],
101
+ }
102
+ }
103
+
104
+ // Copilot's own names for what the call does (as its model sees them in 0.72), where the arguments map
105
+ // cleanly; anything else keeps its name.
106
+ const TOOLS: ToolMap = {
107
+ read: (i) => {
108
+ const filePath = pick(i, ARG.path)
109
+ return filePath ? { name: "read_file", args: { filePath } } : undefined
110
+ },
111
+ write: (i) => {
112
+ const filePath = pick(i, ARG.path)
113
+ const content = pick(i, ARG.content)
114
+ return filePath && content !== undefined ? { name: "create_file", args: { filePath, content } } : undefined
115
+ },
116
+ edit: (i) => {
117
+ const filePath = pick(i, ARG.path)
118
+ const oldString = pick(i, ARG.old)
119
+ const newString = pick(i, ARG.new)
120
+ return filePath && oldString !== undefined && newString !== undefined ? { name: "replace_string_in_file", args: { filePath, oldString, newString } } : undefined
121
+ },
122
+ shell: (i) => {
123
+ const command = pick(i, ARG.command)
124
+ return command ? { name: "run_in_terminal", args: { command, explanation: "", isBackground: false } } : undefined
125
+ },
126
+ list: (i) => {
127
+ const path = pick(i, ARG.dir)
128
+ return path ? { name: "list_dir", args: { path } } : undefined
129
+ },
130
+ search: (i) => {
131
+ const query = pick(i, ARG.query)
132
+ return query ? { name: "grep_search", args: { query, isRegexp: false } } : undefined
133
+ },
134
+ }
135
+
136
+ const markdown = (value: string) => ({ value, supportThemeIcons: false, supportHtml: false })
137
+
138
+ /** A parsed request message with one text part, as ChatRequestTextPart serializes. */
139
+ function message(text: string) {
140
+ const lines = text.split("\n")
141
+ return {
142
+ text,
143
+ parts: [
144
+ {
145
+ range: { start: 0, endExclusive: text.length },
146
+ editorRange: { startLineNumber: 1, startColumn: 1, endLineNumber: lines.length, endColumn: lines[lines.length - 1]!.length + 1 },
147
+ text,
148
+ kind: "text",
149
+ },
150
+ ],
151
+ }
152
+ }
153
+
154
+ type Call = Extract<Turn, { kind: "tool_call" }>
155
+ type Result = Extract<Turn, { kind: "tool_result" }>
156
+
157
+ interface Round {
158
+ id: string
159
+ response: string
160
+ calls: Array<{ id: string; name: string; arguments: string; source: Call }>
161
+ /** Reasoning shown before this round in the chat. */
162
+ thinking: string[]
163
+ /** A result for one of this round's calls has come back: the model call is over. */
164
+ closed: boolean
165
+ summary?: string
166
+ }
167
+
168
+ interface Req {
169
+ at?: number
170
+ text: string
171
+ /** A request the chat shows as a system notification: the continued-from note or a compaction. */
172
+ system?: string
173
+ /** This request's text is the continued-from note itself. */
174
+ isNote?: boolean
175
+ rounds: Round[]
176
+ /** Pasted images, as the chat's attachment entries (variableData.variables). */
177
+ images?: Record<string, unknown>[]
178
+ model?: string
179
+ usage?: { input?: number; output?: number }
180
+ }
181
+
182
+ export interface ChatPlan {
183
+ /** The ISerializableChatData written as the log's Initial entry. */
184
+ data: Record<string, unknown>
185
+ /** The session's entry in chat.ChatSessionStore.index. */
186
+ entry: Record<string, unknown>
187
+ lost: string[]
188
+ /** What a read-back must find. */
189
+ expect: { users: number; calls: number }
190
+ }
191
+
192
+ /**
193
+ * A pasted image as VS Code 1.144 stores it in a request's variableData.variables: the chat attachment entry
194
+ * {kind:"image", id, name, value, mimeType, isPasted, references} with the bytes as {$base64} (mw.toExport turns a Uint8Array
195
+ * value into that and mw.fromExport back; Copilot Chat then builds a ChatReferenceBinaryData from value and mimeType, and
196
+ * ignores an entry with no value). Undefined when the image is gone, too large or not a format Copilot takes.
197
+ */
198
+ export function imageVariable(a: Attachment): Record<string, unknown> | undefined {
199
+ const url = imageDataUrl(a)
200
+ const img = url ? parseDataUrl(url) : undefined
201
+ if (!img) return undefined
202
+ return {
203
+ kind: "image",
204
+ id: createHash("sha256").update(Buffer.from(img.base64, "base64")).digest("hex"),
205
+ name: a.name ?? "Pasted Image",
206
+ icon: { id: "file-media" },
207
+ value: { $base64: img.base64 },
208
+ mimeType: img.mime,
209
+ isPasted: true,
210
+ references: [],
211
+ }
212
+ }
213
+
214
+ /** The chat VS Code would store for `session` continued as local chat `sessionId`. Pure. */
215
+ export function planChat(session: Session, opts: { sessionId: string; now: Date; extensionVersion: string }): ChatPlan {
216
+ const label = toolLabel(session.header.tool)
217
+ const drops = new Map<string, number>()
218
+ const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
219
+ const results = new Map<string, Result>()
220
+ const callIds = new Set<string>()
221
+ for (const t of session.turns) {
222
+ if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
223
+ if (t.kind === "tool_call") callIds.add(t.id)
224
+ }
225
+ const note = continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10))
226
+
227
+ const reqs: Req[] = []
228
+ let req: Req | undefined
229
+ let round: Round | undefined
230
+ let pendingThinking: string[] = []
231
+ let model = session.header.models.at(-1)
232
+ const seen = new Set<string>()
233
+ const roundId = () => randomUUID()
234
+ // Call ids: kept when every provider accepts them, else replaced, the same way for the call and its result.
235
+ const ids = new Map<string, string>()
236
+ const callId = (id: string) => {
237
+ let v = ids.get(id)
238
+ if (!v) {
239
+ v = /^[A-Za-z0-9_-]{1,64}$/.test(id) && ![...ids.values()].includes(id) ? id : `call_${randomUUID().replace(/-/g, "").slice(0, 24)}`
240
+ ids.set(id, v)
241
+ }
242
+ return v
243
+ }
244
+ const at = (s?: string) => {
245
+ const t = s ? Date.parse(s) : Number.NaN
246
+ return Number.isFinite(t) ? t : undefined
247
+ }
248
+
249
+ const openRequest = (text: string, when?: string, system?: string, isNote = false) => {
250
+ req = { text, rounds: [], ...(system ? { system } : {}), ...(isNote ? { isNote } : {}), ...(at(when) ? { at: at(when) } : {}) }
251
+ reqs.push(req)
252
+ round = undefined
253
+ return req
254
+ }
255
+ const currentRound = (when?: string): Round => {
256
+ // Assistant output before any request opens one that carries the note in place of a person's words.
257
+ if (!req) openRequest(note, when, `Continued from ${label}`, true)
258
+ if (!round || round.closed) {
259
+ round = { id: roundId(), response: "", calls: [], thinking: pendingThinking, closed: false }
260
+ pendingThinking = []
261
+ req!.rounds.push(round)
262
+ }
263
+ return round
264
+ }
265
+
266
+ for (const t of session.turns) {
267
+ switch (t.kind) {
268
+ case "user": {
269
+ let text = t.text
270
+ const images: Record<string, unknown>[] = []
271
+ for (const a of t.attachments ?? []) {
272
+ if (a.kind === "file" && a.path) text += `\n\n[Attached file: ${a.path}]`
273
+ else {
274
+ const v = imageVariable(a)
275
+ if (v) images.push(v)
276
+ else drop("attachments")
277
+ }
278
+ }
279
+ if (!text.trim()) {
280
+ if (images.length) drop("attachments without text", images.length)
281
+ break
282
+ }
283
+ if (pendingThinking.length) drop("reasoning with no reply after it", pendingThinking.length)
284
+ pendingThinking = []
285
+ openRequest(text, t.at).images = images
286
+ break
287
+ }
288
+ case "assistant": {
289
+ if (!t.text.trim()) break
290
+ if (t.model) model = t.model
291
+ // Text after a tool call is the next model call.
292
+ if (round?.calls.length) round.closed = true
293
+ const r = currentRound(t.at)
294
+ r.response = r.response ? `${r.response}\n\n${t.text}` : t.text
295
+ req!.model = model
296
+ if (t.usage) req!.usage = { input: (req!.usage?.input ?? 0) + (t.usage.input ?? 0), output: (req!.usage?.output ?? 0) + (t.usage.output ?? 0) }
297
+ if (!req!.at && at(t.at)) req!.at = at(t.at)
298
+ break
299
+ }
300
+ case "tool_call": {
301
+ if (seen.has(t.id)) {
302
+ drop("repeated tool call ids")
303
+ break
304
+ }
305
+ seen.add(t.id)
306
+ const r = currentRound(t.at)
307
+ const mapped = mapTool(TOOLS, t.toolKind, t.input)
308
+ r.calls.push({ id: callId(t.id), name: mapped?.name ?? safeToolName(t.name), arguments: JSON.stringify(mapped?.args ?? asArgs(t.input)), source: t })
309
+ if (model) req!.model = model
310
+ break
311
+ }
312
+ case "tool_result":
313
+ if (!callIds.has(t.callId)) drop("tool results without a call")
314
+ else if (round?.calls.some((c) => c.source.id === t.callId)) round.closed = true
315
+ break
316
+ case "reasoning":
317
+ // Readable reasoning is shown as the chat's thinking part; Copilot replays only its provider's own.
318
+ if (t.text.trim()) {
319
+ pendingThinking.push(t.text)
320
+ drop("reasoning (shown in the chat, not sent back to the model)")
321
+ }
322
+ break
323
+ case "compaction": {
324
+ const r = openRequest(`Summary of the earlier conversation in ${label}`, t.at, `Earlier conversation in ${label}, summarized`)
325
+ round = { id: roundId(), response: "", calls: [], thinking: [], closed: true, summary: t.summary }
326
+ r.rounds.push(round)
327
+ break
328
+ }
329
+ case "file_edit":
330
+ if (!t.callId || !callIds.has(t.callId)) drop("file edits outside a tool call")
331
+ break
332
+ case "subagent":
333
+ drop("subagent sessions (their call and result are kept)")
334
+ break
335
+ }
336
+ }
337
+ if (pendingThinking.length) drop("reasoning with no reply after it", pendingThinking.length)
338
+ if (!reqs.length) throw new Error("nothing to continue: the session has no messages")
339
+
340
+ // Requests, with timestamps that never go backwards.
341
+ let clock = (reqs.find((r) => r.at)?.at ?? opts.now.getTime()) - 1
342
+ const tick = (t?: number) => (clock = t && t > clock ? t : clock + 1)
343
+ const agent = agentData(opts.extensionVersion)
344
+ const copilotSession = randomUUID()
345
+ let users = 0
346
+ let calls = 0
347
+ let noteGiven = reqs.some((r) => r.isNote)
348
+ const requests = reqs.map((r) => {
349
+ const timestamp = tick(r.at)
350
+ const toolCallResults: Record<string, unknown> = {}
351
+ const response: unknown[] = []
352
+ for (const round of r.rounds) {
353
+ for (const text of round.thinking) response.push({ kind: "thinking", value: text })
354
+ if (round.response) response.push(markdown(round.response))
355
+ for (const c of round.calls) {
356
+ const res = results.get(c.source.id)
357
+ if (!res) drop("tool calls without a result (closed as errors)")
358
+ const ok = res?.ok ?? false
359
+ let output = res?.output ?? "No result was recorded for this call; the session moved before it finished."
360
+ if (res?.fullPath) output += `\n\n[Full output: ${res.fullPath}]`
361
+ // Copilot's tools report failure in the text, and so does the reader.
362
+ if (!ok && !/^\s*error\b/i.test(output)) output = `Error: ${output}`
363
+ toolCallResults[c.id] = { $mid: 20, content: [{ $mid: 21, value: output }] }
364
+ response.push({
365
+ kind: "toolInvocationSerialized",
366
+ invocationMessage: `Ran \`${c.name}\``,
367
+ pastTenseMessage: `Ran \`${c.name}\``,
368
+ resultDetails: { input: c.arguments, output: [{ type: "embed", isText: true, value: output }], ...(ok ? {} : { isError: true }) },
369
+ isConfirmed: { type: 1 },
370
+ isComplete: true,
371
+ toolCallId: c.id,
372
+ toolId: c.name,
373
+ })
374
+ calls++
375
+ }
376
+ }
377
+ const rounds = r.rounds.map((x) => ({ id: x.id, response: x.response, toolCalls: x.calls.map((c) => ({ id: c.id, name: c.name, arguments: c.arguments })), toolInputRetry: 0 }))
378
+ const summaries = r.rounds.filter((x) => x.summary).map((x) => ({ toolCallRoundId: x.id, text: x.summary! }))
379
+ if (summaries.length) response.unshift(markdown(summaries.at(-1)!.text))
380
+ const answered = rounds.length > 0
381
+ const completedAt = tick()
382
+ const metadata: Record<string, unknown> = {
383
+ ...(answered ? { toolCallRounds: rounds, toolCallResults } : {}),
384
+ ...(summaries.length ? { summaries } : {}),
385
+ modelMessageId: randomUUID(),
386
+ responseId: randomUUID(),
387
+ sessionId: copilotSession,
388
+ agentId: AGENT_ID,
389
+ codeBlocks: [],
390
+ }
391
+ if (!r.system) {
392
+ users++
393
+ if (!noteGiven) {
394
+ // Sent to the model in place of the request text; the chat shows only the person's words.
395
+ metadata.renderedUserMessage = [{ type: 1, text: `${note}\n\n${r.text}` }]
396
+ noteGiven = true
397
+ }
398
+ }
399
+ return {
400
+ requestId: `request_${randomUUID()}`,
401
+ timestamp,
402
+ message: message(r.text),
403
+ variableData: { variables: r.images ?? [] },
404
+ response,
405
+ responseId: `response_${randomUUID()}`,
406
+ // A request the session moved on from before any reply is kept as a cancelled one, as VS Code keeps them.
407
+ result: answered
408
+ ? { timings: { firstProgress: 0, totalElapsed: completedAt - timestamp }, metadata, ...(r.model ? { details: r.model } : {}) }
409
+ : { errorDetails: { message: "Cancelled" }, metadata },
410
+ followups: [],
411
+ modelState: { value: answered ? 1 : 2, completedAt },
412
+ agent,
413
+ contentReferences: [],
414
+ codeCitations: [],
415
+ timeSpentWaiting: timestamp,
416
+ ...(r.usage?.input !== undefined ? { promptTokens: r.usage.input } : {}),
417
+ ...(r.usage?.output !== undefined ? { completionTokens: r.usage.output } : {}),
418
+ ...(r.system ? { isSystemInitiated: true, systemInitiatedLabel: r.system } : {}),
419
+ }
420
+ })
421
+
422
+ const title = `${(session.header.title ?? firstUserText(session) ?? "Session").replace(/\s+/g, " ").trim().slice(0, 80)} (from ${label})`
423
+ const created = requests[0]!.timestamp
424
+ const last = requests.at(-1)!
425
+ const data = {
426
+ version: TESTED_VERSION,
427
+ creationDate: created,
428
+ customTitle: title,
429
+ initialLocation: "panel",
430
+ // Restores in agent mode, so the next request goes to the agent whose history this is.
431
+ inputState: { attachments: [], mode: { id: "agent", kind: "agent" }, inputText: "", selections: [], contrib: {} },
432
+ responderUsername: "GitHub Copilot",
433
+ sessionId: opts.sessionId,
434
+ requests,
435
+ hasPendingEdits: false,
436
+ pendingRequests: [],
437
+ }
438
+ const entry = {
439
+ sessionId: opts.sessionId,
440
+ title,
441
+ lastMessageDate: last.timestamp,
442
+ timing: { created, lastRequestStarted: last.timestamp, lastRequestEnded: last.modelState.completedAt },
443
+ initialLocation: "panel",
444
+ hasPendingEdits: false,
445
+ isEmpty: false,
446
+ isExternal: false,
447
+ lastResponseState: last.modelState.value,
448
+ }
449
+ const lost = [...drops].map(([what, n]) => `${what} (${n})`)
450
+ const remapped = [...ids].filter(([a, b]) => a !== b).length
451
+ if (remapped) lost.push(`tool call ids, replaced with ids every provider accepts (${remapped})`)
452
+ return { data, entry, lost, expect: { users, calls } }
453
+ }
454
+
455
+ const value = (db: Database, key: string): string | undefined => {
456
+ const row = db.query("SELECT value FROM ItemTable WHERE key = ?").get(key) as { value: string | Uint8Array } | null
457
+ return row ? (typeof row.value === "string" ? row.value : new TextDecoder().decode(row.value)) : undefined
458
+ }
459
+
460
+ /** The chat index in a workspace's state.vscdb, or a new one. */
461
+ function readIndex(db: Database): { version: 1; entries: Record<string, unknown> } {
462
+ const text = value(db, INDEX_KEY)
463
+ if (text === undefined) {
464
+ // With no index, VS Code moves a workspace's chats out of its older key on its next start; an index
465
+ // written first would stop that, so the person opens the folder once and VS Code moves them itself.
466
+ if (value(db, "interactive.sessions") !== undefined) throw new Error("this workspace still has chats in VS Code's older store; open the folder in VS Code once (so it moves them), quit it, and try again")
467
+ return { version: 1, entries: {} }
468
+ }
469
+ const index = JSON.parse(text)
470
+ if (index?.version !== 1 || typeof index.entries !== "object" || index.entries === null) throw new Error(`the chat index in ${db.filename} has a format this writer does not know (version ${String(index?.version)})`)
471
+ return index
472
+ }
473
+
474
+ /** The Copilot Chat writer; `versions` and `running` are for tests, which must not depend on the installed app. */
475
+ export function vscodeCopilotWriter(opts: { versions?: { vscode?: string; copilot?: string }; running?: () => boolean } = {}): Writer {
476
+ const installed = (home: string) => opts.versions ?? installedVSCode(home)
477
+ const running = opts.running ?? (() => isRunning("Code"))
478
+ return {
479
+ tool: TOOL,
480
+ label: LABEL,
481
+ way: "native",
482
+
483
+ async ready(w: WriteOptions): Promise<Readiness> {
484
+ const { vscode, copilot } = installed(w.home)
485
+ if (!vscode) return { ok: false, why: "VS Code is not installed (no Visual Studio Code.app in /Applications or ~/Applications)" }
486
+ if (!TESTED_VSCODE.includes(line(vscode)!)) return { ok: false, why: `VS Code ${vscode} is not a version this writer was checked against (${TESTED_VSCODE.join(", ")})` }
487
+ if (!copilot || !TESTED_COPILOT.includes(line(copilot)!)) return { ok: false, why: `Copilot Chat ${copilot ?? "(not found)"} is not a version this writer was checked against (${TESTED_COPILOT.join(", ")})` }
488
+ if (running()) return { ok: false, why: "VS Code is running; quit it first (it keeps the chat index in a database it rewrites from memory)" }
489
+ if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; VS Code keeps chats per workspace folder` }
490
+ return { ok: true }
491
+ },
492
+
493
+ async write(session: Session, w: WriteOptions): Promise<WriteResult> {
494
+ if (!w.dryRun && running()) throw new Error("VS Code is running; quit it first")
495
+ const { copilot } = installed(w.home)
496
+ const sessionId = randomUUID()
497
+ const storage = join(userDir(w.home), "workspaceStorage", workspaceId(w.cwd))
498
+ const file = join(storage, "chatSessions", `${sessionId}.jsonl`)
499
+ const db = join(storage, "state.vscdb")
500
+ const plan = planChat(session, { sessionId, now: new Date(), extensionVersion: copilot ?? `${TESTED_COPILOT[0]}.0` })
501
+ const written = [file, db, ...(exists(join(storage, "workspace.json")) ? [] : [join(storage, "workspace.json")])]
502
+ let backedUp: string | undefined
503
+ if (!w.dryRun) {
504
+ mkdirSync(join(storage, "chatSessions"), { recursive: true })
505
+ // VS Code writes this beside a workspace's storage the first time it opens the folder.
506
+ if (!exists(join(storage, "workspace.json"))) writeFileSync(join(storage, "workspace.json"), JSON.stringify({ folder: fileUri(w.cwd) }, undefined, 2))
507
+ writeFileSync(file, `${JSON.stringify({ kind: 0, v: plan.data })}\n`, { flag: "wx" })
508
+ try {
509
+ const back = await reader.read({ tool: TOOL, sourceId: sessionId, path: file }, w.home)
510
+ const n = countTurns(back)
511
+ const pairs = unpaired(back)
512
+ if (n.user !== plan.expect.users || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length)
513
+ throw new Error(`the new chat did not read back intact (${n.user} of ${plan.expect.users} requests, ${n.tool_call} of ${plan.expect.calls} tool calls, ${pairs.calls.length + pairs.results.length} unpaired)`)
514
+ if (exists(db)) backedUp = backup(w.home, TOOL, [db])
515
+ const conn = new Database(db, { create: true })
516
+ try {
517
+ // The schema VS Code creates (base/parts/storage/node/storage.ts); a no-op on an existing store.
518
+ conn.exec("CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)")
519
+ conn.transaction(() => {
520
+ const index = readIndex(conn)
521
+ index.entries[sessionId] = plan.entry
522
+ conn.query("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)").run(INDEX_KEY, JSON.stringify(index))
523
+ })()
524
+ } finally {
525
+ conn.close()
526
+ }
527
+ } catch (e) {
528
+ rmSync(file, { force: true })
529
+ throw e
530
+ }
531
+ }
532
+ return {
533
+ tool: TOOL,
534
+ way: "native",
535
+ sessionId,
536
+ written,
537
+ open: `code ${shellQuote(w.cwd)}`,
538
+ note: `In the Chat view, open the chat history (Show Chats…) and pick "${plan.entry.title}".${backedUp ? ` The workspace's state.vscdb was backed up to ${backedUp}.` : ""}`,
539
+ lost: plan.lost,
540
+ dryRun: !!w.dryRun,
541
+ }
542
+ },
543
+ }
544
+ }
545
+
546
+ export const vscodeCopilotNativeWriter = vscodeCopilotWriter()
@@ -0,0 +1,8 @@
1
+ import type { Reader, Writer } from "../types.ts"
2
+ import { zedReader } from "./reader.ts"
3
+ import { zedNativeWriter } from "./writer.ts"
4
+
5
+ // Zed's agent panel: reads native agent threads from threads.db, and continues any session as a new native
6
+ // thread there (written only while Zed is closed, after a backup).
7
+ export const readers: Reader[] = [zedReader]
8
+ export const writers: Writer[] = [zedNativeWriter]