aegis-bridge 2.4.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.js +11 -4
- package/dist/events.d.ts +2 -0
- package/dist/events.js +21 -2
- package/dist/hooks.js +21 -19
- package/dist/monitor.js +1 -1
- package/dist/server.js +66 -261
- package/dist/session.js +14 -8
- package/dist/terminal-parser.js +3 -1
- package/dist/tmux.js +1 -1
- package/dist/validation.d.ts +34 -2
- package/dist/validation.js +20 -3
- package/dist/ws-terminal.js +4 -2
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -146,12 +146,19 @@ function applyEnvOverrides(config) {
|
|
|
146
146
|
case 'tgAllowedUsers':
|
|
147
147
|
config[key] = value.split(',').map(s => Number(s.trim())).filter(n => !isNaN(n) && n > 0);
|
|
148
148
|
break;
|
|
149
|
+
// All remaining env-mapped keys are string-typed — assign directly.
|
|
150
|
+
case 'host':
|
|
151
|
+
case 'authToken':
|
|
152
|
+
case 'tmuxSession':
|
|
153
|
+
case 'stateDir':
|
|
154
|
+
case 'claudeProjectsDir':
|
|
155
|
+
case 'tgBotToken':
|
|
156
|
+
case 'tgGroupId':
|
|
157
|
+
config[key] = value;
|
|
158
|
+
break;
|
|
149
159
|
default:
|
|
150
160
|
// Skip complex types (Record<string,string>) that can't be set from a single env var
|
|
151
|
-
|
|
152
|
-
break;
|
|
153
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
154
|
-
config[key] = value;
|
|
161
|
+
break;
|
|
155
162
|
}
|
|
156
163
|
}
|
|
157
164
|
return config;
|
package/dist/events.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export declare class SessionEventBus {
|
|
|
74
74
|
subscriberCount(sessionId: string): number;
|
|
75
75
|
/** Global emitter for aggregating events across all sessions. */
|
|
76
76
|
private globalEmitter;
|
|
77
|
+
/** #689: Pending setImmediate timers for cleanup on destroy. */
|
|
78
|
+
private pendingTimers;
|
|
77
79
|
/** Subscribe to events from ALL sessions (new and existing). Returns unsubscribe function. */
|
|
78
80
|
subscribeGlobal(handler: (event: GlobalSSEEvent) => void): () => void;
|
|
79
81
|
/** Emit a session created event to global subscribers. */
|
package/dist/events.js
CHANGED
|
@@ -98,7 +98,11 @@ export class SessionEventBus {
|
|
|
98
98
|
}
|
|
99
99
|
const emitter = this.emitters.get(sessionId);
|
|
100
100
|
if (emitter) {
|
|
101
|
-
setImmediate(() =>
|
|
101
|
+
const imm = setImmediate(() => {
|
|
102
|
+
this.pendingTimers.delete(imm);
|
|
103
|
+
emitter.emit('event', event);
|
|
104
|
+
});
|
|
105
|
+
this.pendingTimers.add(imm);
|
|
102
106
|
}
|
|
103
107
|
// Forward to global subscribers
|
|
104
108
|
if (this.globalEmitter) {
|
|
@@ -108,7 +112,11 @@ export class SessionEventBus {
|
|
|
108
112
|
if (this.globalEventBuffer.length > SessionEventBus.BUFFER_SIZE) {
|
|
109
113
|
this.globalEventBuffer.splice(0, this.globalEventBuffer.length - SessionEventBus.BUFFER_SIZE);
|
|
110
114
|
}
|
|
111
|
-
setImmediate(() =>
|
|
115
|
+
const imm = setImmediate(() => {
|
|
116
|
+
this.pendingTimers.delete(imm);
|
|
117
|
+
this.globalEmitter?.emit('event', globalEvent);
|
|
118
|
+
});
|
|
119
|
+
this.pendingTimers.add(imm);
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
122
|
/** Get events emitted after the given event ID for a session. */
|
|
@@ -217,6 +225,8 @@ export class SessionEventBus {
|
|
|
217
225
|
// ── Global (all-session) SSE ──────────────────────────────────────
|
|
218
226
|
/** Global emitter for aggregating events across all sessions. */
|
|
219
227
|
globalEmitter = null;
|
|
228
|
+
/** #689: Pending setImmediate timers for cleanup on destroy. */
|
|
229
|
+
pendingTimers = new Set();
|
|
220
230
|
/** Subscribe to events from ALL sessions (new and existing). Returns unsubscribe function. */
|
|
221
231
|
subscribeGlobal(handler) {
|
|
222
232
|
if (!this.globalEmitter) {
|
|
@@ -226,6 +236,10 @@ export class SessionEventBus {
|
|
|
226
236
|
this.globalEmitter.on('event', handler);
|
|
227
237
|
return () => {
|
|
228
238
|
this.globalEmitter?.off('event', handler);
|
|
239
|
+
// #689: Nullify globalEmitter when all subscribers leave
|
|
240
|
+
if (this.globalEmitter && this.globalEmitter.listenerCount('event') === 0) {
|
|
241
|
+
this.globalEmitter = null;
|
|
242
|
+
}
|
|
229
243
|
};
|
|
230
244
|
}
|
|
231
245
|
/** Emit a session created event to global subscribers. */
|
|
@@ -262,6 +276,11 @@ export class SessionEventBus {
|
|
|
262
276
|
}
|
|
263
277
|
/** Clean up all emitters. */
|
|
264
278
|
destroy() {
|
|
279
|
+
// #689: Clear pending setImmediate timers before removing listeners
|
|
280
|
+
for (const imm of this.pendingTimers) {
|
|
281
|
+
clearImmediate(imm);
|
|
282
|
+
}
|
|
283
|
+
this.pendingTimers.clear();
|
|
265
284
|
for (const emitter of this.emitters.values()) {
|
|
266
285
|
emitter.removeAllListeners();
|
|
267
286
|
}
|
package/dist/hooks.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Issue #169: Phase 1 — HTTP hooks infrastructure.
|
|
15
15
|
* Issue #169: Phase 3 — Hook-driven status detection.
|
|
16
16
|
*/
|
|
17
|
-
import { isValidUUID } from './validation.js';
|
|
17
|
+
import { isValidUUID, hookBodySchema } from './validation.js';
|
|
18
18
|
/** CC hook events that require a decision response. */
|
|
19
19
|
const DECISION_EVENTS = new Set(['PreToolUse', 'PermissionRequest']);
|
|
20
20
|
/** Permission modes that should be auto-approved via hook response. */
|
|
@@ -114,10 +114,15 @@ export function registerHookRoutes(app, deps) {
|
|
|
114
114
|
if (!session) {
|
|
115
115
|
return reply.status(404).send({ error: `Session ${sessionId} not found` });
|
|
116
116
|
}
|
|
117
|
+
// Issue #665: Validate hook body with Zod instead of unsafe casts
|
|
118
|
+
const parseResult = hookBodySchema.safeParse(req.body);
|
|
119
|
+
if (!parseResult.success) {
|
|
120
|
+
return reply.status(400).send({ error: `Invalid hook body: ${parseResult.error.message}` });
|
|
121
|
+
}
|
|
122
|
+
const hookBody = parseResult.data;
|
|
117
123
|
// Issue #88: Track active subagents
|
|
118
|
-
const hookBody = req.body;
|
|
119
124
|
if (eventName === 'SubagentStart') {
|
|
120
|
-
const agentName = hookBody
|
|
125
|
+
const agentName = hookBody.agent_name || hookBody.tool_input?.command || 'unknown';
|
|
121
126
|
deps.sessions.addSubagent(sessionId, agentName);
|
|
122
127
|
deps.eventBus.emit(sessionId, {
|
|
123
128
|
event: 'subagent_start',
|
|
@@ -127,7 +132,7 @@ export function registerHookRoutes(app, deps) {
|
|
|
127
132
|
});
|
|
128
133
|
}
|
|
129
134
|
else if (eventName === 'SubagentStop') {
|
|
130
|
-
const agentName = hookBody
|
|
135
|
+
const agentName = hookBody.agent_name || 'unknown';
|
|
131
136
|
deps.sessions.removeSubagent(sessionId, agentName);
|
|
132
137
|
deps.eventBus.emit(sessionId, {
|
|
133
138
|
event: 'subagent_stop',
|
|
@@ -149,16 +154,15 @@ export function registerHookRoutes(app, deps) {
|
|
|
149
154
|
if (eventName === 'PreCompact' || eventName === 'PostCompact') {
|
|
150
155
|
session.lastActivity = Date.now();
|
|
151
156
|
}
|
|
152
|
-
// Forward the
|
|
153
|
-
deps.eventBus.emitHook(sessionId, eventName,
|
|
157
|
+
// Forward the validated hook event to SSE subscribers
|
|
158
|
+
deps.eventBus.emitHook(sessionId, eventName, hookBody);
|
|
154
159
|
// Issue #89 L25: Capture model field from hook payload for dashboard display
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
deps.sessions.updateSessionModel(sessionId, hookPayload.model);
|
|
160
|
+
if (hookBody.model) {
|
|
161
|
+
deps.sessions.updateSessionModel(sessionId, hookBody.model);
|
|
158
162
|
}
|
|
159
163
|
// Issue #89 L24: Validate permission_mode from PermissionRequest hook
|
|
160
164
|
if (eventName === 'PermissionRequest') {
|
|
161
|
-
const rawMode = hookBody
|
|
165
|
+
const rawMode = hookBody.permission_mode;
|
|
162
166
|
if (rawMode !== undefined && !VALID_PERMISSION_MODES.has(rawMode)) {
|
|
163
167
|
console.warn(`Hooks: invalid permission_mode "${rawMode}" from PermissionRequest, using "default"`);
|
|
164
168
|
hookBody.permission_mode = 'default';
|
|
@@ -167,8 +171,8 @@ export function registerHookRoutes(app, deps) {
|
|
|
167
171
|
// Issue #169 Phase 3: Update session status from hook event
|
|
168
172
|
// Issue #87: Extract timestamp from hook payload for latency calculation
|
|
169
173
|
const hookReceivedAt = Date.now();
|
|
170
|
-
const hookEventTimestamp =
|
|
171
|
-
? new Date(
|
|
174
|
+
const hookEventTimestamp = hookBody.timestamp
|
|
175
|
+
? new Date(hookBody.timestamp).getTime()
|
|
172
176
|
: undefined;
|
|
173
177
|
// Issue #87: Record hook latency if we have a timestamp from the payload
|
|
174
178
|
if (hookEventTimestamp && deps.metrics) {
|
|
@@ -202,22 +206,20 @@ export function registerHookRoutes(app, deps) {
|
|
|
202
206
|
deps.eventBus.emitStatus(sessionId, 'working', 'Elicitation result received (hook: ElicitationResult)');
|
|
203
207
|
break;
|
|
204
208
|
case 'PermissionRequest':
|
|
205
|
-
deps.eventBus.emitApproval(sessionId,
|
|
206
|
-
|| 'Permission requested (hook)');
|
|
209
|
+
deps.eventBus.emitApproval(sessionId, hookBody.permission_prompt || 'Permission requested (hook)');
|
|
207
210
|
break;
|
|
208
211
|
}
|
|
209
212
|
}
|
|
210
213
|
// Decision events need a response body that CC uses
|
|
211
214
|
// Format: { hookSpecificOutput: { hookEventName, permissionDecision, reason? } }
|
|
212
215
|
if (DECISION_EVENTS.has(eventName)) {
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
const permissionPrompt = hookBody?.permission_prompt || '';
|
|
216
|
+
const toolName = hookBody.tool_name || '';
|
|
217
|
+
const permissionPrompt = hookBody.permission_prompt || '';
|
|
216
218
|
if (eventName === 'PreToolUse') {
|
|
217
219
|
// Issue #336: Intercept AskUserQuestion for headless question answering
|
|
218
220
|
if (toolName === 'AskUserQuestion') {
|
|
219
|
-
const toolInput = hookBody
|
|
220
|
-
const toolUseId = hookBody
|
|
221
|
+
const toolInput = hookBody.tool_input;
|
|
222
|
+
const toolUseId = hookBody.tool_use_id || '';
|
|
221
223
|
const questionText = extractQuestionText(toolInput);
|
|
222
224
|
// Emit ask_question SSE event for external clients
|
|
223
225
|
deps.eventBus.emit(sessionId, {
|
package/dist/monitor.js
CHANGED
|
@@ -234,7 +234,7 @@ export class SessionMonitor {
|
|
|
234
234
|
await this.channels.statusChange(this.makePayload('status.permission_timeout', session, detail));
|
|
235
235
|
}
|
|
236
236
|
catch (e) {
|
|
237
|
-
console.error(`Monitor: auto-reject failed for session ${session.id}: ${e.message}`);
|
|
237
|
+
console.error(`Monitor: auto-reject failed for session ${session.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
}
|
package/dist/server.js
CHANGED
|
@@ -236,11 +236,11 @@ const createSessionSchema = z.object({
|
|
|
236
236
|
claudeCommand: z.string().max(10_000).optional(),
|
|
237
237
|
env: z.record(z.string(), z.string()).optional(),
|
|
238
238
|
stallThresholdMs: z.number().int().positive().max(3_600_000).optional(),
|
|
239
|
-
permissionMode: z.enum(['default', 'bypassPermissions', 'plan']).optional(),
|
|
239
|
+
permissionMode: z.enum(['default', 'bypassPermissions', 'plan', 'acceptEdits', 'dontAsk', 'auto']).optional(),
|
|
240
240
|
autoApprove: z.boolean().optional(),
|
|
241
241
|
}).strict();
|
|
242
242
|
// Health — Issue #397: includes tmux server health check
|
|
243
|
-
|
|
243
|
+
async function healthHandler() {
|
|
244
244
|
const pkg = await import('../package.json', { with: { type: 'json' } });
|
|
245
245
|
const activeCount = sessions.listSessions().length;
|
|
246
246
|
const totalCount = metrics.getTotalSessionsCreated();
|
|
@@ -254,23 +254,9 @@ app.get('/v1/health', async () => {
|
|
|
254
254
|
tmux: tmuxHealth,
|
|
255
255
|
timestamp: new Date().toISOString(),
|
|
256
256
|
};
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
app.get('/health',
|
|
260
|
-
const pkg = await import('../package.json', { with: { type: 'json' } });
|
|
261
|
-
const activeCount = sessions.listSessions().length;
|
|
262
|
-
const totalCount = metrics.getTotalSessionsCreated();
|
|
263
|
-
const tmuxHealth = await tmux.isServerHealthy();
|
|
264
|
-
const status = tmuxHealth.healthy ? 'ok' : 'degraded';
|
|
265
|
-
return {
|
|
266
|
-
status,
|
|
267
|
-
version: pkg.default.version,
|
|
268
|
-
uptime: process.uptime(),
|
|
269
|
-
sessions: { active: activeCount, total: totalCount },
|
|
270
|
-
tmux: tmuxHealth,
|
|
271
|
-
timestamp: new Date().toISOString(),
|
|
272
|
-
};
|
|
273
|
-
});
|
|
257
|
+
}
|
|
258
|
+
app.get('/v1/health', healthHandler);
|
|
259
|
+
app.get('/health', healthHandler);
|
|
274
260
|
// Issue #81: Swarm awareness — list all detected CC swarms and their teammates
|
|
275
261
|
app.get('/v1/swarm', async () => {
|
|
276
262
|
const result = await swarmMonitor.scan();
|
|
@@ -335,7 +321,7 @@ app.get('/v1/sessions/:id/metrics', async (req, reply) => {
|
|
|
335
321
|
// Issue #89 L14: Webhook dead letter queue
|
|
336
322
|
app.get('/v1/webhooks/dead-letter', async () => {
|
|
337
323
|
for (const ch of channels.getChannels()) {
|
|
338
|
-
if (ch.name === 'webhook' &&
|
|
324
|
+
if (ch.name === 'webhook' && typeof ch.getDeadLetterQueue === 'function') {
|
|
339
325
|
return ch.getDeadLetterQueue();
|
|
340
326
|
}
|
|
341
327
|
}
|
|
@@ -457,7 +443,7 @@ app.get('/sessions', async () => sessions.listSessions());
|
|
|
457
443
|
/** Validate workDir — delegates to validation.ts (Issue #435). */
|
|
458
444
|
const validateWorkDirWithConfig = (workDir) => validateWorkDir(workDir, config.allowedWorkDirs);
|
|
459
445
|
// Create session (Issue #607: reuse idle session for same workDir)
|
|
460
|
-
|
|
446
|
+
async function createSessionHandler(req, reply) {
|
|
461
447
|
const parsed = createSessionSchema.safeParse(req.body);
|
|
462
448
|
if (!parsed.success) {
|
|
463
449
|
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
@@ -508,58 +494,18 @@ app.post('/v1/sessions', async (req, reply) => {
|
|
|
508
494
|
console.timeEnd("POST_SEND_INITIAL_PROMPT");
|
|
509
495
|
}
|
|
510
496
|
return reply.status(201).send({ ...session, promptDelivery });
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
app.post('/sessions',
|
|
514
|
-
const parsed = createSessionSchema.safeParse(req.body);
|
|
515
|
-
if (!parsed.success) {
|
|
516
|
-
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
517
|
-
}
|
|
518
|
-
const { workDir, name, prompt, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove } = parsed.data;
|
|
519
|
-
if (!workDir)
|
|
520
|
-
return reply.status(400).send({ error: 'workDir is required' });
|
|
521
|
-
const safeWorkDir = await validateWorkDirWithConfig(workDir);
|
|
522
|
-
if (typeof safeWorkDir === 'object')
|
|
523
|
-
return reply.status(400).send({ error: safeWorkDir.error, code: safeWorkDir.code });
|
|
524
|
-
// Issue #607: Check for an existing idle session with the same workDir
|
|
525
|
-
const existing = sessions.findIdleSessionByWorkDir(safeWorkDir);
|
|
526
|
-
if (existing) {
|
|
527
|
-
let promptDelivery;
|
|
528
|
-
if (prompt) {
|
|
529
|
-
promptDelivery = await sessions.sendInitialPrompt(existing.id, prompt);
|
|
530
|
-
metrics.promptSent(promptDelivery.delivered);
|
|
531
|
-
}
|
|
532
|
-
return reply.status(200).send({ ...existing, reused: true, promptDelivery });
|
|
533
|
-
}
|
|
534
|
-
const session = await sessions.createSession({ workDir: safeWorkDir, name, resumeSessionId, claudeCommand, env, stallThresholdMs, permissionMode, autoApprove });
|
|
535
|
-
// Issue #46: Topic first, then prompt (same fix as v1 route)
|
|
536
|
-
await channels.sessionCreated({
|
|
537
|
-
event: 'session.created',
|
|
538
|
-
timestamp: new Date().toISOString(),
|
|
539
|
-
session: { id: session.id, name: session.windowName, workDir },
|
|
540
|
-
detail: `Session created: ${session.windowName}`,
|
|
541
|
-
meta: prompt ? { prompt: prompt.slice(0, 200), permissionMode: permissionMode ?? (autoApprove ? 'bypassPermissions' : undefined) } : undefined,
|
|
542
|
-
});
|
|
543
|
-
let promptDelivery;
|
|
544
|
-
if (prompt) {
|
|
545
|
-
promptDelivery = await sessions.sendInitialPrompt(session.id, prompt);
|
|
546
|
-
metrics.promptSent(promptDelivery.delivered);
|
|
547
|
-
}
|
|
548
|
-
return reply.status(201).send({ ...session, promptDelivery });
|
|
549
|
-
});
|
|
497
|
+
}
|
|
498
|
+
app.post('/v1/sessions', createSessionHandler);
|
|
499
|
+
app.post('/sessions', createSessionHandler);
|
|
550
500
|
// Get session (Issue #20: includes actionHints for interactive states)
|
|
551
|
-
|
|
501
|
+
async function getSessionHandler(req, reply) {
|
|
552
502
|
const session = sessions.getSession(req.params.id);
|
|
553
503
|
if (!session)
|
|
554
504
|
return reply.status(404).send({ error: 'Session not found' });
|
|
555
505
|
return addActionHints(session, sessions);
|
|
556
|
-
}
|
|
557
|
-
app.get('/sessions/:id',
|
|
558
|
-
|
|
559
|
-
if (!session)
|
|
560
|
-
return reply.status(404).send({ error: 'Session not found' });
|
|
561
|
-
return addActionHints(session, sessions);
|
|
562
|
-
});
|
|
506
|
+
}
|
|
507
|
+
app.get('/v1/sessions/:id', getSessionHandler);
|
|
508
|
+
app.get('/sessions/:id', getSessionHandler);
|
|
563
509
|
// #128: Bulk health check — returns health for all sessions in one request
|
|
564
510
|
app.get('/v1/sessions/health', async () => {
|
|
565
511
|
const allSessions = sessions.listSessions();
|
|
@@ -580,43 +526,18 @@ app.get('/v1/sessions/health', async () => {
|
|
|
580
526
|
return results;
|
|
581
527
|
});
|
|
582
528
|
// Session health check (Issue #2)
|
|
583
|
-
|
|
529
|
+
async function sessionHealthHandler(req, reply) {
|
|
584
530
|
try {
|
|
585
531
|
return await sessions.getHealth(req.params.id);
|
|
586
532
|
}
|
|
587
533
|
catch (e) {
|
|
588
534
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
589
535
|
}
|
|
590
|
-
}
|
|
591
|
-
app.get('/sessions/:id/health',
|
|
592
|
-
|
|
593
|
-
return await sessions.getHealth(req.params.id);
|
|
594
|
-
}
|
|
595
|
-
catch (e) {
|
|
596
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
597
|
-
}
|
|
598
|
-
});
|
|
536
|
+
}
|
|
537
|
+
app.get('/v1/sessions/:id/health', sessionHealthHandler);
|
|
538
|
+
app.get('/sessions/:id/health', sessionHealthHandler);
|
|
599
539
|
// Send message (with delivery verification — Issue #1)
|
|
600
|
-
|
|
601
|
-
const parsed = sendMessageSchema.safeParse(req.body);
|
|
602
|
-
if (!parsed.success)
|
|
603
|
-
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
604
|
-
const { text } = parsed.data;
|
|
605
|
-
try {
|
|
606
|
-
const result = await sessions.sendMessage(req.params.id, text);
|
|
607
|
-
await channels.message({
|
|
608
|
-
event: 'message.user',
|
|
609
|
-
timestamp: new Date().toISOString(),
|
|
610
|
-
session: { id: req.params.id, name: '', workDir: '' },
|
|
611
|
-
detail: text,
|
|
612
|
-
});
|
|
613
|
-
return { ok: true, delivered: result.delivered, attempts: result.attempts };
|
|
614
|
-
}
|
|
615
|
-
catch (e) {
|
|
616
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
617
|
-
}
|
|
618
|
-
});
|
|
619
|
-
app.post('/sessions/:id/send', async (req, reply) => {
|
|
540
|
+
async function sendMessageHandler(req, reply) {
|
|
620
541
|
const parsed = sendMessageSchema.safeParse(req.body);
|
|
621
542
|
if (!parsed.success)
|
|
622
543
|
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
@@ -634,26 +555,22 @@ app.post('/sessions/:id/send', async (req, reply) => {
|
|
|
634
555
|
catch (e) {
|
|
635
556
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
636
557
|
}
|
|
637
|
-
}
|
|
558
|
+
}
|
|
559
|
+
app.post('/v1/sessions/:id/send', sendMessageHandler);
|
|
560
|
+
app.post('/sessions/:id/send', sendMessageHandler);
|
|
638
561
|
// Read messages
|
|
639
|
-
|
|
562
|
+
async function readMessagesHandler(req, reply) {
|
|
640
563
|
try {
|
|
641
564
|
return await sessions.readMessages(req.params.id);
|
|
642
565
|
}
|
|
643
566
|
catch (e) {
|
|
644
567
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
645
568
|
}
|
|
646
|
-
}
|
|
647
|
-
app.get('/sessions/:id/read',
|
|
648
|
-
|
|
649
|
-
return await sessions.readMessages(req.params.id);
|
|
650
|
-
}
|
|
651
|
-
catch (e) {
|
|
652
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
653
|
-
}
|
|
654
|
-
});
|
|
569
|
+
}
|
|
570
|
+
app.get('/v1/sessions/:id/read', readMessagesHandler);
|
|
571
|
+
app.get('/sessions/:id/read', readMessagesHandler);
|
|
655
572
|
// Approve
|
|
656
|
-
|
|
573
|
+
async function approveHandler(req, reply) {
|
|
657
574
|
try {
|
|
658
575
|
await sessions.approve(req.params.id);
|
|
659
576
|
// Issue #87: Record permission response latency
|
|
@@ -666,22 +583,11 @@ app.post('/v1/sessions/:id/approve', async (req, reply) => {
|
|
|
666
583
|
catch (e) {
|
|
667
584
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
668
585
|
}
|
|
669
|
-
}
|
|
670
|
-
app.post('/sessions/:id/approve',
|
|
671
|
-
|
|
672
|
-
await sessions.approve(req.params.id);
|
|
673
|
-
const lat = sessions.getLatencyMetrics(req.params.id);
|
|
674
|
-
if (lat !== null && lat.permission_response_ms !== null) {
|
|
675
|
-
metrics.recordPermissionResponse(req.params.id, lat.permission_response_ms);
|
|
676
|
-
}
|
|
677
|
-
return { ok: true };
|
|
678
|
-
}
|
|
679
|
-
catch (e) {
|
|
680
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
681
|
-
}
|
|
682
|
-
});
|
|
586
|
+
}
|
|
587
|
+
app.post('/v1/sessions/:id/approve', approveHandler);
|
|
588
|
+
app.post('/sessions/:id/approve', approveHandler);
|
|
683
589
|
// Reject
|
|
684
|
-
|
|
590
|
+
async function rejectHandler(req, reply) {
|
|
685
591
|
try {
|
|
686
592
|
await sessions.reject(req.params.id);
|
|
687
593
|
const lat = sessions.getLatencyMetrics(req.params.id);
|
|
@@ -693,20 +599,9 @@ app.post('/v1/sessions/:id/reject', async (req, reply) => {
|
|
|
693
599
|
catch (e) {
|
|
694
600
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
695
601
|
}
|
|
696
|
-
}
|
|
697
|
-
app.post('/sessions/:id/reject',
|
|
698
|
-
|
|
699
|
-
await sessions.reject(req.params.id);
|
|
700
|
-
const lat = sessions.getLatencyMetrics(req.params.id);
|
|
701
|
-
if (lat !== null && lat.permission_response_ms !== null) {
|
|
702
|
-
metrics.recordPermissionResponse(req.params.id, lat.permission_response_ms);
|
|
703
|
-
}
|
|
704
|
-
return { ok: true };
|
|
705
|
-
}
|
|
706
|
-
catch (e) {
|
|
707
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
708
|
-
}
|
|
709
|
-
});
|
|
602
|
+
}
|
|
603
|
+
app.post('/v1/sessions/:id/reject', rejectHandler);
|
|
604
|
+
app.post('/sessions/:id/reject', rejectHandler);
|
|
710
605
|
// Issue #336: Answer pending AskUserQuestion
|
|
711
606
|
app.post('/v1/sessions/:id/answer', async (req, reply) => {
|
|
712
607
|
const { questionId, answer } = req.body || {};
|
|
@@ -723,16 +618,7 @@ app.post('/v1/sessions/:id/answer', async (req, reply) => {
|
|
|
723
618
|
return { ok: true };
|
|
724
619
|
});
|
|
725
620
|
// Escape
|
|
726
|
-
|
|
727
|
-
try {
|
|
728
|
-
await sessions.escape(req.params.id);
|
|
729
|
-
return { ok: true };
|
|
730
|
-
}
|
|
731
|
-
catch (e) {
|
|
732
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
733
|
-
}
|
|
734
|
-
});
|
|
735
|
-
app.post('/sessions/:id/escape', async (req, reply) => {
|
|
621
|
+
async function escapeHandler(req, reply) {
|
|
736
622
|
try {
|
|
737
623
|
await sessions.escape(req.params.id);
|
|
738
624
|
return { ok: true };
|
|
@@ -740,9 +626,11 @@ app.post('/sessions/:id/escape', async (req, reply) => {
|
|
|
740
626
|
catch (e) {
|
|
741
627
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
742
628
|
}
|
|
743
|
-
}
|
|
629
|
+
}
|
|
630
|
+
app.post('/v1/sessions/:id/escape', escapeHandler);
|
|
631
|
+
app.post('/sessions/:id/escape', escapeHandler);
|
|
744
632
|
// Interrupt (Ctrl+C)
|
|
745
|
-
|
|
633
|
+
async function interruptHandler(req, reply) {
|
|
746
634
|
try {
|
|
747
635
|
await sessions.interrupt(req.params.id);
|
|
748
636
|
return { ok: true };
|
|
@@ -750,18 +638,11 @@ app.post('/v1/sessions/:id/interrupt', async (req, reply) => {
|
|
|
750
638
|
catch (e) {
|
|
751
639
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
752
640
|
}
|
|
753
|
-
}
|
|
754
|
-
app.post('/sessions/:id/interrupt',
|
|
755
|
-
|
|
756
|
-
await sessions.interrupt(req.params.id);
|
|
757
|
-
return { ok: true };
|
|
758
|
-
}
|
|
759
|
-
catch (e) {
|
|
760
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
761
|
-
}
|
|
762
|
-
});
|
|
641
|
+
}
|
|
642
|
+
app.post('/v1/sessions/:id/interrupt', interruptHandler);
|
|
643
|
+
app.post('/sessions/:id/interrupt', interruptHandler);
|
|
763
644
|
// Kill session
|
|
764
|
-
|
|
645
|
+
async function killSessionHandler(req, reply) {
|
|
765
646
|
if (!sessions.getSession(req.params.id)) {
|
|
766
647
|
return reply.status(404).send({ error: 'Session not found' });
|
|
767
648
|
}
|
|
@@ -776,54 +657,21 @@ app.delete('/v1/sessions/:id', async (req, reply) => {
|
|
|
776
657
|
catch (e) {
|
|
777
658
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
778
659
|
}
|
|
779
|
-
}
|
|
780
|
-
app.delete('/sessions/:id',
|
|
781
|
-
|
|
782
|
-
return reply.status(404).send({ error: 'Session not found' });
|
|
783
|
-
}
|
|
784
|
-
try {
|
|
785
|
-
eventBus.emitEnded(req.params.id, 'killed');
|
|
786
|
-
await channels.sessionEnded(makePayload('session.ended', req.params.id, 'killed'));
|
|
787
|
-
await sessions.killSession(req.params.id);
|
|
788
|
-
monitor.removeSession(req.params.id);
|
|
789
|
-
metrics.cleanupSession(req.params.id);
|
|
790
|
-
return { ok: true };
|
|
791
|
-
}
|
|
792
|
-
catch (e) {
|
|
793
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
794
|
-
}
|
|
795
|
-
});
|
|
660
|
+
}
|
|
661
|
+
app.delete('/v1/sessions/:id', killSessionHandler);
|
|
662
|
+
app.delete('/sessions/:id', killSessionHandler);
|
|
796
663
|
// Capture raw pane
|
|
797
|
-
|
|
664
|
+
async function capturePaneHandler(req, reply) {
|
|
798
665
|
const session = sessions.getSession(req.params.id);
|
|
799
666
|
if (!session)
|
|
800
667
|
return reply.status(404).send({ error: 'Session not found' });
|
|
801
668
|
const pane = await tmux.capturePane(session.windowId);
|
|
802
669
|
return { pane };
|
|
803
|
-
}
|
|
804
|
-
app.get('/sessions/:id/pane',
|
|
805
|
-
|
|
806
|
-
if (!session)
|
|
807
|
-
return reply.status(404).send({ error: 'Session not found' });
|
|
808
|
-
const pane = await tmux.capturePane(session.windowId);
|
|
809
|
-
return { pane };
|
|
810
|
-
});
|
|
670
|
+
}
|
|
671
|
+
app.get('/v1/sessions/:id/pane', capturePaneHandler);
|
|
672
|
+
app.get('/sessions/:id/pane', capturePaneHandler);
|
|
811
673
|
// Slash command
|
|
812
|
-
|
|
813
|
-
const parsed = commandSchema.safeParse(req.body);
|
|
814
|
-
if (!parsed.success)
|
|
815
|
-
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
816
|
-
const { command } = parsed.data;
|
|
817
|
-
try {
|
|
818
|
-
const cmd = command.startsWith('/') ? command : `/${command}`;
|
|
819
|
-
await sessions.sendMessage(req.params.id, cmd);
|
|
820
|
-
return { ok: true };
|
|
821
|
-
}
|
|
822
|
-
catch (e) {
|
|
823
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
824
|
-
}
|
|
825
|
-
});
|
|
826
|
-
app.post('/sessions/:id/command', async (req, reply) => {
|
|
674
|
+
async function commandHandler(req, reply) {
|
|
827
675
|
const parsed = commandSchema.safeParse(req.body);
|
|
828
676
|
if (!parsed.success)
|
|
829
677
|
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
@@ -836,23 +684,11 @@ app.post('/sessions/:id/command', async (req, reply) => {
|
|
|
836
684
|
catch (e) {
|
|
837
685
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
838
686
|
}
|
|
839
|
-
}
|
|
687
|
+
}
|
|
688
|
+
app.post('/v1/sessions/:id/command', commandHandler);
|
|
689
|
+
app.post('/sessions/:id/command', commandHandler);
|
|
840
690
|
// Bash mode
|
|
841
|
-
|
|
842
|
-
const parsed = bashSchema.safeParse(req.body);
|
|
843
|
-
if (!parsed.success)
|
|
844
|
-
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
845
|
-
const { command } = parsed.data;
|
|
846
|
-
try {
|
|
847
|
-
const cmd = command.startsWith('!') ? command : `!${command}`;
|
|
848
|
-
await sessions.sendMessage(req.params.id, cmd);
|
|
849
|
-
return { ok: true };
|
|
850
|
-
}
|
|
851
|
-
catch (e) {
|
|
852
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
853
|
-
}
|
|
854
|
-
});
|
|
855
|
-
app.post('/sessions/:id/bash', async (req, reply) => {
|
|
691
|
+
async function bashHandler(req, reply) {
|
|
856
692
|
const parsed = bashSchema.safeParse(req.body);
|
|
857
693
|
if (!parsed.success)
|
|
858
694
|
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
@@ -865,24 +701,20 @@ app.post('/sessions/:id/bash', async (req, reply) => {
|
|
|
865
701
|
catch (e) {
|
|
866
702
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
867
703
|
}
|
|
868
|
-
}
|
|
704
|
+
}
|
|
705
|
+
app.post('/v1/sessions/:id/bash', bashHandler);
|
|
706
|
+
app.post('/sessions/:id/bash', bashHandler);
|
|
869
707
|
// Session summary (Issue #35)
|
|
870
|
-
|
|
871
|
-
try {
|
|
872
|
-
return await sessions.getSummary(req.params.id);
|
|
873
|
-
}
|
|
874
|
-
catch (e) {
|
|
875
|
-
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
876
|
-
}
|
|
877
|
-
});
|
|
878
|
-
app.get('/sessions/:id/summary', async (req, reply) => {
|
|
708
|
+
async function summaryHandler(req, reply) {
|
|
879
709
|
try {
|
|
880
710
|
return await sessions.getSummary(req.params.id);
|
|
881
711
|
}
|
|
882
712
|
catch (e) {
|
|
883
713
|
return reply.status(404).send({ error: e instanceof Error ? e.message : String(e) });
|
|
884
714
|
}
|
|
885
|
-
}
|
|
715
|
+
}
|
|
716
|
+
app.get('/v1/sessions/:id/summary', summaryHandler);
|
|
717
|
+
app.get('/sessions/:id/summary', summaryHandler);
|
|
886
718
|
// Paginated transcript read
|
|
887
719
|
app.get('/v1/sessions/:id/transcript', async (req, reply) => {
|
|
888
720
|
try {
|
|
@@ -896,7 +728,7 @@ app.get('/v1/sessions/:id/transcript', async (req, reply) => {
|
|
|
896
728
|
}
|
|
897
729
|
});
|
|
898
730
|
// Screenshot capture (Issue #22)
|
|
899
|
-
|
|
731
|
+
async function screenshotHandler(req, reply) {
|
|
900
732
|
const parsed = screenshotSchema.safeParse(req.body);
|
|
901
733
|
if (!parsed.success)
|
|
902
734
|
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
@@ -925,36 +757,9 @@ app.post('/v1/sessions/:id/screenshot', async (req, reply) => {
|
|
|
925
757
|
catch (e) {
|
|
926
758
|
return reply.status(500).send({ error: `Screenshot failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
927
759
|
}
|
|
928
|
-
}
|
|
929
|
-
app.post('/sessions/:id/screenshot',
|
|
930
|
-
|
|
931
|
-
if (!parsed.success)
|
|
932
|
-
return reply.status(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
933
|
-
const { url, fullPage, width, height } = parsed.data;
|
|
934
|
-
const urlError = validateScreenshotUrl(url);
|
|
935
|
-
if (urlError)
|
|
936
|
-
return reply.status(400).send({ error: urlError });
|
|
937
|
-
// Post-DNS-resolution check: resolve hostname and reject private IPs
|
|
938
|
-
const dnsError = await resolveAndCheckIp(new URL(url).hostname);
|
|
939
|
-
if (dnsError)
|
|
940
|
-
return reply.status(400).send({ error: dnsError });
|
|
941
|
-
const session = sessions.getSession(req.params.id);
|
|
942
|
-
if (!session)
|
|
943
|
-
return reply.status(404).send({ error: 'Session not found' });
|
|
944
|
-
if (!isPlaywrightAvailable()) {
|
|
945
|
-
return reply.status(501).send({
|
|
946
|
-
error: 'Playwright is not installed',
|
|
947
|
-
message: 'Install Playwright to enable screenshots: npx playwright install chromium && npm install -D playwright',
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
try {
|
|
951
|
-
const result = await captureScreenshot({ url, fullPage, width, height });
|
|
952
|
-
return reply.status(200).send(result);
|
|
953
|
-
}
|
|
954
|
-
catch (e) {
|
|
955
|
-
return reply.status(500).send({ error: `Screenshot failed: ${e instanceof Error ? e.message : String(e)}` });
|
|
956
|
-
}
|
|
957
|
-
});
|
|
760
|
+
}
|
|
761
|
+
app.post('/v1/sessions/:id/screenshot', screenshotHandler);
|
|
762
|
+
app.post('/sessions/:id/screenshot', screenshotHandler);
|
|
958
763
|
// SSE event stream (Issue #32)
|
|
959
764
|
app.get('/v1/sessions/:id/events', async (req, reply) => {
|
|
960
765
|
const session = sessions.getSession(req.params.id);
|
package/dist/session.js
CHANGED
|
@@ -14,6 +14,18 @@ import { computeStallThreshold } from './config.js';
|
|
|
14
14
|
import { neutralizeBypassPermissions, restoreSettings, cleanOrphanedBackup } from './permission-guard.js';
|
|
15
15
|
import { persistedStateSchema, sessionMapSchema } from './validation.js';
|
|
16
16
|
import { writeHookSettingsFile, cleanupHookSettingsFile } from './hook-settings.js';
|
|
17
|
+
/** Convert parsed JSON arrays to Sets for activeSubagents (#668). */
|
|
18
|
+
function hydrateSessions(raw) {
|
|
19
|
+
const sessions = {};
|
|
20
|
+
for (const [id, s] of Object.entries(raw)) {
|
|
21
|
+
const { activeSubagents, ...rest } = s;
|
|
22
|
+
sessions[id] = {
|
|
23
|
+
...rest,
|
|
24
|
+
activeSubagents: activeSubagents ? new Set(activeSubagents) : undefined,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return sessions;
|
|
28
|
+
}
|
|
17
29
|
/**
|
|
18
30
|
* Detect whether CC is showing numbered permission options (e.g. "1. Yes, 2. No")
|
|
19
31
|
* vs a simple y/N prompt. Returns the approval method to use.
|
|
@@ -99,7 +111,7 @@ export class SessionManager {
|
|
|
99
111
|
const raw = await readFile(this.stateFile, 'utf-8');
|
|
100
112
|
const parsed = persistedStateSchema.safeParse(JSON.parse(raw));
|
|
101
113
|
if (parsed.success && this.isValidState({ sessions: parsed.data })) {
|
|
102
|
-
this.state = { sessions: parsed.data };
|
|
114
|
+
this.state = { sessions: hydrateSessions(parsed.data) };
|
|
103
115
|
}
|
|
104
116
|
else {
|
|
105
117
|
console.warn('State file failed validation, attempting backup restore');
|
|
@@ -110,7 +122,7 @@ export class SessionManager {
|
|
|
110
122
|
const backupRaw = await readFile(backupFile, 'utf-8');
|
|
111
123
|
const backupParsed = persistedStateSchema.safeParse(JSON.parse(backupRaw));
|
|
112
124
|
if (backupParsed.success && this.isValidState({ sessions: backupParsed.data })) {
|
|
113
|
-
this.state = { sessions: backupParsed.data };
|
|
125
|
+
this.state = { sessions: hydrateSessions(backupParsed.data) };
|
|
114
126
|
console.log('Restored state from backup');
|
|
115
127
|
}
|
|
116
128
|
else {
|
|
@@ -130,12 +142,6 @@ export class SessionManager {
|
|
|
130
142
|
this.state = { sessions: {} };
|
|
131
143
|
}
|
|
132
144
|
}
|
|
133
|
-
// #357: Convert deserialized activeSubagents arrays to Sets
|
|
134
|
-
for (const session of Object.values(this.state.sessions)) {
|
|
135
|
-
if (Array.isArray(session.activeSubagents)) {
|
|
136
|
-
session.activeSubagents = new Set(session.activeSubagents);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
145
|
// Create backup of successfully loaded state
|
|
140
146
|
try {
|
|
141
147
|
await writeFile(`${this.stateFile}.bak`, JSON.stringify(this.state, null, 2));
|
package/dist/terminal-parser.js
CHANGED
|
@@ -142,11 +142,13 @@ export function detectUIState(paneText) {
|
|
|
142
142
|
return 'waiting_for_input';
|
|
143
143
|
return 'unknown';
|
|
144
144
|
}
|
|
145
|
+
/** Number of lines from the bottom of the pane to scan for active spinners. */
|
|
146
|
+
const SPINNER_SEARCH_LINES = 30;
|
|
145
147
|
/** Check if any line in the pane has an active spinner character followed by working text. */
|
|
146
148
|
function hasSpinnerAnywhere(lines) {
|
|
147
149
|
// Only check lines in the content area (not the very bottom few which are prompt/footer)
|
|
148
150
|
const searchEnd = Math.max(0, lines.length - 3);
|
|
149
|
-
for (let i = Math.max(0, lines.length -
|
|
151
|
+
for (let i = Math.max(0, lines.length - SPINNER_SEARCH_LINES); i < searchEnd; i++) {
|
|
150
152
|
const stripped = lines[i].trim();
|
|
151
153
|
if (!stripped)
|
|
152
154
|
continue;
|
package/dist/tmux.js
CHANGED
|
@@ -39,7 +39,7 @@ export class TmuxManager {
|
|
|
39
39
|
this.socketName = socketName ?? `aegis-${process.pid}`;
|
|
40
40
|
}
|
|
41
41
|
/** Promise-chain queue that serializes all tmux CLI calls to prevent race conditions. */
|
|
42
|
-
queue = Promise.resolve(
|
|
42
|
+
queue = Promise.resolve();
|
|
43
43
|
/** #403: Counter of in-flight createWindow calls — direct methods must queue when > 0. */
|
|
44
44
|
_creatingCount = 0;
|
|
45
45
|
/** #357: Short-lived cache for window existence checks to reduce CLI calls. */
|
package/dist/validation.d.ts
CHANGED
|
@@ -40,6 +40,25 @@ export declare const webhookEndpointSchema: z.ZodObject<{
|
|
|
40
40
|
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
41
41
|
timeoutMs: z.ZodOptional<z.ZodNumber>;
|
|
42
42
|
}, z.core.$strict>;
|
|
43
|
+
/** POST /v1/hooks/:eventName — CC hook event payload (Issue #665). */
|
|
44
|
+
export declare const hookBodySchema: z.ZodObject<{
|
|
45
|
+
session_id: z.ZodOptional<z.ZodString>;
|
|
46
|
+
agent_name: z.ZodOptional<z.ZodString>;
|
|
47
|
+
agent_type: z.ZodOptional<z.ZodString>;
|
|
48
|
+
tool_name: z.ZodOptional<z.ZodString>;
|
|
49
|
+
tool_input: z.ZodOptional<z.ZodObject<{
|
|
50
|
+
command: z.ZodOptional<z.ZodString>;
|
|
51
|
+
}, z.core.$loose>>;
|
|
52
|
+
tool_use_id: z.ZodOptional<z.ZodString>;
|
|
53
|
+
permission_prompt: z.ZodOptional<z.ZodString>;
|
|
54
|
+
permission_mode: z.ZodOptional<z.ZodString>;
|
|
55
|
+
hook_event_name: z.ZodOptional<z.ZodString>;
|
|
56
|
+
model: z.ZodOptional<z.ZodString>;
|
|
57
|
+
timestamp: z.ZodOptional<z.ZodString>;
|
|
58
|
+
stop_reason: z.ZodOptional<z.ZodString>;
|
|
59
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
60
|
+
command: z.ZodOptional<z.ZodString>;
|
|
61
|
+
}, z.core.$loose>;
|
|
43
62
|
/** POST /v1/sessions/:id/hooks/permission */
|
|
44
63
|
export declare const permissionHookSchema: z.ZodObject<{
|
|
45
64
|
session_id: z.ZodOptional<z.ZodString>;
|
|
@@ -64,6 +83,9 @@ export declare const batchSessionSchema: z.ZodObject<{
|
|
|
64
83
|
default: "default";
|
|
65
84
|
bypassPermissions: "bypassPermissions";
|
|
66
85
|
plan: "plan";
|
|
86
|
+
acceptEdits: "acceptEdits";
|
|
87
|
+
dontAsk: "dontAsk";
|
|
88
|
+
auto: "auto";
|
|
67
89
|
}>>;
|
|
68
90
|
autoApprove: z.ZodOptional<z.ZodBoolean>;
|
|
69
91
|
stallThresholdMs: z.ZodOptional<z.ZodNumber>;
|
|
@@ -82,6 +104,9 @@ export declare const pipelineSchema: z.ZodObject<{
|
|
|
82
104
|
default: "default";
|
|
83
105
|
bypassPermissions: "bypassPermissions";
|
|
84
106
|
plan: "plan";
|
|
107
|
+
acceptEdits: "acceptEdits";
|
|
108
|
+
dontAsk: "dontAsk";
|
|
109
|
+
auto: "auto";
|
|
85
110
|
}>>;
|
|
86
111
|
autoApprove: z.ZodOptional<z.ZodBoolean>;
|
|
87
112
|
}, z.core.$strip>>;
|
|
@@ -104,9 +129,9 @@ export declare const persistedStateSchema: z.ZodRecord<z.ZodString, z.ZodObject<
|
|
|
104
129
|
monitorOffset: z.ZodNumber;
|
|
105
130
|
status: z.ZodEnum<{
|
|
106
131
|
unknown: "unknown";
|
|
132
|
+
permission_prompt: "permission_prompt";
|
|
107
133
|
idle: "idle";
|
|
108
134
|
working: "working";
|
|
109
|
-
permission_prompt: "permission_prompt";
|
|
110
135
|
bash_approval: "bash_approval";
|
|
111
136
|
plan_mode: "plan_mode";
|
|
112
137
|
ask_question: "ask_question";
|
|
@@ -116,7 +141,14 @@ export declare const persistedStateSchema: z.ZodRecord<z.ZodString, z.ZodObject<
|
|
|
116
141
|
lastActivity: z.ZodNumber;
|
|
117
142
|
stallThresholdMs: z.ZodNumber;
|
|
118
143
|
permissionStallMs: z.ZodDefault<z.ZodNumber>;
|
|
119
|
-
permissionMode: z.
|
|
144
|
+
permissionMode: z.ZodEnum<{
|
|
145
|
+
default: "default";
|
|
146
|
+
bypassPermissions: "bypassPermissions";
|
|
147
|
+
plan: "plan";
|
|
148
|
+
acceptEdits: "acceptEdits";
|
|
149
|
+
dontAsk: "dontAsk";
|
|
150
|
+
auto: "auto";
|
|
151
|
+
}>;
|
|
120
152
|
settingsPatched: z.ZodOptional<z.ZodBoolean>;
|
|
121
153
|
hookSettingsFile: z.ZodOptional<z.ZodString>;
|
|
122
154
|
lastHookAt: z.ZodOptional<z.ZodNumber>;
|
package/dist/validation.js
CHANGED
|
@@ -43,6 +43,23 @@ export const webhookEndpointSchema = z.object({
|
|
|
43
43
|
headers: z.record(z.string(), z.string()).optional(),
|
|
44
44
|
timeoutMs: z.number().int().positive().optional(),
|
|
45
45
|
}).strict();
|
|
46
|
+
/** POST /v1/hooks/:eventName — CC hook event payload (Issue #665). */
|
|
47
|
+
export const hookBodySchema = z.object({
|
|
48
|
+
session_id: z.string().optional(),
|
|
49
|
+
agent_name: z.string().optional(),
|
|
50
|
+
agent_type: z.string().optional(),
|
|
51
|
+
tool_name: z.string().optional(),
|
|
52
|
+
tool_input: z.object({ command: z.string().optional() }).passthrough().optional(),
|
|
53
|
+
tool_use_id: z.string().optional(),
|
|
54
|
+
permission_prompt: z.string().optional(),
|
|
55
|
+
permission_mode: z.string().optional(),
|
|
56
|
+
hook_event_name: z.string().optional(),
|
|
57
|
+
model: z.string().optional(),
|
|
58
|
+
timestamp: z.string().optional(),
|
|
59
|
+
stop_reason: z.string().optional(),
|
|
60
|
+
cwd: z.string().optional(),
|
|
61
|
+
command: z.string().optional(),
|
|
62
|
+
}).passthrough();
|
|
46
63
|
/** POST /v1/sessions/:id/hooks/permission */
|
|
47
64
|
export const permissionHookSchema = z.object({
|
|
48
65
|
session_id: z.string().optional(),
|
|
@@ -61,7 +78,7 @@ const batchSessionSpecSchema = z.object({
|
|
|
61
78
|
name: z.string().max(200).optional(),
|
|
62
79
|
workDir: z.string().min(1),
|
|
63
80
|
prompt: z.string().max(100_000).optional(),
|
|
64
|
-
permissionMode: z.enum(['default', 'bypassPermissions', 'plan']).optional(),
|
|
81
|
+
permissionMode: z.enum(['default', 'bypassPermissions', 'plan', 'acceptEdits', 'dontAsk', 'auto']).optional(),
|
|
65
82
|
autoApprove: z.boolean().optional(),
|
|
66
83
|
stallThresholdMs: z.number().int().positive().max(3_600_000).optional(),
|
|
67
84
|
});
|
|
@@ -74,7 +91,7 @@ const pipelineStageSchema = z.object({
|
|
|
74
91
|
workDir: z.string().min(1).optional(),
|
|
75
92
|
prompt: z.string().min(1).max(MAX_INPUT_LENGTH),
|
|
76
93
|
dependsOn: z.array(z.string()).optional(),
|
|
77
|
-
permissionMode: z.enum(['default', 'bypassPermissions', 'plan']).optional(),
|
|
94
|
+
permissionMode: z.enum(['default', 'bypassPermissions', 'plan', 'acceptEdits', 'dontAsk', 'auto']).optional(),
|
|
78
95
|
autoApprove: z.boolean().optional(),
|
|
79
96
|
});
|
|
80
97
|
/** POST /v1/pipelines */
|
|
@@ -122,7 +139,7 @@ export const persistedStateSchema = z.record(z.string(), z.object({
|
|
|
122
139
|
lastActivity: z.number(),
|
|
123
140
|
stallThresholdMs: z.number(),
|
|
124
141
|
permissionStallMs: z.number().default(300_000),
|
|
125
|
-
permissionMode: z.
|
|
142
|
+
permissionMode: z.enum(['default', 'bypassPermissions', 'plan', 'acceptEdits', 'dontAsk', 'auto']),
|
|
126
143
|
settingsPatched: z.boolean().optional(),
|
|
127
144
|
hookSettingsFile: z.string().optional(),
|
|
128
145
|
lastHookAt: z.number().optional(),
|
package/dist/ws-terminal.js
CHANGED
|
@@ -31,7 +31,8 @@ const sessionPolls = new Map();
|
|
|
31
31
|
/** Reset all internal state (for testing). */
|
|
32
32
|
export function _resetForTesting() {
|
|
33
33
|
for (const poll of sessionPolls.values()) {
|
|
34
|
-
|
|
34
|
+
if (poll.timer)
|
|
35
|
+
clearInterval(poll.timer);
|
|
35
36
|
}
|
|
36
37
|
sessionPolls.clear();
|
|
37
38
|
}
|
|
@@ -282,7 +283,8 @@ function evictSubscriber(sessionId, socket, sub) {
|
|
|
282
283
|
poll.subscribers.delete(socket);
|
|
283
284
|
// If no more subscribers, clean up the poll timer
|
|
284
285
|
if (poll.subscribers.size === 0) {
|
|
285
|
-
|
|
286
|
+
if (poll.timer)
|
|
287
|
+
clearInterval(poll.timer);
|
|
286
288
|
sessionPolls.delete(sessionId);
|
|
287
289
|
}
|
|
288
290
|
}
|