aegis-bridge 0.1.0

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 (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +404 -0
  3. package/dashboard/dist/assets/index-BoZwGLAx.css +32 -0
  4. package/dashboard/dist/assets/index-C61BkKH-.js +312 -0
  5. package/dashboard/dist/assets/index-C61BkKH-.js.map +1 -0
  6. package/dashboard/dist/index.html +14 -0
  7. package/dist/api-contracts.d.ts +229 -0
  8. package/dist/api-contracts.js +7 -0
  9. package/dist/api-contracts.typecheck.d.ts +14 -0
  10. package/dist/api-contracts.typecheck.js +1 -0
  11. package/dist/api-error-envelope.d.ts +15 -0
  12. package/dist/api-error-envelope.js +80 -0
  13. package/dist/auth.d.ts +87 -0
  14. package/dist/auth.js +276 -0
  15. package/dist/channels/index.d.ts +8 -0
  16. package/dist/channels/index.js +8 -0
  17. package/dist/channels/manager.d.ts +47 -0
  18. package/dist/channels/manager.js +115 -0
  19. package/dist/channels/telegram-style.d.ts +118 -0
  20. package/dist/channels/telegram-style.js +202 -0
  21. package/dist/channels/telegram.d.ts +91 -0
  22. package/dist/channels/telegram.js +1518 -0
  23. package/dist/channels/types.d.ts +77 -0
  24. package/dist/channels/types.js +8 -0
  25. package/dist/channels/webhook.d.ts +60 -0
  26. package/dist/channels/webhook.js +216 -0
  27. package/dist/cli.d.ts +8 -0
  28. package/dist/cli.js +252 -0
  29. package/dist/config.d.ts +90 -0
  30. package/dist/config.js +214 -0
  31. package/dist/consensus.d.ts +16 -0
  32. package/dist/consensus.js +19 -0
  33. package/dist/continuation-pointer.d.ts +11 -0
  34. package/dist/continuation-pointer.js +65 -0
  35. package/dist/diagnostics.d.ts +27 -0
  36. package/dist/diagnostics.js +95 -0
  37. package/dist/error-categories.d.ts +39 -0
  38. package/dist/error-categories.js +73 -0
  39. package/dist/events.d.ts +133 -0
  40. package/dist/events.js +389 -0
  41. package/dist/fault-injection.d.ts +29 -0
  42. package/dist/fault-injection.js +115 -0
  43. package/dist/file-utils.d.ts +2 -0
  44. package/dist/file-utils.js +37 -0
  45. package/dist/handshake.d.ts +60 -0
  46. package/dist/handshake.js +124 -0
  47. package/dist/hook-settings.d.ts +80 -0
  48. package/dist/hook-settings.js +272 -0
  49. package/dist/hook.d.ts +19 -0
  50. package/dist/hook.js +231 -0
  51. package/dist/hooks.d.ts +32 -0
  52. package/dist/hooks.js +364 -0
  53. package/dist/jsonl-watcher.d.ts +59 -0
  54. package/dist/jsonl-watcher.js +166 -0
  55. package/dist/logger.d.ts +35 -0
  56. package/dist/logger.js +65 -0
  57. package/dist/mcp-server.d.ts +123 -0
  58. package/dist/mcp-server.js +869 -0
  59. package/dist/memory-bridge.d.ts +27 -0
  60. package/dist/memory-bridge.js +137 -0
  61. package/dist/memory-routes.d.ts +3 -0
  62. package/dist/memory-routes.js +100 -0
  63. package/dist/metrics.d.ts +126 -0
  64. package/dist/metrics.js +286 -0
  65. package/dist/model-router.d.ts +53 -0
  66. package/dist/model-router.js +150 -0
  67. package/dist/monitor.d.ts +103 -0
  68. package/dist/monitor.js +820 -0
  69. package/dist/path-utils.d.ts +11 -0
  70. package/dist/path-utils.js +21 -0
  71. package/dist/permission-evaluator.d.ts +10 -0
  72. package/dist/permission-evaluator.js +48 -0
  73. package/dist/permission-guard.d.ts +51 -0
  74. package/dist/permission-guard.js +196 -0
  75. package/dist/permission-request-manager.d.ts +12 -0
  76. package/dist/permission-request-manager.js +36 -0
  77. package/dist/permission-routes.d.ts +7 -0
  78. package/dist/permission-routes.js +28 -0
  79. package/dist/pipeline.d.ts +97 -0
  80. package/dist/pipeline.js +291 -0
  81. package/dist/process-utils.d.ts +4 -0
  82. package/dist/process-utils.js +73 -0
  83. package/dist/question-manager.d.ts +54 -0
  84. package/dist/question-manager.js +80 -0
  85. package/dist/retry.d.ts +11 -0
  86. package/dist/retry.js +34 -0
  87. package/dist/safe-json.d.ts +12 -0
  88. package/dist/safe-json.js +22 -0
  89. package/dist/screenshot.d.ts +28 -0
  90. package/dist/screenshot.js +60 -0
  91. package/dist/server.d.ts +10 -0
  92. package/dist/server.js +1973 -0
  93. package/dist/session-cleanup.d.ts +18 -0
  94. package/dist/session-cleanup.js +11 -0
  95. package/dist/session.d.ts +379 -0
  96. package/dist/session.js +1568 -0
  97. package/dist/shutdown-utils.d.ts +5 -0
  98. package/dist/shutdown-utils.js +24 -0
  99. package/dist/signal-cleanup-helper.d.ts +48 -0
  100. package/dist/signal-cleanup-helper.js +117 -0
  101. package/dist/sse-limiter.d.ts +47 -0
  102. package/dist/sse-limiter.js +61 -0
  103. package/dist/sse-writer.d.ts +31 -0
  104. package/dist/sse-writer.js +94 -0
  105. package/dist/ssrf.d.ts +102 -0
  106. package/dist/ssrf.js +267 -0
  107. package/dist/startup.d.ts +6 -0
  108. package/dist/startup.js +162 -0
  109. package/dist/suppress.d.ts +33 -0
  110. package/dist/suppress.js +79 -0
  111. package/dist/swarm-monitor.d.ts +117 -0
  112. package/dist/swarm-monitor.js +300 -0
  113. package/dist/template-store.d.ts +45 -0
  114. package/dist/template-store.js +142 -0
  115. package/dist/terminal-parser.d.ts +16 -0
  116. package/dist/terminal-parser.js +346 -0
  117. package/dist/tmux-capture-cache.d.ts +18 -0
  118. package/dist/tmux-capture-cache.js +34 -0
  119. package/dist/tmux.d.ts +183 -0
  120. package/dist/tmux.js +906 -0
  121. package/dist/tool-registry.d.ts +40 -0
  122. package/dist/tool-registry.js +83 -0
  123. package/dist/transcript.d.ts +63 -0
  124. package/dist/transcript.js +284 -0
  125. package/dist/utils/circular-buffer.d.ts +11 -0
  126. package/dist/utils/circular-buffer.js +37 -0
  127. package/dist/utils/redact-headers.d.ts +13 -0
  128. package/dist/utils/redact-headers.js +54 -0
  129. package/dist/validation.d.ts +406 -0
  130. package/dist/validation.js +415 -0
  131. package/dist/verification.d.ts +2 -0
  132. package/dist/verification.js +72 -0
  133. package/dist/worktree-lookup.d.ts +24 -0
  134. package/dist/worktree-lookup.js +71 -0
  135. package/dist/ws-terminal.d.ts +32 -0
  136. package/dist/ws-terminal.js +348 -0
  137. package/package.json +83 -0
@@ -0,0 +1,77 @@
1
+ /**
2
+ * channels/types.ts — Notification channel interface.
3
+ *
4
+ * Every notification channel (Telegram, Discord, webhook, Slack, etc.)
5
+ * implements this interface. The bridge doesn't know or care which
6
+ * channels are active — it fires events, channels decide what to do.
7
+ */
8
+ /** Health status for a channel. */
9
+ export interface ChannelHealthStatus {
10
+ channel: string;
11
+ healthy: boolean;
12
+ lastSuccess: number | null;
13
+ lastError: string | null;
14
+ pendingCount: number;
15
+ }
16
+ /** Events a channel can subscribe to. */
17
+ export type SessionEvent = 'session.created' | 'session.ended' | 'message.user' | 'message.assistant' | 'message.thinking' | 'message.tool_use' | 'message.tool_result' | 'status.idle' | 'status.working' | 'status.permission' | 'status.question' | 'status.plan' | 'status.stall' | 'status.dead' | 'status.stopped' | 'status.error' | 'status.rate_limited' | 'status.permission_timeout' | 'status.recovered' | 'swarm.teammate_spawned' | 'swarm.teammate_finished';
18
+ /** Payload for all session events. */
19
+ export interface SessionEventPayload {
20
+ event: SessionEvent;
21
+ timestamp: string;
22
+ session: {
23
+ id: string;
24
+ name: string;
25
+ workDir: string;
26
+ };
27
+ detail: string;
28
+ /** Contextual data — depends on event type. */
29
+ meta?: Record<string, unknown>;
30
+ }
31
+ /** Inbound command from a channel (user replied in Telegram, webhook callback, etc.) */
32
+ export interface InboundCommand {
33
+ sessionId: string;
34
+ action: 'approve' | 'reject' | 'escape' | 'kill' | 'message' | 'command';
35
+ text?: string;
36
+ }
37
+ /** Callback for inbound commands. */
38
+ export type InboundHandler = (cmd: InboundCommand) => Promise<void>;
39
+ /**
40
+ * A notification channel.
41
+ *
42
+ * Channels are initialized once and receive events for the lifetime
43
+ * of the bridge. They can optionally accept inbound commands
44
+ * (bidirectional channels like Telegram).
45
+ */
46
+ export interface Channel {
47
+ /** Human-readable channel name (for logging). */
48
+ readonly name: string;
49
+ /** Initialize the channel. Called once at startup. */
50
+ init?(onInbound: InboundHandler): Promise<void>;
51
+ /** Tear down the channel. Called on shutdown. */
52
+ destroy?(): Promise<void>;
53
+ /** Called when a new session is created. */
54
+ onSessionCreated?(payload: SessionEventPayload): Promise<void>;
55
+ /** Called when a session ends. */
56
+ onSessionEnded?(payload: SessionEventPayload): Promise<void>;
57
+ /** Called when a message is sent to/from CC. */
58
+ onMessage?(payload: SessionEventPayload): Promise<void>;
59
+ /** Called when session status changes (idle, working, permission, question, plan). */
60
+ onStatusChange?(payload: SessionEventPayload): Promise<void>;
61
+ /**
62
+ * Optional: filter which events this channel cares about.
63
+ * Return true to receive the event, false to skip.
64
+ * If not implemented, receives all events.
65
+ */
66
+ filter?(event: SessionEvent): boolean;
67
+ /** Return entries from the dead letter queue (failed deliveries). */
68
+ getDeadLetterQueue?(): Array<{
69
+ timestamp: string;
70
+ endpoint: string;
71
+ event: SessionEvent;
72
+ error: string;
73
+ attempts: number;
74
+ }>;
75
+ /** Return channel health status. */
76
+ getHealth?(): ChannelHealthStatus;
77
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * channels/types.ts — Notification channel interface.
3
+ *
4
+ * Every notification channel (Telegram, Discord, webhook, Slack, etc.)
5
+ * implements this interface. The bridge doesn't know or care which
6
+ * channels are active — it fires events, channels decide what to do.
7
+ */
8
+ export {};
@@ -0,0 +1,60 @@
1
+ /**
2
+ * channels/webhook.ts — Generic webhook notification channel.
3
+ *
4
+ * Fires HTTP POST to configured URLs on session events.
5
+ * Configure via AEGIS_WEBHOOKS (or legacy MANUS_WEBHOOKS) env var or config file.
6
+ */
7
+ import type { Channel, SessionEvent, SessionEventPayload } from './types.js';
8
+ export interface WebhookEndpoint {
9
+ /** URL to POST to. */
10
+ url: string;
11
+ /** Filter: only fire on these events. Omit = all events. */
12
+ events?: SessionEvent[];
13
+ /** Custom headers (e.g. Authorization). */
14
+ headers?: Record<string, string>;
15
+ /** Timeout in ms (default: 5000). */
16
+ timeoutMs?: number;
17
+ }
18
+ export interface WebhookChannelConfig {
19
+ endpoints: WebhookEndpoint[];
20
+ }
21
+ /** Dead letter queue entry for a failed webhook delivery. */
22
+ export interface DeadLetterEntry {
23
+ timestamp: string;
24
+ endpoint: string;
25
+ event: SessionEvent;
26
+ error: string;
27
+ attempts: number;
28
+ }
29
+ export declare class WebhookChannel implements Channel {
30
+ readonly name = "webhook";
31
+ private endpoints;
32
+ /** Issue #89 L14: In-memory dead letter queue for failed deliveries. Max 100 items. */
33
+ private deadLetterQueue;
34
+ static readonly DLQ_MAX_SIZE = 100;
35
+ constructor(config: WebhookChannelConfig);
36
+ /** Create from AEGIS_WEBHOOKS (or legacy MANUS_WEBHOOKS) env var. Returns null if not set or invalid. */
37
+ static fromEnv(): WebhookChannel | null;
38
+ filter(event: SessionEvent): boolean;
39
+ onSessionCreated(payload: SessionEventPayload): Promise<void>;
40
+ onSessionEnded(payload: SessionEventPayload): Promise<void>;
41
+ onMessage(payload: SessionEventPayload): Promise<void>;
42
+ onStatusChange(payload: SessionEventPayload): Promise<void>;
43
+ /** Maximum retry attempts per webhook delivery. */
44
+ static readonly MAX_RETRIES = 5;
45
+ /** Base delay for exponential backoff (ms). */
46
+ static readonly BASE_DELAY_MS = 1000;
47
+ /** Exponential backoff with jitter: delay * (0.5 + Math.random() * 0.5). */
48
+ static backoff(attempt: number): number;
49
+ /** Redact sensitive session metadata from webhook payloads. */
50
+ private static redactPayload;
51
+ private fire;
52
+ /** Issue #25: Deliver webhook with retry + exponential backoff. */
53
+ private deliverWithRetry;
54
+ /** Issue #89 L14: Add a failed delivery to the dead letter queue. */
55
+ private addToDeadLetterQueue;
56
+ /** Issue #89 L14: Get all entries in the dead letter queue. */
57
+ getDeadLetterQueue(): DeadLetterEntry[];
58
+ /** Issue #89 L14: Clear the dead letter queue. Returns number of entries cleared. */
59
+ clearDeadLetterQueue(): number;
60
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * channels/webhook.ts — Generic webhook notification channel.
3
+ *
4
+ * Fires HTTP POST to configured URLs on session events.
5
+ * Configure via AEGIS_WEBHOOKS (or legacy MANUS_WEBHOOKS) env var or config file.
6
+ */
7
+ import { webhookEndpointSchema, getErrorMessage } from '../validation.js';
8
+ import { validateWebhookUrl, resolveAndCheckIp, buildConnectionUrl } from '../ssrf.js';
9
+ import { redactSecretsFromText } from '../utils/redact-headers.js';
10
+ import { RetriableError } from './manager.js';
11
+ export class WebhookChannel {
12
+ name = 'webhook';
13
+ endpoints;
14
+ /** Issue #89 L14: In-memory dead letter queue for failed deliveries. Max 100 items. */
15
+ deadLetterQueue = [];
16
+ static DLQ_MAX_SIZE = 100;
17
+ constructor(config) {
18
+ this.endpoints = config.endpoints;
19
+ }
20
+ /** Create from AEGIS_WEBHOOKS (or legacy MANUS_WEBHOOKS) env var. Returns null if not set or invalid. */
21
+ static fromEnv() {
22
+ const raw = process.env.AEGIS_WEBHOOKS ?? process.env.MANUS_WEBHOOKS;
23
+ if (!raw)
24
+ return null;
25
+ try {
26
+ const parsed = JSON.parse(raw);
27
+ if (!Array.isArray(parsed) || parsed.length === 0)
28
+ return null;
29
+ // Validate each endpoint with Zod schema + SSRF URL check
30
+ const endpoints = [];
31
+ for (let i = 0; i < parsed.length; i++) {
32
+ const result = webhookEndpointSchema.safeParse(parsed[i]);
33
+ if (!result.success) {
34
+ console.error(`Webhook URL validation failed for endpoint ${i}: schema error`, result.error.message);
35
+ return null;
36
+ }
37
+ const urlError = validateWebhookUrl(result.data.url);
38
+ if (urlError) {
39
+ console.error(`Webhook URL validation failed for endpoint ${i}: ${urlError}`, result.data.url);
40
+ return null;
41
+ }
42
+ endpoints.push(result.data);
43
+ }
44
+ return new WebhookChannel({ endpoints });
45
+ }
46
+ catch (e) {
47
+ console.error('Failed to parse AEGIS_WEBHOOKS:', e);
48
+ return null;
49
+ }
50
+ }
51
+ filter(event) {
52
+ // Accept if ANY endpoint wants this event
53
+ return this.endpoints.some(ep => !ep.events || ep.events.length === 0 || ep.events.includes(event));
54
+ }
55
+ async onSessionCreated(payload) {
56
+ await this.fire(payload);
57
+ }
58
+ async onSessionEnded(payload) {
59
+ await this.fire(payload);
60
+ }
61
+ async onMessage(payload) {
62
+ await this.fire(payload);
63
+ }
64
+ async onStatusChange(payload) {
65
+ await this.fire(payload);
66
+ }
67
+ /** Maximum retry attempts per webhook delivery. */
68
+ static MAX_RETRIES = 5;
69
+ /** Base delay for exponential backoff (ms). */
70
+ static BASE_DELAY_MS = 1000;
71
+ /** Exponential backoff with jitter: delay * (0.5 + Math.random() * 0.5). */
72
+ static backoff(attempt) {
73
+ const base = WebhookChannel.BASE_DELAY_MS * Math.pow(2, attempt - 1);
74
+ return base * (0.5 + Math.random() * 0.5);
75
+ }
76
+ /** Redact sensitive session metadata from webhook payloads. */
77
+ static redactPayload(payload) {
78
+ const { session, ...rest } = payload;
79
+ return {
80
+ ...rest,
81
+ session: {
82
+ id: '[REDACTED]',
83
+ name: '[REDACTED]',
84
+ workDir: '[REDACTED]',
85
+ },
86
+ api: {
87
+ read: 'GET /sessions/[REDACTED]/read',
88
+ send: 'POST /sessions/[REDACTED]/send',
89
+ kill: 'DELETE /sessions/[REDACTED]',
90
+ },
91
+ };
92
+ }
93
+ async fire(payload) {
94
+ const body = JSON.stringify(WebhookChannel.redactPayload(payload));
95
+ const promises = this.endpoints.map(async (ep) => {
96
+ // Skip if endpoint filters and this event isn't in the list
97
+ if (ep.events && ep.events.length > 0 && !ep.events.includes(payload.event))
98
+ return;
99
+ await this.deliverWithRetry(ep, body, payload.event);
100
+ });
101
+ const results = await Promise.allSettled(promises);
102
+ const failed = results.filter((r) => r.status === 'rejected');
103
+ if (failed.length > 0) {
104
+ const reasons = failed.map(r => String(r.reason)).join('; ');
105
+ const allFailed = failed.length === results.length;
106
+ if (allFailed) {
107
+ console.error(`Webhook: ${failed.length}/${results.length} endpoint(s) failed (total): ${reasons}`);
108
+ }
109
+ else {
110
+ console.warn(`Webhook: ${failed.length}/${results.length} endpoint(s) failed: ${reasons}`);
111
+ }
112
+ }
113
+ }
114
+ /** Issue #25: Deliver webhook with retry + exponential backoff. */
115
+ async deliverWithRetry(ep, body, event, maxRetries = WebhookChannel.MAX_RETRIES) {
116
+ let lastError = '';
117
+ const hostname = new URL(ep.url).hostname;
118
+ const bareHost = hostname.replace(/^\[|\]$/g, '');
119
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
120
+ try {
121
+ // DNS rebinding protection: resolve and validate IP before each fetch.
122
+ // Skip for literal IPs (already validated at config time).
123
+ let fetchUrl = ep.url;
124
+ let headers = {
125
+ 'Content-Type': 'application/json',
126
+ ...(ep.headers || {}),
127
+ };
128
+ if (bareHost !== '127.0.0.1' && bareHost !== '::1' && bareHost !== 'localhost') {
129
+ const dnsResult = await resolveAndCheckIp(bareHost);
130
+ if (dnsResult.error) {
131
+ lastError = dnsResult.error;
132
+ if (attempt < maxRetries) {
133
+ const delay = WebhookChannel.backoff(attempt);
134
+ console.warn(`Webhook ${ep.url} DNS check failed for ${event} (attempt ${attempt}/${maxRetries}): ${lastError}, retrying in ${Math.round(delay)}ms`);
135
+ await new Promise(r => setTimeout(r, delay));
136
+ continue;
137
+ }
138
+ console.error(`Webhook ${ep.url} DNS check failed after ${maxRetries} attempts for ${event}: ${lastError}`);
139
+ this.addToDeadLetterQueue(ep.url, event, lastError, maxRetries);
140
+ throw new RetriableError(lastError);
141
+ }
142
+ if (dnsResult.resolvedIp) {
143
+ const { connectionUrl, hostHeader } = buildConnectionUrl(ep.url, dnsResult.resolvedIp);
144
+ fetchUrl = connectionUrl;
145
+ headers['Host'] = hostHeader;
146
+ }
147
+ }
148
+ const res = await fetch(fetchUrl, {
149
+ method: 'POST',
150
+ headers,
151
+ body,
152
+ signal: AbortSignal.timeout(ep.timeoutMs || 5000),
153
+ });
154
+ if (res.ok)
155
+ return; // Success
156
+ lastError = `HTTP ${res.status}`;
157
+ // Server error (5xx) — retry; client error (4xx) — don't
158
+ if (res.status >= 500 && attempt < maxRetries) {
159
+ const delay = WebhookChannel.backoff(attempt);
160
+ console.warn(`Webhook ${ep.url} returned ${res.status} for ${event} (attempt ${attempt}/${maxRetries}), retrying in ${Math.round(delay)}ms`);
161
+ await new Promise(r => setTimeout(r, delay));
162
+ continue;
163
+ }
164
+ console.error(`Webhook ${ep.url} returned ${res.status} for ${event} (attempt ${attempt}/${maxRetries})`);
165
+ // Issue #89 L14: Only add to DLQ for 5xx (server) errors, not 4xx client errors
166
+ if (res.status >= 500) {
167
+ this.addToDeadLetterQueue(ep.url, event, lastError, attempt);
168
+ }
169
+ }
170
+ catch (e) {
171
+ lastError = redactSecretsFromText(getErrorMessage(e), ep.headers);
172
+ if (attempt < maxRetries) {
173
+ const delay = WebhookChannel.backoff(attempt);
174
+ console.warn(`Webhook ${ep.url} error for ${event} (attempt ${attempt}/${maxRetries}): ${lastError}, retrying in ${Math.round(delay)}ms`);
175
+ await new Promise(r => setTimeout(r, delay));
176
+ continue;
177
+ }
178
+ console.error(`Webhook ${ep.url} failed after ${maxRetries} attempts for ${event}: ${lastError}`);
179
+ this.addToDeadLetterQueue(ep.url, event, lastError, maxRetries);
180
+ }
181
+ // Final failure — throw so fire() can aggregate.
182
+ // Use RetriableError for 5xx/network (circuit breaker counts these),
183
+ // plain Error for 4xx (circuit breaker ignores these).
184
+ if (lastError.startsWith('HTTP ') && parseInt(lastError.slice(5)) < 500) {
185
+ throw new Error(lastError);
186
+ }
187
+ throw new RetriableError(lastError);
188
+ }
189
+ }
190
+ /** Issue #89 L14: Add a failed delivery to the dead letter queue. */
191
+ addToDeadLetterQueue(endpoint, event, error, attempts) {
192
+ const entry = {
193
+ timestamp: new Date().toISOString(),
194
+ endpoint,
195
+ event,
196
+ error,
197
+ attempts,
198
+ };
199
+ this.deadLetterQueue.push(entry);
200
+ // Evict oldest entries if over max size
201
+ if (this.deadLetterQueue.length > WebhookChannel.DLQ_MAX_SIZE) {
202
+ this.deadLetterQueue = this.deadLetterQueue.slice(-WebhookChannel.DLQ_MAX_SIZE);
203
+ }
204
+ console.warn(`Webhook DLQ: added failed delivery for ${event} to ${endpoint} after ${attempts} attempts`);
205
+ }
206
+ /** Issue #89 L14: Get all entries in the dead letter queue. */
207
+ getDeadLetterQueue() {
208
+ return [...this.deadLetterQueue];
209
+ }
210
+ /** Issue #89 L14: Clear the dead letter queue. Returns number of entries cleared. */
211
+ clearDeadLetterQueue() {
212
+ const count = this.deadLetterQueue.length;
213
+ this.deadLetterQueue = [];
214
+ return count;
215
+ }
216
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli.ts — CLI entry point for Aegis.
4
+ *
5
+ * `npx aegis-bridge` or `aegis-bridge` starts the server with sensible defaults.
6
+ * Auto-detects tmux and claude CLI, prints helpful startup message.
7
+ */
8
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * cli.ts — CLI entry point for Aegis.
4
+ *
5
+ * `npx aegis-bridge` or `aegis-bridge` starts the server with sensible defaults.
6
+ * Auto-detects tmux and claude CLI, prints helpful startup message.
7
+ */
8
+ import { execFileSync } from 'node:child_process';
9
+ import { readFileSync } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { parseIntSafe, getErrorMessage } from './validation.js';
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
15
+ /** Current aegis-bridge version read from package.json at startup. */
16
+ const VERSION = pkg.version;
17
+ /** Check whether a required external dependency can be executed. */
18
+ function checkDependency(command, args) {
19
+ try {
20
+ execFileSync(command, args, { stdio: 'ignore', timeout: 5000 });
21
+ return true;
22
+ }
23
+ catch { /* command not found or exited non-zero */
24
+ return false;
25
+ }
26
+ }
27
+ /** Parse tmux -V output and enforce minimum supported version. */
28
+ function checkTmuxVersion(minMajor = 3, minMinor = 3) {
29
+ try {
30
+ const out = execFileSync('tmux', ['-V'], { encoding: 'utf-8', timeout: 5000 }).trim();
31
+ const m = out.match(/tmux\s+(\d+)\.(\d+)/i);
32
+ if (!m)
33
+ return { ok: false, version: null };
34
+ const major = parseInt(m[1], 10);
35
+ const minor = parseInt(m[2], 10);
36
+ const ok = major > minMajor || (major === minMajor && minor >= minMinor);
37
+ return { ok, version: `${major}.${minor}` };
38
+ }
39
+ catch {
40
+ return { ok: false, version: null };
41
+ }
42
+ }
43
+ /** Render the startup banner shown when launching the HTTP server. */
44
+ function printBanner(port) {
45
+ console.log(`
46
+ ┌─────────────────────────────────────────┐
47
+ │ ⚡ Aegis v${VERSION} │
48
+ │ Claude Code Session Bridge │
49
+ └─────────────────────────────────────────┘
50
+ `);
51
+ }
52
+ /** Issue #5 stretch: create a session from CLI. */
53
+ async function handleCreate(args) {
54
+ // Parse brief text (first non-flag argument)
55
+ let brief = '';
56
+ let cwd = process.cwd();
57
+ let port = parseIntSafe(process.env.AEGIS_PORT, 9100);
58
+ for (let i = 0; i < args.length; i++) {
59
+ if (args[i] === '--cwd' && args[i + 1]) {
60
+ cwd = args[++i];
61
+ }
62
+ else if (args[i] === '--port' && args[i + 1]) {
63
+ port = parseIntSafe(args[++i], 9100);
64
+ }
65
+ else if (!args[i].startsWith('-')) {
66
+ brief = args[i];
67
+ }
68
+ }
69
+ if (!brief) {
70
+ console.error(' ❌ Missing brief. Usage: aegis-bridge create "Build a login page"');
71
+ process.exit(1);
72
+ }
73
+ const baseUrl = `http://127.0.0.1:${port}`;
74
+ const sessionName = `cc-${brief.slice(0, 20).replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase()}`;
75
+ // Create session
76
+ let sessionId;
77
+ try {
78
+ const res = await fetch(`${baseUrl}/v1/sessions`, {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({ workDir: cwd, name: sessionName }),
82
+ });
83
+ if (!res.ok) {
84
+ const err = await res.json().catch(() => ({ error: res.statusText }));
85
+ console.error(` ❌ Failed to create session: ${err.error || res.statusText}`);
86
+ process.exit(1);
87
+ }
88
+ const session = await res.json();
89
+ sessionId = session.id;
90
+ console.log(` ✅ Session created: ${session.windowName}`);
91
+ console.log(` ID: ${sessionId}`);
92
+ }
93
+ catch (e) {
94
+ const cause = e.cause;
95
+ if (cause?.code === 'ECONNREFUSED') {
96
+ console.error(` ❌ Cannot connect to Aegis on port ${port}.`);
97
+ console.error(` Start the server first: aegis-bridge`);
98
+ }
99
+ else {
100
+ console.error(` ❌ ${getErrorMessage(e)}`);
101
+ }
102
+ process.exit(1);
103
+ }
104
+ // Send brief
105
+ try {
106
+ const res = await fetch(`${baseUrl}/v1/sessions/${sessionId}/send`, {
107
+ method: 'POST',
108
+ headers: { 'Content-Type': 'application/json' },
109
+ body: JSON.stringify({ text: brief }),
110
+ });
111
+ const result = await res.json();
112
+ if (result.delivered) {
113
+ console.log(` ✅ Brief delivered (attempt ${result.attempts})`);
114
+ }
115
+ else {
116
+ console.log(` ⚠️ Brief sent but delivery not confirmed after ${result.attempts} attempts`);
117
+ }
118
+ }
119
+ catch (e) {
120
+ console.error(` ⚠️ Failed to send brief: ${getErrorMessage(e)}`);
121
+ }
122
+ // Print next steps
123
+ console.log('');
124
+ console.log(' Next steps:');
125
+ console.log(` Status: curl ${baseUrl}/v1/sessions/${sessionId}/health`);
126
+ console.log(` Read: curl ${baseUrl}/v1/sessions/${sessionId}/read`);
127
+ console.log(` Kill: curl -X DELETE ${baseUrl}/v1/sessions/${sessionId}`);
128
+ }
129
+ /** Main CLI entry point that dispatches subcommands and bootstraps the server. */
130
+ async function main() {
131
+ const args = process.argv.slice(2);
132
+ // Help
133
+ if (args.includes('--help') || args.includes('-h')) {
134
+ console.log(`
135
+ aegis-bridge — Claude Code session bridge
136
+
137
+ Usage:
138
+ aegis-bridge Start the server (port 9100)
139
+ aegis-bridge --port 3000 Custom port
140
+ aegis-bridge create "brief" Create a session and send brief
141
+ aegis-bridge mcp Start MCP server (stdio transport)
142
+ aegis-bridge --help Show this help
143
+
144
+ Create:
145
+ aegis-bridge create "Build a login page" --cwd /path/to/project
146
+ aegis-bridge create "Fix the tests" (uses current directory)
147
+
148
+ MCP server:
149
+ aegis-bridge mcp Start MCP stdio server
150
+ aegis-bridge mcp --port 3000 Custom Aegis API port
151
+ claude mcp add aegis -- npx aegis-bridge mcp
152
+
153
+ Environment variables:
154
+ AEGIS_PORT Server port (default: 9100)
155
+ AEGIS_HOST Server host (default: 127.0.0.1)
156
+ AEGIS_AUTH_TOKEN Bearer token for API auth
157
+ AEGIS_TMUX_SESSION tmux session name (default: aegis)
158
+ AEGIS_STATE_DIR State directory (default: ~/.aegis)
159
+ AEGIS_TG_TOKEN Telegram bot token
160
+ AEGIS_TG_GROUP Telegram group chat ID
161
+ AEGIS_TG_ALLOWED_USERS Allowed Telegram user IDs (comma-separated)
162
+ AEGIS_WEBHOOKS Webhook URLs (comma-separated)
163
+
164
+ API:
165
+ POST /v1/sessions Create a session
166
+ GET /v1/sessions List sessions
167
+ GET /v1/sessions/:id Get session
168
+ POST /v1/sessions/:id/send Send message
169
+ GET /v1/sessions/:id/read Read messages
170
+ GET /v1/sessions/:id/health Health check
171
+ DEL /v1/sessions/:id Kill session
172
+ GET /v1/health Server health
173
+
174
+ Docs: https://github.com/OneStepAt4time/aegis
175
+ `);
176
+ process.exit(0);
177
+ }
178
+ // Version
179
+ if (args.includes('--version') || args.includes('-v')) {
180
+ console.log(`aegis-bridge v${VERSION}`);
181
+ process.exit(0);
182
+ }
183
+ // Subcommand: mcp
184
+ if (args[0] === 'mcp') {
185
+ const mcpArgs = args.slice(1);
186
+ let mcpPort = parseIntSafe(process.env.AEGIS_PORT, 9100);
187
+ const mcpPortIdx = mcpArgs.indexOf('--port');
188
+ if (mcpPortIdx !== -1 && mcpArgs[mcpPortIdx + 1]) {
189
+ mcpPort = parseIntSafe(mcpArgs[mcpPortIdx + 1], 9100);
190
+ }
191
+ const mcpAuth = process.env.AEGIS_AUTH_TOKEN || process.env.AEGIS_TOKEN;
192
+ const { startMcpServer } = await import('./mcp-server.js');
193
+ await startMcpServer(mcpPort, mcpAuth);
194
+ return; // stdio server runs until stdin closes
195
+ }
196
+ // Subcommand: create
197
+ if (args[0] === 'create') {
198
+ await handleCreate(args.slice(1));
199
+ process.exit(0);
200
+ }
201
+ // Port override from CLI
202
+ const portIdx = args.indexOf('--port');
203
+ if (portIdx !== -1 && args[portIdx + 1]) {
204
+ process.env.AEGIS_PORT = args[portIdx + 1];
205
+ }
206
+ // Check dependencies
207
+ const hasTmux = checkDependency('tmux', ['-V']);
208
+ const hasClaude = checkDependency('claude', ['--version']);
209
+ const tmuxVersion = hasTmux ? checkTmuxVersion(3, 3) : { ok: false, version: null };
210
+ if (!hasTmux) {
211
+ console.error(`
212
+ ❌ tmux not found.
213
+
214
+ Install tmux:
215
+ Ubuntu/Debian: sudo apt install tmux
216
+ macOS: brew install tmux
217
+ Windows: winget install psmux
218
+ `);
219
+ process.exit(1);
220
+ }
221
+ if (!tmuxVersion.ok) {
222
+ console.error(`
223
+ ❌ Unsupported tmux version${tmuxVersion.version ? ` (${tmuxVersion.version})` : ''}.
224
+
225
+ Aegis requires tmux/psmux 3.3 or newer.
226
+ `);
227
+ process.exit(1);
228
+ }
229
+ if (!hasClaude) {
230
+ console.error(`
231
+ ⚠️ Claude Code CLI not found.
232
+
233
+ Install Claude Code:
234
+ curl -fsSL https://claude.ai/install.sh | bash
235
+
236
+ Sessions will fail to start without the 'claude' command.
237
+ `);
238
+ // Don't exit — server can still start, just sessions won't work
239
+ }
240
+ const port = parseIntSafe(process.env.AEGIS_PORT, 9100);
241
+ printBanner(port);
242
+ console.log(` Dependencies:`);
243
+ console.log(` tmux: ${hasTmux ? '✅' : '❌'}`);
244
+ console.log(` claude: ${hasClaude ? '✅' : '❌'}`);
245
+ console.log('');
246
+ // Start the server
247
+ await import('./server.js');
248
+ }
249
+ main().catch(err => {
250
+ console.error('Failed to start Aegis:', err);
251
+ process.exit(1);
252
+ });