- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
196 lines
7.8 KiB
TypeScript
196 lines
7.8 KiB
TypeScript
import type { ApprovalHandler, ApprovalRequest, ApprovalResponse } from '@dexto/core';
|
|
import { ApprovalStatus, DenialReason } from '@dexto/core';
|
|
import type { ApprovalCoordinator } from './approval-coordinator.js';
|
|
|
|
/**
|
|
* Creates a manual approval handler that uses ApprovalCoordinator for server communication.
|
|
*
|
|
* This handler emits `approval:request` and waits for `approval:response` via the coordinator,
|
|
* enabling SSE-based approval flows where:
|
|
* 1. Handler emits approval:request → Coordinator → SSE endpoint forwards to client
|
|
* 2. Client sends decision via POST /api/approvals/{approvalId}
|
|
* 3. API route emits approval:response → Coordinator → Handler resolves
|
|
*
|
|
* The returned handler implements the optional cancellation methods (cancel, cancelAll, getPending)
|
|
* for managing pending approval requests.
|
|
*
|
|
* Timeouts are handled per-request using the timeout value from ApprovalRequest, which
|
|
* is set by ApprovalManager based on the request type (tool confirmation vs elicitation).
|
|
*
|
|
* @param coordinator The approval coordinator for request/response communication
|
|
* @returns ApprovalHandler with cancellation support
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const coordinator = new ApprovalCoordinator();
|
|
* const handler = createManualApprovalHandler(coordinator);
|
|
* agent.setApprovalHandler(handler);
|
|
*
|
|
* // Later, cancel a specific approval (if handler supports it)
|
|
* handler.cancel?.('approval-id-123');
|
|
* ```
|
|
*/
|
|
export function createManualApprovalHandler(coordinator: ApprovalCoordinator): ApprovalHandler {
|
|
// Track pending approvals for cancellation support
|
|
const pendingApprovals = new Map<
|
|
string,
|
|
{
|
|
cleanup: () => void;
|
|
resolve: (response: ApprovalResponse) => void;
|
|
request: ApprovalRequest;
|
|
}
|
|
>();
|
|
|
|
const handleApproval = (request: ApprovalRequest): Promise<ApprovalResponse> => {
|
|
return new Promise<ApprovalResponse>((resolve) => {
|
|
// Use per-request timeout (optional - undefined means no timeout)
|
|
// - Tool confirmations use config.toolConfirmation.timeout
|
|
// - Elicitations use config.elicitation.timeout
|
|
const effectiveTimeout = request.timeout;
|
|
|
|
// Set timeout timer ONLY if timeout is specified
|
|
// If undefined, wait indefinitely for user response
|
|
let timer: NodeJS.Timeout | undefined;
|
|
if (effectiveTimeout !== undefined) {
|
|
timer = setTimeout(() => {
|
|
cleanup();
|
|
pendingApprovals.delete(request.approvalId);
|
|
|
|
// Emit timeout response so UI/clients can dismiss the prompt
|
|
const timeoutResponse: ApprovalResponse = {
|
|
approvalId: request.approvalId,
|
|
status: ApprovalStatus.CANCELLED,
|
|
sessionId: request.sessionId,
|
|
reason: DenialReason.TIMEOUT,
|
|
message: `Approval request timed out after ${effectiveTimeout}ms`,
|
|
timeoutMs: effectiveTimeout,
|
|
};
|
|
coordinator.emitResponse(timeoutResponse);
|
|
|
|
// Resolve with CANCELLED response (not reject) to match auto-approve/deny behavior
|
|
// Callers can uniformly check response.status instead of handling exceptions
|
|
resolve(timeoutResponse);
|
|
}, effectiveTimeout);
|
|
}
|
|
|
|
// Cleanup function to remove listener and clear timeout
|
|
let cleanupListener: (() => void) | null = null;
|
|
const cleanup = () => {
|
|
if (timer !== undefined) {
|
|
clearTimeout(timer);
|
|
}
|
|
if (cleanupListener) {
|
|
cleanupListener();
|
|
cleanupListener = null;
|
|
}
|
|
};
|
|
|
|
// Listen for approval:response events
|
|
const listener = (res: ApprovalResponse) => {
|
|
// Only handle responses for this specific approval
|
|
if (res.approvalId === request.approvalId) {
|
|
cleanup();
|
|
pendingApprovals.delete(request.approvalId);
|
|
resolve(res);
|
|
}
|
|
};
|
|
|
|
// Register listener
|
|
coordinator.on('approval:response', listener);
|
|
cleanupListener = () => coordinator.off('approval:response', listener);
|
|
|
|
// Store for cancellation support
|
|
pendingApprovals.set(request.approvalId, {
|
|
cleanup,
|
|
resolve,
|
|
request,
|
|
});
|
|
|
|
// Emit the approval:request event via coordinator
|
|
// SSE endpoints will subscribe to coordinator and forward to clients
|
|
coordinator.emitRequest(request);
|
|
});
|
|
};
|
|
|
|
const handler: ApprovalHandler = Object.assign(handleApproval, {
|
|
cancel: (approvalId: string): void => {
|
|
const pending = pendingApprovals.get(approvalId);
|
|
if (pending) {
|
|
pending.cleanup();
|
|
pendingApprovals.delete(approvalId);
|
|
|
|
// Create cancellation response
|
|
const cancelResponse: ApprovalResponse = {
|
|
approvalId,
|
|
status: ApprovalStatus.CANCELLED,
|
|
sessionId: pending.request.sessionId,
|
|
reason: DenialReason.SYSTEM_CANCELLED,
|
|
message: 'Approval request was cancelled',
|
|
};
|
|
|
|
// Emit cancellation event so UI listeners can dismiss the prompt
|
|
coordinator.emitResponse(cancelResponse);
|
|
|
|
// Resolve with CANCELLED response (not reject) to match auto-approve/deny behavior
|
|
// Callers can uniformly check response.status instead of handling exceptions
|
|
pending.resolve(cancelResponse);
|
|
}
|
|
},
|
|
|
|
cancelAll: (): void => {
|
|
for (const [approvalId] of pendingApprovals) {
|
|
handler.cancel?.(approvalId);
|
|
}
|
|
},
|
|
|
|
getPending: (): string[] => {
|
|
return Array.from(pendingApprovals.keys());
|
|
},
|
|
|
|
getPendingRequests: (): ApprovalRequest[] => {
|
|
return Array.from(pendingApprovals.values()).map((p) => p.request);
|
|
},
|
|
|
|
/**
|
|
* Auto-approve pending requests that match a predicate.
|
|
* Used when a pattern is remembered to auto-approve other parallel requests
|
|
* that would now match the same pattern.
|
|
*/
|
|
autoApprovePending: (
|
|
predicate: (request: ApprovalRequest) => boolean,
|
|
responseData?: Record<string, unknown>
|
|
): number => {
|
|
let count = 0;
|
|
|
|
// Find all pending approvals that match the predicate
|
|
for (const [approvalId, pending] of pendingApprovals) {
|
|
if (predicate(pending.request)) {
|
|
// Clean up the pending state
|
|
pending.cleanup();
|
|
pendingApprovals.delete(approvalId);
|
|
|
|
// Create auto-approval response
|
|
const autoApproveResponse: ApprovalResponse = {
|
|
approvalId,
|
|
status: ApprovalStatus.APPROVED,
|
|
sessionId: pending.request.sessionId,
|
|
message: 'Auto-approved due to matching remembered pattern',
|
|
data: responseData,
|
|
};
|
|
|
|
// Emit response so UI can update
|
|
coordinator.emitResponse(autoApproveResponse);
|
|
|
|
// Resolve the pending promise
|
|
pending.resolve(autoApproveResponse);
|
|
count++;
|
|
}
|
|
}
|
|
|
|
return count;
|
|
},
|
|
});
|
|
|
|
return handler;
|
|
}
|