@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,361 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite"
|
|
2
|
+
import { randomBytes } from "node:crypto"
|
|
3
|
+
import { copyFileSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs"
|
|
4
|
+
import { dirname, extname, join } from "node:path"
|
|
5
|
+
import { toolLabel } from "../../brief.ts"
|
|
6
|
+
import { exists, gitInfo, isDir, isRunning, versionOf } from "../../fsutil.ts"
|
|
7
|
+
import type { Session, Turn } from "../../session/types.ts"
|
|
8
|
+
import { countTurns, firstUserText, unpaired } from "../../session/util.ts"
|
|
9
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
10
|
+
import { cline4Reader } from "./reader.ts"
|
|
11
|
+
import { LABEL, NOTE_KIND, PROCESSES, SESSIONS_TABLE, TESTED_VERSIONS, TOOL, continuedNote, manifestPath, messagesPath, sessionDir, sessionsDb } from "./records.ts"
|
|
12
|
+
|
|
13
|
+
// Continues a portable session as a new Cline 4.x SDK session, written the way Cline's own session importer
|
|
14
|
+
// (core/src/services/session-import) writes an imported Claude Code or Codex conversation: a `sessions` row in
|
|
15
|
+
// sessions.db, a manifest and a messages file, a completed root session whose metadata carries the title,
|
|
16
|
+
// sessionHistoryOrigin {mode: "import"} and continuedFrom (Cline's importer writes importedFrom, which makes Cline summarise on resume). `cline history` lists it and `cline --id <id>`
|
|
17
|
+
// resumes it with the earlier turns on screen; Cline Desktop shows it in its history.
|
|
18
|
+
//
|
|
19
|
+
// Messages are Cline's Anthropic-shaped MessageWithMetadata: one assistant message per model response (its
|
|
20
|
+
// readable reasoning as a thinking block without a signature, its text, its tool_use blocks), then one user
|
|
21
|
+
// message with the tool_result blocks, as Cline's importer pairs them; a call without a result is closed as
|
|
22
|
+
// an error. Tool names stay as they were, as the importer leaves them. A compaction is Cline's own: a
|
|
23
|
+
// compaction_summary message replaces the turns before it. The note saying where the session came from is a
|
|
24
|
+
// user message with displayRole "system", which the model reads and the panel does not show.
|
|
25
|
+
//
|
|
26
|
+
// sessions.db is written only while no Cline CLI, hub or Desktop process runs, after a copy of it is kept
|
|
27
|
+
// under ~/.brain/backups/cline-sdk/.
|
|
28
|
+
|
|
29
|
+
const IMAGE_TYPES: Record<string, string> = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" }
|
|
30
|
+
const IMAGE_LIMIT = 5 * 1024 * 1024
|
|
31
|
+
const NANO = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"
|
|
32
|
+
|
|
33
|
+
type Json = Record<string, any>
|
|
34
|
+
type Call = Extract<Turn, { kind: "tool_call" }>
|
|
35
|
+
type Result = Extract<Turn, { kind: "tool_result" }>
|
|
36
|
+
|
|
37
|
+
const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
|
|
38
|
+
|
|
39
|
+
function realCwd(cwd: string): string {
|
|
40
|
+
try {
|
|
41
|
+
return realpathSync(cwd)
|
|
42
|
+
} catch {
|
|
43
|
+
return cwd
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A session id as Cline makes one: `${Date.now()}_${nanoid(5)}`. */
|
|
48
|
+
export const sessionId = (now: Date) => `${now.getTime()}_${[...randomBytes(5)].map((b) => NANO[b & 63]).join("")}`
|
|
49
|
+
|
|
50
|
+
/** Call ids the providers Cline talks to accept (Anthropic's ^[a-zA-Z0-9_-]{1,64}$). */
|
|
51
|
+
const safeId = (id: string) => id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) || "call"
|
|
52
|
+
|
|
53
|
+
const objectInput = (input: unknown): Json => (input && typeof input === "object" && !Array.isArray(input) ? (input as Json) : { input: input ?? null })
|
|
54
|
+
|
|
55
|
+
/** The provider a model id belongs to, for the history list; Cline resumes with the person's own provider. */
|
|
56
|
+
function providerOf(model: string | undefined, tool: string): string {
|
|
57
|
+
if (!model) return tool
|
|
58
|
+
if (/claude|anthropic/i.test(model)) return "anthropic"
|
|
59
|
+
if (/gemini/i.test(model)) return "gemini"
|
|
60
|
+
if (/gpt|^o\d|codex|openai/i.test(model)) return "openai"
|
|
61
|
+
const slash = model.indexOf("/")
|
|
62
|
+
return slash > 0 ? model.slice(0, slash) : tool
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ClinePlan {
|
|
66
|
+
messages: Json[]
|
|
67
|
+
title: string
|
|
68
|
+
prompt?: string
|
|
69
|
+
lost: string[]
|
|
70
|
+
/** What a read-back must find. */
|
|
71
|
+
expect: { users: number; assistants: number; calls: number }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The messages of a new Cline session continuing `session`. Pure apart from reading attached images. */
|
|
75
|
+
export function planMessages(session: Session, opts: { now: Date }): ClinePlan {
|
|
76
|
+
const drops = new Map<string, number>()
|
|
77
|
+
const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
|
|
78
|
+
const label = toolLabel(session.header.tool)
|
|
79
|
+
|
|
80
|
+
// Cline's compaction replaces what came before it: start from the last summary.
|
|
81
|
+
const lastCompaction = session.turns.map((t) => t.kind).lastIndexOf("compaction")
|
|
82
|
+
const turns = lastCompaction >= 0 ? session.turns.slice(lastCompaction) : session.turns
|
|
83
|
+
if (lastCompaction > 0) drop("turns before the last compaction, replaced by its summary as Cline compacts", lastCompaction)
|
|
84
|
+
const droppedUsers = session.turns.slice(0, Math.max(0, lastCompaction)).filter((t) => t.kind === "user").length
|
|
85
|
+
|
|
86
|
+
const results = new Map<string, Result>()
|
|
87
|
+
for (const t of turns) if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
|
|
88
|
+
const ids = new Map<string, string>()
|
|
89
|
+
const used = new Set<string>()
|
|
90
|
+
let remapped = 0
|
|
91
|
+
const idFor = (id: string) => {
|
|
92
|
+
let v = ids.get(id)
|
|
93
|
+
if (v) return v
|
|
94
|
+
v = safeId(id)
|
|
95
|
+
for (let n = 2; used.has(v); n++) v = `${safeId(id)}_${n}`
|
|
96
|
+
ids.set(id, v)
|
|
97
|
+
used.add(v)
|
|
98
|
+
if (v !== id) remapped++
|
|
99
|
+
return v
|
|
100
|
+
}
|
|
101
|
+
const ts = (at?: string) => {
|
|
102
|
+
const t = at ? Date.parse(at) : Number.NaN
|
|
103
|
+
return Number.isFinite(t) ? { ts: t } : {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const expect = { users: 0, assistants: 0, calls: 0 }
|
|
107
|
+
const messages: Json[] = []
|
|
108
|
+
const note: Json = {
|
|
109
|
+
role: "user",
|
|
110
|
+
content: [{ type: "text", text: continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)) }],
|
|
111
|
+
metadata: { kind: NOTE_KIND, displayRole: "system", userRunSpan: 0 },
|
|
112
|
+
}
|
|
113
|
+
let cur: { msg: Json; calls: Call[] } | undefined
|
|
114
|
+
const flush = () => {
|
|
115
|
+
if (!cur) return
|
|
116
|
+
const { msg, calls } = cur
|
|
117
|
+
cur = undefined
|
|
118
|
+
if (!msg.content.length) return
|
|
119
|
+
messages.push(msg)
|
|
120
|
+
if (!calls.length) return
|
|
121
|
+
const blocks = calls.map((call) => {
|
|
122
|
+
const r = results.get(call.id)
|
|
123
|
+
if (!r) drop("tool calls without a result (closed as errors)")
|
|
124
|
+
const output = r ? (r.fullPath ? `${r.output}\n\n[Full output: ${r.fullPath}]` : r.output) : "No result was recorded for this call; the session moved on before it finished."
|
|
125
|
+
return { type: "tool_result", tool_use_id: idFor(call.id), name: call.name, content: output.trim() ? output : "(no output)", ...(r?.ok === false || !r ? { is_error: true } : {}) }
|
|
126
|
+
})
|
|
127
|
+
const last = calls.map((c) => results.get(c.id)?.at).filter(Boolean).at(-1)
|
|
128
|
+
messages.push({ role: "user", content: blocks, ...ts(last) })
|
|
129
|
+
}
|
|
130
|
+
const assistant = (at: string | undefined, model: string | undefined) => {
|
|
131
|
+
if (!cur) {
|
|
132
|
+
const id = model ?? session.header.models.at(-1)
|
|
133
|
+
cur = { msg: { role: "assistant", content: [], ...(id ? { modelInfo: { id, provider: providerOf(id, session.header.tool) } } : {}), ...ts(at) }, calls: [] }
|
|
134
|
+
}
|
|
135
|
+
return cur
|
|
136
|
+
}
|
|
137
|
+
const seen = new Set<string>()
|
|
138
|
+
|
|
139
|
+
for (const t of turns) {
|
|
140
|
+
switch (t.kind) {
|
|
141
|
+
case "user": {
|
|
142
|
+
flush()
|
|
143
|
+
const content: Json[] = []
|
|
144
|
+
let text = t.text
|
|
145
|
+
for (const a of t.attachments ?? []) {
|
|
146
|
+
if (!a.path) continue // bytes the reader could not keep; it already counted them
|
|
147
|
+
if (a.kind === "file") {
|
|
148
|
+
text += `\n\n[Attached file: ${a.path}]`
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
const mime = a.mime ?? IMAGE_TYPES[extname(a.path).toLowerCase()]
|
|
152
|
+
let size = Number.POSITIVE_INFINITY
|
|
153
|
+
try {
|
|
154
|
+
size = statSync(a.path).size
|
|
155
|
+
} catch {}
|
|
156
|
+
if (mime && Object.values(IMAGE_TYPES).includes(mime) && size <= IMAGE_LIMIT) content.push({ type: "image", data: readFileSync(a.path).toString("base64"), mediaType: mime })
|
|
157
|
+
else drop("images Cline cannot hold")
|
|
158
|
+
}
|
|
159
|
+
if (!text.trim() && !content.length) break
|
|
160
|
+
if (text.trim()) content.unshift({ type: "text", text })
|
|
161
|
+
messages.push({ role: "user", content, ...ts(t.at) })
|
|
162
|
+
if (text.trim()) expect.users++
|
|
163
|
+
break
|
|
164
|
+
}
|
|
165
|
+
case "assistant":
|
|
166
|
+
// Text after a call is a new response: it was written after the call's result came back.
|
|
167
|
+
if (cur?.calls.length) flush()
|
|
168
|
+
if (!t.text.trim()) break
|
|
169
|
+
assistant(t.at, t.model).msg.content.push({ type: "text", text: t.text })
|
|
170
|
+
expect.assistants++
|
|
171
|
+
break
|
|
172
|
+
case "reasoning":
|
|
173
|
+
if (cur?.calls.length) flush()
|
|
174
|
+
if (t.text.trim()) assistant(t.at, undefined).msg.content.push({ type: "thinking", thinking: t.text })
|
|
175
|
+
break
|
|
176
|
+
case "tool_call": {
|
|
177
|
+
if (seen.has(t.id)) {
|
|
178
|
+
drop("repeated tool call ids")
|
|
179
|
+
break
|
|
180
|
+
}
|
|
181
|
+
seen.add(t.id)
|
|
182
|
+
const a = assistant(t.at, undefined)
|
|
183
|
+
a.msg.content.push({ type: "tool_use", id: idFor(t.id), name: t.name, input: objectInput(t.input) })
|
|
184
|
+
a.calls.push(t)
|
|
185
|
+
expect.calls++
|
|
186
|
+
break
|
|
187
|
+
}
|
|
188
|
+
case "tool_result":
|
|
189
|
+
if (!seen.has(t.callId)) drop("tool results without a call")
|
|
190
|
+
else if (results.get(t.callId) !== t) drop("repeated tool results")
|
|
191
|
+
break
|
|
192
|
+
case "compaction":
|
|
193
|
+
flush()
|
|
194
|
+
// Cline's own compaction summary (compaction-shared.ts): off screen, for the model.
|
|
195
|
+
messages.push({
|
|
196
|
+
role: "user",
|
|
197
|
+
content: [{ type: "text", text: `Context summary:\n\n[Summary of the earlier conversation in ${label}]\n\n${t.summary}` }],
|
|
198
|
+
metadata: { kind: "compaction_summary", displayRole: "system", userRunSpan: droppedUsers, summary: t.summary, details: { readFiles: [], modifiedFiles: [] }, tokensBefore: 0, generatedAt: opts.now.getTime() },
|
|
199
|
+
...ts(t.at),
|
|
200
|
+
})
|
|
201
|
+
break
|
|
202
|
+
case "file_edit":
|
|
203
|
+
if (!t.callId || !seen.has(t.callId)) drop("file edits outside a tool call")
|
|
204
|
+
break
|
|
205
|
+
case "subagent":
|
|
206
|
+
drop("subagent sessions (their results stay in the parent's tool results)")
|
|
207
|
+
break
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
flush()
|
|
211
|
+
if (!messages.length) throw new Error("nothing Cline can show: the session has no messages or tool calls")
|
|
212
|
+
// The note opens the model's context, after a compaction summary when there is one.
|
|
213
|
+
messages.splice(lastCompaction >= 0 ? 1 : 0, 0, note)
|
|
214
|
+
|
|
215
|
+
const firstText = firstUserText(session)?.replace(/\s+/g, " ").trim()
|
|
216
|
+
const title = `${session.header.title ?? firstText?.slice(0, 60) ?? "Session"} (from ${label})`.slice(0, 120)
|
|
217
|
+
const lost = [...drops].map(([what, n]) => `${what} (${n})`)
|
|
218
|
+
if (remapped) lost.push(`tool call ids, changed to fit Cline (${remapped})`)
|
|
219
|
+
return { messages, title, ...(firstText ? { prompt: firstText.slice(0, 2000) } : {}), lost, expect }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** A filesystem-safe timestamp for a backup folder. */
|
|
223
|
+
const stamp = (d: Date) => d.toISOString().replace(/[:.]/g, "-")
|
|
224
|
+
|
|
225
|
+
/** Copies sessions.db and its -wal and -shm files; returns the folder. */
|
|
226
|
+
export function backupDb(home: string, path: string, now: Date): string {
|
|
227
|
+
const dir = join(home, ".brain", "backups", TOOL, stamp(now))
|
|
228
|
+
mkdirSync(dir, { recursive: true })
|
|
229
|
+
for (const side of ["", "-wal", "-shm"]) if (exists(path + side)) copyFileSync(path + side, join(dir, `sessions.db${side}`))
|
|
230
|
+
return dir
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const semver = (v: string | undefined) => v?.match(/(\d+\.\d+\.\d+)/)?.[1]
|
|
234
|
+
|
|
235
|
+
/** The Cline 4 writer; `version` and `running` are for tests, which must not depend on what is installed or open. */
|
|
236
|
+
export function cline4Writer(opts: { version?: string; running?: () => boolean } = {}): Writer {
|
|
237
|
+
const installed = () => opts.version ?? semver(versionOf("cline"))
|
|
238
|
+
const running = opts.running ?? (() => PROCESSES.some(isRunning))
|
|
239
|
+
return {
|
|
240
|
+
tool: TOOL,
|
|
241
|
+
label: LABEL,
|
|
242
|
+
way: "native",
|
|
243
|
+
|
|
244
|
+
async ready(w: WriteOptions): Promise<Readiness> {
|
|
245
|
+
const v = installed()
|
|
246
|
+
if (!v) return { ok: false, why: "the Cline CLI is not installed (no `cline` on the PATH)" }
|
|
247
|
+
if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Cline CLI ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
|
|
248
|
+
if (running()) return { ok: false, why: "Cline is running (the CLI, its hub or Cline Desktop); quit it first, since it keeps sessions.db open" }
|
|
249
|
+
if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder` }
|
|
250
|
+
return { ok: true }
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
async write(session: Session, w: WriteOptions): Promise<WriteResult> {
|
|
254
|
+
const now = new Date()
|
|
255
|
+
const cwd = realCwd(w.cwd)
|
|
256
|
+
const id = sessionId(now)
|
|
257
|
+
const plan = planMessages(session, { now })
|
|
258
|
+
const db = sessionsDb(w.home)
|
|
259
|
+
const files = { manifest: manifestPath(w.home, id), messages: messagesPath(w.home, id) }
|
|
260
|
+
const written = [db, files.manifest, files.messages]
|
|
261
|
+
if (!w.dryRun) {
|
|
262
|
+
if (running()) throw new Error("Cline is running; quit it before writing its sessions database")
|
|
263
|
+
if (exists(db)) written.push(backupDb(w.home, db, now))
|
|
264
|
+
const started = new Date(session.header.startedAt && Number.isFinite(Date.parse(session.header.startedAt)) ? Date.parse(session.header.startedAt) : now.getTime()).toISOString()
|
|
265
|
+
const ended = now.toISOString()
|
|
266
|
+
const model = session.header.models.at(-1) ?? "unknown"
|
|
267
|
+
const provider = providerOf(session.header.models.at(-1), session.header.tool)
|
|
268
|
+
const branch = gitInfo(cwd, w.home).gitBranch ?? session.header.project.gitBranch
|
|
269
|
+
const metadata = {
|
|
270
|
+
title: plan.title,
|
|
271
|
+
...(branch ? { git: { branch } } : {}),
|
|
272
|
+
sessionHistoryOrigin: { mode: "import", trigger: session.header.tool },
|
|
273
|
+
// Not `importedFrom`: Cline 3.0.74 summarises a session that has it with the model the first time it resumes
|
|
274
|
+
// it (the wire harness saw the follow-up sent over a model-written summary), which would drop the turns.
|
|
275
|
+
continuedFrom: { tool: session.header.tool, sourceSessionId: session.header.sourceId, sourcePath: session.header.sourcePath ?? "", importedAt: ended },
|
|
276
|
+
}
|
|
277
|
+
const row = { source: "cli", status: "completed", interactive: true, provider, model, enableTools: true, enableSpawn: false, enableTeams: false }
|
|
278
|
+
mkdirSync(sessionDir(w.home, id), { recursive: true })
|
|
279
|
+
writeFileSync(
|
|
280
|
+
files.messages,
|
|
281
|
+
`${JSON.stringify({ version: 1, updated_at: ended, agent: "lead", sessionId: id, origin: { source: row.source, mode: "import", sessionId: id, trigger: session.header.tool }, messages: plan.messages }, null, 2)}\n`,
|
|
282
|
+
)
|
|
283
|
+
writeFileSync(
|
|
284
|
+
files.manifest,
|
|
285
|
+
`${JSON.stringify(
|
|
286
|
+
{
|
|
287
|
+
version: 1,
|
|
288
|
+
session_id: id,
|
|
289
|
+
source: row.source,
|
|
290
|
+
pid: 0,
|
|
291
|
+
started_at: started,
|
|
292
|
+
ended_at: ended,
|
|
293
|
+
exit_code: 0,
|
|
294
|
+
status: row.status,
|
|
295
|
+
interactive: row.interactive,
|
|
296
|
+
provider,
|
|
297
|
+
model,
|
|
298
|
+
cwd,
|
|
299
|
+
workspace_root: cwd,
|
|
300
|
+
enable_tools: row.enableTools,
|
|
301
|
+
enable_spawn: row.enableSpawn,
|
|
302
|
+
enable_teams: row.enableTeams,
|
|
303
|
+
...(plan.prompt ? { prompt: plan.prompt } : {}),
|
|
304
|
+
metadata,
|
|
305
|
+
messages_path: files.messages,
|
|
306
|
+
},
|
|
307
|
+
null,
|
|
308
|
+
2,
|
|
309
|
+
)}\n`,
|
|
310
|
+
)
|
|
311
|
+
mkdirSync(dirname(db), { recursive: true })
|
|
312
|
+
const d = new Database(db)
|
|
313
|
+
try {
|
|
314
|
+
d.exec("PRAGMA journal_mode = WAL")
|
|
315
|
+
d.exec("PRAGMA busy_timeout = 5000")
|
|
316
|
+
d.exec(SESSIONS_TABLE)
|
|
317
|
+
// A plain INSERT, unlike Cline's INSERT OR REPLACE: a new session never replaces a row.
|
|
318
|
+
d.query(
|
|
319
|
+
`INSERT INTO sessions (session_id, source, pid, started_at, ended_at, exit_code, status, status_lock, interactive, provider, model, cwd, workspace_root, team_name,
|
|
320
|
+
enable_tools, enable_spawn, enable_teams, parent_session_id, parent_agent_id, agent_id, conversation_id, is_subagent, prompt, metadata_json, transcript_path, hook_path,
|
|
321
|
+
messages_path, updated_at) VALUES (?, ?, 0, ?, ?, 0, ?, 0, 1, ?, ?, ?, ?, NULL, 1, 0, 0, NULL, NULL, NULL, NULL, 0, ?, ?, '', '', ?, ?)`,
|
|
322
|
+
).run(id, row.source, started, ended, row.status, provider, model, cwd, cwd, plan.prompt ?? null, JSON.stringify(metadata), files.messages, ended)
|
|
323
|
+
} finally {
|
|
324
|
+
d.close()
|
|
325
|
+
}
|
|
326
|
+
let problem: string | undefined
|
|
327
|
+
try {
|
|
328
|
+
const back = await cline4Reader.read({ tool: TOOL, sourceId: id, path: files.messages }, w.home)
|
|
329
|
+
const n = countTurns(back)
|
|
330
|
+
const pairs = unpaired(back)
|
|
331
|
+
if (n.user !== plan.expect.users || n.assistant !== plan.expect.assistants || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length)
|
|
332
|
+
problem = `users ${n.user}/${plan.expect.users}, replies ${n.assistant}/${plan.expect.assistants}, tool calls ${n.tool_call}/${plan.expect.calls}, unpaired ${pairs.calls.length + pairs.results.length}`
|
|
333
|
+
} catch (e) {
|
|
334
|
+
problem = e instanceof Error ? e.message : String(e)
|
|
335
|
+
}
|
|
336
|
+
if (problem) {
|
|
337
|
+
const d2 = new Database(db)
|
|
338
|
+
try {
|
|
339
|
+
d2.query("DELETE FROM sessions WHERE session_id = ?").run(id)
|
|
340
|
+
} finally {
|
|
341
|
+
d2.close()
|
|
342
|
+
}
|
|
343
|
+
rmSync(sessionDir(w.home, id), { recursive: true, force: true })
|
|
344
|
+
throw new Error(`the new Cline session did not read back as written (${problem}); it was removed`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return {
|
|
348
|
+
tool: TOOL,
|
|
349
|
+
way: "native",
|
|
350
|
+
sessionId: id,
|
|
351
|
+
written,
|
|
352
|
+
open: `cd ${shellQuote(cwd)} && cline --id ${id}`,
|
|
353
|
+
note: `Cline Desktop lists it in its history as "${plan.title}".`,
|
|
354
|
+
lost: plan.lost,
|
|
355
|
+
dryRun: !!w.dryRun,
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export const cline4NativeWriter = cline4Writer()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { codebuffReader } from "./reader.ts"
|
|
3
|
+
import { codebuffNativeWriter } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// Codebuff and Freebuff (one CLI under two names, sharing ~/.config/manicode): reads their chats, and continues
|
|
6
|
+
// any session as a new chat that `freebuff --continue <id>` (or `codebuff --continue <id>`) opens.
|
|
7
|
+
export const readers: Reader[] = [codebuffReader]
|
|
8
|
+
export const writers: Writer[] = [codebuffNativeWriter]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs"
|
|
2
|
+
import { cpus } from "node:os"
|
|
3
|
+
import { basename, dirname, join, relative } from "node:path"
|
|
4
|
+
|
|
5
|
+
// The project snapshot a Freebuff chat carries (RunState.sessionState.fileContext), made the way the SDK makes
|
|
6
|
+
// one for a new chat (sdk/src/run-state.ts initialSessionState, common/src/project-file-tree.ts): the folder
|
|
7
|
+
// tree as the system prompt lists it, the project's AGENTS.md / CLAUDE.md, ~/.AGENTS.md, a git summary and
|
|
8
|
+
// the machine. A continued chat keeps the snapshot it started with, so this is what Freebuff itself would
|
|
9
|
+
// have stored had the chat begun here today. Differences from Freebuff's own, kept small on purpose:
|
|
10
|
+
// - ignore rules come from git (`git ls-files --ignored`) where the folder is a git work tree, which covers
|
|
11
|
+
// .gitignore but not .codebuffignore / .manicodeignore; outside git only the default ignores apply;
|
|
12
|
+
// - no symbol index (fileTokenScores is empty): Freebuff skips it for its default agent too;
|
|
13
|
+
// - skills are left for the skill tool to find on disk, and the repository's commit statistics and
|
|
14
|
+
// GitHub visibility (Freebuff asks `gh`) are left out.
|
|
15
|
+
|
|
16
|
+
export interface FileNode {
|
|
17
|
+
name: string
|
|
18
|
+
type: "file" | "directory"
|
|
19
|
+
filePath: string
|
|
20
|
+
children?: FileNode[]
|
|
21
|
+
lastReadTime?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** common/src/constants/paths.ts DEFAULT_IGNORED_PATHS, as gitignore patterns on a name. */
|
|
25
|
+
const DEFAULT_IGNORED = [".git", ".env", ".env.*", "*.min.*", "node_modules", "venv", "virtualenv", ".venv", ".virtualenv", "__pycache__", "*.egg-info/", "*.pyc", ".DS_Store", ".pytest_cache", ".mypy_cache", ".ruff_cache", ".next", "package-lock.json", "bun.lockb"]
|
|
26
|
+
const globRe = (p: string) => new RegExp(`^${p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")}$`)
|
|
27
|
+
const IGNORED = DEFAULT_IGNORED.map((p) => ({ re: globRe(p.replace(/\/$/, "")), dirOnly: p.endsWith("/") }))
|
|
28
|
+
const defaultIgnored = (name: string, dir: boolean, shallow: boolean) => (shallow && name.startsWith(".")) || IGNORED.some((i) => (!i.dirOnly || dir) && i.re.test(name))
|
|
29
|
+
|
|
30
|
+
export const DEFAULT_MAX_FILES = 10_000
|
|
31
|
+
const SHALLOW = { depth: 3, files: 2_000, dirs: 500 }
|
|
32
|
+
|
|
33
|
+
const git = (cwd: string, args: string[]) => {
|
|
34
|
+
try {
|
|
35
|
+
const p = Bun.spawnSync(["git", ...args], { cwd, stdout: "pipe", stderr: "ignore", timeout: 20_000 })
|
|
36
|
+
return p.exitCode === 0 ? p.stdout.toString() : undefined
|
|
37
|
+
} catch {
|
|
38
|
+
return undefined
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True when `dir` is `home` or holds it: Freebuff then scans only a few levels (isValidProjectRoot). */
|
|
43
|
+
const holdsHome = (dir: string, home: string) => !relative(dir, home).startsWith("..")
|
|
44
|
+
|
|
45
|
+
/** The folder tree, breadth first and capped, as getProjectFileTree builds it. */
|
|
46
|
+
export function fileTree(cwd: string, home: string): { tree: FileNode[]; paths: string[] } {
|
|
47
|
+
const shallow = holdsHome(cwd, home)
|
|
48
|
+
const maxFiles = shallow ? SHALLOW.files : DEFAULT_MAX_FILES
|
|
49
|
+
const maxDepth = shallow ? SHALLOW.depth : Number.POSITIVE_INFINITY
|
|
50
|
+
const maxDirs = shallow ? SHALLOW.dirs : Number.POSITIVE_INFINITY
|
|
51
|
+
// Untracked paths git ignores (directories end in "/"); git applies every .gitignore on the way down.
|
|
52
|
+
const ignored = new Set((git(cwd, ["ls-files", "-z", "--others", "--ignored", "--exclude-standard", "--directory"]) ?? "").split("\0").filter(Boolean))
|
|
53
|
+
const root: FileNode = { name: basename(cwd), type: "directory", filePath: "", children: [] }
|
|
54
|
+
const queue: Array<{ node: FileNode; full: string; depth: number }> = [{ node: root, full: cwd, depth: 0 }]
|
|
55
|
+
const seen = new Set<string>()
|
|
56
|
+
const paths: string[] = []
|
|
57
|
+
let dirs = 0
|
|
58
|
+
while (queue.length && paths.length < maxFiles && dirs < maxDirs) {
|
|
59
|
+
const { node, full, depth } = queue.shift()!
|
|
60
|
+
dirs++
|
|
61
|
+
try {
|
|
62
|
+
const real = realpathSync(full)
|
|
63
|
+
if (seen.has(real)) continue
|
|
64
|
+
seen.add(real)
|
|
65
|
+
} catch {
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
let names: string[]
|
|
69
|
+
try {
|
|
70
|
+
names = readdirSync(full).sort()
|
|
71
|
+
} catch {
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
for (const name of names) {
|
|
75
|
+
if (paths.length >= maxFiles) break
|
|
76
|
+
const path = join(full, name)
|
|
77
|
+
const rel = relative(cwd, path).split("\\").join("/")
|
|
78
|
+
let st: ReturnType<typeof statSync>
|
|
79
|
+
try {
|
|
80
|
+
st = statSync(path)
|
|
81
|
+
} catch {
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
const dir = st.isDirectory()
|
|
85
|
+
if (defaultIgnored(name, dir, shallow) || ignored.has(dir ? `${rel}/` : rel)) continue
|
|
86
|
+
if (dir) {
|
|
87
|
+
const child: FileNode = { name, type: "directory", children: [], filePath: rel }
|
|
88
|
+
node.children!.push(child)
|
|
89
|
+
if (depth + 1 < maxDepth) queue.push({ node: child, full: path, depth: depth + 1 })
|
|
90
|
+
} else {
|
|
91
|
+
node.children!.push({ name, type: "file", lastReadTime: st.atimeMs, filePath: rel })
|
|
92
|
+
paths.push(rel)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { tree: root.children!, paths }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const TEST_DIRS = new Set(["__tests__", "__specs__", "test", "tests", "spec", "specs"])
|
|
100
|
+
/** sdk/src/run-state.ts isTestFilePath. */
|
|
101
|
+
export function isTestFilePath(filePath: string): boolean {
|
|
102
|
+
const segments = filePath.split("/")
|
|
103
|
+
const name = segments.pop() ?? ""
|
|
104
|
+
if (segments.some((s) => TEST_DIRS.has(s.toLowerCase()))) return true
|
|
105
|
+
const lower = name.toLowerCase()
|
|
106
|
+
return (
|
|
107
|
+
/\.(?:test|tests|spec|specs|cy)\./.test(lower) ||
|
|
108
|
+
/^(?:test|spec)_.+\.[^.]+$/.test(lower) ||
|
|
109
|
+
/_(?:test|tests|spec|specs)\.[^.]+$/.test(lower) ||
|
|
110
|
+
/^(?:test|tests|spec|specs)\.[^.]+$/.test(lower) ||
|
|
111
|
+
/(?:Test|Tests|TestCase|Spec)\.[^.]+$/.test(name)
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const KNOWLEDGE_NAMES = ["agents.md", "claude.md"]
|
|
116
|
+
const isKnowledgeFile = (p: string) => {
|
|
117
|
+
const name = basename(p).toLowerCase()
|
|
118
|
+
return KNOWLEDGE_NAMES.includes(name) || name.endsWith(".knowledge.md")
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** One instructions file per folder, AGENTS.md before CLAUDE.md (selectKnowledgeFilePaths), with its text. */
|
|
122
|
+
export function knowledgeFiles(cwd: string, paths: string[]): Record<string, string> {
|
|
123
|
+
const byDir = new Map<string, string[]>()
|
|
124
|
+
for (const p of paths.filter(isKnowledgeFile)) byDir.set(dirname(p), [...(byDir.get(dirname(p)) ?? []), p])
|
|
125
|
+
const out: Record<string, string> = {}
|
|
126
|
+
for (const files of byDir.values()) {
|
|
127
|
+
const pick = KNOWLEDGE_NAMES.map((n) => files.find((f) => f.toLowerCase().endsWith(n))).find(Boolean)
|
|
128
|
+
if (!pick) continue
|
|
129
|
+
try {
|
|
130
|
+
out[pick] = readFileSync(join(cwd, pick), "utf8")
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
133
|
+
return out
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** ~/.AGENTS.md, else ~/.CLAUDE.md, matched without case (loadUserKnowledgeFiles). */
|
|
137
|
+
export function userKnowledgeFiles(home: string): Record<string, string> {
|
|
138
|
+
let names: string[]
|
|
139
|
+
try {
|
|
140
|
+
names = readdirSync(home)
|
|
141
|
+
} catch {
|
|
142
|
+
return {}
|
|
143
|
+
}
|
|
144
|
+
for (const want of KNOWLEDGE_NAMES) {
|
|
145
|
+
const name = names.find((n) => n.startsWith(".") && n.slice(1).toLowerCase() === want)
|
|
146
|
+
if (!name) continue
|
|
147
|
+
try {
|
|
148
|
+
return { [`~/${name}`]: readFileSync(join(home, name), "utf8") }
|
|
149
|
+
} catch {}
|
|
150
|
+
}
|
|
151
|
+
return {}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const CHROME: Record<string, string[]> = {
|
|
155
|
+
darwin: ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta", "/Applications/Google Chrome Dev.app/Contents/MacOS/Google Chrome Dev", "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", "/Applications/Chromium.app/Contents/MacOS/Chromium"],
|
|
156
|
+
linux: ["/usr/bin/google-chrome", "/usr/bin/google-chrome-stable", "/usr/bin/google-chrome-beta", "/usr/bin/google-chrome-unstable", "/usr/bin/chromium", "/usr/bin/chromium-browser", "/snap/bin/chromium"],
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The git summary the system prompt prints (getGitChanges), without commit statistics or visibility. */
|
|
160
|
+
function gitChanges(cwd: string, counts: { fileCount: number; fileCountIsLowerBound: boolean; testFileCount: number }) {
|
|
161
|
+
const lists = [["diff", "--name-only", "--"], ["diff", "--cached", "--name-only", "--"], ["ls-files", "--others", "--exclude-standard"]].map((a) => git(cwd, a))
|
|
162
|
+
const changed = [...new Set(lists.flatMap((o) => (o ?? "").split("\n").map((l) => l.trim()).filter(Boolean)))].sort()
|
|
163
|
+
const count = Number.parseInt(git(cwd, ["rev-list", "--count", "HEAD"]) ?? "", 10)
|
|
164
|
+
const shallow = git(cwd, ["rev-parse", "--is-shallow-repository"])?.trim()
|
|
165
|
+
return {
|
|
166
|
+
gitAvailable: lists.some((o) => o !== undefined),
|
|
167
|
+
...(git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"])?.trim() ? { branch: git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"])!.trim() } : {}),
|
|
168
|
+
changedFiles: changed.slice(0, 25),
|
|
169
|
+
changedFileCount: changed.length,
|
|
170
|
+
changedFileScanTruncated: false,
|
|
171
|
+
repositoryVisibility: "unknown" as const,
|
|
172
|
+
...(Number.isFinite(count) ? { commitCount: count } : {}),
|
|
173
|
+
...(shallow ? { historyIsShallow: shallow === "true" } : {}),
|
|
174
|
+
...counts,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The fileContext of a new chat in `cwd`, in the SDK's key order. */
|
|
179
|
+
export function fileContext(cwd: string, home: string) {
|
|
180
|
+
const { tree, paths } = fileTree(cwd, home)
|
|
181
|
+
const counts = { fileCount: paths.length, fileCountIsLowerBound: paths.length >= DEFAULT_MAX_FILES, testFileCount: paths.filter(isTestFilePath).length }
|
|
182
|
+
return {
|
|
183
|
+
projectRoot: cwd,
|
|
184
|
+
cwd,
|
|
185
|
+
fileTree: tree,
|
|
186
|
+
fileTokenScores: {},
|
|
187
|
+
tokenCallers: {},
|
|
188
|
+
knowledgeFiles: knowledgeFiles(cwd, paths),
|
|
189
|
+
userKnowledgeFiles: userKnowledgeFiles(home),
|
|
190
|
+
agentTemplates: {},
|
|
191
|
+
customToolDefinitions: {},
|
|
192
|
+
skills: {},
|
|
193
|
+
includeHomeSkills: false,
|
|
194
|
+
gitChanges: gitChanges(cwd, counts),
|
|
195
|
+
changesSinceLastChat: {},
|
|
196
|
+
shellConfigFiles: {},
|
|
197
|
+
systemInfo: {
|
|
198
|
+
platform: process.platform,
|
|
199
|
+
shell: "bash",
|
|
200
|
+
nodeVersion: process.version,
|
|
201
|
+
arch: process.arch,
|
|
202
|
+
homedir: home,
|
|
203
|
+
cpus: cpus().length,
|
|
204
|
+
chromeAvailable: (CHROME[process.platform] ?? []).some((p) => existsSync(p)),
|
|
205
|
+
},
|
|
206
|
+
}
|
|
207
|
+
}
|