@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,323 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import { exists, gitInfo } from "../../fsutil.ts"
|
|
3
|
+
import { keepImage, parseDataUrl } from "../../session/attachments.ts"
|
|
4
|
+
import { withSnapshot } from "../../sqlite.ts"
|
|
5
|
+
import type { Attachment, Session, Turn } from "../../session/types.ts"
|
|
6
|
+
import { asText, clip, header, iso, lossCounter, noteModel } from "../../session/util.ts"
|
|
7
|
+
import { CONTINUED_NOTE } from "../opencode/note.ts"
|
|
8
|
+
import type { Reader, SessionRef } from "../types.ts"
|
|
9
|
+
import { LABEL, TOOL, mastraToolKind, metadataOf, storePaths, type Part } from "./records.ts"
|
|
10
|
+
|
|
11
|
+
// Reads MastraCode threads from its one store (records.ts), always from a copy taken with its write-ahead
|
|
12
|
+
// log, so a running MastraCode is never disturbed and the newest committed turns are included.
|
|
13
|
+
// Semantics ported from session-migrate's mastracode.py (MIT): a "signal" row of type user is the person's
|
|
14
|
+
// prompt; an assistant row holds every step of a reply, with each tool call and its result folded into one
|
|
15
|
+
// tool-invocation part (unfolded here into a call and a result); readable reasoning is kept; image file parts
|
|
16
|
+
// become attachments; the thread's active observational memory is its compaction. MastraCode's runtime parts
|
|
17
|
+
// (workspace and sandbox metadata) are counted as lost; step boundaries are structure, not content.
|
|
18
|
+
|
|
19
|
+
/** The stores on this machine: the platform's path, and the others when present (a home copied between systems). */
|
|
20
|
+
const stores = (home: string) => [...new Set(Object.values(storePaths(home)))].filter(exists)
|
|
21
|
+
|
|
22
|
+
export function listThreads(d: Database, path: string): SessionRef[] {
|
|
23
|
+
const rows = d
|
|
24
|
+
.query(
|
|
25
|
+
`SELECT t.id, t.resourceId, t.title, t.metadata, t.updatedAt FROM mastra_threads t
|
|
26
|
+
WHERE EXISTS (SELECT 1 FROM mastra_messages m WHERE m.thread_id = t.id)`,
|
|
27
|
+
)
|
|
28
|
+
.all() as Array<Record<string, any>>
|
|
29
|
+
return rows.map((r) => {
|
|
30
|
+
const cwd = metadataOf(r.metadata).projectPath
|
|
31
|
+
const updatedAt = iso(r.updatedAt)
|
|
32
|
+
return {
|
|
33
|
+
tool: TOOL,
|
|
34
|
+
sourceId: String(r.id),
|
|
35
|
+
path,
|
|
36
|
+
...(r.title ? { title: String(r.title) } : {}),
|
|
37
|
+
...(typeof cwd === "string" && cwd ? { cwd } : {}),
|
|
38
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
39
|
+
extra: { resourceId: String(r.resourceId ?? "") },
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const mastracodeReader: Reader = {
|
|
45
|
+
tool: TOOL,
|
|
46
|
+
label: LABEL,
|
|
47
|
+
async list(home) {
|
|
48
|
+
const refs: SessionRef[] = []
|
|
49
|
+
for (const db of stores(home)) {
|
|
50
|
+
try {
|
|
51
|
+
refs.push(...withSnapshot(db, (d) => listThreads(d, db)))
|
|
52
|
+
} catch {
|
|
53
|
+
// A store that cannot be copied or opened lists nothing; read() says why for a thread in it.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return refs.sort((a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? ""))
|
|
57
|
+
},
|
|
58
|
+
async read(ref, home) {
|
|
59
|
+
if (!exists(ref.path)) throw new Error(`MastraCode store not found: ${ref.path}`)
|
|
60
|
+
return withSnapshot(ref.path, (d) => readMastra(d, ref.sourceId, ref.path, home))
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const json = (v: unknown): any => {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(v instanceof Uint8Array ? new TextDecoder().decode(v) : String(v))
|
|
67
|
+
} catch {
|
|
68
|
+
return undefined
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The prefix brain's writer puts before a compaction summary it stores as a user row. */
|
|
73
|
+
export const SUMMARY_PREFIX = /^\[Summary of the earlier conversation in [^\]\n]*\]\n\n/
|
|
74
|
+
|
|
75
|
+
/** A tool result's text: strings as they are, text blocks joined (MCP and AI SDK shapes), else JSON. */
|
|
76
|
+
function resultText(v: unknown): { text: string; images: number } {
|
|
77
|
+
if (typeof v === "string") return { text: v, images: 0 }
|
|
78
|
+
const blocks = Array.isArray(v) ? v : v && typeof v === "object" && Array.isArray((v as any).content) ? (v as any).content : undefined
|
|
79
|
+
// session-migrate's wrapper for a result whose text and structured content differ.
|
|
80
|
+
if (v && typeof v === "object" && (v as any).schema === "session-migrate.mastracode-tool-result.v1" && typeof (v as any).text === "string") return { text: (v as any).text, images: 0 }
|
|
81
|
+
if (v && typeof v === "object" && !Array.isArray(v) && (v as any).type === "text" && typeof (v as any).value === "string") return { text: (v as any).value, images: 0 }
|
|
82
|
+
if (!blocks) return { text: asText(v), images: 0 }
|
|
83
|
+
const parts: string[] = []
|
|
84
|
+
let images = 0
|
|
85
|
+
for (const b of blocks as any[]) {
|
|
86
|
+
if (typeof b === "string") parts.push(b)
|
|
87
|
+
else if (typeof b?.text === "string") parts.push(b.text)
|
|
88
|
+
else if (b?.type === "image" || b?.type === "media") images++
|
|
89
|
+
else parts.push(asText(b))
|
|
90
|
+
}
|
|
91
|
+
return { text: parts.join("\n"), images }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const resultFailed = (v: unknown) => !!v && typeof v === "object" && !Array.isArray(v) && (v as any).isError === true
|
|
95
|
+
|
|
96
|
+
/** One thread from an open MastraCode store. */
|
|
97
|
+
export function readMastra(d: Database, id: string, path: string, home: string): Session {
|
|
98
|
+
const thread = d.query("SELECT id, resourceId, title, metadata, createdAt, updatedAt FROM mastra_threads WHERE id = ?").get(id) as Record<string, any> | null
|
|
99
|
+
if (!thread) throw new Error(`no MastraCode thread ${id} in ${path}`)
|
|
100
|
+
const meta = metadataOf(thread.metadata)
|
|
101
|
+
const cwd = typeof meta.projectPath === "string" && meta.projectPath ? (meta.projectPath as string) : undefined
|
|
102
|
+
const h = header(TOOL, id, {
|
|
103
|
+
sourcePath: path,
|
|
104
|
+
...(thread.title ? { title: String(thread.title) } : {}),
|
|
105
|
+
project: cwd ? { cwd, ...gitInfo(cwd, home) } : {},
|
|
106
|
+
...(iso(thread.createdAt) ? { startedAt: iso(thread.createdAt)! } : {}),
|
|
107
|
+
...(iso(thread.updatedAt) ? { updatedAt: iso(thread.updatedAt)! } : {}),
|
|
108
|
+
})
|
|
109
|
+
if (typeof meta.brain?.continues === "string") h.parent = { id: meta.brain.continues, kind: "continued" }
|
|
110
|
+
const threadModel = typeof meta.currentModelId === "string" && meta.currentModelId ? (meta.currentModelId as string) : undefined
|
|
111
|
+
const lost = lossCounter()
|
|
112
|
+
const turns: Turn[] = []
|
|
113
|
+
const callIds = new Set<string>()
|
|
114
|
+
|
|
115
|
+
// The thread's observational memory, MastraCode's own compaction: what it currently uses in place of older turns.
|
|
116
|
+
try {
|
|
117
|
+
const om = d.query("SELECT activeObservations FROM mastra_observational_memory WHERE threadId = ? ORDER BY updatedAt DESC LIMIT 1").get(id) as { activeObservations?: unknown } | null
|
|
118
|
+
const summary = typeof om?.activeObservations === "string" ? om.activeObservations.trim() : ""
|
|
119
|
+
if (summary) turns.push({ kind: "compaction", ...(h.startedAt ? { at: h.startedAt } : {}), summary })
|
|
120
|
+
} catch {
|
|
121
|
+
// No observational memory table (it is created when MastraCode first observes) or another layout.
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const rows = d.query("SELECT id, content, role, type, createdAt FROM mastra_messages WHERE thread_id = ? ORDER BY createdAt, id").all(id) as Array<Record<string, any>>
|
|
125
|
+
for (const row of rows) {
|
|
126
|
+
const rowAt = iso(row.createdAt)
|
|
127
|
+
let content = json(row.content)
|
|
128
|
+
// Older Mastra rows hold the message's content directly: a string, or a list of AI SDK content parts.
|
|
129
|
+
if (typeof content === "string") content = { format: 2, parts: [{ type: "text", text: content }] }
|
|
130
|
+
else if (Array.isArray(content)) content = { format: 2, parts: content }
|
|
131
|
+
if (!content || typeof content !== "object" || !Array.isArray(content.parts)) {
|
|
132
|
+
lost.add("unreadable messages")
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
const cmeta = (content.metadata && typeof content.metadata === "object" ? content.metadata : {}) as Record<string, any>
|
|
136
|
+
if (row.id === "om-continuation" || cmeta.sessionMigrate?.internal === true) {
|
|
137
|
+
lost.add("observational-memory continuation messages")
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
const role = String(row.role)
|
|
141
|
+
if (role === "system") {
|
|
142
|
+
lost.add("system messages")
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
if (role === "signal") {
|
|
146
|
+
const kind = String(cmeta.signal?.type ?? row.type ?? "")
|
|
147
|
+
if (kind !== "user") {
|
|
148
|
+
lost.add(`signals the tool injected (${kind || "untyped"})`)
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
} else if (role !== "user" && role !== "assistant" && role !== "tool") {
|
|
152
|
+
lost.add(`unknown message roles: ${role}`)
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
const parts = content.parts as Part[]
|
|
156
|
+
const textOf = (p: Part) => (typeof p.text === "string" ? p.text : "")
|
|
157
|
+
|
|
158
|
+
// A compaction summary stored as a tagged user row (by brain, or by session-migrate).
|
|
159
|
+
if (cmeta.brain?.kind === "compaction" || cmeta.sessionMigrate?.kind === "compaction") {
|
|
160
|
+
const summary = parts.filter((p) => p?.type === "text").map(textOf).join("\n\n").replace(SUMMARY_PREFIX, "")
|
|
161
|
+
if (summary.trim()) turns.push({ kind: "compaction", ...(rowAt ? { at: rowAt } : {}), summary })
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const user = role === "user" || role === "signal"
|
|
166
|
+
const model =
|
|
167
|
+
(typeof cmeta.modelId === "string" && cmeta.modelId) ||
|
|
168
|
+
(parts.find((p) => p?.type === "step-start" && typeof p.model === "string")?.model as string | undefined) ||
|
|
169
|
+
(user ? undefined : threadModel) ||
|
|
170
|
+
undefined
|
|
171
|
+
// A part's own time when it has one (tool invocations have none: they take the time of the part before).
|
|
172
|
+
let lastAt = rowAt
|
|
173
|
+
const stampOf = (p: Part) => {
|
|
174
|
+
const at = typeof p.createdAt === "number" ? iso(p.createdAt) : undefined
|
|
175
|
+
if (at) lastAt = at
|
|
176
|
+
return lastAt
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// The person's prompt: its texts joined, with the images pasted into it.
|
|
180
|
+
let userText: string[] = []
|
|
181
|
+
let attachments: Attachment[] = []
|
|
182
|
+
let userAt: string | undefined
|
|
183
|
+
const flushUser = () => {
|
|
184
|
+
const text = userText.join("\n\n")
|
|
185
|
+
const note = text.match(CONTINUED_NOTE)
|
|
186
|
+
if (note) h.parent ??= { id: note[1]!, kind: "continued" }
|
|
187
|
+
else if (text.trim() || attachments.length) turns.push({ kind: "user", ...(userAt ? { at: userAt } : {}), text, ...(attachments.length ? { attachments } : {}) })
|
|
188
|
+
userText = []
|
|
189
|
+
attachments = []
|
|
190
|
+
userAt = undefined
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const call = (p: Part, callId: string, name: string, input: unknown) => {
|
|
194
|
+
let cid = callId
|
|
195
|
+
for (let n = 2; callIds.has(cid); n++) cid = `${callId}#${n}`
|
|
196
|
+
callIds.add(cid)
|
|
197
|
+
const at = stampOf(p)
|
|
198
|
+
turns.push({ kind: "tool_call", ...(at ? { at } : {}), id: cid, name, toolKind: mastraToolKind(name), input: input && typeof input === "object" ? input : {} })
|
|
199
|
+
return cid
|
|
200
|
+
}
|
|
201
|
+
const result = (p: Part, callId: string, ok: boolean, value: unknown) => {
|
|
202
|
+
const { text, images } = resultText(value)
|
|
203
|
+
if (images) lost.add("images in tool results", images)
|
|
204
|
+
const out = clip(text)
|
|
205
|
+
const at = stampOf(p)
|
|
206
|
+
turns.push({ kind: "tool_result", ...(at ? { at } : {}), callId, ok, output: out.text, ...(out.truncated ? { truncated: true } : {}) })
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const p of parts) {
|
|
210
|
+
const type = String(p?.type ?? "")
|
|
211
|
+
if (type === "text") {
|
|
212
|
+
const text = textOf(p)
|
|
213
|
+
if (user) {
|
|
214
|
+
userAt ??= stampOf(p)
|
|
215
|
+
userText.push(text)
|
|
216
|
+
} else if (text.trim()) {
|
|
217
|
+
noteModel(h, model)
|
|
218
|
+
const at = stampOf(p)
|
|
219
|
+
turns.push({ kind: "assistant", ...(at ? { at } : {}), text, ...(model ? { model } : {}) })
|
|
220
|
+
}
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
if (type === "file" || type === "image") {
|
|
224
|
+
const mime = String(p.mimeType ?? p.mediaType ?? "")
|
|
225
|
+
const raw = type === "image" ? p.image : (p.data ?? p.url)
|
|
226
|
+
const data = typeof raw === "string" ? (parseDataUrl(raw)?.base64 ?? (raw.startsWith("data:") || /^https?:/.test(raw) ? undefined : raw)) : undefined
|
|
227
|
+
if (!mime.startsWith("image/")) {
|
|
228
|
+
lost.add("file attachments")
|
|
229
|
+
continue
|
|
230
|
+
}
|
|
231
|
+
const kept = data ? keepImage(data, mime) : undefined
|
|
232
|
+
if (!kept) lost.add("images")
|
|
233
|
+
else if (!user) lost.add("images in replies")
|
|
234
|
+
else {
|
|
235
|
+
userAt ??= stampOf(p)
|
|
236
|
+
attachments.push(kept)
|
|
237
|
+
}
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
if (type === "data-user-message" && p.data && typeof p.data === "object") {
|
|
241
|
+
const text = typeof p.data.message === "string" ? p.data.message : typeof p.data.content === "string" ? p.data.content : ""
|
|
242
|
+
if (text) {
|
|
243
|
+
if (!user) flushUser()
|
|
244
|
+
userAt ??= stampOf(p)
|
|
245
|
+
userText.push(text)
|
|
246
|
+
if (!user) flushUser()
|
|
247
|
+
continue
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (type === "reasoning") {
|
|
251
|
+
const details = Array.isArray(p.details) ? (p.details as any[]) : []
|
|
252
|
+
const text =
|
|
253
|
+
(typeof p.reasoning === "string" && p.reasoning) ||
|
|
254
|
+
(typeof p.text === "string" && p.text) ||
|
|
255
|
+
details
|
|
256
|
+
.filter((x) => x?.type === "text" && typeof x.text === "string")
|
|
257
|
+
.map((x) => x.text as string)
|
|
258
|
+
.join("\n\n")
|
|
259
|
+
const signed = details.some((x) => x?.signature) || /signature|encrypted/i.test(JSON.stringify(p.providerMetadata ?? {}))
|
|
260
|
+
if (text.trim()) {
|
|
261
|
+
const at = stampOf(p)
|
|
262
|
+
turns.push({ kind: "reasoning", ...(at ? { at } : {}), text })
|
|
263
|
+
if (signed) lost.add("reasoning signatures")
|
|
264
|
+
} else if (details.some((x) => x?.type === "redacted")) lost.add("redacted reasoning")
|
|
265
|
+
else if (signed) lost.add("signed reasoning")
|
|
266
|
+
continue
|
|
267
|
+
}
|
|
268
|
+
// A tool call with its result folded in (AI SDK v4 UI part), as MastraCode stores it.
|
|
269
|
+
if (type === "tool-invocation" && p.toolInvocation && typeof p.toolInvocation === "object") {
|
|
270
|
+
const inv = p.toolInvocation as Record<string, any>
|
|
271
|
+
const name = typeof inv.toolName === "string" && inv.toolName ? inv.toolName : "unknown"
|
|
272
|
+
if (typeof inv.toolCallId !== "string" || !inv.toolCallId) {
|
|
273
|
+
lost.add("tool calls without an id")
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
if (user) flushUser()
|
|
277
|
+
const cid = call(p, inv.toolCallId, name, inv.args)
|
|
278
|
+
if (inv.state === "result" && "result" in inv) result(p, cid, inv.isError !== true && !resultFailed(inv.result), inv.result)
|
|
279
|
+
else if (inv.state === "output-error") result(p, cid, false, typeof inv.errorText === "string" ? inv.errorText : asText(inv.errorText))
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
// AI SDK v5 tool parts ("tool-<name>", "dynamic-tool") and model-message tool-call / tool-result parts.
|
|
283
|
+
if ((type.startsWith("tool-") && typeof p.toolCallId === "string" && type !== "tool-result" && type !== "tool-call") || type === "dynamic-tool") {
|
|
284
|
+
const name = type === "dynamic-tool" ? String(p.toolName ?? "unknown") : type.slice(5)
|
|
285
|
+
if (user) flushUser()
|
|
286
|
+
const cid = call(p, String(p.toolCallId), name, p.input)
|
|
287
|
+
if (p.state === "output-available") result(p, cid, !resultFailed(p.output), p.output)
|
|
288
|
+
else if (p.state === "output-error") result(p, cid, false, typeof p.errorText === "string" ? p.errorText : asText(p.errorText))
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
if (type === "tool-call" && typeof p.toolCallId === "string") {
|
|
292
|
+
if (user) flushUser()
|
|
293
|
+
call(p, p.toolCallId, String(p.toolName ?? "unknown"), p.args ?? p.input)
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
if (type === "tool-result" && typeof p.toolCallId === "string") {
|
|
297
|
+
if (user) flushUser()
|
|
298
|
+
const value = "result" in p ? p.result : p.output
|
|
299
|
+
result(p, p.toolCallId, p.isError !== true && !resultFailed(value), value)
|
|
300
|
+
continue
|
|
301
|
+
}
|
|
302
|
+
// Step boundaries carry the model, which is already read; no content.
|
|
303
|
+
if (type === "step-start") continue
|
|
304
|
+
if (type.startsWith("data-")) {
|
|
305
|
+
lost.add(`runtime parts: ${type}`)
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
if (type === "source" || type === "source-url" || type === "source-document") {
|
|
309
|
+
lost.add("cited sources")
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
lost.add(`unknown part types: ${type || "untyped"}`)
|
|
313
|
+
}
|
|
314
|
+
if (user) flushUser()
|
|
315
|
+
else if (!parts.some((p) => p?.type === "text") && typeof content.content === "string" && content.content.trim()) {
|
|
316
|
+
// A reply that kept its text only beside the parts.
|
|
317
|
+
noteModel(h, model)
|
|
318
|
+
turns.push({ kind: "assistant", ...(rowAt ? { at: rowAt } : {}), text: content.content, ...(model ? { model } : {}) })
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
lost.flush(h)
|
|
322
|
+
return { header: h, turns }
|
|
323
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { readFileSync, realpathSync } from "node:fs"
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path"
|
|
4
|
+
import { appSupport, exists, isDir, isRunning, readJson, versionOf } from "../../fsutil.ts"
|
|
5
|
+
import type { ToolKind } from "../../session/types.ts"
|
|
6
|
+
import { toolKindOf } from "../../session/util.ts"
|
|
7
|
+
|
|
8
|
+
// Mastra Code (the `mastracode` CLI, npm `mastracode`): where its store is and what its rows look like.
|
|
9
|
+
// Ported from session-migrate (xhluca/session-migrate, MIT licence): docs/mastracode-format.md and
|
|
10
|
+
// src/session_migrate/formats/mastracode.py, which were checked against mastracode@0.37.1 (source commit
|
|
11
|
+
// 003e7574, @mastra/core 1.63.2, @mastra/libsql 1.22.2, @mastra/memory 1.28.1), and against the store the
|
|
12
|
+
// exact 0.37.1 CLI wrote in test/fixtures/native-corpus/mastracode.
|
|
13
|
+
//
|
|
14
|
+
// Every local conversation lives in one LibSQL (SQLite) file, mastra.db: a thread is a mastra_threads row
|
|
15
|
+
// (id a UUID, resourceId the project identity it is scoped to, title, metadata as SQLite JSONB holding
|
|
16
|
+
// projectPath and the thread's model), a message a mastra_messages row whose content is JSON
|
|
17
|
+
// {format: 2, parts: [...]}: AI SDK UI parts (text, reasoning, tool-invocation with the call and its result
|
|
18
|
+
// folded together, file) plus MastraCode's own runtime parts (step-start, data-*). What the person typed is a
|
|
19
|
+
// "signal" row of type "user"; the model's reply to one prompt, every step of it, is one "assistant" row of
|
|
20
|
+
// type "v2". Observational memory (its own compaction) is in mastra_observational_memory.
|
|
21
|
+
//
|
|
22
|
+
// Only the local file store is read and written. MastraCode can also use a remote LibSQL (MASTRA_DB_URL) or
|
|
23
|
+
// PostgreSQL store; those are never touched. The path overrides MASTRA_DB_PATH, MASTRA_APP_DATA_DIR and
|
|
24
|
+
// XDG_DATA_HOME are not followed (no other adapter follows its tool's overrides); the writer refuses when one
|
|
25
|
+
// would make MastraCode read another store than the one written.
|
|
26
|
+
|
|
27
|
+
export const TOOL = "mastracode"
|
|
28
|
+
export const LABEL = "Mastra Code"
|
|
29
|
+
export const TESTED_VERSIONS = ["0.37.1"]
|
|
30
|
+
|
|
31
|
+
/** The store on each platform, as MastraCode's app-data path puts it (without the env overrides). */
|
|
32
|
+
export const storePaths = (home: string) => ({
|
|
33
|
+
darwin: join(appSupport(home), "mastracode", "mastra.db"),
|
|
34
|
+
linux: join(home, ".local", "share", "mastracode", "mastra.db"),
|
|
35
|
+
win32: join(home, "AppData", "Roaming", "mastracode", "mastra.db"),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
/** The store MastraCode uses on this platform. */
|
|
39
|
+
export function storePath(home: string, platform: string = process.platform): string {
|
|
40
|
+
const p = storePaths(home)
|
|
41
|
+
return platform === "darwin" ? p.darwin : platform === "win32" ? p.win32 : p.linux
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The schema a 0.37.1 store's two conversation tables have (MastraCode creates the rest itself on launch). */
|
|
45
|
+
export const SCHEMA = `
|
|
46
|
+
CREATE TABLE IF NOT EXISTS "mastra_threads" ("id" TEXT NOT NULL PRIMARY KEY,"resourceId" TEXT NOT NULL,"title" TEXT NOT NULL,"metadata" TEXT,"createdAt" TEXT NOT NULL,"updatedAt" TEXT NOT NULL);
|
|
47
|
+
CREATE TABLE IF NOT EXISTS "mastra_messages" ("id" TEXT NOT NULL PRIMARY KEY,"thread_id" TEXT NOT NULL,"content" TEXT NOT NULL,"role" TEXT NOT NULL,"type" TEXT NOT NULL,"createdAt" TEXT NOT NULL,"resourceId" TEXT);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS "mastra_messages_thread_created" ON "mastra_messages"("thread_id","createdAt","id");
|
|
49
|
+
`
|
|
50
|
+
|
|
51
|
+
export const COLUMNS = {
|
|
52
|
+
mastra_threads: ["id", "resourceId", "title", "metadata", "createdAt", "updatedAt"],
|
|
53
|
+
mastra_messages: ["id", "thread_id", "content", "role", "type", "createdAt", "resourceId"],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One message part as stored (AI SDK UI parts and MastraCode's own). */
|
|
57
|
+
export type Part = { type: string; [k: string]: any }
|
|
58
|
+
|
|
59
|
+
/** A mastra_messages row's content. */
|
|
60
|
+
export interface Content {
|
|
61
|
+
format: 2
|
|
62
|
+
parts: Part[]
|
|
63
|
+
/** The reply's text, beside its parts (assistant rows). */
|
|
64
|
+
content?: string
|
|
65
|
+
metadata?: Record<string, any>
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A mastra_messages row before it is inserted. */
|
|
69
|
+
export interface MessageRow {
|
|
70
|
+
id: string
|
|
71
|
+
role: "signal" | "user" | "assistant"
|
|
72
|
+
type: "user" | "v2"
|
|
73
|
+
content: Content
|
|
74
|
+
createdAt: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ThreadRow {
|
|
78
|
+
id: string
|
|
79
|
+
resourceId: string
|
|
80
|
+
title: string
|
|
81
|
+
metadata: Record<string, unknown>
|
|
82
|
+
createdAt: string
|
|
83
|
+
updatedAt: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** What a MastraCode tool does: execute_command is the shell, view reads a file, and so on. */
|
|
87
|
+
export function mastraToolKind(name: string): ToolKind {
|
|
88
|
+
const short = name.replace(/^mastra_workspace_/, "")
|
|
89
|
+
if (short === "view") return "read"
|
|
90
|
+
if (short === "execute_command") return "shell"
|
|
91
|
+
return toolKindOf(short)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// --- SQLite JSONB --------------------------------------------------------------------------------------
|
|
95
|
+
// mastra_threads.metadata is SQLite's binary JSON, not JSON text. A small codec, so neither reading nor
|
|
96
|
+
// writing needs SQLite 3.45's json functions. Each element: a header byte (low nibble the type, high nibble
|
|
97
|
+
// the payload size, or 12-15 for a 1/2/4/8-byte big-endian size after it), then the payload.
|
|
98
|
+
|
|
99
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true })
|
|
100
|
+
const bytes = (s: string) => new TextEncoder().encode(s)
|
|
101
|
+
|
|
102
|
+
/** The value a JSONB blob holds; throws when it is not well-formed JSONB. */
|
|
103
|
+
export function decodeJsonb(b: Uint8Array): unknown {
|
|
104
|
+
const [v, end] = jsonbElement(b, 0, 0)
|
|
105
|
+
if (end !== b.length) throw new Error("trailing bytes after the JSONB value")
|
|
106
|
+
return v
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function jsonbElement(b: Uint8Array, at: number, depth: number): [unknown, number] {
|
|
110
|
+
if (depth > 96 || at >= b.length) throw new Error("malformed JSONB")
|
|
111
|
+
const code = b[at]! >> 4
|
|
112
|
+
const type = b[at]! & 0x0f
|
|
113
|
+
let size = code
|
|
114
|
+
let head = 1
|
|
115
|
+
if (code >= 12) {
|
|
116
|
+
const n = [1, 2, 4, 8][code - 12]!
|
|
117
|
+
if (at + 1 + n > b.length) throw new Error("truncated JSONB header")
|
|
118
|
+
size = 0
|
|
119
|
+
for (let i = 0; i < n; i++) size = size * 256 + b[at + 1 + i]!
|
|
120
|
+
head = 1 + n
|
|
121
|
+
}
|
|
122
|
+
const start = at + head
|
|
123
|
+
const end = start + size
|
|
124
|
+
if (end > b.length) throw new Error("truncated JSONB payload")
|
|
125
|
+
const text = () => utf8.decode(b.subarray(start, end))
|
|
126
|
+
switch (type) {
|
|
127
|
+
case 0:
|
|
128
|
+
return [null, end]
|
|
129
|
+
case 1:
|
|
130
|
+
return [true, end]
|
|
131
|
+
case 2:
|
|
132
|
+
return [false, end]
|
|
133
|
+
case 3: // INT
|
|
134
|
+
case 5: // FLOAT
|
|
135
|
+
case 6: // FLOAT5
|
|
136
|
+
return [Number(text()), end]
|
|
137
|
+
case 4: {
|
|
138
|
+
// INT5: JSON5 integers, hex included
|
|
139
|
+
const t = text()
|
|
140
|
+
const neg = t.startsWith("-")
|
|
141
|
+
const n = Number(t.replace(/^[+-]/, ""))
|
|
142
|
+
return [neg ? -n : n, end]
|
|
143
|
+
}
|
|
144
|
+
case 7: // TEXT: nothing to unescape
|
|
145
|
+
case 10: // TEXTRAW
|
|
146
|
+
return [text(), end]
|
|
147
|
+
case 8: // TEXTJ: JSON escapes
|
|
148
|
+
case 9: {
|
|
149
|
+
// TEXT5: JSON5 escapes, read as JSON when they are
|
|
150
|
+
const t = text()
|
|
151
|
+
try {
|
|
152
|
+
return [JSON.parse(`"${t}"`), end]
|
|
153
|
+
} catch {
|
|
154
|
+
return [t, end]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
case 11: {
|
|
158
|
+
const out: unknown[] = []
|
|
159
|
+
let p = start
|
|
160
|
+
while (p < end) {
|
|
161
|
+
const [v, next] = jsonbElement(b, p, depth + 1)
|
|
162
|
+
out.push(v)
|
|
163
|
+
p = next
|
|
164
|
+
}
|
|
165
|
+
if (p !== end) throw new Error("malformed JSONB array")
|
|
166
|
+
return [out, end]
|
|
167
|
+
}
|
|
168
|
+
case 12: {
|
|
169
|
+
const out: Record<string, unknown> = {}
|
|
170
|
+
let p = start
|
|
171
|
+
while (p < end) {
|
|
172
|
+
const [k, afterKey] = jsonbElement(b, p, depth + 1)
|
|
173
|
+
if (typeof k !== "string") throw new Error("JSONB object key is not text")
|
|
174
|
+
const [v, next] = jsonbElement(b, afterKey, depth + 1)
|
|
175
|
+
out[k] = v
|
|
176
|
+
p = next
|
|
177
|
+
}
|
|
178
|
+
if (p !== end) throw new Error("malformed JSONB object")
|
|
179
|
+
return [out, end]
|
|
180
|
+
}
|
|
181
|
+
default:
|
|
182
|
+
throw new Error(`reserved JSONB element type ${type}`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function jsonbHeader(type: number, size: number): number[] {
|
|
187
|
+
if (size <= 11) return [(size << 4) | type]
|
|
188
|
+
if (size <= 0xff) return [0xc0 | type, size]
|
|
189
|
+
if (size <= 0xffff) return [0xd0 | type, size >> 8, size & 0xff]
|
|
190
|
+
return [0xe0 | type, (size >>> 24) & 0xff, (size >>> 16) & 0xff, (size >>> 8) & 0xff, size & 0xff]
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function jsonbParts(v: unknown, out: number[][], depth: number): void {
|
|
194
|
+
if (depth > 96) throw new Error("metadata nests too deeply for JSONB")
|
|
195
|
+
const leaf = (type: number, payload: Uint8Array): void => void out.push([...jsonbHeader(type, payload.length), ...payload])
|
|
196
|
+
if (v === null || v === undefined) return void out.push(jsonbHeader(0, 0))
|
|
197
|
+
if (v === true) return void out.push(jsonbHeader(1, 0))
|
|
198
|
+
if (v === false) return void out.push(jsonbHeader(2, 0))
|
|
199
|
+
if (typeof v === "number") {
|
|
200
|
+
if (!Number.isFinite(v)) return void out.push(jsonbHeader(0, 0))
|
|
201
|
+
return leaf(Number.isSafeInteger(v) ? 3 : 5, bytes(Number.isSafeInteger(v) ? String(v) : JSON.stringify(v)))
|
|
202
|
+
}
|
|
203
|
+
if (typeof v === "string") {
|
|
204
|
+
const escaped = JSON.stringify(v).slice(1, -1)
|
|
205
|
+
return leaf(escaped === v ? 7 : 8, bytes(escaped))
|
|
206
|
+
}
|
|
207
|
+
const children: number[][] = []
|
|
208
|
+
if (Array.isArray(v)) for (const item of v) jsonbParts(item, children, depth + 1)
|
|
209
|
+
else if (typeof v === "object")
|
|
210
|
+
for (const [k, item] of Object.entries(v as Record<string, unknown>)) {
|
|
211
|
+
if (item === undefined) continue
|
|
212
|
+
jsonbParts(k, children, depth + 1)
|
|
213
|
+
jsonbParts(item, children, depth + 1)
|
|
214
|
+
}
|
|
215
|
+
else throw new Error(`cannot store a ${typeof v} in JSONB`)
|
|
216
|
+
const size = children.reduce((n, c) => n + c.length, 0)
|
|
217
|
+
out.push(jsonbHeader(Array.isArray(v) ? 11 : 12, size))
|
|
218
|
+
out.push(...children)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** A value as SQLite JSONB, the way MastraCode stores a thread's metadata. */
|
|
222
|
+
export function encodeJsonb(v: unknown): Uint8Array {
|
|
223
|
+
const out: number[][] = []
|
|
224
|
+
jsonbParts(v, out, 0)
|
|
225
|
+
return Uint8Array.from(out.flat())
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** A thread's metadata, whether it is JSONB (as MastraCode writes it), JSON text, or missing. */
|
|
229
|
+
export function metadataOf(v: unknown): Record<string, any> {
|
|
230
|
+
try {
|
|
231
|
+
const parsed = v instanceof Uint8Array ? decodeJsonb(v) : typeof v === "string" && v ? JSON.parse(v) : undefined
|
|
232
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? (parsed as Record<string, any>) : {}
|
|
233
|
+
} catch {
|
|
234
|
+
return {}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// --- Project identity ----------------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
/** The folder with symbolic links resolved, as the process working directory MastraCode sees. */
|
|
241
|
+
export function realDir(dir: string): string {
|
|
242
|
+
try {
|
|
243
|
+
return realpathSync(resolve(dir))
|
|
244
|
+
} catch {
|
|
245
|
+
return resolve(dir)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const normalizeGitUrl = (url: string) =>
|
|
250
|
+
url
|
|
251
|
+
.replace(/\.git$/, "")
|
|
252
|
+
.replace(/^git@([^:]+):/, "https://$1/")
|
|
253
|
+
.replace(/^ssh:\/\/git@/, "https://")
|
|
254
|
+
.toLowerCase()
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The resource id MastraCode scopes a project's threads by (utils/project.ts, as session-migrate reproduces
|
|
258
|
+
* it): a slug of the repository's name and the first 12 hex digits of the SHA-256 of its normalised remote
|
|
259
|
+
* URL; without a remote, of the main repository's folder (a worktree) or the project folder itself. Read
|
|
260
|
+
* from .git without running git.
|
|
261
|
+
*/
|
|
262
|
+
export function resourceIdFor(cwd: string): string {
|
|
263
|
+
let root = realDir(cwd)
|
|
264
|
+
let gitDir: string | undefined
|
|
265
|
+
for (let dir = root; ; dir = dirname(dir)) {
|
|
266
|
+
const git = join(dir, ".git")
|
|
267
|
+
if (exists(git)) {
|
|
268
|
+
try {
|
|
269
|
+
gitDir = isDir(git) ? git : resolve(dir, readFileSync(git, "utf8").replace(/^gitdir:\s*/, "").trim())
|
|
270
|
+
root = dir
|
|
271
|
+
} catch {}
|
|
272
|
+
break
|
|
273
|
+
}
|
|
274
|
+
if (dirname(dir) === dir) break
|
|
275
|
+
}
|
|
276
|
+
let url: string | undefined
|
|
277
|
+
let mainRepo: string | undefined
|
|
278
|
+
if (gitDir) {
|
|
279
|
+
let common = gitDir
|
|
280
|
+
try {
|
|
281
|
+
common = resolve(gitDir, readFileSync(join(gitDir, "commondir"), "utf8").trim())
|
|
282
|
+
} catch {}
|
|
283
|
+
if (common !== gitDir) mainRepo = dirname(common)
|
|
284
|
+
let config = ""
|
|
285
|
+
try {
|
|
286
|
+
config = readFileSync(join(common, "config"), "utf8")
|
|
287
|
+
} catch {}
|
|
288
|
+
url = config.match(/\[remote "origin"\][^[]*?url\s*=\s*(\S+)/)?.[1] ?? config.match(/\[remote "[^"]*"\][^[]*?url\s*=\s*(\S+)/)?.[1]
|
|
289
|
+
}
|
|
290
|
+
const source = url ? normalizeGitUrl(url) : (mainRepo ?? root)
|
|
291
|
+
const base = url ? url.replace(/\/+$/, "").split("/").pop()!.replace(/\.git$/, "") || "project" : basename(root)
|
|
292
|
+
const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
|
|
293
|
+
return `${slug || "project"}-${createHash("sha256").update(source).digest("hex").slice(0, 12)}`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// --- The CLI -------------------------------------------------------------------------------------------
|
|
297
|
+
|
|
298
|
+
/** The mastracode binary on PATH (read now, so a PATH set after start counts). */
|
|
299
|
+
export const mastraBin = (): string | undefined => Bun.which("mastracode", { PATH: process.env.PATH ?? "" }) ?? undefined
|
|
300
|
+
|
|
301
|
+
/** The installed MastraCode version, from `mastracode --version`. */
|
|
302
|
+
export const mastraVersion = (bin = mastraBin()) => (bin ? versionOf(bin)?.match(/\d+\.\d+\.\d+/)?.[0] : undefined)
|
|
303
|
+
|
|
304
|
+
/** Why MastraCode would read another store than `store`, from the environment and its settings; undefined when it reads this one. */
|
|
305
|
+
export function otherStore(store: string): string | undefined {
|
|
306
|
+
const env = process.env
|
|
307
|
+
if (env.MASTRA_STORAGE_BACKEND && env.MASTRA_STORAGE_BACKEND !== "libsql") return `MASTRA_STORAGE_BACKEND=${env.MASTRA_STORAGE_BACKEND} points MastraCode at a remote store`
|
|
308
|
+
if (env.MASTRA_DB_URL && resolve(env.MASTRA_DB_URL.replace(/^file:(\/\/)?/, "")) !== store) return "MASTRA_DB_URL points MastraCode at another store"
|
|
309
|
+
if (env.MASTRA_DB_PATH && resolve(env.MASTRA_DB_PATH) !== store) return `MASTRA_DB_PATH points MastraCode at ${env.MASTRA_DB_PATH}`
|
|
310
|
+
if (env.MASTRA_APP_DATA_DIR && join(resolve(env.MASTRA_APP_DATA_DIR), "mastra.db") !== store) return `MASTRA_APP_DATA_DIR points MastraCode at ${env.MASTRA_APP_DATA_DIR}`
|
|
311
|
+
const storage = readJson<{ storage?: { backend?: unknown; libsql?: { url?: unknown } } }>(join(dirname(store), "settings.json"))?.storage
|
|
312
|
+
if (typeof storage?.backend === "string" && storage.backend !== "libsql") return `MastraCode's settings use the ${storage.backend} storage backend`
|
|
313
|
+
if (typeof storage?.libsql?.url === "string" && !storage.libsql.url.startsWith("file:")) return "MastraCode's settings use a remote LibSQL store"
|
|
314
|
+
return undefined
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Why the store cannot be written now: a process holding it open, or MastraCode running; undefined when it is free. */
|
|
318
|
+
export function storeInUse(store: string): string | undefined {
|
|
319
|
+
const files = [store, `${store}-wal`].filter(exists)
|
|
320
|
+
if (files.length) {
|
|
321
|
+
try {
|
|
322
|
+
const p = Bun.spawnSync(["lsof", "-t", "--", ...files], { stdout: "pipe", stderr: "pipe", timeout: 10_000 })
|
|
323
|
+
const pids = p.stdout.toString().split(/\s+/).filter((pid) => pid && Number(pid) !== process.pid)
|
|
324
|
+
if (pids.length) return `the MastraCode store is open in another process (pid ${pids.join(", ")}); close MastraCode first`
|
|
325
|
+
} catch {
|
|
326
|
+
// No lsof: the process checks below still apply.
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (isRunning("mastracode")) return "MastraCode is running; close it first, since it holds the session store open"
|
|
330
|
+
try {
|
|
331
|
+
const p = Bun.spawnSync(["pgrep", "-f", "mastracode/dist/cli|(^|/)mastracode( |$)"], { stdout: "pipe", stderr: "pipe" })
|
|
332
|
+
const pids = p.stdout.toString().split(/\s+/).filter((pid) => pid && Number(pid) !== process.pid)
|
|
333
|
+
if (pids.length) return "MastraCode is running; close it first, since it holds the session store open"
|
|
334
|
+
} catch {}
|
|
335
|
+
return undefined
|
|
336
|
+
}
|