@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.
- package/LICENSE +48 -0
- package/README.md +235 -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 +351 -0
- package/src/dashboard/cli.ts +148 -0
- package/src/dashboard/conversions.ts +170 -0
- package/src/dashboard/report.ts +261 -0
- package/src/dashboard/send.ts +213 -0
- package/src/dashboard/state.ts +95 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +847 -0
- package/src/gateway/connectors/aider.ts +356 -0
- package/src/gateway/connectors/amazon-q-cli.ts +6 -0
- package/src/gateway/connectors/amazon-q-ide.ts +6 -0
- package/src/gateway/connectors/antigravity.ts +6 -0
- package/src/gateway/connectors/auggie.ts +6 -0
- package/src/gateway/connectors/cline-sdk.ts +85 -0
- package/src/gateway/connectors/cline.ts +34 -0
- package/src/gateway/connectors/codebuff.ts +6 -0
- package/src/gateway/connectors/continue.ts +579 -0
- package/src/gateway/connectors/copilot-cli.ts +138 -0
- package/src/gateway/connectors/crush.ts +313 -0
- package/src/gateway/connectors/cursor-cli.ts +6 -0
- package/src/gateway/connectors/cursor.ts +6 -0
- package/src/gateway/connectors/devin.ts +6 -0
- package/src/gateway/connectors/factory-droid.ts +308 -0
- package/src/gateway/connectors/goose.ts +79 -0
- package/src/gateway/connectors/grok-cli.ts +82 -0
- package/src/gateway/connectors/hermes.ts +230 -0
- package/src/gateway/connectors/iflow-cli.ts +50 -0
- package/src/gateway/connectors/index.ts +86 -0
- package/src/gateway/connectors/kilo-cli.ts +434 -0
- package/src/gateway/connectors/kilo-code.ts +32 -0
- package/src/gateway/connectors/kimi-code.ts +241 -0
- package/src/gateway/connectors/kiro-cli.ts +6 -0
- package/src/gateway/connectors/kiro-ide.ts +6 -0
- package/src/gateway/connectors/letta-code.ts +170 -0
- package/src/gateway/connectors/mastracode.ts +91 -0
- package/src/gateway/connectors/mistral-vibe.ts +495 -0
- package/src/gateway/connectors/muse.ts +27 -0
- package/src/gateway/connectors/omp.ts +279 -0
- package/src/gateway/connectors/openhands.ts +296 -0
- package/src/gateway/connectors/pearai.ts +52 -0
- package/src/gateway/connectors/pi.ts +357 -0
- package/src/gateway/connectors/qoder-cli.ts +6 -0
- package/src/gateway/connectors/qwen-code.ts +411 -0
- package/src/gateway/connectors/roo.ts +43 -0
- package/src/gateway/connectors/tabnine-cli.ts +6 -0
- package/src/gateway/connectors/void.ts +6 -0
- package/src/gateway/connectors/vscode-copilot.ts +286 -0
- package/src/gateway/connectors/windsurf.ts +6 -0
- package/src/gateway/connectors/zed.ts +29 -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,53 @@
|
|
|
1
|
+
// Pasted images travel as files under ~/.brain/attachments/<hash>.<ext>, referenced by `Attachment.path`, so the
|
|
2
|
+
// portable session stays small and every writer that can embed an image reads the same file.
|
|
3
|
+
import { createHash } from "node:crypto"
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
5
|
+
import { homedir } from "node:os"
|
|
6
|
+
import { extname, join } from "node:path"
|
|
7
|
+
import { homeDir } from "../fsutil.ts"
|
|
8
|
+
import type { Attachment } from "./types.ts"
|
|
9
|
+
|
|
10
|
+
export const IMAGE_TYPES: Record<string, string> = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" }
|
|
11
|
+
const EXT = Object.fromEntries(Object.entries(IMAGE_TYPES).filter(([e]) => e !== ".jpeg").map(([e, m]) => [m, e]))
|
|
12
|
+
/** Larger images stay behind and are counted as lost, as models reject them anyway. */
|
|
13
|
+
export const IMAGE_LIMIT = 5 * 1024 * 1024
|
|
14
|
+
|
|
15
|
+
/** <home>/.brain/attachments, with the same home as everything else the brain stores (BRAIN_HOME, else HOME). */
|
|
16
|
+
export const attachmentDir = () => join(homeDir() || homedir(), ".brain", "attachments")
|
|
17
|
+
|
|
18
|
+
/** Saves base64 image bytes and returns the attachment, or undefined when they are not a usable image. */
|
|
19
|
+
export function keepImage(base64: string, mime: string | undefined): Attachment | undefined {
|
|
20
|
+
const ext = mime ? EXT[mime] : undefined
|
|
21
|
+
if (!ext || base64.length > IMAGE_LIMIT * 1.4) return undefined
|
|
22
|
+
try {
|
|
23
|
+
const bytes = Buffer.from(base64, "base64")
|
|
24
|
+
if (!bytes.length || bytes.length > IMAGE_LIMIT) return undefined
|
|
25
|
+
const path = join(attachmentDir(), `${createHash("sha256").update(bytes).digest("hex").slice(0, 24)}${ext}`)
|
|
26
|
+
if (!existsSync(path)) {
|
|
27
|
+
mkdirSync(attachmentDir(), { recursive: true })
|
|
28
|
+
writeFileSync(path, bytes)
|
|
29
|
+
}
|
|
30
|
+
return { kind: "image", path, mime: mime! }
|
|
31
|
+
} catch {
|
|
32
|
+
return undefined
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The `data:` URL of a kept image, when its file is still there and small enough. */
|
|
37
|
+
export function imageDataUrl(a: Attachment): string | undefined {
|
|
38
|
+
if (a.kind !== "image" || !a.path) return undefined
|
|
39
|
+
const mime = a.mime ?? IMAGE_TYPES[extname(a.path).toLowerCase()]
|
|
40
|
+
if (!mime || !Object.values(IMAGE_TYPES).includes(mime)) return undefined
|
|
41
|
+
try {
|
|
42
|
+
if (statSync(a.path).size > IMAGE_LIMIT) return undefined
|
|
43
|
+
return `data:${mime};base64,${readFileSync(a.path).toString("base64")}`
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Splits a `data:image/...;base64,...` URL into its parts. */
|
|
50
|
+
export function parseDataUrl(url: unknown): { mime: string; base64: string } | undefined {
|
|
51
|
+
const m = typeof url === "string" ? /^data:(image\/[a-z+.-]+);base64,(.+)$/s.exec(url) : null
|
|
52
|
+
return m ? { mime: m[1]!, base64: m[2]! } : undefined
|
|
53
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// A cache for the cheap summary a reader makes of each session when listing (title, folder, last activity). An entry is
|
|
2
|
+
// valid while the file's size and modified time (and `salt`, for a sibling file the summary also reads) are unchanged,
|
|
3
|
+
// so a session that gets a new message is read again and nothing goes stale. Lives under <home>/.brain/cache/.
|
|
4
|
+
// BRAIN_NO_CACHE=1 turns it off. `version` must change whenever the summary's logic changes.
|
|
5
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
6
|
+
import { dirname, join } from "node:path"
|
|
7
|
+
|
|
8
|
+
interface Entry {
|
|
9
|
+
m: number
|
|
10
|
+
s: number
|
|
11
|
+
k: number
|
|
12
|
+
v: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function peekCache(home: string, name: string, version: number) {
|
|
16
|
+
const off = process.env.BRAIN_NO_CACHE === "1"
|
|
17
|
+
const file = join(home, ".brain", "cache", `${name}-v${version}.json`)
|
|
18
|
+
let map: Record<string, Entry> = {}
|
|
19
|
+
if (!off) {
|
|
20
|
+
try {
|
|
21
|
+
map = JSON.parse(readFileSync(file, "utf8"))
|
|
22
|
+
} catch {}
|
|
23
|
+
}
|
|
24
|
+
const used = new Set<string>()
|
|
25
|
+
let dirty = false
|
|
26
|
+
return {
|
|
27
|
+
async get<T>(path: string, st: { mtimeMs: number; size: number }, salt: number, compute: () => Promise<T>): Promise<T> {
|
|
28
|
+
used.add(path)
|
|
29
|
+
const hit = map[path]
|
|
30
|
+
if (!off && hit && hit.m === st.mtimeMs && hit.s === st.size && hit.k === salt) return hit.v as T
|
|
31
|
+
const v = await compute()
|
|
32
|
+
if (!off) {
|
|
33
|
+
map[path] = { m: st.mtimeMs, s: st.size, k: salt, v }
|
|
34
|
+
dirty = true
|
|
35
|
+
}
|
|
36
|
+
return v
|
|
37
|
+
},
|
|
38
|
+
/** Writes the cache back, dropping entries for files that were not asked for (deleted sessions). */
|
|
39
|
+
flush() {
|
|
40
|
+
if (off) return
|
|
41
|
+
const stale = Object.keys(map).filter((p) => !used.has(p))
|
|
42
|
+
if (!dirty && !stale.length) return
|
|
43
|
+
for (const p of stale) delete map[p]
|
|
44
|
+
try {
|
|
45
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
46
|
+
writeFileSync(file, JSON.stringify(map))
|
|
47
|
+
} catch {}
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The portable session, brain-session/v1: what every reader produces and every writer consumes.
|
|
2
|
+
// Design: docs/sessions/2026-11-08-session-switcher.md, section 3.1. Shaped on OpenTelemetry's GenAI
|
|
3
|
+
// message conventions, with ideas from Harbor's ATIF.
|
|
4
|
+
//
|
|
5
|
+
// A session is a header and a flat, ordered list of turns. Flat on purpose: tools group the same events
|
|
6
|
+
// differently (Claude Code puts text and tool calls in one assistant message, Cursor puts a call and its
|
|
7
|
+
// result in one bubble, Codex gives each its own record), so writers regroup consecutive turns into
|
|
8
|
+
// whatever their tool expects. Serialised as JSONL: the header line, then one line per turn.
|
|
9
|
+
|
|
10
|
+
export const FORMAT = "brain-session/v1"
|
|
11
|
+
|
|
12
|
+
/** What a tool call does, whatever the tool calls it, so a writer can map it onto its own tool names. */
|
|
13
|
+
export type ToolKind = "read" | "edit" | "write" | "shell" | "search" | "list" | "web" | "mcp" | "task" | "todo" | "ask" | "other"
|
|
14
|
+
|
|
15
|
+
export interface Usage {
|
|
16
|
+
input?: number
|
|
17
|
+
output?: number
|
|
18
|
+
cacheRead?: number
|
|
19
|
+
cacheWrite?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Attachment {
|
|
23
|
+
kind: "image" | "file"
|
|
24
|
+
/** Where the bytes are, when they were kept: a path on this machine. */
|
|
25
|
+
path?: string
|
|
26
|
+
mime?: string
|
|
27
|
+
name?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Project {
|
|
31
|
+
/** The folder the session ran in. */
|
|
32
|
+
cwd?: string
|
|
33
|
+
gitRemote?: string
|
|
34
|
+
gitBranch?: string
|
|
35
|
+
gitCommit?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SessionHeader {
|
|
39
|
+
format: typeof FORMAT
|
|
40
|
+
/** Portable id: `<tool>:<sourceId>`. */
|
|
41
|
+
id: string
|
|
42
|
+
/** Reader id of the tool the session came from, e.g. "claude-code". */
|
|
43
|
+
tool: string
|
|
44
|
+
/** The tool version that wrote the session, from the session itself where it says. */
|
|
45
|
+
toolVersion?: string
|
|
46
|
+
/** The session's id in its own tool. */
|
|
47
|
+
sourceId: string
|
|
48
|
+
/** The file or database the session was read from. */
|
|
49
|
+
sourcePath?: string
|
|
50
|
+
title?: string
|
|
51
|
+
project: Project
|
|
52
|
+
startedAt?: string
|
|
53
|
+
updatedAt?: string
|
|
54
|
+
/** Models that answered, in order of first use. */
|
|
55
|
+
models: string[]
|
|
56
|
+
/** Where this session came from, when it is a fork, a subagent, or a continuation of another. */
|
|
57
|
+
parent?: { id: string; kind: "fork" | "subagent" | "continued" }
|
|
58
|
+
/**
|
|
59
|
+
* What did not survive reading, as short phrases ("signed reasoning (12 blocks)", "images (2)",
|
|
60
|
+
* "unknown record types: frame-link"). Writers append what they drop in turn.
|
|
61
|
+
*/
|
|
62
|
+
lost: string[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type Turn =
|
|
66
|
+
| { kind: "user"; at?: string; text: string; attachments?: Attachment[] }
|
|
67
|
+
| { kind: "assistant"; at?: string; text: string; model?: string; usage?: Usage }
|
|
68
|
+
| {
|
|
69
|
+
kind: "tool_call"
|
|
70
|
+
at?: string
|
|
71
|
+
/** Unique within the session; writers remap it to their tool's id style. */
|
|
72
|
+
id: string
|
|
73
|
+
/** The tool's own name for it, e.g. "Bash", "run_terminal_cmd", "shell". */
|
|
74
|
+
name: string
|
|
75
|
+
toolKind: ToolKind
|
|
76
|
+
input: unknown
|
|
77
|
+
}
|
|
78
|
+
| {
|
|
79
|
+
kind: "tool_result"
|
|
80
|
+
at?: string
|
|
81
|
+
/** The id of the tool_call this answers. */
|
|
82
|
+
callId: string
|
|
83
|
+
ok: boolean
|
|
84
|
+
output: string
|
|
85
|
+
/** The output was cut; `fullPath` holds all of it when it was kept. */
|
|
86
|
+
truncated?: boolean
|
|
87
|
+
fullPath?: string
|
|
88
|
+
}
|
|
89
|
+
| { kind: "file_edit"; at?: string; path: string; patch?: string; callId?: string }
|
|
90
|
+
/** Readable reasoning only. Signed or encrypted reasoning never crosses tools: it is counted in `lost`. */
|
|
91
|
+
| { kind: "reasoning"; at?: string; text: string }
|
|
92
|
+
/** A summary that replaced the turns before it (the tool's own compaction). */
|
|
93
|
+
| { kind: "compaction"; at?: string; summary: string }
|
|
94
|
+
/** A subagent ran here; its session is in `Session.children` under this id. */
|
|
95
|
+
| { kind: "subagent"; at?: string; sessionId: string; callId?: string; description?: string }
|
|
96
|
+
|
|
97
|
+
export type TurnKind = Turn["kind"]
|
|
98
|
+
|
|
99
|
+
export interface Session {
|
|
100
|
+
header: SessionHeader
|
|
101
|
+
turns: Turn[]
|
|
102
|
+
/** Subagent sessions, referenced by `subagent` turns. */
|
|
103
|
+
children?: Session[]
|
|
104
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { FORMAT, type Session, type SessionHeader, type ToolKind, type Turn, type TurnKind } from "./types.ts"
|
|
2
|
+
|
|
3
|
+
/** Tool outputs longer than this are cut when converted; the reader keeps a path to the full text where it can. */
|
|
4
|
+
export const OUTPUT_LIMIT = 20_000
|
|
5
|
+
|
|
6
|
+
const KIND_BY_NAME: Array<[RegExp, ToolKind]> = [
|
|
7
|
+
[/^mcp[_-]|^mcp__|CallDynamicTool|GetDynamicTools|use_mcp_tool|access_mcp_resource/i, "mcp"],
|
|
8
|
+
[/^(Agent|Task|spawn_agent|delegate|subagent|new_task)/i, "task"],
|
|
9
|
+
[/todo|update_plan/i, "todo"],
|
|
10
|
+
[/ask_?(user_?)?question|AskUserQuestion|AskQuestion|ask_followup|request_user_input/i, "ask"],
|
|
11
|
+
[/web_?(search|fetch)|WebFetch|WebSearch|fetch_url|fetch_webpage|browser/i, "web"],
|
|
12
|
+
[/(str_?replace|search_replace|apply_patch|apply_diff|replace_in_file|replace_string_in_file|^replace$|insert_content|edit_file|multi_?edit|^Edit$|^MultiEdit$|StrReplace|insert_edit)/i, "edit"],
|
|
13
|
+
[/(^Write$|write_file|write_to_file|create_file|^create$|new_file)/i, "write"],
|
|
14
|
+
[/(^Read$|read_file|view_file|view_image|open_file|^view$|cat_file|read_many_files)/i, "read"],
|
|
15
|
+
[/(grep|ripgrep|search|find|^Glob$|glob|codebase_search|semantic|list_code_definition)/i, "search"],
|
|
16
|
+
[/(list_dir|list_files|^LS$|ls_dir|list_directory)/i, "list"],
|
|
17
|
+
[/(bash|shell|terminal|command|exec|run_cmd|local_shell|powershell|write_stdin)/i, "shell"],
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
/** What a tool call does, from its name: "run_terminal_cmd" and "Bash" are both "shell". */
|
|
21
|
+
export function toolKindOf(name: string): ToolKind {
|
|
22
|
+
for (const [re, kind] of KIND_BY_NAME) if (re.test(name)) return kind
|
|
23
|
+
return "other"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Text cut to `limit` characters, keeping the start and the end, and whether it was cut. */
|
|
27
|
+
export function clip(text: string, limit = OUTPUT_LIMIT): { text: string; truncated: boolean } {
|
|
28
|
+
if (text.length <= limit) return { text, truncated: false }
|
|
29
|
+
const half = Math.floor((limit - 40) / 2)
|
|
30
|
+
return { text: `${text.slice(0, half)}\n[... ${text.length - 2 * half} characters cut ...]\n${text.slice(-half)}`, truncated: true }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Any value as text: strings as they are, everything else as JSON. */
|
|
34
|
+
export function asText(v: unknown): string {
|
|
35
|
+
if (v === undefined || v === null) return ""
|
|
36
|
+
if (typeof v === "string") return v
|
|
37
|
+
try {
|
|
38
|
+
return JSON.stringify(v)
|
|
39
|
+
} catch {
|
|
40
|
+
return String(v)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** A timestamp in ISO form, from ISO strings or epoch seconds/milliseconds; undefined when it is neither. */
|
|
45
|
+
export function iso(v: unknown): string | undefined {
|
|
46
|
+
if (typeof v === "string" && v) {
|
|
47
|
+
const t = Date.parse(v)
|
|
48
|
+
return Number.isFinite(t) ? new Date(t).toISOString() : undefined
|
|
49
|
+
}
|
|
50
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0) return new Date(v < 1e12 ? v * 1000 : v).toISOString()
|
|
51
|
+
return undefined
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function header(tool: string, sourceId: string, fields: Partial<Omit<SessionHeader, "format" | "id" | "tool" | "sourceId">> = {}): SessionHeader {
|
|
55
|
+
return { format: FORMAT, id: `${tool}:${sourceId}`, tool, sourceId, project: {}, models: [], lost: [], ...fields }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Adds a model to the header's list once. */
|
|
59
|
+
export function noteModel(h: SessionHeader, model: string | undefined) {
|
|
60
|
+
if (model && !h.models.includes(model)) h.models.push(model)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Counts things that were dropped while reading, e.g. lost.add("signed reasoning"), then lost.flush(header). */
|
|
64
|
+
export function lossCounter() {
|
|
65
|
+
const counts = new Map<string, number>()
|
|
66
|
+
return {
|
|
67
|
+
add(what: string, n = 1) {
|
|
68
|
+
counts.set(what, (counts.get(what) ?? 0) + n)
|
|
69
|
+
},
|
|
70
|
+
flush(h: SessionHeader) {
|
|
71
|
+
for (const [what, n] of counts) h.lost.push(`${what} (${n})`)
|
|
72
|
+
counts.clear()
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function countTurns(s: Session): Record<TurnKind, number> {
|
|
78
|
+
const out = { user: 0, assistant: 0, tool_call: 0, tool_result: 0, file_edit: 0, reasoning: 0, compaction: 0, subagent: 0 } as Record<TurnKind, number>
|
|
79
|
+
for (const t of s.turns) out[t.kind]++
|
|
80
|
+
return out
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Tool calls without a result and results without a call: a reader or writer that loses pairing shows up here. */
|
|
84
|
+
export function unpaired(s: Session): { calls: string[]; results: string[] } {
|
|
85
|
+
const calls = new Set(s.turns.filter((t) => t.kind === "tool_call").map((t) => (t as Extract<Turn, { kind: "tool_call" }>).id))
|
|
86
|
+
const results = new Set(s.turns.filter((t) => t.kind === "tool_result").map((t) => (t as Extract<Turn, { kind: "tool_result" }>).callId))
|
|
87
|
+
return { calls: [...calls].filter((c) => !results.has(c)), results: [...results].filter((r) => !calls.has(r)) }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function toJsonl(s: Session): string {
|
|
91
|
+
const lines = [JSON.stringify(s.header), ...s.turns.map((t) => JSON.stringify(t))]
|
|
92
|
+
for (const c of s.children ?? []) lines.push(JSON.stringify({ kind: "child", session: c }))
|
|
93
|
+
return `${lines.join("\n")}\n`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function fromJsonl(text: string): Session {
|
|
97
|
+
const lines = text.split("\n").filter((l) => l.trim())
|
|
98
|
+
const header = JSON.parse(lines[0]!) as SessionHeader
|
|
99
|
+
if (header.format !== FORMAT) throw new Error(`not a ${FORMAT} file (format: ${String(header.format)})`)
|
|
100
|
+
const turns: Turn[] = []
|
|
101
|
+
const children: Session[] = []
|
|
102
|
+
for (const l of lines.slice(1)) {
|
|
103
|
+
const v = JSON.parse(l) as Turn | { kind: "child"; session: Session }
|
|
104
|
+
if (v.kind === "child") children.push((v as { session: Session }).session)
|
|
105
|
+
else turns.push(v as Turn)
|
|
106
|
+
}
|
|
107
|
+
return children.length ? { header, turns, children } : { header, turns }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The first user message, the usual best guess at a session's purpose and title. */
|
|
111
|
+
export function firstUserText(s: Session): string | undefined {
|
|
112
|
+
return (s.turns.find((t) => t.kind === "user" && t.text.trim() && !isInjected(t.text)) as Extract<Turn, { kind: "user" }> | undefined)?.text
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Text a tool injected into the conversation (reminders, background-task notices, slash-command echoes)
|
|
117
|
+
* rather than something the person typed. Writers keep it for the model but must not present it as the
|
|
118
|
+
* person's own message in a history list or title.
|
|
119
|
+
*/
|
|
120
|
+
const INJECTED_TAG = "system-reminder|task-notification|ci-monitor-event|command-name|command-message|command-args|local-command-stdout|local-command-stderr|local-command-caveat"
|
|
121
|
+
const LEADING_BLOCK = new RegExp(`^<(${INJECTED_TAG})\\b[^>]*>[\\s\\S]*?(?:</\\1>|$)\\s*`)
|
|
122
|
+
|
|
123
|
+
/** The text with any leading injected blocks removed (what the person actually wrote, if anything). */
|
|
124
|
+
export function withoutInjected(text: string): string {
|
|
125
|
+
let t = text.trimStart()
|
|
126
|
+
for (let m = LEADING_BLOCK.exec(t); m; m = LEADING_BLOCK.exec(t)) t = t.slice(m[0].length)
|
|
127
|
+
return t
|
|
128
|
+
}
|
|
129
|
+
/** True when the text is nothing but injected blocks. */
|
|
130
|
+
export const isInjected = (text: string): boolean => !withoutInjected(text).trim()
|
package/src/sqlite.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
import { copyFileSync, existsSync, mkdtempSync, rmSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { basename, join } from "node:path"
|
|
5
|
+
|
|
6
|
+
// Reading another tool's SQLite store without touching it. Opening a live database, even read-only,
|
|
7
|
+
// can write its -shm file, and `immutable=1` skips whatever is still in the write-ahead log, which is
|
|
8
|
+
// where the newest turns are (Cursor, Codex, Warp, Zed and OpenCode 2 all use WAL). So the database is
|
|
9
|
+
// copied together with its -wal and -shm files and the copy is read.
|
|
10
|
+
|
|
11
|
+
export interface Snapshot {
|
|
12
|
+
db: Database
|
|
13
|
+
close(): void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function snapshot(path: string): Snapshot {
|
|
17
|
+
const dir = mkdtempSync(join(tmpdir(), "brain-sqlite-"))
|
|
18
|
+
let db: Database
|
|
19
|
+
try {
|
|
20
|
+
const copy = join(dir, basename(path))
|
|
21
|
+
copyFileSync(path, copy)
|
|
22
|
+
for (const side of ["-wal", "-shm"]) if (existsSync(path + side)) copyFileSync(path + side, copy + side)
|
|
23
|
+
// Opened read-write: it is our copy, and SQLite needs to write the -shm file to read the log.
|
|
24
|
+
// (Bun rejects an explicit { readonly: false }; the default is read-write.)
|
|
25
|
+
db = new Database(copy)
|
|
26
|
+
} catch (e) {
|
|
27
|
+
// The copy holds the person's data: never leave it behind.
|
|
28
|
+
rmSync(dir, { recursive: true, force: true })
|
|
29
|
+
throw e
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
db,
|
|
33
|
+
close() {
|
|
34
|
+
db.close()
|
|
35
|
+
rmSync(dir, { recursive: true, force: true })
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Runs `use` on a snapshot of the database and always cleans up. */
|
|
41
|
+
export function withSnapshot<T>(path: string, use: (db: Database) => T): T {
|
|
42
|
+
const s = snapshot(path)
|
|
43
|
+
try {
|
|
44
|
+
return use(s.db)
|
|
45
|
+
} finally {
|
|
46
|
+
s.close()
|
|
47
|
+
}
|
|
48
|
+
}
|