{"id":51412971,"url":"https://github.com/mateffy/sandkasten","last_synced_at":"2026-07-04T16:02:18.477Z","repository":{"id":360317653,"uuid":"1249612603","full_name":"mateffy/sandkasten","owner":"mateffy","description":"Sandkasten wraps multiple sandbox providers like Daytona, Fly.io Sprites or simple SSH boxes into a unified API for provisioning machines, preparing environments and executing code in a secure, isolated environment.","archived":false,"fork":false,"pushed_at":"2026-05-25T22:12:18.000Z","size":7,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-26T00:25:04.430Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mateffy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-25T22:11:46.000Z","updated_at":"2026-05-25T22:12:21.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mateffy/sandkasten","commit_stats":null,"previous_names":["mateffy/sandkasten"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/mateffy/sandkasten","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mateffy%2Fsandkasten","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mateffy%2Fsandkasten/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mateffy%2Fsandkasten/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mateffy%2Fsandkasten/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mateffy","download_url":"https://codeload.github.com/mateffy/sandkasten/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mateffy%2Fsandkasten/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35127443,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-04T02:00:05.987Z","response_time":113,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-07-04T16:02:17.963Z","updated_at":"2026-07-04T16:02:18.471Z","avatar_url":"https://github.com/mateffy.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sandkasten – Advanced sandbox SDK for AI coding agent environments\n\nSandkasten wraps multiple sandbox providers like Daytona, Fly.io Sprites or simple SSH boxes into a unified API for provisioning machines, preparing environments and executing code in a secure, isolated environment.\n\n\u003e [!WARNING]\n\u003e This project is currently in private alpha and not available for public use yet (which is why its currently only re-exporting a private @grips package). The full implementation and source will be available here soon. If you're interested in early access or contributing, please reach out!\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Packages](#packages)\n  - [`@grips/format`](#gripsformat)\n  - [`@grips/harness`](#gripsharness)\n  - [`@grips/sdk`](#gripssdk)\n  - [`@grips/cli`](#gripscli)\n  - [`@grips/react`](#gripsreact)\n  - [`website`](#website)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n  - [CLI](#cli)\n  - [SDK](#sdk)\n  - [React Components](#react-components)\n- [Architecture](#architecture)\n- [Development](#development)\n- [Contributing](#contributing)\n\n---\n\n## Overview\n\nModern AI coding agents all store session history differently. GRIPS unifies them under one schema and one API:\n\n- **One schema** for nodes (messages, tool calls, compactions, checkpoints, config changes, and custom events)\n- **One tree model** for branching, forking, and navigating session history\n- **One streaming format** for real-time output (text, reasoning, tool calls, tool execution stdout/stderr)\n- **One SDK** to list, read, create, fork, send messages to, and listen to sessions across every supported harness\n- **One CLI** to inspect and manage sessions from the terminal\n- **One React library** to render session trees, tables, paths, and streaming UIs\n\n---\n\n## Packages\n\n### Package Dependency Graph\n\n```\ncli ──\u003e sdk ──\u003e harness ──\u003e format\n                 react ───\u003e format\n```\n\n| Package | Purpose | Tech |\n|---------|---------|------|\n| `@grips/format` | Pure types and schemas — zero I/O | Effect-TS Schema |\n| `@grips/harness` | Per-agent adapters (Pi, OpenCode, Claude Code, Codex, Cursor) | Effect-TS |\n| `@grips/sdk` | Unified consumer API via Effect `Layer` and `Service` | Effect-TS |\n| `@grips/cli` | Terminal interface and TUI | Effect-TS + citty |\n| `@grips/react` | React components and hooks for rendering sessions | React + Tailwind |\n| `website` | Marketing and documentation site | Vite + React |\n\n---\n\n### `@grips/format`\n\n\u003e Pure types and schemas. No I/O, no Effect runtime.\n\nDefines the canonical data model used by every other package.\n\n**Core types:**\n\n- `Node` — Discriminated union of all node types:\n  - `message` — User / assistant / system messages with usage metadata\n  - `tool_execution` — Tool call with arguments, output, and exit code\n  - `compaction` — Session compaction / summarization event\n  - `branch_summary` — Summary of a branched conversation path\n  - `checkpoint` — Code snapshot checkpoint\n  - `config_change` — Agent configuration change\n  - `custom` — Extensible custom event type\n- `Session` — Session metadata (id, title, cwd, source harness, timestamps)\n- `SessionListItem` — Lightweight session list entry\n- `Tree` — Built from an array of nodes; provides `getPath`, `getChildren`, `findLeaves`, `validate`\n- `StreamEvent` — Real-time streaming events (`text.start`, `text`, `tool_call.delta`, `tool_execution.stdout`, `done`, `error`, etc.)\n\n**Validation:**\n\n```ts\nimport { buildTree } from \"@grips/format\";\n\nconst tree = buildTree(nodes);\nconst result = tree.validate(); // { valid, brokenReferences, orphanedNodes, cycles }\n```\n\n---\n\n### `@grips/harness`\n\n\u003e Per-harness adapters that bridge native agent storage and runtime APIs into the GRIPS format.\n\nEach supported agent has two implementations:\n\n- **SessionSDK** — Read/write session storage (`list`, `get`, `create`, `append`, `delete`, `fork`)\n- **RuntimeSDK** — Drive a live agent (`send`, `listen`, `stop`)\n\n**Supported harnesses:**\n\n| Harness | SessionSDK | RuntimeSDK |\n|---------|------------|------------|\n| Pi (`pi`) | `PiSessionSDK` | `PiRuntimeSDK` |\n| OpenCode (`opencode`) | `OpenCodeSessionSDK` | `OpenCodeRuntimeSDK` |\n| Claude Code (`claude-code`) | `ClaudeCodeSessionSDK` | `ClaudeCodeRuntimeSDK` |\n| Codex (`codex`) | `CodexSessionSDK` | `CodexRuntimeSDK` |\n| Cursor (`cursor`) | `CursorSessionSDK` | `CursorRuntimeSDK` |\n\n**ACP Harness base class:**\n\nAgents that expose a JSON-RPC stdio interface can subclass `AcpHarness` in `runtime-sdk/acp.ts` to inherit standardized process handling.\n\n**Error types:**\n\n- `SessionSDKError` / `RuntimeSDKError` — Tagged errors from adapter operations\n- `AdapterError` — Harness not found or misconfigured\n\n---\n\n### `@grips/sdk`\n\n\u003e Unified consumer API. All harnesses, one interface.\n\nThe `GripsClient` is an Effect `Service` that aggregates every registered harness and routes calls to the right adapter automatically.\n\n**Registry services:**\n\n- `SessionSDKRegistry` — Holds `SessionSDK` instances\n- `RuntimeSDKRegistry` — Holds `RuntimeSDK` instances\n\n**Client API:**\n\n```ts\nimport { GripsClient } from \"@grips/sdk\";\n\n// List sessions across all harnesses\nyield* GripsClient.list({ cwd: \"/project\", harness: \"opencode\", limit: 10 });\n\n// Get a session and its tree\nconst { session, tree } = yield* GripsClient.get(\"opencode:session-123\");\n\n// Create a new session\nyield* GripsClient.create({ harness: \"claude-code\", title: \"Fix login bug\" });\n\n// Send a message and receive a stream of events\nconst stream = yield* GripsClient.send(\"pi:session-456\", { content: \"Refactor auth.ts\" });\n\n// Listen to an already-running session\nconst liveStream = yield* GripsClient.listen(\"codex:session-789\");\n\n// Fork a session from a specific node\nyield* GripsClient.fork(\"cursor:session-abc\", \"node-xyz\", { title: \"Exploration branch\" });\n\n// Stop a running session\nyield* GripsClient.stop(\"opencode:session-123\");\n```\n\nSession IDs are namespaced: `{harness}:{nativeId}`. The client parses the harness prefix and routes to the correct adapter.\n\n---\n\n### `@grips/cli`\n\n\u003e Terminal interface and interactive TUI for managing sessions.\n\n**Commands:**\n\n```bash\n# Interactive TUI (default when no args provided)\ngrips\n\n# List sessions\ngrips sessions list [--cwd DIR] [--harness NAME] [--json]\n\n# Show a session\ngrips sessions get \u003cid\u003e [--json | --tree | --nodes] [--thinking]\n\n# Create a session\ngrips sessions create [--harness NAME] [--cwd DIR] [--title TITLE] [--json]\n\n# Delete a session\ngrips sessions delete \u003cid\u003e\n\n# Fork a session from a node\ngrips sessions fork \u003cid\u003e \u003cnodeId\u003e [--title TITLE] [--json]\n\n# Send a message to a running session\ngrips send \u003cid\u003e --content \"MESSAGE\"\n\n# Listen to a running session\ngrips listen \u003cid\u003e\n\n# Stop a running session\ngrips stop \u003cid\u003e\n```\n\n**Auto-detection:** The CLI automatically detects installed agents by checking for their executables (`opencode`, `claude`, `codex`, `cursor-agent`) or home-directory markers (`~/.pi`) and wires only the available harnesses into the Effect dependency layer.\n\n---\n\n### `@grips/react`\n\n\u003e React components, hooks, and headless primitives for rendering GRIPS sessions.\n\n**Exports:**\n\n- `/` — Components + hooks + mock data\n- `/components` — Styled UI components\n- `/headless` — Unstyled, stateful primitives\n- `/hooks` — Reusable state and stream logic\n\n**Components:**\n\n| Component | Description |\n|-----------|-------------|\n| `SessionTree` | Static hierarchical tree of nodes with type legend |\n| `InteractiveSessionTree` | Expandable / collapsible interactive tree |\n| `SessionTable` | Tabular session list with sorting and filtering |\n| `SessionHeader` | Session metadata header (title, harness, timestamps) |\n| `SessionPath` | Breadcrumb path from root to selected node |\n| `NodeDetail` | Detail view for any node type with type-specific rendering |\n| `SessionView` | Composite view combining tree, path, and detail |\n\n**Hooks:**\n\n| Hook | Purpose |\n|------|---------|\n| `useTree` | Tree state (expand/collapse, select, paths, visibility) |\n| `useStreamEvents` | Collect stream events into state |\n| `useStreamContent` | Accumulate streaming text / reasoning content |\n| `useStreamingNodes` | Merge committed nodes with live draft nodes from a stream |\n| `useSelection` | Single / multi selection state |\n| `usePath` | Compute ancestor path for a node ID |\n\n**Headless primitives:**\n\n| Primitive | Purpose |\n|-----------|---------|\n| `Tree` | Render-agnostic tree component (pass your own node renderer) |\n| `SessionList` | Render-agnostic list component |\n\n**Peer dependencies:** `react \u003e=19`, `react-dom \u003e=19`, `tailwindcss \u003e=4`\n\n---\n\n### `website`\n\n\u003e Vite + React marketing and documentation site.\n\nUses TanStack Router and Tailwind CSS. Built with `vite build` and previewed with `vite preview`.\n\n---\n\n## Installation\n\nThis repository uses **Bun** workspaces.\n\n```bash\n# Install dependencies\nbun install\n\n# Type-check all packages\nbun run typecheck\n\n# Run tests\nbun test\n```\n\n### Using packages in your own project\n\n```bash\n# Install the SDK\nbun add @grips/sdk\n\n# Install React components\nbun add @grips/react\n```\n\n---\n\n## Quick Start\n\n### CLI\n\n```bash\n# Start the interactive TUI\ncd /your/project \u0026\u0026 grips\n\n# List all sessions across all agents\ngrips sessions list\n\n# View a session transcript\ngrips sessions get opencode:abc123\n\n# View the branching tree\ngrips sessions get opencode:abc123 --tree\n\n# Create a session\ngrips sessions create --harness claude-code --title \"Bugfix\"\n\n# Send a message\ngrips send claude-code:xyz789 --content \"Fix the race condition in queue.ts\"\n```\n\n### SDK\n\n```ts\nimport { Effect } from \"effect\";\nimport { GripsClient, SessionSDKRegistry, RuntimeSDKRegistry } from \"@grips/sdk\";\nimport { OpenCodeSessionSDK, OpenCodeRuntimeSDK } from \"@grips/sdk\";\n\nconst layer = Layer.mergeAll(\n  Layer.succeed(SessionSDKRegistry, { sdks: [new OpenCodeSessionSDK()] }),\n  Layer.succeed(RuntimeSDKRegistry, { sdks: [new OpenCodeRuntimeSDK()] })\n).pipe(Layer.provide(GripsClient.Default));\n\nconst program = Effect.gen(function* () {\n  const sessions = yield* GripsClient.list({ limit: 5 });\n  const { session, tree } = yield* GripsClient.get(sessions[0].id);\n  const stream = yield* GripsClient.send(session.id, { content: \"Hello\" });\n\n  yield* Stream.runForEach(stream, (event) =\u003e\n    Effect.sync(() =\u003e console.log(event.type))\n  );\n});\n\nEffect.runPromise(Effect.provide(program, layer));\n```\n\n### React Components\n\n```tsx\nimport { SessionTree, SessionView, useStreamingNodes } from \"@grips/react\";\nimport { mockNodes } from \"@grips/react/data/mock\";\n\n// Static tree\n\u003cSessionTree nodes={mockNodes} /\u003e\n\n// Full interactive view with streaming\nfunction MyApp({ nodes, streamEvents }) {\n  const { nodes: liveNodes, draftNodeIds } = useStreamingNodes(nodes, streamEvents);\n  return \u003cSessionView nodes={liveNodes} autoExpandIds={draftNodeIds} /\u003e;\n}\n```\n\n---\n\n## Architecture\n\n### The Streaming Draft Node Lifecycle\n\nWhen driving a running agent through `RuntimeSDK`, the event stream merges live output into the tree:\n\n1. **Stream Intake** — `useStreamingNodes` merges committed nodes with incoming `StreamEvent`s.\n2. **Draft Node Initialization** — When an active event starts (e.g., `text.start`), a transient \"draft node\" is created with `_draft: true`.\n3. **Delta Merging** — Incremental events continuously merge into the draft node's payload.\n4. **Live Auto-Expansion** — Draft node IDs are returned as `draftNodeIds` and mapped to `autoExpandIds` in the `Tree` component, keeping the active streaming turn visible.\n5. **Commitment** — On completion events (`text.done`, `tool_execution.end`, `done`), the draft node closes. If the runtime appends it to storage, it becomes a permanent committed node.\n\n### Node Format\n\nEvery node shares a common shape:\n\n```ts\ninterface Node {\n  id: string;\n  parentId: string | null;\n  timestamp: number;\n  type: \"message\" | \"tool_execution\" | \"compaction\" | \"branch_summary\" | \"checkpoint\" | \"config_change\" | \"custom\";\n  version?: number;\n  payload: /* type-specific */;\n  metadata: Record\u003cstring, unknown\u003e;\n}\n```\n\nThe `parentId` enables a full directed acyclic graph of session history, supporting branching, forking, and non-linear exploration.\n\n---\n\n## Development\n\n### Scripts\n\n| Script | Description |\n|--------|-------------|\n| `bun run dev` | Start dev servers (Turbo) |\n| `bun run typecheck` | TypeScript check all packages |\n| `bun test` | Run all tests |\n| `bun run fmt` | Format with oxfmt |\n| `bun run lint` | Lint with oxlint |\n| `bun run grips` | Run the CLI locally |\n\n### Testing Conventions\n\n- **Colocation required** — Test files (`*.test.ts`, `*.test.tsx`) and Storybook stories (`*.stories.tsx`) must live in the same directory as the source file they cover. No `__tests__/` or `stories/` directories.\n- Unit tests: `vitest run` in `packages/react/` and `packages/website/`; `bun test` elsewhere.\n- Storybook `play()` tests are real tests and must pass.\n\n### Adding a New Harness Adapter\n\nSee [`AGENTS.md`](./AGENTS.md) for the full workflow. The short version:\n\n1. Implement `SessionSDK` in `packages/harness/src/session-sdk/my-agent.ts`\n2. Implement `RuntimeSDK` in `packages/harness/src/runtime-sdk/my-agent.ts` (or subclass `AcpHarness`)\n3. Export both from `packages/harness/src/index.ts`\n4. Register in `packages/sdk/src/registry.ts`\n5. Add auto-detection in `packages/cli/src/layer.ts`\n6. Add colocated tests\n\n### Adding a New Node Type\n\nSee [`AGENTS.md`](./AGENTS.md) for the full workflow. The short version:\n\n1. Define the payload schema in `packages/format/src/node.ts`\n2. Add streaming events to `packages/format/src/stream.ts` if applicable\n3. Add colors, background, and symbol in `packages/react/src/lib/node-rendering.tsx`\n4. Create a detail component in `packages/react/src/components/NodeDetail/details/`\n5. Wire it into `NodeDetail.tsx`\n6. Add tests and stories\n\n---\n\n## Contributing\n\n1. Follow the existing code style (oxfmt, oxlint).\n2. Keep tests and stories colocated with source files.\n3. Ensure all tests pass before submitting changes.\n4. Make minimal changes — prefer small, focused PRs.\n\n---\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmateffy%2Fsandkasten","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmateffy%2Fsandkasten","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmateffy%2Fsandkasten/lists"}