@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,177 @@
|
|
|
1
|
+
// Cline's original tool protocol (Cline 3.x, Roo Code before "native" tools): the model writes each tool
|
|
2
|
+
// call as XML inside its text, `<read_file><path>src/a.ts</path></read_file>`, and the extension answers in
|
|
3
|
+
// the next user message with a text block headed `[read_file for 'src/a.ts'] Result:`. Neither side has an
|
|
4
|
+
// id, so a result is paired with a call in the assistant message just before it. `<thinking>` blocks in the
|
|
5
|
+
// same text are the model's readable reasoning.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tool names of the XML protocol in Cline and Roo Code, from the tool enums in the installed bundles (Cline
|
|
9
|
+
* 3.56.2 and 4.1.26, Roo 3.54.0) plus names older versions used. A tag is read as a call only when it is one
|
|
10
|
+
* of these, or when the next message carries a result for it, so the HTML in a file the model wrote stays text.
|
|
11
|
+
*/
|
|
12
|
+
export const XML_TOOLS = new Set([
|
|
13
|
+
// Cline
|
|
14
|
+
"execute_command",
|
|
15
|
+
"read_file",
|
|
16
|
+
"write_to_file",
|
|
17
|
+
"replace_in_file",
|
|
18
|
+
"search_files",
|
|
19
|
+
"list_files",
|
|
20
|
+
"list_code_definition_names",
|
|
21
|
+
"browser_action",
|
|
22
|
+
"use_mcp_tool",
|
|
23
|
+
"access_mcp_resource",
|
|
24
|
+
"load_mcp_documentation",
|
|
25
|
+
"ask_followup_question",
|
|
26
|
+
"attempt_completion",
|
|
27
|
+
"plan_mode_respond",
|
|
28
|
+
"act_mode_respond",
|
|
29
|
+
"new_task",
|
|
30
|
+
"focus_chain",
|
|
31
|
+
"web_fetch",
|
|
32
|
+
"web_search",
|
|
33
|
+
"condense",
|
|
34
|
+
"summarize_task",
|
|
35
|
+
"report_bug",
|
|
36
|
+
"new_rule",
|
|
37
|
+
"apply_patch",
|
|
38
|
+
"generate_explanation",
|
|
39
|
+
"use_skill",
|
|
40
|
+
// Roo Code
|
|
41
|
+
"read_command_output",
|
|
42
|
+
"apply_diff",
|
|
43
|
+
"edit",
|
|
44
|
+
"search_and_replace",
|
|
45
|
+
"search_replace",
|
|
46
|
+
"edit_file",
|
|
47
|
+
"insert_content",
|
|
48
|
+
"codebase_search",
|
|
49
|
+
"switch_mode",
|
|
50
|
+
"update_todo_list",
|
|
51
|
+
"run_slash_command",
|
|
52
|
+
"skill",
|
|
53
|
+
"generate_image",
|
|
54
|
+
"custom_tool",
|
|
55
|
+
"fetch_instructions",
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
/** Parameters whose value can itself contain tags (file contents, diffs, a final answer): closed at the last close tag. */
|
|
59
|
+
const LONG_PARAMS = new Set(["content", "diff", "result", "response", "question", "arguments", "todos"])
|
|
60
|
+
|
|
61
|
+
export type Piece =
|
|
62
|
+
| { kind: "text"; text: string }
|
|
63
|
+
| { kind: "thinking"; text: string }
|
|
64
|
+
| { kind: "call"; name: string; input: Record<string, unknown> }
|
|
65
|
+
|
|
66
|
+
const OPEN_TAG = /<([A-Za-z_][A-Za-z0-9_]*)>/g
|
|
67
|
+
|
|
68
|
+
/** Assistant text split into plain text, `<thinking>` reasoning and XML tool calls, in order. */
|
|
69
|
+
export function splitAssistantText(text: string, isTool: (name: string) => boolean): Piece[] {
|
|
70
|
+
const out: Piece[] = []
|
|
71
|
+
const pushText = (t: string) => {
|
|
72
|
+
// A </thinking> left after a call that ended the reasoning early (below) is not text.
|
|
73
|
+
const s = t.replace(/<\/thinking>/g, "").trim()
|
|
74
|
+
if (s) out.push({ kind: "text", text: s })
|
|
75
|
+
}
|
|
76
|
+
let pos = 0
|
|
77
|
+
const re = new RegExp(OPEN_TAG.source, "g")
|
|
78
|
+
let m: RegExpExecArray | null
|
|
79
|
+
while ((m = re.exec(text))) {
|
|
80
|
+
const name = m[1]!
|
|
81
|
+
if (name !== "thinking" && !isTool(name)) continue
|
|
82
|
+
const bodyStart = m.index + m[0].length
|
|
83
|
+
const close = `</${name}>`
|
|
84
|
+
let end = text.indexOf(close, bodyStart)
|
|
85
|
+
// An unclosed tag is a call the model was still writing when the stream stopped: take the rest.
|
|
86
|
+
let bodyEnd = end < 0 ? text.length : end
|
|
87
|
+
// The extension parses calls wherever they are, <thinking> or not, and some models never close <thinking>
|
|
88
|
+
// and go straight on to the call: reasoning ends at a call inside it (a tool tag closed within it, or any
|
|
89
|
+
// tool tag when it is never closed), and the call is read next.
|
|
90
|
+
const call = name === "thinking" ? callInside(text, bodyStart, end, isTool) : -1
|
|
91
|
+
if (call >= 0) end = bodyEnd = call
|
|
92
|
+
pushText(text.slice(pos, m.index))
|
|
93
|
+
const body = text.slice(bodyStart, bodyEnd)
|
|
94
|
+
if (name === "thinking") {
|
|
95
|
+
if (body.trim()) out.push({ kind: "thinking", text: body.trim() })
|
|
96
|
+
} else out.push({ kind: "call", name, input: parseParams(body) })
|
|
97
|
+
pos = call >= 0 ? call : end < 0 ? text.length : end + close.length
|
|
98
|
+
re.lastIndex = pos
|
|
99
|
+
}
|
|
100
|
+
pushText(text.slice(pos))
|
|
101
|
+
return out
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Where a tool call starts inside a <thinking> block that runs from `from` to `end` (-1: never closed), or -1. */
|
|
105
|
+
function callInside(text: string, from: number, end: number, isTool: (name: string) => boolean): number {
|
|
106
|
+
const limit = end < 0 ? text.length : end
|
|
107
|
+
const re = new RegExp(OPEN_TAG.source, "g")
|
|
108
|
+
re.lastIndex = from
|
|
109
|
+
let m: RegExpExecArray | null
|
|
110
|
+
while ((m = re.exec(text)) && m.index < limit) {
|
|
111
|
+
const name = m[1]!
|
|
112
|
+
if (name === "thinking" || !isTool(name)) continue
|
|
113
|
+
// In a closed block, a tag the model only mentioned has no close tag of its own before the block ends.
|
|
114
|
+
if (end < 0) return m.index
|
|
115
|
+
const close = text.indexOf(`</${name}>`, m.index + m[0].length)
|
|
116
|
+
if (close >= 0 && close < end) return m.index
|
|
117
|
+
}
|
|
118
|
+
return -1
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `<path>a</path><content>b</content>` as { path: "a", content: "b" }; repeated tags become arrays, nested tags objects. */
|
|
122
|
+
export function parseParams(body: string): Record<string, unknown> {
|
|
123
|
+
const out: Record<string, unknown> = {}
|
|
124
|
+
const re = new RegExp(OPEN_TAG.source, "g")
|
|
125
|
+
let m: RegExpExecArray | null
|
|
126
|
+
while ((m = re.exec(body))) {
|
|
127
|
+
const name = m[1]!
|
|
128
|
+
const start = m.index + m[0].length
|
|
129
|
+
const close = `</${name}>`
|
|
130
|
+
const end = LONG_PARAMS.has(name) ? body.lastIndexOf(close) : body.indexOf(close, start)
|
|
131
|
+
const raw = end < start ? body.slice(start) : body.slice(start, end)
|
|
132
|
+
const value = paramValue(name, raw)
|
|
133
|
+
const prev = out[name]
|
|
134
|
+
out[name] = prev === undefined ? value : Array.isArray(prev) ? [...prev, value] : [prev, value]
|
|
135
|
+
if (end < start) break
|
|
136
|
+
re.lastIndex = end + close.length
|
|
137
|
+
}
|
|
138
|
+
return out
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function paramValue(name: string, raw: string): unknown {
|
|
142
|
+
// Cline keeps file contents and diffs as written, less the newline after the open tag and before the close.
|
|
143
|
+
if (name === "content" || name === "diff") return raw.replace(/^\r?\n/, "").replace(/\r?\n$/, "")
|
|
144
|
+
const t = raw.trim()
|
|
145
|
+
// Roo's read_file nests its arguments: <args><file><path>a</path></file></args>.
|
|
146
|
+
if (!LONG_PARAMS.has(name) && t.startsWith("<") && t.endsWith(">")) {
|
|
147
|
+
const nested = parseParams(t)
|
|
148
|
+
if (Object.keys(nested).length) return nested
|
|
149
|
+
}
|
|
150
|
+
return t
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* A result header, `[read_file for 'src/a.ts'] Result:`: the tool name and the text after the header. The
|
|
155
|
+
* quoted part is the call's main argument as written, so it can run over several lines (a multi-line
|
|
156
|
+
* ask_followup_question): that form is tried when the header does not close on its first line.
|
|
157
|
+
*/
|
|
158
|
+
export function resultHeader(text: string): { label: string; name: string; rest: string } | undefined {
|
|
159
|
+
const m = text.match(/^\s*\[([^\n]*?)\] Result:[^\S\n]*\n?/) ?? text.match(/^\s*\[([A-Za-z0-9_]+ for '[\s\S]{0,4000}?'[^\n\]]*)\] Result:[^\S\n]*\n?/)
|
|
160
|
+
if (!m) return undefined
|
|
161
|
+
const label = m[1]!
|
|
162
|
+
return { label, name: label.match(/^[A-Za-z0-9_]+/)?.[0] ?? label, rest: text.slice(m[0].length) }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The tool named by a notice that a call never ran: the extension runs one XML call per message, or none after
|
|
167
|
+
* the person rejects one, and answers each call it skipped with a text block of its own, e.g. `Tool [x] was not
|
|
168
|
+
* executed because a tool has already been used in this message.`, Roo's `Skipping tool [x for 'a'] due to user
|
|
169
|
+
* rejecting a previous tool.`, Cline's `Skipping tool due to user rejecting a previous tool. [x for 'a']`.
|
|
170
|
+
*/
|
|
171
|
+
export function skipNotice(text: string): string | undefined {
|
|
172
|
+
const t = text.trimStart()
|
|
173
|
+
return (
|
|
174
|
+
t.match(/^(?:Tool|Skipping tool) \[([A-Za-z0-9_]+)[\s\S]{0,4000}?\] (?:was not executed|was interrupted and not executed|due to user rejecting)/)?.[1] ??
|
|
175
|
+
t.match(/^(?:Skipping tool|Tool was interrupted and not executed) due to user rejecting a previous tool\.\s*\[([A-Za-z0-9_]+)/)?.[1]
|
|
176
|
+
)
|
|
177
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { cline4Reader } from "./reader.ts"
|
|
3
|
+
import { cline4NativeWriter } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// The Cline 4.x SDK runtime (the `cline` CLI 3.x, Cline Desktop, the 4.x extension): reads its sessions from
|
|
6
|
+
// ~/.cline/data, and continues any session as a new one there (written only while Cline is closed, after a
|
|
7
|
+
// backup of sessions.db). Cline's classic task store is the `cline` adapter's.
|
|
8
|
+
export const readers: Reader[] = [cline4Reader]
|
|
9
|
+
export const writers: Writer[] = [cline4NativeWriter]
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { readdirSync } from "node:fs"
|
|
2
|
+
import { exists, gitInfo, readJson } from "../../fsutil.ts"
|
|
3
|
+
import type { Attachment, Session, Turn, Usage } from "../../session/types.ts"
|
|
4
|
+
import { clip, header, iso, lossCounter, noteModel } from "../../session/util.ts"
|
|
5
|
+
import { withSnapshot } from "../../sqlite.ts"
|
|
6
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
7
|
+
import { CONTINUED_NOTE, LABEL, NOTE_KIND, TOOL, clineToolKind, manifestPath, messagesPath, sessionsDb, sessionsDir } from "./records.ts"
|
|
8
|
+
|
|
9
|
+
// Cline 4.x SDK sessions (records.ts has the layout): the root sessions in sessions.db, newest first, or
|
|
10
|
+
// the manifests when there is no database. Subagent and teammate sessions belong to their root session and
|
|
11
|
+
// are not listed. Cline's classic task store (~/.cline/data/tasks, globalStorage) is the `cline` reader's.
|
|
12
|
+
|
|
13
|
+
type Json = Record<string, any>
|
|
14
|
+
|
|
15
|
+
/** What the person typed: Cline wraps it in <user_input mode="..."> and adds <mode_notice> elements. */
|
|
16
|
+
const displayText = (text: string) =>
|
|
17
|
+
text
|
|
18
|
+
.replace(/<mode_notice>[\s\S]*?<\/mode_notice>/g, "")
|
|
19
|
+
.replace(/<\/?(user_input|user_command)\b[^>]*>/g, "")
|
|
20
|
+
.trim()
|
|
21
|
+
|
|
22
|
+
const usageOf = (m: Json | undefined): Usage | undefined => {
|
|
23
|
+
if (!m || typeof m !== "object") return undefined
|
|
24
|
+
const out: Usage = {}
|
|
25
|
+
if (m.inputTokens) out.input = m.inputTokens
|
|
26
|
+
if (m.outputTokens) out.output = m.outputTokens
|
|
27
|
+
if (m.cacheReadTokens) out.cacheRead = m.cacheReadTokens
|
|
28
|
+
if (m.cacheWriteTokens) out.cacheWrite = m.cacheWriteTokens
|
|
29
|
+
return Object.keys(out).length ? out : undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const resultText = (content: unknown, images: () => void): string => {
|
|
33
|
+
if (typeof content === "string") return content
|
|
34
|
+
const parts: string[] = []
|
|
35
|
+
for (const b of Array.isArray(content) ? content : []) {
|
|
36
|
+
if (b?.type === "text" && typeof b.text === "string") parts.push(b.text)
|
|
37
|
+
else if (b?.type === "file" && typeof b.path === "string") parts.push(`[File: ${b.path}]`)
|
|
38
|
+
else images()
|
|
39
|
+
}
|
|
40
|
+
return parts.join("\n")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const cline4Reader: Reader = {
|
|
44
|
+
tool: TOOL,
|
|
45
|
+
label: LABEL,
|
|
46
|
+
async list(home) {
|
|
47
|
+
const db = sessionsDb(home)
|
|
48
|
+
const rows: Json[] = exists(db)
|
|
49
|
+
? withSnapshot(db, (d) =>
|
|
50
|
+
d
|
|
51
|
+
.query(
|
|
52
|
+
"SELECT session_id, cwd, metadata_json, prompt, started_at, ended_at, updated_at, messages_path FROM sessions WHERE is_subagent = 0 AND (parent_session_id IS NULL OR parent_session_id = '') ORDER BY started_at DESC",
|
|
53
|
+
)
|
|
54
|
+
.all() as Json[],
|
|
55
|
+
)
|
|
56
|
+
: manifestRows(home)
|
|
57
|
+
const refs: SessionRef[] = []
|
|
58
|
+
for (const r of rows) {
|
|
59
|
+
let meta: Json = {}
|
|
60
|
+
try {
|
|
61
|
+
meta = typeof r.metadata_json === "string" ? JSON.parse(r.metadata_json) : (r.metadata_json ?? {})
|
|
62
|
+
} catch {}
|
|
63
|
+
const title = typeof meta.title === "string" && meta.title ? meta.title : typeof r.prompt === "string" ? r.prompt.split("\n")[0]?.slice(0, 80) : undefined
|
|
64
|
+
const updated = iso(r.ended_at) ?? iso(r.updated_at) ?? iso(r.started_at)
|
|
65
|
+
refs.push({
|
|
66
|
+
tool: TOOL,
|
|
67
|
+
sourceId: String(r.session_id),
|
|
68
|
+
path: typeof r.messages_path === "string" && r.messages_path ? r.messages_path : messagesPath(home, String(r.session_id)),
|
|
69
|
+
...(title ? { title } : {}),
|
|
70
|
+
...(typeof r.cwd === "string" && r.cwd ? { cwd: r.cwd } : {}),
|
|
71
|
+
...(updated ? { updatedAt: updated } : {}),
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
return refs
|
|
75
|
+
},
|
|
76
|
+
async read(ref, home) {
|
|
77
|
+
const path = exists(ref.path) ? ref.path : messagesPath(home, ref.sourceId)
|
|
78
|
+
const file = readJson<Json>(path)
|
|
79
|
+
if (!file || !Array.isArray(file.messages)) throw new Error(`${path} is not a Cline messages file`)
|
|
80
|
+
const manifest = readJson<Json>(manifestPath(home, ref.sourceId)) ?? {}
|
|
81
|
+
return parseMessages(file, manifest, { id: ref.sourceId, path, home })
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Manifests stand in for the database's rows when Cline has not created sessions.db. */
|
|
86
|
+
function manifestRows(home: string): Json[] {
|
|
87
|
+
let names: string[]
|
|
88
|
+
try {
|
|
89
|
+
names = readdirSync(sessionsDir(home))
|
|
90
|
+
} catch {
|
|
91
|
+
return []
|
|
92
|
+
}
|
|
93
|
+
const rows: Json[] = []
|
|
94
|
+
for (const id of names) {
|
|
95
|
+
const m = readJson<Json>(manifestPath(home, id))
|
|
96
|
+
if (m?.version !== 1 || m.session_id !== id) continue
|
|
97
|
+
rows.push({ session_id: id, cwd: m.cwd, metadata_json: m.metadata, prompt: m.prompt, started_at: m.started_at, ended_at: m.ended_at, messages_path: m.messages_path })
|
|
98
|
+
}
|
|
99
|
+
return rows.sort((a, b) => String(b.started_at).localeCompare(String(a.started_at)))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A messages file (with its manifest) as a portable session. */
|
|
103
|
+
export function parseMessages(file: Json, manifest: Json, ctx: { id: string; path: string; home: string }): Session {
|
|
104
|
+
const cwd = typeof manifest.cwd === "string" && manifest.cwd ? manifest.cwd : undefined
|
|
105
|
+
const title = typeof manifest.metadata?.title === "string" ? manifest.metadata.title : undefined
|
|
106
|
+
const h = header(TOOL, ctx.id, {
|
|
107
|
+
sourcePath: ctx.path,
|
|
108
|
+
...(title ? { title } : {}),
|
|
109
|
+
project: cwd ? { cwd, ...gitInfo(cwd, ctx.home) } : {},
|
|
110
|
+
...(iso(manifest.started_at) ? { startedAt: iso(manifest.started_at) } : {}),
|
|
111
|
+
...(iso(file.updated_at) ?? iso(manifest.ended_at) ? { updatedAt: iso(file.updated_at) ?? iso(manifest.ended_at) } : {}),
|
|
112
|
+
})
|
|
113
|
+
const lost = lossCounter()
|
|
114
|
+
const turns: Turn[] = []
|
|
115
|
+
|
|
116
|
+
for (const m of file.messages as Json[]) {
|
|
117
|
+
if (!m || (m.role !== "user" && m.role !== "assistant")) continue
|
|
118
|
+
const meta = (m.metadata ?? {}) as Json
|
|
119
|
+
const at = typeof m.ts === "number" ? iso(m.ts) : undefined
|
|
120
|
+
const when = at ? { at } : {}
|
|
121
|
+
const blocks: Json[] = typeof m.content === "string" ? [{ type: "text", text: m.content }] : Array.isArray(m.content) ? m.content : []
|
|
122
|
+
if (meta.displayOnly === true) continue // shown in the panel, never sent to the model
|
|
123
|
+
if (meta.kind === "compaction_summary" || meta.kind === "compaction") {
|
|
124
|
+
const summary = typeof meta.summary === "string" && meta.summary ? meta.summary : blocks.map((b) => (b?.type === "text" ? b.text : "")).join("\n")
|
|
125
|
+
if (summary.trim()) turns.push({ kind: "compaction", ...when, summary })
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (meta.kind === NOTE_KIND) {
|
|
129
|
+
const continued = blocks.map((b) => (b?.type === "text" ? String(b.text) : "")).join("\n").match(CONTINUED_NOTE)
|
|
130
|
+
if (continued?.[1] && !h.parent) h.parent = { id: continued[1], kind: "continued" }
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
if (m.role === "user") {
|
|
134
|
+
if (meta.displayRole === "system" || meta.displayRole === "status") {
|
|
135
|
+
lost.add("runtime notices")
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
const parts: string[] = []
|
|
139
|
+
const attachments: Attachment[] = []
|
|
140
|
+
for (const b of blocks) {
|
|
141
|
+
if (b?.type === "tool_result") {
|
|
142
|
+
let images = 0
|
|
143
|
+
const out = clip(resultText(b.content, () => images++))
|
|
144
|
+
if (images) lost.add("images in tool results", images)
|
|
145
|
+
turns.push({ kind: "tool_result", ...when, callId: String(b.tool_use_id), ok: b.is_error !== true, output: out.text, ...(out.truncated ? { truncated: true } : {}) })
|
|
146
|
+
} else if (b?.type === "text" && typeof b.text === "string") {
|
|
147
|
+
const t = displayText(b.text)
|
|
148
|
+
if (t) parts.push(t)
|
|
149
|
+
} else if (b?.type === "file" && typeof b.path === "string") attachments.push({ kind: "file", path: b.path })
|
|
150
|
+
else if (b?.type === "image" || b?.type === "media") lost.add("images")
|
|
151
|
+
else lost.add("unknown user content")
|
|
152
|
+
}
|
|
153
|
+
const text = parts.join("\n\n")
|
|
154
|
+
if (text || attachments.length) turns.push({ kind: "user", ...when, text, ...(attachments.length ? { attachments } : {}) })
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
const model = typeof m.modelInfo?.id === "string" && m.modelInfo.id ? m.modelInfo.id : undefined
|
|
158
|
+
noteModel(h, model)
|
|
159
|
+
const usage = usageOf(m.metrics)
|
|
160
|
+
let usageGiven = false
|
|
161
|
+
for (const b of blocks) {
|
|
162
|
+
if (b?.type === "text" && typeof b.text === "string") {
|
|
163
|
+
if (!b.text.trim()) continue
|
|
164
|
+
turns.push({ kind: "assistant", ...when, text: b.text, ...(model ? { model } : {}), ...(usage && !usageGiven ? { usage } : {}) })
|
|
165
|
+
usageGiven = true
|
|
166
|
+
} else if (b?.type === "thinking") {
|
|
167
|
+
if (typeof b.thinking === "string" && b.thinking.trim()) turns.push({ kind: "reasoning", ...when, text: b.thinking })
|
|
168
|
+
else if (b.signature) lost.add("signed reasoning")
|
|
169
|
+
} else if (b?.type === "redacted_thinking") lost.add("redacted reasoning")
|
|
170
|
+
else if (b?.type === "tool_use" && typeof b.id === "string") {
|
|
171
|
+
const name = String(b.name ?? "tool")
|
|
172
|
+
turns.push({ kind: "tool_call", ...when, id: b.id, name, toolKind: clineToolKind(name), input: b.input ?? {} })
|
|
173
|
+
} else if (b?.type === "image" || b?.type === "media") lost.add("generated media")
|
|
174
|
+
else lost.add("unknown assistant content")
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
lost.flush(h)
|
|
178
|
+
return { header: h, turns }
|
|
179
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import type { ToolKind } from "../../session/types.ts"
|
|
3
|
+
import { toolKindOf } from "../../session/util.ts"
|
|
4
|
+
|
|
5
|
+
// What the Cline 4.x SDK runtime (the `cline` CLI 3.x, Cline Desktop, the 4.x extension) stores, as far as
|
|
6
|
+
// this adapter relies on it. Checked against cline/cline at 94eecbb (2026-11-06, CLI 3.0.74):
|
|
7
|
+
// sdk/packages/shared/src/db/sqlite-db.ts (the sessions table), shared/src/storage/paths.ts (the folders),
|
|
8
|
+
// core/src/session/services/persistence-service.ts (createRootSessionWithArtifacts, persistSessionMessages),
|
|
9
|
+
// core/src/services/session-data.ts (the messages file), core/src/session/models/session-manifest.ts,
|
|
10
|
+
// core/src/runtime/host/history.ts (`cline history`), core/src/services/session-import/ (Cline's own
|
|
11
|
+
// Claude Code / Codex / OpenCode importer, whose output this writer matches), shared/src/llms/messages.ts
|
|
12
|
+
// and apps/cli/src/tui/utils/hydrate-messages.ts (what the CLI shows on resume). Catalogue entry 38.
|
|
13
|
+
//
|
|
14
|
+
// ~/.cline/data/db/sessions.db SQLite (WAL): one `sessions` row per session, with
|
|
15
|
+
// messages_path pointing at the messages file
|
|
16
|
+
// ~/.cline/data/sessions/<id>/<id>.json the manifest (version 1), what the row says
|
|
17
|
+
// ~/.cline/data/sessions/<id>/<id>.messages.json {version 1, updated_at, agent, sessionId, origin,
|
|
18
|
+
// messages: MessageWithMetadata[]}
|
|
19
|
+
//
|
|
20
|
+
// Messages are Anthropic-shaped: an assistant message holds text, thinking and tool_use blocks, and the
|
|
21
|
+
// user message after it holds the tool_result blocks. A user message whose metadata.displayRole is "system"
|
|
22
|
+
// goes to the model but is not shown (compaction summaries use it). Ids are `<ms>_<5 nanoid chars>`.
|
|
23
|
+
|
|
24
|
+
export const TOOL = "cline-sdk"
|
|
25
|
+
export const LABEL = "Cline (CLI, Desktop)"
|
|
26
|
+
|
|
27
|
+
/** Cline CLI releases whose store was checked in source; the writer was run against 3.0.74. */
|
|
28
|
+
export const TESTED_VERSIONS = ["3.0.74"]
|
|
29
|
+
|
|
30
|
+
export const dataDir = (home: string) => join(home, ".cline", "data")
|
|
31
|
+
export const sessionsDb = (home: string) => join(dataDir(home), "db", "sessions.db")
|
|
32
|
+
export const sessionsDir = (home: string) => join(dataDir(home), "sessions")
|
|
33
|
+
export const sessionDir = (home: string, id: string) => join(sessionsDir(home), id)
|
|
34
|
+
export const manifestPath = (home: string, id: string) => join(sessionDir(home, id), `${id}.json`)
|
|
35
|
+
export const messagesPath = (home: string, id: string) => join(sessionDir(home, id), `${id}.messages.json`)
|
|
36
|
+
|
|
37
|
+
/** The sessions table as Cline 3.0.74 creates it (SQLITE_STATEMENTS in sqlite-db.ts). */
|
|
38
|
+
export const SESSIONS_TABLE = `CREATE TABLE IF NOT EXISTS sessions (
|
|
39
|
+
session_id TEXT PRIMARY KEY,
|
|
40
|
+
source TEXT NOT NULL,
|
|
41
|
+
pid INTEGER NOT NULL,
|
|
42
|
+
started_at TEXT NOT NULL,
|
|
43
|
+
ended_at TEXT,
|
|
44
|
+
exit_code INTEGER,
|
|
45
|
+
status TEXT NOT NULL,
|
|
46
|
+
status_lock INTEGER NOT NULL DEFAULT 0,
|
|
47
|
+
interactive INTEGER NOT NULL,
|
|
48
|
+
provider TEXT NOT NULL,
|
|
49
|
+
model TEXT NOT NULL,
|
|
50
|
+
cwd TEXT NOT NULL,
|
|
51
|
+
workspace_root TEXT NOT NULL,
|
|
52
|
+
team_name TEXT,
|
|
53
|
+
enable_tools INTEGER NOT NULL,
|
|
54
|
+
enable_spawn INTEGER NOT NULL,
|
|
55
|
+
enable_teams INTEGER NOT NULL,
|
|
56
|
+
parent_session_id TEXT,
|
|
57
|
+
parent_agent_id TEXT,
|
|
58
|
+
agent_id TEXT,
|
|
59
|
+
conversation_id TEXT,
|
|
60
|
+
is_subagent INTEGER NOT NULL DEFAULT 0,
|
|
61
|
+
prompt TEXT,
|
|
62
|
+
metadata_json TEXT,
|
|
63
|
+
transcript_path TEXT NOT NULL DEFAULT '',
|
|
64
|
+
hook_path TEXT NOT NULL,
|
|
65
|
+
messages_path TEXT,
|
|
66
|
+
updated_at TEXT NOT NULL
|
|
67
|
+
);`
|
|
68
|
+
|
|
69
|
+
/** Process names of a running Cline that holds the store open: the CLI and its hub daemon, Cline Desktop. */
|
|
70
|
+
export const PROCESSES = ["cline", "Cline", "Cline Desktop"]
|
|
71
|
+
|
|
72
|
+
/** The metadata kind of the note saying where a session came from (a system-role message, kept off screen). */
|
|
73
|
+
export const NOTE_KIND = "brain_continued_session"
|
|
74
|
+
|
|
75
|
+
/** Cline's own tools (sdk/packages/core/src/extensions/tools), where the name alone does not say what they do. */
|
|
76
|
+
const KINDS: Record<string, ToolKind> = {
|
|
77
|
+
read_file: "read",
|
|
78
|
+
read_files: "read",
|
|
79
|
+
run_commands: "shell",
|
|
80
|
+
editor: "edit",
|
|
81
|
+
apply_patch: "edit",
|
|
82
|
+
search_codebase: "search",
|
|
83
|
+
fetch_web_content: "web",
|
|
84
|
+
ask_question: "ask",
|
|
85
|
+
skills: "other",
|
|
86
|
+
spawn_agent: "task",
|
|
87
|
+
}
|
|
88
|
+
export const clineToolKind = (name: string): ToolKind => KINDS[name] ?? (name.startsWith("team_") ? "task" : toolKindOf(name))
|
|
89
|
+
|
|
90
|
+
export const continuedNote = (label: string, id: string, when: string) =>
|
|
91
|
+
`This session continues ${label} session ${id}, carried over by brain session on ${when}. The messages that follow are its history; reasoning and anything listed as lost did not carry over. Check files before relying on details, since they may have changed.`
|
|
92
|
+
export const CONTINUED_NOTE = /^This session continues .+? session (\S+), carried over by brain session/
|