@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,302 @@
|
|
|
1
|
+
import { basename, dirname, join } from "node:path"
|
|
2
|
+
import { exists, isDir, ls, readJson, readJsonl } from "../../fsutil.ts"
|
|
3
|
+
import type { Session, SessionHeader, Turn } from "../../session/types.ts"
|
|
4
|
+
import { clip, header, iso, lossCounter, noteModel, toolKindOf } from "../../session/util.ts"
|
|
5
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
6
|
+
import { CONTINUED_NOTE, LABEL, SUMMARY_PREFIX, SYNTHETIC, TOOL, grokKind, sessionsDir, unwrapReminder } from "./records.ts"
|
|
7
|
+
|
|
8
|
+
// Reads Grok CLI sessions: <home>/.grok/sessions/<encoded cwd>/<uuid>/. The conversation comes from
|
|
9
|
+
// chat_history.jsonl, the messages Grok gives the model (tool calls paired with results by id);
|
|
10
|
+
// updates.jsonl adds what only the screen log has: when each prompt and tool call happened and whether a
|
|
11
|
+
// call failed. Subagents are sessions of their own in the same tree, linked from the parent's
|
|
12
|
+
// subagents/<id>/meta.json and named in the spawn_subagent result.
|
|
13
|
+
|
|
14
|
+
type Row = Record<string, any>
|
|
15
|
+
|
|
16
|
+
interface Summary {
|
|
17
|
+
info?: { id?: string; cwd?: string }
|
|
18
|
+
generated_title?: string
|
|
19
|
+
session_summary?: string
|
|
20
|
+
created_at?: string
|
|
21
|
+
updated_at?: string
|
|
22
|
+
last_active_at?: string
|
|
23
|
+
current_model_id?: string
|
|
24
|
+
session_kind?: string
|
|
25
|
+
git_remotes?: string[]
|
|
26
|
+
head_branch?: string
|
|
27
|
+
head_commit?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const textOf = (content: unknown): { text: string; images: number } => {
|
|
31
|
+
if (typeof content === "string") return { text: content, images: 0 }
|
|
32
|
+
let images = 0
|
|
33
|
+
const parts: string[] = []
|
|
34
|
+
for (const p of Array.isArray(content) ? (content as Row[]) : []) {
|
|
35
|
+
if (typeof p?.text === "string") parts.push(p.text)
|
|
36
|
+
else if (p?.type === "image" || p?.type === "image_url") images++
|
|
37
|
+
}
|
|
38
|
+
return { text: parts.join("\n"), images }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What updates.jsonl knows that chat_history.jsonl does not: prompt and call times, failed calls. */
|
|
42
|
+
function screenLog(path: string) {
|
|
43
|
+
const promptAt = new Map<number, string>()
|
|
44
|
+
const callAt = new Map<string, string>()
|
|
45
|
+
const resultAt = new Map<string, string>()
|
|
46
|
+
const failed = new Set<string>()
|
|
47
|
+
for (const o of readJsonl(path).rows) {
|
|
48
|
+
const u = o?.params?.update as Row | undefined
|
|
49
|
+
const at = iso(o?.params?._meta?.agentTimestampMs) ?? iso(o?.timestamp)
|
|
50
|
+
if (!u || !at) continue
|
|
51
|
+
if (u.sessionUpdate === "user_message_chunk" && typeof u._meta?.promptIndex === "number" && !promptAt.has(u._meta.promptIndex)) promptAt.set(u._meta.promptIndex, at)
|
|
52
|
+
if (u.sessionUpdate === "tool_call" && typeof u.toolCallId === "string") callAt.set(u.toolCallId, at)
|
|
53
|
+
if (u.sessionUpdate === "tool_call_update" && typeof u.toolCallId === "string") {
|
|
54
|
+
if (u.status === "failed") failed.add(u.toolCallId)
|
|
55
|
+
if (u.status === "completed" || u.status === "failed") resultAt.set(u.toolCallId, at)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { promptAt, callAt, resultAt, failed }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The conversation from updates.jsonl alone, for a folder with no chat_history.jsonl (the sanitized grok 1.0.5
|
|
63
|
+
* capture in the session-migrate corpus has only summary.json and updates.jsonl). The screen log has the
|
|
64
|
+
* same turns: prompts (user_message_chunk), reply text in chunks, tool_call and its tool_call_update with
|
|
65
|
+
* the result; it lacks the model's own reasoning items and system reminders.
|
|
66
|
+
*/
|
|
67
|
+
function turnsFromScreenLog(path: string, h: SessionHeader, model: string | undefined, lost: ReturnType<typeof lossCounter>): { turns: Turn[]; spawnResults: Array<{ callId: string; text: string }> } {
|
|
68
|
+
const turns: Turn[] = []
|
|
69
|
+
const spawnResults: Array<{ callId: string; text: string }> = []
|
|
70
|
+
const names = new Map<string, string>()
|
|
71
|
+
const answered = new Set<string>()
|
|
72
|
+
let streaming: "user" | "assistant" | "reasoning" | undefined
|
|
73
|
+
let userModel = model
|
|
74
|
+
const append = (kind: "user" | "assistant" | "reasoning", text: string, at: string | undefined) => {
|
|
75
|
+
const last = turns[turns.length - 1]
|
|
76
|
+
if (streaming === kind && last?.kind === kind) {
|
|
77
|
+
;(last as { text: string }).text += text
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
streaming = kind
|
|
81
|
+
if (kind === "assistant") {
|
|
82
|
+
noteModel(h, userModel)
|
|
83
|
+
turns.push({ kind, ...(at ? { at } : {}), text, ...(userModel ? { model: userModel } : {}) })
|
|
84
|
+
} else turns.push({ kind, ...(at ? { at } : {}), text })
|
|
85
|
+
}
|
|
86
|
+
for (const o of readJsonl(path).rows) {
|
|
87
|
+
const u = o?.params?.update as Row | undefined
|
|
88
|
+
if (!u || typeof u.sessionUpdate !== "string") continue
|
|
89
|
+
const at = iso(o?.params?._meta?.agentTimestampMs) ?? iso(o?.timestamp)
|
|
90
|
+
const chunk = typeof u.content?.text === "string" ? (u.content.text as string) : ""
|
|
91
|
+
switch (u.sessionUpdate) {
|
|
92
|
+
case "user_message_chunk":
|
|
93
|
+
if (typeof u._meta?.modelId === "string") userModel = u._meta.modelId
|
|
94
|
+
// Chunks of one prompt share a promptIndex; a new index is a new prompt.
|
|
95
|
+
if (streaming === "user" && (turns[turns.length - 1] as { promptIndex?: number } | undefined)?.promptIndex === u._meta?.promptIndex) append("user", chunk, at)
|
|
96
|
+
else {
|
|
97
|
+
streaming = undefined
|
|
98
|
+
append("user", chunk, at)
|
|
99
|
+
;(turns[turns.length - 1] as { promptIndex?: unknown }).promptIndex = u._meta?.promptIndex
|
|
100
|
+
}
|
|
101
|
+
break
|
|
102
|
+
case "agent_message_chunk":
|
|
103
|
+
append("assistant", chunk, at)
|
|
104
|
+
break
|
|
105
|
+
case "agent_thought_chunk":
|
|
106
|
+
append("reasoning", chunk, at)
|
|
107
|
+
break
|
|
108
|
+
case "tool_call": {
|
|
109
|
+
if (typeof u.toolCallId !== "string") break
|
|
110
|
+
streaming = undefined
|
|
111
|
+
const name = typeof u._meta?.["x.ai/tool"]?.name === "string" ? (u._meta["x.ai/tool"].name as string) : typeof u.title === "string" ? u.title : "unknown"
|
|
112
|
+
names.set(u.toolCallId, name)
|
|
113
|
+
turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: u.toolCallId, name, toolKind: grokKind(name) ?? toolKindOf(name), input: u.rawInput ?? {} })
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
case "tool_call_update": {
|
|
117
|
+
// The first update of a call only restates it (kind, title, locations); the one with a status is the result.
|
|
118
|
+
if (typeof u.toolCallId !== "string" || (u.status !== "completed" && u.status !== "failed") || answered.has(u.toolCallId)) break
|
|
119
|
+
answered.add(u.toolCallId)
|
|
120
|
+
streaming = undefined
|
|
121
|
+
let images = 0
|
|
122
|
+
const parts: string[] = []
|
|
123
|
+
for (const c of Array.isArray(u.content) ? (u.content as Row[]) : []) {
|
|
124
|
+
if (typeof c?.content?.text === "string") parts.push(c.content.text)
|
|
125
|
+
else if (c?.content?.type === "image") images++
|
|
126
|
+
}
|
|
127
|
+
if (images) lost.add("images", images)
|
|
128
|
+
const raw = parts.join("\n")
|
|
129
|
+
const { text, truncated } = clip(raw)
|
|
130
|
+
turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId: u.toolCallId, ok: u.status === "completed", output: text, ...(truncated ? { truncated } : {}) })
|
|
131
|
+
if (names.get(u.toolCallId) === "spawn_subagent") spawnResults.push({ callId: u.toolCallId, text: raw })
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
case "turn_completed":
|
|
135
|
+
streaming = undefined
|
|
136
|
+
break
|
|
137
|
+
default:
|
|
138
|
+
break
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const t of turns) delete (t as { promptIndex?: unknown }).promptIndex
|
|
142
|
+
return { turns: turns.filter((t) => (t.kind === "user" || t.kind === "assistant" || t.kind === "reasoning" ? t.text.trim() : true)), spawnResults }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** One session folder as a portable session (children not attached). */
|
|
146
|
+
function readOne(dir: string): { session: Session; spawnResults: Array<{ callId: string; text: string }> } {
|
|
147
|
+
const summary = readJson<Summary>(join(dir, "summary.json")) ?? {}
|
|
148
|
+
const id = summary.info?.id ?? basename(dir)
|
|
149
|
+
const screenOnly = !exists(join(dir, "chat_history.jsonl")) && exists(join(dir, "updates.jsonl"))
|
|
150
|
+
if (!exists(join(dir, "chat_history.jsonl")) && !screenOnly) throw new Error(`${dir} has no chat_history.jsonl; Grok keeps the conversation there`)
|
|
151
|
+
const cwd = summary.info?.cwd
|
|
152
|
+
const h = header(TOOL, id, {
|
|
153
|
+
sourcePath: dir,
|
|
154
|
+
...(summary.generated_title || summary.session_summary ? { title: summary.generated_title || summary.session_summary } : {}),
|
|
155
|
+
project: {
|
|
156
|
+
...(cwd ? { cwd } : {}),
|
|
157
|
+
...(summary.git_remotes?.[0] ? { gitRemote: summary.git_remotes[0] } : {}),
|
|
158
|
+
...(summary.head_branch ? { gitBranch: summary.head_branch } : {}),
|
|
159
|
+
...(summary.head_commit ? { gitCommit: summary.head_commit } : {}),
|
|
160
|
+
},
|
|
161
|
+
...(iso(summary.created_at) ? { startedAt: iso(summary.created_at) } : {}),
|
|
162
|
+
...(iso(summary.last_active_at ?? summary.updated_at) ? { updatedAt: iso(summary.last_active_at ?? summary.updated_at) } : {}),
|
|
163
|
+
})
|
|
164
|
+
const lost = lossCounter()
|
|
165
|
+
if (screenOnly) {
|
|
166
|
+
const r = turnsFromScreenLog(join(dir, "updates.jsonl"), h, summary.current_model_id, lost)
|
|
167
|
+
lost.add("model history (no chat_history.jsonl: read from the screen log)")
|
|
168
|
+
lost.flush(h)
|
|
169
|
+
return { session: { header: h, turns: r.turns }, spawnResults: r.spawnResults }
|
|
170
|
+
}
|
|
171
|
+
const log = screenLog(join(dir, "updates.jsonl"))
|
|
172
|
+
const { rows, bad } = readJsonl(join(dir, "chat_history.jsonl"))
|
|
173
|
+
if (bad) lost.add("unreadable chat history lines", bad)
|
|
174
|
+
|
|
175
|
+
const turns: Turn[] = []
|
|
176
|
+
const names = new Map<string, string>()
|
|
177
|
+
const spawnResults: Array<{ callId: string; text: string }> = []
|
|
178
|
+
for (const r of rows) {
|
|
179
|
+
switch (r.type) {
|
|
180
|
+
case "system":
|
|
181
|
+
// Grok's own system prompt; it writes a fresh one when the session loads.
|
|
182
|
+
break
|
|
183
|
+
case "user": {
|
|
184
|
+
const { text, images } = textOf(r.content)
|
|
185
|
+
if (images) lost.add("images", images)
|
|
186
|
+
if (r[SYNTHETIC]) {
|
|
187
|
+
const inner = unwrapReminder(text)
|
|
188
|
+
const from = inner.match(CONTINUED_NOTE)
|
|
189
|
+
if (from) h.parent = { id: from[1]!, kind: "continued" }
|
|
190
|
+
else if (inner.startsWith(SUMMARY_PREFIX)) turns.push({ kind: "compaction", summary: inner.replace(/^\[[^\]]*\]\s*/, "") })
|
|
191
|
+
else lost.add("Grok reminders and context (not carried)")
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
if (!text.trim()) break
|
|
195
|
+
const at = typeof r.prompt_index === "number" ? log.promptAt.get(r.prompt_index) : undefined
|
|
196
|
+
turns.push({ kind: "user", ...(at ? { at } : {}), text })
|
|
197
|
+
break
|
|
198
|
+
}
|
|
199
|
+
case "reasoning": {
|
|
200
|
+
const summaryText = (Array.isArray(r.summary) ? (r.summary as Row[]) : []).map((s) => s?.text).filter((t): t is string => typeof t === "string").join("\n")
|
|
201
|
+
if (summaryText.trim()) turns.push({ kind: "reasoning", text: summaryText })
|
|
202
|
+
if (r.encrypted_content) lost.add("encrypted reasoning")
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
case "assistant": {
|
|
206
|
+
const model = typeof r.model_id === "string" ? r.model_id : undefined
|
|
207
|
+
noteModel(h, model)
|
|
208
|
+
const text = typeof r.content === "string" ? r.content : textOf(r.content).text
|
|
209
|
+
if (text.trim()) turns.push({ kind: "assistant", text, ...(model ? { model } : {}) })
|
|
210
|
+
for (const c of Array.isArray(r.tool_calls) ? (r.tool_calls as Row[]) : []) {
|
|
211
|
+
if (typeof c?.id !== "string" || typeof c?.name !== "string") continue
|
|
212
|
+
let input: unknown = c.arguments
|
|
213
|
+
if (typeof c.arguments === "string") {
|
|
214
|
+
try {
|
|
215
|
+
input = JSON.parse(c.arguments)
|
|
216
|
+
} catch {
|
|
217
|
+
input = c.arguments
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
names.set(c.id, c.name)
|
|
221
|
+
const at = log.callAt.get(c.id)
|
|
222
|
+
turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: c.id, name: c.name, toolKind: grokKind(c.name) ?? toolKindOf(c.name), input })
|
|
223
|
+
}
|
|
224
|
+
break
|
|
225
|
+
}
|
|
226
|
+
case "tool_result": {
|
|
227
|
+
if (typeof r.tool_call_id !== "string") break
|
|
228
|
+
const raw = typeof r.content === "string" ? r.content : textOf(r.content).text
|
|
229
|
+
const { text, truncated } = clip(raw)
|
|
230
|
+
const at = log.resultAt.get(r.tool_call_id)
|
|
231
|
+
turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId: r.tool_call_id, ok: !log.failed.has(r.tool_call_id), output: text, ...(truncated ? { truncated } : {}) })
|
|
232
|
+
if (names.get(r.tool_call_id) === "spawn_subagent") spawnResults.push({ callId: r.tool_call_id, text: raw })
|
|
233
|
+
break
|
|
234
|
+
}
|
|
235
|
+
default:
|
|
236
|
+
lost.add(`unknown chat history types: ${String(r.type)}`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
lost.flush(h)
|
|
240
|
+
return { session: { header: h, turns }, spawnResults }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Subagent sessions named in the parent's subagents/ folder, each placed after the spawn call whose result names it. */
|
|
244
|
+
function attachChildren(dir: string, parent: Session, spawnResults: Array<{ callId: string; text: string }>) {
|
|
245
|
+
const sub = join(dir, "subagents")
|
|
246
|
+
const kids: Array<{ session: Session; callId?: string; description?: string }> = []
|
|
247
|
+
for (const childId of ls(sub)) {
|
|
248
|
+
const meta = readJson<Row>(join(sub, childId, "meta.json"))
|
|
249
|
+
const childDir = join(dirname(dir), childId)
|
|
250
|
+
if (!meta || !isDir(childDir)) continue
|
|
251
|
+
try {
|
|
252
|
+
const child = readOne(childDir).session
|
|
253
|
+
child.header.parent = { id: parent.header.id, kind: "subagent" }
|
|
254
|
+
const callId = spawnResults.find((r) => r.text.includes(childId))?.callId
|
|
255
|
+
kids.push({ session: child, ...(callId ? { callId } : {}), ...(typeof meta.description === "string" ? { description: meta.description } : {}) })
|
|
256
|
+
} catch {
|
|
257
|
+
parent.header.lost.push("an unreadable subagent session")
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (!kids.length) return
|
|
261
|
+
const out: Turn[] = []
|
|
262
|
+
const unplaced = kids.filter((k) => !k.callId)
|
|
263
|
+
for (const t of parent.turns) {
|
|
264
|
+
out.push(t)
|
|
265
|
+
if (t.kind !== "tool_result") continue
|
|
266
|
+
for (const k of kids.filter((k) => k.callId === t.callId))
|
|
267
|
+
out.push({ kind: "subagent", ...(k.session.header.startedAt ? { at: k.session.header.startedAt } : {}), sessionId: k.session.header.id, callId: t.callId, ...(k.description ? { description: k.description } : {}) })
|
|
268
|
+
}
|
|
269
|
+
for (const k of unplaced) out.push({ kind: "subagent", ...(k.session.header.startedAt ? { at: k.session.header.startedAt } : {}), sessionId: k.session.header.id, ...(k.description ? { description: k.description } : {}) })
|
|
270
|
+
parent.turns = out
|
|
271
|
+
parent.children = kids.map((k) => k.session)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export const grokReader: Reader = {
|
|
275
|
+
tool: TOOL,
|
|
276
|
+
label: LABEL,
|
|
277
|
+
|
|
278
|
+
async list(home: string): Promise<SessionRef[]> {
|
|
279
|
+
const refs: SessionRef[] = []
|
|
280
|
+
const root = sessionsDir(home)
|
|
281
|
+
for (const group of ls(root)) {
|
|
282
|
+
const groupDir = join(root, group)
|
|
283
|
+
if (!isDir(groupDir)) continue
|
|
284
|
+
for (const name of ls(groupDir)) {
|
|
285
|
+
const dir = join(groupDir, name)
|
|
286
|
+
const s = readJson<Summary>(join(dir, "summary.json"))
|
|
287
|
+
// Subagent sessions are read as part of their parent.
|
|
288
|
+
if (!s?.info?.id || s.session_kind === "subagent" || !(exists(join(dir, "chat_history.jsonl")) || exists(join(dir, "updates.jsonl")))) continue
|
|
289
|
+
const title = s.generated_title || s.session_summary
|
|
290
|
+
const updatedAt = iso(s.last_active_at ?? s.updated_at)
|
|
291
|
+
refs.push({ tool: TOOL, sourceId: s.info.id, path: dir, ...(title ? { title } : {}), ...(s.info.cwd ? { cwd: s.info.cwd } : {}), ...(updatedAt ? { updatedAt } : {}) })
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
async read(ref: SessionRef): Promise<Session> {
|
|
298
|
+
const { session, spawnResults } = readOne(ref.path)
|
|
299
|
+
attachChildren(ref.path, session, spawnResults)
|
|
300
|
+
return session
|
|
301
|
+
},
|
|
302
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { exists, versionOf } from "../../fsutil.ts"
|
|
4
|
+
import type { ToolKind } from "../../session/types.ts"
|
|
5
|
+
|
|
6
|
+
// What the xAI Grok CLI ("Grok Build", the official closed-source binary) keeps per session, as far as
|
|
7
|
+
// this adapter relies on it. Checked against grok 0.2.103: the sessions it wrote on the author's Mac
|
|
8
|
+
// (structure and counts only), sessions it created itself in a throwaway GROK_HOME, and what its own
|
|
9
|
+
// `grok sessions list`, `grok export` and ACP `session/load` + `_x.ai/session/info` do with written ones.
|
|
10
|
+
//
|
|
11
|
+
// A session is a folder <GROK_HOME>/sessions/<url-encoded cwd>/<uuidv7>/ holding two parallel logs:
|
|
12
|
+
// updates.jsonl, the ACP session/update stream the screen is rebuilt from (export and the resume replay
|
|
13
|
+
// read only this), and chat_history.jsonl, the messages the model is given (session/load rebuilds the
|
|
14
|
+
// model context only from this; a session without it resumes with an empty context). summary.json is the
|
|
15
|
+
// entry the session list reads. The search index (sessions/session_search.sqlite) is rebuilt by Grok.
|
|
16
|
+
|
|
17
|
+
export const TOOL = "grok-cli"
|
|
18
|
+
export const LABEL = "Grok CLI"
|
|
19
|
+
|
|
20
|
+
/** Grok versions the writer's files were checked against. */
|
|
21
|
+
export const TESTED_VERSIONS = ["0.2.103"]
|
|
22
|
+
|
|
23
|
+
export const grokHome = (home: string) => join(home, ".grok")
|
|
24
|
+
export const sessionsDir = (home: string) => join(grokHome(home), "sessions")
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The folder name Grok gives a cwd: percent-encoding of everything outside RFC 3986's unreserved set
|
|
28
|
+
* (checked with spaces, @, +, comma, parentheses and ~ in the path). A cwd is not resolved first: /tmp
|
|
29
|
+
* and /private/tmp are different folders.
|
|
30
|
+
*/
|
|
31
|
+
export function encodeCwd(cwd: string): string {
|
|
32
|
+
return encodeURIComponent(cwd).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Past 255 bytes Grok names the folder slug-plus-hash instead, with a hash this adapter does not reproduce. */
|
|
36
|
+
export const MAX_FOLDER_NAME = 255
|
|
37
|
+
|
|
38
|
+
/** Messages Grok adds for the model and marks as not typed by the person (all wrapped in <system-reminder>). */
|
|
39
|
+
export const SYNTHETIC = "synthetic_reason"
|
|
40
|
+
|
|
41
|
+
/** The installed Grok version ("grok 0.2.103 (89c3d36fb6f1)" → "0.2.103"), from the PATH or Grok's own bin folder. */
|
|
42
|
+
export function grokVersion(home: string): string | undefined {
|
|
43
|
+
const own = join(grokHome(home), "bin", "grok")
|
|
44
|
+
const line = versionOf("grok") ?? (exists(own) ? versionOf(own) : undefined)
|
|
45
|
+
return line?.match(/(\d+\.\d+\.\d+)/)?.[1]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The model new sessions start with: [models] default in config.toml, else Grok's documented default. */
|
|
49
|
+
export function defaultModel(home: string): string {
|
|
50
|
+
let text = ""
|
|
51
|
+
try {
|
|
52
|
+
text = readFileSync(join(grokHome(home), "config.toml"), "utf8")
|
|
53
|
+
} catch {
|
|
54
|
+
return "grok-build"
|
|
55
|
+
}
|
|
56
|
+
const section = text.split(/^\s*\[/m).find((s) => /^models\]/.test(s)) ?? ""
|
|
57
|
+
return section.match(/^\s*default\s*=\s*["']([^"']+)["']/m)?.[1] ?? "grok-build"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Grok's built-in tools, by what they do, with the metadata its own tool_call updates carry. */
|
|
61
|
+
export interface GrokTool {
|
|
62
|
+
name: string
|
|
63
|
+
/** `x.ai/tool.kind` */
|
|
64
|
+
kind: string
|
|
65
|
+
label: string
|
|
66
|
+
namespace: string
|
|
67
|
+
readOnly: boolean
|
|
68
|
+
/** ACP ToolKind for the update's `kind`. */
|
|
69
|
+
acp: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const GROK_TOOLS: Record<string, GrokTool> = {
|
|
73
|
+
read_file: { name: "read_file", kind: "read", label: "Read", namespace: "grok_build", readOnly: true, acp: "read" },
|
|
74
|
+
run_terminal_command: { name: "run_terminal_command", kind: "execute", label: "Run Command", namespace: "grok_build", readOnly: false, acp: "execute" },
|
|
75
|
+
search_replace: { name: "search_replace", kind: "edit", label: "Edit", namespace: "grok_build", readOnly: false, acp: "edit" },
|
|
76
|
+
write: { name: "write", kind: "write", label: "Write", namespace: "opencode", readOnly: false, acp: "edit" },
|
|
77
|
+
list_dir: { name: "list_dir", kind: "list", label: "List Files", namespace: "grok_build", readOnly: true, acp: "search" },
|
|
78
|
+
grep: { name: "grep", kind: "search", label: "Search", namespace: "grok_build", readOnly: true, acp: "search" },
|
|
79
|
+
web_search: { name: "web_search", kind: "web_search", label: "Web Search", namespace: "grok_build", readOnly: true, acp: "fetch" },
|
|
80
|
+
todo_write: { name: "todo_write", kind: "plan", label: "Plan", namespace: "grok_build", readOnly: false, acp: "other" },
|
|
81
|
+
spawn_subagent: { name: "spawn_subagent", kind: "task", label: "Subagent", namespace: "grok_build", readOnly: false, acp: "other" },
|
|
82
|
+
ask_user_question: { name: "ask_user_question", kind: "ask_user", label: "Ask User", namespace: "grok_build", readOnly: true, acp: "other" },
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** What a Grok tool name does; Grok's names are checked before the shared name patterns. */
|
|
86
|
+
const KIND_OF: Record<string, ToolKind> = {
|
|
87
|
+
read_file: "read",
|
|
88
|
+
run_terminal_command: "shell",
|
|
89
|
+
get_command_or_subagent_output: "shell",
|
|
90
|
+
search_replace: "edit",
|
|
91
|
+
write: "write",
|
|
92
|
+
list_dir: "list",
|
|
93
|
+
grep: "search",
|
|
94
|
+
search_tool: "search",
|
|
95
|
+
web_search: "web",
|
|
96
|
+
todo_write: "todo",
|
|
97
|
+
spawn_subagent: "task",
|
|
98
|
+
ask_user_question: "ask",
|
|
99
|
+
use_tool: "mcp",
|
|
100
|
+
}
|
|
101
|
+
export const grokKind = (name: string): ToolKind | undefined => KIND_OF[name]
|
|
102
|
+
|
|
103
|
+
const pick = (o: Record<string, unknown>, keys: string[]): unknown => {
|
|
104
|
+
for (const k of keys) if (o[k] !== undefined && o[k] !== null && o[k] !== "") return o[k]
|
|
105
|
+
return undefined
|
|
106
|
+
}
|
|
107
|
+
const str = (v: unknown) => (typeof v === "string" ? v : Array.isArray(v) && v.every((x) => typeof x === "string") ? v.join(" ") : undefined)
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A call from another tool as the Grok tool that does the same thing, with Grok's argument names, so the
|
|
111
|
+
* model sees calls it knows and the screen renders them like its own. Calls whose arguments do not map
|
|
112
|
+
* cleanly (an apply_patch, a filename glob, an MCP call) keep their own name and input.
|
|
113
|
+
*/
|
|
114
|
+
export function asGrokCall(kind: ToolKind, name: string, input: unknown): { tool?: GrokTool; name: string; input: unknown } {
|
|
115
|
+
if (GROK_TOOLS[name]) return { tool: GROK_TOOLS[name], name, input }
|
|
116
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return { name, input }
|
|
117
|
+
const o = input as Record<string, unknown>
|
|
118
|
+
const path = str(pick(o, ["file_path", "target_file", "path", "filePath", "absolute_path", "filename"]))
|
|
119
|
+
const as = (tool: string, args: Record<string, unknown>) => ({ tool: GROK_TOOLS[tool], name: tool, input: args })
|
|
120
|
+
switch (kind) {
|
|
121
|
+
case "read":
|
|
122
|
+
if (path) return as("read_file", { target_file: path, ...(typeof o.offset === "number" ? { offset: o.offset } : {}), ...(typeof o.limit === "number" ? { limit: o.limit } : {}) })
|
|
123
|
+
break
|
|
124
|
+
case "shell": {
|
|
125
|
+
const command = str(pick(o, ["command", "cmd", "commandLine"]))
|
|
126
|
+
if (command) return as("run_terminal_command", { command, ...(typeof o.description === "string" ? { description: o.description } : {}) })
|
|
127
|
+
break
|
|
128
|
+
}
|
|
129
|
+
case "edit": {
|
|
130
|
+
const oldS = str(pick(o, ["old_string", "old_str", "oldText", "old_text"]))
|
|
131
|
+
const newS = pick(o, ["new_string", "new_str", "newText", "new_text"])
|
|
132
|
+
if (path && oldS !== undefined && typeof newS === "string")
|
|
133
|
+
return as("search_replace", { file_path: path, old_string: oldS, new_string: newS, ...(o.replace_all === true ? { replace_all: true } : {}) })
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
case "write": {
|
|
137
|
+
const content = pick(o, ["content", "contents", "file_text", "fileText", "text"])
|
|
138
|
+
if (path && typeof content === "string") return as("write", { file_path: path, content })
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
case "list": {
|
|
142
|
+
const dir = str(pick(o, ["target_directory", "path", "dir_path", "directory", "relative_workspace_path"]))
|
|
143
|
+
if (dir) return as("list_dir", { target_directory: dir })
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
case "search": {
|
|
147
|
+
// Only content searches: a filename glob has a pattern too, but grep would read it as a regex.
|
|
148
|
+
const pattern = str(pick(o, ["pattern", "regex", "query"]))
|
|
149
|
+
if (/grep|ripgrep|search_files/i.test(name) && pattern) return as("grep", { pattern, ...(path ? { path } : {}), ...(typeof o.glob === "string" ? { glob: o.glob } : {}) })
|
|
150
|
+
break
|
|
151
|
+
}
|
|
152
|
+
case "web": {
|
|
153
|
+
const query = str(pick(o, ["query", "q"]))
|
|
154
|
+
if (/search/i.test(name) && query) return as("web_search", { query })
|
|
155
|
+
break
|
|
156
|
+
}
|
|
157
|
+
default:
|
|
158
|
+
break
|
|
159
|
+
}
|
|
160
|
+
return { name, input }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The note the writer puts first; the reader recognises it and records where the session came from. */
|
|
164
|
+
export const continuedNote = (label: string, id: string, when: string) =>
|
|
165
|
+
`This session continues ${label} session ${id}, carried over by brain session on ${when}. The conversation after this note is its history; reasoning and anything listed as lost did not carry over. Check files before relying on details, since they may have changed.`
|
|
166
|
+
export const CONTINUED_NOTE = /This session continues .+? session (\S+), carried over by brain session/
|
|
167
|
+
|
|
168
|
+
/** The start of a compaction summary the writer gives the model in place of the turns it replaced. */
|
|
169
|
+
export const SUMMARY_PREFIX = "[Summary of the earlier conversation"
|
|
170
|
+
|
|
171
|
+
export const reminder = (text: string) => `<system-reminder>\n${text}\n</system-reminder>`
|
|
172
|
+
export const unwrapReminder = (text: string) => text.replace(/^\s*<system-reminder>\n?/, "").replace(/\n?<\/system-reminder>\s*$/, "")
|