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.
Files changed (71) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +244 -0
  3. package/dashboard/dist/assets/index-CijFoeRu.css +32 -0
  4. package/dashboard/dist/assets/index-QtT4j0ht.js +262 -0
  5. package/dashboard/dist/index.html +14 -0
  6. package/dist/auth.d.ts +76 -0
  7. package/dist/auth.js +219 -0
  8. package/dist/channels/index.d.ts +8 -0
  9. package/dist/channels/index.js +9 -0
  10. package/dist/channels/manager.d.ts +39 -0
  11. package/dist/channels/manager.js +101 -0
  12. package/dist/channels/telegram-style.d.ts +118 -0
  13. package/dist/channels/telegram-style.js +203 -0
  14. package/dist/channels/telegram.d.ts +76 -0
  15. package/dist/channels/telegram.js +1396 -0
  16. package/dist/channels/types.d.ts +77 -0
  17. package/dist/channels/types.js +9 -0
  18. package/dist/channels/webhook.d.ts +58 -0
  19. package/dist/channels/webhook.js +162 -0
  20. package/dist/cli.d.ts +8 -0
  21. package/dist/cli.js +223 -0
  22. package/dist/config.d.ts +60 -0
  23. package/dist/config.js +188 -0
  24. package/dist/dashboard/assets/index-CijFoeRu.css +32 -0
  25. package/dist/dashboard/assets/index-QtT4j0ht.js +262 -0
  26. package/dist/dashboard/index.html +14 -0
  27. package/dist/events.d.ts +86 -0
  28. package/dist/events.js +258 -0
  29. package/dist/hook-settings.d.ts +67 -0
  30. package/dist/hook-settings.js +138 -0
  31. package/dist/hook.d.ts +18 -0
  32. package/dist/hook.js +199 -0
  33. package/dist/hooks.d.ts +32 -0
  34. package/dist/hooks.js +279 -0
  35. package/dist/jsonl-watcher.d.ts +57 -0
  36. package/dist/jsonl-watcher.js +159 -0
  37. package/dist/mcp-server.d.ts +60 -0
  38. package/dist/mcp-server.js +788 -0
  39. package/dist/metrics.d.ts +104 -0
  40. package/dist/metrics.js +226 -0
  41. package/dist/monitor.d.ts +84 -0
  42. package/dist/monitor.js +553 -0
  43. package/dist/permission-guard.d.ts +51 -0
  44. package/dist/permission-guard.js +197 -0
  45. package/dist/pipeline.d.ts +84 -0
  46. package/dist/pipeline.js +218 -0
  47. package/dist/screenshot.d.ts +26 -0
  48. package/dist/screenshot.js +57 -0
  49. package/dist/server.d.ts +10 -0
  50. package/dist/server.js +1577 -0
  51. package/dist/session.d.ts +297 -0
  52. package/dist/session.js +1275 -0
  53. package/dist/sse-limiter.d.ts +47 -0
  54. package/dist/sse-limiter.js +62 -0
  55. package/dist/sse-writer.d.ts +31 -0
  56. package/dist/sse-writer.js +95 -0
  57. package/dist/ssrf.d.ts +57 -0
  58. package/dist/ssrf.js +169 -0
  59. package/dist/swarm-monitor.d.ts +114 -0
  60. package/dist/swarm-monitor.js +267 -0
  61. package/dist/terminal-parser.d.ts +16 -0
  62. package/dist/terminal-parser.js +343 -0
  63. package/dist/tmux.d.ts +161 -0
  64. package/dist/tmux.js +725 -0
  65. package/dist/transcript.d.ts +47 -0
  66. package/dist/transcript.js +244 -0
  67. package/dist/validation.d.ts +222 -0
  68. package/dist/validation.js +268 -0
  69. package/dist/ws-terminal.d.ts +32 -0
  70. package/dist/ws-terminal.js +297 -0
  71. package/package.json +71 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * sse-limiter.ts — Connection limiter for SSE endpoints (Issue #300).
3
+ *
4
+ * Tracks active SSE connections per-IP and globally.
5
+ * Enforces configurable limits to prevent unbounded resource consumption.
6
+ */
7
+ export interface SSELimiterConfig {
8
+ /** Maximum total concurrent SSE connections across all IPs. Default: 100 */
9
+ maxConnections?: number;
10
+ /** Maximum concurrent SSE connections per client IP. Default: 10 */
11
+ maxPerIp?: number;
12
+ }
13
+ export interface AcquireResult {
14
+ allowed: true;
15
+ connectionId: string;
16
+ }
17
+ export interface AcquireDeniedResult {
18
+ allowed: false;
19
+ reason: 'per_ip_limit' | 'global_limit';
20
+ /** Current count for the limiting dimension */
21
+ current: number;
22
+ /** Configured limit */
23
+ limit: number;
24
+ }
25
+ export type AcquireResponse = AcquireResult | AcquireDeniedResult;
26
+ export declare class SSEConnectionLimiter {
27
+ private readonly maxConnections;
28
+ private readonly maxPerIp;
29
+ private readonly connections;
30
+ private readonly ipCounts;
31
+ private nextId;
32
+ constructor(config?: SSELimiterConfig);
33
+ /** Current total active connections. */
34
+ get activeCount(): number;
35
+ /** Active connections for a specific IP. */
36
+ activeCountForIp(ip: string): number;
37
+ /**
38
+ * Attempt to acquire a connection slot.
39
+ * Check per-IP limit first (more specific), then global limit.
40
+ */
41
+ acquire(ip: string): AcquireResponse;
42
+ /**
43
+ * Release a connection slot.
44
+ * Safe to call with unknown or already-released IDs (no-op).
45
+ */
46
+ release(connectionId: string): void;
47
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * sse-limiter.ts — Connection limiter for SSE endpoints (Issue #300).
3
+ *
4
+ * Tracks active SSE connections per-IP and globally.
5
+ * Enforces configurable limits to prevent unbounded resource consumption.
6
+ */
7
+ export class SSEConnectionLimiter {
8
+ maxConnections;
9
+ maxPerIp;
10
+ connections = new Map();
11
+ ipCounts = new Map();
12
+ nextId = 1;
13
+ constructor(config) {
14
+ this.maxConnections = config?.maxConnections ?? 100;
15
+ this.maxPerIp = config?.maxPerIp ?? 10;
16
+ }
17
+ /** Current total active connections. */
18
+ get activeCount() {
19
+ return this.connections.size;
20
+ }
21
+ /** Active connections for a specific IP. */
22
+ activeCountForIp(ip) {
23
+ return this.ipCounts.get(ip) ?? 0;
24
+ }
25
+ /**
26
+ * Attempt to acquire a connection slot.
27
+ * Check per-IP limit first (more specific), then global limit.
28
+ */
29
+ acquire(ip) {
30
+ const currentPerIp = this.activeCountForIp(ip);
31
+ if (currentPerIp >= this.maxPerIp) {
32
+ return { allowed: false, reason: 'per_ip_limit', current: currentPerIp, limit: this.maxPerIp };
33
+ }
34
+ if (this.connections.size >= this.maxConnections) {
35
+ return { allowed: false, reason: 'global_limit', current: this.connections.size, limit: this.maxConnections };
36
+ }
37
+ const connectionId = `sse-${this.nextId++}`;
38
+ this.connections.set(connectionId, { ip });
39
+ this.ipCounts.set(ip, currentPerIp + 1);
40
+ return { allowed: true, connectionId };
41
+ }
42
+ /**
43
+ * Release a connection slot.
44
+ * Safe to call with unknown or already-released IDs (no-op).
45
+ */
46
+ release(connectionId) {
47
+ const entry = this.connections.get(connectionId);
48
+ if (!entry)
49
+ return;
50
+ this.connections.delete(connectionId);
51
+ const count = this.ipCounts.get(entry.ip);
52
+ if (count !== undefined) {
53
+ if (count <= 1) {
54
+ this.ipCounts.delete(entry.ip);
55
+ }
56
+ else {
57
+ this.ipCounts.set(entry.ip, count - 1);
58
+ }
59
+ }
60
+ }
61
+ }
62
+ //# sourceMappingURL=sse-limiter.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * sse-writer.ts — SSE write helper with back-pressure handling.
3
+ *
4
+ * Issue #302: Check reply.raw.write() return value and disconnect
5
+ * slow clients after consecutive failed writes.
6
+ */
7
+ import type { ServerResponse, IncomingMessage } from 'node:http';
8
+ export declare class SSEWriter {
9
+ private readonly res;
10
+ private readonly onCleanup;
11
+ private consecutiveFailures;
12
+ private isDestroyed;
13
+ private heartbeatTimer;
14
+ private lastWrite;
15
+ /** Drop slow clients after this many consecutive write() calls returning false. */
16
+ private static readonly MAX_CONSECUTIVE_FAILURES;
17
+ constructor(res: ServerResponse, req: IncomingMessage, onCleanup: () => void);
18
+ /**
19
+ * Write SSE data to the response.
20
+ * Returns true if the write succeeded (or is within failure threshold).
21
+ * Returns false if the connection was destroyed due to back-pressure.
22
+ */
23
+ write(data: string): boolean;
24
+ /**
25
+ * Start heartbeat + idle timeout loop.
26
+ * Returns a stop function to cancel the timer.
27
+ */
28
+ startHeartbeat(intervalMs: number, idleTimeoutMs: number, buildEvent: () => string): () => void;
29
+ private destroy;
30
+ private cleanup;
31
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * sse-writer.ts — SSE write helper with back-pressure handling.
3
+ *
4
+ * Issue #302: Check reply.raw.write() return value and disconnect
5
+ * slow clients after consecutive failed writes.
6
+ */
7
+ export class SSEWriter {
8
+ res;
9
+ onCleanup;
10
+ consecutiveFailures = 0;
11
+ isDestroyed = false;
12
+ heartbeatTimer = null;
13
+ lastWrite = Date.now();
14
+ /** Drop slow clients after this many consecutive write() calls returning false. */
15
+ static MAX_CONSECUTIVE_FAILURES = 3;
16
+ constructor(res, req, onCleanup) {
17
+ this.res = res;
18
+ this.onCleanup = onCleanup;
19
+ req.on('close', () => this.cleanup());
20
+ }
21
+ /**
22
+ * Write SSE data to the response.
23
+ * Returns true if the write succeeded (or is within failure threshold).
24
+ * Returns false if the connection was destroyed due to back-pressure.
25
+ */
26
+ write(data) {
27
+ if (this.isDestroyed)
28
+ return false;
29
+ try {
30
+ const canContinue = this.res.write(data);
31
+ if (!canContinue) {
32
+ this.consecutiveFailures++;
33
+ if (this.consecutiveFailures >= SSEWriter.MAX_CONSECUTIVE_FAILURES) {
34
+ this.destroy();
35
+ return false;
36
+ }
37
+ }
38
+ else {
39
+ this.consecutiveFailures = 0;
40
+ this.lastWrite = Date.now();
41
+ }
42
+ return true;
43
+ }
44
+ catch { /* write failed — destroy connection */
45
+ this.destroy();
46
+ return false;
47
+ }
48
+ }
49
+ /**
50
+ * Start heartbeat + idle timeout loop.
51
+ * Returns a stop function to cancel the timer.
52
+ */
53
+ startHeartbeat(intervalMs, idleTimeoutMs, buildEvent) {
54
+ this.heartbeatTimer = setInterval(() => {
55
+ if (this.isDestroyed) {
56
+ clearInterval(this.heartbeatTimer);
57
+ return;
58
+ }
59
+ if (Date.now() - this.lastWrite > idleTimeoutMs) {
60
+ this.destroy();
61
+ return;
62
+ }
63
+ this.write(buildEvent());
64
+ }, intervalMs);
65
+ return () => {
66
+ if (this.heartbeatTimer) {
67
+ clearInterval(this.heartbeatTimer);
68
+ this.heartbeatTimer = null;
69
+ }
70
+ };
71
+ }
72
+ destroy() {
73
+ if (this.isDestroyed)
74
+ return;
75
+ this.isDestroyed = true;
76
+ if (this.heartbeatTimer) {
77
+ clearInterval(this.heartbeatTimer);
78
+ this.heartbeatTimer = null;
79
+ }
80
+ try {
81
+ this.res.destroy();
82
+ }
83
+ catch { /* already closed */ }
84
+ this.onCleanup();
85
+ }
86
+ cleanup() {
87
+ if (this.heartbeatTimer) {
88
+ clearInterval(this.heartbeatTimer);
89
+ this.heartbeatTimer = null;
90
+ }
91
+ this.isDestroyed = true;
92
+ this.onCleanup();
93
+ }
94
+ }
95
+ //# sourceMappingURL=sse-writer.js.map
package/dist/ssrf.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Check if an IP address (v4 or v6) is private/internal.
3
+ *
4
+ * Rejects:
5
+ * - RFC 1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
6
+ * - Loopback: 127.0.0.0/8, ::1
7
+ * - Link-local: 169.254.0.0/16, fe80::/10
8
+ * - Current network: 0.0.0.0/8
9
+ * - Unspecified: ::
10
+ * - IPv6 unique-local: fc00::/7
11
+ * - CGNAT: 100.64.0.0/10 (RFC 6598)
12
+ */
13
+ export declare function isPrivateIP(ip: string): boolean;
14
+ /**
15
+ * Validate a URL for webhook configuration.
16
+ *
17
+ * Checks:
18
+ * 1. Valid URL format
19
+ * 2. HTTPS scheme required for external hosts
20
+ * 3. HTTP allowed only for localhost / 127.0.0.1
21
+ * 4. Rejects private/internal IP addresses (except 127.0.0.1 in dev mode)
22
+ * 5. Rejects *.local hostnames
23
+ *
24
+ * Returns null if valid, or an error string if invalid.
25
+ */
26
+ export declare function validateWebhookUrl(rawUrl: string): string | null;
27
+ /** DNS lookup result shape (matches node:dns/promises.LookupAddress). */
28
+ export interface DnsLookupResult {
29
+ address: string;
30
+ family: number;
31
+ }
32
+ /** DNS lookup function type for dependency injection. */
33
+ export type DnsLookupFn = (hostname: string) => Promise<DnsLookupResult>;
34
+ /**
35
+ * Resolve a hostname via DNS and check if the resulting IP is private/internal.
36
+ *
37
+ * For literal IP addresses, checks directly without DNS resolution.
38
+ * Returns null if safe, or an error string if the IP is private.
39
+ *
40
+ * @param hostname - Hostname or literal IP to check
41
+ * @param lookupFn - Optional DNS lookup function (for testing)
42
+ */
43
+ export declare function resolveAndCheckIp(hostname: string, lookupFn?: DnsLookupFn): Promise<string | null>;
44
+ /**
45
+ * Validate a URL for the screenshot endpoint to prevent SSRF attacks.
46
+ *
47
+ * Checks:
48
+ * 1. Valid URL format
49
+ * 2. http: or https: scheme only
50
+ * 3. Rejects private/internal IP addresses (literal)
51
+ * 4. Rejects localhost / *.local hostnames
52
+ *
53
+ * For full DNS-resolution protection, call resolveAndCheckIp() separately.
54
+ *
55
+ * Returns null if valid, or an error string if invalid.
56
+ */
57
+ export declare function validateScreenshotUrl(rawUrl: string): string | null;
package/dist/ssrf.js ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * ssrf.ts — Shared SSRF (Server-Side Request Forgery) prevention utilities.
3
+ *
4
+ * Validates URLs by checking scheme, hostname, and DNS resolution against
5
+ * private/internal IP ranges. Used by webhook channel and screenshot endpoint.
6
+ */
7
+ import dns from 'node:dns/promises';
8
+ import net from 'node:net';
9
+ /**
10
+ * Check if an IP address (v4 or v6) is private/internal.
11
+ *
12
+ * Rejects:
13
+ * - RFC 1918: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
14
+ * - Loopback: 127.0.0.0/8, ::1
15
+ * - Link-local: 169.254.0.0/16, fe80::/10
16
+ * - Current network: 0.0.0.0/8
17
+ * - Unspecified: ::
18
+ * - IPv6 unique-local: fc00::/7
19
+ * - CGNAT: 100.64.0.0/10 (RFC 6598)
20
+ */
21
+ export function isPrivateIP(ip) {
22
+ // IPv4
23
+ if (net.isIPv4(ip)) {
24
+ const parts = ip.split('.').map(Number);
25
+ const [a, b] = parts;
26
+ // 0.0.0.0/8
27
+ if (a === 0)
28
+ return true;
29
+ // 10.0.0.0/8
30
+ if (a === 10)
31
+ return true;
32
+ // 127.0.0.0/8
33
+ if (a === 127)
34
+ return true;
35
+ // 169.254.0.0/16
36
+ if (a === 169 && b === 254)
37
+ return true;
38
+ // 172.16.0.0/12
39
+ if (a === 172 && b >= 16 && b <= 31)
40
+ return true;
41
+ // 192.168.0.0/16
42
+ if (a === 192 && b === 168)
43
+ return true;
44
+ // 100.64.0.0/10 (CGNAT)
45
+ if (a === 100 && b >= 64 && b <= 127)
46
+ return true;
47
+ return false;
48
+ }
49
+ // IPv6
50
+ const lower = ip.toLowerCase();
51
+ // ::1 (loopback)
52
+ if (lower === '::1')
53
+ return true;
54
+ // :: (unspecified)
55
+ if (lower === '::')
56
+ return true;
57
+ // fe80::/10 (link-local)
58
+ if (lower.startsWith('fe8') || lower.startsWith('fe9') || lower.startsWith('fea') || lower.startsWith('feb'))
59
+ return true;
60
+ // fc00::/7 (unique-local) — includes fc and fd prefixes
61
+ if (lower.startsWith('fc') || lower.startsWith('fd'))
62
+ return true;
63
+ return false;
64
+ }
65
+ /**
66
+ * Validate a URL for webhook configuration.
67
+ *
68
+ * Checks:
69
+ * 1. Valid URL format
70
+ * 2. HTTPS scheme required for external hosts
71
+ * 3. HTTP allowed only for localhost / 127.0.0.1
72
+ * 4. Rejects private/internal IP addresses (except 127.0.0.1 in dev mode)
73
+ * 5. Rejects *.local hostnames
74
+ *
75
+ * Returns null if valid, or an error string if invalid.
76
+ */
77
+ export function validateWebhookUrl(rawUrl) {
78
+ let parsed;
79
+ try {
80
+ parsed = new URL(rawUrl);
81
+ }
82
+ catch { /* malformed URL string */
83
+ return 'Invalid URL';
84
+ }
85
+ const hostname = parsed.hostname;
86
+ // Scheme check — must be HTTPS, or HTTP only for local dev
87
+ const isLocalDev = hostname === '127.0.0.1' || hostname === '::1' || hostname === 'localhost';
88
+ if (parsed.protocol !== 'https:' && !(parsed.protocol === 'http:' && isLocalDev)) {
89
+ if (parsed.protocol === 'http:') {
90
+ return 'Only HTTPS URLs are allowed for external hosts';
91
+ }
92
+ return 'Only HTTPS URLs are allowed';
93
+ }
94
+ // Reject *.local hostnames (but allow literal localhost for dev)
95
+ if (hostname.endsWith('.local')) {
96
+ return 'Localhost URLs are not allowed';
97
+ }
98
+ // Reject private/internal IPs (except 127.0.0.1/::1 which are allowed for dev over HTTP)
99
+ if (net.isIP(hostname) && isPrivateIP(hostname) && !isLocalDev) {
100
+ return 'Private/internal IP addresses are not allowed';
101
+ }
102
+ return null;
103
+ }
104
+ /** Default DNS lookup using node:dns/promises. */
105
+ const defaultLookup = (hostname) => dns.lookup(hostname);
106
+ /**
107
+ * Resolve a hostname via DNS and check if the resulting IP is private/internal.
108
+ *
109
+ * For literal IP addresses, checks directly without DNS resolution.
110
+ * Returns null if safe, or an error string if the IP is private.
111
+ *
112
+ * @param hostname - Hostname or literal IP to check
113
+ * @param lookupFn - Optional DNS lookup function (for testing)
114
+ */
115
+ export async function resolveAndCheckIp(hostname, lookupFn = defaultLookup) {
116
+ // Literal IP — check directly
117
+ if (net.isIP(hostname)) {
118
+ if (isPrivateIP(hostname)) {
119
+ return `DNS resolution points to a private/internal IP: ${hostname}`;
120
+ }
121
+ return null;
122
+ }
123
+ try {
124
+ const result = await lookupFn(hostname);
125
+ if (isPrivateIP(result.address)) {
126
+ return `DNS resolution points to a private/internal IP: ${result.address}`;
127
+ }
128
+ return null;
129
+ }
130
+ catch { /* DNS lookup failed — treat as unsafe */
131
+ return `DNS resolution failed for ${hostname}`;
132
+ }
133
+ }
134
+ /**
135
+ * Validate a URL for the screenshot endpoint to prevent SSRF attacks.
136
+ *
137
+ * Checks:
138
+ * 1. Valid URL format
139
+ * 2. http: or https: scheme only
140
+ * 3. Rejects private/internal IP addresses (literal)
141
+ * 4. Rejects localhost / *.local hostnames
142
+ *
143
+ * For full DNS-resolution protection, call resolveAndCheckIp() separately.
144
+ *
145
+ * Returns null if valid, or an error string if invalid.
146
+ */
147
+ export function validateScreenshotUrl(rawUrl) {
148
+ let parsed;
149
+ try {
150
+ parsed = new URL(rawUrl);
151
+ }
152
+ catch { /* malformed URL string */
153
+ return 'Invalid URL';
154
+ }
155
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
156
+ return 'Only http and https URLs are allowed';
157
+ }
158
+ const hostname = parsed.hostname;
159
+ // Reject localhost / *.local hostnames
160
+ if (hostname === 'localhost' || hostname.endsWith('.local')) {
161
+ return 'Localhost URLs are not allowed';
162
+ }
163
+ // Reject private/internal IPs
164
+ if (net.isIP(hostname) && isPrivateIP(hostname)) {
165
+ return 'Private/internal IP addresses are not allowed';
166
+ }
167
+ return null;
168
+ }
169
+ //# sourceMappingURL=ssrf.js.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * swarm-monitor.ts — Monitors Claude Code swarm sockets for teammate sessions.
3
+ *
4
+ * Issue #81: Agent Swarm Awareness.
5
+ *
6
+ * When CC spawns teammates/subagents, it creates them in tmux with:
7
+ * - Socket: -L claude-swarm-{pid} (isolated from main session)
8
+ * - Window naming: teammate-{name}
9
+ * - Env vars: CLAUDE_PARENT_SESSION_ID, --agent-id, --agent-name
10
+ *
11
+ * This module discovers those swarm sockets, lists their windows,
12
+ * cross-references with parent sessions, and tracks teammate status.
13
+ */
14
+ import type { SessionManager, SessionInfo } from './session.js';
15
+ /** Information about a single teammate window in a swarm socket. */
16
+ export interface TeammateInfo {
17
+ /** Window ID (e.g. "@0") */
18
+ windowId: string;
19
+ /** Window name (e.g. "teammate-explore-agent") */
20
+ windowName: string;
21
+ /** Working directory of the teammate */
22
+ cwd: string;
23
+ /** Current process running in the pane */
24
+ paneCommand: string;
25
+ /** Whether the teammate process is alive (claude/node running) */
26
+ alive: boolean;
27
+ /** Inferred status from pane command */
28
+ status: 'running' | 'idle' | 'dead';
29
+ }
30
+ /** A detected swarm (parent + its teammates). */
31
+ export interface SwarmInfo {
32
+ /** Socket name (e.g. "claude-swarm-12345") */
33
+ socketName: string;
34
+ /** PID extracted from socket name */
35
+ pid: number;
36
+ /** The parent Aegis session, if found */
37
+ parentSession: SessionInfo | null;
38
+ /** Detected teammate windows */
39
+ teammates: TeammateInfo[];
40
+ /** Aggregated swarm status */
41
+ aggregatedStatus: 'all_idle' | 'some_working' | 'all_dead' | 'no_teammates';
42
+ /** When this swarm was last scanned */
43
+ lastScannedAt: number;
44
+ }
45
+ /** Result of scanning all swarm sockets. */
46
+ export interface SwarmScanResult {
47
+ swarms: SwarmInfo[];
48
+ totalSockets: number;
49
+ totalTeammates: number;
50
+ scannedAt: number;
51
+ }
52
+ export interface SwarmMonitorConfig {
53
+ /** How often to scan for swarm sockets (default: 10s) */
54
+ scanIntervalMs: number;
55
+ /** Glob pattern for swarm socket directories */
56
+ socketGlobPattern: string;
57
+ }
58
+ export declare const DEFAULT_SWARM_CONFIG: SwarmMonitorConfig;
59
+ /** Events emitted by SwarmMonitor when teammate state changes. */
60
+ export type SwarmEvent = {
61
+ type: 'teammate_spawned';
62
+ swarm: SwarmInfo;
63
+ teammate: TeammateInfo;
64
+ } | {
65
+ type: 'teammate_finished';
66
+ swarm: SwarmInfo;
67
+ teammate: TeammateInfo;
68
+ };
69
+ /** Callback for swarm events. */
70
+ export type SwarmEventHandler = (event: SwarmEvent) => void;
71
+ export declare class SwarmMonitor {
72
+ private sessions;
73
+ private config;
74
+ private running;
75
+ private lastResult;
76
+ private timer;
77
+ private eventHandlers;
78
+ constructor(sessions: SessionManager, config?: SwarmMonitorConfig);
79
+ /** Register an event handler for teammate lifecycle events. */
80
+ onEvent(handler: SwarmEventHandler): void;
81
+ private emitEvent;
82
+ /** Start the periodic scan loop. */
83
+ start(): void;
84
+ /** Stop the periodic scan loop. */
85
+ stop(): void;
86
+ /** Get the most recent scan result. */
87
+ getLastResult(): SwarmScanResult | null;
88
+ /** Run a single scan and return the result. */
89
+ scan(): Promise<SwarmScanResult>;
90
+ /** Compare current scan result against previous to detect teammate changes. */
91
+ private detectChanges;
92
+ /** Snapshot of teammates from previous scan for diffing. */
93
+ private previousTeammates;
94
+ /** Cached /tmp listing to avoid redundant I/O on every scan. */
95
+ private cachedSocketNames;
96
+ private cachedSocketAt;
97
+ private static readonly SOCKET_CACHE_TTL_MS;
98
+ /** Discover swarm socket directories in /tmp. */
99
+ private discoverSwarmSockets;
100
+ /** Inspect a single swarm socket and return swarm info. */
101
+ inspectSwarmSocket(socketName: string): Promise<SwarmInfo>;
102
+ /** Extract PID from socket name "claude-swarm-{pid}". */
103
+ private extractPid;
104
+ /** List all windows in a swarm socket. */
105
+ private listSwarmWindows;
106
+ /** Find the parent Aegis session for a swarm by matching the CC process PID. */
107
+ private findParentSession;
108
+ /** Compute aggregated status for a swarm. */
109
+ private computeAggregatedStatus;
110
+ /** Find a specific swarm by parent session ID. */
111
+ findSwarmByParentSessionId(sessionId: string): SwarmInfo | null;
112
+ /** Find all swarms associated with any active session. */
113
+ findActiveSwarms(): SwarmInfo[];
114
+ }