@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,261 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs"
|
|
2
|
+
import { basename, join } from "node:path"
|
|
3
|
+
import { exists, gitInfo, ls, mtime } from "../../fsutil.ts"
|
|
4
|
+
import type { Session, Turn, Usage } from "../../session/types.ts"
|
|
5
|
+
import { clip, header, iso, lossCounter, noteModel } from "../../session/util.ts"
|
|
6
|
+
import { CONTINUED_NOTE } from "../claude-code/records.ts"
|
|
7
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
8
|
+
import { type Entry, NOTE_TYPE, type PiFlavor, activeBranch, bashExecutionText, kindOf, migrate, parseEntries } from "./records.ts"
|
|
9
|
+
|
|
10
|
+
// Reads Pi-format sessions (Pi, and Auggie 1.x which is built on it): <root>/--<cwd>--/<file>.jsonl.
|
|
11
|
+
// The session is the active branch, the walk Pi resumes. Compactions become compaction turns placed where
|
|
12
|
+
// the model's context starts over (before the first entry they kept), with every earlier turn kept in
|
|
13
|
+
// front of them, so a writer can start from the summary or carry the whole history. Context edits are
|
|
14
|
+
// applied as Pi applies them: a removed entry is left out, a replaced one carries its new content.
|
|
15
|
+
|
|
16
|
+
const PEEK = 64 * 1024
|
|
17
|
+
|
|
18
|
+
type Lost = ReturnType<typeof lossCounter>
|
|
19
|
+
|
|
20
|
+
const textOf = (content: unknown, lost?: Lost, imageWhat = "images"): string => {
|
|
21
|
+
if (typeof content === "string") return content
|
|
22
|
+
if (!Array.isArray(content)) return ""
|
|
23
|
+
const parts: string[] = []
|
|
24
|
+
for (const b of content as Entry[]) {
|
|
25
|
+
if (b?.type === "text" && typeof b.text === "string") parts.push(b.text)
|
|
26
|
+
else if (b?.type === "image") lost?.add(imageWhat)
|
|
27
|
+
}
|
|
28
|
+
return parts.join("\n")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const usageOf = (u: Entry | undefined): Usage | undefined => {
|
|
32
|
+
if (!u || typeof u !== "object") return undefined
|
|
33
|
+
const out: Usage = {}
|
|
34
|
+
if (u.input) out.input = u.input
|
|
35
|
+
if (u.output) out.output = u.output
|
|
36
|
+
if (u.cacheRead) out.cacheRead = u.cacheRead
|
|
37
|
+
if (u.cacheWrite) out.cacheWrite = u.cacheWrite
|
|
38
|
+
return Object.keys(out).length ? out : undefined
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One Pi session file as the portable session. */
|
|
42
|
+
export function parseSession(text: string, path: string, f: PiFlavor, home?: string): Session {
|
|
43
|
+
const entries = parseEntries(text)
|
|
44
|
+
if (!entries.length) throw new Error(`${path} is not a ${f.label} session (no session header)`)
|
|
45
|
+
migrate(entries)
|
|
46
|
+
const head = entries[0]!
|
|
47
|
+
const id = String(head.id)
|
|
48
|
+
const cwd = typeof head.cwd === "string" && head.cwd ? head.cwd : undefined
|
|
49
|
+
const branch = activeBranch(entries)
|
|
50
|
+
const lost = lossCounter()
|
|
51
|
+
const turns: Turn[] = []
|
|
52
|
+
const h = header(f.tool, id, { sourcePath: path, project: { ...(cwd ? { cwd, ...gitInfo(cwd, home) } : {}) }, ...(iso(head.timestamp) ? { startedAt: iso(head.timestamp) } : {}) })
|
|
53
|
+
|
|
54
|
+
// Where each compaction's context starts over: before the first entry it kept, when that comes before it.
|
|
55
|
+
const indexOf = new Map(branch.map((e, i) => [e.id as string, i]))
|
|
56
|
+
const placedBefore = new Map<number, Entry[]>()
|
|
57
|
+
const moved = new Set<Entry>()
|
|
58
|
+
for (const [i, e] of branch.entries()) {
|
|
59
|
+
if (e.type !== "compaction") continue
|
|
60
|
+
const k = indexOf.get(e.firstKeptEntryId)
|
|
61
|
+
if (k !== undefined && k < i) {
|
|
62
|
+
placedBefore.set(k, [...(placedBefore.get(k) ?? []), e])
|
|
63
|
+
moved.add(e)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// The latest context edit of each entry on the branch wins.
|
|
67
|
+
const edits = new Map<string, Entry>()
|
|
68
|
+
for (const e of branch) if (e.type === "context_edit" && typeof e.targetId === "string") edits.set(e.targetId, e)
|
|
69
|
+
|
|
70
|
+
const calls = new Map<string, Extract<Turn, { kind: "tool_call" }>>()
|
|
71
|
+
const compaction = (e: Entry) => {
|
|
72
|
+
if (typeof e.summary === "string" && e.summary.trim()) turns.push({ kind: "compaction", ...at(e.timestamp), summary: e.summary })
|
|
73
|
+
}
|
|
74
|
+
let title: string | undefined
|
|
75
|
+
let continued: string | undefined
|
|
76
|
+
let last: string | undefined
|
|
77
|
+
|
|
78
|
+
for (const [i, e] of branch.entries()) {
|
|
79
|
+
for (const c of placedBefore.get(i) ?? []) compaction(c)
|
|
80
|
+
if (typeof e.timestamp === "string") last = e.timestamp
|
|
81
|
+
switch (e.type) {
|
|
82
|
+
case "message": {
|
|
83
|
+
const edit = edits.get(e.id)
|
|
84
|
+
if (edit && edit.replacement === null) break
|
|
85
|
+
const m = edit ? { ...e.message, content: edit.replacement?.content } : e.message
|
|
86
|
+
message(m, e.timestamp)
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
case "compaction":
|
|
90
|
+
if (!moved.has(e)) compaction(e)
|
|
91
|
+
break
|
|
92
|
+
case "custom_message": {
|
|
93
|
+
const note = typeof e.content === "string" ? e.content.match(CONTINUED_NOTE) : null
|
|
94
|
+
if (e.customType === NOTE_TYPE) {
|
|
95
|
+
if (note) continued = note[1]
|
|
96
|
+
} else lost.add("extension messages")
|
|
97
|
+
break
|
|
98
|
+
}
|
|
99
|
+
case "session_info":
|
|
100
|
+
title = typeof e.name === "string" && e.name.trim() ? e.name.trim() : undefined
|
|
101
|
+
break
|
|
102
|
+
case "model_change":
|
|
103
|
+
noteModel(h, typeof e.modelId === "string" ? e.modelId : undefined)
|
|
104
|
+
break
|
|
105
|
+
case "branch_summary":
|
|
106
|
+
lost.add("summaries of abandoned branches")
|
|
107
|
+
break
|
|
108
|
+
case "thinking_level_change":
|
|
109
|
+
case "usage":
|
|
110
|
+
case "custom":
|
|
111
|
+
case "label":
|
|
112
|
+
case "context_edit":
|
|
113
|
+
break
|
|
114
|
+
default:
|
|
115
|
+
lost.add(`unknown entry types: ${String(e.type)}`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function message(m: Entry, when: string | undefined) {
|
|
120
|
+
if (!m || typeof m !== "object") return
|
|
121
|
+
const stamp = at(typeof m.timestamp === "number" ? m.timestamp : when)
|
|
122
|
+
switch (m.role) {
|
|
123
|
+
case "user": {
|
|
124
|
+
const text = textOf(m.content, lost)
|
|
125
|
+
if (text.trim()) turns.push({ kind: "user", ...stamp, text })
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
// Auggie's slash-command and rule messages reach the model as the user's text.
|
|
129
|
+
case "promptCommandInvocation":
|
|
130
|
+
if (typeof m.expandedBody === "string" && m.expandedBody.trim()) turns.push({ kind: "user", ...stamp, text: m.expandedBody })
|
|
131
|
+
break
|
|
132
|
+
case "ruleReference":
|
|
133
|
+
if (typeof m.llmText === "string" && m.llmText.trim()) turns.push({ kind: "user", ...stamp, text: m.llmText })
|
|
134
|
+
break
|
|
135
|
+
case "assistant": {
|
|
136
|
+
const model = typeof m.model === "string" ? m.model : undefined
|
|
137
|
+
noteModel(h, model)
|
|
138
|
+
let usage = usageOf(m.usage)
|
|
139
|
+
for (const b of Array.isArray(m.content) ? (m.content as Entry[]) : []) {
|
|
140
|
+
if (b?.type === "text" && typeof b.text === "string" && b.text.trim()) {
|
|
141
|
+
turns.push({ kind: "assistant", ...stamp, text: b.text, ...(model ? { model } : {}), ...(usage ? { usage } : {}) })
|
|
142
|
+
usage = undefined
|
|
143
|
+
} else if (b?.type === "thinking") {
|
|
144
|
+
if (b.redacted) lost.add("redacted reasoning")
|
|
145
|
+
else if (typeof b.thinking === "string" && b.thinking.trim()) turns.push({ kind: "reasoning", ...stamp, text: b.thinking })
|
|
146
|
+
else if (b.thinkingSignature) lost.add("signed reasoning")
|
|
147
|
+
} else if (b?.type === "toolCall" && typeof b.id === "string") {
|
|
148
|
+
const name = typeof b.name === "string" ? b.name : "tool"
|
|
149
|
+
const call: Extract<Turn, { kind: "tool_call" }> = { kind: "tool_call", ...stamp, id: b.id, name, toolKind: kindOf(name), input: b.arguments ?? {} }
|
|
150
|
+
calls.set(b.id, call)
|
|
151
|
+
turns.push(call)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
break
|
|
155
|
+
}
|
|
156
|
+
case "toolResult": {
|
|
157
|
+
if (typeof m.toolCallId !== "string") break
|
|
158
|
+
const full = clip(textOf(m.content, lost, "tool result images"))
|
|
159
|
+
const d = m.details && typeof m.details === "object" ? (m.details as Entry) : {}
|
|
160
|
+
const fullPath = typeof d.fullOutputPath === "string" ? d.fullOutputPath : undefined
|
|
161
|
+
const ok = m.isError !== true
|
|
162
|
+
turns.push({ kind: "tool_result", ...stamp, callId: m.toolCallId, ok, output: full.text, ...(full.truncated || d.truncation?.truncated ? { truncated: true } : {}), ...(fullPath ? { fullPath } : {}) })
|
|
163
|
+
// Pi's edit and write tools change one file each; the diff is in the edit's details.
|
|
164
|
+
const call = calls.get(m.toolCallId)
|
|
165
|
+
const target = (call?.input as Entry | undefined)?.path
|
|
166
|
+
if (ok && call && (call.name === "edit" || call.name === "write") && typeof target === "string")
|
|
167
|
+
turns.push({ kind: "file_edit", ...stamp, path: target, ...(typeof d.diff === "string" ? { patch: d.diff } : {}), callId: call.id })
|
|
168
|
+
break
|
|
169
|
+
}
|
|
170
|
+
case "bashExecution":
|
|
171
|
+
// A command the person ran with `!`: the model reads it as their message, unless kept out of context.
|
|
172
|
+
if (!m.excludeFromContext) turns.push({ kind: "user", ...stamp, text: bashExecutionText(m) })
|
|
173
|
+
break
|
|
174
|
+
case "custom":
|
|
175
|
+
lost.add("extension messages")
|
|
176
|
+
break
|
|
177
|
+
case "compactionSummary":
|
|
178
|
+
if (typeof m.summary === "string") turns.push({ kind: "compaction", ...stamp, summary: m.summary })
|
|
179
|
+
break
|
|
180
|
+
case "branchSummary":
|
|
181
|
+
lost.add("summaries of abandoned branches")
|
|
182
|
+
break
|
|
183
|
+
case "system":
|
|
184
|
+
lost.add(`${f.label} system prompt and tool declarations (not carried)`)
|
|
185
|
+
break
|
|
186
|
+
default:
|
|
187
|
+
lost.add(`unknown message roles: ${String(m.role)}`)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
h.title = title ?? (turns.find((t) => t.kind === "user") as Extract<Turn, { kind: "user" }> | undefined)?.text.replace(/\s+/g, " ").trim().slice(0, 80)
|
|
192
|
+
if (!h.title) delete h.title
|
|
193
|
+
const updated = iso(last)
|
|
194
|
+
if (updated) h.updatedAt = updated
|
|
195
|
+
if (continued) h.parent = { id: continued, kind: "continued" }
|
|
196
|
+
else if (typeof head.parentSession === "string" && head.parentSession) {
|
|
197
|
+
const parent = basename(head.parentSession, ".jsonl").replace(/^.*_/, "")
|
|
198
|
+
if (parent) h.parent = { id: `${f.tool}:${parent}`, kind: "fork" }
|
|
199
|
+
}
|
|
200
|
+
lost.flush(h)
|
|
201
|
+
return { header: h, turns }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function at(v: unknown): { at?: string } {
|
|
205
|
+
const t = iso(v)
|
|
206
|
+
return t ? { at: t } : {}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The header, the name the person gave it, and the first request, from the start and end of a file. */
|
|
210
|
+
async function peek(path: string): Promise<Omit<SessionRef, "tool" | "path"> | undefined> {
|
|
211
|
+
const file = Bun.file(path)
|
|
212
|
+
const text = file.size <= 4 * PEEK ? await file.text() : `${await file.slice(0, PEEK).text()}\n${await file.slice(file.size - PEEK).text()}`
|
|
213
|
+
const entries = parseEntries(text)
|
|
214
|
+
const head = entries[0]
|
|
215
|
+
if (!head) return undefined
|
|
216
|
+
let name: string | undefined
|
|
217
|
+
let first: string | undefined
|
|
218
|
+
for (const e of entries) {
|
|
219
|
+
if (e.type === "session_info") name = typeof e.name === "string" && e.name.trim() ? e.name.trim() : undefined
|
|
220
|
+
if (!first && e.type === "message" && e.message?.role === "user") first = textOf(e.message.content).replace(/\s+/g, " ").trim().slice(0, 80) || undefined
|
|
221
|
+
}
|
|
222
|
+
const title = name ?? first
|
|
223
|
+
return {
|
|
224
|
+
sourceId: String(head.id),
|
|
225
|
+
...(title ? { title } : {}),
|
|
226
|
+
...(typeof head.cwd === "string" && head.cwd ? { cwd: head.cwd } : {}),
|
|
227
|
+
...(mtime(path) ? { updatedAt: mtime(path) } : {}),
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function piFamilyReader(f: PiFlavor): Reader {
|
|
232
|
+
return {
|
|
233
|
+
tool: f.tool,
|
|
234
|
+
label: f.label,
|
|
235
|
+
|
|
236
|
+
async list(home: string): Promise<SessionRef[]> {
|
|
237
|
+
const root = f.sessionsRoot(home)
|
|
238
|
+
const refs: SessionRef[] = []
|
|
239
|
+
for (const project of ls(root)) {
|
|
240
|
+
const dir = join(root, project)
|
|
241
|
+
for (const name of ls(dir)) {
|
|
242
|
+
if (!name.endsWith(".jsonl")) continue
|
|
243
|
+
const path = join(dir, name)
|
|
244
|
+
try {
|
|
245
|
+
if (!statSync(path).isFile()) continue
|
|
246
|
+
const p = await peek(path)
|
|
247
|
+
if (p) refs.push({ tool: f.tool, path, ...p })
|
|
248
|
+
} catch {
|
|
249
|
+
// Deleted or unreadable between listing and reading: not a session to offer.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
async read(ref: SessionRef, home: string): Promise<Session> {
|
|
257
|
+
if (!exists(ref.path)) throw new Error(`no ${f.label} session at ${ref.path}`)
|
|
258
|
+
return parseSession(readFileSync(ref.path, "utf8"), ref.path, f, home)
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import type { ToolKind } from "../../session/types.ts"
|
|
3
|
+
import { toolKindOf } from "../../session/util.ts"
|
|
4
|
+
|
|
5
|
+
// Facts about Pi's session files, shared by the Pi and Auggie 1.x adapters: Auggie 1.x is built on Pi and
|
|
6
|
+
// keeps the same entries in a different folder. Checked against earendil-works/pi at 1.5.0
|
|
7
|
+
// (packages/coding-agent/src/core/session-manager.ts, messages.ts, tools/*.ts) and the bundle of
|
|
8
|
+
// @augmentcode/auggie 1.2.0-prerelease.202611061509; see docs/sessions/2026-11-08-session-formats.md, entries 17 and 43.
|
|
9
|
+
//
|
|
10
|
+
// A file is a header line {type:"session", version:3, id, timestamp, cwd} and then entries on a tree:
|
|
11
|
+
// each has an id (8 hex characters) and a parentId. Pi resumes at the last entry and walks parentId back to
|
|
12
|
+
// the root, so the active branch is that walk. Messages hold Pi's AgentMessage: user, assistant (content
|
|
13
|
+
// blocks text / thinking / toolCall), toolResult (one message per call, joined by toolCallId), plus
|
|
14
|
+
// Pi's own roles (bashExecution, custom). A compaction entry replaces everything before firstKeptEntryId
|
|
15
|
+
// with its summary; one that keeps nothing names itself.
|
|
16
|
+
|
|
17
|
+
export const SESSION_VERSION = 3
|
|
18
|
+
|
|
19
|
+
/** customType of the hidden note a writer adds; readers skip it. */
|
|
20
|
+
export const NOTE_TYPE = "brain-continued"
|
|
21
|
+
|
|
22
|
+
export type Entry = Record<string, any>
|
|
23
|
+
|
|
24
|
+
export interface PiFlavor {
|
|
25
|
+
/** Reader and writer id, e.g. "pi". */
|
|
26
|
+
tool: string
|
|
27
|
+
label: string
|
|
28
|
+
/** The command, for `--version` and the open command. */
|
|
29
|
+
bin: string
|
|
30
|
+
/** The folder holding one sub-folder of session files per project. */
|
|
31
|
+
sessionsRoot(home: string): string
|
|
32
|
+
/** The session file's name: Pi prefixes the start time, Auggie does not. */
|
|
33
|
+
fileName(id: string, startedAt: string): string
|
|
34
|
+
/** A new session id in the tool's style. */
|
|
35
|
+
newId(): string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The folder Pi keeps a project's sessions in: the cwd without its leading separator, / \ and : turned into -, between --. */
|
|
39
|
+
export const projectFolder = (cwd: string) => `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`
|
|
40
|
+
|
|
41
|
+
export const sessionDir = (f: PiFlavor, home: string, cwd: string) => join(f.sessionsRoot(home), projectFolder(cwd))
|
|
42
|
+
|
|
43
|
+
/** Every entry of a session file, header first; none when the first line is not a session header (Pi's own check). */
|
|
44
|
+
export function parseEntries(text: string): Entry[] {
|
|
45
|
+
const out: Entry[] = []
|
|
46
|
+
for (const line of text.split("\n")) {
|
|
47
|
+
if (!line.trim()) continue
|
|
48
|
+
try {
|
|
49
|
+
out.push(JSON.parse(line))
|
|
50
|
+
} catch {
|
|
51
|
+
// a half-written line: Pi skips it too
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const h = out[0]
|
|
55
|
+
return h?.type === "session" && typeof h.id === "string" ? out : []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Version 1 files have no ids: give them the linear chain Pi's own migration gives them, so one walk reads every version. */
|
|
59
|
+
export function migrate(entries: Entry[]): void {
|
|
60
|
+
if ((entries[0]?.version ?? 1) >= 2) return
|
|
61
|
+
let prev: string | null = null
|
|
62
|
+
entries.forEach((e, i) => {
|
|
63
|
+
if (i === 0) return
|
|
64
|
+
e.id = `v1-${i}`
|
|
65
|
+
e.parentId = prev
|
|
66
|
+
prev = e.id
|
|
67
|
+
})
|
|
68
|
+
for (const e of entries) {
|
|
69
|
+
if (e.type === "compaction" && typeof e.firstKeptEntryIndex === "number") e.firstKeptEntryId = entries[e.firstKeptEntryIndex]?.id
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The active branch: from the last entry back to the root, as Pi resumes it. */
|
|
74
|
+
export function activeBranch(entries: Entry[]): Entry[] {
|
|
75
|
+
const body = entries.filter((e) => e.type !== "session" && e.type !== "queue_state" && typeof e.id === "string")
|
|
76
|
+
const byId = new Map(body.map((e) => [e.id as string, e]))
|
|
77
|
+
const path: Entry[] = []
|
|
78
|
+
const seen = new Set<string>()
|
|
79
|
+
for (let e = body[body.length - 1]; e && !seen.has(e.id); e = e.parentId ? byId.get(e.parentId) : undefined) {
|
|
80
|
+
seen.add(e.id)
|
|
81
|
+
path.push(e)
|
|
82
|
+
}
|
|
83
|
+
return path.reverse()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** What Pi's built-in tools do, so a call reads back with the right kind. */
|
|
87
|
+
const PI_KINDS: Record<string, ToolKind> = { read: "read", bash: "shell", edit: "edit", write: "write", grep: "search", find: "search", ls: "list" }
|
|
88
|
+
|
|
89
|
+
export const kindOf = (name: string): ToolKind => PI_KINDS[name] ?? toolKindOf(name)
|
|
90
|
+
|
|
91
|
+
/** The text Pi gives the model for a shell command the person ran with `!` (messages.ts bashExecutionToText). */
|
|
92
|
+
export function bashExecutionText(m: Entry): string {
|
|
93
|
+
let text = `Ran \`${m.command}\`\n`
|
|
94
|
+
text += m.output ? `\`\`\`\n${m.output}\n\`\`\`` : "(no output)"
|
|
95
|
+
if (m.cancelled) text += "\n\n(command cancelled)"
|
|
96
|
+
else if (m.exitCode !== null && m.exitCode !== undefined && m.exitCode !== 0) text += `\n\nCommand exited with code ${m.exitCode}`
|
|
97
|
+
if (m.truncated && m.fullOutputPath) text += `\n\n[Output truncated. Full output: ${m.fullOutputPath}]`
|
|
98
|
+
return text
|
|
99
|
+
}
|