feat: Add intelligent auto-router and enhanced integrations
- 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>
This commit is contained in:
195
dexto/packages/server/src/approval/manual-approval-handler.ts
Normal file
195
dexto/packages/server/src/approval/manual-approval-handler.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user