@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.
- package/LICENSE +48 -0
- package/README.md +172 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +326 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/connect.ts +845 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- package/src/sqlite.ts +48 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { dirname, join } from "node:path"
|
|
4
|
+
import { toolLabel } from "../../brief.ts"
|
|
5
|
+
import { gitInfo, isDir, versionOf } from "../../fsutil.ts"
|
|
6
|
+
import type { Session, Turn } from "../../session/types.ts"
|
|
7
|
+
import { asText, clip, header, iso, lossCounter, noteModel, toolKindOf } from "../../session/util.ts"
|
|
8
|
+
import type { Readiness, SessionRef, WriteResult, Writer } from "../types.ts"
|
|
9
|
+
import { idMinter, mintId, random62 } from "./ids.ts"
|
|
10
|
+
import { CONTINUED_NOTE, continuedNote } from "./note.ts"
|
|
11
|
+
import { OPENCODE_V2, mapCall } from "./tools.ts"
|
|
12
|
+
import { addUsage, continuedTitle, modelRef, resolveBin, sh, versionNumber, zeroTokens } from "./writer.ts"
|
|
13
|
+
|
|
14
|
+
// OpenCode 2.x (npm @opencode/cli, anomalyco/opencode packages/core). Its SQLite store keeps a session in
|
|
15
|
+
// session_v2 and its messages in session_message, one row per message with the message's JSON (less id and
|
|
16
|
+
// type) in `data`; an assistant step's content (text, reasoning and tool items, each tool holding its own
|
|
17
|
+
// result) is inline in `data` or, since 2026-10-29, in session_part rows by ordinal. The store is event
|
|
18
|
+
// sourced and owned by a background service, so a continued session goes through the service's own
|
|
19
|
+
// importer (`opencode session import`, POST /api/experimental/session/import), which publishes the
|
|
20
|
+
// session.created event and writes the rows itself (core/src/session/transfer.ts).
|
|
21
|
+
//
|
|
22
|
+
// Verified live (2026-11-08) with @opencode/cli 2.2.6 in a temp home with no network: `session import
|
|
23
|
+
// --standalone`, then `session list` and `session export`. Message shapes from packages/schema/src/
|
|
24
|
+
// session-message.ts at v2.2.6; how they reach the model from core/src/session/runner/to-llm-message.ts.
|
|
25
|
+
|
|
26
|
+
export const TESTED_V2 = ["2.2.6"]
|
|
27
|
+
|
|
28
|
+
/** A session as `session export` writes it and `session import` takes it. */
|
|
29
|
+
export interface Transfer {
|
|
30
|
+
info: Record<string, any>
|
|
31
|
+
messages: Array<Record<string, any>>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The portable session as a 2.x transfer: the continuation note as a synthetic message (sent to the model,
|
|
36
|
+
* shown only as its short description), one user message per user turn, one assistant message per model step
|
|
37
|
+
* with its tool items holding their results, and a compaction as a completed summary compaction, from which
|
|
38
|
+
* the loader starts the model's history.
|
|
39
|
+
*/
|
|
40
|
+
export function buildTransferV2(s: Session, o: { cwd: string; now?: number }): { data: Transfer; sessionId: string; lost: string[] } {
|
|
41
|
+
const now = o.now ?? Date.now()
|
|
42
|
+
const next = idMinter(now)
|
|
43
|
+
const sessionId = mintId("ses", now, 1, true)
|
|
44
|
+
const messages: Transfer["messages"] = []
|
|
45
|
+
const lost = new Map<string, number>()
|
|
46
|
+
const drop = (what: string, n = 1) => lost.set(what, (lost.get(what) ?? 0) + n)
|
|
47
|
+
const mapped = { n: 0 }
|
|
48
|
+
const label = toolLabel(s.header.tool)
|
|
49
|
+
|
|
50
|
+
let clock = Date.parse(s.header.startedAt ?? "") || now - s.turns.length * 1000
|
|
51
|
+
const tick = (at?: string) => {
|
|
52
|
+
const t = at ? Date.parse(at) : NaN
|
|
53
|
+
clock = Number.isFinite(t) && t > clock ? t : clock + 1
|
|
54
|
+
return clock
|
|
55
|
+
}
|
|
56
|
+
const firstClock = clock
|
|
57
|
+
|
|
58
|
+
let model = modelRef(s.header.models.at(-1))
|
|
59
|
+
let step: { msg: Record<string, any>; tools: boolean; results: boolean; tokens: ReturnType<typeof zeroTokens> } | undefined
|
|
60
|
+
const calls = new Map<string, Record<string, any>>()
|
|
61
|
+
const answered = new Set<string>()
|
|
62
|
+
|
|
63
|
+
const push = (msg: Record<string, any>) => {
|
|
64
|
+
closeStep()
|
|
65
|
+
messages.push(msg)
|
|
66
|
+
}
|
|
67
|
+
const openStep = (at?: string) => {
|
|
68
|
+
if (step && !step.results) return step
|
|
69
|
+
closeStep()
|
|
70
|
+
const created = tick(at)
|
|
71
|
+
step = {
|
|
72
|
+
msg: { id: next("msg"), type: "assistant", agent: "build", model: { id: model.modelID, providerID: model.providerID }, content: [], time: { created, completed: created } },
|
|
73
|
+
tools: false,
|
|
74
|
+
results: false,
|
|
75
|
+
tokens: zeroTokens(),
|
|
76
|
+
}
|
|
77
|
+
messages.push(step.msg)
|
|
78
|
+
return step
|
|
79
|
+
}
|
|
80
|
+
function closeStep() {
|
|
81
|
+
if (!step) return
|
|
82
|
+
step.msg.finish = step.tools ? "tool-calls" : "stop"
|
|
83
|
+
step.msg.time.completed = clock
|
|
84
|
+
if (Object.values(step.tokens).some((v) => typeof v === "number" && v > 0) || step.tokens.cache.read || step.tokens.cache.write) step.msg.tokens = step.tokens
|
|
85
|
+
step = undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
push({
|
|
89
|
+
id: next("msg"),
|
|
90
|
+
type: "synthetic",
|
|
91
|
+
text: continuedNote(label, s.header.id, new Date(now).toISOString().slice(0, 10)),
|
|
92
|
+
description: `Continued from ${label}`,
|
|
93
|
+
time: { created: tick() },
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
for (const t of s.turns as Turn[]) {
|
|
97
|
+
switch (t.kind) {
|
|
98
|
+
case "user": {
|
|
99
|
+
const n = t.attachments?.length ?? 0
|
|
100
|
+
if (n) drop("attachments", n)
|
|
101
|
+
const text = t.text.trim() ? t.text : n ? `(${n} attachment${n > 1 ? "s" : ""} not carried over)` : ""
|
|
102
|
+
if (text) push({ id: next("msg"), type: "user", text, time: { created: tick(t.at) } })
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
case "assistant": {
|
|
106
|
+
if (!t.text.trim()) break
|
|
107
|
+
if (t.model) model = modelRef(t.model)
|
|
108
|
+
const st = openStep(t.at)
|
|
109
|
+
tick(t.at)
|
|
110
|
+
st.msg.content.push({ type: "text", text: t.text })
|
|
111
|
+
addUsage(st.tokens, t.usage)
|
|
112
|
+
break
|
|
113
|
+
}
|
|
114
|
+
case "reasoning": {
|
|
115
|
+
const st = openStep(t.at)
|
|
116
|
+
const at = tick(t.at)
|
|
117
|
+
st.msg.content.push({ type: "reasoning", text: t.text, time: { created: at, completed: at } })
|
|
118
|
+
// Without native data, 2.x hands readable reasoning to the next model's adapter as text.
|
|
119
|
+
drop("reasoning: kept as text, without provider signatures")
|
|
120
|
+
break
|
|
121
|
+
}
|
|
122
|
+
case "tool_call": {
|
|
123
|
+
const st = openStep(t.at)
|
|
124
|
+
const at = tick(t.at)
|
|
125
|
+
const call = mapCall(OPENCODE_V2, t.name, t.toolKind, t.input, mapped)
|
|
126
|
+
const item = {
|
|
127
|
+
type: "tool",
|
|
128
|
+
id: `call_${random62(24)}`,
|
|
129
|
+
name: call.tool,
|
|
130
|
+
state: { status: "error", input: call.input, error: { type: "tool.execution", message: "No result was recorded for this call." } },
|
|
131
|
+
time: { created: at, ran: at, completed: at },
|
|
132
|
+
}
|
|
133
|
+
st.msg.content.push(item)
|
|
134
|
+
st.tools = true
|
|
135
|
+
calls.set(t.id, item)
|
|
136
|
+
break
|
|
137
|
+
}
|
|
138
|
+
case "tool_result": {
|
|
139
|
+
const item = calls.get(t.callId)
|
|
140
|
+
if (!item) {
|
|
141
|
+
drop("tool results without a call")
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
answered.add(t.callId)
|
|
145
|
+
item.time.completed = Math.max(item.time.created, tick(t.at))
|
|
146
|
+
const input = item.state.input
|
|
147
|
+
item.state = t.ok
|
|
148
|
+
? { status: "completed", input, content: [{ type: "text", text: t.output }], ...(t.truncated ? { metadata: { truncated: true } } : {}) }
|
|
149
|
+
: { status: "error", input, error: { type: "tool.execution", message: t.output || "The call failed." } }
|
|
150
|
+
if (step) step.results = true
|
|
151
|
+
break
|
|
152
|
+
}
|
|
153
|
+
case "compaction":
|
|
154
|
+
push({ id: next("msg"), type: "compaction", status: "completed", reason: "auto", summary: t.summary, time: { created: tick(t.at) } })
|
|
155
|
+
break
|
|
156
|
+
case "file_edit":
|
|
157
|
+
if (!t.callId) drop("file edits not made by a tool call")
|
|
158
|
+
break
|
|
159
|
+
case "subagent":
|
|
160
|
+
drop("subagent sessions (their call and result are kept)")
|
|
161
|
+
break
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
closeStep()
|
|
165
|
+
|
|
166
|
+
const unanswered = calls.size - answered.size
|
|
167
|
+
if (unanswered > 0) drop("calls with no result, marked as failed", unanswered)
|
|
168
|
+
if (calls.size) drop("tool-call ids replaced", calls.size)
|
|
169
|
+
if (mapped.n) drop("tool calls renamed to OpenCode's own tools", mapped.n)
|
|
170
|
+
|
|
171
|
+
const info = {
|
|
172
|
+
id: sessionId,
|
|
173
|
+
// The importer resolves the project from the folder; the field is required, so it carries the default.
|
|
174
|
+
projectID: "global",
|
|
175
|
+
cost: 0,
|
|
176
|
+
tokens: zeroTokens(),
|
|
177
|
+
time: { created: firstClock, updated: Math.max(now, clock) },
|
|
178
|
+
title: continuedTitle(s),
|
|
179
|
+
location: { directory: o.cwd },
|
|
180
|
+
}
|
|
181
|
+
return { data: { info, messages }, sessionId, lost: [...lost].map(([what, n]) => `${what} (${n})`) }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** OpenCode 2.x through `opencode session import`; the OpenCode writer dispatches here for a 2.x install. */
|
|
185
|
+
export const v2Writer: Writer = {
|
|
186
|
+
tool: "opencode",
|
|
187
|
+
label: "OpenCode",
|
|
188
|
+
way: "import",
|
|
189
|
+
async ready(opts): Promise<Readiness> {
|
|
190
|
+
const raw = versionOf(resolveBin({ bin: "opencode", installDir: ".opencode" }, opts.home).path)
|
|
191
|
+
const v = versionNumber(raw)
|
|
192
|
+
if (!raw) return { ok: false, why: "OpenCode is not installed (no opencode on PATH or in ~/.opencode/bin)" }
|
|
193
|
+
if (!v || !TESTED_V2.includes(v)) return { ok: false, why: `OpenCode ${v ?? raw} has not been tested; the 2.x import format was checked on ${TESTED_V2.join(", ")}` }
|
|
194
|
+
if (!isDir(opts.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${opts.cwd}` }
|
|
195
|
+
return { ok: true }
|
|
196
|
+
},
|
|
197
|
+
async write(session, opts): Promise<WriteResult> {
|
|
198
|
+
const bin = resolveBin({ bin: "opencode", installDir: ".opencode" }, opts.home)
|
|
199
|
+
const { data, sessionId, lost } = buildTransferV2(session, { cwd: opts.cwd })
|
|
200
|
+
const file = join(opts.home, ".brain", "exports", `opencode-${sessionId}.json`)
|
|
201
|
+
if (!opts.dryRun) {
|
|
202
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
203
|
+
writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`)
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
tool: "opencode",
|
|
207
|
+
way: "import",
|
|
208
|
+
sessionId,
|
|
209
|
+
written: [file],
|
|
210
|
+
// The background service files the session under the project of the directory it is given.
|
|
211
|
+
open: `cd ${sh(opts.cwd)} && ${sh(bin.command)} session import ${sh(file)} && ${sh(bin.command)} --session ${sessionId}`,
|
|
212
|
+
lost,
|
|
213
|
+
dryRun: !!opts.dryRun,
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---- Reading 2.x stores ----
|
|
219
|
+
|
|
220
|
+
const hasTable = (d: Database, name: string) => !!d.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)
|
|
221
|
+
|
|
222
|
+
const json = (s: unknown) => {
|
|
223
|
+
try {
|
|
224
|
+
return JSON.parse(String(s)) as Record<string, any>
|
|
225
|
+
} catch {
|
|
226
|
+
return undefined
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Top-level sessions in a 2.x database (the reader opens a snapshot of it); none in a 1.x one. */
|
|
231
|
+
export function listV2Db(d: Database, path: string, tool: string): SessionRef[] {
|
|
232
|
+
if (!hasTable(d, "session_v2")) return []
|
|
233
|
+
const rows = d.query("SELECT id, title, directory, time_updated, project_id FROM session_v2 WHERE parent_id IS NULL AND time_archived IS NULL").all() as Array<Record<string, any>>
|
|
234
|
+
return rows.map((r) => ({
|
|
235
|
+
tool,
|
|
236
|
+
sourceId: String(r.id),
|
|
237
|
+
path,
|
|
238
|
+
...(r.title ? { title: String(r.title) } : {}),
|
|
239
|
+
...(r.directory ? { cwd: String(r.directory) } : {}),
|
|
240
|
+
...(iso(r.time_updated) ? { updatedAt: iso(r.time_updated)! } : {}),
|
|
241
|
+
extra: { source: "db2", projectID: r.project_id },
|
|
242
|
+
}))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** One session from a 2.x database, with its subagent sessions two levels down. */
|
|
246
|
+
export function readV2Db(d: Database, path: string, tool: string, id: string, depth = 0): Session {
|
|
247
|
+
if (!hasTable(d, "session_v2")) throw new Error(`no session_v2 table in ${path}`)
|
|
248
|
+
const row = d.query("SELECT * FROM session_v2 WHERE id = ?").get(id) as Record<string, any> | null
|
|
249
|
+
if (!row) throw new Error(`session ${id} not found`)
|
|
250
|
+
const cwd = row.directory ? String(row.directory) : undefined
|
|
251
|
+
const h = header(tool, id, {
|
|
252
|
+
...(row.version ? { toolVersion: String(row.version) } : {}),
|
|
253
|
+
sourcePath: path,
|
|
254
|
+
...(row.title ? { title: String(row.title) } : {}),
|
|
255
|
+
project: { ...(cwd ? { cwd } : {}), ...gitInfo(cwd) },
|
|
256
|
+
...(iso(row.time_created) ? { startedAt: iso(row.time_created)! } : {}),
|
|
257
|
+
...(iso(row.time_updated) ? { updatedAt: iso(row.time_updated)! } : {}),
|
|
258
|
+
...(row.parent_id ? { parent: { id: `${tool}:${row.parent_id}`, kind: "subagent" as const } } : {}),
|
|
259
|
+
})
|
|
260
|
+
const lost = lossCounter()
|
|
261
|
+
const parts = new Map<string, Array<Record<string, any>>>()
|
|
262
|
+
if (hasTable(d, "session_part"))
|
|
263
|
+
for (const p of d.query("SELECT message_id, type, data FROM session_part WHERE session_id = ? ORDER BY message_id, ordinal").all(id) as Array<Record<string, any>>) {
|
|
264
|
+
const data = json(p.data)
|
|
265
|
+
if (!data) {
|
|
266
|
+
lost.add("unreadable message or part records")
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
const list = parts.get(p.message_id) ?? []
|
|
270
|
+
list.push({ ...data, type: p.type })
|
|
271
|
+
parts.set(p.message_id, list)
|
|
272
|
+
}
|
|
273
|
+
const turns: Turn[] = []
|
|
274
|
+
const callIds = new Set<string>()
|
|
275
|
+
for (const m of d.query("SELECT id, type, time_created, data FROM session_message WHERE session_id = ? ORDER BY seq").all(id) as Array<Record<string, any>>) {
|
|
276
|
+
const data = json(m.data)
|
|
277
|
+
if (!data) {
|
|
278
|
+
lost.add("unreadable message or part records")
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
const at = iso(m.time_created)
|
|
282
|
+
switch (m.type) {
|
|
283
|
+
case "user": {
|
|
284
|
+
const files = Array.isArray(data.files) ? data.files.length : 0
|
|
285
|
+
if (files) lost.add("attachment data", files)
|
|
286
|
+
if (typeof data.text === "string" && (data.text.trim() || files))
|
|
287
|
+
turns.push({ kind: "user", ...(at ? { at } : {}), text: data.text, ...(files ? { attachments: data.files.map((f: any) => ({ kind: String(f.mime ?? "").startsWith("image/") ? "image" : "file", ...(f.mime ? { mime: f.mime } : {}), ...(f.name ? { name: f.name } : {}) })) } : {}) })
|
|
288
|
+
break
|
|
289
|
+
}
|
|
290
|
+
case "synthetic": {
|
|
291
|
+
const continued = typeof data.text === "string" ? data.text.match(CONTINUED_NOTE)?.[1] : undefined
|
|
292
|
+
if (continued) h.parent ??= { id: continued, kind: "continued" }
|
|
293
|
+
else lost.add("messages the tool injected as the user")
|
|
294
|
+
break
|
|
295
|
+
}
|
|
296
|
+
case "assistant": {
|
|
297
|
+
const model = data.model?.id ? String(data.model.id) : undefined
|
|
298
|
+
const content: Array<Record<string, any>> = parts.get(m.id) ?? (Array.isArray(data.content) ? data.content : [])
|
|
299
|
+
if (model && content.some((c) => c.type === "text" || c.type === "tool")) noteModel(h, model)
|
|
300
|
+
if (data.error) lost.add("error notices on replies")
|
|
301
|
+
let usage = data.tokens ? { input: data.tokens.input, output: data.tokens.output, cacheRead: data.tokens.cache?.read, cacheWrite: data.tokens.cache?.write } : undefined
|
|
302
|
+
for (const c of content) {
|
|
303
|
+
if (c.type === "text" && typeof c.text === "string" && c.text.trim()) {
|
|
304
|
+
turns.push({ kind: "assistant", ...(at ? { at } : {}), text: c.text, ...(model ? { model } : {}), ...(usage && Object.values(usage).some(Boolean) ? { usage } : {}) })
|
|
305
|
+
usage = undefined
|
|
306
|
+
} else if (c.type === "reasoning") {
|
|
307
|
+
const signed = !!c.native && Object.keys(c.native).length > 0
|
|
308
|
+
if (typeof c.text === "string" && c.text.trim()) {
|
|
309
|
+
turns.push({ kind: "reasoning", ...(at ? { at } : {}), text: c.text })
|
|
310
|
+
if (signed) lost.add("reasoning signatures")
|
|
311
|
+
} else if (signed) lost.add("signed reasoning")
|
|
312
|
+
} else if (c.type === "tool") toolTurns(c, at, turns, callIds, lost)
|
|
313
|
+
}
|
|
314
|
+
break
|
|
315
|
+
}
|
|
316
|
+
case "compaction":
|
|
317
|
+
if (data.status === "completed" && typeof data.summary === "string") turns.push({ kind: "compaction", ...(at ? { at } : {}), summary: data.summary })
|
|
318
|
+
break
|
|
319
|
+
case "shell": {
|
|
320
|
+
// A command the person ran from the prompt: a call and its result, as the model was shown it.
|
|
321
|
+
const callId = `shell-${m.id}`
|
|
322
|
+
turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: callId, name: "shell", toolKind: "shell", input: { command: data.command } })
|
|
323
|
+
if (data.output) turns.push({ kind: "tool_result", callId, ok: data.exit === undefined || data.exit === 0, output: clip(asText(data.output.output)).text })
|
|
324
|
+
break
|
|
325
|
+
}
|
|
326
|
+
case "system":
|
|
327
|
+
case "skill":
|
|
328
|
+
lost.add("system and skill notes")
|
|
329
|
+
break
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
lost.flush(h)
|
|
333
|
+
const s: Session = { header: h, turns }
|
|
334
|
+
if (depth < 2) {
|
|
335
|
+
const children: Session[] = []
|
|
336
|
+
for (const c of d.query("SELECT id FROM session_v2 WHERE parent_id = ?").all(id) as Array<{ id: string }>) {
|
|
337
|
+
try {
|
|
338
|
+
children.push(readV2Db(d, path, tool, c.id, depth + 1))
|
|
339
|
+
} catch {
|
|
340
|
+
h.lost.push(`unreadable subagent session (${c.id})`)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (children.length) s.children = children
|
|
344
|
+
}
|
|
345
|
+
return s
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function toolTurns(c: Record<string, any>, at: string | undefined, turns: Turn[], callIds: Set<string>, lost: ReturnType<typeof lossCounter>) {
|
|
349
|
+
const st = (c.state ?? {}) as Record<string, any>
|
|
350
|
+
const name = String(c.name ?? "unknown")
|
|
351
|
+
let id = String(c.id ?? "")
|
|
352
|
+
for (let n = 2; !id || callIds.has(id); n++) id = `${String(c.id ?? "call")}#${n}`
|
|
353
|
+
callIds.add(id)
|
|
354
|
+
const start = iso(c.time?.created) ?? at
|
|
355
|
+
const end = iso(c.time?.completed) ?? start
|
|
356
|
+
if (c.native && Object.keys(c.native).length) lost.add("tool-call signatures")
|
|
357
|
+
const input = st.status === "streaming" ? asText(st.input) : (st.input ?? {})
|
|
358
|
+
turns.push({ kind: "tool_call", ...(start ? { at: start } : {}), id, name, toolKind: toolKindOf(name), input })
|
|
359
|
+
if (st.status !== "completed" && st.status !== "error") return
|
|
360
|
+
const items: Array<Record<string, any>> = Array.isArray(st.content) ? st.content : []
|
|
361
|
+
const files = items.filter((x) => x.type === "file").length
|
|
362
|
+
if (files) lost.add("attachments in tool results", files)
|
|
363
|
+
const text = items.filter((x) => x.type === "text").map((x) => String(x.text)).join("\n")
|
|
364
|
+
const raw = st.status === "completed" ? text : [st.error?.message, text].filter(Boolean).join("\n")
|
|
365
|
+
const out = clip(raw)
|
|
366
|
+
turns.push({ kind: "tool_result", ...(end ? { at: end } : {}), callId: id, ok: st.status === "completed", output: out.text, ...(out.truncated || st.metadata?.truncated === true ? { truncated: true } : {}) })
|
|
367
|
+
}
|