@ch4p/cli 0.2.1 → 0.2.3

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.
@@ -0,0 +1,2260 @@
1
+ import {
2
+ GatewayServer,
3
+ LogChannel,
4
+ MessageRouter,
5
+ PairingManager,
6
+ Scheduler,
7
+ SessionManager
8
+ } from "./chunk-WYXCGS55.js";
9
+ import {
10
+ BlueBubblesChannel,
11
+ ChannelRegistry,
12
+ CliChannel,
13
+ DiscordChannel,
14
+ GoogleChatChannel,
15
+ IMessageChannel,
16
+ IrcChannel,
17
+ MacOSChannel,
18
+ MatrixChannel,
19
+ SignalChannel,
20
+ SlackChannel,
21
+ TeamsChannel,
22
+ TelegramChannel,
23
+ WebChatChannel,
24
+ WhatsAppChannel,
25
+ ZaloChannel,
26
+ ZaloPersonalChannel
27
+ } from "./chunk-3CYOOHMM.js";
28
+ import {
29
+ DeepgramSTT,
30
+ DefaultSecurityPolicy,
31
+ ElevenLabsTTS,
32
+ NativeEngine,
33
+ ProviderRegistry,
34
+ VoiceProcessor,
35
+ WhisperSTT,
36
+ buildSystemPrompt,
37
+ createClaudeCliEngine,
38
+ createCodexCliEngine,
39
+ createMemoryBackend,
40
+ createObserver
41
+ } from "./chunk-6XPWXGHR.js";
42
+ import {
43
+ LoadSkillTool,
44
+ ToolRegistry
45
+ } from "./chunk-MABLWEGE.js";
46
+ import {
47
+ SkillRegistry
48
+ } from "./chunk-6BURGD2Y.js";
49
+ import {
50
+ AgentLoop,
51
+ ContextManager,
52
+ FormatVerifier,
53
+ LLMVerifier,
54
+ Session,
55
+ ToolWorkerPool,
56
+ createAutoRecallHook,
57
+ createAutoSummarizeHook
58
+ } from "./chunk-UNF4S4CA.js";
59
+ import {
60
+ backoffDelay,
61
+ generateId
62
+ } from "./chunk-YSCX2QQQ.js";
63
+ import {
64
+ getLogsDir,
65
+ loadConfig,
66
+ saveConfig
67
+ } from "./chunk-AORLXQHZ.js";
68
+ import {
69
+ BOLD,
70
+ DIM,
71
+ GREEN,
72
+ RED,
73
+ RESET,
74
+ TEAL,
75
+ YELLOW,
76
+ box,
77
+ kvRow
78
+ } from "./chunk-NMGPBPNU.js";
79
+
80
+ // src/commands/gateway.ts
81
+ import { createRequire } from "module";
82
+ import { writeHeapSnapshot } from "v8";
83
+
84
+ // ../../packages/plugin-x402/dist/index.js
85
+ import { ethers } from "ethers";
86
+ var PUBLIC_PATHS = /* @__PURE__ */ new Set([
87
+ "/health",
88
+ "/.well-known/agent.json",
89
+ "/pair"
90
+ ]);
91
+ var DEFAULT_ASSET = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
92
+ var DEFAULT_NETWORK = "base";
93
+ var DEFAULT_TIMEOUT = 300;
94
+ function pathMatches(urlPath, pattern) {
95
+ if (pattern === "*" || pattern === "/**") return true;
96
+ if (pattern.endsWith("/*")) {
97
+ const prefix = pattern.slice(0, -2);
98
+ return urlPath === prefix || urlPath.startsWith(prefix + "/");
99
+ }
100
+ return urlPath === pattern;
101
+ }
102
+ function extractPath(url) {
103
+ const qIdx = url.indexOf("?");
104
+ return qIdx >= 0 ? url.slice(0, qIdx) : url;
105
+ }
106
+ function send402(res, requirements) {
107
+ const body = JSON.stringify({
108
+ x402Version: 1,
109
+ error: "X402",
110
+ accepts: [requirements]
111
+ });
112
+ res.writeHead(402, {
113
+ "Content-Type": "application/json",
114
+ "Content-Length": Buffer.byteLength(body)
115
+ });
116
+ res.end(body);
117
+ }
118
+ function decodePaymentHeader(header) {
119
+ try {
120
+ const json = Buffer.from(header, "base64").toString("utf-8");
121
+ const parsed = JSON.parse(json);
122
+ if (typeof parsed !== "object" || parsed === null) return null;
123
+ const p = parsed;
124
+ if (p["x402Version"] !== 1) return null;
125
+ if (p["scheme"] !== "exact") return null;
126
+ if (typeof p["network"] !== "string") return null;
127
+ const payload = p["payload"];
128
+ if (typeof payload !== "object" || payload === null) return null;
129
+ const pl = payload;
130
+ if (typeof pl["signature"] !== "string") return null;
131
+ const auth = pl["authorization"];
132
+ if (typeof auth !== "object" || auth === null) return null;
133
+ const a = auth;
134
+ if (typeof a["from"] !== "string" || typeof a["to"] !== "string" || typeof a["value"] !== "string") {
135
+ return null;
136
+ }
137
+ return parsed;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+ function createX402Middleware(config) {
143
+ if (!config.enabled || !config.server) return null;
144
+ const serverCfg = config.server;
145
+ const asset = serverCfg.asset ?? DEFAULT_ASSET;
146
+ const network = serverCfg.network ?? DEFAULT_NETWORK;
147
+ const maxTimeoutSeconds = serverCfg.maxTimeoutSeconds ?? DEFAULT_TIMEOUT;
148
+ const description = serverCfg.description ?? "Payment required to access this gateway resource.";
149
+ const protectedPaths = serverCfg.protectedPaths ?? ["/*"];
150
+ return async (req, res) => {
151
+ const rawUrl = req.url ?? "/";
152
+ const urlPath = extractPath(rawUrl);
153
+ if (PUBLIC_PATHS.has(urlPath)) return false;
154
+ const isProtected = protectedPaths.some((p) => pathMatches(urlPath, p));
155
+ if (!isProtected) return false;
156
+ const matchedRoute = serverCfg.routes?.find((r) => pathMatches(urlPath, r.path));
157
+ const effectiveAmount = matchedRoute?.amount ?? serverCfg.amount;
158
+ const effectiveDescription = matchedRoute?.description ?? description;
159
+ const requirements = {
160
+ scheme: "exact",
161
+ network,
162
+ maxAmountRequired: effectiveAmount,
163
+ resource: urlPath,
164
+ description: effectiveDescription,
165
+ mimeType: "application/json",
166
+ payTo: serverCfg.payTo,
167
+ maxTimeoutSeconds,
168
+ asset,
169
+ extra: {}
170
+ };
171
+ const paymentHeader = req.headers["x-payment"];
172
+ if (!paymentHeader || typeof paymentHeader !== "string") {
173
+ send402(res, requirements);
174
+ return true;
175
+ }
176
+ const payment = decodePaymentHeader(paymentHeader);
177
+ if (!payment) {
178
+ send402(res, requirements);
179
+ return true;
180
+ }
181
+ if (payment.network !== network) {
182
+ send402(res, requirements);
183
+ return true;
184
+ }
185
+ if (serverCfg.verifyPayment) {
186
+ const allowed = await serverCfg.verifyPayment(payment, requirements);
187
+ if (!allowed) {
188
+ send402(res, requirements);
189
+ return true;
190
+ }
191
+ }
192
+ req["_x402Authenticated"] = true;
193
+ return false;
194
+ };
195
+ }
196
+ var PLACEHOLDER_SIG = "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
197
+ var X402PayTool = class {
198
+ name = "x402_pay";
199
+ description = "Generate an X-PAYMENT header for a resource that returned HTTP 402 Payment Required. Provide the 402 response body JSON and a payer wallet address. Returns the base64-encoded X-PAYMENT value to include when retrying the request. Full payment execution requires an IIdentityProvider with wallet signing support.";
200
+ weight = "lightweight";
201
+ parameters = {
202
+ type: "object",
203
+ properties: {
204
+ url: {
205
+ type: "string",
206
+ description: "The URL of the resource that returned 402 (used to match the requirement).",
207
+ minLength: 1
208
+ },
209
+ x402_response: {
210
+ type: "string",
211
+ description: "The JSON body of the 402 response. Stringify the x402 error response object.",
212
+ minLength: 2
213
+ },
214
+ wallet_address: {
215
+ type: "string",
216
+ description: "Payer wallet address (0x + 40 hex chars). If omitted, the identity provider wallet address is used when available.",
217
+ pattern: "^0x[0-9a-fA-F]{40}$"
218
+ }
219
+ },
220
+ required: ["url", "x402_response"],
221
+ additionalProperties: false
222
+ };
223
+ validate(args) {
224
+ if (typeof args !== "object" || args === null) {
225
+ return { valid: false, errors: ["Arguments must be an object."] };
226
+ }
227
+ const { url, x402_response, wallet_address } = args;
228
+ const errors = [];
229
+ if (typeof url !== "string" || url.trim().length === 0) {
230
+ errors.push("url must be a non-empty string.");
231
+ }
232
+ if (typeof x402_response !== "string" || x402_response.trim().length === 0) {
233
+ errors.push("x402_response must be a non-empty string.");
234
+ }
235
+ if (wallet_address !== void 0) {
236
+ if (typeof wallet_address !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(wallet_address)) {
237
+ errors.push("wallet_address must be a valid Ethereum address (0x + 40 hex chars).");
238
+ }
239
+ }
240
+ return errors.length > 0 ? { valid: false, errors } : { valid: true };
241
+ }
242
+ async execute(args, context) {
243
+ const validation = this.validate(args);
244
+ if (!validation.valid) {
245
+ return {
246
+ success: false,
247
+ output: "",
248
+ error: `Invalid arguments: ${validation.errors.join(" ")}`
249
+ };
250
+ }
251
+ const { url, x402_response, wallet_address } = args;
252
+ const x402Context = context;
253
+ let response;
254
+ try {
255
+ response = JSON.parse(x402_response);
256
+ } catch {
257
+ return { success: false, output: "", error: "x402_response is not valid JSON." };
258
+ }
259
+ if (!Array.isArray(response.accepts) || response.accepts.length === 0) {
260
+ return {
261
+ success: false,
262
+ output: "",
263
+ error: 'x402 response contains no payment requirements in "accepts".'
264
+ };
265
+ }
266
+ const requirements = response.accepts.find((r) => r.scheme === "exact" && r.network === "base") ?? response.accepts.find((r) => r.scheme === "exact") ?? response.accepts[0];
267
+ const payer = wallet_address ?? x402Context.agentWalletAddress;
268
+ if (!payer) {
269
+ return {
270
+ success: false,
271
+ output: "",
272
+ error: "No wallet address available. Provide wallet_address or configure agentWalletAddress via toolContextExtensions in the agent runtime."
273
+ };
274
+ }
275
+ const nowSecs = Math.floor(Date.now() / 1e3);
276
+ const randomBytes = new Uint8Array(32);
277
+ if (typeof globalThis.crypto !== "undefined") {
278
+ globalThis.crypto.getRandomValues(randomBytes);
279
+ } else {
280
+ console.warn("x402: crypto.getRandomValues unavailable; using insecure Math.random fallback for nonce.");
281
+ for (let i = 0; i < 32; i++) {
282
+ randomBytes[i] = Math.floor(Math.random() * 256);
283
+ }
284
+ }
285
+ const nonce = "0x" + Array.from(randomBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
286
+ const authorization = {
287
+ from: payer,
288
+ to: requirements.payTo,
289
+ value: requirements.maxAmountRequired,
290
+ validAfter: "0",
291
+ validBefore: String(nowSecs + requirements.maxTimeoutSeconds),
292
+ nonce
293
+ };
294
+ let signature;
295
+ let unsigned = false;
296
+ if (x402Context.x402Signer) {
297
+ try {
298
+ signature = await x402Context.x402Signer(authorization);
299
+ } catch (err) {
300
+ return {
301
+ success: false,
302
+ output: "",
303
+ error: `Signing failed: ${err.message}`
304
+ };
305
+ }
306
+ } else {
307
+ signature = PLACEHOLDER_SIG;
308
+ unsigned = true;
309
+ }
310
+ const paymentPayload = {
311
+ x402Version: 1,
312
+ scheme: "exact",
313
+ network: requirements.network,
314
+ payload: { signature, authorization }
315
+ };
316
+ const headerValue = Buffer.from(JSON.stringify(paymentPayload)).toString("base64");
317
+ const lines = [
318
+ `Resource: ${url}`,
319
+ `Network: ${requirements.network}`,
320
+ `Amount: ${requirements.maxAmountRequired} (asset ${requirements.asset})`,
321
+ `Pay to: ${requirements.payTo}`,
322
+ `From: ${payer}`,
323
+ "",
324
+ "X-PAYMENT header value (add to your retry request):",
325
+ headerValue
326
+ ];
327
+ if (unsigned) {
328
+ lines.push(
329
+ "",
330
+ "WARNING: Placeholder signature \u2014 cannot be used for real on-chain payments.",
331
+ "Configure an IIdentityProvider with a bound wallet to enable live signing."
332
+ );
333
+ }
334
+ return {
335
+ success: true,
336
+ output: lines.join("\n"),
337
+ metadata: {
338
+ headerValue,
339
+ network: requirements.network,
340
+ amount: requirements.maxAmountRequired,
341
+ payTo: requirements.payTo,
342
+ asset: requirements.asset,
343
+ unsigned
344
+ }
345
+ };
346
+ }
347
+ };
348
+ var KNOWN_TOKENS = {
349
+ base: {
350
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
351
+ chainId: 8453,
352
+ name: "USD Coin",
353
+ version: "2"
354
+ },
355
+ "base-sepolia": {
356
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
357
+ chainId: 84532,
358
+ name: "USD Coin",
359
+ version: "2"
360
+ }
361
+ };
362
+ var TRANSFER_WITH_AUTHORIZATION_TYPES = {
363
+ TransferWithAuthorization: [
364
+ { name: "from", type: "address" },
365
+ { name: "to", type: "address" },
366
+ { name: "value", type: "uint256" },
367
+ { name: "validAfter", type: "uint256" },
368
+ { name: "validBefore", type: "uint256" },
369
+ { name: "nonce", type: "bytes32" }
370
+ ]
371
+ };
372
+ function assertValidPrivateKey(key) {
373
+ if (!/^0x[a-fA-F0-9]{64}$/.test(key)) {
374
+ throw new Error(
375
+ "Invalid private key: expected a 0x-prefixed 64-character hex string (32 bytes)."
376
+ );
377
+ }
378
+ }
379
+ function createEIP712Signer(privateKey, opts = {}) {
380
+ assertValidPrivateKey(privateKey);
381
+ const wallet = new ethers.Wallet(privateKey);
382
+ const domain = {
383
+ name: opts.tokenName ?? KNOWN_TOKENS.base.name,
384
+ version: opts.tokenVersion ?? KNOWN_TOKENS.base.version,
385
+ chainId: opts.chainId ?? KNOWN_TOKENS.base.chainId,
386
+ verifyingContract: opts.tokenAddress ?? KNOWN_TOKENS.base.address
387
+ };
388
+ return async (auth) => {
389
+ const message = {
390
+ from: auth.from,
391
+ to: auth.to,
392
+ value: BigInt(auth.value),
393
+ validAfter: BigInt(auth.validAfter),
394
+ validBefore: BigInt(auth.validBefore),
395
+ nonce: auth.nonce
396
+ };
397
+ return wallet.signTypedData(domain, TRANSFER_WITH_AUTHORIZATION_TYPES, message);
398
+ };
399
+ }
400
+ function walletAddress(privateKey) {
401
+ assertValidPrivateKey(privateKey);
402
+ return new ethers.Wallet(privateKey).address;
403
+ }
404
+
405
+ // ../../packages/tunnels/dist/index.js
406
+ import { spawn } from "child_process";
407
+ import { spawn as spawn2, execSync } from "child_process";
408
+ import { spawn as spawn3 } from "child_process";
409
+ var CloudflareTunnel = class {
410
+ id = "cloudflare";
411
+ process = null;
412
+ publicUrl = null;
413
+ active = false;
414
+ startedAt = null;
415
+ // -----------------------------------------------------------------------
416
+ // ITunnelProvider implementation
417
+ // -----------------------------------------------------------------------
418
+ async start(config) {
419
+ if (this.active) {
420
+ throw new Error("Cloudflare tunnel is already running");
421
+ }
422
+ const cfg = config;
423
+ const binaryPath = cfg.binaryPath ?? "cloudflared";
424
+ const protocol = cfg.protocol ?? "http";
425
+ const localUrl = `${protocol}://localhost:${config.port}`;
426
+ const args = ["tunnel"];
427
+ if (cfg.tunnelName) {
428
+ args.push("run", "--url", localUrl, cfg.tunnelName);
429
+ } else {
430
+ args.push("--url", localUrl);
431
+ }
432
+ return new Promise((resolve, reject) => {
433
+ const child = spawn(binaryPath, args, {
434
+ env: { ...process.env },
435
+ stdio: ["ignore", "pipe", "pipe"]
436
+ });
437
+ this.process = child;
438
+ let resolved = false;
439
+ let stderr = "";
440
+ child.stderr?.on("data", (data) => {
441
+ const text = data.toString();
442
+ stderr += text;
443
+ const urlMatch = text.match(/(https:\/\/[a-z0-9-]+\.trycloudflare\.com)/);
444
+ if (urlMatch && !resolved) {
445
+ resolved = true;
446
+ this.publicUrl = urlMatch[1];
447
+ this.active = true;
448
+ this.startedAt = /* @__PURE__ */ new Date();
449
+ resolve({
450
+ publicUrl: this.publicUrl,
451
+ provider: this.id,
452
+ startedAt: this.startedAt
453
+ });
454
+ }
455
+ const namedMatch = text.match(/https:\/\/([a-z0-9-]+\.cfargotunnel\.com)/);
456
+ if (namedMatch && !resolved && cfg.tunnelName) {
457
+ resolved = true;
458
+ this.publicUrl = `https://${namedMatch[1]}`;
459
+ this.active = true;
460
+ this.startedAt = /* @__PURE__ */ new Date();
461
+ resolve({
462
+ publicUrl: this.publicUrl,
463
+ provider: this.id,
464
+ startedAt: this.startedAt
465
+ });
466
+ }
467
+ if (!resolved) {
468
+ const fallback = text.match(/(https:\/\/[a-z0-9-]+\.[a-z0-9.-]*cloudflare[a-z]*\.[a-z]+)/);
469
+ if (fallback) {
470
+ resolved = true;
471
+ this.publicUrl = fallback[1];
472
+ this.active = true;
473
+ this.startedAt = /* @__PURE__ */ new Date();
474
+ resolve({
475
+ publicUrl: this.publicUrl,
476
+ provider: this.id,
477
+ startedAt: this.startedAt
478
+ });
479
+ }
480
+ }
481
+ });
482
+ child.on("error", (err) => {
483
+ if (!resolved) {
484
+ resolved = true;
485
+ reject(new Error(`Failed to start cloudflared: ${err.message}`));
486
+ }
487
+ });
488
+ child.on("close", (code) => {
489
+ this.active = false;
490
+ if (!resolved) {
491
+ resolved = true;
492
+ reject(new Error(
493
+ `cloudflared exited with code ${code}${stderr ? ": " + stderr.slice(0, 500) : ""}`
494
+ ));
495
+ }
496
+ });
497
+ setTimeout(() => {
498
+ if (!resolved) {
499
+ resolved = true;
500
+ try {
501
+ child.kill("SIGTERM");
502
+ } catch {
503
+ }
504
+ reject(new Error("Cloudflare tunnel startup timed out"));
505
+ }
506
+ }, 3e4);
507
+ });
508
+ }
509
+ async stop() {
510
+ if (this.process && !this.process.killed) {
511
+ this.process.kill("SIGTERM");
512
+ }
513
+ this.process = null;
514
+ this.publicUrl = null;
515
+ this.active = false;
516
+ }
517
+ isActive() {
518
+ return this.active;
519
+ }
520
+ getPublicUrl() {
521
+ return this.publicUrl;
522
+ }
523
+ };
524
+ var TailscaleTunnel = class {
525
+ id = "tailscale";
526
+ process = null;
527
+ publicUrl = null;
528
+ active = false;
529
+ startedAt = null;
530
+ // -----------------------------------------------------------------------
531
+ // ITunnelProvider implementation
532
+ // -----------------------------------------------------------------------
533
+ async start(config) {
534
+ if (this.active) {
535
+ throw new Error("Tailscale tunnel is already running");
536
+ }
537
+ const cfg = config;
538
+ const binaryPath = cfg.binaryPath ?? "tailscale";
539
+ const hostname = this.getTailscaleHostname(binaryPath);
540
+ if (!hostname) {
541
+ throw new Error("Could not determine Tailscale hostname. Is Tailscale running?");
542
+ }
543
+ this.publicUrl = `https://${hostname}:${config.port}`;
544
+ const args = ["funnel", String(config.port)];
545
+ if (cfg.background) {
546
+ args.push("--bg");
547
+ }
548
+ return new Promise((resolve, reject) => {
549
+ const child = spawn2(binaryPath, args, {
550
+ env: { ...process.env },
551
+ stdio: ["ignore", "pipe", "pipe"]
552
+ });
553
+ this.process = child;
554
+ let resolved = false;
555
+ let output = "";
556
+ const handler = (data) => {
557
+ const text = data.toString();
558
+ output += text;
559
+ if ((text.includes("Funnel started") || text.includes("https://") || text.includes("Available on the internet")) && !resolved) {
560
+ resolved = true;
561
+ this.active = true;
562
+ this.startedAt = /* @__PURE__ */ new Date();
563
+ const urlMatch = text.match(/(https:\/\/[^\s]+)/);
564
+ if (urlMatch) {
565
+ this.publicUrl = urlMatch[1];
566
+ }
567
+ resolve({
568
+ publicUrl: this.publicUrl,
569
+ provider: this.id,
570
+ startedAt: this.startedAt
571
+ });
572
+ }
573
+ };
574
+ child.stdout?.on("data", handler);
575
+ child.stderr?.on("data", handler);
576
+ child.on("error", (err) => {
577
+ if (!resolved) {
578
+ resolved = true;
579
+ reject(new Error(`Failed to start tailscale funnel: ${err.message}`));
580
+ }
581
+ });
582
+ child.on("close", (code) => {
583
+ this.active = false;
584
+ if (cfg.background && code === 0 && !resolved) {
585
+ resolved = true;
586
+ this.active = true;
587
+ this.startedAt = /* @__PURE__ */ new Date();
588
+ resolve({
589
+ publicUrl: this.publicUrl,
590
+ provider: this.id,
591
+ startedAt: this.startedAt
592
+ });
593
+ return;
594
+ }
595
+ if (!resolved) {
596
+ resolved = true;
597
+ reject(new Error(
598
+ `tailscale funnel exited with code ${code}${output ? ": " + output.slice(0, 500) : ""}`
599
+ ));
600
+ }
601
+ });
602
+ setTimeout(() => {
603
+ if (!resolved) {
604
+ resolved = true;
605
+ this.active = true;
606
+ this.startedAt = /* @__PURE__ */ new Date();
607
+ resolve({
608
+ publicUrl: this.publicUrl,
609
+ provider: this.id,
610
+ startedAt: this.startedAt
611
+ });
612
+ }
613
+ }, 15e3);
614
+ });
615
+ }
616
+ async stop() {
617
+ if (this.process && !this.process.killed) {
618
+ this.process.kill("SIGTERM");
619
+ }
620
+ try {
621
+ execSync("tailscale funnel off", { stdio: "ignore", timeout: 5e3 });
622
+ } catch {
623
+ }
624
+ this.process = null;
625
+ this.publicUrl = null;
626
+ this.active = false;
627
+ }
628
+ isActive() {
629
+ return this.active;
630
+ }
631
+ getPublicUrl() {
632
+ return this.publicUrl;
633
+ }
634
+ // -----------------------------------------------------------------------
635
+ // Private helpers
636
+ // -----------------------------------------------------------------------
637
+ getTailscaleHostname(binary) {
638
+ try {
639
+ const output = execSync(`${binary} status --json`, {
640
+ timeout: 5e3,
641
+ encoding: "utf8"
642
+ });
643
+ const status = JSON.parse(output);
644
+ const dnsName = status.Self?.DNSName;
645
+ return dnsName ? dnsName.replace(/\.$/, "") : null;
646
+ } catch {
647
+ return null;
648
+ }
649
+ }
650
+ };
651
+ var NgrokTunnel = class {
652
+ id = "ngrok";
653
+ process = null;
654
+ publicUrl = null;
655
+ active = false;
656
+ startedAt = null;
657
+ // -----------------------------------------------------------------------
658
+ // ITunnelProvider implementation
659
+ // -----------------------------------------------------------------------
660
+ async start(config) {
661
+ if (this.active) {
662
+ throw new Error("ngrok tunnel is already running");
663
+ }
664
+ const cfg = config;
665
+ const binaryPath = cfg.binaryPath ?? "ngrok";
666
+ const apiUrl = cfg.apiUrl ?? "http://127.0.0.1:4040";
667
+ const args = ["http", String(config.port)];
668
+ if (cfg.authToken) {
669
+ args.push("--authtoken", cfg.authToken);
670
+ }
671
+ if (cfg.subdomain) {
672
+ args.push("--subdomain", cfg.subdomain);
673
+ }
674
+ if (cfg.region) {
675
+ args.push("--region", cfg.region);
676
+ }
677
+ const child = spawn3(binaryPath, args, {
678
+ env: { ...process.env },
679
+ stdio: ["ignore", "ignore", "pipe"]
680
+ });
681
+ this.process = child;
682
+ child.on("error", () => {
683
+ this.active = false;
684
+ });
685
+ child.on("close", () => {
686
+ this.active = false;
687
+ });
688
+ const url = await this.waitForTunnel(apiUrl, 15e3);
689
+ if (!url) {
690
+ this.stop();
691
+ throw new Error("Could not retrieve ngrok public URL from local API");
692
+ }
693
+ this.publicUrl = url;
694
+ this.active = true;
695
+ this.startedAt = /* @__PURE__ */ new Date();
696
+ return {
697
+ publicUrl: this.publicUrl,
698
+ provider: this.id,
699
+ startedAt: this.startedAt
700
+ };
701
+ }
702
+ async stop() {
703
+ if (this.process && !this.process.killed) {
704
+ this.process.kill("SIGTERM");
705
+ }
706
+ this.process = null;
707
+ this.publicUrl = null;
708
+ this.active = false;
709
+ }
710
+ isActive() {
711
+ return this.active;
712
+ }
713
+ getPublicUrl() {
714
+ return this.publicUrl;
715
+ }
716
+ // -----------------------------------------------------------------------
717
+ // Private: poll ngrok local API
718
+ // -----------------------------------------------------------------------
719
+ /**
720
+ * Poll the ngrok local API to get the public tunnel URL.
721
+ * ngrok exposes tunnel info at http://127.0.0.1:4040/api/tunnels.
722
+ */
723
+ async waitForTunnel(apiUrl, timeoutMs) {
724
+ const start = Date.now();
725
+ const pollInterval = 500;
726
+ while (Date.now() - start < timeoutMs) {
727
+ try {
728
+ const response = await fetch(`${apiUrl}/api/tunnels`);
729
+ if (response.ok) {
730
+ const data = await response.json();
731
+ const httpsTunnel = data.tunnels.find((t) => t.proto === "https");
732
+ const anyTunnel = data.tunnels[0];
733
+ const tunnel = httpsTunnel ?? anyTunnel;
734
+ if (tunnel?.public_url) {
735
+ return tunnel.public_url;
736
+ }
737
+ }
738
+ } catch {
739
+ }
740
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
741
+ }
742
+ return null;
743
+ }
744
+ };
745
+ function createTunnelProvider(name) {
746
+ switch (name) {
747
+ case "cloudflare":
748
+ return new CloudflareTunnel();
749
+ case "tailscale":
750
+ return new TailscaleTunnel();
751
+ case "ngrok":
752
+ return new NgrokTunnel();
753
+ default:
754
+ throw new Error(`Unknown tunnel provider: "${name}". Supported: cloudflare, tailscale, ngrok`);
755
+ }
756
+ }
757
+
758
+ // ../../packages/supervisor/dist/index.js
759
+ import { EventEmitter } from "events";
760
+ import { EventEmitter as EventEmitter2 } from "events";
761
+ var DEFAULT_RESTART_POLICY = {
762
+ strategy: "one-for-one",
763
+ maxRestarts: 5,
764
+ windowMs: 6e4,
765
+ backoffBaseMs: 1e3,
766
+ backoffMaxMs: 3e4
767
+ };
768
+ var MAX_CRASH_HISTORY = 256;
769
+ var DEFAULT_HEALTH_CONFIG = {
770
+ heartbeatIntervalMs: 5e3,
771
+ missedThreshold: 3
772
+ };
773
+ var HealthMonitor = class extends EventEmitter {
774
+ config;
775
+ children = /* @__PURE__ */ new Map();
776
+ checkTimer = null;
777
+ constructor(config) {
778
+ super();
779
+ this.config = { ...DEFAULT_HEALTH_CONFIG, ...config };
780
+ }
781
+ // ── Lifecycle ────────────────────────────────────────────────────────
782
+ /** Begin the periodic heartbeat check loop. */
783
+ start() {
784
+ if (this.checkTimer !== null) return;
785
+ this.checkTimer = setInterval(() => {
786
+ this.checkHeartbeats();
787
+ }, this.config.heartbeatIntervalMs);
788
+ this.checkTimer.unref();
789
+ }
790
+ /** Stop the periodic check loop and clear all tracked state. */
791
+ stop() {
792
+ if (this.checkTimer !== null) {
793
+ clearInterval(this.checkTimer);
794
+ this.checkTimer = null;
795
+ }
796
+ }
797
+ /** Remove all tracked children and stop the timer. */
798
+ dispose() {
799
+ this.stop();
800
+ this.children.clear();
801
+ this.removeAllListeners();
802
+ }
803
+ // ── Child registration ───────────────────────────────────────────────
804
+ /** Register a new child for health tracking. Idempotent. */
805
+ registerChild(childId) {
806
+ if (this.children.has(childId)) return;
807
+ this.children.set(childId, {
808
+ lastHeartbeat: Date.now(),
809
+ missedCount: 0,
810
+ healthy: true,
811
+ crashHistory: []
812
+ });
813
+ }
814
+ /** Unregister a child and drop its state. */
815
+ unregisterChild(childId) {
816
+ this.children.delete(childId);
817
+ }
818
+ // ── Heartbeat API ────────────────────────────────────────────────────
819
+ /** Record a heartbeat for a child, resetting its missed count. */
820
+ recordHeartbeat(childId) {
821
+ const state = this.children.get(childId);
822
+ if (!state) return;
823
+ const wasPreviouslyUnhealthy = !state.healthy;
824
+ state.lastHeartbeat = Date.now();
825
+ state.missedCount = 0;
826
+ state.healthy = true;
827
+ if (wasPreviouslyUnhealthy) {
828
+ this.emit("healthy", childId);
829
+ }
830
+ }
831
+ /** Whether a specific child is currently considered healthy. */
832
+ isHealthy(childId) {
833
+ const state = this.children.get(childId);
834
+ if (!state) return false;
835
+ return state.healthy;
836
+ }
837
+ // ── Crash recording ──────────────────────────────────────────────────
838
+ /** Record a crash for a child. Appends to history and emits 'crashed'. */
839
+ recordCrash(childId, error, exitCode, signal) {
840
+ let state = this.children.get(childId);
841
+ if (!state) {
842
+ state = {
843
+ lastHeartbeat: 0,
844
+ missedCount: 0,
845
+ healthy: false,
846
+ crashHistory: []
847
+ };
848
+ this.children.set(childId, state);
849
+ }
850
+ state.healthy = false;
851
+ const record = {
852
+ childId,
853
+ timestamp: Date.now(),
854
+ error,
855
+ exitCode,
856
+ signal
857
+ };
858
+ state.crashHistory.push(record);
859
+ if (state.crashHistory.length > MAX_CRASH_HISTORY) {
860
+ state.crashHistory.shift();
861
+ }
862
+ this.emit("crashed", childId, error);
863
+ }
864
+ /** Notify the monitor that a child has been restarted. */
865
+ recordRestart(childId) {
866
+ const state = this.children.get(childId);
867
+ if (state) {
868
+ state.lastHeartbeat = Date.now();
869
+ state.missedCount = 0;
870
+ state.healthy = true;
871
+ }
872
+ this.emit("restarted", childId);
873
+ }
874
+ // ── Queries ──────────────────────────────────────────────────────────
875
+ /** Get the full crash history for a child. */
876
+ getCrashHistory(childId) {
877
+ return this.children.get(childId)?.crashHistory ?? [];
878
+ }
879
+ /** Returns true only when ALL registered children are healthy. */
880
+ getOverallHealth() {
881
+ if (this.children.size === 0) return true;
882
+ for (const state of this.children.values()) {
883
+ if (!state.healthy) return false;
884
+ }
885
+ return true;
886
+ }
887
+ /** Get the health state snapshot for a specific child. */
888
+ getChildHealth(childId) {
889
+ const state = this.children.get(childId);
890
+ if (!state) return void 0;
891
+ return { ...state, crashHistory: [...state.crashHistory] };
892
+ }
893
+ // ── Internal ─────────────────────────────────────────────────────────
894
+ /** Runs on a timer to detect missed heartbeats. */
895
+ checkHeartbeats() {
896
+ const now = Date.now();
897
+ for (const [childId, state] of this.children) {
898
+ if (!state.healthy && state.missedCount >= this.config.missedThreshold) {
899
+ continue;
900
+ }
901
+ const elapsed = now - state.lastHeartbeat;
902
+ if (elapsed > this.config.heartbeatIntervalMs) {
903
+ state.missedCount += 1;
904
+ if (state.missedCount >= this.config.missedThreshold && state.healthy) {
905
+ state.healthy = false;
906
+ this.emit("unhealthy", childId, state.missedCount);
907
+ }
908
+ }
909
+ }
910
+ }
911
+ };
912
+ var Supervisor = class extends EventEmitter2 {
913
+ children = [];
914
+ childIndex = /* @__PURE__ */ new Map();
915
+ policy;
916
+ health;
917
+ running = false;
918
+ stopping = false;
919
+ /** Tracks in-flight restart promises so we can await them during stop(). */
920
+ pendingRestarts = /* @__PURE__ */ new Map();
921
+ /** AbortController used to cancel pending backoff sleeps on shutdown. */
922
+ shutdownController = null;
923
+ constructor(policy, health) {
924
+ super();
925
+ this.policy = { ...DEFAULT_RESTART_POLICY, ...policy };
926
+ this.health = health ?? new HealthMonitor();
927
+ }
928
+ // ── Queries ──────────────────────────────────────────────────────────
929
+ get isRunning() {
930
+ return this.running;
931
+ }
932
+ getChildState(id) {
933
+ const state = this.childIndex.get(id);
934
+ if (!state) return void 0;
935
+ return { ...state, restartTimestamps: [...state.restartTimestamps] };
936
+ }
937
+ getChildren() {
938
+ return this.children.map((s) => ({
939
+ ...s,
940
+ restartTimestamps: [...s.restartTimestamps]
941
+ }));
942
+ }
943
+ getHealthMonitor() {
944
+ return this.health;
945
+ }
946
+ // ── Child management ─────────────────────────────────────────────────
947
+ /**
948
+ * Register a child specification. If the supervisor is already running the
949
+ * child will be started immediately.
950
+ */
951
+ async addChild(spec) {
952
+ if (this.childIndex.has(spec.id)) {
953
+ throw new Error(`Child "${spec.id}" is already registered`);
954
+ }
955
+ const state = {
956
+ spec,
957
+ handle: null,
958
+ status: "stopped",
959
+ restartCount: 0,
960
+ restartTimestamps: []
961
+ };
962
+ this.children.push(state);
963
+ this.childIndex.set(spec.id, state);
964
+ this.health.registerChild(spec.id);
965
+ if (this.running && !this.stopping) {
966
+ await this.startChild(state);
967
+ }
968
+ }
969
+ /**
970
+ * Remove a child. Shuts the child down first if it is running.
971
+ */
972
+ async removeChild(id) {
973
+ const state = this.childIndex.get(id);
974
+ if (!state) return;
975
+ this.pendingRestarts.delete(id);
976
+ if (state.handle && state.status === "running") {
977
+ await this.stopChild(state);
978
+ }
979
+ const idx = this.children.indexOf(state);
980
+ if (idx !== -1) this.children.splice(idx, 1);
981
+ this.childIndex.delete(id);
982
+ this.health.unregisterChild(id);
983
+ }
984
+ // ── Lifecycle ────────────────────────────────────────────────────────
985
+ /**
986
+ * Start the supervisor and all registered children in order.
987
+ */
988
+ async start() {
989
+ if (this.running) return;
990
+ this.running = true;
991
+ this.stopping = false;
992
+ this.shutdownController = new AbortController();
993
+ this.health.start();
994
+ for (const child of this.children) {
995
+ if (this.stopping) break;
996
+ await this.startChild(child);
997
+ }
998
+ this.emit("supervisor:started");
999
+ }
1000
+ /**
1001
+ * Gracefully stop the supervisor and all children (reverse order).
1002
+ */
1003
+ async stop() {
1004
+ if (!this.running) return;
1005
+ this.stopping = true;
1006
+ this.shutdownController?.abort();
1007
+ if (this.pendingRestarts.size > 0) {
1008
+ await Promise.allSettled([...this.pendingRestarts.values()]);
1009
+ }
1010
+ for (let i = this.children.length - 1; i >= 0; i--) {
1011
+ const child = this.children[i];
1012
+ if (child.status === "running" || child.status === "restarting") {
1013
+ await this.stopChild(child);
1014
+ }
1015
+ }
1016
+ this.health.stop();
1017
+ this.running = false;
1018
+ this.stopping = false;
1019
+ this.pendingRestarts.clear();
1020
+ this.shutdownController = null;
1021
+ this.emit("supervisor:stopped");
1022
+ }
1023
+ // ── Manual restart ───────────────────────────────────────────────────
1024
+ /**
1025
+ * Manually restart a specific child (e.g. triggered via admin API).
1026
+ */
1027
+ async restartChild(id) {
1028
+ const state = this.childIndex.get(id);
1029
+ if (!state) throw new Error(`Unknown child "${id}"`);
1030
+ if (this.stopping) return;
1031
+ if (state.handle && state.status === "running") {
1032
+ await this.stopChild(state);
1033
+ }
1034
+ await this.startChild(state);
1035
+ }
1036
+ // ── Crash handling (called by subclasses) ────────────────────────────
1037
+ /**
1038
+ * Should be called by subclasses or child monitors when a child crashes.
1039
+ * Applies the configured restart strategy.
1040
+ */
1041
+ handleChildCrash(childId, error) {
1042
+ if (this.stopping) return;
1043
+ const state = this.childIndex.get(childId);
1044
+ if (!state) return;
1045
+ state.status = "crashed";
1046
+ state.lastError = error;
1047
+ state.handle = null;
1048
+ this.health.recordCrash(childId, error);
1049
+ this.emit("child:crashed", childId, error);
1050
+ const effectivePolicy = {
1051
+ ...this.policy,
1052
+ ...state.spec.restartPolicy
1053
+ };
1054
+ const restartPromise = this.applyStrategy(effectivePolicy, state).catch((strategyError) => {
1055
+ if (strategyError instanceof Error && strategyError.message.startsWith("Max restarts")) {
1056
+ return;
1057
+ }
1058
+ this.emit("error", strategyError);
1059
+ }).finally(() => {
1060
+ this.pendingRestarts.delete(childId);
1061
+ });
1062
+ this.pendingRestarts.set(childId, restartPromise);
1063
+ }
1064
+ // ── Strategy application ─────────────────────────────────────────────
1065
+ async applyStrategy(policy, crashedState) {
1066
+ if (this.stopping) return;
1067
+ switch (policy.strategy) {
1068
+ case "one-for-one":
1069
+ await this.restartWithBackoff(crashedState, policy);
1070
+ break;
1071
+ case "rest-for-one": {
1072
+ const idx = this.children.indexOf(crashedState);
1073
+ if (idx === -1) return;
1074
+ const toRestart = [];
1075
+ for (let i = this.children.length - 1; i > idx; i--) {
1076
+ const sibling = this.children[i];
1077
+ if (sibling.status === "running") {
1078
+ await this.stopChild(sibling);
1079
+ }
1080
+ toRestart.unshift(sibling);
1081
+ }
1082
+ await this.restartWithBackoff(crashedState, policy);
1083
+ for (const sibling of toRestart) {
1084
+ if (this.stopping) break;
1085
+ await this.startChild(sibling);
1086
+ }
1087
+ break;
1088
+ }
1089
+ case "one-for-all": {
1090
+ for (let i = this.children.length - 1; i >= 0; i--) {
1091
+ const child = this.children[i];
1092
+ if (child !== crashedState && child.status === "running") {
1093
+ await this.stopChild(child);
1094
+ }
1095
+ }
1096
+ for (const child of this.children) {
1097
+ if (this.stopping) break;
1098
+ if (child === crashedState) {
1099
+ await this.restartWithBackoff(child, policy);
1100
+ } else {
1101
+ await this.startChild(child);
1102
+ }
1103
+ }
1104
+ break;
1105
+ }
1106
+ }
1107
+ }
1108
+ // ── Backoff + restart ────────────────────────────────────────────────
1109
+ async restartWithBackoff(state, policy) {
1110
+ if (this.stopping) return;
1111
+ const now = Date.now();
1112
+ state.restartTimestamps = state.restartTimestamps.filter(
1113
+ (ts) => now - ts < policy.windowMs
1114
+ );
1115
+ if (state.restartTimestamps.length >= policy.maxRestarts) {
1116
+ state.status = "crashed";
1117
+ this.emit(
1118
+ "supervisor:max_restarts_exceeded",
1119
+ state.spec.id,
1120
+ state.restartTimestamps.length,
1121
+ policy.windowMs
1122
+ );
1123
+ throw new Error(
1124
+ `Max restarts (${policy.maxRestarts}) exceeded for "${state.spec.id}" within ${policy.windowMs}ms window`
1125
+ );
1126
+ }
1127
+ state.status = "restarting";
1128
+ state.restartCount += 1;
1129
+ state.restartTimestamps.push(now);
1130
+ const delay = backoffDelay(
1131
+ state.restartTimestamps.length - 1,
1132
+ policy.backoffBaseMs,
1133
+ policy.backoffMaxMs
1134
+ );
1135
+ try {
1136
+ await this.interruptibleSleep(delay);
1137
+ } catch {
1138
+ return;
1139
+ }
1140
+ if (this.stopping) return;
1141
+ await this.startChild(state);
1142
+ this.health.recordRestart(state.spec.id);
1143
+ if (state.handle) {
1144
+ this.emit(
1145
+ "child:restarted",
1146
+ state.spec.id,
1147
+ state.handle,
1148
+ state.restartCount
1149
+ );
1150
+ }
1151
+ }
1152
+ // ── Child start / stop primitives ────────────────────────────────────
1153
+ async startChild(state) {
1154
+ if (this.stopping) return;
1155
+ try {
1156
+ const handle = await state.spec.start();
1157
+ state.handle = handle;
1158
+ state.status = "running";
1159
+ this.emit("child:started", state.spec.id, handle);
1160
+ } catch (err) {
1161
+ const error = err instanceof Error ? err : new Error(String(err));
1162
+ state.status = "crashed";
1163
+ state.lastError = error;
1164
+ this.handleChildCrash(state.spec.id, error);
1165
+ }
1166
+ }
1167
+ async stopChild(state) {
1168
+ const { handle, spec } = state;
1169
+ if (!handle) {
1170
+ state.status = "stopped";
1171
+ return;
1172
+ }
1173
+ try {
1174
+ if (spec.shutdown) {
1175
+ await spec.shutdown(handle);
1176
+ } else {
1177
+ handle.kill();
1178
+ }
1179
+ } catch {
1180
+ try {
1181
+ handle.kill();
1182
+ } catch {
1183
+ }
1184
+ }
1185
+ state.handle = null;
1186
+ state.status = "stopped";
1187
+ this.emit("child:stopped", spec.id);
1188
+ }
1189
+ // ── Utilities ────────────────────────────────────────────────────────
1190
+ /**
1191
+ * Sleep that can be interrupted by the shutdown controller.
1192
+ * Throws if aborted so the caller can bail out.
1193
+ */
1194
+ interruptibleSleep(ms) {
1195
+ const controller = this.shutdownController;
1196
+ if (!controller || controller.signal.aborted) {
1197
+ return Promise.reject(new Error("Supervisor is shutting down"));
1198
+ }
1199
+ return new Promise((resolve, reject) => {
1200
+ const timer = setTimeout(() => {
1201
+ cleanup();
1202
+ resolve();
1203
+ }, ms);
1204
+ const onAbort = () => {
1205
+ clearTimeout(timer);
1206
+ cleanup();
1207
+ reject(new Error("Supervisor is shutting down"));
1208
+ };
1209
+ const cleanup = () => {
1210
+ controller.signal.removeEventListener("abort", onAbort);
1211
+ };
1212
+ controller.signal.addEventListener("abort", onAbort);
1213
+ });
1214
+ }
1215
+ };
1216
+
1217
+ // src/agent-router.ts
1218
+ var AgentRouter = class {
1219
+ constructor(config) {
1220
+ this.config = config;
1221
+ const rules = config.routing?.rules ?? [];
1222
+ this.compiledRules = rules.map((rule) => {
1223
+ const channelPattern = rule.channel && rule.channel !== "*" ? new RegExp(`^${rule.channel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "i") : null;
1224
+ const matchPattern = rule.match ? new RegExp(rule.match, "i") : null;
1225
+ return { channelPattern, matchPattern, agent: rule.agent };
1226
+ });
1227
+ }
1228
+ compiledRules;
1229
+ /**
1230
+ * Evaluate routing rules against an inbound message.
1231
+ *
1232
+ * @param msg The inbound message from any channel.
1233
+ * @param defaultSystemPrompt The system prompt built for the default agent.
1234
+ * @returns A RoutingDecision describing which agent to use.
1235
+ */
1236
+ route(msg, defaultSystemPrompt) {
1237
+ const channelId = msg.channelId ?? "";
1238
+ const text = msg.text ?? "";
1239
+ const agents = this.config.routing?.agents ?? {};
1240
+ for (const rule of this.compiledRules) {
1241
+ if (rule.channelPattern && !rule.channelPattern.test(channelId)) {
1242
+ continue;
1243
+ }
1244
+ if (rule.matchPattern && !rule.matchPattern.test(text)) {
1245
+ continue;
1246
+ }
1247
+ const agentCfg = agents[rule.agent];
1248
+ if (!agentCfg) {
1249
+ continue;
1250
+ }
1251
+ return this.buildDecision(rule.agent, agentCfg, defaultSystemPrompt);
1252
+ }
1253
+ return this.defaultDecision(defaultSystemPrompt);
1254
+ }
1255
+ /**
1256
+ * Return a decision for a named agent. Used by tests and direct lookups.
1257
+ */
1258
+ routeToAgent(agentName, defaultSystemPrompt) {
1259
+ const agents = this.config.routing?.agents ?? {};
1260
+ const agentCfg = agents[agentName];
1261
+ if (!agentCfg) return this.defaultDecision(defaultSystemPrompt);
1262
+ return this.buildDecision(agentName, agentCfg, defaultSystemPrompt);
1263
+ }
1264
+ /** True when routing configuration is present and has at least one rule. */
1265
+ hasRules() {
1266
+ return (this.config.routing?.rules?.length ?? 0) > 0;
1267
+ }
1268
+ /** List all defined agent names (excluding "default"). */
1269
+ agentNames() {
1270
+ return Object.keys(this.config.routing?.agents ?? {});
1271
+ }
1272
+ // -------------------------------------------------------------------------
1273
+ // Private helpers
1274
+ // -------------------------------------------------------------------------
1275
+ buildDecision(agentName, agentCfg, defaultSystemPrompt) {
1276
+ return {
1277
+ agentName,
1278
+ systemPrompt: agentCfg.systemPrompt ?? defaultSystemPrompt,
1279
+ model: agentCfg.model,
1280
+ maxIterations: agentCfg.maxIterations ?? 20,
1281
+ toolExclude: agentCfg.toolExclude ?? []
1282
+ };
1283
+ }
1284
+ defaultDecision(defaultSystemPrompt) {
1285
+ return {
1286
+ agentName: "default",
1287
+ systemPrompt: defaultSystemPrompt,
1288
+ model: void 0,
1289
+ maxIterations: 20,
1290
+ toolExclude: []
1291
+ };
1292
+ }
1293
+ };
1294
+
1295
+ // src/commands/gateway.ts
1296
+ function createChannelInstance(channelName) {
1297
+ switch (channelName) {
1298
+ case "telegram":
1299
+ return new TelegramChannel();
1300
+ case "discord":
1301
+ return new DiscordChannel();
1302
+ case "slack":
1303
+ return new SlackChannel();
1304
+ case "cli":
1305
+ return new CliChannel();
1306
+ case "matrix":
1307
+ return new MatrixChannel();
1308
+ case "whatsapp":
1309
+ return new WhatsAppChannel();
1310
+ case "signal":
1311
+ return new SignalChannel();
1312
+ case "imessage":
1313
+ return new IMessageChannel();
1314
+ case "teams":
1315
+ return new TeamsChannel();
1316
+ case "zalo":
1317
+ return new ZaloChannel();
1318
+ case "zalo-personal":
1319
+ return new ZaloPersonalChannel();
1320
+ case "bluebubbles":
1321
+ return new BlueBubblesChannel();
1322
+ case "googlechat":
1323
+ return new GoogleChatChannel();
1324
+ case "webchat":
1325
+ return new WebChatChannel();
1326
+ case "irc":
1327
+ return new IrcChannel();
1328
+ case "macos":
1329
+ return new MacOSChannel();
1330
+ default:
1331
+ return null;
1332
+ }
1333
+ }
1334
+ function createGatewayEngine(config) {
1335
+ const engineId = config.engines?.default ?? "native";
1336
+ const engineConfig = config.engines?.available?.[engineId];
1337
+ if (engineId === "claude-cli") {
1338
+ try {
1339
+ return createClaudeCliEngine({
1340
+ command: engineConfig?.command ?? void 0,
1341
+ cwd: engineConfig?.cwd ?? void 0,
1342
+ timeout: engineConfig?.timeout ?? void 0
1343
+ });
1344
+ } catch (err) {
1345
+ const msg = err instanceof Error ? err.message : String(err);
1346
+ console.warn(` ${YELLOW}\u26A0 ${engineId} engine failed to initialize: ${msg}${RESET}`);
1347
+ console.warn(` ${DIM}Falling back to native SDK engine.${RESET}`);
1348
+ }
1349
+ }
1350
+ if (engineId === "codex-cli") {
1351
+ try {
1352
+ return createCodexCliEngine({
1353
+ command: engineConfig?.command ?? void 0,
1354
+ cwd: engineConfig?.cwd ?? void 0,
1355
+ timeout: engineConfig?.timeout ?? void 0
1356
+ });
1357
+ } catch (err) {
1358
+ const msg = err instanceof Error ? err.message : String(err);
1359
+ console.warn(` ${YELLOW}\u26A0 ${engineId} engine failed to initialize: ${msg}${RESET}`);
1360
+ console.warn(` ${DIM}Falling back to native SDK engine.${RESET}`);
1361
+ }
1362
+ }
1363
+ const providerName = config.agent.provider;
1364
+ const providerConfig = config.providers?.[providerName];
1365
+ const apiKey = providerConfig?.apiKey;
1366
+ if (providerName !== "ollama" && (!apiKey || apiKey.trim().length === 0)) {
1367
+ return null;
1368
+ }
1369
+ try {
1370
+ const provider = ProviderRegistry.createProvider({
1371
+ id: providerName,
1372
+ type: providerName,
1373
+ ...providerConfig
1374
+ });
1375
+ return new NativeEngine({ provider, defaultModel: config.agent.model });
1376
+ } catch {
1377
+ return null;
1378
+ }
1379
+ }
1380
+ var MAX_CONTEXTS = 500;
1381
+ var GATEWAY_CONTEXT_MAX_TOKENS = 32e3;
1382
+ var MAX_PENDING_PER_USER = 2;
1383
+ function buildSafeConfig(cfg) {
1384
+ return {
1385
+ agent: {
1386
+ model: cfg.agent.model,
1387
+ provider: cfg.agent.provider,
1388
+ thinkingLevel: cfg.agent.thinkingLevel
1389
+ },
1390
+ gateway: { requirePairing: cfg.gateway.requirePairing },
1391
+ memory: { autoSave: cfg.memory.autoSave },
1392
+ autonomy: { level: cfg.autonomy.level },
1393
+ observability: { logLevel: cfg.observability.logLevel },
1394
+ skills: { enabled: cfg.skills.enabled },
1395
+ tunnel: { provider: cfg.tunnel.provider }
1396
+ };
1397
+ }
1398
+ function applySafeUpdates(current, updates) {
1399
+ const result = { ...current };
1400
+ if (updates.agent && typeof updates.agent === "object") {
1401
+ const u = updates.agent;
1402
+ result.agent = {
1403
+ ...current.agent,
1404
+ ...u.model !== void 0 && { model: String(u.model) },
1405
+ ...u.provider !== void 0 && { provider: String(u.provider) },
1406
+ ...u.thinkingLevel !== void 0 && { thinkingLevel: u.thinkingLevel }
1407
+ };
1408
+ }
1409
+ if (updates.gateway && typeof updates.gateway === "object") {
1410
+ const u = updates.gateway;
1411
+ result.gateway = {
1412
+ ...current.gateway,
1413
+ ...u.requirePairing !== void 0 && { requirePairing: Boolean(u.requirePairing) }
1414
+ };
1415
+ }
1416
+ if (updates.memory && typeof updates.memory === "object") {
1417
+ const u = updates.memory;
1418
+ result.memory = {
1419
+ ...current.memory,
1420
+ ...u.autoSave !== void 0 && { autoSave: Boolean(u.autoSave) }
1421
+ };
1422
+ }
1423
+ if (updates.autonomy && typeof updates.autonomy === "object") {
1424
+ const u = updates.autonomy;
1425
+ result.autonomy = {
1426
+ ...current.autonomy,
1427
+ ...u.level !== void 0 && { level: u.level }
1428
+ };
1429
+ }
1430
+ if (updates.observability && typeof updates.observability === "object") {
1431
+ const u = updates.observability;
1432
+ result.observability = {
1433
+ ...current.observability,
1434
+ ...u.logLevel !== void 0 && { logLevel: u.logLevel }
1435
+ };
1436
+ }
1437
+ if (updates.skills && typeof updates.skills === "object") {
1438
+ const u = updates.skills;
1439
+ result.skills = {
1440
+ ...current.skills,
1441
+ ...u.enabled !== void 0 && { enabled: Boolean(u.enabled) }
1442
+ };
1443
+ }
1444
+ if (updates.tunnel && typeof updates.tunnel === "object") {
1445
+ const u = updates.tunnel;
1446
+ result.tunnel = {
1447
+ ...current.tunnel,
1448
+ ...u.provider !== void 0 && { provider: String(u.provider) }
1449
+ };
1450
+ }
1451
+ return result;
1452
+ }
1453
+ async function gateway(args) {
1454
+ let config;
1455
+ try {
1456
+ config = loadConfig();
1457
+ } catch (err) {
1458
+ const message = err instanceof Error ? err.message : String(err);
1459
+ console.error(`
1460
+ ${RED}Failed to load config:${RESET} ${message}`);
1461
+ console.error(` ${DIM}Run ${TEAL}ch4p onboard${DIM} to set up ch4p.${RESET}
1462
+ `);
1463
+ process.exitCode = 1;
1464
+ return;
1465
+ }
1466
+ let port = config.gateway.port;
1467
+ for (let i = 0; i < args.length; i++) {
1468
+ if (args[i] === "--port" && args[i + 1]) {
1469
+ const parsed = parseInt(args[i + 1], 10);
1470
+ if (!isNaN(parsed) && parsed > 0 && parsed <= 65535) {
1471
+ port = parsed;
1472
+ }
1473
+ }
1474
+ }
1475
+ const host = config.gateway.allowPublicBind ? "0.0.0.0" : "127.0.0.1";
1476
+ const requirePairing = config.gateway.requirePairing;
1477
+ const sessionManager = new SessionManager();
1478
+ const pairingManager = requirePairing ? new PairingManager() : void 0;
1479
+ const engine = createGatewayEngine(config);
1480
+ if (!engine) {
1481
+ const providerName = config.agent?.provider ?? "unknown";
1482
+ console.error(`
1483
+ ${RED}No engine available.${RESET} Provider "${providerName}" has no API key.`);
1484
+ console.error(` ${DIM}Run ${TEAL}ch4p onboard${DIM} to configure a provider, or set the API key`);
1485
+ console.error(` ${DIM}in ${TEAL}~/.ch4p/.env${DIM} as ${TEAL}${providerName.toUpperCase()}_API_KEY${RESET}
1486
+ `);
1487
+ process.exitCode = 1;
1488
+ return;
1489
+ }
1490
+ let skillRegistry;
1491
+ try {
1492
+ if (config.skills?.enabled && config.skills?.paths?.length) {
1493
+ skillRegistry = SkillRegistry.createFromPaths(config.skills.paths);
1494
+ }
1495
+ } catch {
1496
+ }
1497
+ let memoryBackend;
1498
+ try {
1499
+ const memCfg = {
1500
+ backend: config.memory.backend,
1501
+ vectorWeight: config.memory.vectorWeight,
1502
+ keywordWeight: config.memory.keywordWeight,
1503
+ embeddingProvider: config.memory.embeddingProvider,
1504
+ openaiApiKey: config.providers?.openai?.apiKey || void 0
1505
+ };
1506
+ memoryBackend = createMemoryBackend(memCfg);
1507
+ } catch {
1508
+ }
1509
+ const hasMemory = !!memoryBackend;
1510
+ const hasSearch = !!(config.search?.enabled && config.search.apiKey);
1511
+ const defaultSystemPrompt = buildSystemPrompt({ hasMemory, hasSearch, skillRegistry });
1512
+ const defaultSessionConfig = {
1513
+ engineId: config.engines?.default ?? "native",
1514
+ model: config.agent.model,
1515
+ provider: config.agent.provider,
1516
+ systemPrompt: defaultSystemPrompt
1517
+ };
1518
+ let sharedVerifier;
1519
+ const vCfg = config.verification;
1520
+ if (vCfg?.enabled) {
1521
+ const formatOpts = { maxToolErrorRatio: vCfg.maxToolErrorRatio ?? 0.5 };
1522
+ if (vCfg.semantic && engine) {
1523
+ try {
1524
+ const providerName = config.agent.provider;
1525
+ const providerConfig = config.providers?.[providerName];
1526
+ const verifierProvider = ProviderRegistry.createProvider({
1527
+ id: `${providerName}-verifier`,
1528
+ type: providerName,
1529
+ ...providerConfig
1530
+ });
1531
+ sharedVerifier = new LLMVerifier({ provider: verifierProvider, model: config.agent.model, formatOpts });
1532
+ } catch {
1533
+ sharedVerifier = new FormatVerifier(formatOpts);
1534
+ }
1535
+ } else {
1536
+ sharedVerifier = new FormatVerifier(formatOpts);
1537
+ }
1538
+ }
1539
+ const agentRouter = new AgentRouter(config);
1540
+ if (agentRouter.hasRules()) {
1541
+ const agents = config.routing?.agents ?? {};
1542
+ const rules = config.routing?.rules ?? [];
1543
+ for (const rule of rules) {
1544
+ if (rule.agent && !agents[rule.agent]) {
1545
+ console.warn(
1546
+ ` ${YELLOW}\u26A0 Routing rule references undefined agent "${rule.agent}" \u2014 rule will be skipped.${RESET}`
1547
+ );
1548
+ }
1549
+ }
1550
+ }
1551
+ let agentRegistration;
1552
+ if (config.identity?.enabled) {
1553
+ agentRegistration = {
1554
+ type: "AgentRegistrationFile",
1555
+ name: "ch4p",
1556
+ description: "ch4p personal AI assistant",
1557
+ image: "",
1558
+ services: [],
1559
+ active: true,
1560
+ ...config.identity.agentId ? { agentId: config.identity.agentId } : {},
1561
+ ...config.identity.chainId ? { chainId: config.identity.chainId } : {}
1562
+ };
1563
+ }
1564
+ const x402Cfg = config.x402;
1565
+ const x402Middleware = x402Cfg ? createX402Middleware(x402Cfg) : null;
1566
+ if (x402Cfg?.enabled && x402Cfg.client?.privateKey) {
1567
+ if (!/^0x[a-fA-F0-9]{64}$/.test(x402Cfg.client.privateKey)) {
1568
+ console.error(`
1569
+ ${RED}x402.client.privateKey is invalid:${RESET} expected a 0x-prefixed 64-character hex string.`);
1570
+ console.error(` ${DIM}Set it via the X402_PRIVATE_KEY env var: ${TEAL}"privateKey": "\${X402_PRIVATE_KEY}"${RESET}
1571
+ `);
1572
+ process.exitCode = 1;
1573
+ return;
1574
+ }
1575
+ }
1576
+ const messageRouter = new MessageRouter(sessionManager, defaultSessionConfig);
1577
+ const obsCfg = {
1578
+ observers: config.observability.observers ?? ["console"],
1579
+ logLevel: config.observability.logLevel ?? "info",
1580
+ logPath: `${getLogsDir()}/gateway.jsonl`
1581
+ };
1582
+ const observer = createObserver(obsCfg);
1583
+ const _require = createRequire(import.meta.url);
1584
+ let workerPool;
1585
+ try {
1586
+ const workerScriptPath = _require.resolve("@ch4p/agent/worker");
1587
+ workerPool = new ToolWorkerPool({
1588
+ workerScript: workerScriptPath,
1589
+ maxWorkers: 4,
1590
+ taskTimeoutMs: 6e4
1591
+ });
1592
+ } catch {
1593
+ workerPool = void 0;
1594
+ }
1595
+ let voiceProcessor;
1596
+ const voiceCfg = config.voice;
1597
+ if (voiceCfg?.enabled) {
1598
+ try {
1599
+ const sttProvider = voiceCfg.stt.provider === "deepgram" ? new DeepgramSTT({ apiKey: voiceCfg.stt.apiKey ?? "" }) : new WhisperSTT({ apiKey: voiceCfg.stt.apiKey ?? "" });
1600
+ const ttsProvider = voiceCfg.tts.provider === "elevenlabs" ? new ElevenLabsTTS({ apiKey: voiceCfg.tts.apiKey ?? "", voiceId: voiceCfg.tts.voiceId }) : void 0;
1601
+ voiceProcessor = new VoiceProcessor({
1602
+ stt: sttProvider,
1603
+ tts: ttsProvider,
1604
+ config: voiceCfg
1605
+ });
1606
+ } catch {
1607
+ }
1608
+ }
1609
+ let inFlightCount = 0;
1610
+ let drainResolve = null;
1611
+ function waitForDrain(timeoutMs = 3e4) {
1612
+ if (inFlightCount === 0) return Promise.resolve();
1613
+ return new Promise((resolve) => {
1614
+ const timer = setTimeout(() => {
1615
+ drainResolve = null;
1616
+ console.log(` ${YELLOW}\u26A0 Drain timeout after ${timeoutMs / 1e3}s \u2014 ${inFlightCount} message(s) still in flight${RESET}`);
1617
+ resolve();
1618
+ }, timeoutMs);
1619
+ drainResolve = () => {
1620
+ clearTimeout(timer);
1621
+ resolve();
1622
+ };
1623
+ });
1624
+ }
1625
+ function trackInflight(delta) {
1626
+ inFlightCount += delta;
1627
+ if (inFlightCount <= 0 && drainResolve) {
1628
+ inFlightCount = 0;
1629
+ const cb = drainResolve;
1630
+ drainResolve = null;
1631
+ cb();
1632
+ }
1633
+ }
1634
+ const conversationContexts = /* @__PURE__ */ new Map();
1635
+ const inFlightLoops = /* @__PURE__ */ new Map();
1636
+ const pendingMessages = /* @__PURE__ */ new Map();
1637
+ const rawWebhookHandlers = /* @__PURE__ */ new Map();
1638
+ const logChannel = new LogChannel({
1639
+ onResponse: () => {
1640
+ }
1641
+ });
1642
+ const server = new GatewayServer({
1643
+ port,
1644
+ host,
1645
+ sessionManager,
1646
+ pairingManager,
1647
+ defaultSessionConfig,
1648
+ agentRegistration,
1649
+ preHandler: x402Middleware ?? void 0,
1650
+ onWebhook: (name, payload) => {
1651
+ const syntheticMsg = {
1652
+ id: generateId(16),
1653
+ channelId: `webhook:${name}`,
1654
+ from: { channelId: `webhook:${name}`, userId: payload.userId ?? "webhook" },
1655
+ text: payload.message,
1656
+ timestamp: /* @__PURE__ */ new Date()
1657
+ };
1658
+ handleInboundMessage({
1659
+ msg: syntheticMsg,
1660
+ channel: logChannel,
1661
+ router: messageRouter,
1662
+ engine,
1663
+ config,
1664
+ observer,
1665
+ conversationContexts,
1666
+ agentRouter,
1667
+ defaultSystemPrompt,
1668
+ memoryBackend,
1669
+ skillRegistry,
1670
+ voiceProcessor,
1671
+ onInflightChange: trackInflight,
1672
+ workerPool,
1673
+ inFlightLoops,
1674
+ pendingMessages,
1675
+ sharedVerifier
1676
+ });
1677
+ },
1678
+ onRawWebhook: (name, body) => {
1679
+ const handler = rawWebhookHandlers.get(name);
1680
+ if (!handler) return false;
1681
+ handler(body);
1682
+ return true;
1683
+ },
1684
+ onSteer: (sessionId, message) => {
1685
+ for (const entry of inFlightLoops.values()) {
1686
+ if (entry.loop.getSessionId() === sessionId) {
1687
+ entry.loop.steerEngine(message);
1688
+ return;
1689
+ }
1690
+ }
1691
+ },
1692
+ onGetConfig: () => buildSafeConfig(config),
1693
+ onSaveConfig: async (updates) => {
1694
+ config = applySafeUpdates(config, updates);
1695
+ saveConfig(config);
1696
+ }
1697
+ });
1698
+ try {
1699
+ await server.start();
1700
+ } catch (err) {
1701
+ const message = err instanceof Error ? err.message : String(err);
1702
+ console.error(` ${RED}Failed to start gateway:${RESET} ${message}`);
1703
+ process.exitCode = 1;
1704
+ return;
1705
+ }
1706
+ const addr = server.getAddress();
1707
+ const bindDisplay = addr ? `${addr.host}:${addr.port}` : `${host}:${port}`;
1708
+ console.log("\n" + box("ch4p Gateway", [
1709
+ kvRow("Server", `${GREEN}${BOLD}listening${RESET} on ${bindDisplay}`),
1710
+ kvRow("Pairing", requirePairing ? `${GREEN}required${RESET}` : `${YELLOW}disabled${RESET}`),
1711
+ kvRow("Engine", engine ? engine.name : `${YELLOW}none (no API key)${RESET}`),
1712
+ kvRow("Memory", memoryBackend ? config.memory.backend : `${DIM}disabled${RESET}`),
1713
+ kvRow("Voice", voiceProcessor ? `${GREEN}enabled${RESET} (STT: ${voiceCfg?.stt.provider ?? "?"}, TTS: ${voiceCfg?.tts.provider ?? "none"})` : `${DIM}disabled${RESET}`),
1714
+ kvRow("Workers", workerPool ? `${GREEN}enabled${RESET} ${DIM}(max 4 threads)${RESET}` : `${DIM}inline (worker script not built)${RESET}`),
1715
+ kvRow("Identity", agentRegistration ? `${GREEN}enabled${RESET} (chain ${config.identity?.chainId ?? 8453})` : `${DIM}disabled${RESET}`),
1716
+ kvRow("x402", x402Cfg?.enabled ? `${GREEN}enabled${RESET} ${DIM}(${x402Cfg.server?.network ?? "base"})${RESET}` : `${DIM}disabled${RESET}`)
1717
+ ]));
1718
+ console.log("");
1719
+ console.log(` ${DIM}Routes:${RESET}`);
1720
+ console.log(` ${DIM} GET /health - liveness probe${RESET}`);
1721
+ console.log(` ${DIM} GET /ready - readiness probe${RESET}`);
1722
+ if (agentRegistration) {
1723
+ console.log(` ${DIM} GET /.well-known/agent.json - agent discovery${RESET}`);
1724
+ }
1725
+ console.log(` ${DIM} POST /pair - exchange pairing code for token${RESET}`);
1726
+ console.log(` ${DIM} GET /sessions - list active sessions${RESET}`);
1727
+ console.log(` ${DIM} POST /sessions - create a new session${RESET}`);
1728
+ console.log(` ${DIM} GET /sessions/:id - get session details${RESET}`);
1729
+ console.log(` ${DIM} POST /sessions/:id/steer - inject message into session${RESET}`);
1730
+ console.log(` ${DIM} DELETE /sessions/:id - end a session${RESET}`);
1731
+ console.log("");
1732
+ const channelRegistry = new ChannelRegistry();
1733
+ const startedChannels = [];
1734
+ const channelNames = Object.keys(config.channels);
1735
+ const channelSupervisor = new Supervisor({ strategy: "one-for-one", maxRestarts: 5, windowMs: 6e4 });
1736
+ channelSupervisor.on("child:crashed", (childId, error) => {
1737
+ console.log(` ${YELLOW}\u26A0 Channel ${childId} crashed:${RESET} ${error.message}`);
1738
+ });
1739
+ channelSupervisor.on("child:restarted", (childId, _handle, attempt) => {
1740
+ console.log(` ${GREEN}\u2713${RESET} Channel ${childId} restarted ${DIM}(attempt ${attempt})${RESET}`);
1741
+ });
1742
+ channelSupervisor.on("supervisor:max_restarts_exceeded", (childId, count, windowMs) => {
1743
+ console.log(` ${RED}\u2717${RESET} Channel ${childId} exceeded max restarts (${count} in ${Math.round(windowMs / 1e3)}s)`);
1744
+ });
1745
+ if (channelNames.length > 0) {
1746
+ console.log(` ${BOLD}Channels:${RESET}`);
1747
+ for (const channelName of channelNames) {
1748
+ const channelCfg = config.channels[channelName];
1749
+ const channel = createChannelInstance(channelName);
1750
+ if (!channel) {
1751
+ console.log(` ${YELLOW}\u26A0 Unknown channel type: ${channelName}${RESET}`);
1752
+ continue;
1753
+ }
1754
+ try {
1755
+ await channel.start(channelCfg);
1756
+ channelRegistry.register(channel);
1757
+ startedChannels.push(channel);
1758
+ channel.onMessage((msg) => {
1759
+ handleInboundMessage({
1760
+ msg,
1761
+ channel,
1762
+ router: messageRouter,
1763
+ engine,
1764
+ config,
1765
+ observer,
1766
+ conversationContexts,
1767
+ agentRouter,
1768
+ defaultSystemPrompt,
1769
+ memoryBackend,
1770
+ skillRegistry,
1771
+ voiceProcessor,
1772
+ onInflightChange: trackInflight,
1773
+ workerPool,
1774
+ inFlightLoops,
1775
+ pendingMessages,
1776
+ sharedVerifier
1777
+ });
1778
+ });
1779
+ if (channelName === "teams" && "handleIncomingActivity" in channel) {
1780
+ rawWebhookHandlers.set("teams", (body) => {
1781
+ try {
1782
+ const activity = JSON.parse(body);
1783
+ channel.handleIncomingActivity(activity);
1784
+ } catch {
1785
+ }
1786
+ });
1787
+ }
1788
+ if (channelName === "googlechat" && "handleIncomingEvent" in channel) {
1789
+ rawWebhookHandlers.set("googlechat", (body) => {
1790
+ try {
1791
+ const event = JSON.parse(body);
1792
+ channel.handleIncomingEvent(event);
1793
+ } catch {
1794
+ }
1795
+ });
1796
+ }
1797
+ let alive = true;
1798
+ await channelSupervisor.addChild({
1799
+ id: channelName,
1800
+ start: async () => {
1801
+ if (!alive) {
1802
+ await channel.start(channelCfg);
1803
+ }
1804
+ alive = true;
1805
+ return {
1806
+ id: channelName,
1807
+ kill: () => {
1808
+ alive = false;
1809
+ void channel.stop();
1810
+ },
1811
+ isAlive: () => alive
1812
+ };
1813
+ },
1814
+ shutdown: async () => {
1815
+ alive = false;
1816
+ await channel.stop();
1817
+ }
1818
+ });
1819
+ console.log(` ${GREEN}\u2713${RESET} ${channelName} ${DIM}(${channel.name})${RESET}`);
1820
+ } catch (err) {
1821
+ const errMsg = err instanceof Error ? err.message : String(err);
1822
+ console.log(` ${RED}\u2717${RESET} ${channelName}: ${errMsg}`);
1823
+ }
1824
+ }
1825
+ await channelSupervisor.start();
1826
+ console.log("");
1827
+ } else {
1828
+ console.log(` ${DIM}No channels configured. Add channels to ~/.ch4p/config.json.${RESET}`);
1829
+ console.log("");
1830
+ }
1831
+ let tunnel = null;
1832
+ const tunnelProvider = config.tunnel.provider;
1833
+ if (tunnelProvider && tunnelProvider !== "none") {
1834
+ try {
1835
+ tunnel = createTunnelProvider(tunnelProvider);
1836
+ const tunnelCfg = {
1837
+ ...config.tunnel,
1838
+ port,
1839
+ localHost: host
1840
+ };
1841
+ const tunnelInfo = await tunnel.start(tunnelCfg);
1842
+ server.setTunnelUrl(tunnelInfo.publicUrl);
1843
+ console.log(` ${GREEN}${BOLD}Tunnel active${RESET} ${DIM}(${tunnelProvider})${RESET}`);
1844
+ console.log(` ${BOLD}Public URL${RESET} ${TEAL}${tunnelInfo.publicUrl}${RESET}`);
1845
+ console.log("");
1846
+ } catch (err) {
1847
+ const errMsg = err instanceof Error ? err.message : String(err);
1848
+ console.log(` ${YELLOW}\u26A0 Tunnel failed to start:${RESET} ${errMsg}`);
1849
+ console.log(` ${DIM}Gateway is still accessible locally at ${bindDisplay}.${RESET}`);
1850
+ console.log("");
1851
+ tunnel = null;
1852
+ }
1853
+ } else {
1854
+ console.log(` ${BOLD}Tunnel${RESET} ${DIM}disabled${RESET}`);
1855
+ console.log("");
1856
+ }
1857
+ let scheduler;
1858
+ const schedulerCfg = config.scheduler;
1859
+ if (schedulerCfg?.enabled) {
1860
+ const jobs = schedulerCfg.jobs ?? [];
1861
+ if (jobs.length > 0) {
1862
+ scheduler = new Scheduler({
1863
+ onTrigger: (job) => {
1864
+ const syntheticMsg = {
1865
+ id: generateId(16),
1866
+ channelId: `cron:${job.name}`,
1867
+ from: { channelId: `cron:${job.name}`, userId: job.userId ?? "cron" },
1868
+ text: job.message,
1869
+ timestamp: /* @__PURE__ */ new Date()
1870
+ };
1871
+ handleInboundMessage({
1872
+ msg: syntheticMsg,
1873
+ channel: logChannel,
1874
+ router: messageRouter,
1875
+ engine,
1876
+ config,
1877
+ observer,
1878
+ conversationContexts,
1879
+ agentRouter,
1880
+ defaultSystemPrompt,
1881
+ memoryBackend,
1882
+ skillRegistry,
1883
+ voiceProcessor,
1884
+ onInflightChange: trackInflight,
1885
+ workerPool,
1886
+ inFlightLoops,
1887
+ pendingMessages,
1888
+ sharedVerifier
1889
+ });
1890
+ }
1891
+ });
1892
+ for (const job of jobs) {
1893
+ try {
1894
+ scheduler.addJob(job);
1895
+ console.log(` ${GREEN}\u2713${RESET} cron: ${job.name} ${DIM}(${job.schedule})${RESET}`);
1896
+ } catch (err) {
1897
+ const errMsg = err instanceof Error ? err.message : String(err);
1898
+ console.log(` ${RED}\u2717${RESET} cron: ${job.name}: ${errMsg}`);
1899
+ }
1900
+ }
1901
+ scheduler.start();
1902
+ console.log(` ${BOLD}Scheduler${RESET} ${GREEN}running${RESET} (${scheduler.size} job${scheduler.size === 1 ? "" : "s"})`);
1903
+ console.log("");
1904
+ }
1905
+ }
1906
+ console.log(` ${BOLD}Webhooks${RESET} ${GREEN}enabled${RESET} ${DIM}(POST /webhooks/:name)${RESET}`);
1907
+ console.log("");
1908
+ if (requirePairing && pairingManager) {
1909
+ const code = pairingManager.generateCode("CLI startup");
1910
+ console.log(` ${BOLD}Initial pairing code:${RESET} ${TEAL}${BOLD}${code.code}${RESET}`);
1911
+ console.log(` ${DIM}Expires in 5 minutes. Use POST /pair to exchange for a token.${RESET}`);
1912
+ console.log("");
1913
+ }
1914
+ console.log(` ${DIM}Press Ctrl+C to stop.${RESET}
1915
+ `);
1916
+ const CONTEXT_IDLE_MS = 60 * 6e4;
1917
+ let heapSnapshotTaken = false;
1918
+ const evictionTimer = setInterval(() => {
1919
+ gatewayRateLimiter.evictStale();
1920
+ const now = Date.now();
1921
+ for (const [key, entry] of conversationContexts) {
1922
+ if (now - entry.lastActiveAt > CONTEXT_IDLE_MS) {
1923
+ conversationContexts.delete(key);
1924
+ }
1925
+ }
1926
+ sessionManager.evictIdle(CONTEXT_IDLE_MS);
1927
+ messageRouter.evictStale();
1928
+ server.evictIdleCanvas(CONTEXT_IDLE_MS);
1929
+ const heap = process.memoryUsage();
1930
+ const heapMB = Math.round(heap.heapUsed / 1024 / 1024);
1931
+ const rssMB = Math.round(heap.rss / 1024 / 1024);
1932
+ console.log(
1933
+ ` ${DIM}[eviction] heap=${heapMB}MB rss=${rssMB}MB contexts=${conversationContexts.size} sessions=${sessionManager.size}${RESET}`
1934
+ );
1935
+ if (heapMB > 2e3 && !heapSnapshotTaken) {
1936
+ heapSnapshotTaken = true;
1937
+ try {
1938
+ const snapshotPath = writeHeapSnapshot();
1939
+ console.log(` ${YELLOW}[OOM warning]${RESET} Heap at ${heapMB}MB \u2014 snapshot: ${snapshotPath}`);
1940
+ } catch {
1941
+ }
1942
+ }
1943
+ }, 5 * 6e4);
1944
+ await new Promise((resolve) => {
1945
+ const shutdown = async () => {
1946
+ clearInterval(evictionTimer);
1947
+ console.log(`
1948
+ ${DIM}Shutting down gateway...${RESET}`);
1949
+ if (scheduler) {
1950
+ scheduler.stop();
1951
+ }
1952
+ if (channelSupervisor.isRunning) {
1953
+ try {
1954
+ await channelSupervisor.stop();
1955
+ } catch {
1956
+ }
1957
+ }
1958
+ if (inFlightCount > 0) {
1959
+ console.log(` ${DIM}Draining ${inFlightCount} in-flight message(s)...${RESET}`);
1960
+ await waitForDrain(3e4);
1961
+ }
1962
+ if (tunnel) {
1963
+ try {
1964
+ await tunnel.stop();
1965
+ } catch {
1966
+ }
1967
+ }
1968
+ if (workerPool) {
1969
+ try {
1970
+ await workerPool.shutdown();
1971
+ } catch {
1972
+ }
1973
+ }
1974
+ if (memoryBackend) {
1975
+ try {
1976
+ await memoryBackend.close();
1977
+ } catch {
1978
+ }
1979
+ }
1980
+ await server.stop();
1981
+ await observer.flush?.();
1982
+ console.log(` ${DIM}Goodbye!${RESET}
1983
+ `);
1984
+ resolve();
1985
+ };
1986
+ const onSignal = () => void shutdown();
1987
+ process.once("SIGINT", onSignal);
1988
+ process.once("SIGTERM", onSignal);
1989
+ process.on("unhandledRejection", (reason) => {
1990
+ const msg = reason instanceof Error ? reason.message : String(reason);
1991
+ console.error(` ${RED}\u2717 Unhandled rejection:${RESET} ${msg}`);
1992
+ observer.onError(new Error(`Unhandled rejection: ${msg}`), { source: "gateway" });
1993
+ });
1994
+ });
1995
+ }
1996
+ var RateLimiter = class {
1997
+ constructor(maxRequests, windowMs) {
1998
+ this.maxRequests = maxRequests;
1999
+ this.windowMs = windowMs;
2000
+ }
2001
+ windows = /* @__PURE__ */ new Map();
2002
+ /** Returns true if the request is allowed; false if rate-limited. */
2003
+ allow(key) {
2004
+ const now = Date.now();
2005
+ const cutoff = now - this.windowMs;
2006
+ const timestamps = (this.windows.get(key) ?? []).filter((t) => t > cutoff);
2007
+ if (timestamps.length >= this.maxRequests) {
2008
+ return false;
2009
+ }
2010
+ timestamps.push(now);
2011
+ this.windows.set(key, timestamps);
2012
+ return true;
2013
+ }
2014
+ /** Remove keys whose timestamps have all expired. */
2015
+ evictStale() {
2016
+ const now = Date.now();
2017
+ const cutoff = now - this.windowMs;
2018
+ for (const [key, timestamps] of this.windows) {
2019
+ if (timestamps.every((t) => t <= cutoff)) {
2020
+ this.windows.delete(key);
2021
+ }
2022
+ }
2023
+ }
2024
+ };
2025
+ var gatewayRateLimiter = new RateLimiter(20, 6e4);
2026
+ function handleInboundMessage(opts) {
2027
+ const {
2028
+ msg,
2029
+ channel,
2030
+ router,
2031
+ engine,
2032
+ config,
2033
+ observer,
2034
+ conversationContexts,
2035
+ agentRouter,
2036
+ defaultSystemPrompt,
2037
+ memoryBackend,
2038
+ skillRegistry,
2039
+ voiceProcessor,
2040
+ onInflightChange,
2041
+ workerPool,
2042
+ inFlightLoops,
2043
+ pendingMessages,
2044
+ sharedVerifier
2045
+ } = opts;
2046
+ if (!engine) {
2047
+ channel.send(msg.from, {
2048
+ text: "ch4p is not configured with an LLM engine. Please set up an API key via `ch4p onboard`.",
2049
+ replyTo: msg.id
2050
+ }).catch(() => {
2051
+ });
2052
+ return;
2053
+ }
2054
+ const hasAudio = msg.attachments?.some((a) => a.type === "audio") ?? false;
2055
+ if (!msg.text && !hasAudio) return;
2056
+ const { userId } = msg.from;
2057
+ const rateLimitKey = `${msg.channelId ?? "unknown"}:${userId ?? "anonymous"}`;
2058
+ if (!gatewayRateLimiter.allow(rateLimitKey)) {
2059
+ channel.send(msg.from, {
2060
+ text: "You are sending messages too quickly. Please wait a moment.",
2061
+ replyTo: msg.id
2062
+ }).catch(() => {
2063
+ });
2064
+ return;
2065
+ }
2066
+ const userKey = `${msg.channelId ?? "unknown"}:${msg.from.userId ?? "anonymous"}`;
2067
+ if (inFlightLoops) {
2068
+ const inflight = inFlightLoops.get(userKey);
2069
+ if (inflight) {
2070
+ if (inflight.permissionPending) {
2071
+ inflight.loop.steerEngine(msg.text ?? "");
2072
+ inflight.permissionPending = false;
2073
+ } else if (pendingMessages) {
2074
+ const queue = pendingMessages.get(userKey) ?? [];
2075
+ if (queue.length < MAX_PENDING_PER_USER) {
2076
+ queue.push({ msg, channel });
2077
+ } else {
2078
+ queue[queue.length - 1] = { msg, channel };
2079
+ }
2080
+ pendingMessages.set(userKey, queue);
2081
+ if (queue.length === 1) {
2082
+ channel.send(msg.from, {
2083
+ text: "Got it \u2014 I'll get to your message once I finish what I'm working on.",
2084
+ replyTo: msg.id
2085
+ }).catch(() => {
2086
+ });
2087
+ }
2088
+ } else {
2089
+ channel.send(msg.from, {
2090
+ text: "I'm still working on your previous message. Please wait for me to finish.",
2091
+ replyTo: msg.id
2092
+ }).catch(() => {
2093
+ });
2094
+ }
2095
+ return;
2096
+ }
2097
+ }
2098
+ const routeResult = router.route(msg);
2099
+ if (!routeResult) return;
2100
+ onInflightChange?.(1);
2101
+ void (async () => {
2102
+ let runTimer;
2103
+ try {
2104
+ const processedMsg = voiceProcessor ? await voiceProcessor.processInbound(msg) : msg;
2105
+ if (!processedMsg.text) return;
2106
+ const routing = agentRouter.route(processedMsg, defaultSystemPrompt);
2107
+ const { userId: userId2, groupId, threadId } = msg.from;
2108
+ const contextKey = groupId && threadId ? `${msg.channelId ?? ""}:group:${groupId}:thread:${threadId}` : groupId ? `${msg.channelId ?? ""}:group:${groupId}:user:${userId2 ?? "anonymous"}` : `${msg.channelId ?? ""}:${userId2 ?? "anonymous"}`;
2109
+ let contextEntry = conversationContexts.get(contextKey);
2110
+ if (!contextEntry) {
2111
+ if (conversationContexts.size >= MAX_CONTEXTS) {
2112
+ let oldestKey;
2113
+ let oldestTime = Infinity;
2114
+ for (const [k, v] of conversationContexts) {
2115
+ if (v.lastActiveAt < oldestTime) {
2116
+ oldestTime = v.lastActiveAt;
2117
+ oldestKey = k;
2118
+ }
2119
+ }
2120
+ if (oldestKey) conversationContexts.delete(oldestKey);
2121
+ }
2122
+ const ctx = new ContextManager({ maxTokens: GATEWAY_CONTEXT_MAX_TOKENS });
2123
+ const initPrompt = routing.systemPrompt ?? routeResult.config.systemPrompt ?? defaultSystemPrompt;
2124
+ ctx.setSystemPrompt(initPrompt);
2125
+ contextEntry = { ctx, lastActiveAt: Date.now() };
2126
+ conversationContexts.set(contextKey, contextEntry);
2127
+ } else {
2128
+ contextEntry.lastActiveAt = Date.now();
2129
+ }
2130
+ const sharedContext = contextEntry.ctx;
2131
+ const routedSessionConfig = {
2132
+ ...routeResult.config,
2133
+ model: routing.model ?? routeResult.config.model,
2134
+ systemPrompt: routing.systemPrompt ?? routeResult.config.systemPrompt
2135
+ };
2136
+ const session = new Session(routedSessionConfig, {
2137
+ sharedContext,
2138
+ maxErrors: config.agent.maxSessionErrors
2139
+ });
2140
+ const toolExclude = config.autonomy.level === "readonly" ? ["bash", "file_write", "file_edit", "delegate", "browser"] : ["delegate", "browser"];
2141
+ if (!config.mesh?.enabled) {
2142
+ toolExclude.push("mesh");
2143
+ }
2144
+ for (const t of routing.toolExclude) {
2145
+ if (!toolExclude.includes(t)) toolExclude.push(t);
2146
+ }
2147
+ const tools = ToolRegistry.createDefault({ exclude: toolExclude });
2148
+ if (skillRegistry && skillRegistry.size > 0) {
2149
+ tools.register(new LoadSkillTool(skillRegistry));
2150
+ }
2151
+ const x402PluginCfg = config.x402;
2152
+ if (x402PluginCfg?.enabled) {
2153
+ tools.register(new X402PayTool());
2154
+ }
2155
+ const securityPolicy = new DefaultSecurityPolicy({
2156
+ workspace: routeResult.config.cwd ?? process.cwd(),
2157
+ autonomyLevel: config.autonomy.level,
2158
+ allowedCommands: config.autonomy.allowedCommands,
2159
+ blockedPaths: config.security.blockedPaths
2160
+ });
2161
+ const autoSave = config.memory.autoSave !== false;
2162
+ const memNamespace = `u:${msg.channelId ?? "unknown"}:${userId2 ?? "anonymous"}`;
2163
+ const onBeforeFirstRun = memoryBackend && autoSave ? createAutoRecallHook(memoryBackend, { namespace: memNamespace }) : void 0;
2164
+ const onAfterComplete = memoryBackend && autoSave ? createAutoSummarizeHook(memoryBackend, { namespace: memNamespace }) : void 0;
2165
+ const toolContextExtensions = {};
2166
+ if (config.search?.enabled && config.search.apiKey) {
2167
+ toolContextExtensions.searchApiKey = config.search.apiKey;
2168
+ toolContextExtensions.searchConfig = {
2169
+ maxResults: config.search.maxResults,
2170
+ country: config.search.country,
2171
+ searchLang: config.search.searchLang
2172
+ };
2173
+ }
2174
+ if (x402PluginCfg?.enabled && x402PluginCfg.client?.privateKey) {
2175
+ const cc = x402PluginCfg.client;
2176
+ toolContextExtensions.x402Signer = createEIP712Signer(cc.privateKey, {
2177
+ chainId: cc.chainId,
2178
+ tokenAddress: cc.tokenAddress,
2179
+ tokenName: cc.tokenName,
2180
+ tokenVersion: cc.tokenVersion
2181
+ });
2182
+ toolContextExtensions.agentWalletAddress = walletAddress(cc.privateKey);
2183
+ }
2184
+ toolContextExtensions.resolveEngine = (_engineId) => engine;
2185
+ const loop = new AgentLoop(session, engine, tools.list(), observer, {
2186
+ maxIterations: routing.maxIterations,
2187
+ // Per-agent routing override.
2188
+ maxRetries: 2,
2189
+ enableStateSnapshots: true,
2190
+ verifier: sharedVerifier,
2191
+ memoryBackend,
2192
+ securityPolicy,
2193
+ onBeforeFirstRun,
2194
+ onAfterComplete,
2195
+ toolContextExtensions: Object.keys(toolContextExtensions).length > 0 ? toolContextExtensions : void 0,
2196
+ workerPool,
2197
+ maxToolResults: config.agent.maxToolResults,
2198
+ maxToolOutputLen: config.agent.maxToolOutputLen,
2199
+ maxStateRecords: config.agent.maxStateRecords
2200
+ });
2201
+ if (inFlightLoops) {
2202
+ inFlightLoops.set(userKey, { loop, permissionPending: false });
2203
+ }
2204
+ const DEFAULT_RUN_TIMEOUT_MS = 3e5;
2205
+ const runTimeoutMs = config.agent.runTimeout ?? DEFAULT_RUN_TIMEOUT_MS;
2206
+ runTimer = setTimeout(() => {
2207
+ loop.abort("Gateway run timeout exceeded");
2208
+ }, runTimeoutMs);
2209
+ let responseText = "";
2210
+ const PERM_RE = /\[y\/n\]|\[Y\/N\]|do you want to|allow this|permission required/i;
2211
+ for await (const event of loop.run(processedMsg.text)) {
2212
+ if (event.type === "text") {
2213
+ responseText = event.partial;
2214
+ if (inFlightLoops && PERM_RE.test(event.partial)) {
2215
+ const entry = inFlightLoops.get(userKey);
2216
+ if (entry) entry.permissionPending = true;
2217
+ }
2218
+ } else if (event.type === "complete") {
2219
+ responseText = event.answer;
2220
+ } else if (event.type === "error") {
2221
+ responseText = `Error: ${event.error.message}`;
2222
+ }
2223
+ }
2224
+ if (responseText) {
2225
+ const outbound = {
2226
+ text: responseText,
2227
+ replyTo: msg.id,
2228
+ format: "text"
2229
+ };
2230
+ const finalOutbound = voiceProcessor ? await voiceProcessor.processOutbound(outbound) : outbound;
2231
+ await channel.send(msg.from, finalOutbound);
2232
+ }
2233
+ } catch (err) {
2234
+ const errMsg = err instanceof Error ? err.message : String(err);
2235
+ await channel.send(msg.from, {
2236
+ text: `Sorry, I encountered an error: ${errMsg}`,
2237
+ replyTo: msg.id
2238
+ }).catch(() => {
2239
+ });
2240
+ } finally {
2241
+ clearTimeout(runTimer);
2242
+ const queue = pendingMessages?.get(userKey);
2243
+ const next = queue?.shift();
2244
+ if (queue && queue.length === 0) pendingMessages.delete(userKey);
2245
+ if (next) {
2246
+ inFlightLoops?.delete(userKey);
2247
+ handleInboundMessage({ ...opts, msg: next.msg, channel: next.channel });
2248
+ } else {
2249
+ inFlightLoops?.delete(userKey);
2250
+ onInflightChange?.(-1);
2251
+ }
2252
+ }
2253
+ })();
2254
+ }
2255
+
2256
+ export {
2257
+ buildSafeConfig,
2258
+ applySafeUpdates,
2259
+ gateway
2260
+ };