@pocketcoder/host
v0.0.19
Published
PocketCoder host daemon - connects your local machine to PocketCoder cloud
Maintainers
Readme
PocketCoder Daemon
The PocketCoder Daemon is a local Node.js process that acts as a bridge between Claude Code and the mobile PWA client. It manages Claude Code execution, file synchronization, and real-time communication with the cloud relay server.
Table of Contents
- Overview
- Tech Stack
- Installation & Usage
- Architecture
- Startup Sequence
- Authentication
- Relay Connection
- Message Handling
- Features
- Control Plane HTTP API
- Database Schema
- CLI Commands
- Error Handling
- Configuration
- Testing
Overview
flowchart LR
subgraph Internet
Client["Client<br/>(PWA)"]
Relay["Relay<br/>(Fly.io)"]
end
subgraph Local
Daemon["Daemon<br/>(Local)"]
Claude["Claude<br/>Code"]
end
subgraph Cloud
Supabase["Supabase<br/>(Auth/DB)"]
end
Client <-->|WebSocket| Relay
Relay <-->|WebSocket| Daemon
Relay -->|Auth| Supabase
Daemon -->|Spawn| ClaudeKey Responsibilities:
- Claude Code Management: Spawning and managing Claude Code CLI processes
- Filesystem Operations: Reading, writing, and watching project files
- Shell Commands: Executing shell commands with streaming output
- Git Operations: Status, branching, staging, committing, and pushing
- Relay Communication: Maintaining WebSocket connection with the cloud relay
- Local Storage: Persisting projects, command history, and auth tokens in SQLite
Tech Stack
| Component | Library | Purpose |
| -------------------- | ------------------- | ---------------------------- |
| Runtime | Node.js 24 LTS | JavaScript runtime |
| Language | TypeScript (strict) | Type-safe development |
| WebSocket | ws | Relay connection |
| Database | better-sqlite3 | Local persistence |
| File Watcher | chokidar | Filesystem change detection |
| CLI Framework | commander | Command-line interface |
| Process Spawning | execa | Claude Code & shell commands |
| HTTP Server | fastify | Control plane API |
| Testing | vitest | Unit and integration tests |
| Build | tsc | TypeScript compilation |
Installation & Usage
Prerequisites
- Node.js 24 LTS or later
- Claude Code CLI installed and configured
- Git (for git operations)
Installation
# Install globally
npm install -g pocketcoder
# Or run without install
npx pocketcoder startBasic Usage
# Login with GitHub (opens browser)
pocketcoder login
# Start the daemon (runs in background)
pocketcoder start
# Start in foreground (for debugging)
pocketcoder start --foreground
# Register a project
pocketcoder add /path/to/project
pocketcoder add . # Current directory
# List registered projects
pocketcoder projects
# Check daemon status
pocketcoder status
# View logs
pocketcoder logs --tail 100 -f
# Stop the daemon
pocketcoder stop
# Remove a project
pocketcoder remove <projectId>
# Set machine label
pocketcoder machine label "Work Laptop"Architecture
graph TB
subgraph "Local Machine"
CLI["CLI Commands<br/>(pocketcoder)"]
CP["Control Plane HTTP<br/>:17333"]
Daemon["Daemon Core"]
subgraph "Services"
CR["Claude Runner"]
CMD["Command Runner"]
FW["File Watcher"]
GIT["Git Service"]
MQ["Message Queue"]
end
subgraph "Data Layer"
DB["SQLite Database<br/>(daemon.db)"]
AUTH["Auth Storage<br/>(auth.json)"]
end
CLI -->|HTTP| CP
CP -->|Control| Daemon
Daemon -->|Spawn| CR
Daemon -->|Execute| CMD
Daemon -->|Watch| FW
Daemon -->|Exec| GIT
Daemon -->|Queue| MQ
CR -->|Stream Events| MQ
Daemon -->|Read/Write| DB
Daemon -->|Load/Save| AUTH
end
subgraph "Cloud"
Relay["Relay Server<br/>WebSocket"]
end
Daemon <-->|WebSocket| RelayDirectory Structure
apps/host/
├── src/
│ ├── index.ts # Entry point
│ ├── daemon.ts # Main startup orchestration
│ ├── cli/ # CLI command handlers
│ │ ├── index.ts
│ │ ├── start.ts
│ │ ├── stop.ts
│ │ ├── add.ts
│ │ └── ...
│ ├── relay/ # Relay communication
│ │ ├── websocket-client.ts
│ │ ├── message-router.ts
│ │ ├── relay-request.ts
│ │ └── messages/
│ │ ├── daemon-register.ts
│ │ └── ...
│ ├── handlers/ # Message handlers
│ │ ├── fs-list.ts
│ │ ├── fs-read.ts
│ │ ├── fs-write.ts
│ │ ├── git-status.ts
│ │ ├── command-exec.ts
│ │ ├── agent-session-new.ts
│ │ └── ...
│ ├── services/ # Core services
│ │ ├── claude-runner.ts
│ │ ├── command-runner.ts
│ │ ├── file-watcher.ts
│ │ ├── git.ts
│ │ ├── message-queue.ts
│ │ ├── stream-json-parser.ts
│ │ └── shutdown.ts
│ ├── auth/ # Authentication
│ │ ├── login.ts
│ │ ├── relay-auth.ts
│ │ ├── token-storage.ts
│ │ └── token-refresh.ts
│ ├── db/ # Database layer
│ │ ├── database.ts
│ │ └── repositories/
│ │ ├── projects.ts
│ │ ├── command-runs.ts
│ │ └── fs-state.ts
│ ├── control-plane/ # HTTP control plane
│ │ ├── server.ts
│ │ └── routes/
│ └── utils/ # Utilities
│ ├── logger.ts
│ ├── paths.ts
│ ├── machine-info.ts
│ └── config.ts
└── tests/ # Test filesStartup Sequence
The daemon startup follows a 3-phase process:
sequenceDiagram
participant CLI as CLI
participant Daemon as Daemon
participant DB as SQLite
participant Auth as Auth
participant Relay as Relay
Note over Daemon: Phase 1 - Local Init
Daemon->>Daemon: Create config dirs
Daemon->>DB: Open/Migrate database
Daemon->>Daemon: Clean old command runs
Daemon->>Daemon: Start Control Plane HTTP :17333
Daemon->>Daemon: Write PID file
Note over Daemon: Phase 2 - Authentication
Daemon->>Auth: Load existing tokens
alt Has valid tokens
Daemon->>Relay: token_refresh (if expired)
Relay-->>Daemon: New tokens
else No tokens / invalid
Daemon->>Daemon: Start OAuth server
Daemon->>Daemon: Open browser
Note over Daemon: User completes OAuth
Daemon->>Relay: auth_exchange {code}
Relay-->>Daemon: tokens + user info
end
Daemon->>Auth: Save tokens
Note over Daemon: Phase 3 - Relay Connection
Daemon->>Daemon: Setup message router
Daemon->>Relay: Connect WebSocket
Relay-->>Daemon: connected
Daemon->>Relay: daemon_register
Daemon->>Daemon: Start file watchers
Daemon->>Daemon: Register signal handlers
Note over Daemon: Ready ✓Shutdown Sequence
sequenceDiagram
participant Signal as SIGTERM/SIGINT
participant Daemon as Daemon
participant Services as Services
participant DB as Database
Signal->>Daemon: Shutdown signal
Daemon->>Daemon: Stop accepting requests
Note over Daemon,Services: LIFO cleanup order
Daemon->>Services: Stop file watchers
Daemon->>Services: Clear message queues
Daemon->>Services: Kill Claude processes
Daemon->>Services: Kill running commands
Daemon->>Services: Close relay connection
Daemon->>DB: Close database
Daemon->>Daemon: Remove PID file
Daemon->>Signal: Exit 0Authentication
The daemon uses GitHub OAuth via Supabase Auth. Important: The daemon does NOT interact with Supabase directly - all token exchange goes through the Relay.
sequenceDiagram
participant User as User
participant Daemon as Daemon
participant OAuth as OAuth Server
participant Supabase as Supabase
participant Relay as Relay
User->>Daemon: pocketcoder login
Daemon->>Daemon: Start local callback server :random
Daemon->>OAuth: Open browser to auth URL
User->>OAuth: Sign in with GitHub
OAuth->>Daemon: Callback with code
Daemon->>Relay: auth_exchange { code }
Relay->>Supabase: Exchange code for tokens
Supabase-->>Relay: access_token, refresh_token
Relay-->>Daemon: auth_exchange_response
Daemon->>Daemon: Generate machineKey (UUID v4)
Daemon->>Daemon: Save tokens to auth.json
Daemon-->>User: Login completeToken Storage
Tokens are stored with restrictive permissions (0600):
| OS | Path |
| ------- | ----------------------------------------------------- |
| macOS | ~/Library/Application Support/pocketcoder/auth.json |
| Linux | ~/.config/pocketcoder/auth.json |
| Windows | %APPDATA%\pocketcoder\auth.json |
{
"machine_key": "550e8400-e29b-41d4-a716-446655440000",
"access_token": "eyJ...",
"refresh_token": "...",
"expires_at": 1707666000,
"user_id": "github|12345"
}Automatic Token Refresh
sequenceDiagram
participant Daemon as Daemon
participant Relay as Relay
participant Supabase as Supabase
Note over Daemon: Token expired
Daemon->>Relay: token_refresh {refresh_token}
Relay->>Supabase: POST /auth/v1/token
Supabase-->>Relay: New tokens
Relay-->>Daemon: token_refresh_response
Daemon->>Daemon: Update auth.jsonRelay Connection
Connection State Machine
stateDiagram-v2
[*] --> disconnected
disconnected --> connecting: connect()
connecting --> connected: WebSocket opens
connecting --> disconnected: Error/Close
connected --> connected: Heartbeat ping/pong
connected --> reconnecting: Unexpected disconnect
reconnecting --> connected: Reconnect success
reconnecting --> reconnecting: Retry with backoff
reconnecting --> disconnected: Manual disconnect()
connected --> disconnected: Manual disconnect()
disconnected --> [*]Reconnection Strategy
- Exponential backoff: 1s, 2s, 4s, 8s... maximum 60s
- On reconnect: Re-sends
daemon_registerwith full state - File watchers: Continue running (local state preserved)
- Message queue: Maintained, processed on reconnect
Daemon Registration
sequenceDiagram
participant Daemon as Daemon
participant Relay as Relay
participant Client as Client
Daemon->>Relay: Connect WebSocket
Daemon->>Relay: daemon_register
Note right of Daemon: machineKey, token,<br/>machine info, projects
Note over Relay: Validate JWT<br/>Store in map
Relay--)Client: daemon_status {connected: true}
Relay-->>Daemon: ReadyMessage Handling
Message Flow
graph TB
subgraph "Relay Server"
RRelay["Relay WebSocket"]
end
subgraph "Daemon"
WS["WebSocket Client"]
Router["Message Router"]
subgraph "Handlers"
H1["fs_* Handlers"]
H2["git_* Handlers"]
H3["command_exec"]
H4["agent_* Handlers"]
end
subgraph "Services"
CR["Claude Runner"]
CMD["Command Runner"]
FW["File Watcher"]
end
end
RRelay -->|Message| WS
WS -->|message event| Router
Router -->|Dispatch| H1
Router -->|Dispatch| H2
Router -->|Dispatch| H3
Router -->|Dispatch| H4
H3 -->|Execute| CMD
H4 -->|Spawn| CR
CR -->|Events| Router
CMD -->|Output| Router
FW -->|fs_changed| WS
Router -->|send| WS
WS -->|JSON| RRelayHandler Pattern
All message handlers follow this signature:
export async function handleMessageType(
message: RequestMessage,
send: (response: Message) => void
): Promise<void> {
// 1. Extract and validate payload
// 2. Query database / filesystem
// 3. Perform operation
// 4. Send response via send()
// 5. Stream events (if applicable)
}Supported Message Types
| Category | Message Type | Description |
| -------------- | ----------------------- | ------------------------ |
| Filesystem | fs_list | List directory contents |
| | fs_read | Read file content |
| | fs_write | Write file content |
| Git | git_status | Get git status |
| | git_branches | List branches |
| | git_branch_create | Create new branch |
| | git_checkout | Checkout branch/ref |
| | git_stage | Stage files |
| | git_unstage | Unstage files |
| | git_commit | Create commit |
| | git_push | Push to remote |
| | git_file_base | Get file at HEAD |
| Commands | command_exec | Execute shell command |
| | command_cancel | Cancel running command |
| Agent | agent_session_new | Start new Claude session |
| | agent_message | Send message to session |
| | agent_sessions_list | List existing sessions |
| | agent_cancel | Cancel running session |
| | agent_prompt_response | Respond to Claude prompt |
| System | machines_list | List user's machines |
| | projects_list | List projects |
Features
Filesystem Operations
sequenceDiagram
participant Client as Client
participant Relay as Relay
participant Daemon as Daemon
Note over Client,Daemon: READ OPERATIONS
Client->>Relay: fs_list {path: "src"}
Relay->>Daemon: forward
Note over Daemon: Read directory
Daemon-->>Relay: fs_list_response
Relay-->>Client: {children: [...]}
Client->>Relay: fs_read {path: "src/app.ts"}
Relay->>Daemon: forward
Daemon-->>Relay: fs_read_response
Relay-->>Client: {content: "..."}
Note over Client,Daemon: WRITE OPERATIONS
Client->>Relay: fs_write {path, content}
Relay->>Daemon: forward
Note over Daemon: Write to disk
Daemon-->>Relay: fs_write_response
Relay-->>Client: {success: true}Lazy Tree Loading
The filesystem is loaded lazily for performance:
- Root:
fs_list(path="")returns only root level - Expand folder:
fs_list(path="src")loads that directory - Prevents sending large
node_modulesdirectories
Limits
| Limit | Value | | --------------------------- | --------- | | Maximum file read size | 10 MB | | Maximum file write size | 1 MB | | Maximum tree depth | 20 levels | | Maximum entries per listing | 10,000 |
File Watcher
The daemon uses chokidar to monitor filesystem changes in real-time.
graph TB
subgraph "Chokidar Watch"
WATCH["watch(projectPath)"]
IGNORE["Ignored Patterns:<br/>node_modules, .git,<br/>dist, build, .next"]
end
subgraph "Change Detection"
ADD["add/addDir → create"]
MOD["change → modify"]
DEL["unlink/unlinkDir → delete"]
end
subgraph "Accumulation"
PEND["Pending Changes"]
DEBOUNCE["Debounce Timer<br/>(250ms)"]
end
subgraph "Event Generation"
SEQ["Increment sequence"]
TRUNC["Truncate if > 500"]
EVENT["fs_changed event"]
end
WATCH -->|Event| ADD
WATCH -->|Event| MOD
WATCH -->|Event| DEL
ADD --> PEND
MOD --> PEND
DEL --> PEND
PEND -->|Schedule| DEBOUNCE
DEBOUNCE -->|Fire| SEQ
SEQ --> TRUNC
TRUNC --> EVENT
IGNORE -.->|Filters| WATCHEvent Message
{
type: 'fs_changed',
sessionId: 'proj_abc',
payload: {
seq: 43, // Incremental sequence
changes: [
{ kind: 'create', path: 'src/new.ts' },
{ kind: 'modify', path: 'src/app.ts' },
{ kind: 'delete', path: 'src/old.ts' }
],
changesTruncated?: boolean // true if >500 changes
}
}Synchronization
- Each change increments
seqglobally per project - Client tracks last
seqreceived - On seq gap: client triggers full
fs_listrefresh
Command Runner
Non-interactive shell command execution with streaming output.
sequenceDiagram
participant Client as Client
participant Relay as Relay
participant Daemon as Daemon
Client->>Relay: command_exec {command: "npm test"}
Relay->>Daemon: forward
Note over Daemon: Spawn process
loop Streaming output
Daemon--)Relay: command_output {stdoutChunk}
Relay--)Client: forward
Daemon--)Relay: command_output {stderrChunk}
Relay--)Client: forward
end
Daemon-->>Relay: command_exit {exitCode: 0}
Relay-->>Client: forward
Note over Client,Daemon: CANCELLATION
Client->>Relay: command_cancel {requestId}
Relay->>Daemon: forward
Note over Daemon: kill (SIGTERM)
Daemon-->>Relay: command_exit {exitCode: null, signal: "SIGTERM"}
Relay-->>Client: forwardDesign Principles
- One-shot execution: Command must terminate on its own
- No interactive TTY: stdout/stderr captured and streamed
- Cancellation support: Kill process on request
- Timeout protection: 120 second default timeout
Limits
| Limit | Value | | ----------------- | --------------------- | | Default timeout | 120 seconds | | Maximum output | 300 KB (truncated) | | Working directory | Fixed to project path |
Git Integration
sequenceDiagram
participant Client as Client
participant Relay as Relay
participant Daemon as Daemon
Note over Client,Daemon: STATUS & BRANCHES
Client->>Relay: git_status
Relay->>Daemon: forward
Note over Daemon: git status --porcelain
Daemon-->>Relay: git_status_response
Relay-->>Client: {branch, staged, unstaged}
Note over Client,Daemon: STAGE & COMMIT
Client->>Relay: git_stage {paths}
Relay->>Daemon: forward
Note over Daemon: git add
Daemon-->>Relay: git_stage_response
Relay-->>Client: success
Client->>Relay: git_commit {message}
Relay->>Daemon: forward
Note over Daemon: git commit -m
Daemon-->>Relay: git_commit_response
Relay-->>Client: {sha: "abc123"}
Note over Client,Daemon: PUSH
Client->>Relay: git_push
Relay->>Daemon: forward
Note over Daemon: git push
Daemon-->>Relay: git_push_response
Relay-->>Client: success
Note over Client,Daemon: DIFF (via file base)
Client->>Relay: git_file_base {path}
Relay->>Daemon: forward
Note over Daemon: git show HEAD:path
Daemon-->>Relay: git_file_base_response
Relay-->>Client: {content}Git Operations
| Operation | Git Command | Description |
| ------------------- | --------------------------- | ------------------------------ |
| git_status | git status --porcelain=v1 | Branch, staged, unstaged files |
| git_branches | git for-each-ref | List all branches |
| git_branch_create | git branch <name> | Create new branch |
| git_checkout | git checkout <ref> | Switch branch/commit |
| git_stage | git add <paths> | Stage files |
| git_unstage | git reset HEAD <paths> | Unstage files |
| git_commit | git commit -m | Create commit |
| git_push | git push | Push to remote |
| git_file_base | git show HEAD:<path> | Get file at HEAD |
Claude Code Integration
sequenceDiagram
participant Client as Client
participant Relay as Relay
participant Daemon as Daemon
participant Claude as Claude CLI
Note over Client,Claude: NEW SESSION
Client->>Relay: agent_session_new {content}
Relay->>Daemon: forward
Note over Daemon: Generate agentSessionId
Daemon->>Claude: claude -p --output-format stream-json --session-id uuid "prompt"
Daemon-->>Relay: agent_session_new_response {agentSessionId}
Relay-->>Client: forward
loop Streaming events
Claude-->>Daemon: JSON line
Daemon--)Relay: agent_event {type: "text"}
Relay--)Client: forward
Daemon--)Relay: agent_event {type: "tool_use"}
Relay--)Client: forward
end
Claude-->>Daemon: result line
Daemon--)Relay: agent_event {type: "done"}
Relay--)Client: forward
Note over Client,Claude: CONTINUE SESSION
Client->>Relay: agent_message {agentSessionId, content}
Relay->>Daemon: forward
Daemon->>Claude: claude -p --resume sessionId "prompt"
loop Streaming
Daemon--)Relay: agent_event
Relay--)Client: forward
end
Note over Client,Claude: CANCEL
Client->>Relay: agent_cancel
Relay->>Daemon: forward
Note over Daemon: kill (SIGTERM)
Daemon--)Relay: agent_event {type: "done", cancelled: true}
Relay--)Client: forwardExecution Model
The daemon does NOT keep Claude Code processes alive permanently:
- Message arrives from client
- Daemon spawns
claude -p --output-format stream-json --verbose ... - Streaming response to client
- Process terminates
- Next message resumes with
--resume <agentSessionId>
Claude CLI Flags
| Flag | Description |
| ----------------------------- | ------------------------- |
| -p, --print | Non-interactive mode |
| --output-format stream-json | Structured JSON output |
| --verbose | Required with stream-json |
| --session-id <uuid> | Create specific session |
| -r, --resume <uuid> | Resume existing session |
Stream JSON Parser
Claude Code emits line-delimited JSON. The daemon parses and transforms:
graph LR
subgraph "Claude stdout"
SYS["system init"]
ASS["assistant"]
USER["user"]
RES["result"]
end
subgraph "StreamJsonBuffer"
BUF["Parse lines"]
end
subgraph "AgentEvents"
INIT["init"]
TEXT["text"]
FC["file_create"]
FE["file_edit"]
TU["tool_use"]
PROMPT["prompt"]
DONE["done"]
end
SYS --> BUF
ASS --> BUF
USER --> BUF
RES --> BUF
BUF --> INIT
BUF --> TEXT
BUF --> FC
BUF --> FE
BUF --> TU
BUF --> PROMPT
BUF --> DONEMessage Queue
- FIFO queue per session: Max 50 messages
- One Claude process per agentSessionId
- Queue drains after process completes
- If queue full: Reject with
DAEMON_BUSY
Control Plane HTTP API
Local HTTP server on 127.0.0.1:17333 for daemon management.
| Route | Method | Description |
| ---------------- | ------ | ------------------------ |
| /health | GET | Health check (empty 200) |
| /status | GET | Detailed daemon status |
| /stop | POST | Graceful shutdown |
| /machine-label | GET | Get machine label |
| /machine-label | POST | Set machine label |
| /re-register | POST | Re-register with relay |
Status Response
{
"version": "1.0.0",
"machineKey": "550e8400-e29b-41d4-a716-446655440000",
"loggedIn": true,
"relay": {
"connected": true,
"lastConnectedAt": "2026-02-12T10:00:00Z",
"lastHeartbeatAt": "2026-02-12T10:15:00Z"
},
"capabilities": {
"hasGit": true,
"hasClaude": true,
"claudeStoreFound": true,
"warnings": []
},
"projects": [
{
"projectId": "550e8400-...",
"name": "my-app",
"path": "/Users/dev/my-app",
"watcherRunning": true,
"lastFsSeq": 123
}
]
}Database Schema
SQLite database at ~/.pocketcoder/daemon.db:
erDiagram
PROJECTS ||--o{ FS_STATE : "1:1"
PROJECTS ||--o{ COMMAND_RUNS : "1:N"
PROJECTS ||--o{ CLAUDE_PROJECT_DIRS : "1:N"
PROJECTS {
text project_id PK
text name
text path UK
datetime created_at
datetime last_opened_at
}
FS_STATE {
text project_id PK,FK
integer last_seq
}
COMMAND_RUNS {
text request_id PK
text project_id FK
text command
datetime started_at
datetime ended_at
integer exit_code
boolean truncated
text output_preview
}
CLAUDE_PROJECT_DIRS {
text project_id PK,FK
text claude_dir_path PK
datetime last_verified_at
}Retention Policy
| Table | Policy |
| --------------------- | ---------------------------- |
| projects | No limit |
| fs_state | No limit (1 row per project) |
| claude_project_dirs | No limit |
| command_runs | 7 days or max 10,000 rows |
Cleanup runs on startup and every 6 hours.
CLI Commands
| Command | Description |
| ---------------------------------- | ------------------------- |
| pocketcoder login | Authenticate with GitHub |
| pocketcoder logout | Delete stored credentials |
| pocketcoder start | Start daemon (background) |
| pocketcoder start --foreground | Start in foreground |
| pocketcoder stop | Stop daemon gracefully |
| pocketcoder status | Show daemon status |
| pocketcoder add <path> | Register project |
| pocketcoder remove <projectId> | Unregister project |
| pocketcoder projects | List registered projects |
| pocketcoder logs [--tail N] [-f] | View daemon logs |
| pocketcoder machine label <text> | Set machine label |
| pocketcoder enable-autostart | Enable autostart |
| pocketcoder disable-autostart | Disable autostart |
Error Handling
All errors are caught at router level and converted to error responses:
{
type: "error",
requestId: "req_123",
error: {
code: "FILE_NOT_FOUND",
message: "File does not exist",
details?: { path: "src/missing.ts" }
}
}Error Codes
| Code | Description |
| ---------------------- | ------------------------ |
| UNKNOWN_ERROR | Generic error |
| INVALID_REQUEST | Missing/invalid payload |
| UNAUTHORIZED | Authentication failed |
| PROJECT_NOT_FOUND | Project doesn't exist |
| FILE_NOT_FOUND | File doesn't exist |
| FILE_TOO_LARGE | File exceeds 10MB |
| OUTSIDE_PROJECT_ROOT | Path escape attempt |
| GIT_NOT_INITIALIZED | Not a git repo |
| COMMAND_TIMEOUT | Command exceeded timeout |
| DAEMON_BUSY | Queue full (50 max) |
Configuration
Environment Variables
| Variable | Description | Default |
| ------------- | --------------------- | ------------------------------- |
| GATEWAY_URL | Gateway WebSocket URL | wss://gateway.pocketcoder.dev |
| LOG_LEVEL | Logging level | info |
Config Directory
| OS | Path |
| ------- | -------------------------------------------- |
| macOS | ~/Library/Application Support/pocketcoder/ |
| Linux | ~/.config/pocketcoder/ |
| Windows | %APPDATA%\pocketcoder\ |
Files
| File | Purpose |
| ------------------- | ---------------------------- |
| daemon.db | SQLite database |
| auth.json | Authentication tokens |
| machine-label.txt | User-set machine name |
| daemon.pid | PID file (running indicator) |
| logs/daemon.log | Log file |
Testing
# Run all tests
pnpm test
# Run tests in watch mode
pnpm test -- --watch
# Run specific test file
pnpm test -- --run src/handlers/fs-list.test.ts
# Run with coverage
pnpm test -- --coverageTest Structure
tests/
├── handlers/ # Message handler tests
├── services/ # Service tests
├── auth/ # Auth flow tests
├── relay/ # Relay connection tests
├── db/ # Database tests
├── cli/ # CLI command tests
└── utils/ # Utility testsSecurity
Network Security
- Outbound-only: Daemon only makes outbound WebSocket connection
- No public listeners: Control plane bound to
127.0.0.1only - JWT authentication: All relay communication authenticated
File Security
- Path validation: All paths validated to be within project root
- Symlink protection: No escape via symlinks
- Token storage: Restrictive permissions (0600)
Threat Model
- Without stolen JWT: Access probability = 0
- With stolen JWT: Full access (focus on secure storage)
- Anti-enumeration: Always respond
not_foundon auth errors
Performance Limits
| Aspect | Limit | Rationale | | --------------------- | -------------- | ------------------------ | | Message Queue | 50 per session | Prevent unbounded memory | | File Changes | 500 per event | Batch large fs events | | Command Output | 300 KB | Prevent huge payloads | | Command Timeout | 120s | Prevent stuck processes | | File Size | 10 MB | Binary detection | | Debounce Delay | 250ms | Batch file events | | Relay Request Timeout | 15s | Prevent blocking |
License
See the root LICENSE file.
