aegis-bridge 2.2.2
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/LICENSE +21 -0
- package/README.md +244 -0
- package/dashboard/dist/assets/index-CijFoeRu.css +32 -0
- package/dashboard/dist/assets/index-QtT4j0ht.js +262 -0
- package/dashboard/dist/index.html +14 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +219 -0
- package/dist/channels/index.d.ts +8 -0
- package/dist/channels/index.js +9 -0
- package/dist/channels/manager.d.ts +39 -0
- package/dist/channels/manager.js +101 -0
- package/dist/channels/telegram-style.d.ts +118 -0
- package/dist/channels/telegram-style.js +203 -0
- package/dist/channels/telegram.d.ts +76 -0
- package/dist/channels/telegram.js +1396 -0
- package/dist/channels/types.d.ts +77 -0
- package/dist/channels/types.js +9 -0
- package/dist/channels/webhook.d.ts +58 -0
- package/dist/channels/webhook.js +162 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +223 -0
- package/dist/config.d.ts +60 -0
- package/dist/config.js +188 -0
- package/dist/dashboard/assets/index-CijFoeRu.css +32 -0
- package/dist/dashboard/assets/index-QtT4j0ht.js +262 -0
- package/dist/dashboard/index.html +14 -0
- package/dist/events.d.ts +86 -0
- package/dist/events.js +258 -0
- package/dist/hook-settings.d.ts +67 -0
- package/dist/hook-settings.js +138 -0
- package/dist/hook.d.ts +18 -0
- package/dist/hook.js +199 -0
- package/dist/hooks.d.ts +32 -0
- package/dist/hooks.js +279 -0
- package/dist/jsonl-watcher.d.ts +57 -0
- package/dist/jsonl-watcher.js +159 -0
- package/dist/mcp-server.d.ts +60 -0
- package/dist/mcp-server.js +788 -0
- package/dist/metrics.d.ts +104 -0
- package/dist/metrics.js +226 -0
- package/dist/monitor.d.ts +84 -0
- package/dist/monitor.js +553 -0
- package/dist/permission-guard.d.ts +51 -0
- package/dist/permission-guard.js +197 -0
- package/dist/pipeline.d.ts +84 -0
- package/dist/pipeline.js +218 -0
- package/dist/screenshot.d.ts +26 -0
- package/dist/screenshot.js +57 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +1577 -0
- package/dist/session.d.ts +297 -0
- package/dist/session.js +1275 -0
- package/dist/sse-limiter.d.ts +47 -0
- package/dist/sse-limiter.js +62 -0
- package/dist/sse-writer.d.ts +31 -0
- package/dist/sse-writer.js +95 -0
- package/dist/ssrf.d.ts +57 -0
- package/dist/ssrf.js +169 -0
- package/dist/swarm-monitor.d.ts +114 -0
- package/dist/swarm-monitor.js +267 -0
- package/dist/terminal-parser.d.ts +16 -0
- package/dist/terminal-parser.js +343 -0
- package/dist/tmux.d.ts +161 -0
- package/dist/tmux.js +725 -0
- package/dist/transcript.d.ts +47 -0
- package/dist/transcript.js +244 -0
- package/dist/validation.d.ts +222 -0
- package/dist/validation.js +268 -0
- package/dist/ws-terminal.d.ts +32 -0
- package/dist/ws-terminal.js +297 -0
- package/package.json +71 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Aegis Dashboard</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🛡️</text></svg>" />
|
|
8
|
+
<script type="module" crossorigin src="/dashboard/assets/index-QtT4j0ht.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-CijFoeRu.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body class="bg-[#0a0a0f] text-gray-200 antialiased">
|
|
12
|
+
<div id="root"></div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* events.ts — SSE event emitter for session monitoring.
|
|
3
|
+
*
|
|
4
|
+
* Issue #32: Real-time Server-Sent Events for session lifecycle.
|
|
5
|
+
* Subscribers receive events via an EventEmitter pattern.
|
|
6
|
+
* The monitor pushes events; the SSE route consumes them.
|
|
7
|
+
*/
|
|
8
|
+
export interface SessionSSEEvent {
|
|
9
|
+
event: 'status' | 'message' | 'system' | 'approval' | 'ended' | 'heartbeat' | 'stall' | 'dead' | 'hook' | 'subagent_start' | 'subagent_stop';
|
|
10
|
+
sessionId: string;
|
|
11
|
+
timestamp: string;
|
|
12
|
+
data: Record<string, unknown>;
|
|
13
|
+
/** Issue #87: Unix timestamp (ms) when the event was emitted by Aegis. */
|
|
14
|
+
emittedAt?: number;
|
|
15
|
+
/** Issue #308: Incrementing event ID for Last-Event-ID replay. */
|
|
16
|
+
id?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface GlobalSSEEvent {
|
|
19
|
+
event: 'session_status_change' | 'session_message' | 'session_approval' | 'session_ended' | 'session_created' | 'session_stall' | 'session_dead' | 'session_subagent_start' | 'session_subagent_stop';
|
|
20
|
+
sessionId: string;
|
|
21
|
+
timestamp: string;
|
|
22
|
+
data: Record<string, unknown>;
|
|
23
|
+
/** Issue #301: Incrementing event ID for Last-Event-ID replay. */
|
|
24
|
+
id?: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Per-session event bus. Subscribers (SSE connections) register here.
|
|
28
|
+
* The monitor calls emit() when events happen.
|
|
29
|
+
*/
|
|
30
|
+
export declare class SessionEventBus {
|
|
31
|
+
private emitters;
|
|
32
|
+
/** #224: Track emitters that are ending so new subscribers get fresh emitters. */
|
|
33
|
+
private readonly endingEmitters;
|
|
34
|
+
/** Global incrementing event ID counter. */
|
|
35
|
+
private nextEventId;
|
|
36
|
+
/** Maximum events to buffer per session for Last-Event-ID replay. */
|
|
37
|
+
private static readonly BUFFER_SIZE;
|
|
38
|
+
/** Per-session ring buffer for event replay. */
|
|
39
|
+
private eventBuffers;
|
|
40
|
+
/** Global ring buffer for event replay across all sessions (Issue #301). */
|
|
41
|
+
private globalEventBuffer;
|
|
42
|
+
/** Get or create the emitter for a session. */
|
|
43
|
+
private getEmitter;
|
|
44
|
+
/** Subscribe to events for a session. Returns unsubscribe function. */
|
|
45
|
+
subscribe(sessionId: string, handler: (event: SessionSSEEvent) => void): () => void;
|
|
46
|
+
/** Emit an event to all subscribers for a session (and global subscribers). */
|
|
47
|
+
emit(sessionId: string, event: SessionSSEEvent): void;
|
|
48
|
+
/** Get events emitted after the given event ID for a session. */
|
|
49
|
+
getEventsSince(sessionId: string, lastEventId: number): SessionSSEEvent[];
|
|
50
|
+
/** Emit a status change event. */
|
|
51
|
+
emitStatus(sessionId: string, status: string, detail: string): void;
|
|
52
|
+
/** Emit a message event. */
|
|
53
|
+
emitMessage(sessionId: string, role: string, text: string, contentType?: string, toolMeta?: {
|
|
54
|
+
tool_name?: string;
|
|
55
|
+
tool_id?: string;
|
|
56
|
+
}): void;
|
|
57
|
+
/** Issue #89 L33: Emit a system message event (differentiated from user/assistant messages). */
|
|
58
|
+
emitSystem(sessionId: string, text: string, contentType?: string): void;
|
|
59
|
+
/** Emit an approval request event. */
|
|
60
|
+
emitApproval(sessionId: string, prompt: string): void;
|
|
61
|
+
/** Emit a session ended event. */
|
|
62
|
+
emitEnded(sessionId: string, reason: string): void;
|
|
63
|
+
/** Emit a stall event. */
|
|
64
|
+
emitStall(sessionId: string, stallType: string, detail: string): void;
|
|
65
|
+
/** Emit a dead session event. */
|
|
66
|
+
emitDead(sessionId: string, detail: string): void;
|
|
67
|
+
/** Emit a Claude Code hook event (e.g. Stop, PreToolUse, etc.). */
|
|
68
|
+
emitHook(sessionId: string, hookEvent: string, hookData: Record<string, unknown>): void;
|
|
69
|
+
/** Check if a session has any subscribers. */
|
|
70
|
+
hasSubscribers(sessionId: string): boolean;
|
|
71
|
+
/** Get the number of subscribers for a session. */
|
|
72
|
+
subscriberCount(sessionId: string): number;
|
|
73
|
+
/** Global emitter for aggregating events across all sessions. */
|
|
74
|
+
private globalEmitter;
|
|
75
|
+
/** Subscribe to events from ALL sessions (new and existing). Returns unsubscribe function. */
|
|
76
|
+
subscribeGlobal(handler: (event: GlobalSSEEvent) => void): () => void;
|
|
77
|
+
/** Emit a session created event to global subscribers. */
|
|
78
|
+
emitCreated(sessionId: string, name: string, workDir: string): void;
|
|
79
|
+
/** Get global events emitted after the given event ID (Issue #301). */
|
|
80
|
+
getGlobalEventsSince(lastEventId: number): Array<{
|
|
81
|
+
id: number;
|
|
82
|
+
event: GlobalSSEEvent;
|
|
83
|
+
}>;
|
|
84
|
+
/** Clean up all emitters. */
|
|
85
|
+
destroy(): void;
|
|
86
|
+
}
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* events.ts — SSE event emitter for session monitoring.
|
|
3
|
+
*
|
|
4
|
+
* Issue #32: Real-time Server-Sent Events for session lifecycle.
|
|
5
|
+
* Subscribers receive events via an EventEmitter pattern.
|
|
6
|
+
* The monitor pushes events; the SSE route consumes them.
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from 'node:events';
|
|
9
|
+
/** Map per-session event types to global event types. */
|
|
10
|
+
function toGlobalEvent(event) {
|
|
11
|
+
const typeMap = {
|
|
12
|
+
status: 'session_status_change',
|
|
13
|
+
message: 'session_message',
|
|
14
|
+
system: 'session_message',
|
|
15
|
+
approval: 'session_approval',
|
|
16
|
+
ended: 'session_ended',
|
|
17
|
+
heartbeat: 'session_status_change',
|
|
18
|
+
stall: 'session_stall',
|
|
19
|
+
dead: 'session_dead',
|
|
20
|
+
subagent_start: 'session_subagent_start',
|
|
21
|
+
subagent_stop: 'session_subagent_stop',
|
|
22
|
+
hook: 'session_message',
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
event: typeMap[event.event] || 'session_status_change',
|
|
26
|
+
sessionId: event.sessionId,
|
|
27
|
+
timestamp: event.timestamp,
|
|
28
|
+
data: event.data,
|
|
29
|
+
id: event.id,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Per-session event bus. Subscribers (SSE connections) register here.
|
|
34
|
+
* The monitor calls emit() when events happen.
|
|
35
|
+
*/
|
|
36
|
+
export class SessionEventBus {
|
|
37
|
+
emitters = new Map();
|
|
38
|
+
/** #224: Track emitters that are ending so new subscribers get fresh emitters. */
|
|
39
|
+
endingEmitters = new WeakSet();
|
|
40
|
+
/** Global incrementing event ID counter. */
|
|
41
|
+
nextEventId = 1;
|
|
42
|
+
/** Maximum events to buffer per session for Last-Event-ID replay. */
|
|
43
|
+
static BUFFER_SIZE = 50;
|
|
44
|
+
/** Per-session ring buffer for event replay. */
|
|
45
|
+
eventBuffers = new Map();
|
|
46
|
+
/** Global ring buffer for event replay across all sessions (Issue #301). */
|
|
47
|
+
globalEventBuffer = [];
|
|
48
|
+
/** Get or create the emitter for a session. */
|
|
49
|
+
getEmitter(sessionId) {
|
|
50
|
+
let emitter = this.emitters.get(sessionId);
|
|
51
|
+
// #224: If emitter is ending (session ended), create a fresh one
|
|
52
|
+
if (emitter && this.endingEmitters.has(emitter)) {
|
|
53
|
+
this.emitters.delete(sessionId);
|
|
54
|
+
emitter = undefined;
|
|
55
|
+
}
|
|
56
|
+
if (!emitter) {
|
|
57
|
+
emitter = new EventEmitter();
|
|
58
|
+
emitter.setMaxListeners(50); // Allow many concurrent SSE clients
|
|
59
|
+
this.emitters.set(sessionId, emitter);
|
|
60
|
+
}
|
|
61
|
+
return emitter;
|
|
62
|
+
}
|
|
63
|
+
/** Subscribe to events for a session. Returns unsubscribe function. */
|
|
64
|
+
subscribe(sessionId, handler) {
|
|
65
|
+
const emitter = this.getEmitter(sessionId);
|
|
66
|
+
emitter.on('event', handler);
|
|
67
|
+
return () => {
|
|
68
|
+
emitter.off('event', handler);
|
|
69
|
+
// Clean up emitter if no more listeners and not ending
|
|
70
|
+
if (emitter.listenerCount('event') === 0 && !this.endingEmitters.has(emitter)) {
|
|
71
|
+
this.emitters.delete(sessionId);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Emit an event to all subscribers for a session (and global subscribers). */
|
|
76
|
+
emit(sessionId, event) {
|
|
77
|
+
// Issue #87: Stamp emittedAt for latency measurement
|
|
78
|
+
event.emittedAt = Date.now();
|
|
79
|
+
// Issue #308: Assign incrementing ID for Last-Event-ID replay
|
|
80
|
+
event.id = this.nextEventId++;
|
|
81
|
+
// Push to ring buffer
|
|
82
|
+
let buffer = this.eventBuffers.get(sessionId);
|
|
83
|
+
if (!buffer) {
|
|
84
|
+
buffer = [];
|
|
85
|
+
this.eventBuffers.set(sessionId, buffer);
|
|
86
|
+
}
|
|
87
|
+
buffer.push({ id: event.id, event });
|
|
88
|
+
if (buffer.length > SessionEventBus.BUFFER_SIZE) {
|
|
89
|
+
buffer.splice(0, buffer.length - SessionEventBus.BUFFER_SIZE);
|
|
90
|
+
}
|
|
91
|
+
const emitter = this.emitters.get(sessionId);
|
|
92
|
+
if (emitter) {
|
|
93
|
+
setImmediate(() => emitter.emit('event', event));
|
|
94
|
+
}
|
|
95
|
+
// Forward to global subscribers
|
|
96
|
+
if (this.globalEmitter) {
|
|
97
|
+
const globalEvent = toGlobalEvent(event);
|
|
98
|
+
// Issue #301: push to global ring buffer
|
|
99
|
+
this.globalEventBuffer.push({ id: event.id, event: globalEvent });
|
|
100
|
+
if (this.globalEventBuffer.length > SessionEventBus.BUFFER_SIZE) {
|
|
101
|
+
this.globalEventBuffer.splice(0, this.globalEventBuffer.length - SessionEventBus.BUFFER_SIZE);
|
|
102
|
+
}
|
|
103
|
+
setImmediate(() => this.globalEmitter.emit('event', globalEvent));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Get events emitted after the given event ID for a session. */
|
|
107
|
+
getEventsSince(sessionId, lastEventId) {
|
|
108
|
+
const buffer = this.eventBuffers.get(sessionId);
|
|
109
|
+
if (!buffer)
|
|
110
|
+
return [];
|
|
111
|
+
return buffer.filter(e => e.id > lastEventId).map(e => e.event);
|
|
112
|
+
}
|
|
113
|
+
/** Emit a status change event. */
|
|
114
|
+
emitStatus(sessionId, status, detail) {
|
|
115
|
+
this.emit(sessionId, {
|
|
116
|
+
event: 'status',
|
|
117
|
+
sessionId,
|
|
118
|
+
timestamp: new Date().toISOString(),
|
|
119
|
+
data: { status, detail },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** Emit a message event. */
|
|
123
|
+
emitMessage(sessionId, role, text, contentType, toolMeta) {
|
|
124
|
+
this.emit(sessionId, {
|
|
125
|
+
event: 'message',
|
|
126
|
+
sessionId,
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
data: { role, text, contentType, ...toolMeta },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/** Issue #89 L33: Emit a system message event (differentiated from user/assistant messages). */
|
|
132
|
+
emitSystem(sessionId, text, contentType) {
|
|
133
|
+
this.emit(sessionId, {
|
|
134
|
+
event: 'system',
|
|
135
|
+
sessionId,
|
|
136
|
+
timestamp: new Date().toISOString(),
|
|
137
|
+
data: { role: 'system', text, contentType, isSystem: true },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/** Emit an approval request event. */
|
|
141
|
+
emitApproval(sessionId, prompt) {
|
|
142
|
+
this.emit(sessionId, {
|
|
143
|
+
event: 'approval',
|
|
144
|
+
sessionId,
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
data: { prompt },
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** Emit a session ended event. */
|
|
150
|
+
emitEnded(sessionId, reason) {
|
|
151
|
+
this.emit(sessionId, {
|
|
152
|
+
event: 'ended',
|
|
153
|
+
sessionId,
|
|
154
|
+
timestamp: new Date().toISOString(),
|
|
155
|
+
data: { reason },
|
|
156
|
+
});
|
|
157
|
+
// #224: Mark emitter as ending so new subscribers don't get silently deleted
|
|
158
|
+
const emitter = this.emitters.get(sessionId);
|
|
159
|
+
if (emitter) {
|
|
160
|
+
this.endingEmitters.add(emitter);
|
|
161
|
+
}
|
|
162
|
+
// Clean up after a short delay (let clients receive the event)
|
|
163
|
+
// Capture reference — only delete if it's still the same emitter
|
|
164
|
+
// #357: Also delete the per-session event buffer to prevent unbounded map growth
|
|
165
|
+
setTimeout(() => {
|
|
166
|
+
if (this.emitters.get(sessionId) === emitter) {
|
|
167
|
+
this.emitters.delete(sessionId);
|
|
168
|
+
}
|
|
169
|
+
this.eventBuffers.delete(sessionId);
|
|
170
|
+
}, 1000);
|
|
171
|
+
}
|
|
172
|
+
/** Emit a stall event. */
|
|
173
|
+
emitStall(sessionId, stallType, detail) {
|
|
174
|
+
this.emit(sessionId, {
|
|
175
|
+
event: 'stall',
|
|
176
|
+
sessionId,
|
|
177
|
+
timestamp: new Date().toISOString(),
|
|
178
|
+
data: { stallType, detail },
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** Emit a dead session event. */
|
|
182
|
+
emitDead(sessionId, detail) {
|
|
183
|
+
this.emit(sessionId, {
|
|
184
|
+
event: 'dead',
|
|
185
|
+
sessionId,
|
|
186
|
+
timestamp: new Date().toISOString(),
|
|
187
|
+
data: { reason: detail },
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
/** Emit a Claude Code hook event (e.g. Stop, PreToolUse, etc.). */
|
|
191
|
+
emitHook(sessionId, hookEvent, hookData) {
|
|
192
|
+
this.emit(sessionId, {
|
|
193
|
+
event: 'hook',
|
|
194
|
+
sessionId,
|
|
195
|
+
timestamp: new Date().toISOString(),
|
|
196
|
+
data: { hookEvent, ...hookData },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
/** Check if a session has any subscribers. */
|
|
200
|
+
hasSubscribers(sessionId) {
|
|
201
|
+
const emitter = this.emitters.get(sessionId);
|
|
202
|
+
return !!emitter && emitter.listenerCount('event') > 0;
|
|
203
|
+
}
|
|
204
|
+
/** Get the number of subscribers for a session. */
|
|
205
|
+
subscriberCount(sessionId) {
|
|
206
|
+
const emitter = this.emitters.get(sessionId);
|
|
207
|
+
return emitter ? emitter.listenerCount('event') : 0;
|
|
208
|
+
}
|
|
209
|
+
// ── Global (all-session) SSE ──────────────────────────────────────
|
|
210
|
+
/** Global emitter for aggregating events across all sessions. */
|
|
211
|
+
globalEmitter = null;
|
|
212
|
+
/** Subscribe to events from ALL sessions (new and existing). Returns unsubscribe function. */
|
|
213
|
+
subscribeGlobal(handler) {
|
|
214
|
+
if (!this.globalEmitter) {
|
|
215
|
+
this.globalEmitter = new EventEmitter();
|
|
216
|
+
this.globalEmitter.setMaxListeners(50);
|
|
217
|
+
}
|
|
218
|
+
this.globalEmitter.on('event', handler);
|
|
219
|
+
return () => {
|
|
220
|
+
this.globalEmitter?.off('event', handler);
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
/** Emit a session created event to global subscribers. */
|
|
224
|
+
emitCreated(sessionId, name, workDir) {
|
|
225
|
+
if (!this.globalEmitter)
|
|
226
|
+
return;
|
|
227
|
+
const id = this.nextEventId++;
|
|
228
|
+
const globalEvent = {
|
|
229
|
+
event: 'session_created',
|
|
230
|
+
sessionId,
|
|
231
|
+
timestamp: new Date().toISOString(),
|
|
232
|
+
data: { name, workDir },
|
|
233
|
+
id,
|
|
234
|
+
};
|
|
235
|
+
// Issue #301: buffer global-only events
|
|
236
|
+
this.globalEventBuffer.push({ id, event: globalEvent });
|
|
237
|
+
if (this.globalEventBuffer.length > SessionEventBus.BUFFER_SIZE) {
|
|
238
|
+
this.globalEventBuffer.splice(0, this.globalEventBuffer.length - SessionEventBus.BUFFER_SIZE);
|
|
239
|
+
}
|
|
240
|
+
this.globalEmitter.emit('event', globalEvent);
|
|
241
|
+
}
|
|
242
|
+
/** Get global events emitted after the given event ID (Issue #301). */
|
|
243
|
+
getGlobalEventsSince(lastEventId) {
|
|
244
|
+
return this.globalEventBuffer.filter(e => e.id > lastEventId);
|
|
245
|
+
}
|
|
246
|
+
/** Clean up all emitters. */
|
|
247
|
+
destroy() {
|
|
248
|
+
for (const emitter of this.emitters.values()) {
|
|
249
|
+
emitter.removeAllListeners();
|
|
250
|
+
}
|
|
251
|
+
this.emitters.clear();
|
|
252
|
+
this.eventBuffers.clear();
|
|
253
|
+
this.globalEventBuffer = [];
|
|
254
|
+
this.globalEmitter?.removeAllListeners();
|
|
255
|
+
this.globalEmitter = null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hook-settings.ts — Generate CC settings.json with HTTP hooks for Aegis.
|
|
3
|
+
*
|
|
4
|
+
* When Aegis creates a CC session, it writes a per-session settings file
|
|
5
|
+
* that configures HTTP hooks pointing to Aegis's hook receiver endpoint.
|
|
6
|
+
* The file is passed to CC via the `--settings` CLI flag.
|
|
7
|
+
*
|
|
8
|
+
* Only events that support `type: "http"` hooks are included:
|
|
9
|
+
* Stop, PreToolUse, PostToolUse, PermissionRequest, TaskCompleted
|
|
10
|
+
*
|
|
11
|
+
* Events like Notification, SessionEnd, etc. only support `type: "command"`
|
|
12
|
+
* and are excluded.
|
|
13
|
+
*
|
|
14
|
+
* Issue #169: Phase 2 — Inject CC settings.json with HTTP hooks.
|
|
15
|
+
*/
|
|
16
|
+
/** CC hook events that support `type: "http"`.
|
|
17
|
+
*
|
|
18
|
+
* All CC hook events support HTTP hooks. We register the most useful ones
|
|
19
|
+
* for Aegis status detection and event forwarding.
|
|
20
|
+
*
|
|
21
|
+
* Excluded (low value for Aegis):
|
|
22
|
+
* - InstructionsLoaded, ConfigChange, CwdChanged, FileChanged (informational)
|
|
23
|
+
* - WorktreeCreate, WorktreeRemove (worktree management)
|
|
24
|
+
* - Elicitation, ElicitationResult (MCP-specific)
|
|
25
|
+
* - PreCompact, PostCompact (internal optimization)
|
|
26
|
+
*/
|
|
27
|
+
declare const HTTP_HOOK_EVENTS: readonly ["Stop", "StopFailure", "PreToolUse", "PostToolUse", "PostToolUseFailure", "PermissionRequest", "TaskCompleted", "SessionStart", "SessionEnd", "UserPromptSubmit", "SubagentStart", "SubagentStop", "Notification", "TeammateIdle"];
|
|
28
|
+
export { HTTP_HOOK_EVENTS };
|
|
29
|
+
export type HttpHookEvent = typeof HTTP_HOOK_EVENTS[number];
|
|
30
|
+
/** Shape of a single HTTP hook entry in CC settings.json. */
|
|
31
|
+
interface HttpHookConfig {
|
|
32
|
+
type: 'http';
|
|
33
|
+
url: string;
|
|
34
|
+
}
|
|
35
|
+
/** Shape of the `hooks` section in CC settings.json. */
|
|
36
|
+
export interface HookSettings {
|
|
37
|
+
hooks: Record<string, Array<{
|
|
38
|
+
matcher?: string;
|
|
39
|
+
hooks: HttpHookConfig[];
|
|
40
|
+
}>>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Generate the hooks section of a CC settings.json for a given session.
|
|
44
|
+
*
|
|
45
|
+
* @param baseUrl - Aegis base URL (e.g. "http://localhost:9100")
|
|
46
|
+
* @param sessionId - Aegis session ID (used as query param for routing)
|
|
47
|
+
*/
|
|
48
|
+
export declare function generateHookSettings(baseUrl: string, sessionId: string): HookSettings;
|
|
49
|
+
/**
|
|
50
|
+
* Write hook settings to a temporary file and return its path.
|
|
51
|
+
*
|
|
52
|
+
* Issue #339: Reads .claude/settings.local.json from workDir and deep-merges
|
|
53
|
+
* hook settings into it, so CC gets both project settings (env vars, permissions,
|
|
54
|
+
* bypassPermissions) AND Aegis hooks in a single --settings file.
|
|
55
|
+
*
|
|
56
|
+
* @param baseUrl - Aegis base URL
|
|
57
|
+
* @param sessionId - Aegis session ID
|
|
58
|
+
* @param workDir - Project working directory (to read settings.local.json from)
|
|
59
|
+
* @returns Path to the temporary settings file
|
|
60
|
+
*/
|
|
61
|
+
export declare function writeHookSettingsFile(baseUrl: string, sessionId: string, workDir?: string): Promise<string>;
|
|
62
|
+
/**
|
|
63
|
+
* Clean up a hook settings temp file.
|
|
64
|
+
*
|
|
65
|
+
* @param filePath - Path to the temporary settings file
|
|
66
|
+
*/
|
|
67
|
+
export declare function cleanupHookSettingsFile(filePath: string): Promise<void>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hook-settings.ts — Generate CC settings.json with HTTP hooks for Aegis.
|
|
3
|
+
*
|
|
4
|
+
* When Aegis creates a CC session, it writes a per-session settings file
|
|
5
|
+
* that configures HTTP hooks pointing to Aegis's hook receiver endpoint.
|
|
6
|
+
* The file is passed to CC via the `--settings` CLI flag.
|
|
7
|
+
*
|
|
8
|
+
* Only events that support `type: "http"` hooks are included:
|
|
9
|
+
* Stop, PreToolUse, PostToolUse, PermissionRequest, TaskCompleted
|
|
10
|
+
*
|
|
11
|
+
* Events like Notification, SessionEnd, etc. only support `type: "command"`
|
|
12
|
+
* and are excluded.
|
|
13
|
+
*
|
|
14
|
+
* Issue #169: Phase 2 — Inject CC settings.json with HTTP hooks.
|
|
15
|
+
*/
|
|
16
|
+
import { readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
|
|
17
|
+
import { existsSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import { ccSettingsSchema } from './validation.js';
|
|
21
|
+
/** CC hook events that support `type: "http"`.
|
|
22
|
+
*
|
|
23
|
+
* All CC hook events support HTTP hooks. We register the most useful ones
|
|
24
|
+
* for Aegis status detection and event forwarding.
|
|
25
|
+
*
|
|
26
|
+
* Excluded (low value for Aegis):
|
|
27
|
+
* - InstructionsLoaded, ConfigChange, CwdChanged, FileChanged (informational)
|
|
28
|
+
* - WorktreeCreate, WorktreeRemove (worktree management)
|
|
29
|
+
* - Elicitation, ElicitationResult (MCP-specific)
|
|
30
|
+
* - PreCompact, PostCompact (internal optimization)
|
|
31
|
+
*/
|
|
32
|
+
const HTTP_HOOK_EVENTS = [
|
|
33
|
+
// Status detection (highest value)
|
|
34
|
+
'Stop',
|
|
35
|
+
'StopFailure',
|
|
36
|
+
'PreToolUse',
|
|
37
|
+
'PostToolUse',
|
|
38
|
+
'PostToolUseFailure',
|
|
39
|
+
'PermissionRequest',
|
|
40
|
+
'TaskCompleted',
|
|
41
|
+
// Session lifecycle
|
|
42
|
+
'SessionStart',
|
|
43
|
+
'SessionEnd',
|
|
44
|
+
'UserPromptSubmit',
|
|
45
|
+
// Subagent tracking
|
|
46
|
+
'SubagentStart',
|
|
47
|
+
'SubagentStop',
|
|
48
|
+
// Notifications
|
|
49
|
+
'Notification',
|
|
50
|
+
'TeammateIdle',
|
|
51
|
+
];
|
|
52
|
+
export { HTTP_HOOK_EVENTS };
|
|
53
|
+
/**
|
|
54
|
+
* Generate the hooks section of a CC settings.json for a given session.
|
|
55
|
+
*
|
|
56
|
+
* @param baseUrl - Aegis base URL (e.g. "http://localhost:9100")
|
|
57
|
+
* @param sessionId - Aegis session ID (used as query param for routing)
|
|
58
|
+
*/
|
|
59
|
+
export function generateHookSettings(baseUrl, sessionId) {
|
|
60
|
+
const hooks = {};
|
|
61
|
+
for (const event of HTTP_HOOK_EVENTS) {
|
|
62
|
+
hooks[event] = [
|
|
63
|
+
{
|
|
64
|
+
hooks: [
|
|
65
|
+
{
|
|
66
|
+
type: 'http',
|
|
67
|
+
url: `${baseUrl}/v1/hooks/${event}?sessionId=${sessionId}`,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
return { hooks };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Write hook settings to a temporary file and return its path.
|
|
77
|
+
*
|
|
78
|
+
* Issue #339: Reads .claude/settings.local.json from workDir and deep-merges
|
|
79
|
+
* hook settings into it, so CC gets both project settings (env vars, permissions,
|
|
80
|
+
* bypassPermissions) AND Aegis hooks in a single --settings file.
|
|
81
|
+
*
|
|
82
|
+
* @param baseUrl - Aegis base URL
|
|
83
|
+
* @param sessionId - Aegis session ID
|
|
84
|
+
* @param workDir - Project working directory (to read settings.local.json from)
|
|
85
|
+
* @returns Path to the temporary settings file
|
|
86
|
+
*/
|
|
87
|
+
export async function writeHookSettingsFile(baseUrl, sessionId, workDir) {
|
|
88
|
+
const hookSettings = generateHookSettings(baseUrl, sessionId);
|
|
89
|
+
// Issue #339: Read project's settings.local.json and merge hooks into it.
|
|
90
|
+
// This ensures CC gets env vars, permissions, and bypassPermissions alongside hooks.
|
|
91
|
+
let merged = {};
|
|
92
|
+
if (workDir) {
|
|
93
|
+
const projectSettingsPath = join(workDir, '.claude', 'settings.local.json');
|
|
94
|
+
if (existsSync(projectSettingsPath)) {
|
|
95
|
+
try {
|
|
96
|
+
const raw = await readFile(projectSettingsPath, 'utf-8');
|
|
97
|
+
const parsed = ccSettingsSchema.safeParse(JSON.parse(raw));
|
|
98
|
+
if (parsed.success) {
|
|
99
|
+
merged = parsed.data;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Malformed settings file — use empty base, hooks will still work
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Deep-merge: project settings as base, hook settings override
|
|
108
|
+
const combined = {
|
|
109
|
+
...merged,
|
|
110
|
+
hooks: {
|
|
111
|
+
...(merged.hooks ?? {}),
|
|
112
|
+
...hookSettings.hooks,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
const settingsDir = join(tmpdir(), 'aegis-hooks');
|
|
116
|
+
if (!existsSync(settingsDir)) {
|
|
117
|
+
await mkdir(settingsDir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
const filePath = join(settingsDir, `hooks-${sessionId}.json`);
|
|
120
|
+
await writeFile(filePath, JSON.stringify(combined, null, 2) + '\n', 'utf-8');
|
|
121
|
+
return filePath;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Clean up a hook settings temp file.
|
|
125
|
+
*
|
|
126
|
+
* @param filePath - Path to the temporary settings file
|
|
127
|
+
*/
|
|
128
|
+
export async function cleanupHookSettingsFile(filePath) {
|
|
129
|
+
try {
|
|
130
|
+
if (existsSync(filePath)) {
|
|
131
|
+
await unlink(filePath);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Non-fatal: temp file cleanup failed
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=hook-settings.js.map
|
package/dist/hook.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* hook.ts — Claude Code SessionStart hook for Aegis.
|
|
4
|
+
*
|
|
5
|
+
* Writes session_id → window_id mapping to ~/.aegis/session_map.json.
|
|
6
|
+
* Falls back to ~/.manus/ for backward compatibility.
|
|
7
|
+
* Called by CC's hook system, reads payload from stdin.
|
|
8
|
+
*
|
|
9
|
+
* Install: add to ~/.claude/settings.json:
|
|
10
|
+
* {
|
|
11
|
+
* "hooks": {
|
|
12
|
+
* "SessionStart": [{
|
|
13
|
+
* "hooks": [{ "type": "command", "command": "node /path/to/dist/hook.js", "timeout": 5 }]
|
|
14
|
+
* }]
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
export {};
|