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:
admin
2026-01-28 00:27:56 +04:00
Unverified
parent 3b128ba3bd
commit b52318eeae
1724 changed files with 351216 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
import { EventEmitter } from 'node:events';
import type { ApprovalRequest, ApprovalResponse } from '@dexto/core';
/**
* Event coordinator for approval request/response flow between handler and server.
*
* Provides explicit separation between agent lifecycle events (on AgentEventBus)
* and server-mode coordination events (on ApprovalCoordinator).
*
* Used by:
* - ManualApprovalHandler: Emits requests, listens for responses
* - Streaming endpoints: Listens for requests, helps emit responses
* - Approval routes: Emits responses from client submissions
*/
export class ApprovalCoordinator extends EventEmitter {
// Track approvalId -> sessionId mapping for multi-client SSE routing
private approvalSessions = new Map<string, string | undefined>();
/**
* Emit an approval request.
* Called by ManualApprovalHandler when tool/command needs approval.
*/
public emitRequest(request: ApprovalRequest): void {
// Store sessionId mapping for later lookup when client submits response
this.approvalSessions.set(request.approvalId, request.sessionId);
this.emit('approval:request', request);
}
/**
* Emit an approval response.
* Called by API routes when user submits decision.
*/
public emitResponse(response: ApprovalResponse): void {
this.emit('approval:response', response);
// Clean up the mapping after response is emitted
this.approvalSessions.delete(response.approvalId);
}
/**
* Get the sessionId associated with an approval request.
* Used by API routes to attach sessionId to responses for SSE routing.
*/
public getSessionId(approvalId: string): string | undefined {
return this.approvalSessions.get(approvalId);
}
/**
* Subscribe to approval requests.
* Used by streaming endpoints to forward requests to SSE clients.
*
* @param handler Callback to handle approval requests
* @param options Optional AbortSignal for cleanup
*/
public onRequest(
handler: (request: ApprovalRequest) => void,
options?: { signal?: AbortSignal }
): void {
const listener = (request: ApprovalRequest) => handler(request);
this.on('approval:request', listener);
// Cleanup on abort signal
if (options?.signal) {
options.signal.addEventListener('abort', () => {
this.off('approval:request', listener);
});
}
}
/**
* Subscribe to approval responses.
* Used by ManualApprovalHandler to resolve pending approval promises.
*
* @param handler Callback to handle approval responses
* @param options Optional AbortSignal for cleanup
*/
public onResponse(
handler: (response: ApprovalResponse) => void,
options?: { signal?: AbortSignal }
): void {
const listener = (response: ApprovalResponse) => handler(response);
this.on('approval:response', listener);
// Cleanup on abort signal
if (options?.signal) {
options.signal.addEventListener('abort', () => {
this.off('approval:response', listener);
});
}
}
}

View File

@@ -0,0 +1,311 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { ApprovalRequest, ApprovalResponse } from '@dexto/core';
import { ApprovalType, ApprovalStatus, DenialReason } from '@dexto/core';
import { createManualApprovalHandler } from './manual-approval-handler.js';
import type { ApprovalCoordinator } from './approval-coordinator.js';
describe('createManualApprovalHandler', () => {
let mockCoordinator: ApprovalCoordinator;
let listeners: Map<string, ((response: ApprovalResponse) => void)[]>;
beforeEach(() => {
listeners = new Map();
mockCoordinator = {
on: vi.fn((event: string, listener: (response: ApprovalResponse) => void) => {
const eventListeners = listeners.get(event) || [];
eventListeners.push(listener);
listeners.set(event, eventListeners);
}),
off: vi.fn((event: string, listener: (response: ApprovalResponse) => void) => {
const eventListeners = listeners.get(event) || [];
const index = eventListeners.indexOf(listener);
if (index > -1) {
eventListeners.splice(index, 1);
}
}),
emitRequest: vi.fn(),
emitResponse: vi.fn(),
} as unknown as ApprovalCoordinator;
});
describe('Timeout Configuration', () => {
it('should not timeout when timeout is undefined (infinite wait)', async () => {
const handler = createManualApprovalHandler(mockCoordinator);
const request: ApprovalRequest = {
approvalId: 'test-infinite-1',
type: ApprovalType.TOOL_CONFIRMATION,
timestamp: new Date(),
// No timeout - should wait indefinitely
metadata: {
toolName: 'test_tool',
toolCallId: 'test-call-id',
args: {},
},
};
// Start the approval request (won't resolve until we emit a response)
const approvalPromise = handler(request);
// Verify the request was emitted
expect(mockCoordinator.emitRequest).toHaveBeenCalledWith(request);
// Wait a bit to ensure no timeout occurred
await new Promise((resolve) => setTimeout(resolve, 100));
// Manually resolve by emitting a response
const eventListeners = listeners.get('approval:response') || [];
eventListeners.forEach((listener) => {
listener({
approvalId: 'test-infinite-1',
status: ApprovalStatus.APPROVED,
});
});
const response = await approvalPromise;
expect(response.status).toBe(ApprovalStatus.APPROVED);
});
it('should timeout when timeout is specified', async () => {
const handler = createManualApprovalHandler(mockCoordinator);
const request: ApprovalRequest = {
approvalId: 'test-timeout-1',
type: ApprovalType.TOOL_CONFIRMATION,
timestamp: new Date(),
timeout: 50, // 50ms timeout
metadata: {
toolName: 'test_tool',
toolCallId: 'test-call-id',
args: {},
},
};
const response = await handler(request);
expect(response.status).toBe(ApprovalStatus.CANCELLED);
expect(response.reason).toBe(DenialReason.TIMEOUT);
expect(response.message).toContain('timed out');
expect(response.timeoutMs).toBe(50);
});
it('should emit timeout response to coordinator when timeout occurs', async () => {
const handler = createManualApprovalHandler(mockCoordinator);
const request: ApprovalRequest = {
approvalId: 'test-timeout-emit',
type: ApprovalType.TOOL_CONFIRMATION,
timestamp: new Date(),
timeout: 50,
metadata: {
toolName: 'test_tool',
toolCallId: 'test-call-id',
args: {},
},
};
await handler(request);
// Verify coordinator received the timeout response
expect(mockCoordinator.emitResponse).toHaveBeenCalledWith(
expect.objectContaining({
approvalId: 'test-timeout-emit',
status: ApprovalStatus.CANCELLED,
reason: DenialReason.TIMEOUT,
})
);
});
it('should clear timeout when response is received before timeout', async () => {
vi.useFakeTimers();
const handler = createManualApprovalHandler(mockCoordinator);
const request: ApprovalRequest = {
approvalId: 'test-clear-timeout',
type: ApprovalType.TOOL_CONFIRMATION,
timestamp: new Date(),
timeout: 5000, // 5 second timeout
metadata: {
toolName: 'test_tool',
toolCallId: 'test-call-id',
args: {},
},
};
const approvalPromise = handler(request);
// Emit response before timeout
const eventListeners = listeners.get('approval:response') || [];
eventListeners.forEach((listener) => {
listener({
approvalId: 'test-clear-timeout',
status: ApprovalStatus.APPROVED,
});
});
const response = await approvalPromise;
expect(response.status).toBe(ApprovalStatus.APPROVED);
// Advance time past the timeout - should not cause any issues
vi.advanceTimersByTime(6000);
vi.useRealTimers();
});
it('should handle elicitation with no timeout (infinite wait)', async () => {
const handler = createManualApprovalHandler(mockCoordinator);
const request: ApprovalRequest = {
approvalId: 'test-elicitation-infinite',
type: ApprovalType.ELICITATION,
timestamp: new Date(),
// No timeout for elicitation
metadata: {
schema: { type: 'object' as const, properties: {} },
prompt: 'Enter data',
serverName: 'TestServer',
},
};
const approvalPromise = handler(request);
// Wait briefly
await new Promise((resolve) => setTimeout(resolve, 100));
// Resolve the elicitation
const eventListeners = listeners.get('approval:response') || [];
eventListeners.forEach((listener) => {
listener({
approvalId: 'test-elicitation-infinite',
status: ApprovalStatus.APPROVED,
data: { formData: { name: 'test' } },
});
});
const response = await approvalPromise;
expect(response.status).toBe(ApprovalStatus.APPROVED);
});
});
describe('Cancellation Support', () => {
it('should support cancelling pending approvals', async () => {
const handler = createManualApprovalHandler(mockCoordinator);
const request: ApprovalRequest = {
approvalId: 'test-cancel-1',
type: ApprovalType.TOOL_CONFIRMATION,
timestamp: new Date(),
metadata: {
toolName: 'test_tool',
toolCallId: 'test-call-id',
args: {},
},
};
const approvalPromise = handler(request);
// Cancel the approval
handler.cancel?.('test-cancel-1');
const response = await approvalPromise;
expect(response.status).toBe(ApprovalStatus.CANCELLED);
expect(response.reason).toBe(DenialReason.SYSTEM_CANCELLED);
});
it('should track pending approvals', () => {
const handler = createManualApprovalHandler(mockCoordinator);
const request1: ApprovalRequest = {
approvalId: 'pending-1',
type: ApprovalType.TOOL_CONFIRMATION,
timestamp: new Date(),
metadata: { toolName: 'tool1', toolCallId: 'test-call-id-1', args: {} },
};
const request2: ApprovalRequest = {
approvalId: 'pending-2',
type: ApprovalType.TOOL_CONFIRMATION,
timestamp: new Date(),
metadata: { toolName: 'tool2', toolCallId: 'test-call-id-2', args: {} },
};
// Start both requests (don't await)
handler(request1);
handler(request2);
const pending = handler.getPending?.() || [];
expect(pending).toContain('pending-1');
expect(pending).toContain('pending-2');
});
it('should cancel all pending approvals', async () => {
const handler = createManualApprovalHandler(mockCoordinator);
const request1: ApprovalRequest = {
approvalId: 'cancel-all-1',
type: ApprovalType.TOOL_CONFIRMATION,
timestamp: new Date(),
metadata: { toolName: 'tool1', toolCallId: 'test-call-id-1', args: {} },
};
const request2: ApprovalRequest = {
approvalId: 'cancel-all-2',
type: ApprovalType.TOOL_CONFIRMATION,
timestamp: new Date(),
metadata: { toolName: 'tool2', toolCallId: 'test-call-id-2', args: {} },
};
const promise1 = handler(request1);
const promise2 = handler(request2);
// Cancel all
handler.cancelAll?.();
const [response1, response2] = await Promise.all([promise1, promise2]);
expect(response1.status).toBe(ApprovalStatus.CANCELLED);
expect(response2.status).toBe(ApprovalStatus.CANCELLED);
});
});
describe('Response Handling', () => {
it('should only handle responses for matching approvalId', async () => {
const handler = createManualApprovalHandler(mockCoordinator);
const request: ApprovalRequest = {
approvalId: 'test-match-1',
type: ApprovalType.TOOL_CONFIRMATION,
timestamp: new Date(),
metadata: { toolName: 'test_tool', toolCallId: 'test-call-id', args: {} },
};
const approvalPromise = handler(request);
// Emit response for different approvalId - should be ignored
const eventListeners = listeners.get('approval:response') || [];
eventListeners.forEach((listener) => {
listener({
approvalId: 'different-id',
status: ApprovalStatus.APPROVED,
});
});
// Wait a bit - request should still be pending
await new Promise((resolve) => setTimeout(resolve, 50));
// Now emit correct response
eventListeners.forEach((listener) => {
listener({
approvalId: 'test-match-1',
status: ApprovalStatus.DENIED,
reason: DenialReason.USER_DENIED,
});
});
const response = await approvalPromise;
expect(response.status).toBe(ApprovalStatus.DENIED);
expect(response.reason).toBe(DenialReason.USER_DENIED);
});
});
});

View 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;
}