Replace hardcoded PowerShell status hooks with stdin-forwarding hooks that send full Claude Code hook data (tool_input, tool_response, prompt, session_id, model, etc.) to /api/claude-hook endpoint. - PowerShell hooks read stdin JSON and POST to /api/claude-hook - Server derives status for backward-compat FAB animations - Server extracts assistant_response from transcript on Stop events - New /api/claude-permission endpoint with Promise-based allow/deny flow - HookNotifications.vue: toast system showing session, prompt, tool use, tool results, notifications, and final assistant response - WebSocket broadcast for claude-hook and claude-permission message types
112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
import { jsonResponse, errorResponse } from '../utils/cors'
|
|
import { PORT_TERMINAL } from '../config'
|
|
|
|
interface PermissionPayload {
|
|
hook_event_name?: string
|
|
session_id?: string
|
|
tool_name?: string
|
|
tool_input?: unknown
|
|
cwd?: string
|
|
[key: string]: unknown
|
|
}
|
|
|
|
interface PendingPermission {
|
|
resolve: (decision: string) => void
|
|
timer: ReturnType<typeof setTimeout>
|
|
payload: PermissionPayload
|
|
}
|
|
|
|
// Map of requestId -> pending permission promise resolver
|
|
const pendingPermissions = new Map<string, PendingPermission>()
|
|
|
|
const PERMISSION_TIMEOUT_MS = 60_000
|
|
|
|
export async function handleClaudePermission(req: Request): Promise<Response | null> {
|
|
if (req.method !== 'POST') return null
|
|
|
|
try {
|
|
const body = await req.json() as PermissionPayload
|
|
const requestId = `perm_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
|
|
// Broadcast permission request to UI via WebSocket
|
|
try {
|
|
await fetch(`http://localhost:${PORT_TERMINAL}/claude-permission`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ requestId, ...body })
|
|
})
|
|
} catch (e) {
|
|
console.error('[claude-permission] Failed to broadcast to terminal server:', e)
|
|
}
|
|
|
|
// Also broadcast claude-status for backward compat (animations)
|
|
try {
|
|
await fetch(`http://localhost:${PORT_TERMINAL}/claude-status`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ status: 'permissionRequest', tool: body.tool_name })
|
|
})
|
|
} catch (e) {
|
|
console.error('[claude-permission] Failed to forward status:', e)
|
|
}
|
|
|
|
// Wait for UI decision (or timeout)
|
|
const decision = await new Promise<string>((resolve) => {
|
|
const timer = setTimeout(() => {
|
|
pendingPermissions.delete(requestId)
|
|
resolve('ask')
|
|
}, PERMISSION_TIMEOUT_MS)
|
|
|
|
pendingPermissions.set(requestId, { resolve, timer, payload: body })
|
|
})
|
|
|
|
return jsonResponse({ decision, requestId })
|
|
} catch (e) {
|
|
return errorResponse('Invalid JSON body', 400)
|
|
}
|
|
}
|
|
|
|
export async function handleClaudePermissionRespond(req: Request): Promise<Response | null> {
|
|
if (req.method !== 'POST') return null
|
|
|
|
try {
|
|
const body = await req.json() as { requestId: string, decision: 'allow' | 'deny' }
|
|
|
|
if (!body.requestId || !body.decision) {
|
|
return errorResponse('Missing requestId or decision', 400)
|
|
}
|
|
|
|
if (!['allow', 'deny'].includes(body.decision)) {
|
|
return errorResponse('Decision must be "allow" or "deny"', 400)
|
|
}
|
|
|
|
const pending = pendingPermissions.get(body.requestId)
|
|
if (!pending) {
|
|
return errorResponse('Permission request not found or already expired', 404)
|
|
}
|
|
|
|
// Resolve the pending promise
|
|
clearTimeout(pending.timer)
|
|
pendingPermissions.delete(body.requestId)
|
|
pending.resolve(body.decision)
|
|
|
|
return jsonResponse({ success: true, requestId: body.requestId, decision: body.decision })
|
|
} catch (e) {
|
|
return errorResponse('Invalid JSON body', 400)
|
|
}
|
|
}
|
|
|
|
// List pending permissions (useful for UI to recover state)
|
|
export async function handleClaudePermissionList(req: Request): Promise<Response | null> {
|
|
if (req.method !== 'GET') return null
|
|
|
|
const pending = Array.from(pendingPermissions.entries()).map(([id, p]) => ({
|
|
requestId: id,
|
|
tool_name: p.payload.tool_name,
|
|
tool_input: p.payload.tool_input,
|
|
session_id: p.payload.session_id
|
|
}))
|
|
|
|
return jsonResponse({ pending })
|
|
}
|