@adamhancock/workstream-daemon
v0.1.15
Published
Background daemon that maintains real-time VS Code instance and Chrome tab metadata for instant access
Maintainers
Readme
Workstream Daemon
Optional background service that maintains a real-time index of VS Code instances with metadata. This makes the Raycast extension and CLI tool instant by pre-fetching all metadata.
What It Does
The daemon runs in the background and:
- ⚡ Polls VS Code instances every 5 seconds
- 📊 Fetches git status, PR info, and Claude Code status
- 💾 Maintains a cache file at
~/.workstream-daemon/instances.json - 🔴 Uses Redis pub/sub for real-time updates
- 🚀 Makes the Raycast extension load instantly (< 10ms)
Prerequisites
Redis must be running locally:
# macOS (Homebrew)
brew install redis
brew services start redis
# Or manually
redis-serverVerify Redis is running:
redis-cli ping # Should return "PONG"Installation
Option 1: Install via npm/pnpm (Recommended)
pnpm add -g @adamhancock/workstream-daemonThen you can use the workstream command:
workstream start # Start the daemon
workstream status # Check status
workstream stop # Stop the daemon
workstream install # Install as macOS serviceOption 2: Install from source
cd packages/workstream-daemon
pnpm install
pnpm link --globalInstall as macOS LaunchAgent (auto-start on login)
workstream installThis will:
- Create a LaunchAgent plist file
- Configure it to run automatically on login
- Start the daemon immediately
- Set up logging to
~/Library/Logs/workstream-daemon.log
Usage
CLI Commands
workstream start # Start daemon in background
workstream stop # Stop running daemon
workstream console # Run daemon in foreground with live output (debugging)
workstream status # Check if daemon is running
workstream logs # Watch all logs in real-time
workstream logs stdout # Watch only stdout logs
workstream logs stderr # Watch only error logs
workstream install # Install as macOS service (auto-start)
workstream uninstall # Remove macOS service
workstream help # Show help messageRunning Manually (for development)
The easiest way to run the daemon with live output:
workstream consoleOr run directly with pnpm (same behavior):
pnpm startCheck if Running
# Using CLI (recommended)
workstream status
# Or check process manually
ps aux | grep workstream-daemon
# Or check Redis
redis-cli keys "workstream:*"View Logs
# Using CLI (recommended) - watches both stdout and stderr
workstream logs
# Watch only stdout
workstream logs stdout
# Watch only errors
workstream logs stderr
# Or manually with tail
tail -f ~/Library/Logs/workstream-daemon.log
tail -f ~/Library/Logs/workstream-daemon-error.logView Cache
# View file cache
cat ~/.workstream-daemon/instances.json | jq
# View Redis data
redis-cli keys "workstream:*"
redis-cli get workstream:timestamp
redis-cli smembers workstream:instances:listUninstallation
workstream uninstallThis will:
- Stop the daemon
- Remove the LaunchAgent
- Keep cache files (delete manually if needed)
To completely remove:
workstream uninstall
rm -rf ~/.workstream-daemon
pnpm remove -g @adamhancock/workstream-daemonConfiguration
Edit these constants in src/index.ts:
POLL_INTERVAL: How often to poll (default: 5000ms)INSTANCE_TTL: Redis key expiration (default: 30 seconds)CACHE_DIR: Cache directory (default: ~/.workstream-daemon)
Redis configuration in src/redis-client.ts:
host: Redis host (default: localhost)port: Redis port (default: 6379)
Claude Code Integration
The daemon can receive real-time notifications from Claude Code via hooks:
Setup
Ensure redis-cli is available (installed automatically with Redis):
which redis-cli # Should show path to redis-cliCopy the hook script to
~/.claude/:cp notify-daemon.sh ~/.claude/notify-daemon.sh chmod +x ~/.claude/notify-daemon.shConfigure Claude hooks in
~/.claude/settings.json:{ "hooks": { "UserPromptSubmit": [{ "matcher": "*", "hooks": [{"type": "command", "command": "~/.claude/notify-daemon.sh"}] }], "PreToolUse": [{ "matcher": "*", "hooks": [{"type": "command", "command": "~/.claude/notify-daemon.sh"}] }], "PreCompact": [{ "matcher": "*", "hooks": [{"type": "command", "command": "~/.claude/notify-daemon.sh"}] }], "Notification": [{ "matcher": "*", "hooks": [{"type": "command", "command": "~/.claude/notify-daemon.sh"}] }], "Stop": [{ "matcher": "*", "hooks": [{"type": "command", "command": "~/.claude/notify-daemon.sh"}] }] } }Note: The hook script automatically parses the event type from Claude's JSON context. It detects:
- AskUserQuestion and ExitPlanMode tools (interactive prompts)
- Notification events with type
permission_prompt(tool approval) oridle_prompt(waiting for input) - PreCompact events (context compaction)
These trigger appropriate status changes (
waiting_for_input,compacting_started, etc.). No arguments needed!
What It Does
The daemon tracks Claude's working state using hooks instead of CPU monitoring for accurate, real-time status:
🚀 Work Started: When you submit a prompt or Claude uses any tool
- Updates status to "Working" (purple in Raycast)
- Triggered by: User message submission or any tool use
🤔 Waiting for Input: When Claude shows an interactive prompt or needs approval
- Triggered by: AskUserQuestion tool, ExitPlanMode (plan approval), or Notification hook with
permission_promptoridle_prompttype - Updates status to "Waiting" (orange in Raycast)
- Sends macOS notification: "🤔 Claude needs your attention in [project]"
- Triggered by: AskUserQuestion tool, ExitPlanMode (plan approval), or Notification hook with
✅ Work Stopped: When Claude finishes a task (Stop hook)
- Clears working and waiting status (gray "Idle" in Raycast)
- Sends macOS notification: "✅ Claude finished working in [project]"
This hook-based approach is more reliable than CPU monitoring since it directly tracks Claude's actual state changes.
Testing
Test the hook script manually:
# Test work started
echo '{"session_id":"test"}' | CLAUDE_PROJECT_DIR="$(pwd)" ~/.claude/notify-daemon.sh work_started
# Test waiting for input
echo '{"session_id":"test"}' | CLAUDE_PROJECT_DIR="$(pwd)" ~/.claude/notify-daemon.sh waiting_for_input
# Test compacting context
echo '{"session_id":"test"}' | CLAUDE_PROJECT_DIR="$(pwd)" ~/.claude/notify-daemon.sh compacting_started
# Test work stopped
echo '{"session_id":"test"}' | CLAUDE_PROJECT_DIR="$(pwd)" ~/.claude/notify-daemon.sh work_stoppedYou should see status updates in Raycast and notifications for waiting/finished events. The compacting status will show as "Compacting" in the dashboard.
How Clients Use It
Raycast Extension
The Raycast extension can read from Redis or the cache file for instant results:
import Redis from 'ioredis';
const redis = new Redis({ host: 'localhost', port: 6379 });
async function loadFromRedis() {
try {
// Get instance paths
const paths = await redis.smembers('workstream:instances:list');
// Get each instance data
const pipeline = redis.pipeline();
for (const path of paths) {
const key = `workstream:instance:${Buffer.from(path).toString('base64')}`;
pipeline.get(key);
}
const results = await pipeline.exec();
return results.map(([, data]) => JSON.parse(data as string));
} catch {
// Redis not available, fallback to file cache
return null;
}
}Redis Pub/Sub Client (Real-time Updates)
import Redis from 'ioredis';
const subscriber = new Redis({ host: 'localhost', port: 6379 });
subscriber.subscribe('workstream:updates');
subscriber.on('message', async (channel, message) => {
if (channel === 'workstream:updates') {
const { type, count, timestamp } = JSON.parse(message);
if (type === 'instances') {
console.log(`Received update: ${count} instances at ${timestamp}`);
// Load latest data from Redis
const instances = await loadFromRedis();
}
}
});
// Trigger refresh
const publisher = new Redis({ host: 'localhost', port: 6379 });
await publisher.publish('workstream:refresh', JSON.stringify({ type: 'refresh' }));Performance Comparison
Without Daemon:
- First load: 2-3 seconds (fetches everything)
- Cached loads: ~500ms (reads from Raycast cache)
With Daemon (Redis):
- Every load: < 10ms (reads from Redis)
- Always up-to-date (refreshed every 5 seconds)
- Real-time updates via pub/sub
Troubleshooting
Daemon won't start
Check logs:
tail -f ~/Library/Logs/workstream-daemon-error.logRedis connection issues
Check if Redis is running:
redis-cli pingStart Redis:
brew services start redisCache not updating
Check if daemon is running:
launchctl list | grep workstreamCheck Redis data:
redis-cli keys "workstream:*"
redis-cli ttl workstream:timestampRestart daemon:
launchctl stop com.workstream.daemon
launchctl start com.workstream.daemonArchitecture
┌─────────────────┐
│ VS Code │
│ Instances │
└────────┬────────┘
│
↓ (lsof every 5s)
┌─────────────────┐ ┌──────────────┐
│ Workstream │ ──────→ │ Cache File │
│ Daemon │ writes │ .json │
└────────┬────────┘ └──────────────┘
│
│ Redis Pub/Sub + Storage
↓
┌─────────────────┐
│ Redis Server │
│ (localhost) │
└────────┬────────┘
│
↓ reads + subscribes
┌─────────────────┐ ┌──────────────┐
│ Raycast Ext │ │ CLI Tool │
│ (live updates) │ │ (instant) │
└─────────────────┘ └──────────────┘Redis Data Structure
Keys:
- workstream:instances:list (SET) → Set of instance paths
- workstream:instance:{base64path} (STRING) → JSON instance data
- workstream:timestamp (STRING) → Last update timestamp
Pub/Sub Channels:
- workstream:updates → Instance list updates
- workstream:refresh → Trigger refresh requests
- workstream:claude:{base64path} → Claude status updates
TTL: All keys expire after 30 seconds if daemon stopsDevelopment
Watch mode
pnpm run devTesting locally
- Start Redis:
redis-server - Run daemon:
pnpm start - Check Redis:
redis-cli keys "workstream:*" - Monitor pub/sub:
redis-cli subscribe workstream:updates
License
MIT
