@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,332 @@
|
|
|
1
|
+
import { statSync } from "node:fs"
|
|
2
|
+
import { basename, dirname, join } from "node:path"
|
|
3
|
+
import { exists, gitInfo, isDir, ls, mtime, readJsonl } from "../../fsutil.ts"
|
|
4
|
+
import { keepImage, parseDataUrl } from "../../session/attachments.ts"
|
|
5
|
+
import type { Attachment, Session, Turn, Usage } from "../../session/types.ts"
|
|
6
|
+
import { clip, header, lossCounter, noteModel } from "../../session/util.ts"
|
|
7
|
+
import { CONTINUED_NOTE } from "../claude-code/records.ts"
|
|
8
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
9
|
+
import { FAILED_PREFIX, isObj, isRetainedMarker, KNOWN_TYPES, kindOf, LABEL, QUIET_RUN_EVENTS, type Rec, runEvent, sessionsRoot, str, textBlocks, TOOL, usIso } from "./records.ts"
|
|
10
|
+
|
|
11
|
+
// Reads Muse Code sessions: <home>/.local/share/muse/sessions/YYYY/MM/DD/<uuid>/session.jsonl (records.ts).
|
|
12
|
+
// Semantics ported from session-migrate's Muse adapter (xhluca/session-migrate, MIT).
|
|
13
|
+
//
|
|
14
|
+
// A person's prompt is the text of runtime.user_intent.accepted; a run `started` event repeats it, and is the
|
|
15
|
+
// only record of a prompt Muse took another way (the capture's `--image` turn has no accepted intent), so a
|
|
16
|
+
// started prompt becomes a turn only when no accepted intent already gave it. The model's text, tool calls
|
|
17
|
+
// and tool results are the run's committed events; a failed call is the one whose tool_batch.effect.terminal
|
|
18
|
+
// outcome is not "completed", or whose result text Muse prefixed "tool failed:". Muse keeps no image bytes:
|
|
19
|
+
// when the request the model got carried far more user bytes than the prompt, that is counted as lost.
|
|
20
|
+
|
|
21
|
+
const PEEK = 64 * 1024
|
|
22
|
+
/** Extra bytes in a run's first request beyond its prompt that mean something else was sent with it. */
|
|
23
|
+
const ATTACHED_SLACK = 1024
|
|
24
|
+
|
|
25
|
+
type Lost = ReturnType<typeof lossCounter>
|
|
26
|
+
|
|
27
|
+
/** The prompt of an accepted intent: its text blocks without brain's note, which names the session it continues. */
|
|
28
|
+
function intentPrompt(p: Rec, lost?: Lost): { text: string; attachments: Attachment[]; continued?: string } {
|
|
29
|
+
const texts: string[] = []
|
|
30
|
+
const attachments: Attachment[] = []
|
|
31
|
+
for (const m of Array.isArray(p.model_messages) ? p.model_messages : []) {
|
|
32
|
+
if (!isObj(m) || !Array.isArray(m.content)) continue
|
|
33
|
+
for (const b of m.content) {
|
|
34
|
+
if (!isObj(b)) continue
|
|
35
|
+
if (b.kind === "text" && typeof b.text === "string") texts.push(b.text)
|
|
36
|
+
else if (lost) {
|
|
37
|
+
// Not seen in Muse 0.2.1's log; kept when it is a data URL, counted otherwise.
|
|
38
|
+
const d = parseDataUrl(b.url ?? b.image_url ?? b.data)
|
|
39
|
+
const kept = d ? keepImage(d.base64, d.mime) : undefined
|
|
40
|
+
if (kept) attachments.push(kept)
|
|
41
|
+
else lost.add("prompt attachments Muse did not keep")
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
let continued: string | undefined
|
|
46
|
+
const note = texts[0]?.match(CONTINUED_NOTE)
|
|
47
|
+
if (note) {
|
|
48
|
+
continued = note[1]
|
|
49
|
+
texts.shift()
|
|
50
|
+
}
|
|
51
|
+
return { text: texts.join("\n\n"), attachments, ...(continued ? { continued } : {}) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const usageOf = (u: unknown): Usage | undefined => {
|
|
55
|
+
if (!isObj(u)) return undefined
|
|
56
|
+
const out: Usage = {}
|
|
57
|
+
const cached = typeof u.cache_read_tokens === "number" ? u.cache_read_tokens : typeof u.cached_tokens === "number" ? u.cached_tokens : 0
|
|
58
|
+
if (typeof u.input_tokens === "number") out.input = Math.max(0, u.input_tokens - cached)
|
|
59
|
+
if (typeof u.output_tokens === "number") out.output = u.output_tokens
|
|
60
|
+
if (cached) out.cacheRead = cached
|
|
61
|
+
if (typeof u.cache_write_tokens === "number" && u.cache_write_tokens) out.cacheWrite = u.cache_write_tokens
|
|
62
|
+
return Object.keys(out).length ? out : undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseArgs(a: unknown): unknown {
|
|
66
|
+
if (typeof a !== "string") return a ?? {}
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(a)
|
|
69
|
+
} catch {
|
|
70
|
+
return { arguments: a }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** One Muse session's records (in file order) as the portable session; `bad` is the count of unreadable lines. */
|
|
75
|
+
export function parseSession(rows: Rec[], bad: number, path: string, home?: string): Session {
|
|
76
|
+
const first = rows.find((r) => !isRetainedMarker(r))
|
|
77
|
+
const meta = first?.payload_type === "runtime.session.metadata" && isObj(first.payload?.record) ? (first.payload.record as Rec) : undefined
|
|
78
|
+
if (!first || !meta) throw new Error(`${path} is not a ${LABEL} session (it does not begin with runtime.session.metadata)`)
|
|
79
|
+
const lost = lossCounter()
|
|
80
|
+
if (bad) lost.add("unreadable lines", bad)
|
|
81
|
+
|
|
82
|
+
const sourceId = str(first.stream?.id) ?? basename(dirname(path))
|
|
83
|
+
let cwd = str(meta.workspace_root)
|
|
84
|
+
if (!cwd) cwd = str(rows.find((r) => r.payload_type === "runtime.session.route_facts")?.payload?.record?.cwd)
|
|
85
|
+
const startedAt = usIso(first.recorded_at)
|
|
86
|
+
const h = header(TOOL, sourceId, { sourcePath: path, project: cwd ? { cwd, ...gitInfo(cwd, home) } : {}, ...(startedAt ? { startedAt } : {}) })
|
|
87
|
+
const version = str(meta.build?.semver)
|
|
88
|
+
if (version) h.toolVersion = version
|
|
89
|
+
let model = str(meta.model_id)
|
|
90
|
+
noteModel(h, model)
|
|
91
|
+
|
|
92
|
+
const turns: Turn[] = []
|
|
93
|
+
/** Accepted prompts whose run has not started yet, by text: the started event repeats them. */
|
|
94
|
+
const pending = new Map<string, number>()
|
|
95
|
+
const calls = new Map<string, Extract<Turn, { kind: "tool_call" }>>()
|
|
96
|
+
const answered = new Set<string>()
|
|
97
|
+
const failed = new Set<string>()
|
|
98
|
+
const prompts = new Map<string, string>() // run id → prompt
|
|
99
|
+
const traced = new Set<string>()
|
|
100
|
+
const reminders = new Set<string>()
|
|
101
|
+
let reply: Extract<Turn, { kind: "assistant" }> | undefined
|
|
102
|
+
let continued: string | undefined
|
|
103
|
+
let last: string | undefined
|
|
104
|
+
|
|
105
|
+
for (const r of rows) {
|
|
106
|
+
if (isRetainedMarker(r)) {
|
|
107
|
+
lost.add("status records Muse left out of its log")
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
const when = usIso(r.recorded_at)
|
|
111
|
+
if (when) last = when
|
|
112
|
+
const at = when ? { at: when } : {}
|
|
113
|
+
const p = isObj(r.payload) ? r.payload : {}
|
|
114
|
+
switch (r.payload_type) {
|
|
115
|
+
case "run.model.configured":
|
|
116
|
+
model = str(p.record?.model_id) ?? model
|
|
117
|
+
noteModel(h, model)
|
|
118
|
+
break
|
|
119
|
+
case "runtime.user_intent.accepted": {
|
|
120
|
+
const { text, attachments, continued: from } = intentPrompt(p, lost)
|
|
121
|
+
if (from) continued = from
|
|
122
|
+
if (!text.trim() && !attachments.length) break
|
|
123
|
+
turns.push({ kind: "user", ...at, text, ...(attachments.length ? { attachments } : {}) })
|
|
124
|
+
pending.set(text, (pending.get(text) ?? 0) + 1)
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
case "tool_batch.effect.terminal": {
|
|
128
|
+
const rec = isObj(p.record) ? p.record : {}
|
|
129
|
+
const outcome = isObj(rec.outcome) ? str(rec.outcome.kind) : undefined
|
|
130
|
+
const callId = str(rec.call_id)
|
|
131
|
+
if (callId && outcome && outcome !== "completed") failed.add(callId)
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
case "runtime.session":
|
|
135
|
+
run(r, at)
|
|
136
|
+
break
|
|
137
|
+
default:
|
|
138
|
+
if (!KNOWN_TYPES.has(String(r.payload_type))) lost.add(`unknown ${LABEL} record types: ${String(r.payload_type)}`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function run(r: Rec, at: { at?: string }) {
|
|
143
|
+
const e = runEvent(r)
|
|
144
|
+
if (!e) return // security mode, agent tree, task lifecycle: Muse's own bookkeeping
|
|
145
|
+
const runId = String(r.payload.run_id ?? "")
|
|
146
|
+
const kind = String(e.kind)
|
|
147
|
+
switch (kind) {
|
|
148
|
+
case "started": {
|
|
149
|
+
const prompt = typeof e.prompt === "string" ? e.prompt : ""
|
|
150
|
+
prompts.set(runId, prompt)
|
|
151
|
+
const note = prompt.match(CONTINUED_NOTE)
|
|
152
|
+
if (note) {
|
|
153
|
+
continued = note[1]
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
const waiting = pending.get(prompt) ?? 0
|
|
157
|
+
if (waiting) pending.set(prompt, waiting - 1)
|
|
158
|
+
else if (prompt.trim()) turns.push({ kind: "user", ...at, text: prompt })
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
case "assistant_message_committed": {
|
|
162
|
+
if (typeof e.text !== "string" || !e.text.trim()) return
|
|
163
|
+
reply = { kind: "assistant", ...at, text: e.text, ...(model ? { model } : {}) }
|
|
164
|
+
turns.push(reply)
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
case "reasoning_committed":
|
|
168
|
+
if (typeof e.text === "string" && e.text.trim()) turns.push({ kind: "reasoning", ...at, text: e.text })
|
|
169
|
+
else lost.add("encrypted or empty reasoning")
|
|
170
|
+
return
|
|
171
|
+
case "assistant_tool_calls_committed":
|
|
172
|
+
for (const c of Array.isArray(e.tool_calls) ? e.tool_calls : []) {
|
|
173
|
+
if (!isObj(c)) continue
|
|
174
|
+
const id = str(c.call_id) ?? str(c.id)
|
|
175
|
+
if (!id || calls.has(id)) continue
|
|
176
|
+
const name = str(c.name) ?? "unknown_tool"
|
|
177
|
+
const call: Extract<Turn, { kind: "tool_call" }> = { kind: "tool_call", ...at, id, name, toolKind: kindOf(name), input: parseArgs(c.args ?? c.arguments) }
|
|
178
|
+
calls.set(id, call)
|
|
179
|
+
turns.push(call)
|
|
180
|
+
}
|
|
181
|
+
return
|
|
182
|
+
case "tool_result_batch_committed":
|
|
183
|
+
for (const res of Array.isArray(e.results) ? e.results : []) {
|
|
184
|
+
if (!isObj(res)) continue
|
|
185
|
+
const callId = str(res.tool_call_id)
|
|
186
|
+
if (!callId || !calls.has(callId)) {
|
|
187
|
+
lost.add("tool results without a call")
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
if (answered.has(callId)) continue
|
|
191
|
+
answered.add(callId)
|
|
192
|
+
const text = typeof res.text === "string" ? res.text : ""
|
|
193
|
+
const ok = !failed.has(callId) && !text.startsWith(FAILED_PREFIX.trimEnd())
|
|
194
|
+
const c = clip(text)
|
|
195
|
+
turns.push({ kind: "tool_result", ...at, callId, ok, output: c.text, ...(c.truncated ? { truncated: true } : {}) })
|
|
196
|
+
// A file tool that succeeded changed the file it names; Muse keeps no diff beside the call.
|
|
197
|
+
const call = calls.get(callId)!
|
|
198
|
+
const input = isObj(call.input) ? call.input : {}
|
|
199
|
+
const target = str(input.path) ?? str(input.file_path)
|
|
200
|
+
if (ok && target && (call.toolKind === "edit" || call.toolKind === "write")) turns.push({ kind: "file_edit", ...at, path: target, callId })
|
|
201
|
+
}
|
|
202
|
+
return
|
|
203
|
+
case "model_completed": {
|
|
204
|
+
noteModel(h, str(e.model))
|
|
205
|
+
const usage = usageOf(e.usage)
|
|
206
|
+
if (reply && usage && !reply.usage) reply.usage = usage
|
|
207
|
+
reply = undefined
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
case "model_input_trace_recorded": {
|
|
211
|
+
// The first request of a run says how many bytes the person's message had; Muse logs only its text.
|
|
212
|
+
if (traced.has(runId) || !prompts.has(runId)) return
|
|
213
|
+
traced.add(runId)
|
|
214
|
+
const lanes = Array.isArray(e.bounded?.aggregates) ? (e.bounded.aggregates as Rec[]) : []
|
|
215
|
+
const sent = lanes.filter((l) => l?.source?.value === "current_user").reduce((n, l) => n + (typeof l.byte_count === "number" ? l.byte_count : 0), 0)
|
|
216
|
+
if (sent > Buffer.byteLength(prompts.get(runId)!) + ATTACHED_SLACK) lost.add(`images or expanded files sent with prompts, not kept by ${LABEL}`)
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
case "memory_reminder_child_session_linked":
|
|
220
|
+
reminders.add(String(e.child_session_id ?? e.task_id ?? reminders.size))
|
|
221
|
+
return
|
|
222
|
+
case "context_block_updated":
|
|
223
|
+
lost.add(`context blocks ${LABEL} injected`)
|
|
224
|
+
return
|
|
225
|
+
default:
|
|
226
|
+
if (/compact/i.test(kind)) {
|
|
227
|
+
const summary = str(e.summary) ?? str(e.text)
|
|
228
|
+
if (summary) turns.push({ kind: "compaction", ...at, summary })
|
|
229
|
+
else lost.add("compactions without a readable summary")
|
|
230
|
+
} else if (!QUIET_RUN_EVENTS.has(kind)) lost.add(`unknown ${LABEL} run events: ${kind}`)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (reminders.size) lost.add(`${LABEL} reminder-agent runs`, reminders.size)
|
|
235
|
+
// Calls the log never answered get a result saying so, so every call is paired.
|
|
236
|
+
const open = [...calls.keys()].filter((id) => !answered.has(id))
|
|
237
|
+
if (open.length) lost.add("tool results not recorded", open.length)
|
|
238
|
+
for (const id of open) {
|
|
239
|
+
const i = turns.findIndex((t) => t.kind === "tool_call" && t.id === id)
|
|
240
|
+
turns.splice(i + 1, 0, { kind: "tool_result", callId: id, ok: false, output: "(no result recorded)" })
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const title = (turns.find((t) => t.kind === "user") as Extract<Turn, { kind: "user" }> | undefined)?.text.replace(/\s+/g, " ").trim().slice(0, 80)
|
|
244
|
+
if (title) h.title = title
|
|
245
|
+
if (last) h.updatedAt = last
|
|
246
|
+
if (continued) h.parent = { id: continued, kind: "continued" }
|
|
247
|
+
lost.flush(h)
|
|
248
|
+
return { header: h, turns }
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The session id, folder and first prompt, from the start of a file only. */
|
|
252
|
+
async function peek(path: string): Promise<Omit<SessionRef, "tool" | "path"> | undefined> {
|
|
253
|
+
const file = Bun.file(path)
|
|
254
|
+
const whole = file.size <= PEEK
|
|
255
|
+
const lines = (whole ? await file.text() : await file.slice(0, PEEK).text()).split("\n")
|
|
256
|
+
if (!whole) lines.pop() // the cut line
|
|
257
|
+
let meta: Rec | undefined
|
|
258
|
+
let prompt: string | undefined
|
|
259
|
+
for (const line of lines) {
|
|
260
|
+
if (!line.trim()) continue
|
|
261
|
+
let r: Rec
|
|
262
|
+
try {
|
|
263
|
+
r = JSON.parse(line)
|
|
264
|
+
} catch {
|
|
265
|
+
continue // a torn last line
|
|
266
|
+
}
|
|
267
|
+
if (!isObj(r) || isRetainedMarker(r)) continue
|
|
268
|
+
if (!meta) {
|
|
269
|
+
if (r.payload_type !== "runtime.session.metadata") return undefined
|
|
270
|
+
meta = r
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
if (r.payload_type === "runtime.user_intent.accepted") prompt = intentPrompt(r.payload ?? {}).text
|
|
274
|
+
else {
|
|
275
|
+
const e = runEvent(r)
|
|
276
|
+
if (e?.kind === "started" && typeof e.prompt === "string" && !CONTINUED_NOTE.test(e.prompt)) prompt = e.prompt
|
|
277
|
+
}
|
|
278
|
+
if (prompt?.trim()) break
|
|
279
|
+
prompt = undefined
|
|
280
|
+
}
|
|
281
|
+
// A session Muse opened but never prompted has nothing to continue.
|
|
282
|
+
if (!meta || (!prompt && whole)) return undefined
|
|
283
|
+
const cwd = str(meta.payload?.record?.workspace_root)
|
|
284
|
+
const title = prompt?.replace(/\s+/g, " ").trim().slice(0, 80)
|
|
285
|
+
const updatedAt = mtime(path)
|
|
286
|
+
return {
|
|
287
|
+
sourceId: str(meta.stream?.id) ?? basename(dirname(path)),
|
|
288
|
+
...(title ? { title } : {}),
|
|
289
|
+
...(cwd ? { cwd } : {}),
|
|
290
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Every session file: sessions/<year>/<month>/<day>/<id>/session.jsonl (subagent logs below a session are not sessions). */
|
|
295
|
+
export function sessionFiles(home: string): string[] {
|
|
296
|
+
const root = sessionsRoot(home)
|
|
297
|
+
const out: string[] = []
|
|
298
|
+
for (const y of ls(root).filter((n) => /^\d{4}$/.test(n)))
|
|
299
|
+
for (const m of ls(join(root, y)).filter((n) => /^\d{2}$/.test(n)))
|
|
300
|
+
for (const d of ls(join(root, y, m)).filter((n) => /^\d{2}$/.test(n)))
|
|
301
|
+
for (const id of ls(join(root, y, m, d))) {
|
|
302
|
+
const p = join(root, y, m, d, id, "session.jsonl")
|
|
303
|
+
if (isDir(join(root, y, m, d, id)) && exists(p)) out.push(p)
|
|
304
|
+
}
|
|
305
|
+
return out
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export const reader: Reader = {
|
|
309
|
+
tool: TOOL,
|
|
310
|
+
label: LABEL,
|
|
311
|
+
|
|
312
|
+
async list(home) {
|
|
313
|
+
const refs: SessionRef[] = []
|
|
314
|
+
for (const path of sessionFiles(home)) {
|
|
315
|
+
try {
|
|
316
|
+
if (!statSync(path).isFile()) continue
|
|
317
|
+
const p = await peek(path)
|
|
318
|
+
if (p) refs.push({ tool: TOOL, path, ...p })
|
|
319
|
+
} catch {
|
|
320
|
+
// Deleted or unreadable between listing and reading: not a session to offer.
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
async read(ref, home) {
|
|
327
|
+
if (!exists(ref.path)) throw new Error(`no ${LABEL} session at ${ref.path}`)
|
|
328
|
+
const { rows, bad } = readJsonl(ref.path)
|
|
329
|
+
if (!rows.length) throw new Error(`${ref.path} holds no ${LABEL} records`)
|
|
330
|
+
return parseSession(rows, bad, ref.path, home)
|
|
331
|
+
},
|
|
332
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
import type { ToolKind } from "../../session/types.ts"
|
|
3
|
+
import { toolKindOf } from "../../session/util.ts"
|
|
4
|
+
|
|
5
|
+
// Facts about Muse Code's session store, shared by its reader and writer. Ported from session-migrate
|
|
6
|
+
// (xhluca/session-migrate, MIT licence: src/session_migrate/formats/muse.py and docs/muse-qwen-kimi-formats.md),
|
|
7
|
+
// checked against the real capture of Muse Code 0.2.1 (0.2.1-R1215.1, build b3170a534f) in
|
|
8
|
+
// test/fixtures/native-corpus/muse.
|
|
9
|
+
//
|
|
10
|
+
// Muse keeps one durable JSONL event stream per session:
|
|
11
|
+
// $XDG_DATA_HOME/muse/sessions/YYYY/MM/DD/<uuid>/session.jsonl (XDG data home defaults to ~/.local/share)
|
|
12
|
+
// Every line is an envelope {schema_version: 1, id, stream: {kind: "session", id}, sequence (1, 2, ...),
|
|
13
|
+
// recorded_at (microseconds, strictly increasing), record_type: "event", durability: "durable", causation_id,
|
|
14
|
+
// payload_type, payload_schema_version, payload}. The first is runtime.session.metadata (workspace_root, build,
|
|
15
|
+
// model). Each turn the person starts is runtime.user_intent.accepted (the prompt as model_messages and
|
|
16
|
+
// refill_blocks), a runtime.session run event `started`, and runtime.user_intent.materialized linking both;
|
|
17
|
+
// the model's work follows as run events (assistant_message_committed, assistant_tool_calls_committed,
|
|
18
|
+
// tool_result_batch_committed) and the run ends with `terminal`. Each process start and exit adds
|
|
19
|
+
// session.opened.observed and session.end; a resume adds session.resumed. Most of the rest is telemetry:
|
|
20
|
+
// tasks, reminder agents (child sessions under <uuid>/subagent/), context diagnostics, usage.
|
|
21
|
+
|
|
22
|
+
export const TOOL = "muse"
|
|
23
|
+
export const LABEL = "Muse Code"
|
|
24
|
+
/** The command (`muse --version` prints "Muse Code 0.2.1 (0.2.1-R1215.1)"). */
|
|
25
|
+
export const BIN = "muse"
|
|
26
|
+
|
|
27
|
+
/** The build Muse Code 0.2.1 records in its session metadata. */
|
|
28
|
+
export const BUILD_SHA = "b3170a534f"
|
|
29
|
+
|
|
30
|
+
export type Rec = Record<string, any>
|
|
31
|
+
|
|
32
|
+
export const isObj = (v: unknown): v is Rec => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
33
|
+
export const str = (v: unknown): string | undefined => (typeof v === "string" && v.trim() ? v : undefined)
|
|
34
|
+
|
|
35
|
+
/** <home>/.local/share/muse, the default XDG data home; $XDG_DATA_HOME is not followed, as other adapters do not follow theirs. */
|
|
36
|
+
export const museHome = (home: string) => join(home, ".local", "share", "muse")
|
|
37
|
+
export const sessionsRoot = (home: string) => join(museHome(home), "sessions")
|
|
38
|
+
|
|
39
|
+
const pad = (n: number) => String(n).padStart(2, "0")
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Where a session started at `at` lives. Muse dates the folder in local time: the capture started at
|
|
43
|
+
* 2026-09-01T00:38Z and session-migrate's reload test puts it under 2026/08/31.
|
|
44
|
+
*/
|
|
45
|
+
export const sessionPath = (home: string, id: string, at: Date) =>
|
|
46
|
+
join(sessionsRoot(home), String(at.getFullYear()), pad(at.getMonth() + 1), pad(at.getDate()), id, "session.jsonl")
|
|
47
|
+
|
|
48
|
+
/** Muse's recorded_at (microseconds since the epoch) as ISO time. */
|
|
49
|
+
export function usIso(v: unknown): string | undefined {
|
|
50
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) return undefined
|
|
51
|
+
return new Date(Math.floor(v / 1000)).toISOString()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The durable placeholder Muse writes in place of an ephemeral status record it left out. */
|
|
55
|
+
export function isRetainedMarker(r: Rec): boolean {
|
|
56
|
+
const o = r.omitted_record
|
|
57
|
+
return typeof r.retained_marker === "string" && isObj(r.position) && isObj(r.stream) && isObj(o) && o.durability === "ephemeral"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The run event of a runtime.session record, when it is one. */
|
|
61
|
+
export const runEvent = (r: Rec): Rec | undefined => (r.payload_type === "runtime.session" && isObj(r.payload) && r.payload.kind === "run" && isObj(r.payload.event) ? r.payload.event : undefined)
|
|
62
|
+
|
|
63
|
+
/** Text blocks of a Muse content list ({kind: "text", text}). */
|
|
64
|
+
export const textBlocks = (content: unknown): string[] =>
|
|
65
|
+
Array.isArray(content) ? content.filter((b) => isObj(b) && b.kind === "text" && typeof b.text === "string").map((b) => (b as Rec).text as string) : []
|
|
66
|
+
|
|
67
|
+
/** What Muse's built-in tools do; read_file is the one the capture shows, others go by name. */
|
|
68
|
+
const MUSE_KINDS: Record<string, ToolKind> = { read_file: "read" }
|
|
69
|
+
export const kindOf = (name: string): ToolKind => MUSE_KINDS[name] ?? toolKindOf(name)
|
|
70
|
+
|
|
71
|
+
/** How Muse words a failed tool's result for the model (it keeps no separate error flag on the result). */
|
|
72
|
+
export const FAILED_PREFIX = "tool failed: "
|
|
73
|
+
|
|
74
|
+
/** Run events that are telemetry, not conversation. */
|
|
75
|
+
export const QUIET_RUN_EVENTS = new Set([
|
|
76
|
+
"started",
|
|
77
|
+
"model_completed",
|
|
78
|
+
"model_response_created",
|
|
79
|
+
"terminal",
|
|
80
|
+
"context_block_diagnostic",
|
|
81
|
+
"model_request_configured",
|
|
82
|
+
"provider_request_options_configured",
|
|
83
|
+
"model_input_trace_recorded",
|
|
84
|
+
"goal_usage_attribution",
|
|
85
|
+
"resource_usage_sampled",
|
|
86
|
+
"task_stream_linked",
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
/** Record types that carry nothing of the conversation (or that the reader handles itself). */
|
|
90
|
+
export const KNOWN_TYPES = new Set([
|
|
91
|
+
"runtime.session",
|
|
92
|
+
"runtime.session.metadata",
|
|
93
|
+
"runtime.session.route_facts",
|
|
94
|
+
"session.opened.observed",
|
|
95
|
+
"session.resumed",
|
|
96
|
+
"session.end",
|
|
97
|
+
"runtime.command_intake.received",
|
|
98
|
+
"runtime.command_intake.settled",
|
|
99
|
+
"run.model.configured",
|
|
100
|
+
"runtime.user_intent.accepted",
|
|
101
|
+
"runtime.user_intent.materialized",
|
|
102
|
+
"tool_batch.effect.started",
|
|
103
|
+
"tool_batch.effect.terminal",
|
|
104
|
+
])
|