@wolpertingerlabs/drawlatch 1.0.0-alpha.10.0 → 1.0.0-alpha.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/drawlatch.js CHANGED
@@ -81,6 +81,7 @@ try {
81
81
  follow: { type: "boolean", default: false },
82
82
  path: { type: "boolean", default: false },
83
83
  full: { type: "boolean", default: false },
84
+ requests: { type: "boolean", default: false },
84
85
  ttl: { type: "string", default: "300" },
85
86
  },
86
87
  strict: false,
@@ -185,6 +186,13 @@ switch (subcommand) {
185
186
  await cmdSync();
186
187
  }
187
188
  break;
189
+ case "watch":
190
+ if (values.help) {
191
+ printWatchHelp();
192
+ } else {
193
+ await cmdWatch();
194
+ }
195
+ break;
188
196
  case "help":
189
197
  printHelp();
190
198
  {
@@ -477,39 +485,154 @@ async function cmdLogs() {
477
485
 
478
486
  const lines = parseInt(values.lines, 10) || 50;
479
487
  const follow = values.follow;
488
+ const showRequests = values.requests;
480
489
 
481
490
  const tailArgs = follow
482
491
  ? ["-n", String(lines), "-f", LOG_FILE]
483
492
  : ["-n", String(lines), LOG_FILE];
484
493
 
485
- const tail = spawn("tail", tailArgs, { stdio: "inherit" });
494
+ if (showRequests) {
495
+ // Show everything — pipe directly to stdout
496
+ const tail = spawn("tail", tailArgs, { stdio: "inherit" });
486
497
 
487
- tail.on("error", () => {
488
- // Fallback: read last N lines with Node.js if tail is not available
489
- try {
490
- const content = readFileSync(LOG_FILE, "utf-8");
491
- const allLines = content.split("\n");
492
- const lastLines = allLines.slice(-lines).join("\n");
493
- console.log(lastLines);
494
- if (follow) {
495
- console.log(
496
- "\n(Live following not available \u2014 'tail' command not found)",
497
- );
498
- }
499
- } catch (err) {
500
- console.error(`Error reading log file: ${err.message}`);
501
- process.exit(1);
498
+ tail.on("error", () => logsFallback(lines, follow, null));
499
+
500
+ process.on("SIGINT", () => {
501
+ tail.kill();
502
+ process.exit(0);
503
+ });
504
+
505
+ await new Promise((res) => tail.on("close", res));
506
+ } else {
507
+ // Filter out [audit] lines (request/response noise from poll_events etc.)
508
+ const tail = spawn("tail", tailArgs, { stdio: ["ignore", "pipe", "inherit"] });
509
+ const grepProc = spawn("grep", ["-v", "^\\[audit\\]"], {
510
+ stdio: [tail.stdout, "inherit", "inherit"],
511
+ });
512
+
513
+ tail.on("error", () => logsFallback(lines, follow, "[audit]"));
514
+
515
+ process.on("SIGINT", () => {
516
+ tail.kill();
517
+ grepProc.kill();
518
+ process.exit(0);
519
+ });
520
+
521
+ await new Promise((res) => grepProc.on("close", res));
522
+ }
523
+ }
524
+
525
+ function logsFallback(lines, follow, filterPrefix) {
526
+ try {
527
+ const content = readFileSync(LOG_FILE, "utf-8");
528
+ let allLines = content.split("\n");
529
+ if (filterPrefix) {
530
+ allLines = allLines.filter((l) => !l.startsWith(filterPrefix));
502
531
  }
503
- });
532
+ const lastLines = allLines.slice(-lines).join("\n");
533
+ console.log(lastLines);
534
+ if (follow) {
535
+ console.log(
536
+ "\n(Live following not available \u2014 'tail' command not found)",
537
+ );
538
+ }
539
+ } catch (err) {
540
+ console.error(`Error reading log file: ${err.message}`);
541
+ process.exit(1);
542
+ }
543
+ }
504
544
 
505
- // Forward SIGINT to cleanly exit
545
+ async function cmdWatch() {
546
+ const config = loadRemoteConfig();
547
+ const port = config.port;
548
+ const host = config.host;
549
+
550
+ // Verify the server is running
551
+ const healthy = await healthCheck(host, port);
552
+ if (!healthy) {
553
+ console.error("Remote server is not running. Start it first:");
554
+ console.error(" drawlatch start");
555
+ process.exit(1);
556
+ }
557
+
558
+ const showFull = values.full;
559
+ const sourceFilter = positionals[0] || null;
560
+
561
+ console.log("Watching for events" + (sourceFilter ? ` from "${sourceFilter}"` : "") + "...");
562
+ console.log("Press Ctrl+C to stop.\n");
563
+
564
+ const controller = new AbortController();
506
565
  process.on("SIGINT", () => {
507
- tail.kill();
566
+ controller.abort();
508
567
  process.exit(0);
509
568
  });
510
569
 
511
- // Wait for tail to exit (when using --no-follow)
512
- await new Promise((res) => tail.on("close", res));
570
+ try {
571
+ const res = await fetch(
572
+ `http://${connectHost(host)}:${port}/events/stream`,
573
+ { signal: controller.signal },
574
+ );
575
+
576
+ if (!res.ok) {
577
+ console.error(`Failed to connect to event stream: HTTP ${res.status}`);
578
+ process.exit(1);
579
+ }
580
+
581
+ const decoder = new TextDecoder();
582
+ let buffer = "";
583
+
584
+ for await (const chunk of res.body) {
585
+ buffer += decoder.decode(chunk, { stream: true });
586
+
587
+ // Parse SSE lines
588
+ let newlineIdx;
589
+ while ((newlineIdx = buffer.indexOf("\n\n")) !== -1) {
590
+ const message = buffer.slice(0, newlineIdx);
591
+ buffer = buffer.slice(newlineIdx + 2);
592
+
593
+ for (const line of message.split("\n")) {
594
+ if (!line.startsWith("data: ")) continue;
595
+
596
+ try {
597
+ const event = JSON.parse(line.slice(6));
598
+
599
+ // Apply source filter if set
600
+ if (sourceFilter && event.source !== sourceFilter) continue;
601
+
602
+ const time = new Date(event.receivedAt).toLocaleTimeString();
603
+ const source = event.source;
604
+ const instance = event.instanceId ? `:${event.instanceId}` : "";
605
+ const caller = event.callerAlias || "?";
606
+ const eventType = event.eventType;
607
+
608
+ const dataStr = JSON.stringify(event.data);
609
+
610
+ if (showFull) {
611
+ console.log(
612
+ `\x1b[2m${time}\x1b[0m \x1b[36m${source}${instance}\x1b[0m \x1b[33m${eventType}\x1b[0m \x1b[2m(${caller})\x1b[0m`
613
+ );
614
+ console.log(dataStr);
615
+ console.log("");
616
+ } else {
617
+ const preview =
618
+ dataStr.length > 100
619
+ ? dataStr.slice(0, 100) + "…"
620
+ : dataStr;
621
+ console.log(
622
+ `\x1b[2m${time}\x1b[0m \x1b[36m${source}${instance}\x1b[0m \x1b[33m${eventType}\x1b[0m \x1b[2m(${caller})\x1b[0m ${preview}`
623
+ );
624
+ }
625
+ } catch {
626
+ // Skip malformed SSE data
627
+ }
628
+ }
629
+ }
630
+ }
631
+ } catch (err) {
632
+ if (err.name === "AbortError") return;
633
+ console.error(`Event stream error: ${err.message}`);
634
+ process.exit(1);
635
+ }
513
636
  }
514
637
 
515
638
  function cmdConfig() {
@@ -1126,6 +1249,7 @@ Commands:
1126
1249
  status Show server status (PID, port, uptime, health, sessions)
1127
1250
  logs View and follow remote server logs
1128
1251
  config Show effective configuration
1252
+ watch Watch ingestor events in real time
1129
1253
  doctor Validate setup and diagnose issues
1130
1254
  generate-keys Generate Ed25519 + X25519 keypairs
1131
1255
  sync Exchange keys with a callboard instance
@@ -1220,19 +1344,42 @@ function printLogsHelp() {
1220
1344
  console.log(`
1221
1345
  drawlatch logs
1222
1346
 
1223
- View server logs.
1347
+ View server logs (request/response audit lines are hidden by default).
1224
1348
 
1225
1349
  Usage: drawlatch logs [options]
1226
1350
 
1227
1351
  Options:
1228
1352
  -n, --lines <number> Number of lines to show (default: 50)
1229
1353
  --follow Follow/tail the log output (default: print and exit)
1354
+ --requests Include request/response audit lines (noisy with polling)
1230
1355
  -h, --help Show this help message
1231
1356
 
1232
1357
  Log file: ~/.drawlatch/logs/drawlatch.log
1233
1358
  `);
1234
1359
  }
1235
1360
 
1361
+ function printWatchHelp() {
1362
+ console.log(`
1363
+ drawlatch watch
1364
+
1365
+ Watch ingestor events in real time.
1366
+
1367
+ Usage: drawlatch watch [source] [options]
1368
+
1369
+ Arguments:
1370
+ source Filter to a specific connection (e.g., "discord-bot", "github")
1371
+
1372
+ Options:
1373
+ --full Show full event payload (default: truncate to 100 chars)
1374
+ -h, --help Show this help message
1375
+
1376
+ Examples:
1377
+ drawlatch watch Watch all events
1378
+ drawlatch watch github Watch only GitHub events
1379
+ drawlatch watch discord-bot --full Watch Discord events with full payloads
1380
+ `);
1381
+ }
1382
+
1236
1383
  function printConfigHelp() {
1237
1384
  console.log(`
1238
1385
  drawlatch config
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "GitHub API",
3
- "stability": "beta",
3
+ "stability": "stable",
4
4
  "category": "developer-tools",
5
5
  "description": "GitHub REST API — repositories, issues, pull requests, users, organizations, and more. Auth is handled automatically via the GITHUB_TOKEN environment variable. Includes a webhook ingestor for real-time events (push, pull_request, issues, etc.) — set GITHUB_WEBHOOK_SECRET and use poll_events to retrieve them.",
6
6
  "docsUrl": "https://docs.github.com/en/rest",
@@ -13,7 +13,8 @@
13
13
  },
14
14
  "secrets": {
15
15
  "GITHUB_TOKEN": "${GITHUB_TOKEN}",
16
- "GITHUB_WEBHOOK_SECRET": "${GITHUB_WEBHOOK_SECRET}"
16
+ "GITHUB_WEBHOOK_SECRET": "${GITHUB_WEBHOOK_SECRET}",
17
+ "GITHUB_WEBHOOK_URL": "${GITHUB_WEBHOOK_URL}"
17
18
  },
18
19
  "allowedEndpoints": ["https://api.github.com/**"],
19
20
  "ingestor": {
@@ -21,7 +22,51 @@
21
22
  "webhook": {
22
23
  "path": "github",
23
24
  "signatureHeader": "X-Hub-Signature-256",
24
- "signatureSecret": "GITHUB_WEBHOOK_SECRET"
25
+ "signatureSecret": "GITHUB_WEBHOOK_SECRET",
26
+ "callbackUrl": "${GITHUB_WEBHOOK_URL}",
27
+ "lifecycle": {
28
+ "list": {
29
+ "method": "GET",
30
+ "url": "https://api.github.com/repos/${repoFilter}/hooks",
31
+ "headers": {
32
+ "Authorization": "Bearer ${GITHUB_TOKEN}",
33
+ "Accept": "application/vnd.github+json",
34
+ "X-GitHub-Api-Version": "2022-11-28"
35
+ },
36
+ "callbackUrlField": "config.url",
37
+ "idField": "id"
38
+ },
39
+ "register": {
40
+ "method": "POST",
41
+ "url": "https://api.github.com/repos/${repoFilter}/hooks",
42
+ "headers": {
43
+ "Authorization": "Bearer ${GITHUB_TOKEN}",
44
+ "Accept": "application/vnd.github+json",
45
+ "X-GitHub-Api-Version": "2022-11-28"
46
+ },
47
+ "body": {
48
+ "name": "web",
49
+ "active": true,
50
+ "events": ["push", "pull_request", "issues", "issue_comment", "create", "delete", "release", "workflow_run", "check_run"],
51
+ "config": {
52
+ "url": "${GITHUB_WEBHOOK_URL}",
53
+ "content_type": "json",
54
+ "secret": "${GITHUB_WEBHOOK_SECRET}",
55
+ "insecure_ssl": "0"
56
+ }
57
+ },
58
+ "idField": "id"
59
+ },
60
+ "unregister": {
61
+ "method": "DELETE",
62
+ "url": "https://api.github.com/repos/${repoFilter}/hooks/${_webhookId}",
63
+ "headers": {
64
+ "Authorization": "Bearer ${GITHUB_TOKEN}",
65
+ "Accept": "application/vnd.github+json",
66
+ "X-GitHub-Api-Version": "2022-11-28"
67
+ }
68
+ }
69
+ }
25
70
  }
26
71
  },
27
72
  "testIngestor": {
@@ -37,12 +82,21 @@
37
82
  {
38
83
  "key": "repoFilter",
39
84
  "label": "Repository Filter",
40
- "description": "Only capture webhook events from these repositories (owner/repo format). Leave empty for all.",
85
+ "description": "Register a repo-level webhook and only capture events from this repository (owner/repo format). Mutually exclusive with Organization for registration.",
41
86
  "type": "text[]",
42
87
  "instanceKey": true,
43
88
  "placeholder": "e.g., octocat/Hello-World",
44
89
  "group": "Filtering"
45
90
  },
91
+ {
92
+ "key": "orgFilter",
93
+ "label": "Organization",
94
+ "description": "Register an org-level webhook that receives events from all repos in the organization. Mutually exclusive with Repository Filter for registration.",
95
+ "type": "text",
96
+ "instanceKey": true,
97
+ "placeholder": "e.g., my-org",
98
+ "group": "Filtering"
99
+ },
46
100
  {
47
101
  "key": "eventFilter",
48
102
  "label": "Event Types",
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Shared E2E test setup for connection ingestor tests.
3
+ *
4
+ * Provides helpers to:
5
+ * - Load .env.e2e environment variables
6
+ * - Build a RemoteServerConfig with in-memory keys and requested connections
7
+ * - Boot an Express server on a random port
8
+ * - Generate valid webhook signatures (GitHub, Trello)
9
+ * - Wait for ingestor state transitions and event arrival
10
+ */
11
+ import type { Server } from 'node:http';
12
+ import { IngestorManager } from '../manager.js';
13
+ import type { RemoteServerConfig } from '../../../shared/config.js';
14
+ import type { IngestedEvent, IngestorState } from '../types.js';
15
+ export declare function loadE2EEnv(): void;
16
+ /**
17
+ * Check that all required env vars are set.
18
+ * Returns the missing var names (empty array = all present).
19
+ */
20
+ export declare function checkEnvVars(vars: string[]): string[];
21
+ /**
22
+ * Build a RemoteServerConfig for E2E tests.
23
+ *
24
+ * Creates a single caller ("e2e-client") with the requested connections.
25
+ * Env vars are injected into caller.env so secret resolution works against
26
+ * process.env (already loaded by loadE2EEnv).
27
+ */
28
+ export declare function buildE2EConfig(connections: string[], opts?: {
29
+ env?: Record<string, string>;
30
+ ingestorOverrides?: Record<string, Record<string, unknown>>;
31
+ }): RemoteServerConfig;
32
+ export interface E2EServer {
33
+ server: Server;
34
+ baseUrl: string;
35
+ ingestorManager: IngestorManager;
36
+ /** Gracefully shut down server and stop all ingestors. */
37
+ teardown: () => Promise<void>;
38
+ }
39
+ /**
40
+ * Boot an Express server with the given config on a random port.
41
+ * Starts all ingestors and returns handles for testing.
42
+ */
43
+ export declare function bootServer(config: RemoteServerConfig): Promise<E2EServer>;
44
+ /** Generate a valid GitHub HMAC-SHA256 signature for a raw body. */
45
+ export declare function signGitHubPayload(rawBody: string | Buffer, secret: string): string;
46
+ /** Generate a valid Trello HMAC-SHA1 base64 signature for body + callbackUrl. */
47
+ export declare function signTrelloPayload(rawBody: string | Buffer, callbackUrl: string, secret: string): string;
48
+ /**
49
+ * Wait for an ingestor to reach a specific state.
50
+ * Polls every 250ms until the timeout is reached.
51
+ */
52
+ export declare function waitForIngestorState(manager: IngestorManager, callerAlias: string, connection: string, targetState: IngestorState, timeoutMs?: number): Promise<void>;
53
+ /**
54
+ * Poll until at least one event appears for a connection.
55
+ * Returns the events array once non-empty.
56
+ */
57
+ export declare function pollUntilEvent(manager: IngestorManager, callerAlias: string, connection: string, timeoutMs?: number): Promise<IngestedEvent[]>;
58
+ /** Common shape every IngestedEvent must satisfy (for use with toMatchObject). */
59
+ export declare const INGESTED_EVENT_SHAPE: {
60
+ id: any;
61
+ idempotencyKey: any;
62
+ receivedAt: any;
63
+ receivedAtMs: any;
64
+ callerAlias: string;
65
+ source: any;
66
+ eventType: any;
67
+ data: any;
68
+ };
69
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Shared E2E test setup for connection ingestor tests.
3
+ *
4
+ * Provides helpers to:
5
+ * - Load .env.e2e environment variables
6
+ * - Build a RemoteServerConfig with in-memory keys and requested connections
7
+ * - Boot an Express server on a random port
8
+ * - Generate valid webhook signatures (GitHub, Trello)
9
+ * - Wait for ingestor state transitions and event arrival
10
+ */
11
+ import crypto from 'node:crypto';
12
+ import path from 'node:path';
13
+ import dotenv from 'dotenv';
14
+ import { expect } from 'vitest';
15
+ import { createApp } from '../../server.js';
16
+ import { generateKeyBundle, extractPublicKeys } from '../../../shared/crypto/index.js';
17
+ import { IngestorManager } from '../manager.js';
18
+ // ── Env loading ─────────────────────────────────────────────────────────
19
+ /** Load .env.e2e from the project root (idempotent). */
20
+ let envLoaded = false;
21
+ export function loadE2EEnv() {
22
+ if (envLoaded)
23
+ return;
24
+ dotenv.config({ path: path.resolve(import.meta.dirname, '../../../../.env.e2e') });
25
+ envLoaded = true;
26
+ }
27
+ /**
28
+ * Check that all required env vars are set.
29
+ * Returns the missing var names (empty array = all present).
30
+ */
31
+ export function checkEnvVars(vars) {
32
+ loadE2EEnv();
33
+ return vars.filter((v) => !process.env[v]);
34
+ }
35
+ // ── Config builder ──────────────────────────────────────────────────────
36
+ /**
37
+ * Build a RemoteServerConfig for E2E tests.
38
+ *
39
+ * Creates a single caller ("e2e-client") with the requested connections.
40
+ * Env vars are injected into caller.env so secret resolution works against
41
+ * process.env (already loaded by loadE2EEnv).
42
+ */
43
+ export function buildE2EConfig(connections, opts) {
44
+ return {
45
+ host: '127.0.0.1',
46
+ port: 0,
47
+ callers: {
48
+ 'e2e-client': {
49
+ connections,
50
+ env: opts?.env,
51
+ ingestorOverrides: opts?.ingestorOverrides,
52
+ },
53
+ },
54
+ rateLimitPerMinute: 600,
55
+ };
56
+ }
57
+ /**
58
+ * Boot an Express server with the given config on a random port.
59
+ * Starts all ingestors and returns handles for testing.
60
+ */
61
+ export async function bootServer(config) {
62
+ const serverKeys = generateKeyBundle();
63
+ const clientKeys = generateKeyBundle();
64
+ const clientPub = extractPublicKeys(clientKeys);
65
+ const ingestorManager = new IngestorManager(config);
66
+ const app = createApp({
67
+ config,
68
+ ownKeys: serverKeys,
69
+ authorizedPeers: [{ alias: 'e2e-client', keys: clientPub }],
70
+ ingestorManager,
71
+ disableRateLimiting: true,
72
+ });
73
+ // Start ingestors
74
+ await ingestorManager.startAll();
75
+ // Listen on random port
76
+ const server = await new Promise((resolve) => {
77
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
78
+ });
79
+ const addr = server.address();
80
+ const baseUrl = `http://127.0.0.1:${addr.port}`;
81
+ return {
82
+ server,
83
+ baseUrl,
84
+ ingestorManager,
85
+ teardown: async () => {
86
+ await ingestorManager.stopAll();
87
+ await new Promise((resolve, reject) => {
88
+ server.close((err) => (err ? reject(err) : resolve()));
89
+ });
90
+ },
91
+ };
92
+ }
93
+ // ── Signature helpers ───────────────────────────────────────────────────
94
+ /** Generate a valid GitHub HMAC-SHA256 signature for a raw body. */
95
+ export function signGitHubPayload(rawBody, secret) {
96
+ const buf = typeof rawBody === 'string' ? Buffer.from(rawBody) : rawBody;
97
+ const sig = crypto.createHmac('sha256', secret).update(buf).digest('hex');
98
+ return `sha256=${sig}`;
99
+ }
100
+ /** Generate a valid Trello HMAC-SHA1 base64 signature for body + callbackUrl. */
101
+ export function signTrelloPayload(rawBody, callbackUrl, secret) {
102
+ const bodyStr = typeof rawBody === 'string' ? rawBody : rawBody.toString('utf-8');
103
+ return crypto.createHmac('sha1', secret).update(bodyStr + callbackUrl).digest('base64');
104
+ }
105
+ // ── Polling helpers ─────────────────────────────────────────────────────
106
+ /**
107
+ * Wait for an ingestor to reach a specific state.
108
+ * Polls every 250ms until the timeout is reached.
109
+ */
110
+ export async function waitForIngestorState(manager, callerAlias, connection, targetState, timeoutMs = 15_000) {
111
+ const start = Date.now();
112
+ while (Date.now() - start < timeoutMs) {
113
+ const statuses = manager.getStatuses(callerAlias);
114
+ const status = statuses.find((s) => s.connection === connection);
115
+ if (status?.state === targetState)
116
+ return;
117
+ await new Promise((r) => setTimeout(r, 250));
118
+ }
119
+ throw new Error(`Timed out waiting for ${connection} to reach state "${targetState}" (${timeoutMs}ms)`);
120
+ }
121
+ /**
122
+ * Poll until at least one event appears for a connection.
123
+ * Returns the events array once non-empty.
124
+ */
125
+ export async function pollUntilEvent(manager, callerAlias, connection, timeoutMs = 10_000) {
126
+ const start = Date.now();
127
+ while (Date.now() - start < timeoutMs) {
128
+ const events = manager.getEvents(callerAlias, connection);
129
+ if (events.length > 0)
130
+ return events;
131
+ await new Promise((r) => setTimeout(r, 250));
132
+ }
133
+ throw new Error(`Timed out waiting for events from ${connection} (${timeoutMs}ms)`);
134
+ }
135
+ // ── Shared assertions ───────────────────────────────────────────────────
136
+ /** Common shape every IngestedEvent must satisfy (for use with toMatchObject). */
137
+ export const INGESTED_EVENT_SHAPE = {
138
+ id: expect.any(Number),
139
+ idempotencyKey: expect.any(String),
140
+ receivedAt: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T/),
141
+ receivedAtMs: expect.any(Number),
142
+ callerAlias: 'e2e-client',
143
+ source: expect.any(String),
144
+ eventType: expect.any(String),
145
+ data: expect.anything(),
146
+ };
147
+ //# sourceMappingURL=setup.js.map
@@ -43,6 +43,10 @@ export declare class IngestorManager {
43
43
  private readonly config;
44
44
  /** Active ingestor instances, keyed by `callerAlias:connectionAlias:instanceId`. */
45
45
  private ingestors;
46
+ /** Trigger rule engines per caller. Created during startAll() for callers with triggerRules. */
47
+ private triggerEngines;
48
+ /** Global event listeners (e.g. SSE streams). Called for every event from every ingestor. */
49
+ private eventListeners;
46
50
  /**
47
51
  * Optional config loader for hot-reload support. When provided, `startOne()`
48
52
  * uses it to get fresh config from disk instead of the constructor snapshot.
@@ -63,6 +67,12 @@ export declare class IngestorManager {
63
67
  * Internal: create, register, and start a single ingestor instance.
64
68
  */
65
69
  private startIngestor;
70
+ /**
71
+ * Initialize trigger rule engines for callers with triggerRules config.
72
+ * Subscribes to 'event' emissions from matching ingestors and dispatches
73
+ * to Claude Code remote triggers.
74
+ */
75
+ private initTriggerEngines;
66
76
  /**
67
77
  * Stop all running ingestors. Called during graceful shutdown.
68
78
  */
@@ -82,6 +92,15 @@ export declare class IngestorManager {
82
92
  * Get status of all ingestors for a caller.
83
93
  */
84
94
  getStatuses(callerAlias: string): IngestorStatus[];
95
+ /**
96
+ * Subscribe to all events from all ingestors (current and future).
97
+ * Used by the SSE /events/stream endpoint to fan out events to CLI watchers.
98
+ */
99
+ onEvent(listener: (event: IngestedEvent) => void): void;
100
+ /** Unsubscribe a global event listener. */
101
+ offEvent(listener: (event: IngestedEvent) => void): void;
102
+ /** Forward an ingestor event to all global listeners. */
103
+ private notifyEventListeners;
85
104
  /**
86
105
  * Find all webhook ingestor instances that match a given webhook path.
87
106
  * Returns all matching instances across all callers (for fan-out dispatch).