Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ pnpm install
pnpm dev
```

## Native module runtimes

`better-sqlite3` is used from both Node-based tests and the Electron app. Rebuild it for the runtime you are about to use:

```bash
pnpm run rebuild:native:node # Node / vitest / core tests
pnpm run rebuild:native:electron # Electron app / Playwright e2e
```

If you hit a `NODE_MODULE_VERSION` mismatch, rerun the matching rebuild command and try again.

## Project structure

```
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The missing search engine for your own data.
<img src="docs/spool-v0.png" alt="Spool" width="720">
</p>

Search your Claude Code sessions, Codex CLI history, GitHub stars, Twitter bookmarks, and YouTube likes — locally, instantly.
Search your Claude Code sessions, Codex CLI history, Gemini CLI chats, GitHub stars, Twitter bookmarks, and YouTube likes — locally, instantly.

> **Early stage.** Spool is under active development — expect rough edges. Feedback, bug reports, and ideas are very welcome via [Issues](https://github.com/spool-lab/spool/issues) or [Discord](https://discord.gg/aqeDxQUs5E).

Expand All @@ -28,7 +28,7 @@ pnpm build

Spool indexes your AI conversations and bookmarks into a single local search box.

- **AI sessions** — watches Claude/Codex session dirs in real time, including profile-based paths like `~/.claude-profiles/*/projects` and `~/.codex-profiles/*/sessions`
- **AI sessions** — watches Claude/Codex/Gemini session dirs in real time, including profile-based paths like `~/.claude-profiles/*/projects`, `~/.codex-profiles/*/sessions`, and Gemini’s project temp dirs under `~/.gemini/tmp/*/chats`
- **Bookmarks & stars** — pulls from 50+ platforms via [OpenCLI](https://github.com/jackwener/opencli)
- **URL capture** — save any URL with `Cmd+K`
- **Agent search** — a `/spool` skill inside Claude Code feeds matching fragments back into your conversation
Expand Down Expand Up @@ -56,6 +56,13 @@ pnpm test # runs all tests

> **Note:** The `electron-rebuild` step is required whenever you run `pnpm install` or switch Node.js versions. Without it, the Electron app will crash at launch with a `NODE_MODULE_VERSION` mismatch error from `better-sqlite3`.

If you switch between **Node-side tests** and **Electron app/e2e runs**, rebuild `better-sqlite3` for the matching runtime:

```bash
pnpm run rebuild:native:node # before @spool/core / Node-based tests
pnpm run rebuild:native:electron # before launching the Electron app or e2e tests
```

## Release

```bash
Expand Down
6 changes: 3 additions & 3 deletions docs/spool-positioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,23 @@

> **The missing search engine for your own data.**

Search your `[Claude Code sessions · ChatGPT history · GitHub stars · Twitter bookmarks · YouTube likes]` — locally.
Search your `[Claude Code sessions · Codex history · Gemini chats · ChatGPT history · GitHub stars · Twitter bookmarks · YouTube likes]` — locally.

---

## Landing Page

### Your coding agent is already the best search engine you have.

Spool lets Claude Code, Codex, and any coding agent search your personal data — past sessions, bookmarks, stars, saves — from a single search box.
Spool lets Claude Code, Codex, Gemini CLI, and any coding agent search your personal data — past sessions, bookmarks, stars, saves — from a single search box.

### 50+ platforms, pulled to your machine.

OpenCLI pulls your bookmarks, stars, and saves from Twitter, GitHub, Reddit, YouTube, and more — no API keys, no tokens. Spool indexes them all locally.

### Every agent session, indexed automatically.

Spool watches `~/.claude/` and `~/.codex/` in real time. Every conversation you have with Claude Code or Codex — searchable the moment it's written.
Spool watches `~/.claude/`, `~/.codex/`, and Gemini CLI’s `~/.gemini/tmp/*/chats` in real time. Every conversation you have with Claude Code, Codex, or Gemini CLI — searchable the moment it's written.

### Context that flows back in.

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
"build": "turbo build",
"dev": "turbo dev",
"test": "turbo test",
"test:core": "pnpm --filter @spool/core test",
"test:e2e": "pnpm --filter @spool/app test:e2e",
"rebuild:native:node": "pnpm --filter @spool/core run rebuild:native:node",
"rebuild:native:electron": "pnpm --filter @spool/app run rebuild:native:electron",
"lint": "turbo lint",
"clean": "turbo clean"
},
Expand Down
36 changes: 35 additions & 1 deletion packages/app/e2e/fast-search.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ test('home view shows title and session counts after sync', async () => {
await expect(window.locator('h1')).toContainText('Spool')
await waitForSync(window)
await expect(window.locator('text=Claude Chats')).toBeVisible()
await expect(window.locator('text=Gemini Chats')).toBeVisible()
})

test('search finds fixture content by canary keyword', async () => {
Expand Down Expand Up @@ -72,11 +73,44 @@ test('codex search results expose resume-related actions', async () => {

const row = window.locator('[data-testid="fragment-row"]').first()
await expect(row).toBeVisible({ timeout: 5000 })
await expect(row.locator('text=codex')).toBeVisible()
await expect(row.locator('[data-testid="source-badge"][data-source="codex"]')).toBeVisible()

const copyCommand = row.getByRole('button', { name: 'Copy Command' })
await expect(copyCommand).toBeVisible()

const resumeInCli = row.getByRole('button', { name: 'Resume in CLI' })
await expect(resumeInCli).toBeVisible()
})

test('gemini search results expose resume-related actions', async () => {
const { window } = ctx

await search(window, 'OBOE_CANARY_55')

const row = window.locator('[data-testid="fragment-row"]').first()
await expect(row).toBeVisible({ timeout: 5000 })
await expect(row.locator('[data-testid="source-badge"][data-source="gemini"]')).toBeVisible()

const copyCommand = row.getByRole('button', { name: 'Copy Command' })
await expect(copyCommand).toBeVisible()

const resumeInCli = row.getByRole('button', { name: 'Resume in CLI' })
await expect(resumeInCli).toBeVisible()
})

test('whitespace-separated terms narrow shared PR number matches', async () => {
const { window } = ctx

await search(window, '4242')

const broadRows = window.locator('[data-testid="fragment-row"]')
await expect(broadRows).toHaveCount(3)
await expect(window.locator('[data-testid="match-count"]').first()).toContainText('2 matches')

await search(window, '查看一下 4242')

const narrowedRows = window.locator('[data-testid="fragment-row"]')
await expect(narrowedRows).toHaveCount(2)
await expect(narrowedRows.first()).toContainText('请直接查看一下 4242 这个变更。')
await expect(narrowedRows.nth(1)).toContainText('可以帮我查看一下这个变更单 4242 的结论吗?')
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type":"user","sessionId":"test-session-uuid-003","cwd":"/tmp/test-project","uuid":"msg-201","timestamp":"2026-01-17T09:00:00Z","message":{"role":"user","content":"可以帮我查看一下这个变更单 4242 的结论吗?"}}
{"type":"assistant","uuid":"msg-202","parentUuid":"msg-201","timestamp":"2026-01-17T09:00:10Z","message":{"role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"我看过变更单 4242 了。主要问题集中在搜索语义和结果排序,需要把 fast search 从连续短语匹配放宽到多关键词匹配。"}]}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type":"user","sessionId":"test-session-uuid-004","cwd":"/tmp/test-project","uuid":"msg-301","timestamp":"2026-01-18T11:00:00Z","message":{"role":"user","content":"顺手总结一下 #4242 改了什么。"}}
{"type":"assistant","uuid":"msg-302","parentUuid":"msg-301","timestamp":"2026-01-18T11:00:08Z","message":{"role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"变更单 4242 主要调整了搜索、状态栏和会话恢复逻辑。"}]}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"type":"user","sessionId":"test-session-uuid-005","cwd":"/tmp/test-project","uuid":"msg-401","timestamp":"2026-01-19T08:30:00Z","message":{"role":"user","content":"请直接查看一下 4242 这个变更。"}}
{"type":"assistant","uuid":"msg-402","parentUuid":"msg-401","timestamp":"2026-01-19T08:30:08Z","message":{"role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"我先按 exact phrase 命中来看,这个会优先排在只包含所有关键词但不连续的结果前面。"}]}}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/tmp/test-gemini-project
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"projects": {
"/tmp/test-gemini-project": "test-gemini-project"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"sessionId": "abcd1234-1111-2222-3333-444455556666",
"projectHash": "test-hash",
"startTime": "2026-01-18T08:20:00Z",
"lastUpdated": "2026-01-18T08:21:12Z",
"messages": [
{
"id": "gem-user-001",
"timestamp": "2026-01-18T08:20:00Z",
"type": "user",
"content": [
{
"text": "Where is OBOE_CANARY_55 validated in the request pipeline?"
}
]
},
{
"id": "gemini-001",
"timestamp": "2026-01-18T08:20:08Z",
"type": "gemini",
"content": "OBOE_CANARY_55 is validated in src/server/pipeline.ts before the middleware chain executes.",
"tokens": {
"input": 100,
"output": 30,
"cached": 0,
"thoughts": 0,
"tool": 0,
"total": 130
},
"model": "gemini-2.5-pro",
"toolCalls": [
{
"id": "read_file_1",
"name": "read_file",
"displayName": "ReadFile",
"status": "success"
}
]
}
],
"kind": "main",
"summary": "Investigate OBOE_CANARY_55 validation flow."
}
5 changes: 5 additions & 0 deletions packages/app/e2e/helpers/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ export async function launchApp(opts: { mockAgent?: 'success' | 'error' } = {}):

const claudeDir = join(tmpDir, 'claude', 'projects')
const codexDir = join(tmpDir, 'codex', 'sessions')
const geminiCliHome = join(tmpDir, 'gemini-cli-home')
cpSync(join(FIXTURES_DIR, 'claude-projects'), claudeDir, { recursive: true })
cpSync(join(FIXTURES_DIR, 'codex-sessions'), codexDir, { recursive: true })
cpSync(join(FIXTURES_DIR, 'gemini-cli-home'), geminiCliHome, { recursive: true })

const env: Record<string, string> = {
...process.env as Record<string, string>,
SPOOL_DATA_DIR: join(tmpDir, 'data'),
SPOOL_ELECTRON_USER_DATA_DIR: join(tmpDir, 'electron-user-data'),
SPOOL_CLAUDE_DIR: claudeDir,
SPOOL_CODEX_DIR: codexDir,
SPOOL_GEMINI_DIR: geminiCliHome,
GEMINI_CLI_HOME: geminiCliHome,
ELECTRON_DISABLE_GPU: '1',
}

Expand Down
15 changes: 9 additions & 6 deletions packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
"productName": "Spool",
"main": "./out/main/index.js",
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build && electron-builder --publish never",
"build:mac": "electron-vite build && electron-builder --mac --arm64",
"build:linux": "electron-vite build && electron-builder --linux",
"build:electron": "electron-vite build",
"rebuild:native": "pnpm run rebuild:native:electron",
"rebuild:native:electron": "electron-rebuild -f -w better-sqlite3",
"build:core": "pnpm --filter @spool/core build",
"dev": "pnpm run build:core && pnpm run rebuild:native && electron-vite dev",
"build": "pnpm run build:core && electron-vite build && electron-builder --publish never",
"build:mac": "pnpm run build:core && electron-vite build && electron-builder --mac --arm64",
"build:linux": "pnpm run build:core && electron-vite build && electron-builder --linux",
"build:electron": "pnpm run build:core && electron-vite build",
"preview": "electron-vite preview",
"clean": "rm -rf out dist-electron",
"test:e2e": "npx playwright test --config e2e/playwright.config.ts"
"test:e2e": "pnpm run build:core && pnpm run rebuild:native:electron && npx playwright test --config e2e/playwright.config.ts"
},
"dependencies": {
"@agentclientprotocol/sdk": "^0.17.1",
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/main/acp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { describe, expect, it } from 'vitest'
import { AcpManager } from './acp.js'

describe('AcpManager builtin agents', () => {
it('includes Gemini CLI as a native ACP agent', () => {
const manager = new AcpManager()
const builtins = manager.getBuiltinAgents()

expect(builtins['gemini']).toEqual({
name: 'Gemini CLI',
bin: 'gemini',
acpMode: 'native',
})
})
})
Loading
Loading