@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,405 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import { createHash } from "node:crypto"
|
|
3
|
+
import { dirname, join, resolve } from "node:path"
|
|
4
|
+
import type { Session, SessionHeader, Turn } from "../../session/types.ts"
|
|
5
|
+
import { appSupport, exists, gitInfo, ls, mtime, readJson } from "../../fsutil.ts"
|
|
6
|
+
import { withSnapshot } from "../../sqlite.ts"
|
|
7
|
+
import { clip, header, iso, lossCounter, noteModel, toolKindOf } from "../../session/util.ts"
|
|
8
|
+
import type { Reader, SessionRef, Writer } from "../types.ts"
|
|
9
|
+
import { bytes, messageOrder, toolFailed, toolResultText } from "../cursor-ide/agentkv.ts"
|
|
10
|
+
import { type Loss, userText } from "../cursor-ide/transcripts.ts"
|
|
11
|
+
import { Workspaces } from "../cursor-ide/workspaces.ts"
|
|
12
|
+
|
|
13
|
+
// Cursor CLI chats (catalogue entry 24 and its fact-check). Each chat is its own SQLite store,
|
|
14
|
+
// <home>/.cursor/chats/<md5 of the resolved cwd>/<agentId>/store.db, and ACP sessions keep the same store
|
|
15
|
+
// at <home>/.cursor/acp-sessions/<sessionId>/store.db. A store has two tables (cursor-agent 2025.09 and
|
|
16
|
+
// 2026.10 bundles agree): meta(key, value), whose key "0" holds the chat's metadata as JSON, hex-encoded
|
|
17
|
+
// by both bundles (plain JSON is accepted too); and blobs(id, data), content-addressed by SHA-256. JSON
|
|
18
|
+
// blobs are AI-SDK messages (role user/assistant/tool/system; parts text, reasoning, tool-call,
|
|
19
|
+
// tool-result); the rest are protobuf conversation state. Message order comes from the protobuf root
|
|
20
|
+
// that meta's latestRootBlobId names (agentkv.ts), not from the order blobs were stored in, which
|
|
21
|
+
// differs (blobs are written with INSERT OR REPLACE); only when the root cannot be read is the stored
|
|
22
|
+
// order used. 2026.x adds a meta.json sidecar per chat that `agent ls`
|
|
23
|
+
// reads ({schemaVersion 1, title, createdAtMs, updatedAtMs, hasConversation, isSubagent, cwd}); an ACP
|
|
24
|
+
// session's meta.json is {schemaVersion 1, cwd, title}. Subagents are chats of their own in the same
|
|
25
|
+
// folder, with subagentInfo {parentAgentId, toolCallId} in their metadata. No CLI chat exists on the Mac
|
|
26
|
+
// this was written on, so the shapes come from the two bundles and the IDE's agentKv blobs, which the
|
|
27
|
+
// same harness writes. There is no writer: in phase 1 Cursor is reached by the continuation brief.
|
|
28
|
+
|
|
29
|
+
const TOOL = "cursor-cli"
|
|
30
|
+
|
|
31
|
+
/** Where cursor-agent keeps its state (CURSOR_CONFIG_DIR and XDG_CONFIG_HOME overrides are not followed). */
|
|
32
|
+
const cursorDir = (home: string) => join(home, ".cursor")
|
|
33
|
+
const chatsDir = (home: string) => join(cursorDir(home), "chats")
|
|
34
|
+
const acpDir = (home: string) => join(cursorDir(home), "acp-sessions")
|
|
35
|
+
|
|
36
|
+
/** The chats folder for a project: md5 of the resolved path, as cursor-agent names it. */
|
|
37
|
+
export const bucketOf = (cwd: string) => createHash("md5").update(resolve(cwd)).digest("hex")
|
|
38
|
+
|
|
39
|
+
/** The default chat name, which says nothing. */
|
|
40
|
+
const DEFAULT_NAME = "New Agent"
|
|
41
|
+
|
|
42
|
+
const str = (v: unknown) => (typeof v === "string" && v ? v : undefined)
|
|
43
|
+
|
|
44
|
+
/** Milliseconds from a time cursor-agent stored in seconds, milliseconds, micro- or nanoseconds. */
|
|
45
|
+
function ms(v: unknown): number | undefined {
|
|
46
|
+
const t = typeof v === "number" ? v : typeof v === "string" && /^\d+$/.test(v) ? Number(v) : Number.NaN
|
|
47
|
+
if (!Number.isFinite(t) || t <= 0) return undefined
|
|
48
|
+
return t < 1e11 ? t * 1000 : t >= 1e17 ? t / 1e6 : t >= 1e14 ? t / 1e3 : t
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** When the store was last written: its write-ahead log is newer than the file while uncheckpointed. */
|
|
52
|
+
const storeTime = (store: string) => [mtime(store), mtime(`${store}-wal`)].filter((x): x is string => !!x).sort().pop()
|
|
53
|
+
|
|
54
|
+
const hasTable = (db: Database, name: string) => !!db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(name)
|
|
55
|
+
|
|
56
|
+
const asString = (v: unknown) => (typeof v === "string" ? v : v instanceof Uint8Array ? new TextDecoder().decode(v) : undefined)
|
|
57
|
+
|
|
58
|
+
/** The metadata JSON from meta key "0": hex of the JSON text in both bundles, plain JSON in some builds. */
|
|
59
|
+
export function decodeMeta(value: unknown): Record<string, any> | undefined {
|
|
60
|
+
const t = asString(value)?.trim()
|
|
61
|
+
if (!t) return undefined
|
|
62
|
+
const text = /^[0-9a-f]+$/i.test(t) && t.length % 2 === 0 ? Buffer.from(t, "hex").toString("utf8") : t
|
|
63
|
+
try {
|
|
64
|
+
const v = JSON.parse(text)
|
|
65
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : undefined
|
|
66
|
+
} catch {
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface Store {
|
|
72
|
+
meta: Record<string, any>
|
|
73
|
+
/** PRAGMA user_version: the store schema (1 in both bundles). */
|
|
74
|
+
version?: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readMeta(db: Database): Store {
|
|
78
|
+
const version = (db.query("PRAGMA user_version").get() as { user_version?: number } | null)?.user_version
|
|
79
|
+
if (!hasTable(db, "meta")) return { meta: {}, version }
|
|
80
|
+
const rows = db.query("SELECT key, value FROM meta").all() as Array<{ key: string; value: unknown }>
|
|
81
|
+
const zero = rows.find((r) => r.key === "0")
|
|
82
|
+
const meta = zero ? decodeMeta(zero.value) : undefined
|
|
83
|
+
// Without key "0", the fields as rows of their own (agentId, name, createdAt, lastUsedModel...).
|
|
84
|
+
return { meta: meta ?? Object.fromEntries(rows.filter((r) => r.key !== "0").map((r) => [r.key, asString(r.value)])), version }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- list -------------------------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
interface Found {
|
|
90
|
+
id: string
|
|
91
|
+
store: string
|
|
92
|
+
kind: "chat" | "acp"
|
|
93
|
+
title?: string
|
|
94
|
+
cwd?: string
|
|
95
|
+
updatedAt?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** What `agent ls` shows for one chat folder, from its meta.json, else from the store's metadata row. */
|
|
99
|
+
function chatEntry(id: string, dir: string): Found | undefined {
|
|
100
|
+
const store = join(dir, "store.db")
|
|
101
|
+
const side = readJson<Record<string, any>>(join(dir, "meta.json"))
|
|
102
|
+
if (side && typeof side.createdAtMs === "number") {
|
|
103
|
+
if (side.isSubagent === true || side.hasConversation !== true) return undefined
|
|
104
|
+
return { id, store, kind: "chat", title: str(side.title), cwd: str(side.cwd), updatedAt: iso(ms(side.updatedAtMs)) ?? storeTime(store) }
|
|
105
|
+
}
|
|
106
|
+
// Older builds keep no sidecar; cursor-agent then opens the store, and so does this.
|
|
107
|
+
let entry: Found | undefined
|
|
108
|
+
try {
|
|
109
|
+
entry = withSnapshot(store, (db) => {
|
|
110
|
+
const { meta } = readMeta(db)
|
|
111
|
+
if (meta.subagentInfo) return undefined // read with its parent
|
|
112
|
+
const blobs = hasTable(db, "blobs") ? ((db.query("SELECT count(*) AS n FROM blobs").get() as { n: number }).n ?? 0) : 0
|
|
113
|
+
if (!str(meta.latestRootBlobId) && !blobs) return undefined // never used
|
|
114
|
+
const name = str(meta.name)
|
|
115
|
+
return { id, store, kind: "chat" as const, title: name !== DEFAULT_NAME ? name : undefined, cwd: str(meta.workspacePath), updatedAt: storeTime(store) }
|
|
116
|
+
})
|
|
117
|
+
} catch {
|
|
118
|
+
return undefined // not a store cursor-agent could open either
|
|
119
|
+
}
|
|
120
|
+
return entry
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function list(home: string): Promise<SessionRef[]> {
|
|
124
|
+
const found: Found[] = []
|
|
125
|
+
for (const bucket of ls(chatsDir(home))) {
|
|
126
|
+
for (const id of ls(join(chatsDir(home), bucket))) {
|
|
127
|
+
const dir = join(chatsDir(home), bucket, id)
|
|
128
|
+
if (!exists(join(dir, "store.db"))) continue
|
|
129
|
+
const e = chatEntry(id, dir)
|
|
130
|
+
if (e) found.push(e)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const id of ls(acpDir(home))) {
|
|
134
|
+
const store = join(acpDir(home), id, "store.db")
|
|
135
|
+
if (!exists(store)) continue
|
|
136
|
+
const side = readJson<Record<string, any>>(join(acpDir(home), id, "meta.json"))
|
|
137
|
+
found.push({ id, store, kind: "acp", title: str(side?.title), cwd: str(side?.cwd), updatedAt: storeTime(store) })
|
|
138
|
+
}
|
|
139
|
+
// A chat's folder is named by md5 of its cwd: match it against folders known from elsewhere.
|
|
140
|
+
if (found.some((f) => !f.cwd)) {
|
|
141
|
+
const byBucket = folderIndex(home, found.map((f) => f.cwd))
|
|
142
|
+
for (const f of found) if (!f.cwd && f.kind === "chat") f.cwd = byBucket.get(dirname(dirname(f.store)).split("/").pop()!)
|
|
143
|
+
}
|
|
144
|
+
return found
|
|
145
|
+
.map((f): SessionRef => ({
|
|
146
|
+
tool: TOOL,
|
|
147
|
+
sourceId: f.id,
|
|
148
|
+
path: f.store,
|
|
149
|
+
...(f.title ? { title: f.title } : {}),
|
|
150
|
+
...(f.cwd ? { cwd: f.cwd } : {}),
|
|
151
|
+
...(f.updatedAt ? { updatedAt: f.updatedAt } : {}),
|
|
152
|
+
extra: { kind: f.kind },
|
|
153
|
+
}))
|
|
154
|
+
.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** md5 bucket → folder, for every folder known: sessions that record theirs and Cursor's workspaces. */
|
|
158
|
+
function folderIndex(home: string, known: Array<string | undefined>): Map<string, string> {
|
|
159
|
+
const out = new Map<string, string>()
|
|
160
|
+
const folders = [...known.filter((x): x is string => !!x), ...new Workspaces(join(appSupport(home), "Cursor", "User", "workspaceStorage")).folders()]
|
|
161
|
+
for (const f of folders) out.set(bucketOf(f), f)
|
|
162
|
+
return out
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---- read -------------------------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
/** The text of a message's text parts (or its string content). */
|
|
168
|
+
const textOf = (m: Record<string, any>) =>
|
|
169
|
+
typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.filter((p: any) => p?.type === "text" && typeof p.text === "string").map((p: any) => p.text).join("\n\n") : ""
|
|
170
|
+
|
|
171
|
+
interface Conv {
|
|
172
|
+
h: SessionHeader
|
|
173
|
+
lost: Loss
|
|
174
|
+
turns: Turn[]
|
|
175
|
+
/** Original tool-call id → the id used in the session (made unique if a store repeats one). */
|
|
176
|
+
calls: Map<string, string>
|
|
177
|
+
used: Set<string>
|
|
178
|
+
fallbackModel?: string
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function userTurns(m: Record<string, any>, c: Conv) {
|
|
182
|
+
const parts: Array<Record<string, any>> = typeof m.content === "string" ? [{ type: "text", text: m.content }] : Array.isArray(m.content) ? m.content : []
|
|
183
|
+
const texts: string[] = []
|
|
184
|
+
let at: string | undefined
|
|
185
|
+
let context = false
|
|
186
|
+
for (const p of parts) {
|
|
187
|
+
if (p?.type === "text" && typeof p.text === "string") {
|
|
188
|
+
const u = userText(p.text)
|
|
189
|
+
at ??= u.at
|
|
190
|
+
if (u.text) texts.push(u.text)
|
|
191
|
+
else context ||= u.context
|
|
192
|
+
} else if (p?.type === "image") c.lost.add("images")
|
|
193
|
+
else if (p?.type === "file") c.lost.add("attached files")
|
|
194
|
+
else c.lost.add(`unknown record types: user part ${String(p?.type)}`)
|
|
195
|
+
}
|
|
196
|
+
if (texts.length) c.turns.push({ kind: "user", ...(at ? { at } : {}), text: texts.join("\n\n") })
|
|
197
|
+
else if (context) c.lost.add("injected context messages")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function assistantTurns(m: Record<string, any>, c: Conv) {
|
|
201
|
+
const parts: Array<Record<string, any>> = typeof m.content === "string" ? [{ type: "text", text: m.content }] : Array.isArray(m.content) ? m.content : []
|
|
202
|
+
const model = str(m.providerOptions?.cursor?.modelName) ?? parts.map((p) => str(p?.providerOptions?.cursor?.modelName)).find(Boolean) ?? c.fallbackModel
|
|
203
|
+
noteModel(c.h, model)
|
|
204
|
+
let text = ""
|
|
205
|
+
const flush = () => {
|
|
206
|
+
if (text.trim()) c.turns.push({ kind: "assistant", text, ...(model ? { model } : {}) })
|
|
207
|
+
text = ""
|
|
208
|
+
}
|
|
209
|
+
for (const p of parts) {
|
|
210
|
+
switch (p?.type) {
|
|
211
|
+
case "text":
|
|
212
|
+
text += typeof p.text === "string" ? p.text : ""
|
|
213
|
+
break
|
|
214
|
+
case "reasoning":
|
|
215
|
+
flush()
|
|
216
|
+
if (typeof p.text === "string" && p.text.trim()) c.turns.push({ kind: "reasoning", text: p.text })
|
|
217
|
+
else if (p.signature || p.providerOptions?.anthropic?.signature) c.lost.add("signed reasoning")
|
|
218
|
+
break
|
|
219
|
+
case "redacted-reasoning":
|
|
220
|
+
c.lost.add("redacted reasoning")
|
|
221
|
+
break
|
|
222
|
+
case "tool-call": {
|
|
223
|
+
flush()
|
|
224
|
+
const orig = str(p.toolCallId) ?? `call-${c.used.size + 1}`
|
|
225
|
+
let id = orig
|
|
226
|
+
for (let n = 2; c.used.has(id); n++) id = `${orig}#${n}`
|
|
227
|
+
c.used.add(id)
|
|
228
|
+
c.calls.set(orig, id)
|
|
229
|
+
const name = str(p.toolName) ?? "unknown"
|
|
230
|
+
c.turns.push({ kind: "tool_call", id, name, toolKind: toolKindOf(name), input: p.args ?? p.input ?? {} })
|
|
231
|
+
break
|
|
232
|
+
}
|
|
233
|
+
default:
|
|
234
|
+
c.lost.add(`unknown record types: assistant part ${String(p?.type)}`)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
flush()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function toolTurns(m: Record<string, any>, c: Conv) {
|
|
241
|
+
for (const p of (Array.isArray(m.content) ? m.content : []) as Array<Record<string, any>>) {
|
|
242
|
+
if (p?.type !== "tool-result") {
|
|
243
|
+
c.lost.add(`unknown record types: tool part ${String(p?.type)}`)
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
const images = Array.isArray(p.experimental_content) ? p.experimental_content.filter((x: any) => x?.type === "image").length : 0
|
|
247
|
+
if (images) c.lost.add("images", images)
|
|
248
|
+
const orig = str(p.toolCallId) ?? ""
|
|
249
|
+
const out = clip(toolResultText(p))
|
|
250
|
+
c.turns.push({ kind: "tool_result", callId: c.calls.get(orig) ?? orig, ok: !toolFailed(m, p), output: out.text, ...(out.truncated ? { truncated: true } : {}) })
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The JSON messages of a store in conversation order: as the protobuf root lists them, else, when the
|
|
256
|
+
* root cannot be read, in the order they were stored. Protobuf blobs are skipped.
|
|
257
|
+
*/
|
|
258
|
+
function messages(db: Database, meta: Record<string, any>, lost: Loss): Array<Record<string, any>> {
|
|
259
|
+
if (!hasTable(db, "blobs")) return []
|
|
260
|
+
let rows: Array<{ id: unknown; data: unknown }>
|
|
261
|
+
try {
|
|
262
|
+
rows = db.query("SELECT id, data FROM blobs ORDER BY rowid").all() as Array<{ id: unknown; data: unknown }>
|
|
263
|
+
} catch {
|
|
264
|
+
rows = db.query("SELECT id, data FROM blobs").all() as Array<{ id: unknown; data: unknown }>
|
|
265
|
+
}
|
|
266
|
+
const json = new Map<string, Record<string, any>>()
|
|
267
|
+
const decoder = new TextDecoder("utf-8", { fatal: true })
|
|
268
|
+
for (const r of rows) {
|
|
269
|
+
let text: string | undefined
|
|
270
|
+
try {
|
|
271
|
+
text = typeof r.data === "string" ? r.data : r.data instanceof Uint8Array ? decoder.decode(r.data) : undefined
|
|
272
|
+
} catch {
|
|
273
|
+
continue // binary: protobuf graph state
|
|
274
|
+
}
|
|
275
|
+
if (!text || text[0] !== "{") continue
|
|
276
|
+
let v: unknown
|
|
277
|
+
try {
|
|
278
|
+
v = JSON.parse(text)
|
|
279
|
+
} catch {
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
if (v && typeof v === "object" && typeof (v as Record<string, unknown>).role === "string") json.set(String(r.id), v as Record<string, any>)
|
|
283
|
+
else lost.add("unknown record types: JSON blob without a role")
|
|
284
|
+
}
|
|
285
|
+
const one = db.query("SELECT data FROM blobs WHERE id = ?")
|
|
286
|
+
const blob = (id: string) => bytes((one.get(id) as { data: unknown } | null)?.data)
|
|
287
|
+
const rootId = str(meta.latestRootBlobId)?.toLowerCase()
|
|
288
|
+
const root = rootId ? blob(rootId) : undefined
|
|
289
|
+
const order = root ? messageOrder(root, blob) : undefined
|
|
290
|
+
if (!order?.some((id) => json.has(id))) {
|
|
291
|
+
if (json.size && rootId) lost.add("conversation order (root not readable; stored order used)")
|
|
292
|
+
return [...json.values()]
|
|
293
|
+
}
|
|
294
|
+
const missing = order.filter((id) => !json.has(id)).length
|
|
295
|
+
if (missing) lost.add("messages the root lists but the store lacks", missing)
|
|
296
|
+
// Messages no root lists (left by a rewind or an earlier state of the chat): Cursor's own transcript
|
|
297
|
+
// writer shows none of them either.
|
|
298
|
+
const listed = new Set(order)
|
|
299
|
+
const outside = [...json].filter(([id, m]) => !listed.has(id) && m.role !== "system").length
|
|
300
|
+
if (outside) lost.add("messages outside the conversation (not in its root)", outside)
|
|
301
|
+
return order.filter((id) => json.has(id)).map((id) => json.get(id)!)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
interface Opened {
|
|
305
|
+
store: Store
|
|
306
|
+
messages: Array<Record<string, any>>
|
|
307
|
+
lost: Loss
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function open(storePath: string): Opened {
|
|
311
|
+
return withSnapshot(storePath, (db) => {
|
|
312
|
+
const lost = lossCounter()
|
|
313
|
+
const store = readMeta(db)
|
|
314
|
+
return { store, messages: messages(db, store.meta, lost), lost }
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Subagent chats in `bucket` spawned by `agentId`, with the tool call that spawned each. */
|
|
319
|
+
function subagentsOf(bucket: string, agentId: string, self: string): Array<{ id: string; store: string; callId?: string }> {
|
|
320
|
+
const out: Array<{ id: string; store: string; callId?: string }> = []
|
|
321
|
+
for (const id of ls(bucket)) {
|
|
322
|
+
const store = join(bucket, id, "store.db")
|
|
323
|
+
if (id === self || !exists(store)) continue
|
|
324
|
+
const side = readJson<Record<string, any>>(join(bucket, id, "meta.json"))
|
|
325
|
+
if (side && side.isSubagent !== true && typeof side.createdAtMs === "number") continue // a sidecar says it is a main chat
|
|
326
|
+
try {
|
|
327
|
+
const info = withSnapshot(store, (db) => readMeta(db).meta.subagentInfo) as Record<string, any> | undefined
|
|
328
|
+
if (str(info?.parentAgentId) === agentId) out.push({ id, store, callId: str(info?.toolCallId) })
|
|
329
|
+
} catch {
|
|
330
|
+
/* unreadable sibling: not ours to report */
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return out
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function readStore(ref: { sourceId: string; path: string; kind: string; cwd?: string; updatedAt?: string }, home: string, depth: number): Session {
|
|
337
|
+
const { store, messages: msgs, lost } = open(ref.path)
|
|
338
|
+
const dir = dirname(ref.path)
|
|
339
|
+
const side = readJson<Record<string, any>>(join(dir, "meta.json"))
|
|
340
|
+
const meta = store.meta
|
|
341
|
+
const name = str(meta.name)
|
|
342
|
+
const h = header(TOOL, ref.sourceId, {
|
|
343
|
+
toolVersion: [store.version !== undefined ? `store v${store.version}` : undefined, typeof side?.schemaVersion === "number" ? `meta.json v${side.schemaVersion}` : undefined].filter(Boolean).join(", ") || undefined,
|
|
344
|
+
sourcePath: ref.path,
|
|
345
|
+
title: str(side?.title) ?? (name !== DEFAULT_NAME ? name : undefined),
|
|
346
|
+
startedAt: iso(ms(meta.createdAt) ?? ms(side?.createdAtMs)),
|
|
347
|
+
updatedAt: ref.updatedAt ?? iso(ms(side?.updatedAtMs)) ?? storeTime(ref.path),
|
|
348
|
+
})
|
|
349
|
+
const parent = str(meta.subagentInfo?.parentAgentId)
|
|
350
|
+
if (parent) h.parent = { id: `${TOOL}:${parent}`, kind: "subagent" }
|
|
351
|
+
const c: Conv = { h, lost, turns: [], calls: new Map(), used: new Set(), fallbackModel: str(meta.lastUsedModel) }
|
|
352
|
+
for (const m of msgs) {
|
|
353
|
+
// A compaction's summary comes back as an ordinary message flagged isSummary; it is not something
|
|
354
|
+
// the person or the model said (Cursor's transcript writer leaves it out).
|
|
355
|
+
if (m.providerOptions?.cursor?.isSummary === true) {
|
|
356
|
+
const summary = textOf(m)
|
|
357
|
+
if (summary.trim()) c.turns.push({ kind: "compaction", summary })
|
|
358
|
+
} else if (m.role === "user") userTurns(m, c)
|
|
359
|
+
else if (m.role === "assistant") assistantTurns(m, c)
|
|
360
|
+
else if (m.role === "tool") toolTurns(m, c)
|
|
361
|
+
else if (m.role !== "system") lost.add(`unknown record types: role ${String(m.role)}`) // the system prompt is Cursor's own; targets write theirs
|
|
362
|
+
}
|
|
363
|
+
if (!h.models.length) noteModel(h, str(meta.lastUsedModel))
|
|
364
|
+
if (!msgs.length && str(meta.latestRootBlobId)) lost.add("conversation kept only as protobuf state (not decoded)")
|
|
365
|
+
|
|
366
|
+
const cwd = ref.cwd ?? str(side?.cwd) ?? str(meta.workspacePath) ?? (ref.kind === "chat" ? folderIndex(home, []).get(dirname(dir).split("/").pop()!) : undefined)
|
|
367
|
+
h.project = { ...(cwd ? { cwd } : {}), ...gitInfo(cwd) }
|
|
368
|
+
|
|
369
|
+
// Subagents live beside the chat (or, for an ACP session, in its project's chats folder) and are placed
|
|
370
|
+
// after the tool call that started them.
|
|
371
|
+
const children: Session[] = []
|
|
372
|
+
const bucket = ref.kind === "chat" ? dirname(dir) : cwd ? join(chatsDir(home), bucketOf(cwd)) : undefined
|
|
373
|
+
const agentId = str(meta.agentId) ?? ref.sourceId
|
|
374
|
+
if (bucket && depth < 3) {
|
|
375
|
+
for (const sub of subagentsOf(bucket, agentId, ref.sourceId)) {
|
|
376
|
+
let child: Session
|
|
377
|
+
try {
|
|
378
|
+
child = readStore({ sourceId: sub.id, path: sub.store, kind: "chat", cwd }, home, depth + 1)
|
|
379
|
+
} catch {
|
|
380
|
+
lost.add("unreadable subagent chats")
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
children.push(child)
|
|
384
|
+
const turn: Turn = { kind: "subagent", sessionId: child.header.id, ...(sub.callId ? { callId: c.calls.get(sub.callId) ?? sub.callId } : {}), ...(child.header.title ? { description: child.header.title } : {}) }
|
|
385
|
+
const at = sub.callId ? c.turns.findIndex((t) => t.kind === "tool_call" && t.id === (c.calls.get(sub.callId!) ?? sub.callId)) : -1
|
|
386
|
+
if (at < 0) c.turns.push(turn)
|
|
387
|
+
else {
|
|
388
|
+
// After the call's result when there is one, so the call and result stay together.
|
|
389
|
+
const res = c.turns.findIndex((t, i) => i > at && t.kind === "tool_result" && t.callId === (c.turns[at] as Extract<Turn, { kind: "tool_call" }>).id)
|
|
390
|
+
c.turns.splice((res >= 0 ? res : at) + 1, 0, turn)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
lost.flush(h)
|
|
395
|
+
return children.length ? { header: h, turns: c.turns, children } : { header: h, turns: c.turns }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function read(ref: SessionRef, home: string): Promise<Session> {
|
|
399
|
+
if (!exists(ref.path)) throw new Error(`no Cursor CLI store at ${ref.path}`)
|
|
400
|
+
const kind = ref.extra?.kind === "acp" || ref.path.startsWith(acpDir(home)) ? "acp" : "chat"
|
|
401
|
+
return readStore({ sourceId: ref.sourceId, path: ref.path, kind, ...(ref.cwd ? { cwd: ref.cwd } : {}), ...(ref.updatedAt ? { updatedAt: ref.updatedAt } : {}) }, home, 0)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export const readers: Reader[] = [{ tool: TOOL, label: "Cursor CLI", list, read }]
|
|
405
|
+
export const writers: Writer[] = []
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { asText } from "../../session/util.ts"
|
|
2
|
+
|
|
3
|
+
// Cursor's agent harness, in the IDE and the CLI alike, keeps the conversation the model sees as
|
|
4
|
+
// content-addressed blobs: AI-SDK messages as JSON, plus a protobuf root, ConversationStateStructure,
|
|
5
|
+
// that lists them in order. The order matters: blobs are written with INSERT OR REPLACE, so the order
|
|
6
|
+
// they were stored in is not the conversation's (on the Mac this was written on, a tool result was
|
|
7
|
+
// stored before the call that asked for it in 2 of 4 conversations).
|
|
8
|
+
//
|
|
9
|
+
// Only the fields needed for the order are read, by field number, from the protobuf wire format
|
|
10
|
+
// (cursor-agent 2025.09 and 2026.10 bundles agree): 1 root_prompt_messages_json, the blob ids of the
|
|
11
|
+
// current prompt's messages; 13 summary_archives (2026.10) and 11 summary_archive (older), blob ids of
|
|
12
|
+
// ConversationSummaryArchive messages, whose field 1, summarized_messages, lists the messages a
|
|
13
|
+
// compaction replaced. Cursor's own transcript writer reads the same: every archive's messages, then the
|
|
14
|
+
// root's. Blob ids are 32 raw bytes, written as lower-case hex wherever a store keys blobs by text.
|
|
15
|
+
|
|
16
|
+
/** The length-delimited fields of one protobuf message, by field number; undefined when the bytes are not protobuf. */
|
|
17
|
+
export function protoFields(buf: Uint8Array): Map<number, Uint8Array[]> | undefined {
|
|
18
|
+
const out = new Map<number, Uint8Array[]>()
|
|
19
|
+
let i = 0
|
|
20
|
+
const varint = (): number | undefined => {
|
|
21
|
+
let v = 0
|
|
22
|
+
for (let shift = 1, n = 0; n < 10; n++, shift *= 128) {
|
|
23
|
+
if (i >= buf.length) return undefined
|
|
24
|
+
const b = buf[i++]!
|
|
25
|
+
v += (b & 0x7f) * shift
|
|
26
|
+
if (!(b & 0x80)) return v
|
|
27
|
+
}
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
while (i < buf.length) {
|
|
31
|
+
const tag = varint()
|
|
32
|
+
if (tag === undefined || tag < 8) return undefined
|
|
33
|
+
const wire = tag % 8
|
|
34
|
+
if (wire === 0) {
|
|
35
|
+
if (varint() === undefined) return undefined
|
|
36
|
+
} else if (wire === 1) i += 8
|
|
37
|
+
else if (wire === 5) i += 4
|
|
38
|
+
else if (wire === 2) {
|
|
39
|
+
const n = varint()
|
|
40
|
+
if (n === undefined || i + n > buf.length) return undefined
|
|
41
|
+
const no = Math.floor(tag / 8)
|
|
42
|
+
out.set(no, [...(out.get(no) ?? []), buf.subarray(i, i + n)])
|
|
43
|
+
i += n
|
|
44
|
+
} else return undefined // groups (3, 4) are not used by these messages
|
|
45
|
+
if (i > buf.length) return undefined
|
|
46
|
+
}
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const hex = (b: Uint8Array) => Buffer.from(b).toString("hex")
|
|
51
|
+
const ids = (f: Uint8Array[] | undefined) => (f ?? []).filter((b) => b.length === 32).map(hex)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The blob ids of a conversation's messages, oldest first: each compaction's archived messages, then the
|
|
55
|
+
* current prompt's. Undefined when `root` is not a ConversationStateStructure that lists any message.
|
|
56
|
+
*/
|
|
57
|
+
export function messageOrder(root: Uint8Array, getBlob: (id: string) => Uint8Array | undefined): string[] | undefined {
|
|
58
|
+
const top = protoFields(root)
|
|
59
|
+
const prompt = ids(top?.get(1))
|
|
60
|
+
if (!top || !prompt.length) return undefined
|
|
61
|
+
const archives = [...new Set([...ids(top.get(13)), ...ids(top.get(11))])]
|
|
62
|
+
const order: string[] = []
|
|
63
|
+
for (const a of archives) {
|
|
64
|
+
const blob = getBlob(a)
|
|
65
|
+
order.push(...ids(blob ? protoFields(blob)?.get(1) : undefined))
|
|
66
|
+
}
|
|
67
|
+
order.push(...prompt)
|
|
68
|
+
return [...new Set(order)] // a message kept across a compaction is listed in both; it happened once
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The root of an IDE composer: composerData.conversationState is "~" and the root's bytes in base64. */
|
|
72
|
+
export function composerRoot(state: unknown): Uint8Array | undefined {
|
|
73
|
+
if (typeof state !== "string" || !state.startsWith("~") || state.length < 2) return undefined
|
|
74
|
+
const b = Buffer.from(state.slice(1), "base64")
|
|
75
|
+
return b.length ? new Uint8Array(b) : undefined
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A blob's bytes from a SQLite value, which is a BLOB; text values cannot hold protobuf intact. */
|
|
79
|
+
export const bytes = (v: unknown) => (v instanceof Uint8Array ? v : undefined)
|
|
80
|
+
|
|
81
|
+
/** What a tool-result part returned to the model: its result, else the text parts of experimental_content. */
|
|
82
|
+
export function toolResultText(p: Record<string, any>): string {
|
|
83
|
+
if (typeof p.result === "string") return p.result
|
|
84
|
+
if (p.result !== undefined && p.result !== null) return asText(p.result)
|
|
85
|
+
if (Array.isArray(p.experimental_content)) return p.experimental_content.map((c: any) => (c?.type === "text" ? String(c.text ?? "") : `[${String(c?.type ?? "part")}]`)).join("\n")
|
|
86
|
+
return ""
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether a tool message reports failure. The harness flags it on the message
|
|
91
|
+
* (providerOptions.cursor.highLevelToolCallResult.isError), not on the part; every tool message seen holds
|
|
92
|
+
* one result, so the message's flag is trusted only then.
|
|
93
|
+
*/
|
|
94
|
+
export const toolFailed = (m: Record<string, any>, p: Record<string, any>) =>
|
|
95
|
+
p.isError === true || (Array.isArray(m.content) && m.content.length === 1 && m.providerOptions?.cursor?.highLevelToolCallResult?.isError === true)
|