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