### Install Project Dependencies Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/desktop/README.md Run this command to install all required project dependencies using pnpm. ```bash $ pnpm install ``` -------------------------------- ### Start Development Server Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/desktop/README.md Launch the application in development mode. ```bash $ pnpm dev ``` -------------------------------- ### Define Client Capabilities Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md Example of the capability object sent by the client to the agent during initialization. ```ts { fs: { readTextFile: true, writeTextFile: true }, terminal: true, // implements createTerminal etc. _meta: { terminal_output: true }, // wants live Bash terminal frames auth: { terminal: true, _meta: { "terminal-auth": true, gateway: true } }, elicitation: { ... }, // structured form input } ``` -------------------------------- ### Define Agent Capabilities Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md Example of the capability object advertised by the claude-agent-acp. ```ts { protocolVersion: 1, agentCapabilities: { promptCapabilities: { image: true, embeddedContext: true }, mcpCapabilities: { http: true, sse: true }, loadSession: true, sessionCapabilities: { fork: {}, list: {}, resume: {}, close: {} }, _meta: { claudeCode: { promptQueueing: true } }, }, authMethods: [ /* claude/console terminal logins, optional gateway method */ ], } ``` -------------------------------- ### Define HTML entry point Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/server/CLAUDE.md Use standard HTML files to import frontend scripts. ```html

Hello, world!

``` -------------------------------- ### Implement server with Bun.serve Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/server/CLAUDE.md Configure a server with routing and optional WebSocket support using Bun's native API. ```ts import index from "./index.html" Bun.serve({ routes: { "/": index, "/api/users/:id": { GET: (req) => { return new Response(JSON.stringify({ id: req.params.id })); }, }, }, // optional websocket support websocket: { open: (ws) => { ws.send("Hello, world!"); }, message: (ws, message) => { ws.send(message); }, close: (ws) => { // handle close } }, development: { hmr: true, console: true, } }) ``` -------------------------------- ### projects.create Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Creates a new project with the specified name and root path. ```APIDOC ## projects.create ### Description Creates a new project. The server validates that the root path is absolute, exists, and is not already associated with an existing project. ### Signature `projects.create({ name: string, rootPath: AbsolutePath }) → Project` ``` -------------------------------- ### projects.create({ name, rootPath }) Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Creates a new project with the specified name and root path. ```APIDOC ## projects.create({ name, rootPath }) ### Description Creates a new project. ### Parameters - **name** (string) - The name of the project. - **rootPath** (string) - The root directory path for the project. ### Returns - **Project** - The created project object. ``` -------------------------------- ### hello Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md Initializes the connection by performing authentication and client identification. This must be the first message sent after the WebSocket opens. ```APIDOC ## hello ### Description Performs authentication and client identification. Must be the first message sent after the WebSocket opens. ### Request - **type** (string) - Required - "hello" - **id** (string) - Required - Unique request identifier ### Response - **relayVersion** (string) - Relay version information - **agent** (object) - Agent name and version - **session** (object) - Session mode ("fresh" or "resumed") and sequence number - **capabilities** (object) - Supported features including resume status and available channels ``` -------------------------------- ### Initialize SQLite Engine Pragmas Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/db.md Startup configuration for the SQLite database connection, including WAL mode and foreign key enforcement. ```sql PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000; ``` -------------------------------- ### Build Application for Target Platforms Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/desktop/README.md Execute platform-specific build commands to package the application for Windows, macOS, or Linux. ```bash # For windows $ pnpm build:win # For macOS $ pnpm build:mac # For Linux $ pnpm build:linux ``` -------------------------------- ### Create Workspace Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Initializes a new workspace as either a local directory or a git worktree. ```ts workspaces.create({ projectId: ProjectId, name: string, config: | { kind: "local" } | { kind: "worktree"; branch?: string; baseBranch?: string }, }) → Workspace ``` -------------------------------- ### Build abduco binaries Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/desktop/resources/bin/README.md Commands to build the abduco binaries for the host architecture or specific targets. ```bash # From apps/desktop. Builds for the HOST arch and writes resources/bin/abduco-. scripts/build-abduco.sh ``` ```bash TARGET=linux-arm64 scripts/build-abduco.sh # needs a matching CC/SDK ABDUCO_VERSION=0.6 scripts/build-abduco.sh # override the pinned version ``` -------------------------------- ### Initialize Electron ACP Client Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md Sets up a WebSocket connection and initializes the ACP client with file system capabilities and session management. ```typescript // apps/desktop/src/main/acp-client.ts import WebSocket from "ws"; import * as acp from "@agentclientprotocol/sdk"; import { wsAcpStream, encode } from "@sandcastle/acp-transport"; const ws = new WebSocket("wss://relay.sandcastle.dev/acp", { headers: { Authorization: `Bearer ${token}` }, }); await new Promise(r => ws.once("open", r)); let lastSeq = 0; const onNonAcp = (env) => { if (env.kind === "control") handleControl(env); // else if (env.kind === "git") ... }; // Wrap the stream so we (a) tag each inbound ACP envelope's seq, (b) emit envelopes outbound. const stream = wsAcpStream(ws, onNonAcp, env => { if (env.seq) lastSeq = env.seq; }); const conn = new acp.ClientSideConnection(_a => ({ async sessionUpdate(p) { sendToRenderer("session-update", p); }, async requestPermission(p) { return await askRendererForPermission(p); }, async readTextFile(p) { return { content: await fs.readFile(p.path, "utf8") }; }, async writeTextFile(p) { await fs.writeFile(p.path, p.content); return {}; }, }), stream); // Control: hello before any ACP traffic ws.send(encode({ kind: "control", type: "hello", id: 1, payload: { token } })); // On reconnect: send control.resume with our lastSeq before sending any ACP frames ws.on("open-after-reconnect", () => { ws.send(encode({ kind: "control", type: "resume", id: 2, payload: { sessionId, since: lastSeq } })); }); // Normal ACP usage from here: const init = await conn.initialize({ protocolVersion: acp.PROTOCOL_VERSION, clientCapabilities: { fs: { readTextFile: true, writeTextFile: true }, _meta: { terminal_output: true } }, }); const session = await conn.newSession({ cwd: "/var/sandcastle/wsp/abc", mcpServers: [] }); ``` -------------------------------- ### Process Tree Comparison Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/pty-persistence.md Visual representation of the process hierarchy difference between the current direct-spawn approach and the proposed abduco-based persistence model. ```text Today: Electron main ──spawn──▶ zsh ──▶ claude (one tree; quit ⇒ SIGHUP ⇒ dead) Proposed: Electron main ──spawn──▶ abduco client ──socket──▶ abduco server (daemon, setsid) ──▶ zsh ──▶ claude ▲ quit kills only the client; server + zsh + claude keep running ``` -------------------------------- ### workspaces.create Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Creates a new workspace for a project, supporting either local or worktree configurations. ```APIDOC ## workspaces.create ### Description Creates a new workspace. Supports 'local' kind (one per project) or 'worktree' kind (git-based). ### Signature `workspaces.create({ projectId: ProjectId, name: string, config: { kind: "local" } | { kind: "worktree"; branch?: string; baseBranch?: string } }) → Workspace` ``` -------------------------------- ### projects.list() Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Retrieves a list of all available projects. ```APIDOC ## projects.list() ### Description Retrieves a list of all projects. ### Returns - **Project[]** - A list of project objects. ``` -------------------------------- ### workspaces.create({ projectId, name, config }) Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Creates a new workspace within a project. ```APIDOC ## workspaces.create({ projectId, name, config }) ### Description Creates a new workspace. ### Parameters - **projectId** (string) - The ID of the project. - **name** (string) - The name of the workspace. - **config** (object) - Configuration settings for the workspace. ### Returns - **Workspace** - The created workspace object. ``` -------------------------------- ### Configure Atom Caching and Lifecycle Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md Demonstrates how to set registry-wide defaults, per-atom idle TTL, and keep-alive status to control memory usage and disposal. ```ts // Registry-wide default (ours is 400ms — see rpc/registry.ts) AtomRegistry.make({ defaultIdleTTL: 400 }) // Per-atom idle TTL. Finite => dispose after idle; Infinity => keepAlive; 0 => dispose immediately when idle myAtom.pipe(Atom.setIdleTTL("5 minutes")) myAtom.pipe(Atom.setIdleTTL(0)) // evict the instant it's unobserved // Pin forever (survives no-subscribers). Opt out again with Atom.autoDispose myAtom.pipe(Atom.keepAlive) ``` -------------------------------- ### Run server with hot reloading Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/server/CLAUDE.md Execute the server script with the --hot flag for development. ```sh bun --hot ./index.ts ``` -------------------------------- ### Development Workflow Commands Source: https://github.com/andresmarpz/sandcastle/blob/main/README.md Standard commands for managing the monorepo development lifecycle. ```bash pnpm install pnpm dev # run the desktop app with HMR pnpm quality # typecheck + biome check pnpm build # production build ``` -------------------------------- ### Pattern Match AsyncResult in UI Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md Standard and waiting-aware pattern matching for rendering UI components based on the current state. ```ts AsyncResult.match(result, { onInitial: () => , onFailure: (f) => , onSuccess: (s) => , }) // Waiting-aware: onWaiting fires first for any waiting result AND for Initial AsyncResult.matchWithWaiting(result, { onWaiting: (r) => /* show stale AsyncResult.value(r) with a subtle spinner */, onError: (e) => ..., onDefect: (d) => ..., onSuccess: (s) => ..., }) ``` -------------------------------- ### Create React frontend component Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/server/CLAUDE.md Render a React component into the document body with direct CSS imports. ```tsx import React from "react"; import { createRoot } from "react-dom/client"; // import .css files directly and it works import './index.css'; const root = createRoot(document.body); export default function Frontend() { return

Hello, world!

; } root.render(); ``` -------------------------------- ### Create service-backed atoms with Atom.runtime Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md Provides atoms access to sync-engine services like databases or network layers. ```ts const runtime = Atom.runtime(MyServicesLayer) const thing = runtime.atom(Effect.gen(function* () { /* use services */ })) const doThing = runtime.fn(handler, { reactivityKeys: ["things"] }) ``` -------------------------------- ### Package Dependency Layering Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/directories.md Visualizes the dependency relationship between the contracts and entities packages. ```text @sandcastle/contracts │ uses entity types in RPC payloads ▼ @sandcastle/entities ``` -------------------------------- ### Monorepo Directory Structure Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/directories.md Visual representation of the project root, including application directories, shared packages, and configuration files. ```text sandcastle/ ├── apps/ │ ├── server/ # Bun server │ └── desktop/ # Electron + React client ├── packages/ │ ├── entities/ # Domain entities: Project, Workspace, branded IDs │ └── contracts/ # WS RPC group + typed errors ├── docs/ │ ├── architecture.md │ ├── directories.md # this file │ ├── db.md │ └── rpc-contract.md ├── patches/ # pnpm patch-package outputs ├── package.json # root scripts; private ├── pnpm-workspace.yaml ├── pnpm-lock.yaml ├── tsconfig.json # root TS base └── turbo.json ``` -------------------------------- ### Directory structure for packages/contracts Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/directories.md Represents the file layout for the contracts package, which defines the WebSocket RPC interface. ```text packages/contracts/ ├── src/ │ ├── index.ts │ ├── errors.ts # typed error union (ProjectPathInvalid, WorktreeCreateFailed, …) │ └── rpc/ │ ├── index.ts # SandcastleRpc group export │ ├── projects.ts # projects.{list, create, rename, delete} │ └── workspaces.ts # workspaces.{list, create, delete} ├── package.json # deps: effect, @effect/rpc, @sandcastle/entities └── tsconfig.json ``` -------------------------------- ### Abduco Session Helper Utilities Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/pty-persistence.md Provides helpers for resolving the abduco binary path, defining the socket directory, and generating filesystem-safe session names from leafId. ```ts import { createHash } from "node:crypto"; import os from "node:os"; import { join } from "node:path"; import process from "node:process"; import { app } from "electron"; // abduco is unix-only. Windows falls back to a plain shell (no persistence). export const PERSISTENCE_SUPPORTED = process.platform !== "win32"; // Resolve the bundled binary. resources/** is asarUnpack'd (electron-builder.yml), // so in prod it lives under process.resourcesPath; in dev, under the repo resources/. export const abducoBin = (): string => { const arch = process.arch; // "arm64" | "x64" const name = `abduco-${process.platform}-${arch}`; return app.isPackaged ? join(process.resourcesPath, "bin", name) : join(app.getAppPath(), "resources", "bin", name); }; // abduco nests sockets as ///@ // and sun_path caps at ~104 bytes on macOS. os.tmpdir() looks short but on macOS // is /var/folders//T (~48 bytes), which overflowed once abduco appended its // own nesting → "create-session: File name too long". Anchor at a short fixed root. export const socketDir = (): string => "/tmp/sandcastle"; // abduco addresses sessions by NAME inside ABDUCO_SOCKET_DIR. Hash leafId to a // short, filesystem-safe name so the socket path stays well under the limit. export const sessionName = (leafId: string): string => createHash("sha256").update(leafId).digest("hex").slice(0, 16); export const socketPath = (leafId: string): string => join(socketDir(), sessionName(leafId)); ``` -------------------------------- ### Release Tagging Source: https://github.com/andresmarpz/sandcastle/blob/main/README.md Commands to trigger the automated release workflow via GitHub Actions. ```bash git tag v && git push origin v ``` -------------------------------- ### Configure Atom Refetching Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md Use withRefresh for live data polling or refreshOnWindowFocus for desktop-friendly revalidation. ```ts // Re-run every 30s while the atom is mounted (interval/polling) const liveStatus = Atom.make(fetchStatus).pipe(Atom.withRefresh("30 seconds")) // On window focus (built-in) const atom = Atom.make(fetchThing).pipe(Atom.refreshOnWindowFocus) // equivalently swr({ revalidateOnFocus: true }) ``` -------------------------------- ### List Workspaces Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Retrieves all active workspaces for a project, ordered by creation time. ```ts workspaces.list({ projectId: ProjectId }) → Workspace[] ``` -------------------------------- ### Provide Registry Context Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md Configures the application registry using a shared singleton within the React component tree. ```typescript // rpc/registry.ts owns the registry; main.tsx provides it: {app} ``` -------------------------------- ### Implement persistent storage with Atom.kvs Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md Creates a writable atom backed by localStorage or a KeyValueStore for durable local preferences. ```ts const theme = Atom.kvs({ runtime: kvsRuntime, // Atom.runtime providing a KeyValueStore key: "theme", schema: Schema.Literal("light", "dark"), defaultValue: () => "dark", // mode: "async" to expose load as AsyncResult; default is sync }) ``` -------------------------------- ### projects.list Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Retrieves a list of all active, non-soft-deleted projects, ordered by creation date. ```APIDOC ## projects.list ### Description Returns all active (non-soft-deleted) projects, in creation order. ### Signature `projects.list() → Project[]` ``` -------------------------------- ### Projects RPC Methods Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Method signatures for project management operations. ```ts projects.list() → Project[] ``` ```ts projects.create({ name: string, rootPath: AbsolutePath, }) → Project ``` ```ts projects.rename({ projectId: ProjectId, name: string }) → Project ``` ```ts projects.delete({ projectId: ProjectId }) → void ``` -------------------------------- ### Signal-Driven Refresh Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md Use signal-driven refreshes for lightweight pub/sub scenarios where reactivity keys are unnecessary. ```ts const bumpSignal = Atom.make(0) const atom = Atom.make(build).pipe(Atom.makeRefreshOnSignal(bumpSignal)) registry.set(bumpSignal, (n) => n + 1) // forces `atom` to rebuild ``` -------------------------------- ### Run tests with Bun Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/server/CLAUDE.md Use the built-in test runner for unit tests. ```ts import { test, expect } from "bun:test"; test("hello world", () => { expect(1).toBe(1); }); ``` -------------------------------- ### Render SWR Atoms in React Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md Demonstrates consuming SWR-enabled atoms using useAtomSuspense for non-blocking refreshes or useAtomValue for manual loading state handling. ```ts // Suspends once on initial load; renders through refreshes const user = useAtomSuspense(userAtom) // AsyncResult.Success // Or read the raw AsyncResult and show your own "refreshing" affordance: const result = useAtomValue(userAtom) const data = AsyncResult.value(result) // Option, present even while waiting const refreshing = result.waiting ``` -------------------------------- ### Importing Atom and React Bindings Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md Standard import map for accessing core reactivity primitives and React-specific hooks. ```ts // Core (lives inside `effect` now) import { Atom, AsyncResult, AtomRegistry, AtomRef, AtomRpc, AtomHttpApi, Hydration, Reactivity, } from "effect/unstable/reactivity" // React bindings import { useAtomValue, useAtom, useAtomSet, useAtomRefresh, useAtomMount, useAtomSuspense, useAtomSubscribe, useAtomInitialValues, useAtomRef, useAtomRefProp, useAtomRefPropValue, RegistryContext, RegistryProvider, scheduleTask, HydrationBoundary, make as scopedAtom, } from "@effect/atom-react" ``` -------------------------------- ### workspaces.list({ projectId }) Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Lists all workspaces associated with a specific project. ```APIDOC ## workspaces.list({ projectId }) ### Description Retrieves a list of workspaces for a given project. ### Parameters - **projectId** (string) - The ID of the project. ### Returns - **Workspace[]** - A list of workspace objects. ``` -------------------------------- ### workspaces.list Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Retrieves a list of all active workspaces for a specific project. ```APIDOC ## workspaces.list ### Description Returns all active workspaces under the given project, in creation order. Rejects if the project is unknown or soft-deleted. ### Signature `workspaces.list({ projectId: ProjectId }) → Workspace[]` ``` -------------------------------- ### Visualize ACP Architecture Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md Diagram showing the communication flow from the Electron UI to the Anthropic API. ```text Our Electron UI ──ACP via WS──▶ Our Node relay ──ACP via stdio──▶ claude-agent-acp ──Claude SDK──▶ claude (binary) ──HTTPS──▶ Anthropic API ``` -------------------------------- ### Define Project Domain Model Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/architecture.md Represents the project structure stored in the database, including path and git status metadata. ```ts Project = { id: ProjectId // uuid, branded name: string // user-facing label rootPath: AbsolutePath // absolute path on the local filesystem isGit: boolean // probed at create-time createdAt: IsoDateTime updatedAt: IsoDateTime deletedAt: IsoDateTime | null } ``` -------------------------------- ### projects.rename Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Updates the display name of an existing project. ```APIDOC ## projects.rename ### Description Updates the name and updated_at timestamp for the specified project. ### Signature `projects.rename({ projectId: ProjectId, name: string }) → Project` ``` -------------------------------- ### Proposed Repository Layout Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md The project structure separates the Node.js relay application, the Electron desktop client, and shared ACP packages. ```text sandcastle/ ├── apps/ │ ├── api/ (this — the Node relay) │ │ ├── src/ │ │ │ ├── index.ts # HTTP + WS server entry │ │ │ ├── auth.ts # WS upgrade auth, JWT verification │ │ │ ├── relay.ts # SessionBroker, frame router, child lifecycle │ │ │ ├── transport.ts # ws ↔ acp.Stream adapters; ndjson reframer │ │ │ ├── control.ts # sandcastle/* method handlers │ │ │ ├── interceptors.ts # audit log, metrics, _meta tagging │ │ │ └── env.ts # per-user env construction │ │ └── package.json │ └── desktop/ (Electron app) │ ├── src/ │ │ ├── main/ # Electron main: WS connection + ACP client │ │ ├── preload/ # IPC bridge │ │ └── renderer/ # React UI rendering session/update notifications │ └── package.json └── packages/ ├── acp-transport/ # shared: wsStream(ws): acp.Stream, control-method types └── acp-types/ # shared: re-exports + sandcastle/* schemas ``` -------------------------------- ### Perform Manual SWR via Registry Refresh Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md Triggers a background refresh for tracked atoms while maintaining the stale value on screen. ```ts // lib/prefetch.ts (paraphrased) for (const atom of tracked) appRegistry.refresh(atom) // keepAlive keeps stale value on screen ``` -------------------------------- ### projects.rename({ projectId, name }) Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/rpc-contract.md Renames an existing project. ```APIDOC ## projects.rename({ projectId, name }) ### Description Renames an existing project identified by its ID. ### Parameters - **projectId** (string) - The ID of the project to rename. - **name** (string) - The new name for the project. ### Returns - **Project** - The updated project object. ``` -------------------------------- ### Create PTY Session Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/pty-persistence.md Initializes a PTY session, optionally wrapping it with abduco for persistence. The shell and environment variables are applied only during the initial creation. ```ts const DETACH_KEY = "^g"; // neutralize accidental user-detach; we detach by killing the client const KEEPALIVE_ON_QUIT = PERSISTENCE_SUPPORTED; const createSession = async (sender, opts: CreateOptions) => { const shell = opts.shell ?? defaultShell(); const cwd = opts.cwd ?? homeDir(); const { env: mcpEnv, args: mcpArgs } = registerSession(opts.id, sender.id, opts.workspaceId, shell); const env = buildEnv({ ...opts.env, ...mcpEnv, SANDCASTLE_SESSION_ID: opts.id }); let file: string, args: string[]; if (PERSISTENCE_SUPPORTED) { await fs.mkdir(socketDir(), { recursive: true }); env.ABDUCO_SOCKET_DIR = socketDir(); file = abducoBin(); // attach-or-create; neutralize detach key; run the shell as the session command args = ["-e", DETACH_KEY, "-A", sessionName(opts.leafId), shell, ...shellArgs(shell), ...mcpArgs]; } else { file = shell; args = [...shellArgs(shell), ...mcpArgs]; } const pty = nodePty.spawn(file, args, { name: "xterm-256color", cols: opts.cols ?? 80, rows: opts.rows ?? 24, cwd, env, useConpty: process.platform === "win32", }); const session: Session = { id: opts.id, leafId: opts.leafId, pty, webContentsId: sender.id, shellPid: null, // resolved lazily, see §5 }; sessions.set(opts.id, session); watchRenderer(sender); wireSessionEvents(session); void resolveShellPid(session); // best-effort eager resolve for cwd/foreground/kill }; ``` -------------------------------- ### Sandcastle Disk Layout Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/architecture.md The directory structure used by Sandcastle for storing the SQLite database and git worktrees. ```text ~/.sandcastle/ ├── sandcastle.db # SQLite └── worktrees/ └── {projectId}/ └── {workspaceId}/ # `git worktree add` lives here ``` -------------------------------- ### Reactivity Keys for Mutations Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md Implement query-key style invalidation by tagging queries with keys and invalidating them upon mutation success. Prefer stable, primitive values for keys to avoid hashing issues. ```ts // Query: declares what it depends on Client.query("workspaces.list", { projectId }, { reactivityKeys: ["workspaces", projectId], timeToLive: Duration.infinity, }) // Mutation: invalidates the same keys after it succeeds Client.mutation("workspaces.create") // pass reactivityKeys when you set it: registry.set(createWorkspace, { projectId, name, reactivityKeys: ["workspaces", projectId] }) ``` -------------------------------- ### pnpm Workspace Configuration Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/directories.md Defines the workspace packages for the pnpm monorepo. ```yaml # pnpm-workspace.yaml packages: - apps/* - packages/* ``` -------------------------------- ### Request Permission Options Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md Defines the permission options presented to the user when a tool requires authorization. ```typescript [ { kind: "allow_always", name: "Always Allow Bash(npm test:*)", optionId: "allow_always" }, { kind: "allow_once", name: "Allow", optionId: "allow" }, { kind: "reject_once", name: "Reject", optionId: "reject" }, ] ``` -------------------------------- ### resume Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md Reattaches to a previously active session using a session ID and sequence number. ```APIDOC ## resume ### Description Reattaches to a previously-active session. If successful, buffered ACP frames are replayed. ### Request - **type** (string) - Required - "resume" - **id** (string) - Required - Unique request identifier - **sessionId** (string) - Required - The ID of the session to resume - **since** (number) - Required - The sequence number to resume from ### Response - **ok** (boolean) - Success status - **replayedThrough** (number) - Sequence number up to which frames were replayed - **reason** (string) - Optional - Error reason if ok is false (e.g., "buffer_expired") ``` -------------------------------- ### Broker Session State Machine Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md Visual representation of the SessionBroker lifecycle states including IDLE, ATTACHED, ACTIVE (DETACHED), DRAINING, and TORNDOWN. ```text ┌──────────┐ WS attaches, ACP initialize done │ IDLE │ ──────────────────────────────────┐ └────┬─────┘ │ │ first prompt creates broker session ▼ ▼ ┌──────────┐ ┌──────────┐ WS detaches (close/error) │ ACTIVE │ │ ATTACHED │ ◀───────────────────────────▶│ (DETACHED│ └────┬─────┘ WS attaches with valid │ buffer │ │ control.resume │ fills) │ │ └────┬─────┘ │ idle for grace_period (no WS, no work) │ ▼ │ ┌──────────┐ ◀───────────────────────────────-─┘ │ DRAINING │ send remaining frames, flush buffer └────┬─────┘ │ ▼ ┌──────────┐ SIGTERM child, free Redis stream │ TORNDOWN │ └────┬─────┘ ``` -------------------------------- ### Perform atomic multi-writes with Atom.batch Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/atom-best-practices.md Use to apply multiple sync-engine changes simultaneously to prevent UI flickering during intermediate states. ```ts Atom.batch(() => { registry.set(a, 1) registry.set(b, "x") }) // derived atoms recompute once, subscribers notified once, after commit ``` -------------------------------- ### Define BufferStore Interface Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md Interface for managing buffered envelopes to support soft-reconnects, allowing for in-memory or persistent storage implementations. ```ts interface BufferStore { append(sessionId: string, env: Envelope): number; // returns assigned seq range(sessionId: string, since: number): Envelope[]; // strictly > since, in seq order oldest(sessionId: string): number; // smallest seq still retained trim(sessionId: string): void; // enforce caps drop(sessionId: string): void; // teardown } ``` -------------------------------- ### Directory structure for packages/entities Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/directories.md Represents the file layout for the domain entities package, which defines transport-agnostic data shapes. ```text packages/entities/ ├── src/ │ ├── index.ts │ ├── ids.ts # branded IDs: ProjectId, WorkspaceId │ ├── time.ts # IsoDateTime schema + helpers │ ├── project.ts # Project │ └── workspace.ts # Workspace, WorkspaceKind, WorkspaceCreateConfig ├── package.json # deps: effect └── tsconfig.json ``` -------------------------------- ### Initialize ACP Connection in Electron Source: https://github.com/andresmarpz/sandcastle/blob/main/client-server-acp.md Initializes the ACP client connection using the custom WebSocket stream adapter. ```typescript const conn = new acp.ClientSideConnection(_agent => new MyClient(), wsAcpStream(ws, handleControl)); ``` -------------------------------- ### Define Projects Table Schema Source: https://github.com/andresmarpz/sandcastle/blob/main/docs/db.md Schema definition for the projects table, including indexes for active project lookups and path uniqueness. ```sql CREATE TABLE projects ( id TEXT PRIMARY KEY, -- ProjectId (uuid, branded) name TEXT NOT NULL, root_path TEXT NOT NULL, -- absolute, canonical, immutable is_git INTEGER NOT NULL, -- bool: probed at create created_at TEXT NOT NULL, -- ISO-8601 updated_at TEXT NOT NULL, deleted_at TEXT ); CREATE INDEX projects_active ON projects(deleted_at) WHERE deleted_at IS NULL; CREATE UNIQUE INDEX projects_active_root_path ON projects(root_path) WHERE deleted_at IS NULL; ``` -------------------------------- ### Resolve binary path in TypeScript Source: https://github.com/andresmarpz/sandcastle/blob/main/apps/desktop/resources/bin/README.md Logic used to locate the abduco binary based on the current environment (packaged vs development). ```typescript join(process.resourcesPath, "bin", "abduco--") ``` ```typescript join(app.getAppPath(), "resources", "bin", "abduco--") ```