@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,537 @@
|
|
|
1
|
+
// brain org: encryption at rest.
|
|
2
|
+
//
|
|
3
|
+
// WHAT IT IS. The whole organisation database (Chitta's tables AND the `org_*` tables: one file) is encrypted by libSQL's built-in
|
|
4
|
+
// whole-file AES-256 (the same driver Chitta itself switches to when `CONTEXT_DB_KEY` is set). Pages, the write-ahead log (-wal)
|
|
5
|
+
// and the shared-memory index (-shm) are all written through the cipher: there is no application-level encryption of single
|
|
6
|
+
// columns here and nothing is left readable on purpose. The proof is in test/org-crypto.test.ts: it reads the raw bytes of the
|
|
7
|
+
// three files and looks for item text, user ids and audit text.
|
|
8
|
+
//
|
|
9
|
+
// WHAT IT IS NOT. It protects the file at rest (a copied disk, a stolen backup, a snapshot, another account on the machine that
|
|
10
|
+
// can read the file but not the key). It does not protect against someone who can read the server process's memory or the key
|
|
11
|
+
// itself (`BRAIN_ORG_KEY`, the key file): while the server runs the key and decrypted pages are in its memory. The key is never
|
|
12
|
+
// stored in the database and never printed or logged by this code.
|
|
13
|
+
//
|
|
14
|
+
// HOW THE KEY GETS IN. `BRAIN_ORG_KEY` (an environment variable) or `--key-file <path>` (a file holding the key: the usual way with
|
|
15
|
+
// Docker/Kubernetes secrets and a Secret Manager mount). Never a command-line argument (it would show in `ps`). A key is
|
|
16
|
+
// 32-512 characters from `A-Za-z0-9+/=_.~-` (the generated one is 64 hex characters = 256 random bits).
|
|
17
|
+
//
|
|
18
|
+
// HOW CHITTA IS KEPT IN STEP. Chitta decides at construction time whether to open its store with libSQL by reading the
|
|
19
|
+
// `CONTEXT_DB_KEY` environment variable. `withKeyEnv` sets it only for the duration of that synchronous constructor call and then
|
|
20
|
+
// puts the previous value back, so the key is not left in the process environment (child processes never inherit it).
|
|
21
|
+
//
|
|
22
|
+
// ONE CONNECTION, TWO DRIVERS. `OrgDb` runs its queries on the connection Chitta opened. libSQL's handle is almost, but not quite,
|
|
23
|
+
// bun:sqlite's: no `transaction()`, named parameters bound without their `$`, and rows come back with an extra `_metadata`
|
|
24
|
+
// property. `adapt` closes exactly those gaps, so the same SQL (including the visibility predicate) runs unchanged on both.
|
|
25
|
+
//
|
|
26
|
+
// LIMITS (the honest list): libSQL must be installed where Chitta (or brain) can resolve it (`bun add libsql` in Chitta's folder;
|
|
27
|
+
// it is a dev dependency of Chitta, not shipped by default). While encrypted, Chitta cannot load the sqlite-vec extension: it
|
|
28
|
+
// uses libSQL's native vector index when it works and a brute-force scan otherwise (correct, slower on very large stores), and
|
|
29
|
+
// decryption costs CPU on every page read. Metadata that is not in the file is not hidden: the file's size, its modification
|
|
30
|
+
// times and the names of the files are visible to anyone who can list the directory. Converting a database (`encrypt`, `rekey`)
|
|
31
|
+
// is a logical copy; the previous file is kept next to it as `*.bak-<time>` (plain text for `encrypt`!) until you remove it.
|
|
32
|
+
import { Database } from "bun:sqlite"
|
|
33
|
+
import { randomBytes } from "node:crypto"
|
|
34
|
+
import { chmodSync, closeSync, existsSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs"
|
|
35
|
+
import { createRequire } from "node:module"
|
|
36
|
+
import { dirname } from "node:path"
|
|
37
|
+
import { chittaCandidates } from "../memory/store.ts"
|
|
38
|
+
|
|
39
|
+
export const SQLITE_MAGIC = "SQLite format 3\u0000"
|
|
40
|
+
|
|
41
|
+
/** A problem with the encryption key or with opening an encrypted file: the message is safe to show (it never holds the key). */
|
|
42
|
+
export class OrgKeyError extends Error {
|
|
43
|
+
constructor(message: string) {
|
|
44
|
+
super(message)
|
|
45
|
+
this.name = "OrgKeyError"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type DbState = "missing" | "empty" | "plain" | "encrypted"
|
|
50
|
+
|
|
51
|
+
/** What the file's first bytes say: a plain SQLite database starts with "SQLite format 3\0"; an encrypted one starts with
|
|
52
|
+
* cipher text (indistinguishable from random). An empty file is a database about to be created. */
|
|
53
|
+
export function dbState(path: string): DbState {
|
|
54
|
+
if (!existsSync(path)) return "missing"
|
|
55
|
+
const fd = openSync(path, "r")
|
|
56
|
+
try {
|
|
57
|
+
const buf = Buffer.alloc(16)
|
|
58
|
+
const n = readSync(fd, buf, 0, 16, 0)
|
|
59
|
+
if (n === 0) return "empty"
|
|
60
|
+
return n === 16 && buf.toString("latin1") === SQLITE_MAGIC ? "plain" : "encrypted"
|
|
61
|
+
} finally {
|
|
62
|
+
closeSync(fd)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- keys -------------------------------------------------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
const KEY_RE = /^[A-Za-z0-9+/=_.~-]{32,512}$/
|
|
69
|
+
|
|
70
|
+
/** Trims and checks a key; the error never contains the key. */
|
|
71
|
+
export function validateKey(raw: string, where = "the key"): string {
|
|
72
|
+
const k = raw.trim()
|
|
73
|
+
if (!KEY_RE.test(k)) throw new OrgKeyError(`${where} is not usable: it must be 32 to 512 characters from A-Z a-z 0-9 + / = _ . ~ - (a generated key is 64 hex characters: use \`brain org admin init --encrypt\`)`)
|
|
74
|
+
return k
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 256 random bits, as 64 hex characters. */
|
|
78
|
+
export const generateKey = (): string => randomBytes(32).toString("hex")
|
|
79
|
+
|
|
80
|
+
/** Writes a new key file (mode 0600, refuses to overwrite). */
|
|
81
|
+
export function writeKeyFile(path: string, key: string): void {
|
|
82
|
+
writeFileSync(path, `${key}\n`, { mode: 0o600, flag: "wx" })
|
|
83
|
+
try {
|
|
84
|
+
chmodSync(path, 0o600)
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Reads a key file. A file others can read is reported through `warning` (secret mounts are often 0444: a warning, not a refusal). */
|
|
89
|
+
export function readKeyFile(path: string): { key: string; warning?: string } {
|
|
90
|
+
let text: string
|
|
91
|
+
try {
|
|
92
|
+
text = readFileSync(path, "utf8")
|
|
93
|
+
} catch {
|
|
94
|
+
throw new OrgKeyError(`cannot read the key file ${path}`)
|
|
95
|
+
}
|
|
96
|
+
const key = validateKey(text, `the key in ${path}`)
|
|
97
|
+
let warning: string | undefined
|
|
98
|
+
try {
|
|
99
|
+
if (statSync(path).mode & 0o044) warning = `the key file ${path} is readable by other users: restrict it (chmod 600)`
|
|
100
|
+
} catch {}
|
|
101
|
+
return { key, ...(warning ? { warning } : {}) }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface KeySource {
|
|
105
|
+
/** The key itself (for programs: tests, an embedding application). */
|
|
106
|
+
key?: string
|
|
107
|
+
keyFile?: string
|
|
108
|
+
env?: Record<string, string | undefined>
|
|
109
|
+
}
|
|
110
|
+
/** The key from, in order: `key`, `keyFile`, `BRAIN_ORG_KEY`. A key file that cannot be read is an error, never a silent fallback. */
|
|
111
|
+
export function resolveKey(s: KeySource = {}): { key?: string; from?: "option" | "file" | "env"; warning?: string } {
|
|
112
|
+
if (s.key !== undefined) return { key: validateKey(s.key, "the key"), from: "option" }
|
|
113
|
+
if (s.keyFile) {
|
|
114
|
+
const r = readKeyFile(s.keyFile)
|
|
115
|
+
return { key: r.key, from: "file", ...(r.warning ? { warning: r.warning } : {}) }
|
|
116
|
+
}
|
|
117
|
+
const e = (s.env ?? process.env).BRAIN_ORG_KEY
|
|
118
|
+
if (e !== undefined && e.trim() !== "") return { key: validateKey(e, "BRAIN_ORG_KEY"), from: "env" }
|
|
119
|
+
return {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- libSQL -----------------------------------------------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
interface RawStmt {
|
|
125
|
+
all(...p: unknown[]): unknown[]
|
|
126
|
+
get(...p: unknown[]): unknown
|
|
127
|
+
run(...p: unknown[]): unknown
|
|
128
|
+
}
|
|
129
|
+
export interface RawHandle {
|
|
130
|
+
prepare(sql: string): RawStmt
|
|
131
|
+
exec(sql: string): void
|
|
132
|
+
close(): void
|
|
133
|
+
}
|
|
134
|
+
type RawCtor = new (path: string, opts?: { encryptionKey?: string }) => RawHandle
|
|
135
|
+
|
|
136
|
+
const NO_LIBSQL = (tried: string[]): OrgKeyError =>
|
|
137
|
+
new OrgKeyError(
|
|
138
|
+
`encryption needs the \`libsql\` package and none was found${tried.length ? ` (looked next to ${tried.join(", ")})` : ""}. Install it where Chitta lives: \`cd <chitta folder> && bun add libsql\` (BRAIN_CHITTA points to that folder), or in brain: \`bun add libsql\`.`,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
let libsqlCache: RawCtor | undefined
|
|
142
|
+
const requireFrom = (file: string): ((id: string) => unknown) => createRequire(file)
|
|
143
|
+
|
|
144
|
+
/** The libSQL constructor, found next to brain or next to any Chitta that brain would load. */
|
|
145
|
+
export function loadLibsql(): RawCtor {
|
|
146
|
+
if (libsqlCache) return libsqlCache
|
|
147
|
+
const tried: string[] = []
|
|
148
|
+
const places = [import.meta.url, ...chittaCandidates().filter(existsSync)]
|
|
149
|
+
for (const p of places) {
|
|
150
|
+
try {
|
|
151
|
+
const mod = requireFrom(p)("libsql") as { default?: RawCtor } & RawCtor
|
|
152
|
+
libsqlCache = (mod.default ?? mod) as RawCtor
|
|
153
|
+
return libsqlCache
|
|
154
|
+
} catch {
|
|
155
|
+
tried.push(p === import.meta.url ? "brain" : dirname(p))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
throw NO_LIBSQL(tried)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The first Chitta entry file (in brain's usual search order) whose folder can resolve `libsql`: the one to load for an encrypted
|
|
162
|
+
* database (Chitta's own switch to libSQL resolves the package from its own folder). Undefined when none can. */
|
|
163
|
+
export function chittaWithLibsql(): string | undefined {
|
|
164
|
+
for (const file of chittaCandidates()) {
|
|
165
|
+
if (!existsSync(file)) continue
|
|
166
|
+
try {
|
|
167
|
+
requireFrom(file)("libsql")
|
|
168
|
+
return file
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
return undefined
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const isPlainObject = (x: unknown): x is Record<string, unknown> => typeof x === "object" && x !== null && Object.getPrototypeOf(x) === Object.prototype
|
|
175
|
+
|
|
176
|
+
function bindArgs(p: unknown[]): unknown[] {
|
|
177
|
+
const conv = (v: unknown) => (typeof v === "boolean" ? (v ? 1 : 0) : v)
|
|
178
|
+
// bun:sqlite binds `{ $name: v }` to `$name`; libSQL wants the key without the sigil (and silently binds NULL otherwise).
|
|
179
|
+
if (p.length === 1 && isPlainObject(p[0])) return [Object.fromEntries(Object.entries(p[0]).map(([k, v]) => [/^[$@:]/.test(k) ? k.slice(1) : k, conv(v)]))]
|
|
180
|
+
return p.map(conv)
|
|
181
|
+
}
|
|
182
|
+
function clean<T>(row: T): T {
|
|
183
|
+
if (row && typeof row === "object" && "_metadata" in (row as object)) delete (row as Record<string, unknown>)._metadata
|
|
184
|
+
return row
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Makes a libSQL-shaped handle behave like the bun:sqlite `Database` the organisation code was written against. */
|
|
188
|
+
export function adapt(raw: { query?: (sql: string) => RawStmt; prepare?: (sql: string) => RawStmt; exec(sql: string): void; close(): void }): Database {
|
|
189
|
+
const prep = (sql: string): RawStmt => (raw.query ? raw.query(sql) : raw.prepare!(sql))
|
|
190
|
+
const cache = new Map<string, RawStmt>()
|
|
191
|
+
const stmt = (sql: string): RawStmt => {
|
|
192
|
+
let s = cache.get(sql)
|
|
193
|
+
if (!s) {
|
|
194
|
+
if (cache.size > 300) cache.clear()
|
|
195
|
+
s = prep(sql)
|
|
196
|
+
cache.set(sql, s)
|
|
197
|
+
}
|
|
198
|
+
return s
|
|
199
|
+
}
|
|
200
|
+
let depth = 0
|
|
201
|
+
let seq = 0
|
|
202
|
+
const handle = {
|
|
203
|
+
query(sql: string) {
|
|
204
|
+
const s = stmt(sql)
|
|
205
|
+
return {
|
|
206
|
+
all: (...p: unknown[]) => (s.all(...bindArgs(p)) as unknown[]).map(clean),
|
|
207
|
+
get: (...p: unknown[]) => clean(s.get(...bindArgs(p)) ?? null),
|
|
208
|
+
run: (...p: unknown[]) => s.run(...bindArgs(p)),
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
exec: (sql: string) => raw.exec(sql),
|
|
212
|
+
close: () => raw.close(),
|
|
213
|
+
/** `db.transaction(fn)` returns a function that runs `fn` in a transaction (a savepoint when already inside one). */
|
|
214
|
+
transaction<A extends unknown[], R>(fn: (...a: A) => R): (...a: A) => R {
|
|
215
|
+
return (...a: A): R => {
|
|
216
|
+
let sp: string | undefined = depth > 0 ? `brain_sp_${++seq}` : undefined
|
|
217
|
+
if (!sp) {
|
|
218
|
+
try {
|
|
219
|
+
raw.exec("BEGIN IMMEDIATE")
|
|
220
|
+
} catch (e) {
|
|
221
|
+
if (!/within a transaction/i.test(String((e as Error)?.message))) throw e
|
|
222
|
+
sp = `brain_sp_${++seq}` // Chitta holds a transaction open on this connection: join it
|
|
223
|
+
raw.exec(`SAVEPOINT ${sp}`)
|
|
224
|
+
}
|
|
225
|
+
} else raw.exec(`SAVEPOINT ${sp}`)
|
|
226
|
+
depth++
|
|
227
|
+
try {
|
|
228
|
+
const r = fn(...a)
|
|
229
|
+
raw.exec(sp ? `RELEASE ${sp}` : "COMMIT")
|
|
230
|
+
return r
|
|
231
|
+
} catch (e) {
|
|
232
|
+
try {
|
|
233
|
+
raw.exec(sp ? `ROLLBACK TO ${sp}; RELEASE ${sp}` : "ROLLBACK")
|
|
234
|
+
} catch {}
|
|
235
|
+
throw e
|
|
236
|
+
} finally {
|
|
237
|
+
depth--
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
return handle as unknown as Database
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The handle as is when it already has `transaction` (bun:sqlite), otherwise adapted. */
|
|
246
|
+
export const ensureTransactions = (db: Database): Database => (typeof (db as { transaction?: unknown }).transaction === "function" ? db : adapt(db as never))
|
|
247
|
+
|
|
248
|
+
const WRONG_KEY = /not a database|file is encrypted|decrypt|malformed|SQLITE_NOTADB|SQLITE_CORRUPT/i
|
|
249
|
+
|
|
250
|
+
/** Opens the database file with its own connection: plain with bun:sqlite, encrypted with libSQL. Refuses a key on a plain file and
|
|
251
|
+
* no key on an encrypted one, each with a message that says what to do. Wrong key: "does not open". */
|
|
252
|
+
export function openSqlite(path: string, key?: string): Database {
|
|
253
|
+
const state = dbState(path)
|
|
254
|
+
if (!key) {
|
|
255
|
+
if (state === "encrypted") throw new OrgKeyError(`${path} is encrypted: give the key with BRAIN_ORG_KEY or --key-file <path>`)
|
|
256
|
+
return new Database(path, { create: true })
|
|
257
|
+
}
|
|
258
|
+
if (state === "plain") throw new OrgKeyError(`${path} is not encrypted but a key was given: convert it with \`brain org admin encrypt --db ${path}\`, or serve it without a key`)
|
|
259
|
+
const L = loadLibsql()
|
|
260
|
+
let raw: RawHandle | undefined
|
|
261
|
+
try {
|
|
262
|
+
raw = new L(path, { encryptionKey: key })
|
|
263
|
+
raw.prepare("SELECT count(*) AS c FROM sqlite_master").get()
|
|
264
|
+
} catch (e) {
|
|
265
|
+
try {
|
|
266
|
+
raw?.close()
|
|
267
|
+
} catch {}
|
|
268
|
+
if (WRONG_KEY.test(String((e as Error)?.message ?? e))) throw new OrgKeyError(`the key does not open ${path} (wrong key, or the file is damaged)`)
|
|
269
|
+
throw new OrgKeyError(`cannot open ${path} with the key: ${String((e as Error)?.message ?? e).split("\n")[0]}`)
|
|
270
|
+
}
|
|
271
|
+
return adapt(raw as never)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Runs `fn` (synchronously) with CONTEXT_DB_KEY set to `key` (or removed when none), then restores the previous value. Chitta reads
|
|
275
|
+
* this variable once, when it opens its store; the key must not stay in the environment afterwards. */
|
|
276
|
+
export function withKeyEnv<T>(key: string | undefined, fn: () => T): T {
|
|
277
|
+
const prev = process.env.CONTEXT_DB_KEY
|
|
278
|
+
if (key) process.env.CONTEXT_DB_KEY = key
|
|
279
|
+
else delete process.env.CONTEXT_DB_KEY
|
|
280
|
+
try {
|
|
281
|
+
return fn()
|
|
282
|
+
} finally {
|
|
283
|
+
if (prev === undefined) delete process.env.CONTEXT_DB_KEY
|
|
284
|
+
else process.env.CONTEXT_DB_KEY = prev
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---- one server per file ----------------------------------------------------------------------------------------------------
|
|
289
|
+
|
|
290
|
+
const lockPath = (db: string): string => `${db}.lock`
|
|
291
|
+
|
|
292
|
+
/** Marks the database as served by this process. Not a mutex (a second server is not refused: that is not this file's job); it lets
|
|
293
|
+
* `encrypt`, `rekey` and `restore` refuse to replace a file a live server has open. Returns a function that releases it. */
|
|
294
|
+
export function claimServerLock(db: string): () => void {
|
|
295
|
+
const token = `${process.pid}:${randomBytes(8).toString("hex")}`
|
|
296
|
+
try {
|
|
297
|
+
writeFileSync(lockPath(db), `${token}\n`, { mode: 0o600 })
|
|
298
|
+
} catch {
|
|
299
|
+
return () => {}
|
|
300
|
+
}
|
|
301
|
+
return () => {
|
|
302
|
+
try {
|
|
303
|
+
if (readFileSync(lockPath(db), "utf8").trim() === token) unlinkSync(lockPath(db))
|
|
304
|
+
} catch {}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The pid of a live process serving this database, if there is one (a stale lock of a dead process is ignored). */
|
|
309
|
+
export function liveServer(db: string): number | undefined {
|
|
310
|
+
try {
|
|
311
|
+
const pid = Number(readFileSync(lockPath(db), "utf8").split(":")[0])
|
|
312
|
+
if (!Number.isInteger(pid) || pid <= 0) return undefined
|
|
313
|
+
process.kill(pid, 0)
|
|
314
|
+
return pid
|
|
315
|
+
} catch (e) {
|
|
316
|
+
return (e as NodeJS.ErrnoException)?.code === "EPERM" ? -1 : undefined
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ---- converting a database: encrypt, rekey ----------------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
const qIdent = (s: string): string => `"${s.replaceAll('"', '""')}"`
|
|
323
|
+
|
|
324
|
+
export interface ConvertResult {
|
|
325
|
+
/** The previous file, kept next to the database. For `encrypt` it holds PLAIN TEXT until you remove it. */
|
|
326
|
+
backup: string
|
|
327
|
+
tables: Record<string, number>
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** Copies every ordinary table (and its indexes, views, triggers) of the database at `path` into a new file encrypted with `newKey`,
|
|
331
|
+
* verifies the row counts and an integrity check on the new file, and swaps it into place (the old file becomes `<path>.bak-<time>`).
|
|
332
|
+
* `oldKey` is the current key, or undefined for a plain database. Works for Chitta's tables and for every `org_*` table, including
|
|
333
|
+
* ones added later: tables are discovered, not listed. Virtual tables (full-text, vector) and their shadow tables are not copied:
|
|
334
|
+
* Chitta rebuilds the full-text index when it next opens the store, and this function rebuilds the vector index it needs. */
|
|
335
|
+
export function convertDatabase(o: { path: string; oldKey?: string; newKey: string; now?: () => number }): ConvertResult {
|
|
336
|
+
const { path } = o
|
|
337
|
+
const state = dbState(path)
|
|
338
|
+
if (state === "missing" || state === "empty") throw new OrgKeyError(`no database at ${path}`)
|
|
339
|
+
if (state === "plain" && o.oldKey) throw new OrgKeyError(`${path} is not encrypted: do not pass a current key`)
|
|
340
|
+
if (state === "encrypted" && !o.oldKey) throw new OrgKeyError(`${path} is encrypted: give the current key (BRAIN_ORG_KEY or --key-file)`)
|
|
341
|
+
const live = liveServer(path)
|
|
342
|
+
if (live !== undefined) throw new OrgKeyError(`a server (pid ${live > 0 ? live : "unknown"}) is running on ${path}: stop it first, then convert`)
|
|
343
|
+
const newKey = validateKey(o.newKey, "the new key")
|
|
344
|
+
const L = loadLibsql()
|
|
345
|
+
const stamp = (o.now ?? Date.now)()
|
|
346
|
+
const tmp = `${path}.converting-${process.pid}.tmp`
|
|
347
|
+
const backup = `${path}.bak-${stamp}`
|
|
348
|
+
for (const f of [tmp, `${tmp}-wal`, `${tmp}-shm`]) rmSync(f, { force: true })
|
|
349
|
+
closeSync(openSync(tmp, "wx", 0o600))
|
|
350
|
+
|
|
351
|
+
const counts: Record<string, number> = {}
|
|
352
|
+
let src: RawHandle | undefined
|
|
353
|
+
try {
|
|
354
|
+
try {
|
|
355
|
+
src = o.oldKey ? new L(path, { encryptionKey: o.oldKey }) : new L(path)
|
|
356
|
+
src.prepare("SELECT count(*) FROM sqlite_master").get()
|
|
357
|
+
} catch (e) {
|
|
358
|
+
try {
|
|
359
|
+
src?.close()
|
|
360
|
+
} catch {}
|
|
361
|
+
src = undefined
|
|
362
|
+
throw new OrgKeyError(WRONG_KEY.test(String((e as Error)?.message)) ? `the current key does not open ${path} (wrong key, or the file is damaged)` : `cannot open ${path}: ${String((e as Error)?.message).split("\n")[0]}`)
|
|
363
|
+
}
|
|
364
|
+
const S = src
|
|
365
|
+
const rows = (sql: string, ...p: unknown[]): Array<Record<string, unknown>> => (S.prepare(sql).all(...p) as Array<Record<string, unknown>>).map((r) => clean(r))
|
|
366
|
+
try {
|
|
367
|
+
S.exec("PRAGMA busy_timeout = 3000")
|
|
368
|
+
S.exec("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
369
|
+
} catch {}
|
|
370
|
+
S.prepare("ATTACH DATABASE ? AS dst KEY ?").run(tmp, newKey)
|
|
371
|
+
const master = rows("SELECT type, name, tbl_name, sql FROM main.sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%'")
|
|
372
|
+
const virtual = master.filter((m) => m.type === "table" && /^\s*CREATE\s+VIRTUAL/i.test(String(m.sql))).map((m) => String(m.name))
|
|
373
|
+
const skipTable = (name: string): boolean => virtual.some((v) => name === v || name.startsWith(`${v}_`)) || name.startsWith("libsql_") || /_idx_shadow$/.test(name) || name === "vec_native"
|
|
374
|
+
const tables = master.filter((m) => m.type === "table" && !virtual.includes(String(m.name)) && !skipTable(String(m.name)))
|
|
375
|
+
const copied = new Set(tables.map((t) => String(t.name)))
|
|
376
|
+
const qualify = (sql: string, kind: "TABLE" | "INDEX" | "VIEW" | "TRIGGER"): string =>
|
|
377
|
+
sql.replace(new RegExp(`^\\s*CREATE\\s+(UNIQUE\\s+)?${kind}\\s+(IF\\s+NOT\\s+EXISTS\\s+)?`, "i"), (_m, u) => `CREATE ${u ?? ""}${kind} dst.`)
|
|
378
|
+
S.exec("BEGIN")
|
|
379
|
+
try {
|
|
380
|
+
for (const t of tables) S.exec(qualify(String(t.sql), "TABLE"))
|
|
381
|
+
for (const t of tables) {
|
|
382
|
+
const name = qIdent(String(t.name))
|
|
383
|
+
S.exec(`INSERT INTO dst.${name} SELECT * FROM main.${name}`)
|
|
384
|
+
counts[String(t.name)] = Number((S.prepare(`SELECT count(*) AS c FROM main.${name}`).get() as { c: number }).c)
|
|
385
|
+
}
|
|
386
|
+
for (const m of master) {
|
|
387
|
+
if (m.type === "index" && copied.has(String(m.tbl_name)) && !/libsql_vector_idx/i.test(String(m.sql))) S.exec(qualify(String(m.sql), "INDEX"))
|
|
388
|
+
else if ((m.type === "view" || m.type === "trigger") && !virtual.includes(String(m.tbl_name))) S.exec(qualify(String(m.sql), m.type === "view" ? "VIEW" : "TRIGGER"))
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
S.exec("DELETE FROM dst.sqlite_sequence")
|
|
392
|
+
S.exec("INSERT INTO dst.sqlite_sequence SELECT * FROM main.sqlite_sequence")
|
|
393
|
+
} catch {} // no AUTOINCREMENT tables
|
|
394
|
+
S.exec("COMMIT")
|
|
395
|
+
} catch (e) {
|
|
396
|
+
try {
|
|
397
|
+
S.exec("ROLLBACK")
|
|
398
|
+
} catch {}
|
|
399
|
+
throw e
|
|
400
|
+
}
|
|
401
|
+
S.exec("DETACH DATABASE dst")
|
|
402
|
+
S.close()
|
|
403
|
+
src = undefined
|
|
404
|
+
|
|
405
|
+
// Second step, on the new file alone: verify, and rebuild the vector index Chitta keeps for an encrypted store.
|
|
406
|
+
const D = new L(tmp, { encryptionKey: newKey })
|
|
407
|
+
try {
|
|
408
|
+
if (String((D.prepare("PRAGMA integrity_check").get() as { integrity_check?: string })?.integrity_check ?? "") !== "ok") throw new OrgKeyError("the converted database failed its integrity check; nothing was changed")
|
|
409
|
+
for (const [name, n] of Object.entries(counts)) {
|
|
410
|
+
const c = Number((D.prepare(`SELECT count(*) AS c FROM ${qIdent(name)}`).get() as { c: number }).c)
|
|
411
|
+
if (c !== n) throw new OrgKeyError(`the converted database holds ${c} rows in ${name} instead of ${n}; nothing was changed`)
|
|
412
|
+
}
|
|
413
|
+
rebuildNativeVectors(D)
|
|
414
|
+
for (const pragma of ["PRAGMA journal_mode = WAL", "PRAGMA wal_checkpoint(TRUNCATE)"]) {
|
|
415
|
+
try {
|
|
416
|
+
D.exec(pragma) // best effort: closing the connection checkpoints anyway; a log left behind travels with the file below
|
|
417
|
+
} catch {}
|
|
418
|
+
}
|
|
419
|
+
} finally {
|
|
420
|
+
D.close()
|
|
421
|
+
}
|
|
422
|
+
} catch (e) {
|
|
423
|
+
try {
|
|
424
|
+
src?.close()
|
|
425
|
+
} catch {}
|
|
426
|
+
for (const f of [tmp, `${tmp}-wal`, `${tmp}-shm`]) rmSync(f, { force: true })
|
|
427
|
+
throw e
|
|
428
|
+
}
|
|
429
|
+
// swap: the old file stays as the backup; a stale -wal/-shm of the old file must not be read against the new one
|
|
430
|
+
renameSync(path, backup)
|
|
431
|
+
try {
|
|
432
|
+
if (statSync(`${path}-wal`).size > 0) renameSync(`${path}-wal`, `${backup}-wal`) // the old file's log stays with the old file
|
|
433
|
+
} catch {}
|
|
434
|
+
rmSync(`${path}-wal`, { force: true })
|
|
435
|
+
rmSync(`${path}-shm`, { force: true })
|
|
436
|
+
rmSync(`${tmp}-shm`, { force: true }) // the shared-memory index is rebuilt on open; a log (if one is left) must stay with its file
|
|
437
|
+
try {
|
|
438
|
+
renameSync(`${tmp}-wal`, `${path}-wal`)
|
|
439
|
+
} catch {}
|
|
440
|
+
renameSync(tmp, path)
|
|
441
|
+
try {
|
|
442
|
+
chmodSync(path, 0o600)
|
|
443
|
+
chmodSync(backup, 0o600)
|
|
444
|
+
} catch {}
|
|
445
|
+
return { backup, tables: counts }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Chitta's native vector index for an encrypted store (table `vec_native` + a DiskANN index over it) must hold EVERY chunk, or a
|
|
449
|
+
* search that uses it would silently miss the others. Rebuilt here from the chunks' stored embeddings, with Chitta's own DDL. */
|
|
450
|
+
/**
|
|
451
|
+
* libSQL keeps one row per vector index in `libsql_vector_meta_shadow` (a WITHOUT ROWID table keyed by the index name). `VACUUM INTO`
|
|
452
|
+
* re-creates each vector index in the copy, which writes that row, and then copies the source's rows of the table as well: the copy
|
|
453
|
+
* holds the name twice and fails SQLite's integrity check ("row not in PRIMARY KEY order"), so every backup of a store with
|
|
454
|
+
* vectors was refused. Found on the first real deployment. Keep one row per index name, rewritten in key order.
|
|
455
|
+
*/
|
|
456
|
+
export function dedupeVectorMeta(D: RawHandle): void {
|
|
457
|
+
if (!D.prepare("SELECT 1 AS x FROM sqlite_master WHERE type = 'table' AND name = 'libsql_vector_meta_shadow'").get()) return
|
|
458
|
+
const d = D.prepare("SELECT count(*) - count(DISTINCT name) AS d FROM libsql_vector_meta_shadow").get() as { d: number }
|
|
459
|
+
if (!Number(d.d)) return
|
|
460
|
+
D.exec("BEGIN")
|
|
461
|
+
try {
|
|
462
|
+
D.exec("CREATE TEMP TABLE brain_vector_meta AS SELECT name, metadata FROM libsql_vector_meta_shadow GROUP BY name")
|
|
463
|
+
D.exec("DELETE FROM libsql_vector_meta_shadow")
|
|
464
|
+
D.exec("INSERT INTO libsql_vector_meta_shadow (name, metadata) SELECT name, metadata FROM brain_vector_meta ORDER BY name")
|
|
465
|
+
D.exec("DROP TABLE brain_vector_meta")
|
|
466
|
+
D.exec("COMMIT")
|
|
467
|
+
} catch (e) {
|
|
468
|
+
try {
|
|
469
|
+
D.exec("ROLLBACK")
|
|
470
|
+
} catch {}
|
|
471
|
+
throw e
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function rebuildNativeVectors(D: RawHandle): void {
|
|
476
|
+
try {
|
|
477
|
+
const first = D.prepare("SELECT embedding FROM chunks WHERE embedding IS NOT NULL LIMIT 1").get() as { embedding?: ArrayBuffer | Uint8Array | string } | undefined
|
|
478
|
+
if (!first?.embedding) return
|
|
479
|
+
const bytes = typeof first.embedding === "string" ? null : first.embedding.byteLength
|
|
480
|
+
if (bytes === null) return // legacy JSON embeddings: Chitta rewrites them itself; brute force serves until then
|
|
481
|
+
const dim = bytes / 4
|
|
482
|
+
D.exec("DROP INDEX IF EXISTS vec_native_idx")
|
|
483
|
+
D.exec("DROP TABLE IF EXISTS vec_native")
|
|
484
|
+
D.exec(`CREATE TABLE vec_native (rowid INTEGER PRIMARY KEY, embedding F32_BLOB(${dim}))`)
|
|
485
|
+
const ins = D.prepare("INSERT INTO vec_native (rowid, embedding) VALUES (?, vector32(?))")
|
|
486
|
+
const all = D.prepare("SELECT rowid AS r, embedding AS e FROM chunks WHERE embedding IS NOT NULL").all() as Array<{ r: number; e: ArrayBuffer | Uint8Array }>
|
|
487
|
+
D.exec("BEGIN")
|
|
488
|
+
for (const c of all) {
|
|
489
|
+
const u8 = c.e instanceof Uint8Array ? c.e : new Uint8Array(c.e)
|
|
490
|
+
if (u8.byteLength !== dim * 4) continue
|
|
491
|
+
const f = new Float32Array(u8.slice().buffer)
|
|
492
|
+
ins.run(Math.trunc(c.r), JSON.stringify(Array.from(f)))
|
|
493
|
+
}
|
|
494
|
+
D.exec("COMMIT")
|
|
495
|
+
D.exec("CREATE INDEX vec_native_idx ON vec_native (libsql_vector_idx(embedding, 'metric=cosine'))")
|
|
496
|
+
} catch {
|
|
497
|
+
try {
|
|
498
|
+
D.exec("ROLLBACK")
|
|
499
|
+
} catch {}
|
|
500
|
+
try {
|
|
501
|
+
D.exec("DROP INDEX IF EXISTS vec_native_idx")
|
|
502
|
+
D.exec("DROP TABLE IF EXISTS vec_native")
|
|
503
|
+
} catch {} // brute force serves (Chitta falls back by itself)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** `VACUUM INTO` copies libSQL's vector index tables in a state its own integrity check rejects ("row not in PRIMARY KEY order for
|
|
508
|
+
* libsql_vector_meta_shadow"). The copy is rebuilt here, from the chunks, so a backup restores to an index that is consistent. */
|
|
509
|
+
export function rebuildVectorIndexAt(path: string, key: string): void {
|
|
510
|
+
const L = loadLibsql()
|
|
511
|
+
const D = new L(path, { encryptionKey: key })
|
|
512
|
+
try {
|
|
513
|
+
dedupeVectorMeta(D)
|
|
514
|
+
if (D.prepare("SELECT 1 AS x FROM sqlite_master WHERE name = 'vec_native'").get()) rebuildNativeVectors(D)
|
|
515
|
+
} finally {
|
|
516
|
+
D.close()
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Best-effort removal of a file that held plain text: overwritten with zeros, then unlinked. On SSDs, copy-on-write and journaling
|
|
521
|
+
* file systems this does NOT guarantee the old bytes are gone: delete the disk or snapshot if that matters. */
|
|
522
|
+
export function wipeFile(path: string): void {
|
|
523
|
+
try {
|
|
524
|
+
const size = statSync(path).size
|
|
525
|
+
const fd = openSync(path, "r+")
|
|
526
|
+
try {
|
|
527
|
+
const zero = Buffer.alloc(1 << 20)
|
|
528
|
+
for (let off = 0; off < size; off += zero.length) {
|
|
529
|
+
const n = Math.min(zero.length, size - off)
|
|
530
|
+
writeSync(fd, zero, 0, n, off)
|
|
531
|
+
}
|
|
532
|
+
} finally {
|
|
533
|
+
closeSync(fd)
|
|
534
|
+
}
|
|
535
|
+
} catch {}
|
|
536
|
+
rmSync(path, { force: true })
|
|
537
|
+
}
|