@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,432 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync } from "node:fs"
|
|
2
|
+
import { join } from "node:path"
|
|
3
|
+
import { appSupport, exists, gitInfo, isDir, ls, mtime, readJson } from "../../fsutil.ts"
|
|
4
|
+
import type { Attachment, Session, Turn } from "../../session/types.ts"
|
|
5
|
+
import { asText, clip, header, iso, lossCounter, noteModel, toolKindOf } from "../../session/util.ts"
|
|
6
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
7
|
+
import { CONTINUED_NOTE, HOSTS, SHOWN, SUMMARY_LEAD, extensionVersion } from "./records.ts"
|
|
8
|
+
import { XML_TOOLS, resultHeader, skipNotice, splitAssistantText } from "./xml.ts"
|
|
9
|
+
|
|
10
|
+
// Cline's classic task store, and Roo Code's, which forked from it (catalogue entries 37 and 39). Each
|
|
11
|
+
// VS Code-family app keeps its own copy under <app>/User/globalStorage/<extension id>/:
|
|
12
|
+
// tasks/<id>/api_conversation_history.json what the model saw: Anthropic-style {role, content} messages
|
|
13
|
+
// tasks/<id>/ui_messages.json what the panel showed, with timestamps
|
|
14
|
+
// tasks/<id>/task_metadata.json models used, files in context
|
|
15
|
+
// state/taskHistory.json (Cline), tasks/_index.json and tasks/<id>/history_item.json (Roo): the task list
|
|
16
|
+
// Tool calls are either native tool_use/tool_result blocks or XML in the text (see xml.ts).
|
|
17
|
+
|
|
18
|
+
interface Flavor {
|
|
19
|
+
tool: string
|
|
20
|
+
label: string
|
|
21
|
+
/** The extension's id, which names its globalStorage folder. */
|
|
22
|
+
extension: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One task's line in the extension's own task list. */
|
|
26
|
+
interface IndexEntry {
|
|
27
|
+
task?: string
|
|
28
|
+
ts?: number
|
|
29
|
+
cwd?: string
|
|
30
|
+
parentId?: string
|
|
31
|
+
native?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type Block = Record<string, any>
|
|
35
|
+
/** Roo also marks its own context management: a condensed summary (isSummary) and a sliding-window note (isTruncationMarker). */
|
|
36
|
+
type Msg = { role?: string; content?: unknown; ts?: number; isSummary?: boolean; isTruncationMarker?: boolean }
|
|
37
|
+
|
|
38
|
+
const API_FILE = "api_conversation_history.json"
|
|
39
|
+
const UI_FILE = "ui_messages.json"
|
|
40
|
+
|
|
41
|
+
export function taskReader(f: Flavor): Reader {
|
|
42
|
+
return {
|
|
43
|
+
tool: f.tool,
|
|
44
|
+
label: f.label,
|
|
45
|
+
async list(home) {
|
|
46
|
+
const refs: SessionRef[] = []
|
|
47
|
+
for (const host of HOSTS) {
|
|
48
|
+
const root = join(appSupport(home), host.app, "User", "globalStorage", f.extension)
|
|
49
|
+
const tasks = join(root, "tasks")
|
|
50
|
+
if (!isDir(tasks)) continue
|
|
51
|
+
const index = loadIndex(root)
|
|
52
|
+
for (const id of ls(tasks)) {
|
|
53
|
+
const dir = join(tasks, id)
|
|
54
|
+
// The index can list tasks whose folder is gone, and folders can lack the index entry: the folder decides.
|
|
55
|
+
if (!exists(join(dir, API_FILE))) continue
|
|
56
|
+
let e = index.get(id) ?? historyItem(dir)
|
|
57
|
+
// Older index entries lack the folder, and some tasks have no entry: the history's first message says both.
|
|
58
|
+
if (!e?.cwd || !e.task) e = { ...peek(join(dir, API_FILE)), ...e }
|
|
59
|
+
const times = [iso(e?.ts), mtime(join(dir, API_FILE)), mtime(join(dir, UI_FILE))].filter((t): t is string => !!t)
|
|
60
|
+
refs.push({
|
|
61
|
+
tool: f.tool,
|
|
62
|
+
sourceId: id,
|
|
63
|
+
path: dir,
|
|
64
|
+
...(e?.task ? { title: titleOf(e.task) } : {}),
|
|
65
|
+
...(e?.cwd ? { cwd: e.cwd } : {}),
|
|
66
|
+
...(times.length ? { updatedAt: times.sort().at(-1)! } : {}),
|
|
67
|
+
extra: { host: host.app },
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
72
|
+
},
|
|
73
|
+
async read(ref, home) {
|
|
74
|
+
return readTask(f, ref, home)
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const titleOf = (task: string) => {
|
|
80
|
+
const line = task.replace(/\s+/g, " ").trim()
|
|
81
|
+
return line.length > 120 ? `${line.slice(0, 119)}…` : line
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The task list: Cline's state/taskHistory.json or Roo's tasks/_index.json. */
|
|
85
|
+
function loadIndex(root: string): Map<string, IndexEntry> {
|
|
86
|
+
const out = new Map<string, IndexEntry>()
|
|
87
|
+
const add = (items: unknown) => {
|
|
88
|
+
if (!Array.isArray(items)) return
|
|
89
|
+
for (const it of items as Array<Record<string, any>>) if (typeof it?.id === "string") out.set(it.id, entryOf(it))
|
|
90
|
+
}
|
|
91
|
+
add(readJson(join(root, "state", "taskHistory.json")))
|
|
92
|
+
add(readJson<{ entries?: unknown }>(join(root, "tasks", "_index.json"))?.entries)
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The task text and folder from the start of a task's history, for tasks the index does not list (older Roo
|
|
98
|
+
* kept its list in VS Code's own database): the first 64 KB hold the first message, without parsing the rest.
|
|
99
|
+
*/
|
|
100
|
+
function peek(apiFile: string): IndexEntry | undefined {
|
|
101
|
+
let head = ""
|
|
102
|
+
try {
|
|
103
|
+
const fd = openSync(apiFile, "r")
|
|
104
|
+
try {
|
|
105
|
+
const buf = Buffer.alloc(64 * 1024)
|
|
106
|
+
head = buf.toString("utf8", 0, readSync(fd, buf, 0, buf.length, 0))
|
|
107
|
+
} finally {
|
|
108
|
+
closeSync(fd)
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
return undefined
|
|
112
|
+
}
|
|
113
|
+
const out: IndexEntry = {}
|
|
114
|
+
const raw = head.match(/"text"\s*:\s*"((?:[^"\\]|\\.)*)"/)?.[1]
|
|
115
|
+
if (raw) {
|
|
116
|
+
try {
|
|
117
|
+
const task = unwrap(JSON.parse(`"${raw}"`) as string)
|
|
118
|
+
if (task) out.task = task
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
const cwd = head.match(/# Current (?:Working|Workspace) Directory \((.+?)\) Files/)?.[1]
|
|
122
|
+
if (cwd) out.cwd = cwd
|
|
123
|
+
return out.task || out.cwd ? out : undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Roo's per-task copy of its index entry (newer versions). */
|
|
127
|
+
function historyItem(dir: string): IndexEntry | undefined {
|
|
128
|
+
const it = readJson<Record<string, any>>(join(dir, "history_item.json"))
|
|
129
|
+
return it && typeof it === "object" ? entryOf(it) : undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function entryOf(it: Record<string, any>): IndexEntry {
|
|
133
|
+
const cwd = it.cwdOnTaskInitialization ?? it.workspace ?? it.shadowGitConfigWorkTree
|
|
134
|
+
return {
|
|
135
|
+
...(typeof it.task === "string" ? { task: it.task } : {}),
|
|
136
|
+
...(typeof it.ts === "number" ? { ts: it.ts } : {}),
|
|
137
|
+
...(typeof cwd === "string" && cwd ? { cwd } : {}),
|
|
138
|
+
...(typeof it.parentTaskId === "string" ? { parentId: it.parentTaskId } : {}),
|
|
139
|
+
...(it.toolProtocol === "native" ? { native: true } : {}),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readTask(f: Flavor, ref: SessionRef, home: string): Session {
|
|
144
|
+
const dir = ref.path
|
|
145
|
+
const api = readJson<Msg[]>(join(dir, API_FILE))
|
|
146
|
+
if (!Array.isArray(api)) throw new Error(`${API_FILE} is missing or not JSON in ${dir}`)
|
|
147
|
+
const ui = readJson<Array<Record<string, any>>>(join(dir, UI_FILE))
|
|
148
|
+
const meta = readJson<Record<string, any>>(join(dir, "task_metadata.json")) ?? {}
|
|
149
|
+
const root = join(dir, "..", "..")
|
|
150
|
+
const entry = loadIndex(root).get(ref.sourceId) ?? historyItem(dir)
|
|
151
|
+
const app = (ref.extra?.host as string | undefined) ?? HOSTS.find((h) => dir.includes(`/${h.app}/User/globalStorage/`))?.app
|
|
152
|
+
|
|
153
|
+
// When each message was sent. Roo stamps its messages; Cline does not, but every panel message records how
|
|
154
|
+
// long the conversation was when it appeared (conversationHistoryIndex), which dates each message closely.
|
|
155
|
+
const uiTimes = new Map<number, number>()
|
|
156
|
+
const uiTs: number[] = []
|
|
157
|
+
for (const u of Array.isArray(ui) ? ui : []) {
|
|
158
|
+
if (typeof u?.ts !== "number") continue
|
|
159
|
+
uiTs.push(u.ts)
|
|
160
|
+
const k = u.conversationHistoryIndex
|
|
161
|
+
if (typeof k === "number" && !uiTimes.has(k)) uiTimes.set(k, u.ts)
|
|
162
|
+
}
|
|
163
|
+
const at = (i: number) => iso(api[i]?.ts) ?? iso(uiTimes.get(i))
|
|
164
|
+
|
|
165
|
+
const usage = (Array.isArray(meta.model_usage) ? meta.model_usage : [])
|
|
166
|
+
.filter((u: any) => typeof u?.model_id === "string")
|
|
167
|
+
.sort((a: any, b: any) => (a.ts ?? 0) - (b.ts ?? 0)) as Array<{ ts?: number; model_id: string }>
|
|
168
|
+
const modelAt = (when: string | undefined): string | undefined => {
|
|
169
|
+
if (!usage.length) return undefined
|
|
170
|
+
if (!when) return usage.at(-1)!.model_id
|
|
171
|
+
const t = Date.parse(when)
|
|
172
|
+
return [...usage].reverse().find((u) => (u.ts ?? 0) <= t)?.model_id ?? usage[0]!.model_id
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const lost = lossCounter()
|
|
176
|
+
let cwd = ref.cwd ?? entry?.cwd
|
|
177
|
+
/** The session this task continues, when the writer made it. */
|
|
178
|
+
let continued: string | undefined
|
|
179
|
+
const native = !!entry?.native || api.some((m) => Array.isArray(m.content) && (m.content as Block[]).some((b) => b?.type === "tool_use"))
|
|
180
|
+
const turns: Turn[] = []
|
|
181
|
+
const models: string[] = []
|
|
182
|
+
/** XML calls from the last assistant message, waiting for their results. */
|
|
183
|
+
let waiting: Array<{ id: string; name: string; done: boolean }> = []
|
|
184
|
+
// Native call ids are the provider's, and some repeat them across a task (Gemini numbers calls per message:
|
|
185
|
+
// "read_file-0" again and again). Turns need them unique, so a repeat gets a suffix, and a result answers the
|
|
186
|
+
// latest call issued under its id, since results come in the message right after their call.
|
|
187
|
+
const usedIds = new Set<string>()
|
|
188
|
+
const latestId = new Map<string, string>()
|
|
189
|
+
const callIdFor = (raw: string) => {
|
|
190
|
+
let id = raw
|
|
191
|
+
for (let k = 2; usedIds.has(id); k++) id = `${raw}#${k}`
|
|
192
|
+
usedIds.add(id)
|
|
193
|
+
latestId.set(raw, id)
|
|
194
|
+
return id
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
api.forEach((m, i) => {
|
|
198
|
+
const blocks: Block[] = typeof m.content === "string" ? [{ type: "text", text: m.content }] : Array.isArray(m.content) ? (m.content as Block[]) : []
|
|
199
|
+
const when = at(i)
|
|
200
|
+
// Roo condenses the context without deleting anything: the older messages stay (marked condenseParent) and a
|
|
201
|
+
// summary is added, as an assistant message (3.38) or a user one (3.54). It is the tool's own compaction,
|
|
202
|
+
// not something the model or the person said. A sliding-window truncation leaves a marker message the same way.
|
|
203
|
+
if (m.isSummary) {
|
|
204
|
+
// A summary the writer made also repeats the continuation note and carries what the person said next.
|
|
205
|
+
const texts = blocks.filter((b) => b?.type === "text").map((b) => String(b.text ?? ""))
|
|
206
|
+
const said = texts.filter((t) => /^\s*<user_message>/.test(t)).map(unwrap).filter(Boolean)
|
|
207
|
+
const summary = texts
|
|
208
|
+
.filter((t) => !CONTINUED_NOTE.test(t.trimStart()) && !/^\s*<user_message>/.test(t))
|
|
209
|
+
.join("\n\n")
|
|
210
|
+
.replace(/^\s*## Conversation Summary\n/, "")
|
|
211
|
+
.trim()
|
|
212
|
+
if (texts.some((t) => CONTINUED_NOTE.test(t.trimStart()))) continued ??= texts.find((t) => CONTINUED_NOTE.test(t.trimStart()))!.trimStart().match(CONTINUED_NOTE)![1]
|
|
213
|
+
if (summary) turns.push({ kind: "compaction", ...(when ? { at: when } : {}), summary })
|
|
214
|
+
if (said.length) turns.push({ kind: "user", ...(when ? { at: when } : {}), text: said.join("\n\n") })
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
if (m.isTruncationMarker) {
|
|
218
|
+
lost.add("context truncation markers")
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
if (m.role === "assistant") {
|
|
222
|
+
waiting = []
|
|
223
|
+
const model = modelAt(when)
|
|
224
|
+
if (model && !models.includes(model)) models.push(model)
|
|
225
|
+
const next = api[i + 1]
|
|
226
|
+
const answered = new Set<string>()
|
|
227
|
+
if (next?.role === "user" && Array.isArray(next.content))
|
|
228
|
+
for (const b of next.content as Block[]) if (b?.type === "text") {
|
|
229
|
+
const h = resultHeader(String(b.text ?? ""))
|
|
230
|
+
if (h) answered.add(h.name)
|
|
231
|
+
const skipped = skipNotice(String(b.text ?? ""))
|
|
232
|
+
if (skipped) answered.add(skipped)
|
|
233
|
+
}
|
|
234
|
+
// XML is read only in tasks on the XML protocol: in a native task, tags in the text were never run.
|
|
235
|
+
const isTool = native ? () => false : (name: string) => XML_TOOLS.has(name) || answered.has(name)
|
|
236
|
+
let n = 0
|
|
237
|
+
for (const b of blocks) {
|
|
238
|
+
switch (b?.type) {
|
|
239
|
+
case "text":
|
|
240
|
+
for (const p of splitAssistantText(String(b.text ?? ""), isTool)) {
|
|
241
|
+
if (p.kind === "text") turns.push({ kind: "assistant", ...(when ? { at: when } : {}), text: p.text, ...(model ? { model } : {}) })
|
|
242
|
+
else if (p.kind === "thinking") turns.push({ kind: "reasoning", ...(when ? { at: when } : {}), text: p.text })
|
|
243
|
+
else {
|
|
244
|
+
const id = `xml-${i}-${n++}`
|
|
245
|
+
pushCall(turns, { id, name: p.name, input: p.input, at: when, model })
|
|
246
|
+
waiting.push({ id, name: p.name, done: false })
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
break
|
|
250
|
+
case "thinking":
|
|
251
|
+
if (typeof b.thinking === "string" && b.thinking.trim()) {
|
|
252
|
+
turns.push({ kind: "reasoning", ...(when ? { at: when } : {}), text: b.thinking })
|
|
253
|
+
if (b.signature) lost.add("reasoning signatures")
|
|
254
|
+
} else lost.add("signed reasoning")
|
|
255
|
+
break
|
|
256
|
+
case "redacted_thinking":
|
|
257
|
+
lost.add("encrypted reasoning")
|
|
258
|
+
break
|
|
259
|
+
case "thoughtSignature":
|
|
260
|
+
lost.add("Gemini thought signatures")
|
|
261
|
+
break
|
|
262
|
+
case "tool_use":
|
|
263
|
+
pushCall(turns, { id: callIdFor(String(b.id ?? `tool-${i}-${n++}`)), name: String(b.name ?? "unknown"), input: b.input ?? {}, at: when, model })
|
|
264
|
+
break
|
|
265
|
+
case "image":
|
|
266
|
+
lost.add("image data")
|
|
267
|
+
break
|
|
268
|
+
default:
|
|
269
|
+
lost.add(`unknown block types: ${String(b?.type)}`)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
if (m.role !== "user") {
|
|
275
|
+
lost.add(`unknown message roles: ${String(m.role)}`)
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// A user message: tool results first (they answer the message before), then what the person wrote.
|
|
280
|
+
const texts: string[] = []
|
|
281
|
+
const attachments: Attachment[] = []
|
|
282
|
+
let result: { id: string; body: string[] } | undefined
|
|
283
|
+
const flush = () => {
|
|
284
|
+
if (!result) return
|
|
285
|
+
const { output, feedback } = splitFeedback(result.body.join("\n\n"))
|
|
286
|
+
const c = clip(output || "(no output)")
|
|
287
|
+
turns.push({ kind: "tool_result", ...(when ? { at: when } : {}), callId: result.id, ok: !FAILED.test(output), output: c.text, ...(c.truncated ? { truncated: true } : {}) })
|
|
288
|
+
if (feedback) texts.push(feedback)
|
|
289
|
+
result = undefined
|
|
290
|
+
}
|
|
291
|
+
for (const b of blocks) {
|
|
292
|
+
if (b?.type === "text") {
|
|
293
|
+
const t = String(b.text ?? "")
|
|
294
|
+
const lead = t.trimStart()
|
|
295
|
+
if (lead.startsWith("<environment_details>")) {
|
|
296
|
+
flush()
|
|
297
|
+
cwd ??= t.match(/# Current (?:Working|Workspace) Directory \((.+?)\) Files/)?.[1]
|
|
298
|
+
lost.add("environment details blocks")
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
// A call the extension skipped is answered by a notice of its own, not by the open result's output.
|
|
302
|
+
const skipped = skipNotice(t)
|
|
303
|
+
if (skipped) {
|
|
304
|
+
flush()
|
|
305
|
+
const call = waiting.find((w) => !w.done && w.name === skipped) ?? waiting.find((w) => !w.done)
|
|
306
|
+
if (call) {
|
|
307
|
+
call.done = true
|
|
308
|
+
turns.push({ kind: "tool_result", ...(when ? { at: when } : {}), callId: call.id, ok: false, output: t.trim() })
|
|
309
|
+
} else lost.add("harness notices")
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
const h = resultHeader(t)
|
|
313
|
+
if (h) {
|
|
314
|
+
flush()
|
|
315
|
+
const call = waiting.find((w) => !w.done && w.name === h.name) ?? waiting.find((w) => !w.done)
|
|
316
|
+
if (call) {
|
|
317
|
+
call.done = true
|
|
318
|
+
result = { id: call.id, body: h.rest.trim() ? [h.rest] : [] }
|
|
319
|
+
} else texts.push(t.trim()) // a result for no call we know: keep what the model saw, as text
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
// Resuming a task adds the extension's notice, and Cline 3.5x puts the person's words in a block of their
|
|
323
|
+
// own after a lead-in ("New instructions for task continuation: <user_message>…"): only the words are theirs.
|
|
324
|
+
if (lead.startsWith("[TASK RESUMPTION]") || RESUME_LEAD.test(lead)) {
|
|
325
|
+
flush()
|
|
326
|
+
const said = t.match(/<user_message>([\s\S]*?)<\/user_message>/)?.[1]?.trim()
|
|
327
|
+
if (said) texts.push(said)
|
|
328
|
+
else lost.add("task resumption notices")
|
|
329
|
+
continue
|
|
330
|
+
}
|
|
331
|
+
if (result) {
|
|
332
|
+
result.body.push(t)
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
// The writer's note on where the task came from, and its marker for a summary that replaced earlier turns.
|
|
336
|
+
const note = lead.match(CONTINUED_NOTE)
|
|
337
|
+
if (note) {
|
|
338
|
+
continued ??= note[1]
|
|
339
|
+
continue
|
|
340
|
+
}
|
|
341
|
+
if (lead.startsWith(SUMMARY_LEAD)) {
|
|
342
|
+
flush()
|
|
343
|
+
// What the person said before the summary in the same message comes first.
|
|
344
|
+
if (texts.length) turns.push({ kind: "user", ...(when ? { at: when } : {}), text: texts.splice(0).join("\n\n") })
|
|
345
|
+
const summary = t.slice(t.indexOf("]") + 1).trim()
|
|
346
|
+
if (summary) turns.push({ kind: "compaction", ...(when ? { at: when } : {}), summary })
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
if (lead.startsWith("[ERROR]")) {
|
|
350
|
+
lost.add("harness notices")
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
const own = unwrap(t)
|
|
354
|
+
if (own) texts.push(own)
|
|
355
|
+
} else if (b?.type === "tool_result") {
|
|
356
|
+
flush()
|
|
357
|
+
const c = clip(resultText(b.content, lost) || "(no output)")
|
|
358
|
+
const raw = String(b.tool_use_id)
|
|
359
|
+
turns.push({ kind: "tool_result", ...(when ? { at: when } : {}), callId: latestId.get(raw) ?? raw, ok: !b.is_error, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
|
|
360
|
+
} else if (b?.type === "image") {
|
|
361
|
+
if (result) lost.add("images in tool results")
|
|
362
|
+
else {
|
|
363
|
+
const mime = b.source?.media_type
|
|
364
|
+
attachments.push({ kind: "image", ...(typeof mime === "string" ? { mime } : {}) })
|
|
365
|
+
lost.add("image data")
|
|
366
|
+
}
|
|
367
|
+
} else lost.add(`unknown block types: ${String(b?.type)}`)
|
|
368
|
+
}
|
|
369
|
+
flush()
|
|
370
|
+
if (texts.length || attachments.length)
|
|
371
|
+
turns.push({ kind: "user", ...(when ? { at: when } : {}), text: texts.join("\n\n"), ...(attachments.length ? { attachments } : {}) })
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
const firstAt = /^\d{12,14}$/.test(ref.sourceId) ? iso(Number(ref.sourceId)) : undefined
|
|
375
|
+
const startedAt = firstAt ?? turns.find((t) => t.at)?.at ?? iso(uiTs[0])
|
|
376
|
+
const updatedAt = iso(uiTs.reduce((a, b) => Math.max(a, b), 0)) ?? iso(entry?.ts) ?? mtime(join(dir, API_FILE))
|
|
377
|
+
const toolVersion = extensionVersion(home, app, f.extension)
|
|
378
|
+
const h = header(f.tool, ref.sourceId, {
|
|
379
|
+
...(toolVersion ? { toolVersion } : {}),
|
|
380
|
+
sourcePath: dir,
|
|
381
|
+
...(entry?.task ? { title: titleOf(entry.task) } : ref.title ? { title: ref.title } : {}),
|
|
382
|
+
project: { ...(cwd ? { cwd } : {}), ...gitInfo(cwd) },
|
|
383
|
+
...(startedAt ? { startedAt } : {}),
|
|
384
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
385
|
+
...(entry?.parentId
|
|
386
|
+
? { parent: { id: `${f.tool}:${entry.parentId}`, kind: "subagent" as const } }
|
|
387
|
+
: continued
|
|
388
|
+
? { parent: { id: continued, kind: "continued" as const } }
|
|
389
|
+
: {}),
|
|
390
|
+
})
|
|
391
|
+
for (const m of models) noteModel(h, m)
|
|
392
|
+
lost.flush(h)
|
|
393
|
+
return { header: h, turns }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Cline's lead-ins to what the person typed when resuming, and its note when they typed nothing (plan mode). */
|
|
397
|
+
const RESUME_LEAD = /^(?:New instructions for task continuation|New message to respond to with plan_mode_respond tool|\(The user did not provide a new message)/
|
|
398
|
+
|
|
399
|
+
/** Results that say the call did not go through. */
|
|
400
|
+
const FAILED = /The user denied this operation|The tool execution failed|^\s*<error>|^\s*Error[: ]/i
|
|
401
|
+
|
|
402
|
+
function pushCall(turns: Turn[], c: { id: string; name: string; input: unknown; at: string | undefined; model: string | undefined }) {
|
|
403
|
+
const shown = SHOWN[c.name]
|
|
404
|
+
const text = shown ? (c.input as Record<string, unknown> | undefined)?.[shown] : undefined
|
|
405
|
+
if (typeof text === "string" && text.trim()) turns.push({ kind: "assistant", ...(c.at ? { at: c.at } : {}), text: text.trim(), ...(c.model ? { model: c.model } : {}) })
|
|
406
|
+
turns.push({ kind: "tool_call", ...(c.at ? { at: c.at } : {}), id: c.id, name: c.name, toolKind: toolKindOf(c.name), input: c.input })
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** What the person typed, out of the tags Cline wraps it in. */
|
|
410
|
+
function unwrap(t: string): string {
|
|
411
|
+
return t.replace(/<(task|feedback|answer|user_message)>\s*([\s\S]*?)\s*<\/\1>/g, "$2").trim()
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Feedback the person gave in reply to a tool (a denied edit, a finished task) is their message, not the tool's output. */
|
|
415
|
+
function splitFeedback(body: string): { output: string; feedback?: string } {
|
|
416
|
+
const m = body.match(/<feedback>\s*([\s\S]*?)\s*<\/feedback>/)
|
|
417
|
+
if (!m) return { output: body.trim() }
|
|
418
|
+
return { output: body.replace(m[0], "").trim(), feedback: m[1]! }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** A native tool_result's content as text; images in it are counted, not kept. */
|
|
422
|
+
function resultText(content: unknown, lost: ReturnType<typeof lossCounter>): string {
|
|
423
|
+
if (typeof content === "string") return content
|
|
424
|
+
if (!Array.isArray(content)) return asText(content)
|
|
425
|
+
const out: string[] = []
|
|
426
|
+
for (const c of content as Block[]) {
|
|
427
|
+
if (c?.type === "text") out.push(String(c.text ?? ""))
|
|
428
|
+
else if (c?.type === "image") lost.add("images in tool results")
|
|
429
|
+
else out.push(asText(c))
|
|
430
|
+
}
|
|
431
|
+
return out.join("\n")
|
|
432
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import { appSupport, ls, readJson } from "../../fsutil.ts"
|
|
3
|
+
import type { ToolKind } from "../../session/types.ts"
|
|
4
|
+
|
|
5
|
+
// Facts about the Cline-lineage task store shared by the reader and the writers: which editors host the
|
|
6
|
+
// extensions, which versions the writers were checked against, and the tool names each extension's model
|
|
7
|
+
// knows. Checked against the sources at the installed versions (cline v3.56.2 and v4.1.26, Roo-Code v3.38.1
|
|
8
|
+
// and v3.54.0) and the task folders on the author's Mac (structure only).
|
|
9
|
+
|
|
10
|
+
/** VS Code-family apps that host the extensions: their data folder, extensions folder and process name. */
|
|
11
|
+
export interface Host {
|
|
12
|
+
app: string
|
|
13
|
+
extensions: string
|
|
14
|
+
process: string
|
|
15
|
+
/** The command that opens a folder in the app. */
|
|
16
|
+
cli: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const HOSTS: Host[] = [
|
|
20
|
+
{ app: "Code", extensions: ".vscode/extensions", process: "Code", cli: "code" },
|
|
21
|
+
{ app: "Code - Insiders", extensions: ".vscode-insiders/extensions", process: "Code - Insiders", cli: "code-insiders" },
|
|
22
|
+
{ app: "Cursor", extensions: ".cursor/extensions", process: "Cursor", cli: "cursor" },
|
|
23
|
+
{ app: "Windsurf", extensions: ".windsurf/extensions", process: "Windsurf", cli: "windsurf" },
|
|
24
|
+
{ app: "VSCodium", extensions: ".vscode-oss/extensions", process: "VSCodium", cli: "codium" },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
export const hostByApp = (app: string) => HOSTS.find((h) => h.app === app)
|
|
28
|
+
|
|
29
|
+
/** The extension's folder in a host's global storage. */
|
|
30
|
+
export const storageDir = (home: string, app: string, extension: string) => join(appSupport(home), app, "User", "globalStorage", extension)
|
|
31
|
+
|
|
32
|
+
/** The host's own global state database, where VS Code keeps every extension's globalState. */
|
|
33
|
+
export const stateDb = (home: string, app: string) => join(appSupport(home), app, "User", "globalStorage", "state.vscdb")
|
|
34
|
+
|
|
35
|
+
const num = (v: string) => v.split(".").map(Number)
|
|
36
|
+
|
|
37
|
+
/** Semantic-version order: negative when a is older than b. */
|
|
38
|
+
export function compareVersions(a: string, b: string): number {
|
|
39
|
+
const [x, y] = [num(a), num(b)]
|
|
40
|
+
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) - (y[i] ?? 0)
|
|
41
|
+
return 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The extension's version installed in an app. The host's extensions.json names the active copy (an
|
|
46
|
+
* update can leave the old folder behind until the editor cleans up); without it the newest folder wins.
|
|
47
|
+
*/
|
|
48
|
+
export function extensionVersion(home: string, app: string | undefined, extension: string): string | undefined {
|
|
49
|
+
const host = HOSTS.find((h) => h.app === app)
|
|
50
|
+
if (!host) return undefined
|
|
51
|
+
const dir = join(home, host.extensions)
|
|
52
|
+
const manifest = readJson<Array<{ identifier?: { id?: string }; version?: string }>>(join(dir, "extensions.json"))
|
|
53
|
+
const listed = Array.isArray(manifest)
|
|
54
|
+
? manifest.find((e) => e?.identifier?.id?.toLowerCase() === extension.toLowerCase() && typeof e.version === "string")?.version
|
|
55
|
+
: undefined
|
|
56
|
+
if (listed) return listed
|
|
57
|
+
const versions = ls(dir)
|
|
58
|
+
.map((d) => d.match(new RegExp(`^${extension.replace(/\./g, "\\.")}-(\\d+\\.\\d+\\.\\d+)`, "i"))?.[1])
|
|
59
|
+
.filter((v): v is string => !!v)
|
|
60
|
+
return versions.sort(compareVersions).at(-1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The note that opens a task the writer creates, as its own text block in the first API message: the model
|
|
65
|
+
* reads it, the task panel does not show it (the panel shows ui_messages.json, not the API history), and the
|
|
66
|
+
* reader recognises it.
|
|
67
|
+
*/
|
|
68
|
+
export const continuedNote = (label: string, id: string, when: string) =>
|
|
69
|
+
`[This task continues ${label} session ${id}, carried over by brain session on ${when}. The turns below are its history; reasoning and anything listed as lost did not carry over. Tool names may differ from yours. Check files before relying on details, since they may have changed.]`
|
|
70
|
+
export const CONTINUED_NOTE = /^\[This task continues .+? session (\S+), carried over by brain session/
|
|
71
|
+
|
|
72
|
+
/** Tools whose input is the text the person saw (the final answer, a plan): that text is also an assistant turn. */
|
|
73
|
+
export const SHOWN: Record<string, string> = { attempt_completion: "result", plan_mode_respond: "response" }
|
|
74
|
+
|
|
75
|
+
/** How the writer marks a compaction summary in Cline's history, where the summary is a plain user text block. */
|
|
76
|
+
export const SUMMARY_LEAD = "[Summary of the earlier conversation"
|
|
77
|
+
export const summaryText = (label: string, summary: string) => `${SUMMARY_LEAD} in ${label}, which replaced the turns before it]\n\n${summary}`
|
|
78
|
+
|
|
79
|
+
type Input = Record<string, unknown>
|
|
80
|
+
const str = (v: unknown) => (typeof v === "string" && v ? v : undefined)
|
|
81
|
+
const pathOf = (i: Input) => str(i.file_path) ?? str(i.path) ?? str(i.filePath) ?? str(i.target_file) ?? str(i.absolute_path) ?? str(i.file)
|
|
82
|
+
const commandOf = (i: Input) => (Array.isArray(i.command) ? i.command.map(String).join(" ") : str(i.command) ?? str(i.cmd))
|
|
83
|
+
|
|
84
|
+
/** A native tool call in the target's own vocabulary, or undefined to keep the call as it was. */
|
|
85
|
+
export type Mapping = (input: Input, name: string) => { name: string; input: Input } | undefined
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Portable tool kinds to the native tools each extension defines (Cline src/core/prompts/system-prompt/tools,
|
|
89
|
+
* Roo src/core/prompts/tools/native-tools). Only shapes that translate cleanly are mapped; anything else keeps
|
|
90
|
+
* its own name and input, which the model still reads as a past call.
|
|
91
|
+
*/
|
|
92
|
+
const COMMON: Partial<Record<ToolKind, Mapping>> = {
|
|
93
|
+
shell: (i) => {
|
|
94
|
+
const command = commandOf(i)
|
|
95
|
+
return command ? { name: "execute_command", input: { command } } : undefined
|
|
96
|
+
},
|
|
97
|
+
read: (i) => {
|
|
98
|
+
const path = pathOf(i)
|
|
99
|
+
return path ? { name: "read_file", input: { path } } : undefined
|
|
100
|
+
},
|
|
101
|
+
write: (i) => {
|
|
102
|
+
const path = pathOf(i)
|
|
103
|
+
const content = str(i.content) ?? str(i.file_text) ?? str(i.contents) ?? (typeof i.content === "string" ? "" : undefined)
|
|
104
|
+
return path && content !== undefined ? { name: "write_to_file", input: { path, content } } : undefined
|
|
105
|
+
},
|
|
106
|
+
search: (i, name) => {
|
|
107
|
+
// A glob search (Glob, glob_file_search) matches file names, which search_files cannot express.
|
|
108
|
+
const regex = /glob/i.test(name) ? undefined : str(i.pattern) ?? str(i.regex) ?? str(i.query)
|
|
109
|
+
if (!regex) return undefined
|
|
110
|
+
const pattern = str(i.glob) ?? str(i.include) ?? str(i.file_pattern)
|
|
111
|
+
return { name: "search_files", input: { path: str(i.path) ?? ".", regex, ...(pattern ? { file_pattern: pattern } : {}) } }
|
|
112
|
+
},
|
|
113
|
+
list: (i) => ({ name: "list_files", input: { path: pathOf(i) ?? str(i.dir_path) ?? str(i.target_directory) ?? ".", recursive: i.recursive === true } }),
|
|
114
|
+
ask: (i) => {
|
|
115
|
+
const question = str(i.question) ?? str((i.questions as Input[] | undefined)?.[0]?.question)
|
|
116
|
+
return question ? { name: "ask_followup_question", input: { question } } : undefined
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const oldNew = (i: Input) => {
|
|
121
|
+
const path = pathOf(i)
|
|
122
|
+
const before = typeof i.old_string === "string" ? i.old_string : typeof i.oldString === "string" ? i.oldString : typeof i.old_str === "string" ? i.old_str : undefined
|
|
123
|
+
const after = typeof i.new_string === "string" ? i.new_string : typeof i.newString === "string" ? i.newString : typeof i.new_str === "string" ? i.new_str : undefined
|
|
124
|
+
return path && before !== undefined && after !== undefined ? { path, before, after } : undefined
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const CLINE_TOOLS: Partial<Record<ToolKind, Mapping>> = {
|
|
128
|
+
...COMMON,
|
|
129
|
+
shell: (i) => {
|
|
130
|
+
const command = commandOf(i)
|
|
131
|
+
return command ? { name: "execute_command", input: { command, requires_approval: false } } : undefined
|
|
132
|
+
},
|
|
133
|
+
// Cline edits with SEARCH/REPLACE blocks in replace_in_file.
|
|
134
|
+
edit: (i) => {
|
|
135
|
+
const e = oldNew(i)
|
|
136
|
+
return e ? { name: "replace_in_file", input: { path: e.path, diff: `------- SEARCH\n${e.before}\n=======\n${e.after}\n+++++++ REPLACE` } } : undefined
|
|
137
|
+
},
|
|
138
|
+
web: (i) => {
|
|
139
|
+
const url = str(i.url)
|
|
140
|
+
return url ? { name: "web_fetch", input: { url } } : undefined
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const ROO_TOOLS: Partial<Record<ToolKind, Mapping>> = {
|
|
145
|
+
...COMMON,
|
|
146
|
+
// search_replace takes Claude-style old and new strings in both 3.38 and 3.54.
|
|
147
|
+
edit: (i) => {
|
|
148
|
+
const e = oldNew(i)
|
|
149
|
+
return e ? { name: "search_replace", input: { file_path: e.path, old_string: e.before, new_string: e.after } } : undefined
|
|
150
|
+
},
|
|
151
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
import { copyFileSync, mkdirSync } from "node:fs"
|
|
3
|
+
import { basename, join } from "node:path"
|
|
4
|
+
import { exists } from "../../fsutil.ts"
|
|
5
|
+
|
|
6
|
+
// An extension's globalState in a VS Code-family app's own database (<app>/User/globalStorage/state.vscdb):
|
|
7
|
+
// one ItemTable row per extension, keyed by its id as the publisher wrote it ("RooVeterinaryInc.roo-cline"),
|
|
8
|
+
// holding the whole state as JSON text. VS Code reads the table once at startup and writes its in-memory copy
|
|
9
|
+
// back, so this is only written while the app is closed, after a backup.
|
|
10
|
+
|
|
11
|
+
/** The row's key and JSON value, or undefined when the extension has never stored state there. */
|
|
12
|
+
export function readState(db: Database, extension: string): { key: string; value: Record<string, unknown> } | undefined {
|
|
13
|
+
const row = db.query("SELECT key, value FROM ItemTable WHERE lower(key) = lower(?)").get(extension) as { key: string; value: string | Uint8Array } | null
|
|
14
|
+
if (!row) return undefined
|
|
15
|
+
const text = typeof row.value === "string" ? row.value : new TextDecoder().decode(row.value)
|
|
16
|
+
const value = JSON.parse(text) as unknown
|
|
17
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${extension}'s state in ${db.filename} is not a JSON object`)
|
|
18
|
+
return { key: row.key, value: value as Record<string, unknown> }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Copies the database and its journal files into `dir`, returning the copies. */
|
|
22
|
+
export function backupDb(path: string, dir: string): string[] {
|
|
23
|
+
mkdirSync(dir, { recursive: true })
|
|
24
|
+
const out: string[] = []
|
|
25
|
+
for (const side of ["", "-journal", "-wal", "-shm"]) {
|
|
26
|
+
if (!exists(path + side)) continue
|
|
27
|
+
const copy = join(dir, basename(path) + side)
|
|
28
|
+
copyFileSync(path + side, copy)
|
|
29
|
+
out.push(copy)
|
|
30
|
+
}
|
|
31
|
+
return out
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Applies `change` to an extension's state in one transaction. The key keeps the case the row already has;
|
|
36
|
+
* a new row takes `extension` as written.
|
|
37
|
+
*/
|
|
38
|
+
export function updateState(path: string, extension: string, change: (state: Record<string, unknown>) => void) {
|
|
39
|
+
// Never creates the file: an editor that has not made its database yet has no state to add to.
|
|
40
|
+
const db = new Database(path, { readwrite: true, create: false })
|
|
41
|
+
try {
|
|
42
|
+
db.exec("BEGIN IMMEDIATE")
|
|
43
|
+
try {
|
|
44
|
+
const cur = readState(db, extension)
|
|
45
|
+
const state = cur?.value ?? {}
|
|
46
|
+
change(state)
|
|
47
|
+
db.query("INSERT OR REPLACE INTO ItemTable (key, value) VALUES (?, ?)").run(cur?.key ?? extension, JSON.stringify(state))
|
|
48
|
+
db.exec("COMMIT")
|
|
49
|
+
} catch (e) {
|
|
50
|
+
db.exec("ROLLBACK")
|
|
51
|
+
throw e
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
db.close()
|
|
55
|
+
}
|
|
56
|
+
}
|