BrainSync Universal is intentionally one source file, no build step, no transpiler. This document explains the moving parts so you can find your way around src/extension.js.
┌──────────────────────────────────────────────────────┐
│ IDE host (VS Code / Kiro / Windsurf / Cursor / ...) │
└─────────────┬───────────────────────────┬────────────┘
│ │
activates extension file watchers
│ │
▼ ▼
BrainSyncController schedule auto-sync
│
┌─────────────────────┼──────────────────────┐
▼ ▼ ▼
BrainSyncStore BrainSyncDashboard Watchers
(memory.jsonl ops) (webview UI) (debounced triggers)
│
▼
Per-workspace `.brainsync/` ~/.brainsync-vault/projects/<key>/
memory.jsonl ◀──────── mirror ───────► memory.jsonl
shadows/ latest-context.md
sync-state.json
backups/
Every captured note, conversation, rule, decision, lesson, or imported chunk lives as one line of newline-delimited JSON in .brainsync/memory.jsonl. The shape is MemoryEntry, fields documented at the top of extension.js. Entries are deduped by ${projectKey}:${kind}:${hash} where hash = sha256(projectKey || kind || title || content). The newer updatedAt wins on dedup.
The canonical log is the workspace log at <workspace>/.brainsync/memory.jsonl. On every read, BrainSync also reads the vault log at ~/.brainsync-vault/projects/<project-key>/memory.jsonl and merges them. Writes go to both. The vault is what lets the same repo opened in different IDEs converge: the project key is derived from the git remote URL (or workspace path fallback), so all hosts hit the same vault folder.
Every time BrainSync writes to an export target (e.g., .windsurfrules), it stores a byte-exact copy in .brainsync/shadows/<target-flattened>. On the next sync, we diff currentFile vs shadow. Anything novel in currentFile is content the IDE agent added — we extract it, ingest it as memory, then re-render the export with everything included. This is what makes "Windsurf agent edits .windsurfrules, Kiro picks it up next session" work without complex parsing.
The diff is line-based and skips known boilerplate markers (frontmatter, BrainSync section headers, etc.) so we don't false-positive on our own template.
A naive watcher would chase its own tail: write .windsurfrules → watcher fires → trigger import → trigger export → write .windsurfrules → repeat forever. Three guards prevent that:
lastWriteTimecooldown — when BrainSync writes, we record the time. The watcher checksDate.now() - lastWriteTime < 1500msand ignores changes inside that window.- No-op hash skip — if the rendered context hash matches the previous export and no entries were rescued from shadows, the entire export phase is skipped.
- Adaptive debounce — base 800ms, backs off to 4s after 3 consecutive no-ops, 8s after 6+. Resets to 800ms on real change. This keeps idle sync near-zero CPU while staying responsive.
.brainsync/sync-state.json records { mtimeMs, size } per file the importer touches. On the next pass, files whose stat hasn't changed are skipped before any read happens. This is the single biggest performance win on large workspaces.
PHASE 1 — RESCUE
for each export target:
read current file
read shadow
delta = diff(current, shadow)
if delta has novel content:
ingest as memory entries
PHASE 2 — BUILD
entries = readAllEntries() // workspace + vault, deduped, ranked
contextText = buildContext(entries) // markdown sections by kind
if hash(contextText) == lastExportHash and no rescues:
return noop
PHASE 3 — WRITE
for each export target:
next = targetSpecificContext(target, contextText) // wraps with frontmatter for .mdc / kiro
if existing == next: ensure shadow exists, continue
backup existing → .brainsync/backups/<timestamp>/
write next atomically
write shadow := next
update sync-state mtime
update sync-state.lastExportHash
notify dashboard
writeTextAtomic writes to a .tmp file in the same directory then renames. On Windows, where rename can fail with EPERM if a watcher has the target open, it falls back through vscode.workspace.fs.writeFile → fs.copyFile → direct fs.writeFile. The cleanup of the tmp file happens in a finally block.
Beyond rule files, BrainSync ingests "off-disk" chat sources where the IDE writes plaintext:
| Source | Format | What we extract |
|---|---|---|
Antigravity Brain (~/.gemini/antigravity/brain/) |
Plain .txt / .md / .json |
Per-session overview log (*.system_generated/logs/overview.txt) and standalone artifacts that mention the project. |
Windsurf Cascade NDJSON (~/AppData/Roaming/Windsurf/User/acp-events/*.ndjson) |
NDJSON event stream | Stitched user/agent message chunks per session, filtered to ones mentioning the project. |
Windsurf Cascade SQLite (state.vscdb → windsurf.acp.metadataCache) |
SQLite TEXT cell with JSON | Per-session metadata (title, status, timestamps, cwd) for sessions whose workspace matches the project. |
Windsurf Cascade .pb files |
Encrypted protobuf | Cannot read. Confirmed by entropy analysis (~37% printable, no readable runs ≥ 20 chars). The metadata reader above is the workaround. |
For sources we can't read, the workflow is "select chat text in the IDE → run BrainSync Universal: Capture Clipboard as Conversation". One keystroke gets the actual content into BrainSync without trying to decrypt anything.
To read Windsurf's state.vscdb, BrainSync ships a vendored copy of sql.js under vendor/sql.js/ (only sql-wasm.js and sql-wasm.wasm, ~700KB combined). This is a pure WASM build of SQLite that runs in any Node context. We chose this over better-sqlite3 because:
- No native compile step — works inside an Electron-based extension host without ABI matching.
- One file (the WASM) plus one tiny loader. Easy to vendor.
- BrainSync only does point lookups; performance isn't the bottleneck.
The loader is lazy — loadSqlJs() only initializes when a Windsurf metadata read is actually requested.
src/
extension.js # everything: store, sync, importers, watchers, UI
vendor/
sql.js/
sql-wasm.js # ~46KB
sql-wasm.wasm # ~644KB
package.json # upstream metadata + license
LICENSE # MIT
resources/
brainsync.svg # activity-bar icon
docs/
ARCHITECTURE.md # this file
PUBLISHING.md # marketplace + Open VSX walkthrough
scripts/
build-vsix.ps1 # convenience wrapper around vsce
package.json # extension manifest
README.md
CHANGELOG.md
CONTRIBUTING.md
LICENSE
extension.js is intentionally a single file. Splitting it across modules would add a build step and not help readability much — the file is ~2200 lines of straight-line code with each section commented. If you find a section growing beyond ~600 lines, that's the right time to extract.
- No transpiler / bundler. Plain ES2022, runs as-is.
- No test framework yet. Smoke testing is "load the VSIX, look at the rule files, run a few captures." If you contribute one, prefer Node
node:testover Jest/vitest to keep the dependency tree small. - No telemetry. Not optional, not opt-in, not "anonymous." None.
- No outbound network at runtime. The only network calls in the project are at publish time (
vsce publish,ovsx publish). - No vector store / embeddings. Search is keyword-scored. If you need semantic, plug your own retriever in alongside; don't replace the canonical store.