Skip to content
Open
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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Short guide for AI agents in this repo. Prefer progressive loading: start with t

## What is HAPI?

Local-first platform for running AI coding agents (Claude Code, Codex, Gemini) with remote control via web/phone. CLI wraps agents and connects to hub; hub serves web app and handles real-time sync.
Local-first platform for running AI coding agents (Claude Code, Codex, Gemini, OpenCode, Cursor) with remote control via web/phone. CLI wraps agents and connects to hub; hub serves web app and handles real-time sync.

## Repo layout

Expand Down Expand Up @@ -36,7 +36,7 @@ Bun workspaces; `shared` consumed by cli, hub, web.
```

**Data flow:**
1. CLI spawns agent (claude/codex/gemini), connects to hub via Socket.IO
1. CLI spawns agent (claude/codex/gemini/opencode/cursor), connects to hub via Socket.IO
2. Agent events → CLI → hub (socket `message` event) → DB + SSE broadcast
3. Web subscribes to SSE `/api/events`, receives live updates
4. User actions → Web → hub REST API → RPC to CLI → agent
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# HAPI

Run official Claude Code / Codex / Gemini / OpenCode sessions locally and control them remotely through a Web / PWA / Telegram Mini App.
Run official Claude Code / Codex / Gemini / OpenCode / Cursor sessions locally and control them remotely through a Web / PWA / Telegram Mini App.

> **Why HAPI?** HAPI is a local-first alternative to Happy. See [Why Not Happy?](docs/guide/why-hapi.md) for the key differences.

Expand All @@ -9,7 +9,7 @@ Run official Claude Code / Codex / Gemini / OpenCode sessions locally and contro
- **Seamless Handoff** - Work locally, switch to remote when needed, switch back anytime. No context loss, no session restart.
- **Native First** - HAPI wraps your AI agent instead of replacing it. Same terminal, same experience, same muscle memory.
- **AFK Without Stopping** - Step away from your desk? Approve AI requests from your phone with one tap.
- **Your AI, Your Choice** - Claude Code, Codex, Gemini, OpenCode—different models, one unified workflow.
- **Your AI, Your Choice** - Claude Code, Codex, Gemini, OpenCode, Cursor—different models, one unified workflow.
- **Terminal Anywhere** - Run commands from your phone or browser, directly connected to the working machine.
- **Voice Control** - Talk to your AI agent hands-free using the built-in voice assistant.

Expand Down
6 changes: 5 additions & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# hapi CLI

Run Claude Code, Codex, Gemini, or OpenCode sessions from your terminal and control them remotely through the hapi hub.
Run Claude Code, Codex, Gemini, OpenCode, or Cursor sessions from your terminal and control them remotely through the hapi hub.

## What it does

- Starts Claude Code sessions and registers them with hapi-hub.
- Starts Codex mode for OpenAI-based sessions.
- Starts Cursor mode using Cursor CLI.
- Starts Gemini mode via ACP (Anthropic Code Plugins).
- Starts OpenCode mode via ACP and its plugin hook system.
- Provides an MCP stdio bridge for external tools.
Expand All @@ -26,6 +27,7 @@ Run Claude Code, Codex, Gemini, or OpenCode sessions from your terminal and cont
- `hapi` - Start a Claude Code session (passes through Claude CLI flags). See `src/index.ts`.
- `hapi codex` - Start Codex mode. See `src/codex/runCodex.ts`.
- `hapi codex resume <sessionId>` - Resume existing Codex session.
- `hapi cursor` - Start Cursor mode. See `src/cursor/runCursor.ts`.
- `hapi gemini` - Start Gemini mode via ACP. See `src/agent/runners/runAgentSession.ts`.
Note: Gemini runs in remote mode only; it waits for messages from the hub UI/Telegram.
- `hapi opencode` - Start OpenCode mode via ACP. See `src/opencode/runOpencode.ts`.
Expand Down Expand Up @@ -103,6 +105,7 @@ Data is stored in `~/.hapi/` (or `$HAPI_HOME`):
## Requirements

- Claude CLI installed and logged in (`claude` on PATH).
- Cursor CLI installed and logged in (`agent` on PATH).
- OpenCode CLI installed (`opencode` on PATH).
- Bun for building from source.

Expand All @@ -127,6 +130,7 @@ bun run build:single-exe
- `src/api/` - Bot communication (Socket.IO + REST).
- `src/claude/` - Claude Code integration.
- `src/codex/` - Codex mode integration.
- `src/cursor/` - Cursor mode integration.
- `src/agent/` - Multi-agent support (Gemini via ACP).
- `src/opencode/` - OpenCode ACP + hook integration.
- `src/runner/` - Background service.
Expand Down
16 changes: 16 additions & 0 deletions cli/src/agent/runners/cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { AgentRegistry } from '@/agent/AgentRegistry';
import { CursorCliBackend } from '@/cursor/CursorCliBackend';

export type CursorRunnerOptions = {
model?: string;
resumeSessionId?: string;
cursorArgs?: string[];
};

export function registerCursorAgent(options: CursorRunnerOptions = {}): void {
AgentRegistry.register('cursor', () => new CursorCliBackend({
model: options.model,
resumeSessionId: options.resumeSessionId,
extraArgs: options.cursorArgs
}));
}
15 changes: 15 additions & 0 deletions cli/src/agent/runners/runAgentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ export async function runAgentSession(opts: {
agentType: string;
startedBy?: 'runner' | 'terminal';
}): Promise<void> {
const applyBackendSessionIdToMetadata = (backendSessionId: string) => {
if (opts.agentType !== 'cursor') {
return;
}
session.updateMetadata((metadata) => ({
...metadata,
cursorSessionId: backendSessionId
}));
};

const initialState: AgentState = {
controlledByUser: false
};
Expand Down Expand Up @@ -70,6 +80,7 @@ export async function runAgentSession(opts: {
cwd: process.cwd(),
mcpServers
});
applyBackendSessionIdToMetadata(agentSessionId);

let thinking = false;
let shouldExit = false;
Expand Down Expand Up @@ -138,6 +149,10 @@ export async function runAgentSession(opts: {
session.sendCodexMessage(converted);
}
});
const activeSessionId = backend.getActiveSessionId?.()
if (activeSessionId) {
applyBackendSessionIdToMetadata(activeSessionId);
}
} catch (error) {
logger.warn('[ACP] Prompt failed', error);
session.sendSessionEvent({
Expand Down
1 change: 1 addition & 0 deletions cli/src/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface AgentBackend {
cancelPrompt(sessionId: string): Promise<void>;
respondToPermission(sessionId: string, request: PermissionRequest, response: PermissionResponse): Promise<void>;
onPermissionRequest(handler: (request: PermissionRequest) => void): void;
getActiveSessionId?(): string | null;
disconnect(): Promise<void>;
}

Expand Down
1 change: 1 addition & 0 deletions cli/src/commands/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ ${chalk.bold('Usage:')}
hapi [options] Start Claude with Telegram control (direct-connect)
hapi auth Manage authentication
hapi codex Start Codex mode
hapi cursor Start Cursor mode
hapi gemini Start Gemini ACP mode
hapi opencode Start OpenCode ACP mode
hapi mcp Start MCP stdio bridge
Expand Down
65 changes: 65 additions & 0 deletions cli/src/commands/cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import chalk from 'chalk'
import { authAndSetupMachineIfNeeded } from '@/ui/auth'
import { initializeToken } from '@/ui/tokenInit'
import { maybeAutoStartServer } from '@/utils/autoStartServer'
import type { CommandDefinition } from './types'

export const cursorCommand: CommandDefinition = {
name: 'cursor',
requiresRuntimeAssets: true,
run: async ({ commandArgs }) => {
try {
const options: {
startedBy?: 'runner' | 'terminal'
model?: string
resumeSessionId?: string
cursorArgs?: string[]
} = {}
const passthroughArgs: string[] = []

for (let i = 0; i < commandArgs.length; i++) {
const arg = commandArgs[i]
if (arg === '--started-by') {
options.startedBy = commandArgs[++i] as 'runner' | 'terminal'
} else if (arg === '--yolo') {
// Cursor mode always runs in print mode with --force. Keep the flag for compatibility.
continue
} else if (arg === '--model') {
const model = commandArgs[++i]
if (!model) {
throw new Error('Missing --model value')
}
options.model = model
} else if (arg === '--resume') {
const resumeSessionId = commandArgs[++i]
if (!resumeSessionId) {
throw new Error('Missing --resume value')
}
options.resumeSessionId = resumeSessionId
} else if (arg === '--hapi-starting-mode') {
// Cursor integration is remote-only; consume this runner flag without forwarding it.
i += 1
} else {
passthroughArgs.push(arg)
}
}

if (passthroughArgs.length > 0) {
options.cursorArgs = passthroughArgs
}

await initializeToken()
await maybeAutoStartServer()
await authAndSetupMachineIfNeeded()

const { runCursor } = await import('@/cursor/runCursor')
await runCursor(options)
} catch (error) {
console.error(chalk.red('Error:'), error instanceof Error ? error.message : 'Unknown error')
if (process.env.DEBUG) {
console.error(error)
}
process.exit(1)
}
}
}
2 changes: 2 additions & 0 deletions cli/src/commands/registry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { authCommand } from './auth'
import { claudeCommand } from './claude'
import { codexCommand } from './codex'
import { cursorCommand } from './cursor'
import { connectCommand } from './connect'
import { runnerCommand } from './runner'
import { doctorCommand } from './doctor'
Expand All @@ -16,6 +17,7 @@ const COMMANDS: CommandDefinition[] = [
authCommand,
connectCommand,
codexCommand,
cursorCommand,
geminiCommand,
opencodeCommand,
mcpCommand,
Expand Down
Loading