@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,215 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { resolve } from "node:path"
|
|
3
|
+
import type { SessionHeader, Turn } from "../../session/types.ts"
|
|
4
|
+
import { asText, clip, iso, noteModel, toolKindOf, type lossCounter } from "../../session/util.ts"
|
|
5
|
+
import { CONTINUED_NOTE, idMaker, type PartList, resultText, snapshotText } from "../gemini-cli/records.ts"
|
|
6
|
+
|
|
7
|
+
// Facts about iFlow CLI's session records, shared by its reader and writer. iFlow CLI is a Gemini CLI fork
|
|
8
|
+
// that records sessions in Claude Code's shape. Checked against its last release, 0.5.19 (bundle/iflow.js:
|
|
9
|
+
// the session message builder, the JSONL storage, and the resume path that turns records back into the
|
|
10
|
+
// screen's history and the model's).
|
|
11
|
+
//
|
|
12
|
+
// A session is ~/.iflow/projects/<project name>/session-<uuid>.jsonl (IFLOW_HOME moves ~/.iflow), one record
|
|
13
|
+
// per line, linked by uuid/parentUuid. The loader sorts records by timestamp and starts from the last
|
|
14
|
+
// compression record. type "user" holds the person's text (a string, or text and image blocks), a tool
|
|
15
|
+
// result (a tool_result block whose content is the tool's response object, plus toolUseResult), a
|
|
16
|
+
// compression summary (isCompactSummary) or a hidden note (isMeta); type "assistant" holds a Claude-style
|
|
17
|
+
// message whose content is text and tool_use blocks. iFlow keeps no reasoning in its records.
|
|
18
|
+
|
|
19
|
+
export const TOOL = "iflow-cli"
|
|
20
|
+
export const LABEL = "iFlow CLI"
|
|
21
|
+
|
|
22
|
+
/** What iFlow CLI writes in every record's `version` (process.env.IFLOW_CLI_VERSION, which it never sets). */
|
|
23
|
+
export const RECORD_VERSION = "1.0.0"
|
|
24
|
+
|
|
25
|
+
/** iFlow CLI's project folder name (ProjectNameGenerator.fromPath). */
|
|
26
|
+
export function projectName(cwd: string): string {
|
|
27
|
+
let n = resolve(cwd)
|
|
28
|
+
.replace(/^\//, "")
|
|
29
|
+
.replace(/\\/g, "-")
|
|
30
|
+
.replace(/\//g, "-")
|
|
31
|
+
.replace(/:/g, "-")
|
|
32
|
+
.replace(/\s+/g, "-")
|
|
33
|
+
.replace(/[^\w\-_.]/g, "-")
|
|
34
|
+
if (!n.startsWith("-")) n = `-${n}`
|
|
35
|
+
n = n.replace(/-+/g, "-")
|
|
36
|
+
return n || "-unnamed-project"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type Block =
|
|
40
|
+
| { type: "text"; text: string }
|
|
41
|
+
| { type: "image"; source?: { type?: string; media_type?: string; data?: string } }
|
|
42
|
+
| { type: "tool_use"; id: string; name: string; input: unknown }
|
|
43
|
+
| { type: "tool_result"; tool_use_id: string; content: unknown }
|
|
44
|
+
| { type: "thinking"; thinking?: string }
|
|
45
|
+
|
|
46
|
+
export interface IflowRecord {
|
|
47
|
+
uuid: string
|
|
48
|
+
parentUuid: string | null
|
|
49
|
+
sessionId: string
|
|
50
|
+
timestamp: string
|
|
51
|
+
type: "user" | "assistant"
|
|
52
|
+
isSidechain?: boolean
|
|
53
|
+
userType?: string
|
|
54
|
+
message: { role: string; content: string | Block[]; id?: string; type?: string; model?: string; stop_reason?: null | string; stop_sequence?: null | string; usage?: { input_tokens?: number; output_tokens?: number } }
|
|
55
|
+
cwd?: string
|
|
56
|
+
gitBranch?: string | null
|
|
57
|
+
version?: string
|
|
58
|
+
toolUseResult?: { toolName?: string; status?: string; timestamp?: number }
|
|
59
|
+
isCompactSummary?: boolean
|
|
60
|
+
compressionInfo?: { originalTokenCount?: number; newTokenCount?: number; summary?: string }
|
|
61
|
+
isMeta?: boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The note that ends the first message of a session brain continued, with the blank line before it. */
|
|
65
|
+
const NOTE_SUFFIX = /(?:^|\n\n)(\[This session continues [^\n]*\])$/
|
|
66
|
+
|
|
67
|
+
/** Text without brain's note, and the source session it names. */
|
|
68
|
+
export function stripNote(text: string): { text: string; source?: string } {
|
|
69
|
+
const m = text.match(NOTE_SUFFIX)
|
|
70
|
+
const source = m?.[1]!.match(CONTINUED_NOTE)?.[1]
|
|
71
|
+
return source ? { text: text.slice(0, m!.index), source } : { text }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isObj = (v: unknown): v is Record<string, any> => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
75
|
+
|
|
76
|
+
/** A session file's records in the order iFlow loads them: by timestamp, file order breaking ties. */
|
|
77
|
+
export function readRecords(path: string): { records: IflowRecord[]; bad: number } {
|
|
78
|
+
let text: string
|
|
79
|
+
try {
|
|
80
|
+
text = readFileSync(path, "utf8")
|
|
81
|
+
} catch {
|
|
82
|
+
return { records: [], bad: 0 }
|
|
83
|
+
}
|
|
84
|
+
const rows: Array<{ r: IflowRecord; i: number }> = []
|
|
85
|
+
let bad = 0
|
|
86
|
+
text.split("\n").forEach((line, i) => {
|
|
87
|
+
if (!line.trim()) return
|
|
88
|
+
try {
|
|
89
|
+
const r = JSON.parse(line)
|
|
90
|
+
if (isObj(r) && (r.type === "user" || r.type === "assistant") && isObj(r.message)) rows.push({ r: r as IflowRecord, i })
|
|
91
|
+
else bad++
|
|
92
|
+
} catch {
|
|
93
|
+
bad++
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
const time = (r: IflowRecord) => {
|
|
97
|
+
const t = Date.parse(r.timestamp)
|
|
98
|
+
return Number.isFinite(t) ? t : 0
|
|
99
|
+
}
|
|
100
|
+
rows.sort((a, b) => time(a.r) - time(b.r) || a.i - b.i)
|
|
101
|
+
return { records: rows.map((x) => x.r), bad }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** iFlow's own names for tools whose kind toolKindOf cannot tell from the name. */
|
|
105
|
+
const KIND: Record<string, Extract<Turn, { kind: "tool_call" }>["toolKind"]> = { replace: "edit" }
|
|
106
|
+
|
|
107
|
+
/** A tool result's text and whether it failed: the tool's response object (responseParts), or plain text. */
|
|
108
|
+
function resultOf(content: unknown, status: string | undefined): { text: string; ok: boolean } {
|
|
109
|
+
const failed = status !== undefined && status !== "success"
|
|
110
|
+
if (isObj(content)) {
|
|
111
|
+
if (content.responseParts !== undefined) {
|
|
112
|
+
const r = resultText(content.responseParts as PartList)
|
|
113
|
+
const error = content.error ? asText(isObj(content.error) ? content.error.message ?? content.error : content.error) : ""
|
|
114
|
+
return { text: r.text || error || (typeof content.resultDisplay === "string" ? content.resultDisplay : ""), ok: r.ok && !error && !failed }
|
|
115
|
+
}
|
|
116
|
+
if (typeof content.resultDisplay === "string") return { text: content.resultDisplay, ok: !failed }
|
|
117
|
+
}
|
|
118
|
+
return { text: asText(content), ok: !failed }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The portable turns of a session's records; `h` gets its models, version and source note. */
|
|
122
|
+
export function recordsToTurns(records: IflowRecord[], h: SessionHeader, lost: ReturnType<typeof lossCounter>): Turn[] {
|
|
123
|
+
const turns: Turn[] = []
|
|
124
|
+
const newId = idMaker()
|
|
125
|
+
// iFlow's own call ids, to the turn ids given to them, while a call waits for its result.
|
|
126
|
+
const waiting = new Map<string, Extract<Turn, { kind: "tool_call" }>>()
|
|
127
|
+
for (const r of records) {
|
|
128
|
+
const at = iso(r.timestamp)
|
|
129
|
+
const content = r.message.content
|
|
130
|
+
const blocks: Block[] = typeof content === "string" ? [{ type: "text", text: content }] : Array.isArray(content) ? content : []
|
|
131
|
+
if (r.version && r.version !== RECORD_VERSION && !h.toolVersion) h.toolVersion = r.version
|
|
132
|
+
if (r.isSidechain) {
|
|
133
|
+
lost.add("subagent records")
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
if (r.type === "assistant") {
|
|
137
|
+
noteModel(h, r.message.model)
|
|
138
|
+
let text = ""
|
|
139
|
+
const flush = () => {
|
|
140
|
+
if (text.trim()) turns.push({ kind: "assistant", ...(at ? { at } : {}), text, ...(r.message.model ? { model: r.message.model } : {}) })
|
|
141
|
+
text = ""
|
|
142
|
+
}
|
|
143
|
+
for (const b of blocks) {
|
|
144
|
+
if (b.type === "text") text += text ? `\n${b.text}` : b.text
|
|
145
|
+
else if (b.type === "thinking") {
|
|
146
|
+
flush()
|
|
147
|
+
if (b.thinking?.trim()) turns.push({ kind: "reasoning", ...(at ? { at } : {}), text: b.thinking })
|
|
148
|
+
} else if (b.type === "tool_use") {
|
|
149
|
+
flush()
|
|
150
|
+
const call = { kind: "tool_call" as const, ...(at ? { at } : {}), id: newId(b.id), name: b.name, toolKind: KIND[b.name] ?? toolKindOf(b.name), input: b.input ?? {} }
|
|
151
|
+
waiting.set(b.id, call)
|
|
152
|
+
turns.push(call)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
flush()
|
|
156
|
+
const u = r.message.usage
|
|
157
|
+
const last = [...turns].reverse().find((t) => t.kind === "assistant") as Extract<Turn, { kind: "assistant" }> | undefined
|
|
158
|
+
if (u && last && last.at === at && (u.input_tokens || u.output_tokens)) last.usage = { input: u.input_tokens ?? 0, output: u.output_tokens ?? 0 }
|
|
159
|
+
continue
|
|
160
|
+
}
|
|
161
|
+
if (r.isCompactSummary) {
|
|
162
|
+
const text = r.compressionInfo?.summary ?? stripNote(blocks.map((b) => (b.type === "text" ? b.text : "")).join("")).text
|
|
163
|
+
const note = stripNote(typeof content === "string" ? content : "")
|
|
164
|
+
if (note.source) h.parent = { id: note.source, kind: "continued" }
|
|
165
|
+
turns.push({ kind: "compaction", ...(at ? { at } : {}), summary: text })
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
if (r.isMeta) {
|
|
169
|
+
lost.add("iFlow CLI meta messages")
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
const results = blocks.filter((b): b is Extract<Block, { type: "tool_result" }> => b.type === "tool_result")
|
|
173
|
+
if (results.length) {
|
|
174
|
+
for (const b of results) {
|
|
175
|
+
const call = waiting.get(b.tool_use_id)
|
|
176
|
+
if (!call) {
|
|
177
|
+
lost.add("tool results without a call")
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
waiting.delete(b.tool_use_id)
|
|
181
|
+
const res = resultOf(b.content, r.toolUseResult?.status)
|
|
182
|
+
const c = clip(res.text)
|
|
183
|
+
turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId: call.id, ok: res.ok, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
|
|
184
|
+
}
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
let text = blocks.map((b) => (b.type === "text" ? b.text : "")).join("")
|
|
188
|
+
const note = stripNote(text)
|
|
189
|
+
if (note.source) {
|
|
190
|
+
h.parent = { id: note.source, kind: "continued" }
|
|
191
|
+
text = note.text
|
|
192
|
+
}
|
|
193
|
+
// A summary brain wrote for a compacted session, in the form Gemini CLI's compression writes.
|
|
194
|
+
const summary = snapshotText(text)
|
|
195
|
+
if (summary !== undefined) {
|
|
196
|
+
turns.push({ kind: "compaction", ...(at ? { at } : {}), summary })
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
const images = blocks.filter((b) => b.type === "image").length
|
|
200
|
+
if (images) lost.add("inline images", images)
|
|
201
|
+
if (text.trim()) turns.push({ kind: "user", ...(at ? { at } : {}), text })
|
|
202
|
+
}
|
|
203
|
+
// Calls that never got a result (iFlow records only successful ones) are closed as failed, after the call.
|
|
204
|
+
if (waiting.size) {
|
|
205
|
+
lost.add("tool results not recorded", waiting.size)
|
|
206
|
+
const open = new Set(waiting.values())
|
|
207
|
+
const out: Turn[] = []
|
|
208
|
+
for (const t of turns) {
|
|
209
|
+
out.push(t)
|
|
210
|
+
if (t.kind === "tool_call" && open.has(t)) out.push({ kind: "tool_result", ...(t.at ? { at: t.at } : {}), callId: t.id, ok: false, output: "(no result recorded: iFlow CLI keeps only successful results)" })
|
|
211
|
+
}
|
|
212
|
+
return out
|
|
213
|
+
}
|
|
214
|
+
return turns
|
|
215
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { toolLabel } from "../../brief.ts"
|
|
5
|
+
import { exists, gitInfo, isDir, readJson, versionOf } from "../../fsutil.ts"
|
|
6
|
+
import type { Session } from "../../session/types.ts"
|
|
7
|
+
import { countTurns, header, lossCounter, unpaired } from "../../session/util.ts"
|
|
8
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
9
|
+
import { ALIASES, type Call, clock, drops, imageData, mapCall, realCwd, regroup, resultOutput, shellQuote, type ToolSpec } from "../gemini-cli/exchanges.ts"
|
|
10
|
+
import { continuedNote, snapshotOf } from "../gemini-cli/records.ts"
|
|
11
|
+
import { type Block, type IflowRecord, LABEL, projectName, RECORD_VERSION, readRecords, recordsToTurns, TOOL } from "./records.ts"
|
|
12
|
+
|
|
13
|
+
// Continues a portable session in iFlow CLI as a new session of its own: projects/<project name>/
|
|
14
|
+
// session-<uuid>.jsonl under ~/.iflow, with the session added to config/projects.json as iFlow's
|
|
15
|
+
// createSession adds it. Records are built the way its session message builder builds them, so
|
|
16
|
+
// `iflow --resume <id>` loads the file (checked by running 0.5.19's own loader code, taken from its bundle).
|
|
17
|
+
//
|
|
18
|
+
// The person's messages are string content; a model response is one assistant record of text and tool_use
|
|
19
|
+
// blocks; each result is a user record holding a tool_result block with the tool's response object
|
|
20
|
+
// (responseParts, resultDisplay) and toolUseResult, which is what iFlow's resume turns into a tool group.
|
|
21
|
+
// A compacted session starts from its last summary, as iFlow loads its own from the last compression record
|
|
22
|
+
// on. The summary is a <state_snapshot> user message, not iFlow's isCompactSummary record: resuming from that
|
|
23
|
+
// record (rQ) gives the model the calls after it without their results, which the API rejects, while a plain
|
|
24
|
+
// message goes through the resume that pairs every call with its result. iFlow keeps no reasoning.
|
|
25
|
+
// iFlow hides isMeta messages from the model as well as the screen, so the note on where the session came
|
|
26
|
+
// from ends the first message the model reads (the first user message, or the summary); at the end, the
|
|
27
|
+
// session picker, which shows each session's first message, still shows the person's request.
|
|
28
|
+
|
|
29
|
+
/** Versions this writer was checked against; others get the brief. 0.5.19 is iFlow CLI's last release. */
|
|
30
|
+
export const TESTED_VERSIONS = ["0.5.19"]
|
|
31
|
+
|
|
32
|
+
/** The installed iFlow CLI version ("0.5.19"), or undefined when it is not installed. */
|
|
33
|
+
export const iflowVersion = (bin = "iflow") => versionOf(bin)?.match(/(\d+\.\d+\.\d+)/)?.[1]
|
|
34
|
+
|
|
35
|
+
/** iFlow CLI's own tool for a portable call, by what the call does. */
|
|
36
|
+
function iflowTool(call: Call): ToolSpec | undefined {
|
|
37
|
+
const input = (call.input ?? {}) as Record<string, unknown>
|
|
38
|
+
switch (call.toolKind) {
|
|
39
|
+
case "read":
|
|
40
|
+
return { name: "read_file", args: { absolute_path: ALIASES.file } }
|
|
41
|
+
case "edit":
|
|
42
|
+
return { name: "replace", args: { file_path: ALIASES.file, old_string: ALIASES.oldText, new_string: ALIASES.newText } }
|
|
43
|
+
case "write":
|
|
44
|
+
return { name: "write_file", args: { file_path: ALIASES.file, content: ALIASES.content } }
|
|
45
|
+
case "shell":
|
|
46
|
+
return { name: "run_shell_command", args: { command: ALIASES.command } }
|
|
47
|
+
case "search":
|
|
48
|
+
return { name: /glob/i.test(call.name) ? "glob" : "search_file_content", args: { pattern: ALIASES.pattern, path: ALIASES.dir } }
|
|
49
|
+
case "list":
|
|
50
|
+
return { name: "list_directory", args: { path: ALIASES.dir } }
|
|
51
|
+
case "web":
|
|
52
|
+
return ALIASES.url.some((k) => input[k] !== undefined) ? { name: "web_fetch", args: { url: ALIASES.url, prompt: ["prompt"] } } : { name: "web_search", args: { query: ALIASES.query } }
|
|
53
|
+
case "todo":
|
|
54
|
+
return { name: "todo_write", args: { todos: ["todos"] } }
|
|
55
|
+
default:
|
|
56
|
+
return undefined
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface IflowPlan {
|
|
61
|
+
records: IflowRecord[]
|
|
62
|
+
lost: string[]
|
|
63
|
+
/** What a read-back must find. */
|
|
64
|
+
expect: { users: number; assistants: number; calls: number }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The records of the new session `sessionId` continuing `session`. Pure apart from reading attached images. */
|
|
68
|
+
export function planRecords(session: Session, opts: { sessionId: string; cwd: string; gitBranch?: string; now: Date }): IflowPlan {
|
|
69
|
+
const { drop, list } = drops()
|
|
70
|
+
const exchanges = regroup(session, { fromLastCompaction: true, keepReasoning: false, drop })
|
|
71
|
+
const label = toolLabel(session.header.tool)
|
|
72
|
+
const stamp = clock(session, opts.now)
|
|
73
|
+
const note = continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10))
|
|
74
|
+
const expect = { users: 0, assistants: 0, calls: 0 }
|
|
75
|
+
const records: IflowRecord[] = []
|
|
76
|
+
let parent: string | null = null
|
|
77
|
+
let lastModel = session.header.models[session.header.models.length - 1] ?? "unknown"
|
|
78
|
+
let noted = false
|
|
79
|
+
const withNote = (text: string) => {
|
|
80
|
+
if (noted) return text
|
|
81
|
+
noted = true
|
|
82
|
+
return text ? `${text}\n\n${note}` : note
|
|
83
|
+
}
|
|
84
|
+
const where = { cwd: opts.cwd, gitBranch: opts.gitBranch ?? null, version: RECORD_VERSION }
|
|
85
|
+
const add = (at: string | undefined, r: Omit<IflowRecord, "uuid" | "parentUuid" | "sessionId" | "timestamp" | "isSidechain" | "userType">, withPlace = true) => {
|
|
86
|
+
const out = { uuid: randomUUID(), parentUuid: parent, sessionId: opts.sessionId, timestamp: stamp(at), isSidechain: false, userType: "external", ...r, ...(withPlace ? where : {}) } as IflowRecord
|
|
87
|
+
records.push(out)
|
|
88
|
+
parent = out.uuid
|
|
89
|
+
return out
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A session that opens with the model gets the note on its own first.
|
|
93
|
+
if (exchanges[0]?.kind === "model") add(exchanges[0].at, { type: "user", message: { role: "user", content: withNote("") } })
|
|
94
|
+
for (const x of exchanges) {
|
|
95
|
+
if (x.kind === "user") {
|
|
96
|
+
let text = x.text
|
|
97
|
+
const images: Block[] = []
|
|
98
|
+
for (const a of x.attachments) {
|
|
99
|
+
if (a.kind === "file") {
|
|
100
|
+
if (a.path) text += `\n\n[Attached file: ${a.path}]`
|
|
101
|
+
continue
|
|
102
|
+
}
|
|
103
|
+
const img = imageData(a)
|
|
104
|
+
if (img) images.push({ type: "image", source: { type: "base64", media_type: img.mimeType, data: img.data } })
|
|
105
|
+
else if (a.path) drop("images that could not be attached")
|
|
106
|
+
}
|
|
107
|
+
if (!text.trim() && !images.length) continue
|
|
108
|
+
const body = withNote(text)
|
|
109
|
+
add(x.at, { type: "user", message: { role: "user", content: images.length ? [{ type: "text", text: body }, ...images] : body } })
|
|
110
|
+
if (text.trim()) expect.users++
|
|
111
|
+
} else if (x.kind === "compaction") {
|
|
112
|
+
add(x.at, { type: "user", message: { role: "user", content: withNote(snapshotOf(label, x.summary)) } })
|
|
113
|
+
} else {
|
|
114
|
+
if (x.model) lastModel = x.model
|
|
115
|
+
const calls = x.calls.map((a) => ({ a, id: `call_${randomUUID().replace(/-/g, "").slice(0, 24)}`, ...mapCall(a.call, iflowTool(a.call)) }))
|
|
116
|
+
const content: Block[] = [...(x.text.trim() ? [{ type: "text" as const, text: x.text }] : []), ...calls.map((c) => ({ type: "tool_use" as const, id: c.id, name: c.name, input: c.args }))]
|
|
117
|
+
if (!content.length) continue
|
|
118
|
+
const usage = { input_tokens: (x.usage?.input ?? 0) + (x.usage?.cacheRead ?? 0), output_tokens: x.usage?.output ?? 0 }
|
|
119
|
+
// Assistant records carry no cwd, branch or version, as iFlow writes them.
|
|
120
|
+
add(x.at, { type: "assistant", message: { id: `msg-${randomUUID()}`, type: "message", role: "assistant", content, model: lastModel, stop_reason: null, stop_sequence: null, usage } }, false)
|
|
121
|
+
if (x.text.trim()) expect.assistants++
|
|
122
|
+
for (const c of calls) {
|
|
123
|
+
if (!c.a.result) drop("tool calls without a result (closed as errors)")
|
|
124
|
+
if (c.a.edits.length) drop("file edit diffs", c.a.edits.length)
|
|
125
|
+
const ok = c.a.result?.ok ?? false
|
|
126
|
+
const output = resultOutput(c.a.result)
|
|
127
|
+
const response = { callId: c.id, responseParts: [{ functionResponse: { id: c.id, name: c.name, response: ok ? { output } : { error: output } } }], resultDisplay: output }
|
|
128
|
+
const r = add(c.a.result?.at ?? c.a.call.at, { type: "user", message: { role: "user", content: [{ tool_use_id: c.id, type: "tool_result", content: response }] }, toolUseResult: { toolName: c.name, status: ok ? "success" : "error" } })
|
|
129
|
+
r.toolUseResult!.timestamp = Date.parse(r.timestamp)
|
|
130
|
+
expect.calls++
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!records.length) throw new Error("nothing iFlow CLI can resume: the session has no messages")
|
|
135
|
+
return { records, lost: list(), expect }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Adds the session to config/projects.json as createSession does, replacing the file in one step. */
|
|
139
|
+
function register(home: string, project: string, sessionId: string, now: Date) {
|
|
140
|
+
const file = join(home, ".iflow", "config", "projects.json")
|
|
141
|
+
mkdirSync(join(home, ".iflow", "config"), { recursive: true })
|
|
142
|
+
const data = readJson<Record<string, { name?: string; path?: string; sessions?: string[]; createdAt?: string; lastActivity?: string }>>(file) ?? {}
|
|
143
|
+
const entry = data[project] ?? { name: project, path: project, sessions: [], createdAt: now.toISOString() }
|
|
144
|
+
data[project] = { ...entry, sessions: [...(entry.sessions ?? []), sessionId], lastActivity: now.toISOString() }
|
|
145
|
+
const tmp = `${file}.${randomUUID()}.tmp`
|
|
146
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2))
|
|
147
|
+
renameSync(tmp, file)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The iFlow CLI writer; `version` and `bin` are for tests, which must not depend on the installed binary. */
|
|
151
|
+
export function iflowWriter(opts: { bin?: string; version?: string } = {}): Writer {
|
|
152
|
+
const installed = () => opts.version ?? iflowVersion(opts.bin)
|
|
153
|
+
return {
|
|
154
|
+
tool: TOOL,
|
|
155
|
+
label: LABEL,
|
|
156
|
+
way: "native",
|
|
157
|
+
|
|
158
|
+
async ready(w: WriteOptions): Promise<Readiness> {
|
|
159
|
+
const v = installed()
|
|
160
|
+
if (!v) return { ok: false, why: "iFlow CLI is not installed (`iflow --version` did not run)" }
|
|
161
|
+
if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `iFlow CLI ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
|
|
162
|
+
if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; iFlow CLI lists and resumes sessions per project folder` }
|
|
163
|
+
return { ok: true }
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
async write(session: Session, w: WriteOptions): Promise<WriteResult> {
|
|
167
|
+
const cwd = realCwd(w.cwd)
|
|
168
|
+
const project = projectName(cwd)
|
|
169
|
+
const dir = join(w.home, ".iflow", "projects", project)
|
|
170
|
+
let sessionId = `session-${randomUUID()}`
|
|
171
|
+
while (exists(join(dir, `${sessionId}.jsonl`))) sessionId = `session-${randomUUID()}`
|
|
172
|
+
const path = join(dir, `${sessionId}.jsonl`)
|
|
173
|
+
const now = new Date()
|
|
174
|
+
const branch = gitInfo(cwd, w.home).gitBranch
|
|
175
|
+
const plan = planRecords(session, { sessionId, cwd, now, ...(branch ? { gitBranch: branch } : {}) })
|
|
176
|
+
if (!w.dryRun) {
|
|
177
|
+
mkdirSync(dir, { recursive: true })
|
|
178
|
+
// "wx": a new file only; an existing session is never touched.
|
|
179
|
+
writeFileSync(path, `${plan.records.map((r) => JSON.stringify(r)).join("\n")}\n`, { flag: "wx" })
|
|
180
|
+
const h = header(TOOL, sessionId)
|
|
181
|
+
const turns = recordsToTurns(readRecords(path).records, h, lossCounter())
|
|
182
|
+
const got = countTurns({ header: h, turns })
|
|
183
|
+
const pairs = unpaired({ header: h, turns })
|
|
184
|
+
if (got.user !== plan.expect.users || got.assistant !== plan.expect.assistants || got.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length) {
|
|
185
|
+
rmSync(path, { force: true })
|
|
186
|
+
throw new Error(`the new iFlow CLI session did not read back as written (users ${got.user}/${plan.expect.users}, replies ${got.assistant}/${plan.expect.assistants}, tool calls ${got.tool_call}/${plan.expect.calls}); nothing was kept`)
|
|
187
|
+
}
|
|
188
|
+
register(w.home, project, sessionId, now)
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
tool: TOOL,
|
|
192
|
+
way: "native",
|
|
193
|
+
sessionId,
|
|
194
|
+
written: [path, join(w.home, ".iflow", "config", "projects.json")],
|
|
195
|
+
open: `cd ${shellQuote(cwd)} && iflow --resume ${sessionId}`,
|
|
196
|
+
note: "iFlow CLI's service shut down on 2026-04-17, so the session opens but its models may no longer answer.",
|
|
197
|
+
lost: plan.lost,
|
|
198
|
+
dryRun: !!w.dryRun,
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export const iflowNativeWriter = iflowWriter()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Reader, Writer } from "./types.ts";
|
|
2
|
+
import * as claudeCode from "./claude-code/index.ts";
|
|
3
|
+
import * as codex from "./codex/index.ts";
|
|
4
|
+
import * as cursorIde from "./cursor-ide/index.ts";
|
|
5
|
+
import * as cursorCli from "./cursor-cli/index.ts";
|
|
6
|
+
import * as geminiCli from "./gemini-cli/index.ts";
|
|
7
|
+
import * as vscodeCopilot from "./vscode-copilot/index.ts";
|
|
8
|
+
import * as copilotCli from "./copilot-cli/index.ts";
|
|
9
|
+
import * as cline from "./cline/index.ts";
|
|
10
|
+
import * as opencode from "./opencode/index.ts";
|
|
11
|
+
import * as aider from "./aider/index.ts";
|
|
12
|
+
import * as amazonQCli from "./amazon-q-cli/index.ts";
|
|
13
|
+
import * as amazonQIde from "./amazon-q-ide/index.ts";
|
|
14
|
+
import * as auggie from "./auggie/index.ts";
|
|
15
|
+
import * as cline4 from "./cline4/index.ts";
|
|
16
|
+
import * as codebuff from "./codebuff/index.ts";
|
|
17
|
+
import * as continueAdapter from "./continue/index.ts";
|
|
18
|
+
import * as crush from "./crush/index.ts";
|
|
19
|
+
import * as factoryDroid from "./factory-droid/index.ts";
|
|
20
|
+
import * as goose from "./goose/index.ts";
|
|
21
|
+
import * as grokCli from "./grok-cli/index.ts";
|
|
22
|
+
import * as iflowCli from "./iflow-cli/index.ts";
|
|
23
|
+
import * as kiloCli from "./kilo-cli/index.ts";
|
|
24
|
+
import * as kiloCode from "./kilo-code/index.ts";
|
|
25
|
+
import * as kimi from "./kimi/index.ts";
|
|
26
|
+
import * as kiroCli from "./kiro-cli/index.ts";
|
|
27
|
+
import * as kiroIde from "./kiro-ide/index.ts";
|
|
28
|
+
import * as letta from "./letta/index.ts";
|
|
29
|
+
import * as mistralVibe from "./mistral-vibe/index.ts";
|
|
30
|
+
import * as openhands from "./openhands/index.ts";
|
|
31
|
+
import * as pearai from "./pearai/index.ts";
|
|
32
|
+
import * as pi from "./pi/index.ts";
|
|
33
|
+
import * as qoderCli from "./qoder-cli/index.ts";
|
|
34
|
+
import * as qwenCode from "./qwen-code/index.ts";
|
|
35
|
+
import * as tabnineCli from "./tabnine-cli/index.ts";
|
|
36
|
+
import * as voidAdapter from "./void/index.ts";
|
|
37
|
+
import * as zed from "./zed/index.ts";
|
|
38
|
+
import * as antigravity from "./antigravity/index.ts";
|
|
39
|
+
import * as devin from "./devin/index.ts";
|
|
40
|
+
import * as hermes from "./hermes/index.ts";
|
|
41
|
+
import * as mastracode from "./mastracode/index.ts";
|
|
42
|
+
import * as muse from "./muse/index.ts";
|
|
43
|
+
import * as omp from "./omp/index.ts";
|
|
44
|
+
import { briefWriter, briefWriters } from "../brief-writers.ts";
|
|
45
|
+
|
|
46
|
+
// Every adapter. Each module exports the readers and writers for one tool or format family; a tool with
|
|
47
|
+
// no native or import writer is still a target through the continuation brief (brief-writers.ts).
|
|
48
|
+
const MODULES = [
|
|
49
|
+
claudeCode,
|
|
50
|
+
codex,
|
|
51
|
+
cursorIde,
|
|
52
|
+
cursorCli,
|
|
53
|
+
geminiCli,
|
|
54
|
+
vscodeCopilot,
|
|
55
|
+
copilotCli,
|
|
56
|
+
cline,
|
|
57
|
+
opencode,
|
|
58
|
+
aider,
|
|
59
|
+
amazonQCli,
|
|
60
|
+
amazonQIde,
|
|
61
|
+
auggie,
|
|
62
|
+
cline4,
|
|
63
|
+
codebuff,
|
|
64
|
+
continueAdapter,
|
|
65
|
+
crush,
|
|
66
|
+
factoryDroid,
|
|
67
|
+
goose,
|
|
68
|
+
grokCli,
|
|
69
|
+
iflowCli,
|
|
70
|
+
kiloCli,
|
|
71
|
+
kiloCode,
|
|
72
|
+
kimi,
|
|
73
|
+
kiroCli,
|
|
74
|
+
kiroIde,
|
|
75
|
+
letta,
|
|
76
|
+
mistralVibe,
|
|
77
|
+
openhands,
|
|
78
|
+
pearai,
|
|
79
|
+
pi,
|
|
80
|
+
qoderCli,
|
|
81
|
+
qwenCode,
|
|
82
|
+
tabnineCli,
|
|
83
|
+
voidAdapter,
|
|
84
|
+
zed,
|
|
85
|
+
antigravity,
|
|
86
|
+
devin,
|
|
87
|
+
hermes,
|
|
88
|
+
mastracode,
|
|
89
|
+
muse,
|
|
90
|
+
omp,
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
export const READERS: Reader[] = MODULES.flatMap((m) => m.readers);
|
|
94
|
+
|
|
95
|
+
const OWN: Writer[] = MODULES.flatMap((m) => m.writers);
|
|
96
|
+
const BRIEFED = new Set(briefWriters.map((w) => w.tool));
|
|
97
|
+
/**
|
|
98
|
+
* Every other target with its own writer gets the brief too, as the last resort: when its native or import writer is not
|
|
99
|
+
* ready (the tool missing, or a version that format was never checked on), the session still goes there as a brief.
|
|
100
|
+
*/
|
|
101
|
+
const FALLBACK_BRIEFS: Writer[] = [...new Map(OWN.filter((w) => !BRIEFED.has(w.tool)).map((w) => [w.tool, w.label] as const))].map(([tool, label]) => briefWriter(tool, label));
|
|
102
|
+
|
|
103
|
+
/** Native and import writers first, so a target that has one prefers it over the brief. */
|
|
104
|
+
export const WRITERS: Writer[] = [...OWN, ...briefWriters, ...FALLBACK_BRIEFS];
|
|
105
|
+
|
|
106
|
+
export const readerFor = (tool: string) => READERS.find((r) => r.tool === tool);
|
|
107
|
+
|
|
108
|
+
/** The writers that can continue a session in `target`, best way first: native, then import, then brief. */
|
|
109
|
+
export function writersFor(target: string): Writer[] {
|
|
110
|
+
const order = { native: 0, import: 1, brief: 2 } as const;
|
|
111
|
+
return WRITERS.filter((w) => w.tool === target).sort((a, b) => order[a.way] - order[b.way]);
|
|
112
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { familyReader } from "../opencode/reader.ts"
|
|
3
|
+
import { KILO_V1 } from "../opencode/tools.ts"
|
|
4
|
+
import { importWriter, type Target } from "../opencode/writer.ts"
|
|
5
|
+
|
|
6
|
+
// Kilo CLI 7.x (Kilo-Org/kilocode), an OpenCode fork: the OpenCode 1.x session, message and part tables in
|
|
7
|
+
// ~/.local/share/kilo/kilo.db (kilo-<channel>.db on other channels), and OpenCode's export/import JSON with
|
|
8
|
+
// the same schema (packages/schema/src/v1/session.ts; packages/opencode/src/cli/cmd/import.ts). So it reads
|
|
9
|
+
// with OpenCode's reader and continues a session through `kilo import`, verified live on 7.8.27 (temp home,
|
|
10
|
+
// no network): import, then `kilo session list` and `kilo export`. On 7.8.28 the wire harness (wire/run.ts kilo-cli) imports
|
|
11
|
+
// and continues the session with the real binary against a fake model server, plain and compacted.
|
|
12
|
+
//
|
|
13
|
+
// Checked against a real Kilo 7.5.0 export (session-migrate's capture, test/native-corpus-stores.test.ts): its default agent is
|
|
14
|
+
// `code` (OpenCode's is `build`), so the session is written with that agent.
|
|
15
|
+
//
|
|
16
|
+
// A signed-in Kilo also syncs every new session, an imported one included, to Kilo's cloud (KiloSessions).
|
|
17
|
+
|
|
18
|
+
export const KILO_TARGET: Target = { tool: "kilo-cli", label: "Kilo CLI", bin: "kilo", installDir: ".kilo", tested: ["7.8.27", "7.8.28"], slug: true, dialect: KILO_V1 }
|
|
19
|
+
|
|
20
|
+
export const readers: Reader[] = [familyReader({ tool: "kilo-cli", label: "Kilo CLI", app: "kilo", sqlite: true })]
|
|
21
|
+
export const writers: Writer[] = [importWriter(KILO_TARGET)]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { taskReader } from "../cline/reader.ts"
|
|
3
|
+
import { kiloCodeWriters } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// The Kilo Code extension. Its 7.x sessions live in the Kilo CLI's database, which the kilo-cli reader already
|
|
6
|
+
// lists (listing them here too would show each twice), so this reader covers what only the extension has: the
|
|
7
|
+
// Cline-style task folders of Kilo Code before 7.0 (a Roo Code fork), in <app>/User/globalStorage/kilocode.kilo-code/
|
|
8
|
+
// tasks, which 7.x still reads for its legacy migration (packages/kilo-vscode/src/legacy-migration). The writer
|
|
9
|
+
// continues a session as a 7.x session through the extension's bundled CLI (writer.ts).
|
|
10
|
+
export const readers: Reader[] = [taskReader({ tool: "kilo-code", label: "Kilo Code", extension: "kilocode.kilo-code" })]
|
|
11
|
+
export const writers: Writer[] = kiloCodeWriters
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import { exists, isDir, ls, readJson } from "../../fsutil.ts"
|
|
4
|
+
import type { Session } from "../../session/types.ts"
|
|
5
|
+
import type { Readiness, WriteOptions, WriteResult, Writer } from "../types.ts"
|
|
6
|
+
import { HOSTS, compareVersions } from "../cline/records.ts"
|
|
7
|
+
import { buildExport, continuedTitle, projectIdOf, sh } from "../opencode/writer.ts"
|
|
8
|
+
import { OPENCODE_V1 } from "../opencode/tools.ts"
|
|
9
|
+
|
|
10
|
+
// Continuing a session in the Kilo Code extension (kilocode.kilo-code 7.x). The extension no longer keeps
|
|
11
|
+
// Cline-style task folders: it runs the Kilo CLI it bundles (<extension>/bin/kilo serve, with
|
|
12
|
+
// KILO_DISABLE_CHANNEL_DB=true; packages/kilo-vscode/src/services/cli-backend/server-manager.ts), so its sessions
|
|
13
|
+
// are the CLI's, in ~/.local/share/kilo/kilo.db, and its panel lists them with session.list({directory, roots}).
|
|
14
|
+
// So the session is written as the export bundle Kilo's own importer takes (the OpenCode 1.x format the Kilo CLI
|
|
15
|
+
// writer also uses, ../opencode/writer.ts), and `open` runs the bundled binary's `import` from the folder, with
|
|
16
|
+
// the same environment the extension gives it, so the session lands in the store the panel reads.
|
|
17
|
+
//
|
|
18
|
+
// Verified live on the 7.8.27 and 7.8.28 binaries (the extension and the CLI are released together, from one
|
|
19
|
+
// monorepo at one version): import, then the extension's own listing and message routes on `kilo serve`, in a
|
|
20
|
+
// temp home with outbound network denied.
|
|
21
|
+
|
|
22
|
+
const EXTENSION = "kilocode.kilo-code"
|
|
23
|
+
export const TESTED_VERSIONS = ["7.8.27", "7.8.28"]
|
|
24
|
+
|
|
25
|
+
/** The installed extension's folder and version in an app: extensions.json names the active copy; else the newest folder. */
|
|
26
|
+
export function installedExtension(home: string, app: string): { dir: string; version: string } | undefined {
|
|
27
|
+
const host = HOSTS.find((h) => h.app === app)
|
|
28
|
+
if (!host) return undefined
|
|
29
|
+
const root = join(home, host.extensions)
|
|
30
|
+
const manifest = readJson<Array<{ identifier?: { id?: string }; version?: string; relativeLocation?: string; location?: { path?: string } }>>(join(root, "extensions.json"))
|
|
31
|
+
const entry = Array.isArray(manifest) ? manifest.find((e) => e?.identifier?.id?.toLowerCase() === EXTENSION && typeof e.version === "string") : undefined
|
|
32
|
+
if (entry?.version) {
|
|
33
|
+
const dir = entry.relativeLocation ? join(root, entry.relativeLocation) : entry.location?.path
|
|
34
|
+
if (dir && isDir(dir)) return { dir, version: entry.version }
|
|
35
|
+
}
|
|
36
|
+
const found = ls(root)
|
|
37
|
+
.map((d) => ({ d, v: d.match(/^kilocode\.kilo-code-(\d+\.\d+\.\d+)/i)?.[1] }))
|
|
38
|
+
.filter((x): x is { d: string; v: string } => !!x.v)
|
|
39
|
+
.sort((a, b) => compareVersions(a.v, b.v))
|
|
40
|
+
.at(-1)
|
|
41
|
+
return found ? { dir: join(root, found.d), version: found.v } : undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function kiloCodeWriter(app: string): Writer {
|
|
45
|
+
const host = HOSTS.find((h) => h.app === app)!
|
|
46
|
+
const tool = app === "Code" ? "kilo-code" : `kilo-code-${app.toLowerCase()}`
|
|
47
|
+
const label = app === "Code" ? "Kilo Code" : `Kilo Code (${app})`
|
|
48
|
+
const where = app === "Code" ? "VS Code" : app
|
|
49
|
+
const binOf = (dir: string) => join(dir, "bin", process.platform === "win32" ? "kilo.exe" : "kilo")
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
tool,
|
|
53
|
+
label,
|
|
54
|
+
way: "import",
|
|
55
|
+
|
|
56
|
+
async ready(w: WriteOptions): Promise<Readiness> {
|
|
57
|
+
const ext = installedExtension(w.home, app)
|
|
58
|
+
if (!ext) return { ok: false, why: `Kilo Code is not installed in ${where}` }
|
|
59
|
+
if (compareVersions(ext.version, "7.0.0") < 0) return { ok: false, why: `Kilo Code ${ext.version} keeps Cline-style tasks; this writer targets the 7.x extension, which runs the Kilo CLI` }
|
|
60
|
+
if (!TESTED_VERSIONS.includes(ext.version)) return { ok: false, why: `Kilo Code ${ext.version} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
|
|
61
|
+
if (!exists(binOf(ext.dir))) return { ok: false, why: `Kilo Code ${ext.version} has no bundled CLI at ${binOf(ext.dir)}` }
|
|
62
|
+
if (!isDir(w.cwd)) return { ok: false, why: `the folder to continue in does not exist: ${w.cwd}` }
|
|
63
|
+
return { ok: true }
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
async write(session: Session, w: WriteOptions): Promise<WriteResult> {
|
|
67
|
+
const ext = installedExtension(w.home, app)
|
|
68
|
+
const version = ext?.version ?? TESTED_VERSIONS.at(-1)!
|
|
69
|
+
const bin = ext ? binOf(ext.dir) : `<${where} extensions>/kilocode.kilo-code-${version}/bin/kilo`
|
|
70
|
+
const { bundle, sessionId, lost } = buildExport(session, { label: "Kilo Code", dialect: OPENCODE_V1, cwd: w.cwd, version, projectID: projectIdOf(w.cwd, "kilo"), slug: true })
|
|
71
|
+
const file = join(w.home, ".brain", "exports", `${tool}-${sessionId}.json`)
|
|
72
|
+
if (!w.dryRun) {
|
|
73
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
74
|
+
writeFileSync(file, `${JSON.stringify(bundle, null, 2)}\n`)
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
tool,
|
|
78
|
+
way: "import",
|
|
79
|
+
sessionId,
|
|
80
|
+
written: [file],
|
|
81
|
+
// The importer files the session under the project of the folder it runs in; the channel setting is the
|
|
82
|
+
// extension's own, so the session goes to the database its panel reads.
|
|
83
|
+
open: `cd ${sh(w.cwd)} && KILO_DISABLE_CHANNEL_DB=true ${sh(bin)} import ${sh(file)} && ${host.cli} ${sh(w.cwd)}`,
|
|
84
|
+
note: `In ${where}, open Kilo Code's session history and pick "${continuedTitle(session).slice(0, 80)}".`,
|
|
85
|
+
lost,
|
|
86
|
+
dryRun: !!w.dryRun,
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const kiloCodeWriters: Writer[] = ["Code", "Cursor", "Windsurf"].map(kiloCodeWriter)
|