Documentation Index
Fetch the complete documentation index at: https://mintlify.com/tiann/hapi/llms.txt
Use this file to discover all available pages before exploring further.
Overview
HAPI uses an RPC (Remote Procedure Call) system to enable the web app to invoke methods on the CLI.
Flow:
- CLI registers RPC handlers via Socket.IO
- Web calls REST endpoint
- Hub routes to CLI via Socket.IO
rpc-request event
- CLI executes method and returns result
- Hub returns result to web
Architecture
Registering Handlers
CLI Side
import { io } from 'socket.io-client'
const socket = io('http://127.0.0.1:3006/cli', {
auth: {
sessionId: 'abc123',
namespace: 'default'
}
})
// Register RPC method
socket.emit('rpc-register', {
method: 'uploadFile'
})
// Handle RPC requests
socket.on('rpc-request', async (data, callback) => {
const { method, params } = data
const parsedParams = JSON.parse(params)
try {
let result
switch (method) {
case 'uploadFile':
result = await handleUploadFile(parsedParams)
break
case 'deleteUploadFile':
result = await handleDeleteUpload(parsedParams)
break
case 'checkPathExists':
result = await handleCheckPath(parsedParams)
break
case 'gitStatus':
result = await handleGitStatus(parsedParams)
break
default:
result = { success: false, error: 'Unknown method' }
}
callback(JSON.stringify(result))
} catch (error) {
callback(JSON.stringify({
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}))
}
})
// Unregister on disconnect
socket.on('disconnect', () => {
socket.emit('rpc-unregister', { method: 'uploadFile' })
})
Common RPC Methods
uploadFile
Called by: POST /api/sessions/:id/upload
Params:
{
filename: string
content: string // base64
mimeType: string
}
Returns:
{
success: boolean
path?: string
error?: string
}
Implementation Example:
async function handleUploadFile(params: any) {
const { filename, content, mimeType } = params
const buffer = Buffer.from(content, 'base64')
const tmpPath = `/tmp/hapi-upload-${sessionId}-${filename}`
await fs.writeFile(tmpPath, buffer)
return {
success: true,
path: tmpPath
}
}
deleteUploadFile
Called by: POST /api/sessions/:id/upload/delete
Params:
Returns:
{
success: boolean
error?: string
}
checkPathExists
Called by: POST /api/machines/:id/paths/exists
Params:
Returns:
{
[path: string]: boolean
}
Implementation Example:
async function handleCheckPath(params: any) {
const { paths } = params
const result: Record<string, boolean> = {}
for (const path of paths) {
try {
await fs.access(path)
result[path] = true
} catch {
result[path] = false
}
}
return result
}
gitStatus
Called by: GET /api/sessions/:id/git-status
Params:
Returns:
{
success: boolean
branch?: string
files?: Array<{
path: string
status: string
}>
error?: string
}
gitDiffNumstat
Called by: GET /api/sessions/:id/git-diff-numstat
Params:
{
cwd: string
staged?: boolean
}
Returns:
{
success: boolean
files?: Array<{
path: string
added: number
removed: number
}>
error?: string
}
gitDiffFile
Called by: GET /api/sessions/:id/git-diff-file
Params:
{
cwd: string
filePath: string
staged?: boolean
}
Returns:
{
success: boolean
diff?: string
error?: string
}
readFile
Called by: GET /api/sessions/:id/file
Params:
Returns:
{
success: boolean
content?: string
error?: string
}
runRipgrep
Called by: GET /api/sessions/:id/files
Params:
{
args: string[]
cwd: string
}
Returns:
{
success: boolean
stdout?: string
stderr?: string
error?: string
}
listDirectory
Called by: GET /api/sessions/:id/directory
Params:
Returns:
{
success: boolean
entries?: Array<{
name: string
type: 'file' | 'directory'
size?: number
}>
error?: string
}
spawnSession
Called by: POST /api/machines/:id/spawn
Params:
{
directory: string
agent?: 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode'
model?: string
yolo?: boolean
sessionType?: 'simple' | 'worktree'
worktreeName?: string
}
Returns:
{
success: boolean
sessionId?: string
error?: string
}
listSlashCommands
Called by: GET /api/sessions/:id/slash-commands
Params:
Returns:
{
success: boolean
commands?: string[]
error?: string
}
listSkills
Called by: GET /api/sessions/:id/skills
Params: None
Returns:
{
success: boolean
skills?: Array<{
name: string
description: string
}>
error?: string
}
Error Handling
Timeout
RPC calls timeout after 30 seconds by default:
const result = await engine.callRpc(
sessionId,
'uploadFile',
params,
30000 // 30 second timeout
)
No Handler Registered
If no CLI has registered the method:
{
"success": false,
"error": "No handler registered for method"
}
CLI Error
If the CLI handler throws an error:
{
"success": false,
"error": "File not found: /tmp/upload.txt"
}
Hub-Side Implementation
RPC Registry
The hub maintains a registry mapping methods to sockets:
class RpcRegistry {
private handlers = new Map<string, Socket>()
register(socket: Socket, method: string) {
this.handlers.set(method, socket)
}
unregister(socket: Socket, method: string) {
if (this.handlers.get(method) === socket) {
this.handlers.delete(method)
}
}
async call(method: string, params: any, timeout = 30000): Promise<any> {
const socket = this.handlers.get(method)
if (!socket) {
throw new Error('No handler registered')
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('RPC timeout'))
}, timeout)
socket.emit('rpc-request',
{
method,
params: JSON.stringify(params)
},
(response: string) => {
clearTimeout(timer)
resolve(JSON.parse(response))
}
)
})
}
}
Calling RPC from REST
app.post('/sessions/:id/upload', async (c) => {
const sessionId = c.req.param('id')
const { filename, content, mimeType } = await c.req.json()
const result = await rpcGateway.call('uploadFile', {
filename,
content,
mimeType
})
return c.json(result)
})
Best Practices
- Register on connect - Register all RPC methods immediately after connecting
- Unregister on disconnect - Clean up handlers when socket disconnects
- Handle errors - Always wrap handler code in try/catch
- Return structured responses - Use
{success, ...} format
- Validate params - Check parameter types and values
- Set timeouts - Use reasonable timeouts for long operations
- Log failures - Log RPC errors for debugging