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,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' | '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,9 @@
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 {};
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,58 @@
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
+ private fire;
50
+ /** Issue #25: Deliver webhook with retry + exponential backoff. */
51
+ private deliverWithRetry;
52
+ /** Issue #89 L14: Add a failed delivery to the dead letter queue. */
53
+ private addToDeadLetterQueue;
54
+ /** Issue #89 L14: Get all entries in the dead letter queue. */
55
+ getDeadLetterQueue(): DeadLetterEntry[];
56
+ /** Issue #89 L14: Clear the dead letter queue. Returns number of entries cleared. */
57
+ clearDeadLetterQueue(): number;
58
+ }
@@ -0,0 +1,162 @@
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 } from '../ssrf.js';
9
+ export class WebhookChannel {
10
+ name = 'webhook';
11
+ endpoints;
12
+ /** Issue #89 L14: In-memory dead letter queue for failed deliveries. Max 100 items. */
13
+ deadLetterQueue = [];
14
+ static DLQ_MAX_SIZE = 100;
15
+ constructor(config) {
16
+ this.endpoints = config.endpoints;
17
+ }
18
+ /** Create from AEGIS_WEBHOOKS (or legacy MANUS_WEBHOOKS) env var. Returns null if not set or invalid. */
19
+ static fromEnv() {
20
+ const raw = process.env.AEGIS_WEBHOOKS ?? process.env.MANUS_WEBHOOKS;
21
+ if (!raw)
22
+ return null;
23
+ try {
24
+ const parsed = JSON.parse(raw);
25
+ if (!Array.isArray(parsed) || parsed.length === 0)
26
+ return null;
27
+ // Validate each endpoint with Zod schema + SSRF URL check
28
+ const endpoints = [];
29
+ for (let i = 0; i < parsed.length; i++) {
30
+ const result = webhookEndpointSchema.safeParse(parsed[i]);
31
+ if (!result.success) {
32
+ console.error(`Webhook URL validation failed for endpoint ${i}: schema error`, result.error.message);
33
+ return null;
34
+ }
35
+ const urlError = validateWebhookUrl(result.data.url);
36
+ if (urlError) {
37
+ console.error(`Webhook URL validation failed for endpoint ${i}: ${urlError}`, result.data.url);
38
+ return null;
39
+ }
40
+ endpoints.push(result.data);
41
+ }
42
+ return new WebhookChannel({ endpoints });
43
+ }
44
+ catch (e) {
45
+ console.error('Failed to parse AEGIS_WEBHOOKS:', e);
46
+ return null;
47
+ }
48
+ }
49
+ filter(event) {
50
+ // Accept if ANY endpoint wants this event
51
+ return this.endpoints.some(ep => !ep.events || ep.events.length === 0 || ep.events.includes(event));
52
+ }
53
+ async onSessionCreated(payload) {
54
+ await this.fire(payload);
55
+ }
56
+ async onSessionEnded(payload) {
57
+ await this.fire(payload);
58
+ }
59
+ async onMessage(payload) {
60
+ await this.fire(payload);
61
+ }
62
+ async onStatusChange(payload) {
63
+ await this.fire(payload);
64
+ }
65
+ /** Maximum retry attempts per webhook delivery. */
66
+ static MAX_RETRIES = 5;
67
+ /** Base delay for exponential backoff (ms). */
68
+ static BASE_DELAY_MS = 1000;
69
+ /** Exponential backoff with jitter: delay * (0.5 + Math.random() * 0.5). */
70
+ static backoff(attempt) {
71
+ const base = WebhookChannel.BASE_DELAY_MS * Math.pow(2, attempt - 1);
72
+ return base * (0.5 + Math.random() * 0.5);
73
+ }
74
+ async fire(payload) {
75
+ const body = JSON.stringify({
76
+ ...payload,
77
+ api: {
78
+ read: `GET /sessions/${payload.session.id}/read`,
79
+ send: `POST /sessions/${payload.session.id}/send`,
80
+ kill: `DELETE /sessions/${payload.session.id}`,
81
+ },
82
+ });
83
+ const promises = this.endpoints.map(async (ep) => {
84
+ // Skip if endpoint filters and this event isn't in the list
85
+ if (ep.events && ep.events.length > 0 && !ep.events.includes(payload.event))
86
+ return;
87
+ await this.deliverWithRetry(ep, body, payload.event);
88
+ });
89
+ await Promise.allSettled(promises);
90
+ }
91
+ /** Issue #25: Deliver webhook with retry + exponential backoff. */
92
+ async deliverWithRetry(ep, body, event, maxRetries = WebhookChannel.MAX_RETRIES) {
93
+ let lastError = '';
94
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
95
+ try {
96
+ const res = await fetch(ep.url, {
97
+ method: 'POST',
98
+ headers: {
99
+ 'Content-Type': 'application/json',
100
+ ...(ep.headers || {}),
101
+ },
102
+ body,
103
+ signal: AbortSignal.timeout(ep.timeoutMs || 5000),
104
+ });
105
+ if (res.ok)
106
+ return; // Success
107
+ lastError = `HTTP ${res.status}`;
108
+ // Server error (5xx) — retry; client error (4xx) — don't
109
+ if (res.status >= 500 && attempt < maxRetries) {
110
+ const delay = WebhookChannel.backoff(attempt);
111
+ console.warn(`Webhook ${ep.url} returned ${res.status} for ${event} (attempt ${attempt}/${maxRetries}), retrying in ${Math.round(delay)}ms`);
112
+ await new Promise(r => setTimeout(r, delay));
113
+ continue;
114
+ }
115
+ console.error(`Webhook ${ep.url} returned ${res.status} for ${event} (attempt ${attempt}/${maxRetries})`);
116
+ // Issue #89 L14: Only add to DLQ for 5xx (server) errors, not 4xx client errors
117
+ if (res.status >= 500) {
118
+ this.addToDeadLetterQueue(ep.url, event, lastError, attempt);
119
+ }
120
+ return;
121
+ }
122
+ catch (e) {
123
+ lastError = getErrorMessage(e);
124
+ if (attempt < maxRetries) {
125
+ const delay = WebhookChannel.backoff(attempt);
126
+ console.warn(`Webhook ${ep.url} error for ${event} (attempt ${attempt}/${maxRetries}): ${lastError}, retrying in ${Math.round(delay)}ms`);
127
+ await new Promise(r => setTimeout(r, delay));
128
+ continue;
129
+ }
130
+ console.error(`Webhook ${ep.url} failed after ${maxRetries} attempts for ${event}: ${lastError}`);
131
+ this.addToDeadLetterQueue(ep.url, event, lastError, maxRetries);
132
+ }
133
+ }
134
+ }
135
+ /** Issue #89 L14: Add a failed delivery to the dead letter queue. */
136
+ addToDeadLetterQueue(endpoint, event, error, attempts) {
137
+ const entry = {
138
+ timestamp: new Date().toISOString(),
139
+ endpoint,
140
+ event,
141
+ error,
142
+ attempts,
143
+ };
144
+ this.deadLetterQueue.push(entry);
145
+ // Evict oldest entries if over max size
146
+ if (this.deadLetterQueue.length > WebhookChannel.DLQ_MAX_SIZE) {
147
+ this.deadLetterQueue = this.deadLetterQueue.slice(-WebhookChannel.DLQ_MAX_SIZE);
148
+ }
149
+ console.warn(`Webhook DLQ: added failed delivery for ${event} to ${endpoint} after ${attempts} attempts`);
150
+ }
151
+ /** Issue #89 L14: Get all entries in the dead letter queue. */
152
+ getDeadLetterQueue() {
153
+ return [...this.deadLetterQueue];
154
+ }
155
+ /** Issue #89 L14: Clear the dead letter queue. Returns number of entries cleared. */
156
+ clearDeadLetterQueue() {
157
+ const count = this.deadLetterQueue.length;
158
+ this.deadLetterQueue = [];
159
+ return count;
160
+ }
161
+ }
162
+ //# sourceMappingURL=webhook.js.map
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,223 @@
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 { execSync } 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
+ const VERSION = pkg.version;
16
+ function checkDependency(name, command) {
17
+ try {
18
+ execSync(`${command} 2>/dev/null`, { stdio: 'ignore' });
19
+ return true;
20
+ }
21
+ catch { /* command not found or exited non-zero */
22
+ return false;
23
+ }
24
+ }
25
+ function printBanner(port) {
26
+ console.log(`
27
+ ┌─────────────────────────────────────────┐
28
+ │ ⚡ Aegis v${VERSION} │
29
+ │ Claude Code Session Bridge │
30
+ └─────────────────────────────────────────┘
31
+ `);
32
+ }
33
+ /** Issue #5 stretch: create a session from CLI. */
34
+ async function handleCreate(args) {
35
+ // Parse brief text (first non-flag argument)
36
+ let brief = '';
37
+ let cwd = process.cwd();
38
+ let port = parseIntSafe(process.env.AEGIS_PORT, 9100);
39
+ for (let i = 0; i < args.length; i++) {
40
+ if (args[i] === '--cwd' && args[i + 1]) {
41
+ cwd = args[++i];
42
+ }
43
+ else if (args[i] === '--port' && args[i + 1]) {
44
+ port = parseIntSafe(args[++i], 9100);
45
+ }
46
+ else if (!args[i].startsWith('-')) {
47
+ brief = args[i];
48
+ }
49
+ }
50
+ if (!brief) {
51
+ console.error(' ❌ Missing brief. Usage: aegis-bridge create "Build a login page"');
52
+ process.exit(1);
53
+ }
54
+ const baseUrl = `http://127.0.0.1:${port}`;
55
+ const sessionName = `cc-${brief.slice(0, 20).replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase()}`;
56
+ // Create session
57
+ let sessionId;
58
+ try {
59
+ const res = await fetch(`${baseUrl}/v1/sessions`, {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({ workDir: cwd, name: sessionName }),
63
+ });
64
+ if (!res.ok) {
65
+ const err = await res.json().catch(() => ({ error: res.statusText }));
66
+ console.error(` ❌ Failed to create session: ${err.error || res.statusText}`);
67
+ process.exit(1);
68
+ }
69
+ const session = await res.json();
70
+ sessionId = session.id;
71
+ console.log(` ✅ Session created: ${session.windowName}`);
72
+ console.log(` ID: ${sessionId}`);
73
+ }
74
+ catch (e) {
75
+ const cause = e.cause;
76
+ if (cause?.code === 'ECONNREFUSED') {
77
+ console.error(` ❌ Cannot connect to Aegis on port ${port}.`);
78
+ console.error(` Start the server first: aegis-bridge`);
79
+ }
80
+ else {
81
+ console.error(` ❌ ${getErrorMessage(e)}`);
82
+ }
83
+ process.exit(1);
84
+ }
85
+ // Send brief
86
+ try {
87
+ const res = await fetch(`${baseUrl}/v1/sessions/${sessionId}/send`, {
88
+ method: 'POST',
89
+ headers: { 'Content-Type': 'application/json' },
90
+ body: JSON.stringify({ text: brief }),
91
+ });
92
+ const result = await res.json();
93
+ if (result.delivered) {
94
+ console.log(` ✅ Brief delivered (attempt ${result.attempts})`);
95
+ }
96
+ else {
97
+ console.log(` ⚠️ Brief sent but delivery not confirmed after ${result.attempts} attempts`);
98
+ }
99
+ }
100
+ catch (e) {
101
+ console.error(` ⚠️ Failed to send brief: ${getErrorMessage(e)}`);
102
+ }
103
+ // Print next steps
104
+ console.log('');
105
+ console.log(' Next steps:');
106
+ console.log(` Status: curl ${baseUrl}/v1/sessions/${sessionId}/health`);
107
+ console.log(` Read: curl ${baseUrl}/v1/sessions/${sessionId}/read`);
108
+ console.log(` Kill: curl -X DELETE ${baseUrl}/v1/sessions/${sessionId}`);
109
+ }
110
+ async function main() {
111
+ const args = process.argv.slice(2);
112
+ // Help
113
+ if (args.includes('--help') || args.includes('-h')) {
114
+ console.log(`
115
+ aegis-bridge — Claude Code session bridge
116
+
117
+ Usage:
118
+ aegis-bridge Start the server (port 9100)
119
+ aegis-bridge --port 3000 Custom port
120
+ aegis-bridge create "brief" Create a session and send brief
121
+ aegis-bridge mcp Start MCP server (stdio transport)
122
+ aegis-bridge --help Show this help
123
+
124
+ Create:
125
+ aegis-bridge create "Build a login page" --cwd /path/to/project
126
+ aegis-bridge create "Fix the tests" (uses current directory)
127
+
128
+ MCP server:
129
+ aegis-bridge mcp Start MCP stdio server
130
+ aegis-bridge mcp --port 3000 Custom Aegis API port
131
+ claude mcp add aegis -- npx aegis-bridge mcp
132
+
133
+ Environment variables:
134
+ AEGIS_PORT Server port (default: 9100)
135
+ AEGIS_HOST Server host (default: 127.0.0.1)
136
+ AEGIS_AUTH_TOKEN Bearer token for API auth
137
+ AEGIS_TMUX_SESSION tmux session name (default: aegis)
138
+ AEGIS_STATE_DIR State directory (default: ~/.aegis)
139
+ AEGIS_TG_TOKEN Telegram bot token
140
+ AEGIS_TG_GROUP Telegram group chat ID
141
+ AEGIS_TG_ALLOWED_USERS Allowed Telegram user IDs (comma-separated)
142
+ AEGIS_WEBHOOKS Webhook URLs (comma-separated)
143
+
144
+ API:
145
+ POST /v1/sessions Create a session
146
+ GET /v1/sessions List sessions
147
+ GET /v1/sessions/:id Get session
148
+ POST /v1/sessions/:id/send Send message
149
+ GET /v1/sessions/:id/read Read messages
150
+ GET /v1/sessions/:id/health Health check
151
+ DEL /v1/sessions/:id Kill session
152
+ GET /v1/health Server health
153
+
154
+ Docs: https://github.com/OneStepAt4time/aegis
155
+ `);
156
+ process.exit(0);
157
+ }
158
+ // Version
159
+ if (args.includes('--version') || args.includes('-v')) {
160
+ console.log(`aegis-bridge v${VERSION}`);
161
+ process.exit(0);
162
+ }
163
+ // Subcommand: mcp
164
+ if (args[0] === 'mcp') {
165
+ const mcpArgs = args.slice(1);
166
+ let mcpPort = parseIntSafe(process.env.AEGIS_PORT, 9100);
167
+ const mcpPortIdx = mcpArgs.indexOf('--port');
168
+ if (mcpPortIdx !== -1 && mcpArgs[mcpPortIdx + 1]) {
169
+ mcpPort = parseIntSafe(mcpArgs[mcpPortIdx + 1], 9100);
170
+ }
171
+ const mcpAuth = process.env.AEGIS_AUTH_TOKEN || process.env.AEGIS_TOKEN;
172
+ const { startMcpServer } = await import('./mcp-server.js');
173
+ await startMcpServer(mcpPort, mcpAuth);
174
+ return; // stdio server runs until stdin closes
175
+ }
176
+ // Subcommand: create
177
+ if (args[0] === 'create') {
178
+ await handleCreate(args.slice(1));
179
+ process.exit(0);
180
+ }
181
+ // Port override from CLI
182
+ const portIdx = args.indexOf('--port');
183
+ if (portIdx !== -1 && args[portIdx + 1]) {
184
+ process.env.AEGIS_PORT = args[portIdx + 1];
185
+ }
186
+ // Check dependencies
187
+ const hasTmux = checkDependency('tmux', 'tmux -V');
188
+ const hasClaude = checkDependency('claude', 'claude --version');
189
+ if (!hasTmux) {
190
+ console.error(`
191
+ ❌ tmux not found.
192
+
193
+ Install tmux:
194
+ Ubuntu/Debian: sudo apt install tmux
195
+ macOS: brew install tmux
196
+ `);
197
+ process.exit(1);
198
+ }
199
+ if (!hasClaude) {
200
+ console.error(`
201
+ ⚠️ Claude Code CLI not found.
202
+
203
+ Install Claude Code:
204
+ curl -fsSL https://claude.ai/install.sh | bash
205
+
206
+ Sessions will fail to start without the 'claude' command.
207
+ `);
208
+ // Don't exit — server can still start, just sessions won't work
209
+ }
210
+ const port = parseIntSafe(process.env.AEGIS_PORT, 9100);
211
+ printBanner(port);
212
+ console.log(` Dependencies:`);
213
+ console.log(` tmux: ${hasTmux ? '✅' : '❌'}`);
214
+ console.log(` claude: ${hasClaude ? '✅' : '❌'}`);
215
+ console.log('');
216
+ // Start the server
217
+ await import('./server.js');
218
+ }
219
+ main().catch(err => {
220
+ console.error('Failed to start Aegis:', err);
221
+ process.exit(1);
222
+ });
223
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,60 @@
1
+ /**
2
+ * config.ts — Configuration loader for Aegis.
3
+ *
4
+ * Priority (highest to lowest):
5
+ * 1. CLI argument --config <path>
6
+ * 2. ./aegis.config.json (cwd) — fallback: ./manus.config.json
7
+ * 3. ~/.aegis/config.json — fallback: ~/.manus/config.json
8
+ * 4. Defaults
9
+ *
10
+ * Environment variables override config file values.
11
+ * AEGIS_* env vars take priority; MANUS_* still supported for backward compat.
12
+ */
13
+ export interface Config {
14
+ /** HTTP server port */
15
+ port: number;
16
+ /** HTTP server host */
17
+ host: string;
18
+ /** Bearer auth token (empty = no auth) */
19
+ authToken: string;
20
+ /** tmux session name */
21
+ tmuxSession: string;
22
+ /** Directory for bridge state (state.json, session_map.json) */
23
+ stateDir: string;
24
+ /** Directory where Claude Code stores projects (~/.claude/projects) */
25
+ claudeProjectsDir: string;
26
+ /** Max session age in milliseconds */
27
+ maxSessionAgeMs: number;
28
+ /** Reaper check interval in milliseconds */
29
+ reaperIntervalMs: number;
30
+ /** Telegram bot token */
31
+ tgBotToken: string;
32
+ /** Telegram group chat ID */
33
+ tgGroupId: string;
34
+ /** Allowed Telegram user IDs for inbound commands (empty = allow all) */
35
+ tgAllowedUsers: number[];
36
+ /** Webhook URLs (comma-separated or array) */
37
+ webhooks: string[];
38
+ /** Default env vars injected into every CC session (e.g. model overrides, API keys).
39
+ * Per-session env vars from the API merge on top (per-session wins). */
40
+ defaultSessionEnv: Record<string, string>;
41
+ /** Default permission mode for new sessions (default: "bypassPermissions").
42
+ * Aegis is headless — there is no human at the TTY to approve prompts.
43
+ * Set explicitly to "default" if approval gating is needed.
44
+ * Values: "default" | "plan" | "acceptEdits" | "bypassPermissions" | "dontAsk" | "auto" */
45
+ defaultPermissionMode: string;
46
+ /** Stall threshold for monitor (ms). */
47
+ stallThresholdMs: number;
48
+ /** Maximum total concurrent SSE connections (default: 100). Env: AEGIS_SSE_MAX_CONNECTIONS */
49
+ sseMaxConnections: number;
50
+ /** Maximum concurrent SSE connections per client IP (default: 10). Env: AEGIS_SSE_MAX_PER_IP */
51
+ sseMaxPerIp: number;
52
+ /** Allowed working directories for session creation (Issue #349).
53
+ * Empty array = all directories allowed (backward compatible).
54
+ * Paths are resolved and symlink-resolved before checking. */
55
+ allowedWorkDirs: string[];
56
+ }
57
+ /** Load and merge configuration from all sources */
58
+ export declare function loadConfig(): Promise<Config>;
59
+ /** Get config without async file loading (for tests or synchronous contexts) */
60
+ export declare function getConfig(): Config;