@100xprompt/brain 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +48 -0
- package/README.md +235 -2
- package/package.json +48 -4
- package/src/adapters/aider/index.ts +8 -0
- package/src/adapters/aider/reader.ts +129 -0
- package/src/adapters/aider/records.ts +220 -0
- package/src/adapters/aider/writer.ts +269 -0
- package/src/adapters/amazon-q-cli/index.ts +8 -0
- package/src/adapters/amazon-q-cli/reader.ts +84 -0
- package/src/adapters/amazon-q-cli/records.ts +191 -0
- package/src/adapters/amazon-q-cli/writer.ts +370 -0
- package/src/adapters/amazon-q-ide/index.ts +9 -0
- package/src/adapters/amazon-q-ide/reader.ts +131 -0
- package/src/adapters/amazon-q-ide/records.ts +141 -0
- package/src/adapters/amazon-q-ide/writer.ts +325 -0
- package/src/adapters/antigravity/index.ts +9 -0
- package/src/adapters/antigravity/proto.ts +194 -0
- package/src/adapters/antigravity/reader.ts +424 -0
- package/src/adapters/antigravity/records.ts +236 -0
- package/src/adapters/antigravity/writer.ts +463 -0
- package/src/adapters/auggie/index.ts +36 -0
- package/src/adapters/claude-code/index.ts +8 -0
- package/src/adapters/claude-code/reader.ts +639 -0
- package/src/adapters/claude-code/records.ts +88 -0
- package/src/adapters/claude-code/writer.ts +329 -0
- package/src/adapters/cline/index.ts +12 -0
- package/src/adapters/cline/reader.ts +432 -0
- package/src/adapters/cline/records.ts +151 -0
- package/src/adapters/cline/vscdb.ts +56 -0
- package/src/adapters/cline/writer.ts +656 -0
- package/src/adapters/cline/xml.ts +177 -0
- package/src/adapters/cline4/index.ts +9 -0
- package/src/adapters/cline4/reader.ts +179 -0
- package/src/adapters/cline4/records.ts +92 -0
- package/src/adapters/cline4/writer.ts +361 -0
- package/src/adapters/codebuff/index.ts +8 -0
- package/src/adapters/codebuff/project.ts +207 -0
- package/src/adapters/codebuff/reader.ts +215 -0
- package/src/adapters/codebuff/records.ts +173 -0
- package/src/adapters/codebuff/writer.ts +383 -0
- package/src/adapters/codex/index.ts +8 -0
- package/src/adapters/codex/native.ts +239 -0
- package/src/adapters/codex/reader.ts +747 -0
- package/src/adapters/codex/records.ts +114 -0
- package/src/adapters/codex/writer.ts +479 -0
- package/src/adapters/continue/index.ts +9 -0
- package/src/adapters/continue/reader.ts +68 -0
- package/src/adapters/continue/records.ts +418 -0
- package/src/adapters/continue/writer.ts +162 -0
- package/src/adapters/copilot-cli/index.ts +9 -0
- package/src/adapters/copilot-cli/reader.ts +186 -0
- package/src/adapters/copilot-cli/records.ts +241 -0
- package/src/adapters/copilot-cli/writer.ts +353 -0
- package/src/adapters/crush/index.ts +8 -0
- package/src/adapters/crush/reader.ts +180 -0
- package/src/adapters/crush/records.ts +169 -0
- package/src/adapters/crush/writer.ts +333 -0
- package/src/adapters/cursor-cli/index.ts +405 -0
- package/src/adapters/cursor-ide/agentkv.ts +95 -0
- package/src/adapters/cursor-ide/index.ts +501 -0
- package/src/adapters/cursor-ide/transcripts.ts +122 -0
- package/src/adapters/cursor-ide/workspaces.ts +119 -0
- package/src/adapters/cursor-ide/writer.ts +170 -0
- package/src/adapters/devin/index.ts +8 -0
- package/src/adapters/devin/reader.ts +305 -0
- package/src/adapters/devin/records.ts +277 -0
- package/src/adapters/devin/writer.ts +523 -0
- package/src/adapters/factory-droid/index.ts +12 -0
- package/src/adapters/factory-droid/reader.ts +184 -0
- package/src/adapters/factory-droid/records.ts +79 -0
- package/src/adapters/factory-droid/writer.ts +350 -0
- package/src/adapters/gemini-cli/exchanges.ts +228 -0
- package/src/adapters/gemini-cli/index.ts +396 -0
- package/src/adapters/gemini-cli/records.ts +521 -0
- package/src/adapters/gemini-cli/writer.ts +354 -0
- package/src/adapters/goose/index.ts +8 -0
- package/src/adapters/goose/reader.ts +138 -0
- package/src/adapters/goose/records.ts +199 -0
- package/src/adapters/goose/writer.ts +340 -0
- package/src/adapters/grok-cli/image.ts +44 -0
- package/src/adapters/grok-cli/index.ts +8 -0
- package/src/adapters/grok-cli/reader.ts +302 -0
- package/src/adapters/grok-cli/records.ts +172 -0
- package/src/adapters/grok-cli/writer.ts +329 -0
- package/src/adapters/hermes/index.ts +8 -0
- package/src/adapters/hermes/reader.ts +249 -0
- package/src/adapters/hermes/records.ts +285 -0
- package/src/adapters/hermes/writer.ts +452 -0
- package/src/adapters/iflow-cli/index.ts +72 -0
- package/src/adapters/iflow-cli/records.ts +215 -0
- package/src/adapters/iflow-cli/writer.ts +204 -0
- package/src/adapters/index.ts +112 -0
- package/src/adapters/kilo-cli/index.ts +21 -0
- package/src/adapters/kilo-code/index.ts +11 -0
- package/src/adapters/kilo-code/writer.ts +92 -0
- package/src/adapters/kimi/index.ts +9 -0
- package/src/adapters/kimi/reader.ts +92 -0
- package/src/adapters/kimi/records.ts +221 -0
- package/src/adapters/kimi/writer.ts +367 -0
- package/src/adapters/kiro-cli/index.ts +8 -0
- package/src/adapters/kiro-cli/reader.ts +87 -0
- package/src/adapters/kiro-cli/records.ts +260 -0
- package/src/adapters/kiro-cli/writer.ts +305 -0
- package/src/adapters/kiro-ide/index.ts +8 -0
- package/src/adapters/kiro-ide/writer.ts +52 -0
- package/src/adapters/letta/index.ts +8 -0
- package/src/adapters/letta/reader.ts +177 -0
- package/src/adapters/letta/records.ts +119 -0
- package/src/adapters/letta/writer.ts +413 -0
- package/src/adapters/mastracode/index.ts +8 -0
- package/src/adapters/mastracode/reader.ts +323 -0
- package/src/adapters/mastracode/records.ts +336 -0
- package/src/adapters/mastracode/writer.ts +385 -0
- package/src/adapters/mistral-vibe/index.ts +9 -0
- package/src/adapters/mistral-vibe/reader.ts +62 -0
- package/src/adapters/mistral-vibe/records.ts +104 -0
- package/src/adapters/mistral-vibe/writer.ts +359 -0
- package/src/adapters/muse/index.ts +10 -0
- package/src/adapters/muse/reader.ts +332 -0
- package/src/adapters/muse/records.ts +104 -0
- package/src/adapters/muse/writer.ts +387 -0
- package/src/adapters/omp/index.ts +13 -0
- package/src/adapters/omp/reader.ts +314 -0
- package/src/adapters/omp/records.ts +153 -0
- package/src/adapters/omp/writer.ts +216 -0
- package/src/adapters/opencode/ids.ts +52 -0
- package/src/adapters/opencode/index.ts +31 -0
- package/src/adapters/opencode/note.ts +8 -0
- package/src/adapters/opencode/reader.ts +452 -0
- package/src/adapters/opencode/tools.ts +174 -0
- package/src/adapters/opencode/v2.ts +367 -0
- package/src/adapters/opencode/writer.ts +379 -0
- package/src/adapters/openhands/index.ts +8 -0
- package/src/adapters/openhands/reader.ts +222 -0
- package/src/adapters/openhands/records.ts +136 -0
- package/src/adapters/openhands/writer.ts +404 -0
- package/src/adapters/pearai/index.ts +10 -0
- package/src/adapters/pi/index.ts +28 -0
- package/src/adapters/pi/reader.ts +261 -0
- package/src/adapters/pi/records.ts +99 -0
- package/src/adapters/pi/writer.ts +362 -0
- package/src/adapters/qoder-cli/index.ts +8 -0
- package/src/adapters/qoder-cli/reader.ts +83 -0
- package/src/adapters/qoder-cli/records.ts +45 -0
- package/src/adapters/qoder-cli/writer.ts +72 -0
- package/src/adapters/qwen-code/index.ts +84 -0
- package/src/adapters/qwen-code/records.ts +194 -0
- package/src/adapters/qwen-code/writer.ts +224 -0
- package/src/adapters/tabnine-cli/index.ts +8 -0
- package/src/adapters/tabnine-cli/reader.ts +106 -0
- package/src/adapters/tabnine-cli/records.ts +33 -0
- package/src/adapters/tabnine-cli/writer.ts +156 -0
- package/src/adapters/types.ts +73 -0
- package/src/adapters/void/index.ts +9 -0
- package/src/adapters/void/reader.ts +48 -0
- package/src/adapters/void/records.ts +285 -0
- package/src/adapters/void/writer.ts +93 -0
- package/src/adapters/vscode-copilot/index.ts +9 -0
- package/src/adapters/vscode-copilot/reader.ts +212 -0
- package/src/adapters/vscode-copilot/records.ts +388 -0
- package/src/adapters/vscode-copilot/shared.ts +68 -0
- package/src/adapters/vscode-copilot/writer.ts +546 -0
- package/src/adapters/zed/index.ts +8 -0
- package/src/adapters/zed/reader.ts +198 -0
- package/src/adapters/zed/records.ts +87 -0
- package/src/adapters/zed/writer.ts +341 -0
- package/src/brief-writers.ts +71 -0
- package/src/brief.ts +238 -0
- package/src/cli.ts +351 -0
- package/src/dashboard/cli.ts +148 -0
- package/src/dashboard/conversions.ts +170 -0
- package/src/dashboard/report.ts +261 -0
- package/src/dashboard/send.ts +213 -0
- package/src/dashboard/state.ts +95 -0
- package/src/fsutil.ts +133 -0
- package/src/gateway/common.ts +232 -0
- package/src/gateway/connect.ts +847 -0
- package/src/gateway/connectors/aider.ts +356 -0
- package/src/gateway/connectors/amazon-q-cli.ts +6 -0
- package/src/gateway/connectors/amazon-q-ide.ts +6 -0
- package/src/gateway/connectors/antigravity.ts +6 -0
- package/src/gateway/connectors/auggie.ts +6 -0
- package/src/gateway/connectors/cline-sdk.ts +85 -0
- package/src/gateway/connectors/cline.ts +34 -0
- package/src/gateway/connectors/codebuff.ts +6 -0
- package/src/gateway/connectors/continue.ts +579 -0
- package/src/gateway/connectors/copilot-cli.ts +138 -0
- package/src/gateway/connectors/crush.ts +313 -0
- package/src/gateway/connectors/cursor-cli.ts +6 -0
- package/src/gateway/connectors/cursor.ts +6 -0
- package/src/gateway/connectors/devin.ts +6 -0
- package/src/gateway/connectors/factory-droid.ts +308 -0
- package/src/gateway/connectors/goose.ts +79 -0
- package/src/gateway/connectors/grok-cli.ts +82 -0
- package/src/gateway/connectors/hermes.ts +230 -0
- package/src/gateway/connectors/iflow-cli.ts +50 -0
- package/src/gateway/connectors/index.ts +86 -0
- package/src/gateway/connectors/kilo-cli.ts +434 -0
- package/src/gateway/connectors/kilo-code.ts +32 -0
- package/src/gateway/connectors/kimi-code.ts +241 -0
- package/src/gateway/connectors/kiro-cli.ts +6 -0
- package/src/gateway/connectors/kiro-ide.ts +6 -0
- package/src/gateway/connectors/letta-code.ts +170 -0
- package/src/gateway/connectors/mastracode.ts +91 -0
- package/src/gateway/connectors/mistral-vibe.ts +495 -0
- package/src/gateway/connectors/muse.ts +27 -0
- package/src/gateway/connectors/omp.ts +279 -0
- package/src/gateway/connectors/openhands.ts +296 -0
- package/src/gateway/connectors/pearai.ts +52 -0
- package/src/gateway/connectors/pi.ts +357 -0
- package/src/gateway/connectors/qoder-cli.ts +6 -0
- package/src/gateway/connectors/qwen-code.ts +411 -0
- package/src/gateway/connectors/roo.ts +43 -0
- package/src/gateway/connectors/tabnine-cli.ts +6 -0
- package/src/gateway/connectors/void.ts +6 -0
- package/src/gateway/connectors/vscode-copilot.ts +286 -0
- package/src/gateway/connectors/windsurf.ts +6 -0
- package/src/gateway/connectors/zed.ts +29 -0
- package/src/gateway/signin.ts +323 -0
- package/src/handoff.ts +104 -0
- package/src/hooks.ts +455 -0
- package/src/memory/audit.ts +514 -0
- package/src/memory/blobs.ts +94 -0
- package/src/memory/cli.ts +117 -0
- package/src/memory/distill.ts +381 -0
- package/src/memory/externalize.ts +435 -0
- package/src/memory/lookups.ts +70 -0
- package/src/memory/mcp-install.ts +401 -0
- package/src/memory/mcp-server.ts +296 -0
- package/src/memory/prepare.ts +179 -0
- package/src/memory/preview.ts +154 -0
- package/src/memory/project.ts +35 -0
- package/src/memory/redact.ts +72 -0
- package/src/memory/relevance.ts +187 -0
- package/src/memory/repomap.ts +516 -0
- package/src/memory/store.ts +270 -0
- package/src/memory/sync.ts +166 -0
- package/src/memory/tidy.ts +193 -0
- package/src/memory/types.ts +118 -0
- package/src/org/admin-ops.ts +96 -0
- package/src/org/auth.ts +112 -0
- package/src/org/backup.ts +115 -0
- package/src/org/cli.ts +438 -0
- package/src/org/client.ts +404 -0
- package/src/org/config.ts +44 -0
- package/src/org/crypto.ts +537 -0
- package/src/org/db.ts +435 -0
- package/src/org/graph.ts +318 -0
- package/src/org/oidc.ts +435 -0
- package/src/org/policy.ts +398 -0
- package/src/org/server.ts +920 -0
- package/src/org/sso.ts +316 -0
- package/src/org/tls.ts +66 -0
- package/src/org/types.ts +193 -0
- package/src/self.ts +68 -0
- package/src/session/attachments.ts +53 -0
- package/src/session/peekcache.ts +50 -0
- package/src/session/types.ts +104 -0
- package/src/session/util.ts +130 -0
- package/src/sqlite.ts +48 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto"
|
|
2
|
+
import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { userInfo } from "node:os"
|
|
4
|
+
import { extname, join } from "node:path"
|
|
5
|
+
import { toolLabel } from "../../brief.ts"
|
|
6
|
+
import { gitInfo, isDir, ls, 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 { reader } from "./reader.ts"
|
|
11
|
+
import { LABEL, MESSAGES, META, TOOL } from "./records.ts"
|
|
12
|
+
|
|
13
|
+
// Continues a portable session in Mistral Vibe through its own importer. Vibe 3.x keeps sessions in a
|
|
14
|
+
// private Unified store that is not a supported write target (ADR 0006); what it does accept is a session
|
|
15
|
+
// folder in the format Vibe 2.x wrote ($VIBE_HOME/logs/session/session_<time>_<id8>/: meta.json and
|
|
16
|
+
// messages.jsonl). Vibe 3.1.0 lists such a folder in `vibe --resume`, previews it, and on resume imports
|
|
17
|
+
// its history into a new Unified session (session_interop.export_legacy_committed_history), keeping each
|
|
18
|
+
// message, each tool call with its arguments and each result, failures included, and readable reasoning.
|
|
19
|
+
//
|
|
20
|
+
// Calls become Vibe's own tools where their arguments fit (read_file, write_file, edit, bash, grep). The
|
|
21
|
+
// note saying where the session came from is an injected user message right after the first request;
|
|
22
|
+
// Vibe's legacy history skips injected messages, though its importer turns the note into a plain one.
|
|
23
|
+
// The importer does not carry a compaction boundary into the Unified store either, so the session starts
|
|
24
|
+
// from the last compaction summary, written as Vibe writes one (context_boundary "compaction").
|
|
25
|
+
|
|
26
|
+
/** Vibe versions whose resume importer this writer was checked against. */
|
|
27
|
+
export const TESTED_VERSIONS = ["3.1.0"]
|
|
28
|
+
|
|
29
|
+
/** False for 3.1.0: see imagesOf in planMessages. */
|
|
30
|
+
export const VIBE_RESUMES_IMPORTED_IMAGES = false
|
|
31
|
+
|
|
32
|
+
/** The installed version (`vibe --version` prints "vibe 3.1.0"), or undefined when it is missing. */
|
|
33
|
+
export function vibeVersion(bin = "vibe"): string | undefined {
|
|
34
|
+
return versionOf(bin)?.match(/(\d+\.\d+\.\d+(?:[-.\w]*)?)/)?.[1]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const continuedNote = (label: string, id: string, when: string) =>
|
|
38
|
+
`<system-reminder>\nThis session continues ${label} session ${id}, carried over by brain session on ${when}. The messages below are its history; anything listed as lost did not carry over. Check files before relying on details, since they may have changed.\n</system-reminder>`
|
|
39
|
+
|
|
40
|
+
const shellQuote = (s: string) => (/^[\w./@%+=:,-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`)
|
|
41
|
+
|
|
42
|
+
/** Vibe matches a session to the folder it is resumed from by the resolved path. */
|
|
43
|
+
function realCwd(cwd: string): string {
|
|
44
|
+
try {
|
|
45
|
+
return realpathSync(cwd)
|
|
46
|
+
} catch {
|
|
47
|
+
return cwd
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Call = Extract<Turn, { kind: "tool_call" }>
|
|
52
|
+
type Json = Record<string, unknown>
|
|
53
|
+
|
|
54
|
+
// Argument names other tools use for the same thing.
|
|
55
|
+
const FILE = ["file_path", "path", "filePath", "absolute_path", "target_file", "file", "filename"]
|
|
56
|
+
const OLD = ["old_string", "oldString", "old_str", "old_text"]
|
|
57
|
+
const NEW = ["new_string", "newString", "new_str", "new_text"]
|
|
58
|
+
const CONTENT = ["content", "contents", "file_text", "text"]
|
|
59
|
+
const COMMAND = ["command", "cmd", "commandLine"]
|
|
60
|
+
const PATTERN = ["pattern", "regex", "query"]
|
|
61
|
+
const DIR = ["path", "dir_path", "directory", "target_directory"]
|
|
62
|
+
|
|
63
|
+
/** The call as Vibe's own tool when its arguments fit one; otherwise undefined and it keeps its own. */
|
|
64
|
+
export function vibeTool(c: Call): { name: string; args: Json } | undefined {
|
|
65
|
+
const input = c.input && typeof c.input === "object" && !Array.isArray(c.input) ? (c.input as Json) : {}
|
|
66
|
+
const str = (keys: string[]) => {
|
|
67
|
+
const v = keys.map((k) => input[k]).find((x) => typeof x === "string" || (Array.isArray(x) && x.every((y) => typeof y === "string")))
|
|
68
|
+
return Array.isArray(v) ? v.join(" ") : (v as string | undefined)
|
|
69
|
+
}
|
|
70
|
+
const path = str(FILE)
|
|
71
|
+
switch (c.toolKind) {
|
|
72
|
+
case "read":
|
|
73
|
+
return path ? { name: "read_file", args: { file_path: path } } : undefined
|
|
74
|
+
case "edit": {
|
|
75
|
+
const [o, n] = [str(OLD), str(NEW)]
|
|
76
|
+
return path && o && n !== undefined ? { name: "edit", args: { file_path: path, old_string: o, new_string: n } } : undefined
|
|
77
|
+
}
|
|
78
|
+
case "write": {
|
|
79
|
+
const content = str(CONTENT)
|
|
80
|
+
return path && content !== undefined ? { name: "write_file", args: { file_path: path, content } } : undefined
|
|
81
|
+
}
|
|
82
|
+
case "shell": {
|
|
83
|
+
const command = str(COMMAND)
|
|
84
|
+
return command ? { name: "bash", args: { command } } : undefined
|
|
85
|
+
}
|
|
86
|
+
case "search": {
|
|
87
|
+
// Vibe has grep and no glob tool; a glob search keeps its own name.
|
|
88
|
+
const pattern = str(PATTERN)
|
|
89
|
+
const dir = str(DIR)
|
|
90
|
+
return pattern && !/glob/i.test(c.name) ? { name: "grep", args: { pattern, ...(dir ? { path: dir } : {}) } } : undefined
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface MessagesPlan {
|
|
98
|
+
messages: Json[]
|
|
99
|
+
lost: string[]
|
|
100
|
+
/** What a read-back must find. */
|
|
101
|
+
expect: { users: number; assistants: number; calls: number }
|
|
102
|
+
/** Image bytes to write under the session folder, by relative path (attachments/<sha1>.<ext>). */
|
|
103
|
+
assets: Map<string, Buffer>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The messages.jsonl lines of a new Vibe session folder continuing `session`. Pure. */
|
|
107
|
+
export function planMessages(session: Session, opts: { now: Date }): MessagesPlan {
|
|
108
|
+
const drops = new Map<string, number>()
|
|
109
|
+
const drop = (what: string, n = 1) => drops.set(what, (drops.get(what) ?? 0) + n)
|
|
110
|
+
const label = toolLabel(session.header.tool)
|
|
111
|
+
|
|
112
|
+
// Vibe's importer copies every message into the model's context, so the session starts from the last summary.
|
|
113
|
+
let turns = session.turns
|
|
114
|
+
let last = -1
|
|
115
|
+
turns.forEach((t, i) => {
|
|
116
|
+
if (t.kind === "compaction") last = i
|
|
117
|
+
})
|
|
118
|
+
if (last >= 0) {
|
|
119
|
+
const before = turns.slice(0, last).filter((t) => t.kind !== "compaction").length
|
|
120
|
+
if (before) drop("turns before the last compaction (the summary stands in for them)", before)
|
|
121
|
+
turns = turns.slice(last)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const note: Json = { role: "user", content: continuedNote(label, session.header.id, opts.now.toISOString().slice(0, 10)), injected: true, message_id: randomUUID() }
|
|
125
|
+
// The note follows the first request: Vibe's import drops `injected`, and the first user message becomes
|
|
126
|
+
// the imported session's preview, which should be the person's words.
|
|
127
|
+
const firstUser = turns.findIndex((t) => t.kind === "user" && t.text.trim())
|
|
128
|
+
const messages: Json[] = firstUser < 0 ? [note] : []
|
|
129
|
+
const expect = { users: 0, assistants: 0, calls: 0 }
|
|
130
|
+
const assets = new Map<string, Buffer>()
|
|
131
|
+
// An image whose bytes are still on disk is snapshotted the way Vibe 2.24.3 does for an @mentioned image:
|
|
132
|
+
// attachments/<sha1 of the bytes>.<ext> and {source: {kind: "file", path}, alias, mime_type} on the message.
|
|
133
|
+
const imagesOf = (list: NonNullable<Extract<Turn, { kind: "user" }>["attachments"]>) => {
|
|
134
|
+
const out: Json[] = []
|
|
135
|
+
for (const a of list) {
|
|
136
|
+
// Vibe 3.1.0 imports a message with an image into its own store as {type: "image", data, mimeType}, then refuses
|
|
137
|
+
// to resume it ("image.attachment: Field required"): the whole session becomes unresumable. Found by the wire
|
|
138
|
+
// harness with the real tool, for file and inline sources and for blob resources alike. Until a Vibe version
|
|
139
|
+
// resumes its own imported images, an image is dropped and listed as lost.
|
|
140
|
+
if (!VIBE_RESUMES_IMPORTED_IMAGES) {
|
|
141
|
+
drop("images (Vibe 3.1.0 cannot resume an imported session that holds one)")
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
let bytes: Buffer | undefined
|
|
145
|
+
if (a.kind === "image" && a.path) {
|
|
146
|
+
try {
|
|
147
|
+
bytes = readFileSync(a.path)
|
|
148
|
+
} catch {
|
|
149
|
+
bytes = undefined
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!bytes || !a.path) {
|
|
153
|
+
drop("attachments")
|
|
154
|
+
continue
|
|
155
|
+
}
|
|
156
|
+
const rel = `attachments/${createHash("sha1").update(bytes).digest("hex")}${extname(a.path).toLowerCase() || ".png"}`
|
|
157
|
+
assets.set(rel, bytes)
|
|
158
|
+
out.push({ source: { kind: "file", path: rel }, alias: a.name ?? a.path.split("/").pop(), mime_type: a.mime ?? "image/png" })
|
|
159
|
+
}
|
|
160
|
+
return out
|
|
161
|
+
}
|
|
162
|
+
// Call ids stay as they were when an OpenAI-style API takes them, else get a call_ id; each is used once.
|
|
163
|
+
const ids = new Map<string, string>()
|
|
164
|
+
const used = new Set<string>()
|
|
165
|
+
const idFor = (id: string) => {
|
|
166
|
+
let v = ids.get(id)
|
|
167
|
+
if (v) return v
|
|
168
|
+
const base = /^[A-Za-z0-9_-]{1,64}$/.test(id) ? id : `call_${id.replace(/[^A-Za-z0-9]/g, "").slice(-40) || "x"}`
|
|
169
|
+
v = base
|
|
170
|
+
for (let n = 2; used.has(v); n++) v = `${base}_${n}`
|
|
171
|
+
ids.set(id, v)
|
|
172
|
+
used.add(v)
|
|
173
|
+
return v
|
|
174
|
+
}
|
|
175
|
+
const results = new Map<string, Extract<Turn, { kind: "tool_result" }>>()
|
|
176
|
+
for (const t of turns) if (t.kind === "tool_result" && !results.has(t.callId)) results.set(t.callId, t)
|
|
177
|
+
|
|
178
|
+
let cur: { reasoning: string[]; text: string; calls: Call[] } | undefined
|
|
179
|
+
let pendingReasoning: string[] = []
|
|
180
|
+
const answered = new Set<string>()
|
|
181
|
+
const nameOf = (c: Call) => vibeTool(c)?.name ?? (c.name.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64) || "tool")
|
|
182
|
+
const flush = () => {
|
|
183
|
+
if (!cur) return
|
|
184
|
+
const reasoning = cur.reasoning.join("\n\n")
|
|
185
|
+
const toolCalls = cur.calls.map((c, index) => ({ id: idFor(c.id), index, function: { name: nameOf(c), arguments: JSON.stringify(vibeTool(c)?.args ?? c.input ?? {}) }, type: "function" }))
|
|
186
|
+
messages.push({ role: "assistant", content: cur.text, ...(reasoning ? { reasoning_content: reasoning } : {}), ...(toolCalls.length ? { tool_calls: toolCalls } : {}), message_id: randomUUID() })
|
|
187
|
+
if (cur.text.trim()) expect.assistants++
|
|
188
|
+
// A call the session never answered is closed as a failure, so every request has its result.
|
|
189
|
+
for (const c of cur.calls) {
|
|
190
|
+
if (results.has(c.id)) continue
|
|
191
|
+
messages.push({ role: "tool", content: "<tool_error>No result was recorded for this call; the session moved on before it finished.</tool_error>", tool_call_id: idFor(c.id), name: nameOf(c) })
|
|
192
|
+
answered.add(c.id)
|
|
193
|
+
drop("tool calls without a result (closed as failures)")
|
|
194
|
+
}
|
|
195
|
+
cur = undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const [i, t] of turns.entries()) {
|
|
199
|
+
switch (t.kind) {
|
|
200
|
+
case "user":
|
|
201
|
+
flush()
|
|
202
|
+
if (pendingReasoning.length) drop("reasoning without a reply", pendingReasoning.length)
|
|
203
|
+
pendingReasoning = []
|
|
204
|
+
const images = imagesOf(t.attachments ?? [])
|
|
205
|
+
if (!t.text.trim()) break
|
|
206
|
+
messages.push({ role: "user", content: t.text, ...(images.length ? { images } : {}), message_id: randomUUID() })
|
|
207
|
+
if (i === firstUser) messages.push(note)
|
|
208
|
+
expect.users++
|
|
209
|
+
break
|
|
210
|
+
case "assistant":
|
|
211
|
+
if (cur?.calls.length || (cur?.text.trim() && t.text.trim())) flush()
|
|
212
|
+
cur ??= { reasoning: pendingReasoning, text: "", calls: [] }
|
|
213
|
+
pendingReasoning = []
|
|
214
|
+
cur.text = t.text
|
|
215
|
+
break
|
|
216
|
+
case "reasoning":
|
|
217
|
+
if (cur?.calls.length || cur?.text.trim()) flush()
|
|
218
|
+
pendingReasoning.push(t.text)
|
|
219
|
+
break
|
|
220
|
+
case "tool_call":
|
|
221
|
+
if (ids.has(t.id)) {
|
|
222
|
+
drop("repeated tool call ids")
|
|
223
|
+
break
|
|
224
|
+
}
|
|
225
|
+
idFor(t.id)
|
|
226
|
+
cur ??= { reasoning: pendingReasoning, text: "", calls: [] }
|
|
227
|
+
pendingReasoning = []
|
|
228
|
+
cur.calls.push(t)
|
|
229
|
+
expect.calls++
|
|
230
|
+
break
|
|
231
|
+
case "tool_result": {
|
|
232
|
+
if (!ids.has(t.callId)) {
|
|
233
|
+
drop("tool results without a call")
|
|
234
|
+
break
|
|
235
|
+
}
|
|
236
|
+
if (answered.has(t.callId) || results.get(t.callId) !== t) {
|
|
237
|
+
drop("repeated tool results")
|
|
238
|
+
break
|
|
239
|
+
}
|
|
240
|
+
const call = cur?.calls.find((c) => c.id === t.callId)
|
|
241
|
+
if (call) flush()
|
|
242
|
+
let text = t.output
|
|
243
|
+
if (t.fullPath) text += `\n\n[Full output: ${t.fullPath}]`
|
|
244
|
+
const name = call ? nameOf(call) : undefined
|
|
245
|
+
messages.push({ role: "tool", content: t.ok ? text : `<tool_error>${text || "The tool failed."}</tool_error>`, tool_call_id: idFor(t.callId), ...(name ? { name } : {}) })
|
|
246
|
+
answered.add(t.callId)
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
case "compaction":
|
|
250
|
+
flush()
|
|
251
|
+
messages.push({ role: "user", content: `[Summary of the earlier conversation in ${label}, which replaced the turns before it]\n\n${t.summary}`, context_boundary: "compaction", message_id: randomUUID() })
|
|
252
|
+
break
|
|
253
|
+
case "file_edit":
|
|
254
|
+
if (!t.callId || !ids.has(t.callId)) drop("file edits outside a tool call")
|
|
255
|
+
break
|
|
256
|
+
case "subagent":
|
|
257
|
+
drop("subagent transcripts (their results stay in the parent's tool results)")
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
flush()
|
|
262
|
+
if (pendingReasoning.length) drop("reasoning without a reply", pendingReasoning.length)
|
|
263
|
+
// A tool message names its tool; one answered after the assistant message was flushed gets it here.
|
|
264
|
+
const names = new Map<string, string>()
|
|
265
|
+
for (const m of messages) for (const c of (m.tool_calls as Array<{ id: string; function: { name: string } }> | undefined) ?? []) names.set(c.id, c.function.name)
|
|
266
|
+
for (const m of messages) if (m.role === "tool" && !m.name) m.name = names.get(m.tool_call_id as string) ?? "tool"
|
|
267
|
+
return { messages, lost: [...drops].map(([what, n]) => `${what} (${n})`), expect, assets }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** `2026-11-08T10:00:00.000Z` → `20261108_100000`, the folder-name stamp Vibe uses (local time). */
|
|
271
|
+
function stamp(d: Date): string {
|
|
272
|
+
const p = (n: number) => String(n).padStart(2, "0")
|
|
273
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The Mistral Vibe writer; `version` is for tests, which must not depend on an installed binary. */
|
|
277
|
+
export function mistralVibeWriter(opts: { version?: string } = {}): Writer {
|
|
278
|
+
const installed = () => opts.version ?? vibeVersion()
|
|
279
|
+
return {
|
|
280
|
+
tool: TOOL,
|
|
281
|
+
label: LABEL,
|
|
282
|
+
way: "import",
|
|
283
|
+
|
|
284
|
+
async ready(w: WriteOptions): Promise<Readiness> {
|
|
285
|
+
const v = installed()
|
|
286
|
+
if (!v) return { ok: false, why: "Mistral Vibe is not installed (`vibe --version` did not run)" }
|
|
287
|
+
if (!TESTED_VERSIONS.includes(v)) return { ok: false, why: `Mistral Vibe ${v} is not a version this writer was checked against (${TESTED_VERSIONS.join(", ")})` }
|
|
288
|
+
if (!isDir(w.cwd)) return { ok: false, why: `${w.cwd} is not a folder; Vibe offers a session from its working directory` }
|
|
289
|
+
return { ok: true }
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
async write(session: Session, w: WriteOptions): Promise<WriteResult> {
|
|
293
|
+
const cwd = realCwd(w.cwd)
|
|
294
|
+
const root = join(w.home, ".vibe", "logs", "session")
|
|
295
|
+
const now = new Date()
|
|
296
|
+
let sessionId = randomUUID()
|
|
297
|
+
// Vibe finds a folder by the first 8 characters of the id, so no other folder may share them.
|
|
298
|
+
const taken = (id: string) => ls(root).some((n) => n.startsWith("session_") && n.endsWith(`_${id.slice(0, 8)}`))
|
|
299
|
+
while (taken(sessionId)) sessionId = randomUUID()
|
|
300
|
+
const dir = join(root, `session_${stamp(now)}_${sessionId.slice(0, 8)}`)
|
|
301
|
+
const plan = planMessages(session, { now })
|
|
302
|
+
const git = gitInfo(cwd, w.home)
|
|
303
|
+
const title = `${session.header.title ?? firstUserText(session)?.replace(/\s+/g, " ").trim().slice(0, 60) ?? "Session"} (from ${toolLabel(session.header.tool)})`
|
|
304
|
+
const meta = {
|
|
305
|
+
session_id: sessionId,
|
|
306
|
+
parent_session_id: null,
|
|
307
|
+
start_time: now.toISOString(),
|
|
308
|
+
end_time: now.toISOString(),
|
|
309
|
+
git_commit: git.gitCommit ?? null,
|
|
310
|
+
git_branch: git.gitBranch ?? null,
|
|
311
|
+
environment: { working_directory: cwd },
|
|
312
|
+
origin_directory: cwd,
|
|
313
|
+
username: userInfo().username,
|
|
314
|
+
title,
|
|
315
|
+
// "manual" keeps Vibe from replacing the title with a generated one.
|
|
316
|
+
title_source: "manual",
|
|
317
|
+
total_messages: plan.messages.length,
|
|
318
|
+
import_provenance: { source: session.header.tool, session_id: session.header.sourceId, by: "brain session" },
|
|
319
|
+
}
|
|
320
|
+
const paths = { meta: join(dir, META), messages: join(dir, MESSAGES) }
|
|
321
|
+
if (!w.dryRun) {
|
|
322
|
+
mkdirSync(root, { recursive: true })
|
|
323
|
+
// mkdir without recursive on the session folder itself: an existing session is never touched.
|
|
324
|
+
mkdirSync(dir)
|
|
325
|
+
try {
|
|
326
|
+
for (const [rel, bytes] of plan.assets) {
|
|
327
|
+
mkdirSync(join(dir, "attachments"), { recursive: true })
|
|
328
|
+
writeFileSync(join(dir, rel), bytes)
|
|
329
|
+
}
|
|
330
|
+
writeFileSync(paths.messages, plan.messages.map((m) => `${JSON.stringify(m)}\n`).join(""), { flag: "wx" })
|
|
331
|
+
// meta.json last: the listing reads it, and a folder without it is skipped.
|
|
332
|
+
writeFileSync(paths.meta, JSON.stringify(meta, null, 2), { flag: "wx" })
|
|
333
|
+
const back = await reader.read({ tool: TOOL, sourceId: sessionId, path: paths.messages, extra: { dir } }, w.home)
|
|
334
|
+
const n = countTurns(back)
|
|
335
|
+
const pairs = unpaired(back)
|
|
336
|
+
if (n.user !== plan.expect.users || n.assistant !== plan.expect.assistants || n.tool_call !== plan.expect.calls || pairs.calls.length || pairs.results.length)
|
|
337
|
+
throw new Error(
|
|
338
|
+
`the new Mistral Vibe session did not read back as written (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}); nothing was kept`,
|
|
339
|
+
)
|
|
340
|
+
} catch (e) {
|
|
341
|
+
rmSync(dir, { recursive: true, force: true })
|
|
342
|
+
throw e
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
tool: TOOL,
|
|
347
|
+
way: "import",
|
|
348
|
+
sessionId,
|
|
349
|
+
written: [paths.messages, paths.meta],
|
|
350
|
+
open: `cd ${shellQuote(cwd)} && vibe --resume ${sessionId}`,
|
|
351
|
+
note: "Vibe imports the session into its own store the first time it is resumed.",
|
|
352
|
+
lost: plan.lost,
|
|
353
|
+
dryRun: !!w.dryRun,
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export const mistralVibeImportWriter = mistralVibeWriter()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Reader, Writer } from "../types.ts"
|
|
2
|
+
import { reader } from "./reader.ts"
|
|
3
|
+
import { museNativeWriter } from "./writer.ts"
|
|
4
|
+
|
|
5
|
+
// Muse Code (Meta's coding agent): reads its durable session event streams under ~/.local/share/muse, and
|
|
6
|
+
// continues any session as a new native Muse session that `muse exec --session-id <id>` resumes.
|
|
7
|
+
// Format and lifecycle ported from session-migrate (xhluca/session-migrate, MIT), checked against Muse 0.2.1.
|
|
8
|
+
export { reader }
|
|
9
|
+
export const readers: Reader[] = [reader]
|
|
10
|
+
export const writers: Writer[] = [museNativeWriter]
|