@interactive-inc/claude-funnel 0.60.0 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +2 -2
  2. package/dist/bin.js +428 -761
  3. package/dist/{channels-2g_BU1N0.d.ts → channels-B8RQPrVq.d.ts} +17 -16
  4. package/dist/claude.d.ts +5 -7
  5. package/dist/claude.js +143 -36
  6. package/dist/{connector-descriptor-6SXJoszo.d.ts → connector-descriptor-ClEEbuW3.d.ts} +50 -11
  7. package/dist/connector-diagnostics-recorder-COtNEmUp.js +42 -0
  8. package/dist/connectors/discord.d.ts +31 -37
  9. package/dist/connectors/discord.js +3 -3
  10. package/dist/connectors/gh.d.ts +37 -33
  11. package/dist/connectors/gh.js +3 -3
  12. package/dist/connectors/schedule.d.ts +9 -57
  13. package/dist/connectors/schedule.js +3 -3
  14. package/dist/connectors/slack.d.ts +71 -131
  15. package/dist/connectors/slack.js +4 -3
  16. package/dist/diagnostics.d.ts +1 -1
  17. package/dist/diagnostics.js +1 -1
  18. package/dist/discord-connector-DIFkYBbi.js +250 -0
  19. package/dist/discord-connector-schema-D-bOVAKt.d.ts +22 -0
  20. package/dist/docs.js +1 -1
  21. package/dist/doctor.d.ts +1 -1
  22. package/dist/doctor.js +1 -1
  23. package/dist/{file-process-guard-DOlCr4GF.d.ts → file-process-guard-DGHxALfI.d.ts} +8 -6
  24. package/dist/{file-system-o51IsM0W.d.ts → file-system-VhwwXZbm.d.ts} +8 -0
  25. package/dist/flume-source-listener-Dim5szHG.d.ts +133 -0
  26. package/dist/{funnel-diagnostics-CSiJmPlZ.js → funnel-diagnostics-Cvk6Sk4x.js} +193 -43
  27. package/dist/{funnel-diagnostics-DpXOsCty.d.ts → funnel-diagnostics-b9ar0Ing.d.ts} +67 -5
  28. package/dist/{funnel-docs-BxXZ9Ksx.js → funnel-docs-C-ge0MuB.js} +42 -6
  29. package/dist/{funnel-doctor-CZf_0Luq.d.ts → funnel-doctor-CnRQi4kM.d.ts} +2 -2
  30. package/dist/{funnel-doctor-DiJCjHsg.js → funnel-doctor-XrI2GBH8.js} +1 -1
  31. package/dist/funnel-error-0t1MK1R6.js +75 -0
  32. package/dist/{funnel-recovery-DnLrdWO9.d.ts → funnel-recovery-CMhY8Jfk.d.ts} +1 -1
  33. package/dist/gateway/daemon.js +167 -527
  34. package/dist/gateway.d.ts +3 -3
  35. package/dist/gateway.js +3 -3
  36. package/dist/gh-connector-BUGCOEWS.js +187 -0
  37. package/dist/{gh-connector-schema-Rzwc1c1N.js → gh-connector-schema-CAqIhzGr.js} +7 -0
  38. package/dist/gh-connector-schema-DWQaB6gX.d.ts +16 -0
  39. package/dist/{index-CgY8NdMz.d.ts → index-DxRikYmu.d.ts} +37 -19
  40. package/dist/index.d.ts +182 -22
  41. package/dist/index.js +365 -174
  42. package/dist/{local-config-json-schema-JyLqOQNX.js → local-config-json-schema-DexV8vX3.js} +24 -4
  43. package/dist/local-config.d.ts +39 -2
  44. package/dist/local-config.js +53 -2
  45. package/dist/logger.js +1 -1
  46. package/dist/loopback-fetch-CVNuN3YZ.js +40 -0
  47. package/dist/{local-config-sync-Dh1Croqe.d.ts → memory-token-prompter-DP_YV9xX.d.ts} +30 -3
  48. package/dist/node-file-system-BOXIHW_Q.js +174 -0
  49. package/dist/{profiles-DSzTeKQw.js → profiles-ZHLONml4.js} +49 -49
  50. package/dist/{profiles-Cy5wXQ0L.d.ts → profiles-cVZQkM69.d.ts} +3 -3
  51. package/dist/profiles.d.ts +1 -1
  52. package/dist/profiles.js +1 -1
  53. package/dist/recovery.d.ts +1 -1
  54. package/dist/recovery.js +1 -1
  55. package/dist/resolve-connector-token-DxDG9mhf.js +22 -0
  56. package/dist/{schedule-connector-L4uzg5M8.js → schedule-connector-9k3gOIgl.js} +54 -55
  57. package/dist/schedule-connector-schema-Z0RXLgPI.d.ts +49 -0
  58. package/dist/settings-reader-BNxjsxCB.d.ts +27 -0
  59. package/dist/{settings-store-CUKSeTXC.js → settings-store-C2QdOH-t.js} +23 -4
  60. package/dist/slack-connector-BU86fIge.js +359 -0
  61. package/dist/slack-event-processor-BhCf5Wiy.d.ts +95 -0
  62. package/dist/slack-event-processor-xFDG3US0.js +176 -0
  63. package/dist/slot-fields-D-pvMgTK.js +249 -0
  64. package/dist/{memory-diagnostic-log-CI60kNfB.js → sqlite-diagnostic-log-DOTPW-tG.js} +373 -249
  65. package/dist/{yaml-render-qW34NlYz.js → yaml-render--J1_3BSA.js} +28 -21
  66. package/package.json +2 -4
  67. package/dist/discord-connector-BL36yvbL.js +0 -250
  68. package/dist/gateway-base-url-Dy4Ykuoh.js +0 -14
  69. package/dist/gh-connector-DpiixfQZ.js +0 -226
  70. package/dist/http-client-oICicjuO.d.ts +0 -18
  71. package/dist/memory-token-prompter-B4sjyaAq.d.ts +0 -57
  72. package/dist/memory-token-prompter-CZde7e6y.js +0 -61
  73. package/dist/node-file-system-Blr8pAir.js +0 -48
  74. package/dist/settings-reader-BIFB_j2f.d.ts +0 -18
  75. package/dist/slack-connector-DQIFPdBF.js +0 -484
  76. package/dist/slot-fields-CMoRpwuy.js +0 -45
  77. /package/dist/{connector-adapter-DU9Rvyec.js → connector-adapter-Dvs8N7ew.js} +0 -0
  78. /package/dist/{connector-listener-DR3aKOuK.js → connector-listener-mPGZYa8e.js} +0 -0
  79. /package/dist/{diagnostic-sql-reader-C9zR-Csp.js → diagnostic-sql-reader-oXZnWFf_.js} +0 -0
  80. /package/dist/{discord-connector-schema-B_N6IXLz.js → discord-connector-schema-B4YpWpR3.js} +0 -0
  81. /package/dist/{error-message-of-Byi4y0Uf.js → error-message-of-ColuYmAk.js} +0 -0
  82. /package/dist/{funnel-log-sqlite-sink-kqJbx2H7.js → funnel-log-sqlite-sink-DLYkY0pZ.js} +0 -0
  83. /package/dist/{funnel-recovery-BFdPjL6Z.js → funnel-recovery-DKnEutUS.js} +0 -0
  84. /package/dist/{node-http-client-lowp60Oa.js → node-http-client-u00atiKx.js} +0 -0
  85. /package/dist/{schedule-connector-schema-CfyuMCMh.js → schedule-connector-schema-DKEPZnVv.js} +0 -0
  86. /package/dist/{settings-reader-CtQ-Ix8_.js → settings-reader-9FcX3qS1.js} +0 -0
  87. /package/dist/{settings-schema-D1xcOqRu.d.ts → settings-schema-BL_c2Udm.d.ts} +0 -0
  88. /package/dist/{slack-connector-schema-C1zEf4TG.js → slack-connector-schema-Dem8to4P.js} +0 -0
package/dist/index.js CHANGED
@@ -1,23 +1,28 @@
1
- import { t as gatewayLoopbackUrl } from "./gateway-base-url-Dy4Ykuoh.js";
1
+ import { n as gatewayLoopbackUrl, t as loopbackFetch } from "./loopback-fetch-CVNuN3YZ.js";
2
2
  import { t as FunnelFileSystem } from "./file-system-Wvzc2ePY.js";
3
- import { t as NodeFunnelFileSystem } from "./node-file-system-Blr8pAir.js";
3
+ import { t as NodeFunnelFileSystem } from "./node-file-system-BOXIHW_Q.js";
4
4
  import { t as FunnelLogger } from "./logger-B6iyNbxM.js";
5
5
  import { n as FunnelProcessRunner, t as NodeFunnelProcessRunner } from "./node-process-runner-DxTvycoK.js";
6
- import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-CtQ-Ix8_.js";
7
- import { a as resolveFunnelDir, c as channelConfigSchema, d as settingsSchema, f as baseConnectorConfigSchema, i as SETTINGS_PATH, l as channelDeliveryModeSchema, n as FUNNEL_DIR, o as resolveFunnelPort, p as NodeFunnelIdGenerator, r as FunnelSettingsStore, s as SETTINGS_VERSION, t as DEFAULT_GATEWAY_PORT, u as profileConfigSchema } from "./settings-store-CUKSeTXC.js";
8
- import { a as FunnelMcp, o as FileProcessGuard, s as FunnelClaude, t as renderYaml } from "./yaml-render-qW34NlYz.js";
9
- import { a as toDiagnosticEvent, i as toDiagnosticConnectionError, n as previewOf, r as queryRows, t as FunnelDiagnostics } from "./funnel-diagnostics-CSiJmPlZ.js";
10
- import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-C9zR-Csp.js";
11
- import { t as FunnelDoctor } from "./funnel-doctor-DiJCjHsg.js";
12
- import { t as FunnelDocs } from "./funnel-docs-BxXZ9Ksx.js";
13
- import { a as FunnelLocalConfig, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigSync, t as funnelJsonSchema } from "./local-config-json-schema-JyLqOQNX.js";
14
- import { t as FunnelProfiles } from "./profiles-DSzTeKQw.js";
15
- import { t as FunnelRecovery } from "./funnel-recovery-BFdPjL6Z.js";
16
- import { C as funnelTmpDir, S as publishResponseSchema, _ as funnelEventSchema, a as connectorConnectionEventSchema, b as FunnelChannelPublisher, c as MemoryFunnelEventLog, d as DEFAULT_GATEWAY_TOKEN_PATH, f as FunnelGatewayToken, g as FunnelEventLog, h as SqliteFunnelEventLog, i as ConnectorDiagnosticLog, l as channelWsProtocols, m as FunnelListenerSupervisor, n as SqliteConnectorDiagnosticLog, o as connectorProcessedEventSchema, p as FunnelGatewayServer, r as CONNECTOR_CONNECTION_STATUSES, s as connectorRawEventSchema, t as MemoryConnectorDiagnosticLog, u as channelWsUrl, v as FunnelBroadcaster, x as publishRequestSchema, y as requireBearerToken } from "./memory-diagnostic-log-CI60kNfB.js";
17
- import { n as FunnelHttpClient, t as NodeFunnelHttpClient } from "./node-http-client-lowp60Oa.js";
18
- import { t as FunnelConnectorAdapter } from "./connector-adapter-DU9Rvyec.js";
19
- import { t as FunnelConnectorListener } from "./connector-listener-DR3aKOuK.js";
20
- import { r as scheduleEntrySchema, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-CfyuMCMh.js";
6
+ import { n as FunnelHttpClient, t as NodeFunnelHttpClient } from "./node-http-client-u00atiKx.js";
7
+ import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-9FcX3qS1.js";
8
+ import { a as resolveFunnelDir, c as channelConfigSchema, d as settingsSchema, f as baseConnectorConfigSchema, i as SETTINGS_PATH, l as channelDeliveryModeSchema, n as FUNNEL_DIR, o as resolveFunnelPort, p as NodeFunnelIdGenerator, r as FunnelSettingsStore, s as SETTINGS_VERSION, t as DEFAULT_GATEWAY_PORT, u as profileConfigSchema } from "./settings-store-C2QdOH-t.js";
9
+ import { a as FunnelConnectorTypeMismatchError, c as FunnelTokenCollisionError, i as FunnelConnectorNotFoundError, n as FunnelChannelAlreadyExistsError, o as FunnelError, r as FunnelChannelNotFoundError, s as FunnelGatewayBindError, t as FunnelAuthFailedError } from "./funnel-error-0t1MK1R6.js";
10
+ import { a as FunnelMcp, o as FunnelFileProcessGuard, s as FunnelClaude, t as renderYaml } from "./yaml-render--J1_3BSA.js";
11
+ import { a as toDiagnosticEvent, i as toDiagnosticConnectionError, n as previewOf, r as queryRows, t as FunnelDiagnostics } from "./funnel-diagnostics-Cvk6Sk4x.js";
12
+ import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-oXZnWFf_.js";
13
+ import { t as FunnelDoctor } from "./funnel-doctor-XrI2GBH8.js";
14
+ import { t as FunnelDocs } from "./funnel-docs-C-ge0MuB.js";
15
+ import { a as FunnelTokenPrompter, i as FunnelLocalConfigSync, n as MemoryFunnelTokenPrompter, o as FunnelLocalConfig, r as NodeFunnelTokenPrompter, t as funnelJsonSchema } from "./local-config-json-schema-DexV8vX3.js";
16
+ import { t as discordConnectorSchema } from "./discord-connector-schema-B4YpWpR3.js";
17
+ import { t as ghConnectorSchema } from "./gh-connector-schema-CAqIhzGr.js";
18
+ import { t as slackConnectorSchema } from "./slack-connector-schema-Dem8to4P.js";
19
+ import { t as FunnelProfiles } from "./profiles-ZHLONml4.js";
20
+ import { t as FunnelRecovery } from "./funnel-recovery-DKnEutUS.js";
21
+ import { C as funnelTmpDir, S as connectorRawEventSchema, _ as MemoryConnectorDiagnosticLog, a as DEFAULT_GATEWAY_TOKEN_PATH, b as connectorConnectionEventSchema, c as FunnelListenerRegistry, d as funnelEventSchema, f as FunnelBroadcaster, g as publishResponseSchema, h as publishRequestSchema, i as channelWsUrl, l as SqliteFunnelEventLog, m as FunnelChannelPublisher, n as MemoryFunnelEventLog, o as FunnelGatewayToken, p as requireBearerToken, r as channelWsProtocols, s as FunnelGatewayServer, t as SqliteConnectorDiagnosticLog, u as FunnelEventLog, v as CONNECTOR_CONNECTION_STATUSES, x as connectorProcessedEventSchema, y as ConnectorDiagnosticLog } from "./sqlite-diagnostic-log-DOTPW-tG.js";
22
+ import { t as FunnelConnectorAdapter } from "./connector-adapter-Dvs8N7ew.js";
23
+ import { t as FunnelConnectorListener } from "./connector-listener-mPGZYa8e.js";
24
+ import { t as FunnelSlackEventProcessor } from "./slack-event-processor-xFDG3US0.js";
25
+ import { n as scheduleConnectorSchema, r as scheduleEntrySchema, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-DKEPZnVv.js";
21
26
  import { dirname, join, resolve } from "node:path";
22
27
  import { hc } from "hono/client";
23
28
  import { appendFileSync, existsSync, mkdirSync } from "node:fs";
@@ -27,9 +32,32 @@ import { createFactory } from "hono/factory";
27
32
  import { HTTPException } from "hono/http-exception";
28
33
  import { zValidator } from "@hono/zod-validator";
29
34
  import { Hono } from "hono";
35
+ //#region lib/engine/time/clock.ts
36
+ /**
37
+ * Time boundary. Default NodeFunnelClock returns `new Date()`; MemoryFunnelClock
38
+ * is settable and `advance(ms)`-able for deterministic schedule / timeout tests.
39
+ */
40
+ var FunnelClock = class {
41
+ millis() {
42
+ return this.now().getTime();
43
+ }
44
+ iso() {
45
+ return this.now().toISOString();
46
+ }
47
+ };
48
+ //#endregion
49
+ //#region lib/engine/time/node-clock.ts
50
+ var NodeFunnelClock = class extends FunnelClock {
51
+ now() {
52
+ return /* @__PURE__ */ new Date();
53
+ }
54
+ };
55
+ //#endregion
30
56
  //#region lib/engine/connectors/connector-registry.ts
31
57
  const defaultFs$1 = new NodeFunnelFileSystem();
32
58
  const defaultProcess$1 = new NodeFunnelProcessRunner();
59
+ const defaultHttp = new NodeFunnelHttpClient();
60
+ const defaultClock$2 = new NodeFunnelClock();
33
61
  /**
34
62
  * Dispatches connector work to injected descriptors by `type`. Replaces the old
35
63
  * hard-coded factory: core never imports a concrete connector, so listener and
@@ -43,15 +71,21 @@ var FunnelConnectorRegistry = class {
43
71
  descriptors;
44
72
  fs;
45
73
  process;
74
+ http;
75
+ clock;
46
76
  logger;
47
77
  diagnosticLog;
78
+ signal;
48
79
  dir;
49
80
  constructor(deps) {
50
81
  this.descriptors = new Map(deps.descriptors.map((descriptor) => [descriptor.type, descriptor]));
51
82
  this.fs = deps.fs ?? defaultFs$1;
52
83
  this.process = deps.process ?? defaultProcess$1;
84
+ this.http = deps.http ?? defaultHttp;
85
+ this.clock = deps.clock ?? defaultClock$2;
53
86
  this.logger = deps.logger;
54
87
  this.diagnosticLog = deps.diagnosticLog;
88
+ this.signal = deps.signal;
55
89
  this.dir = deps.dir ?? FUNNEL_DIR;
56
90
  Object.freeze(this);
57
91
  }
@@ -104,8 +138,11 @@ var FunnelConnectorRegistry = class {
104
138
  channelId,
105
139
  fs: this.fs,
106
140
  process: this.process,
141
+ http: this.http,
142
+ clock: this.clock,
107
143
  logger: this.logger,
108
144
  diagnosticLog: this.diagnosticLog,
145
+ signal: this.signal,
109
146
  connectorDir: (channel, connector) => this.connectorDir(channel, connector)
110
147
  };
111
148
  }
@@ -113,32 +150,12 @@ var FunnelConnectorRegistry = class {
113
150
  return {
114
151
  fs: this.fs,
115
152
  process: this.process,
153
+ http: this.http,
116
154
  logger: this.logger
117
155
  };
118
156
  }
119
157
  };
120
158
  //#endregion
121
- //#region lib/engine/time/clock.ts
122
- /**
123
- * Time boundary. Default NodeFunnelClock returns `new Date()`; MemoryFunnelClock
124
- * is settable and `advance(ms)`-able for deterministic schedule / timeout tests.
125
- */
126
- var FunnelClock = class {
127
- millis() {
128
- return this.now().getTime();
129
- }
130
- iso() {
131
- return this.now().toISOString();
132
- }
133
- };
134
- //#endregion
135
- //#region lib/engine/time/node-clock.ts
136
- var NodeFunnelClock = class extends FunnelClock {
137
- now() {
138
- return /* @__PURE__ */ new Date();
139
- }
140
- };
141
- //#endregion
142
159
  //#region lib/engine/channels/channels.ts
143
160
  const defaultClock$1 = new NodeFunnelClock();
144
161
  const defaultIdGenerator = new NodeFunnelIdGenerator();
@@ -178,40 +195,40 @@ var FunnelChannels = class {
178
195
  return this.list().find((c) => c.id === id) ?? null;
179
196
  }
180
197
  add(input) {
181
- const settings = this.store.read();
182
- if (settings.channels.some((c) => c.name === input.name)) throw new Error(`channel "${input.name}" already exists`);
183
- const channel = {
184
- id: this.idGenerator.generate(),
185
- name: input.name,
186
- delivery: input.delivery ?? "fanout",
187
- connectors: []
188
- };
189
- settings.channels.push(channel);
190
- this.store.write(settings);
191
- return channel;
198
+ return this.store.update((settings) => {
199
+ if (settings.channels.some((c) => c.name === input.name)) throw new FunnelChannelAlreadyExistsError(input.name);
200
+ const channel = {
201
+ id: this.idGenerator.generate(),
202
+ name: input.name,
203
+ delivery: input.delivery ?? "fanout",
204
+ connectors: []
205
+ };
206
+ settings.channels.push(channel);
207
+ return channel;
208
+ });
192
209
  }
193
210
  setDelivery(name, delivery) {
194
- const settings = this.store.read();
195
- const channel = this.requireChannel(settings, name);
196
- channel.delivery = delivery;
197
- this.store.write(settings);
211
+ this.store.update((settings) => {
212
+ const channel = this.requireChannel(settings, name);
213
+ channel.delivery = delivery;
214
+ });
198
215
  }
199
216
  remove(name) {
200
- const settings = this.store.read();
201
- const index = settings.channels.findIndex((c) => c.name === name);
202
- if (index < 0) throw new Error(`channel "${name}" not found`);
203
- const channel = settings.channels[index];
204
- if (channel && this.profileChecker?.hasChannelRef(channel.id)) throw new Error(`channel "${name}" is referenced by a profile`);
205
- settings.channels.splice(index, 1);
206
- this.store.write(settings);
217
+ this.store.update((settings) => {
218
+ const index = settings.channels.findIndex((c) => c.name === name);
219
+ if (index < 0) throw new FunnelChannelNotFoundError(name);
220
+ const channel = settings.channels[index];
221
+ if (channel && this.profileChecker?.hasChannelRef(channel.id)) throw new Error(`channel "${name}" is referenced by a profile`);
222
+ settings.channels.splice(index, 1);
223
+ });
207
224
  }
208
225
  rename(oldName, newName) {
209
- const settings = this.store.read();
210
- const channel = settings.channels.find((c) => c.name === oldName);
211
- if (!channel) throw new Error(`channel "${oldName}" not found`);
212
- if (settings.channels.some((c) => c.name === newName)) throw new Error(`channel "${newName}" already exists`);
213
- channel.name = newName;
214
- this.store.write(settings);
226
+ this.store.update((settings) => {
227
+ const channel = settings.channels.find((c) => c.name === oldName);
228
+ if (!channel) throw new FunnelChannelNotFoundError(oldName);
229
+ if (settings.channels.some((c) => c.name === newName)) throw new FunnelChannelAlreadyExistsError(newName);
230
+ channel.name = newName;
231
+ });
215
232
  }
216
233
  listConnectors(channelName) {
217
234
  return this.requireChannel(this.store.read(), channelName).connectors;
@@ -231,35 +248,35 @@ var FunnelChannels = class {
231
248
  return out;
232
249
  }
233
250
  addConnector(channelName, input) {
234
- const settings = this.store.read();
235
- const channel = this.requireChannel(settings, channelName);
236
- if (channel.connectors.some((c) => c.name === input.name)) throw new Error(`connector "${input.name}" already exists in channel "${channelName}"`);
237
- const candidate = this.registry.buildConfig(input, {
238
- id: this.idGenerator.generate(),
239
- now: this.clock.iso()
251
+ return this.store.update((settings) => {
252
+ const channel = this.requireChannel(settings, channelName);
253
+ if (channel.connectors.some((c) => c.name === input.name)) throw new Error(`connector "${input.name}" already exists in channel "${channelName}"`);
254
+ const candidate = this.registry.buildConfig(input, {
255
+ id: this.idGenerator.generate(),
256
+ now: this.clock.iso()
257
+ });
258
+ this.assertNoTokenCollision(settings, candidate);
259
+ channel.connectors.push(candidate);
260
+ return candidate;
240
261
  });
241
- this.assertNoTokenCollision(settings, candidate);
242
- channel.connectors.push(candidate);
243
- this.store.write(settings);
244
- return candidate;
245
262
  }
246
263
  removeConnector(channelName, connectorName) {
247
- const settings = this.store.read();
248
- const channel = this.requireChannel(settings, channelName);
249
- const index = channel.connectors.findIndex((c) => c.name === connectorName);
250
- if (index < 0) throw new Error(`connector "${connectorName}" not found in channel "${channelName}"`);
251
- channel.connectors.splice(index, 1);
252
- this.store.write(settings);
264
+ this.store.update((settings) => {
265
+ const channel = this.requireChannel(settings, channelName);
266
+ const index = channel.connectors.findIndex((c) => c.name === connectorName);
267
+ if (index < 0) throw new FunnelConnectorNotFoundError(channelName, connectorName);
268
+ channel.connectors.splice(index, 1);
269
+ });
253
270
  }
254
271
  renameConnector(channelName, oldName, newName) {
255
- const settings = this.store.read();
256
- const channel = this.requireChannel(settings, channelName);
257
- const connector = channel.connectors.find((c) => c.name === oldName);
258
- if (!connector) throw new Error(`connector "${oldName}" not found in channel "${channelName}"`);
259
- if (channel.connectors.some((c) => c.name === newName)) throw new Error(`connector "${newName}" already exists in channel "${channelName}"`);
260
- connector.name = newName;
261
- connector.updatedAt = this.clock.iso();
262
- this.store.write(settings);
272
+ this.store.update((settings) => {
273
+ const channel = this.requireChannel(settings, channelName);
274
+ const connector = channel.connectors.find((c) => c.name === oldName);
275
+ if (!connector) throw new Error(`connector "${oldName}" not found in channel "${channelName}"`);
276
+ if (channel.connectors.some((c) => c.name === newName)) throw new Error(`connector "${newName}" already exists in channel "${channelName}"`);
277
+ connector.name = newName;
278
+ connector.updatedAt = this.clock.iso();
279
+ });
263
280
  }
264
281
  /**
265
282
  * Update a connector's mutable fields generically. The connector's descriptor
@@ -267,14 +284,14 @@ var FunnelChannels = class {
267
284
  * so a slot can move between a literal and an env reference cleanly).
268
285
  */
269
286
  updateConnector(channelName, connectorName, fields) {
270
- const settings = this.store.read();
271
- const channel = this.requireChannel(settings, channelName);
272
- const connector = channel.connectors.find((c) => c.name === connectorName);
273
- if (!connector) throw new Error(`connector "${connectorName}" not found in channel "${channelName}"`);
274
- const updated = this.registry.applyUpdate(connector, fields, { now: this.clock.iso() });
275
- this.assertNoTokenCollision(settings, updated);
276
- this.replaceConnector(channel, connector.name, updated);
277
- this.store.write(settings);
287
+ this.store.update((settings) => {
288
+ const channel = this.requireChannel(settings, channelName);
289
+ const connector = channel.connectors.find((c) => c.name === connectorName);
290
+ if (!connector) throw new FunnelConnectorNotFoundError(channelName, connectorName);
291
+ const updated = this.registry.applyUpdate(connector, fields, { now: this.clock.iso() });
292
+ this.assertNoTokenCollision(settings, updated);
293
+ this.replaceConnector(channel, connector.name, updated);
294
+ });
278
295
  }
279
296
  /** Back-compat wrapper for `updateConnector` on a slack connector. */
280
297
  updateSlackConnector(channelName, connectorName, fields) {
@@ -294,23 +311,21 @@ var FunnelChannels = class {
294
311
  * result; the config is persisted only when the operation actually mutated it.
295
312
  */
296
313
  connectorOp(channelName, connectorName, operation, args) {
297
- const settings = this.store.read();
298
- const channel = this.requireChannel(settings, channelName);
299
- const connector = channel.connectors.find((c) => c.name === connectorName);
300
- if (!connector) throw new Error(`connector "${connectorName}" not found in channel "${channelName}"`);
301
- const outcome = this.registry.runOperation(connector, operation, args, {
302
- generateId: () => this.idGenerator.generate(),
303
- now: this.clock.iso()
314
+ return this.store.update((settings) => {
315
+ const channel = this.requireChannel(settings, channelName);
316
+ const connector = channel.connectors.find((c) => c.name === connectorName);
317
+ if (!connector) throw new FunnelConnectorNotFoundError(channelName, connectorName);
318
+ const outcome = this.registry.runOperation(connector, operation, args, {
319
+ generateId: () => this.idGenerator.generate(),
320
+ now: this.clock.iso()
321
+ });
322
+ if (outcome.config !== connector) this.replaceConnector(channel, connector.name, outcome.config);
323
+ return outcome.result;
304
324
  });
305
- if (outcome.config !== connector) {
306
- this.replaceConnector(channel, connector.name, outcome.config);
307
- this.store.write(settings);
308
- }
309
- return outcome.result;
310
325
  }
311
326
  async call(channelName, connectorName, input) {
312
327
  const connector = this.getConnector(channelName, connectorName);
313
- if (!connector) throw new Error(`connector "${connectorName}" not found in channel "${channelName}"`);
328
+ if (!connector) throw new FunnelConnectorNotFoundError(channelName, connectorName);
314
329
  const adapter = this.registry.createAdapter(connector);
315
330
  if (!adapter) throw new Error(`connector type "${connector.type}" does not support outbound calls`);
316
331
  return await adapter.call(input);
@@ -338,7 +353,7 @@ var FunnelChannels = class {
338
353
  }
339
354
  requireChannel(settings, name) {
340
355
  const channel = settings.channels.find((c) => c.name === name);
341
- if (!channel) throw new Error(`channel "${name}" not found`);
356
+ if (!channel) throw new FunnelChannelNotFoundError(name);
342
357
  return channel;
343
358
  }
344
359
  replaceConnector(channel, connectorName, next) {
@@ -418,6 +433,9 @@ var MemoryFunnelFileSystem = class extends FunnelFileSystem {
418
433
  mode: this.modes.get(path) ?? null
419
434
  };
420
435
  }
436
+ withFileLock(_lockPath, fn) {
437
+ return fn();
438
+ }
421
439
  setMtime(path, mtimeMs) {
422
440
  this.mtimes.set(path, mtimeMs);
423
441
  }
@@ -602,6 +620,9 @@ var MockFunnelSettingsReader = class extends FunnelSettingsReader {
602
620
  write(settings) {
603
621
  this.state = settings;
604
622
  }
623
+ update(mutator) {
624
+ return mutator(this.state);
625
+ }
605
626
  };
606
627
  //#endregion
607
628
  //#region lib/engine/time/memory-clock.ts
@@ -622,6 +643,31 @@ var MemoryFunnelClock = class extends FunnelClock {
622
643
  }
623
644
  };
624
645
  //#endregion
646
+ //#region lib/engine/http/memory-http-client.ts
647
+ var MemoryFunnelHttpClient = class extends FunnelHttpClient {
648
+ calls = [];
649
+ handler = () => ({
650
+ status: 200,
651
+ body: ""
652
+ });
653
+ on(handler) {
654
+ this.handler = handler;
655
+ return this;
656
+ }
657
+ async fetch(request) {
658
+ this.calls.push(request);
659
+ const response = await this.handler(request);
660
+ const status = response.status ?? 200;
661
+ const body = response.body ?? "";
662
+ return {
663
+ status,
664
+ ok: status >= 200 && status < 300,
665
+ text: async () => body,
666
+ json: async () => JSON.parse(body)
667
+ };
668
+ }
669
+ };
670
+ //#endregion
625
671
  //#region lib/gateway/resolve-daemon-script.ts
626
672
  /**
627
673
  * Locate the daemon entry script. Works in both dev (running from source)
@@ -838,7 +884,7 @@ var FunnelListenersClient = class {
838
884
  async list() {
839
885
  if (!this.isDaemonRunning()) return { state: "offline" };
840
886
  try {
841
- const res = await fetch(`${gatewayLoopbackUrl(this.port)}/listeners`, { headers: this.authHeaders() });
887
+ const res = await loopbackFetch(`${gatewayLoopbackUrl(this.port)}/listeners`, { headers: this.authHeaders() });
842
888
  if (!res.ok) return {
843
889
  state: "error",
844
890
  reason: `HTTP ${res.status}`
@@ -880,12 +926,18 @@ var FunnelListenersClient = class {
880
926
  }
881
927
  async call(method, path) {
882
928
  try {
883
- const res = await fetch(`${gatewayLoopbackUrl(this.port)}${path}`, {
929
+ const res = await loopbackFetch(`${gatewayLoopbackUrl(this.port)}${path}`, {
884
930
  method,
885
931
  headers: this.authHeaders()
886
- });
932
+ }, 3e4);
887
933
  if (!res.ok) {
888
- const parsed = opErrorBodySchema.safeParse(await res.json().catch(() => null));
934
+ let body = null;
935
+ try {
936
+ body = await res.json();
937
+ } catch {
938
+ body = null;
939
+ }
940
+ const parsed = opErrorBodySchema.safeParse(body);
889
941
  return {
890
942
  state: "error",
891
943
  reason: (parsed.success ? parsed.data.reason : void 0) ?? `HTTP ${res.status}`
@@ -944,6 +996,7 @@ var Funnel = class Funnel {
944
996
  process;
945
997
  logger;
946
998
  clock;
999
+ http;
947
1000
  onError;
948
1001
  constructor(props = {}) {
949
1002
  const dir = props.dir ?? resolveFunnelDir();
@@ -952,6 +1005,7 @@ var Funnel = class Funnel {
952
1005
  const process = props.process ?? new NodeFunnelProcessRunner();
953
1006
  const clock = props.clock ?? new NodeFunnelClock();
954
1007
  const idGenerator = props.idGenerator ?? new NodeFunnelIdGenerator();
1008
+ const http = props.http ?? new NodeFunnelHttpClient();
955
1009
  this.paths = {
956
1010
  dir,
957
1011
  tmpDir,
@@ -961,6 +1015,7 @@ var Funnel = class Funnel {
961
1015
  this.process = process;
962
1016
  this.logger = props.logger;
963
1017
  this.clock = clock;
1018
+ this.http = http;
964
1019
  this.onError = props.onError ?? noopOnError;
965
1020
  const store = props.store ?? new FunnelSettingsStore({
966
1021
  path: this.paths.settings,
@@ -971,8 +1026,11 @@ var Funnel = class Funnel {
971
1026
  descriptors: props.connectors ?? [],
972
1027
  fs,
973
1028
  process,
1029
+ http,
1030
+ clock,
974
1031
  logger: this.logger,
975
1032
  diagnosticLog: props.diagnosticLog,
1033
+ signal: props.signal,
976
1034
  dir
977
1035
  });
978
1036
  this.profiles = new FunnelProfiles({
@@ -1020,13 +1078,14 @@ var Funnel = class Funnel {
1020
1078
  mcp,
1021
1079
  gateway: this.gateway,
1022
1080
  sessions: this.profiles,
1023
- guard: new FileProcessGuard({
1081
+ guard: new FunnelFileProcessGuard({
1024
1082
  fs,
1025
1083
  process,
1026
1084
  dir
1027
1085
  }),
1028
1086
  process,
1029
- logger: this.logger
1087
+ logger: this.logger,
1088
+ dir
1030
1089
  });
1031
1090
  this.diagnostics = new FunnelDiagnostics({
1032
1091
  gateway: this.gateway,
@@ -1048,9 +1107,20 @@ var Funnel = class Funnel {
1048
1107
  Object.freeze(this);
1049
1108
  }
1050
1109
  /**
1051
- * Sandboxed Funnel wired with in-memory implementations for every IO boundary.
1052
- * Touches no real disk, processes, wall-clock time, or UUIDs — safe for tests
1053
- * and ad-hoc experiments. Override individual fields by passing them in `props`.
1110
+ * Sandboxed Funnel wired with in-memory implementations for every IO
1111
+ * boundary. Touches no real disk, processes, wall-clock time, UUIDs, HTTP,
1112
+ * TTY prompts, or diagnostic SQLite safe for tests and ad-hoc
1113
+ * experiments. Override individual fields by passing them in `props`.
1114
+ *
1115
+ * NOT covered by `inMemory()`:
1116
+ * - `gatewayServer()` still calls `Bun.serve` and binds a real port; use
1117
+ * `port: 0` to let the OS pick one. The WebSocket subscription path
1118
+ * also crosses the real socket.
1119
+ * - Flume sources opened by listeners (Slack Socket Mode, Discord
1120
+ * Gateway, GitHub poll) still open real WebSockets / HTTP. Pass
1121
+ * `flumeDeps` to the descriptor's options if a test needs them stubbed.
1122
+ * - `funnel.gateway` (daemon process) — `start()` still spawns a child
1123
+ * process; only the in-process `gatewayServer()` is sandbox-friendly.
1054
1124
  */
1055
1125
  static inMemory(props = {}) {
1056
1126
  return new Funnel({
@@ -1061,6 +1131,9 @@ var Funnel = class Funnel {
1061
1131
  logger: props.logger ?? new MemoryFunnelLogger(),
1062
1132
  clock: props.clock ?? new MemoryFunnelClock(),
1063
1133
  idGenerator: props.idGenerator ?? new MemoryFunnelIdGenerator(),
1134
+ http: props.http ?? new MemoryFunnelHttpClient(),
1135
+ tokenPrompter: props.tokenPrompter ?? new MemoryFunnelTokenPrompter(),
1136
+ diagnosticLog: props.diagnosticLog ?? new MemoryConnectorDiagnosticLog(),
1064
1137
  dir: props.dir ?? SANDBOX_DIR,
1065
1138
  tmpDir: props.tmpDir ?? SANDBOX_TMP_DIR
1066
1139
  });
@@ -1093,7 +1166,7 @@ var Funnel = class Funnel {
1093
1166
  * independently of FunnelClaude (e.g. checking if a named profile is running).
1094
1167
  */
1095
1168
  createProcessGuard() {
1096
- return new FileProcessGuard({
1169
+ return new FunnelFileProcessGuard({
1097
1170
  fs: this.fs,
1098
1171
  process: this.process,
1099
1172
  dir: this.paths.dir
@@ -1181,31 +1254,6 @@ var NoopFunnelLogger = class extends FunnelLogger {
1181
1254
  error() {}
1182
1255
  };
1183
1256
  //#endregion
1184
- //#region lib/engine/http/memory-http-client.ts
1185
- var MemoryFunnelHttpClient = class extends FunnelHttpClient {
1186
- calls = [];
1187
- handler = () => ({
1188
- status: 200,
1189
- body: ""
1190
- });
1191
- on(handler) {
1192
- this.handler = handler;
1193
- return this;
1194
- }
1195
- async fetch(request) {
1196
- this.calls.push(request);
1197
- const response = await this.handler(request);
1198
- const status = response.status ?? 200;
1199
- const body = response.body ?? "";
1200
- return {
1201
- status,
1202
- ok: status >= 200 && status < 300,
1203
- text: async () => body,
1204
- json: async () => JSON.parse(body)
1205
- };
1206
- }
1207
- };
1208
- //#endregion
1209
1257
  //#region lib/gateway/service-routes.ts
1210
1258
  /**
1211
1259
  * Mountable Hono app that exposes the service layer (`FunnelDiagnostics` +
@@ -1234,24 +1282,65 @@ const buildServiceRoutes = (deps) => {
1234
1282
  });
1235
1283
  app.get("/diagnostics/events", async (c) => {
1236
1284
  const channel = c.req.query("channel") ?? null;
1285
+ const connector = c.req.query("connector");
1237
1286
  const limit = Number(c.req.query("limit") ?? "20");
1238
- const events = await deps.diagnostics.recentEvents(channel, limit);
1287
+ const events = await deps.diagnostics.recentEvents(channel, {
1288
+ connector,
1289
+ limit
1290
+ });
1239
1291
  return c.json(events);
1240
1292
  });
1241
1293
  app.get("/diagnostics/dropped", async (c) => {
1242
1294
  const channel = c.req.query("channel") ?? null;
1295
+ const connector = c.req.query("connector");
1243
1296
  const limit = Number(c.req.query("limit") ?? "20");
1244
- const events = await deps.diagnostics.droppedEvents(channel, limit);
1297
+ const events = await deps.diagnostics.droppedEvents(channel, {
1298
+ connector,
1299
+ limit
1300
+ });
1245
1301
  return c.json(events);
1246
1302
  });
1247
1303
  app.get("/diagnostics/errors", async (c) => {
1248
1304
  const channel = c.req.query("channel") ?? null;
1305
+ const connector = c.req.query("connector");
1249
1306
  const limit = Number(c.req.query("limit") ?? "20");
1250
- const errors = await deps.diagnostics.connectionErrors(channel, limit);
1307
+ const errors = await deps.diagnostics.connectionErrors(channel, {
1308
+ connector,
1309
+ limit
1310
+ });
1251
1311
  return c.json(errors);
1252
1312
  });
1313
+ app.get("/diagnostics/raw", async (c) => {
1314
+ const channel = c.req.query("channel") ?? null;
1315
+ const connector = c.req.query("connector");
1316
+ const limit = Number(c.req.query("limit") ?? "20");
1317
+ const events = await deps.diagnostics.rawEvents(channel, {
1318
+ connector,
1319
+ limit
1320
+ });
1321
+ return c.json(events);
1322
+ });
1323
+ app.get("/diagnostics/connection", async (c) => {
1324
+ const channel = c.req.query("channel") ?? null;
1325
+ const connector = c.req.query("connector");
1326
+ const limit = Number(c.req.query("limit") ?? "20");
1327
+ const rows = await deps.diagnostics.connectionTimeline(channel, {
1328
+ connector,
1329
+ limit
1330
+ });
1331
+ return c.json(rows);
1332
+ });
1333
+ app.get("/diagnostics/logs", async (c) => {
1334
+ const grep = c.req.query("grep") ?? void 0;
1335
+ const limit = Number(c.req.query("limit") ?? "200");
1336
+ const result = await deps.diagnostics.recentLogs({
1337
+ grep,
1338
+ limit
1339
+ });
1340
+ return c.json(result);
1341
+ });
1253
1342
  app.post("/diagnostics/replay", async (c) => {
1254
- const body = await c.req.json().catch(() => ({}));
1343
+ const body = await readJsonBody(c);
1255
1344
  const channel = typeof body.channel === "string" ? body.channel : null;
1256
1345
  const seq = typeof body.seq === "number" ? body.seq : void 0;
1257
1346
  if (!channel) return c.json({ error: "channel is required" }, 400);
@@ -1259,13 +1348,22 @@ const buildServiceRoutes = (deps) => {
1259
1348
  return c.json(result);
1260
1349
  });
1261
1350
  app.post("/doctor", async (c) => {
1262
- const body = await c.req.json().catch(() => ({}));
1351
+ const body = await readJsonBody(c);
1263
1352
  const mode = body.mode === "safe" || body.mode === "aggressive" || body.mode === "off" ? body.mode : "off";
1264
1353
  const report = await deps.doctor.run(mode);
1265
1354
  return c.json(report);
1266
1355
  });
1267
1356
  return app;
1268
1357
  };
1358
+ const isStringKeyedObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
1359
+ const readJsonBody = async (c) => {
1360
+ try {
1361
+ const body = await c.req.json();
1362
+ return isStringKeyedObject(body) ? body : {};
1363
+ } catch {
1364
+ return {};
1365
+ }
1366
+ };
1269
1367
  //#endregion
1270
1368
  //#region lib/cli/factory.ts
1271
1369
  const factory = createFactory();
@@ -1627,7 +1725,7 @@ const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param"
1627
1725
  "bot-token": z.string().optional(),
1628
1726
  "app-token": z.string().optional(),
1629
1727
  "poll-interval": z.coerce.number().int().positive().optional()
1630
- }).passthrough()), async (c) => {
1728
+ }).loose()), async (c) => {
1631
1729
  const param = c.req.valid("param");
1632
1730
  const query = c.req.valid("query");
1633
1731
  const funnel = c.env.funnel;
@@ -1719,7 +1817,7 @@ options:
1719
1817
  output / valid YAML (or raw text when the adapter returns text)`), zValidator$1("query", z.object({
1720
1818
  method: z.string(),
1721
1819
  path: z.string().optional()
1722
- }).passthrough()), async (c) => {
1820
+ }).loose()), async (c) => {
1723
1821
  const param = c.req.valid("param");
1724
1822
  const query = c.req.valid("query");
1725
1823
  const funnel = c.env.funnel;
@@ -1840,7 +1938,7 @@ const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$7))
1840
1938
  const querySchema = z.object({
1841
1939
  content: z.string().min(1, { message: "--content is required" }),
1842
1940
  connector: z.string().min(1).optional()
1843
- }).passthrough();
1941
+ }).loose();
1844
1942
  const channelsPublishHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", querySchema), async (c) => {
1845
1943
  const param = c.req.valid("param");
1846
1944
  const query = c.req.valid("query");
@@ -2143,7 +2241,7 @@ const RESERVED_KEYS$1 = ["profile", "channel"];
2143
2241
  const claudeHandler = factory.createHandlers(helpGuard(claudeHelp), zValidator$1("query", z.object({
2144
2242
  profile: z.string().optional(),
2145
2243
  channel: z.string().optional()
2146
- }).passthrough()), async (c) => {
2244
+ }).loose()), async (c) => {
2147
2245
  const query = c.req.valid("query");
2148
2246
  const { funnel, claude, profiles, localConfig, localConfigSync } = c.env;
2149
2247
  const userArgs = queryToCliArgs(c.req.url, RESERVED_KEYS$1);
@@ -2204,35 +2302,41 @@ const claudeHandler = factory.createHandlers(helpGuard(claudeHelp), zValidator$1
2204
2302
  });
2205
2303
  //#endregion
2206
2304
  //#region lib/cli/routes/debug.ts
2207
- const debugHelp = `funnel debug / per-channel inspection (events, drops, connection errors, replay)
2305
+ const debugHelp = `funnel debug / per-channel inspection (events, drops, connection lifecycle, logs, replay)
2208
2306
 
2209
- usage / funnel debug [subcommand] [--channel <name>] [--all] [--limit <N>]
2307
+ usage / funnel debug [subcommand] [--channel <name>] [--connector <name>] [--all] [--limit <N>]
2210
2308
 
2211
2309
  subcommands:
2212
2310
  (none) / full diagnosis for one channel (or --all for every channel)
2213
2311
  events / last N processed events with outcome
2214
2312
  dropped / events filtered out (skip:*) with payload
2215
- errors / listener auth-failed and error events
2313
+ errors / listener auth-failed and error rows
2314
+ connection / full connection lifecycle (started / connected / disconnected / stopped + errors)
2315
+ raw / raw inbound events before any processing (pre-processor drops)
2316
+ logs / tail of the gateway daemon log (FunnelLogger + flume internal logs)
2216
2317
  replay / re-send a past event into a channel
2217
2318
 
2218
2319
  options:
2219
2320
  --channel <name> / channel to inspect (auto-selected when only one exists)
2321
+ --connector <name> / restrict events/dropped/errors/connection/raw to one connector
2220
2322
  --all / diagnose every channel
2221
- --limit <N> / number of rows (default 5 for diagnosis, 20 for subcommands)
2323
+ --limit <N> / number of rows (default 5 for diagnosis, 20 for subcommands, 200 for logs)
2222
2324
 
2223
2325
  For the common case, prefer fnl doctor — it runs a full diagnosis and can apply
2224
2326
  safe fixes in one shot. fnl debug is the lower-level view.
2225
2327
 
2226
2328
  output / valid YAML
2227
2329
 
2228
- programmable / funnel.diagnostics.diagnose() / .diagnoseAll() / .recentEvents() / .droppedEvents() / .connectionErrors() / .replay()
2330
+ programmable / funnel.diagnostics.diagnose() / .diagnoseAll() / .recentEvents() / .droppedEvents() / .rawEvents() / .connectionErrors() / .connectionTimeline() / .recentLogs() / .replay()
2229
2331
 
2230
2332
  examples:
2231
2333
  funnel debug
2232
2334
  funnel debug --all
2233
2335
  funnel debug --channel ops
2234
2336
  funnel debug events --channel ops --limit 50
2235
- funnel debug dropped --channel ops`;
2337
+ funnel debug dropped --channel ops
2338
+ funnel debug connection --channel ops --connector slack-prod
2339
+ funnel debug logs --grep slack`;
2236
2340
  const debugEventsHelp = `funnel debug events / last N processed events
2237
2341
 
2238
2342
  usage / funnel debug events [--channel <name>] [--limit <N>]
@@ -2256,8 +2360,30 @@ const debugReplayHelp = `funnel debug replay / re-publish a past event into a ch
2256
2360
  usage / funnel debug replay --channel <name> [--seq <N>]
2257
2361
 
2258
2362
  programmable / funnel.diagnostics.replay(channel, seq?)`;
2363
+ const debugRawHelp = `funnel debug raw / raw inbound events before any processing
2364
+
2365
+ usage / funnel debug raw [--channel <name>] [--connector <name>] [--limit <N>]
2366
+
2367
+ shows every event the listener received, including those dropped pre-processor
2368
+ (envelope shape changes, pre-READY, malformed payloads).
2369
+
2370
+ programmable / funnel.diagnostics.rawEvents(channel, { connector, limit })`;
2371
+ const debugConnectionHelp = `funnel debug connection / full lifecycle (started / connected / disconnected / stopped + auth-failed / error)
2372
+
2373
+ usage / funnel debug connection [--channel <name>] [--connector <name>] [--limit <N>]
2374
+
2375
+ programmable / funnel.diagnostics.connectionTimeline(channel, { connector, limit })`;
2376
+ const debugLogsHelp = `funnel debug logs / tail of the gateway daemon log
2377
+
2378
+ usage / funnel debug logs [--grep <substr>] [--limit <N>]
2379
+
2380
+ shows funnel.log entries — structured FunnelLogger output and forwarded flume
2381
+ logs. Use --grep to filter (case-insensitive substring).
2382
+
2383
+ programmable / funnel.diagnostics.recentLogs({ grep, limit })`;
2259
2384
  const channelLimitQuery = z.object({
2260
2385
  channel: z.string().optional(),
2386
+ connector: z.string().optional(),
2261
2387
  limit: z.string().optional()
2262
2388
  });
2263
2389
  const resolveTargetChannel = (c, channelArg) => {
@@ -2332,7 +2458,10 @@ const debugEventsHandler = factory.createHandlers(helpGuard(debugEventsHelp), zV
2332
2458
  const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
2333
2459
  const resolved = resolveTargetChannel(c, query.channel);
2334
2460
  if (resolved.kind === "error") return c.text(renderYaml(resolved.payload));
2335
- const events = await funnel.diagnostics.recentEvents(resolved.name, limit);
2461
+ const events = await funnel.diagnostics.recentEvents(resolved.name, {
2462
+ connector: query.connector,
2463
+ limit
2464
+ });
2336
2465
  return c.text(renderYaml({ events }));
2337
2466
  });
2338
2467
  const debugDroppedHandler = factory.createHandlers(helpGuard(debugDroppedHelp), zValidator$1("query", channelLimitQuery), async (c) => {
@@ -2341,7 +2470,10 @@ const debugDroppedHandler = factory.createHandlers(helpGuard(debugDroppedHelp),
2341
2470
  const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
2342
2471
  const resolved = resolveTargetChannel(c, query.channel);
2343
2472
  if (resolved.kind === "error") return c.text(renderYaml(resolved.payload));
2344
- const events = await funnel.diagnostics.droppedEvents(resolved.name, limit);
2473
+ const events = await funnel.diagnostics.droppedEvents(resolved.name, {
2474
+ connector: query.connector,
2475
+ limit
2476
+ });
2345
2477
  return c.text(renderYaml({ dropped: events }));
2346
2478
  });
2347
2479
  const debugErrorsHandler = factory.createHandlers(helpGuard(debugErrorsHelp), zValidator$1("query", channelLimitQuery), async (c) => {
@@ -2350,7 +2482,10 @@ const debugErrorsHandler = factory.createHandlers(helpGuard(debugErrorsHelp), zV
2350
2482
  const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
2351
2483
  const resolved = resolveTargetChannel(c, query.channel);
2352
2484
  if (resolved.kind === "error") return c.text(renderYaml(resolved.payload));
2353
- const errors = await funnel.diagnostics.connectionErrors(resolved.name, limit);
2485
+ const errors = await funnel.diagnostics.connectionErrors(resolved.name, {
2486
+ connector: query.connector,
2487
+ limit
2488
+ });
2354
2489
  return c.text(renderYaml({ errors }));
2355
2490
  });
2356
2491
  const debugReplayHandler = factory.createHandlers(helpGuard(debugReplayHelp), zValidator$1("query", z.object({
@@ -2366,6 +2501,43 @@ const debugReplayHandler = factory.createHandlers(helpGuard(debugReplayHelp), zV
2366
2501
  const result = await funnel.diagnostics.replay(resolved.name, seq);
2367
2502
  return c.text(renderYaml(result));
2368
2503
  });
2504
+ const debugRawHandler = factory.createHandlers(helpGuard(debugRawHelp), zValidator$1("query", channelLimitQuery), async (c) => {
2505
+ const query = c.req.valid("query");
2506
+ const funnel = c.env.funnel;
2507
+ const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
2508
+ const resolved = resolveTargetChannel(c, query.channel);
2509
+ if (resolved.kind === "error") return c.text(renderYaml(resolved.payload));
2510
+ const events = await funnel.diagnostics.rawEvents(resolved.name, {
2511
+ connector: query.connector,
2512
+ limit
2513
+ });
2514
+ return c.text(renderYaml({ raw: events }));
2515
+ });
2516
+ const debugConnectionHandler = factory.createHandlers(helpGuard(debugConnectionHelp), zValidator$1("query", channelLimitQuery), async (c) => {
2517
+ const query = c.req.valid("query");
2518
+ const funnel = c.env.funnel;
2519
+ const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
2520
+ const resolved = resolveTargetChannel(c, query.channel);
2521
+ if (resolved.kind === "error") return c.text(renderYaml(resolved.payload));
2522
+ const timeline = await funnel.diagnostics.connectionTimeline(resolved.name, {
2523
+ connector: query.connector,
2524
+ limit
2525
+ });
2526
+ return c.text(renderYaml({ connection: timeline }));
2527
+ });
2528
+ const debugLogsHandler = factory.createHandlers(helpGuard(debugLogsHelp), zValidator$1("query", z.object({
2529
+ grep: z.string().optional(),
2530
+ limit: z.string().optional()
2531
+ })), async (c) => {
2532
+ const query = c.req.valid("query");
2533
+ const funnel = c.env.funnel;
2534
+ const limit = query.limit ? Math.max(1, Number(query.limit)) : 200;
2535
+ const result = await funnel.diagnostics.recentLogs({
2536
+ grep: query.grep,
2537
+ limit
2538
+ });
2539
+ return c.text(renderYaml(result));
2540
+ });
2369
2541
  const docsIndexHandler = factory.createHandlers(helpGuard(`funnel docs / embedded documentation
2370
2542
 
2371
2543
  usage / funnel docs [topic]
@@ -2452,14 +2624,21 @@ examples:
2452
2624
  funnel gateway logs
2453
2625
  funnel gateway sql --preset recent`;
2454
2626
  const renderGatewayStatus = async (c) => {
2455
- const status = c.env.funnel.gateway.getStatus();
2627
+ const funnel = c.env.funnel;
2628
+ const status = funnel.gateway.getStatus();
2456
2629
  if (!status.running) return c.text(renderYaml({ running: false }), 503);
2457
- const res = await fetch(`${gatewayLoopbackUrl(status.port)}/status`).catch(() => null);
2458
- if (!res) return c.text(renderYaml({
2630
+ const token = funnel.gatewayToken.read();
2631
+ let res = null;
2632
+ try {
2633
+ res = await loopbackFetch(`${gatewayLoopbackUrl(status.port)}/status`, { headers: token ? { authorization: `Bearer ${token}` } : {} });
2634
+ } catch {
2635
+ res = null;
2636
+ }
2637
+ if (!res || !res.ok) return c.text(renderYaml({
2459
2638
  running: true,
2460
2639
  pid: status.pid,
2461
2640
  port: status.port,
2462
- error: "health check failed"
2641
+ error: res ? `status check failed (${res.status})` : "status check failed"
2463
2642
  }));
2464
2643
  const data = await res.json();
2465
2644
  return c.text(renderYaml({
@@ -2769,7 +2948,13 @@ const HEALTH_POLL_INTERVAL_MS = 100;
2769
2948
  const waitForHealth = async (port) => {
2770
2949
  const deadline = Date.now() + HEALTH_TIMEOUT_MS;
2771
2950
  while (Date.now() < deadline) {
2772
- if ((await fetch(`${gatewayLoopbackUrl(port)}/health`).catch(() => null))?.ok) return true;
2951
+ let ok = false;
2952
+ try {
2953
+ ok = (await loopbackFetch(`${gatewayLoopbackUrl(port)}/health`)).ok;
2954
+ } catch {
2955
+ ok = false;
2956
+ }
2957
+ if (ok) return true;
2773
2958
  await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_INTERVAL_MS));
2774
2959
  }
2775
2960
  return false;
@@ -2937,7 +3122,7 @@ const launchHelp = `funnel profiles <name> run — launch a profile (sugar for f
2937
3122
  usage: funnel profiles <name> run [additional claude args...]
2938
3123
  funnel profiles <name> (alias)`;
2939
3124
  const RESERVED_KEYS = [];
2940
- const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), helpGuard(launchHelp), zValidator$1("query", z.object({}).passthrough()), async (c) => {
3125
+ const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), helpGuard(launchHelp), zValidator$1("query", z.object({}).loose()), async (c) => {
2941
3126
  const param = c.req.valid("param");
2942
3127
  c.env.funnel;
2943
3128
  const { profiles, claude } = c.env;
@@ -3113,7 +3298,13 @@ const buildStatusReport = async (funnel, profiles) => {
3113
3298
  const gatewayStatus = funnel.gateway.getStatus();
3114
3299
  let gatewayData = null;
3115
3300
  if (gatewayStatus.running) {
3116
- const res = await fetch(`${gatewayLoopbackUrl(gatewayStatus.port)}/status`).catch(() => null);
3301
+ const token = funnel.gatewayToken.read();
3302
+ let res = null;
3303
+ try {
3304
+ res = await loopbackFetch(`${gatewayLoopbackUrl(gatewayStatus.port)}/status`, { headers: token ? { authorization: `Bearer ${token}` } : {} });
3305
+ } catch {
3306
+ res = null;
3307
+ }
3117
3308
  if (res && res.ok) {
3118
3309
  const body = await res.json();
3119
3310
  if (isGatewayStatus(body)) gatewayData = body;
@@ -3215,6 +3406,6 @@ const updateHandler = factory.createHandlers(helpGuard(updateHelp), async (c) =>
3215
3406
  const routes = factory.createApp().onError((error, c) => {
3216
3407
  if (error instanceof HTTPException) return c.text(error.message, error.status);
3217
3408
  return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
3218
- }).get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...channelsAddHelpHandler).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...channelsRemoveHelpHandler).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...channelsRenameHelpHandler).post("/channels/:channel/rename", ...channelsChannelRenameHelpHandler).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...channelsPublishHelpHandler).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel/validate", ...channelsValidateHandler).get("/channels/validate", ...channelsValidateHelpHandler).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...channelsConnectorsAddHelpHandler).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...channelsConnectorsRemoveHelpHandler).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...channelsConnectorsSetHelpHandler).post("/channels/:channel/connectors/set/:connector", ...channelsConnectorsSetHandler).post("/channels/:channel/connectors/rename/:connector/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/:connector/rename/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/rename", ...channelsConnectorsRenameHelpHandler).post("/channels/:channel/connectors/:connector/rename", ...channelsConnectorRenameHelpHandler).post("/channels/:channel/connectors/:connector/request", ...channelsConnectorsRequestHandler).get("/channels/:channel/connectors/:connector", ...channelsConnectorsShowHandler).get("/channels/:channel/connectors/:connector/schedules", ...channelsConnectorsSchedulesGroupHandler).post("/channels/:channel/connectors/:connector/schedules/add", ...channelsConnectorSchedulesAddHelpHandler).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...channelsConnectorSchedulesRemoveHelpHandler).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...profilesAddHelpHandler).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...profilesSetHelpHandler).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...profilesRemoveHelpHandler).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...profilesRenameHelpHandler).post("/profiles/:profile/rename", ...profilesProfileRenameHelpHandler).post("/profiles/:profile/as-default", ...profilesAsDefaultHandler).get("/profiles/:profile/run", ...profilesLaunchHandler).get("/profiles/:profile", ...profilesLaunchHandler).get("/gateway", ...gatewayGroupHandler).get("/gateway/status", ...gatewayStatusHandler).get("/gateway/start", ...gatewayStartHandler).get("/gateway/stop", ...gatewayStopHandler).get("/gateway/restart", ...gatewayRestartHandler).get("/gateway/run", ...gatewayRunHandler).get("/gateway/logs", ...gatewayLogsHandler).get("/gateway/sql", ...gatewaySqlHandler).get("/gateway/listeners", ...gatewayListenersHandler).get("/debug", ...debugHandler).get("/debug/events", ...debugEventsHandler).get("/debug/dropped", ...debugDroppedHandler).get("/debug/errors", ...debugErrorsHandler).get("/debug/replay", ...debugReplayHandler).get("/docs", ...docsIndexHandler).get("/docs/:topic", ...docsTopicHandler).get("/doctor", ...doctorHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
3409
+ }).get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...channelsAddHelpHandler).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...channelsRemoveHelpHandler).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...channelsRenameHelpHandler).post("/channels/:channel/rename", ...channelsChannelRenameHelpHandler).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...channelsPublishHelpHandler).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel/validate", ...channelsValidateHandler).get("/channels/validate", ...channelsValidateHelpHandler).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...channelsConnectorsAddHelpHandler).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...channelsConnectorsRemoveHelpHandler).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...channelsConnectorsSetHelpHandler).post("/channels/:channel/connectors/set/:connector", ...channelsConnectorsSetHandler).post("/channels/:channel/connectors/rename/:connector/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/:connector/rename/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/rename", ...channelsConnectorsRenameHelpHandler).post("/channels/:channel/connectors/:connector/rename", ...channelsConnectorRenameHelpHandler).post("/channels/:channel/connectors/:connector/request", ...channelsConnectorsRequestHandler).get("/channels/:channel/connectors/:connector", ...channelsConnectorsShowHandler).get("/channels/:channel/connectors/:connector/schedules", ...channelsConnectorsSchedulesGroupHandler).post("/channels/:channel/connectors/:connector/schedules/add", ...channelsConnectorSchedulesAddHelpHandler).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...channelsConnectorSchedulesRemoveHelpHandler).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...profilesAddHelpHandler).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...profilesSetHelpHandler).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...profilesRemoveHelpHandler).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...profilesRenameHelpHandler).post("/profiles/:profile/rename", ...profilesProfileRenameHelpHandler).post("/profiles/:profile/as-default", ...profilesAsDefaultHandler).get("/profiles/:profile/run", ...profilesLaunchHandler).get("/profiles/:profile", ...profilesLaunchHandler).get("/gateway", ...gatewayGroupHandler).get("/gateway/status", ...gatewayStatusHandler).get("/gateway/start", ...gatewayStartHandler).get("/gateway/stop", ...gatewayStopHandler).get("/gateway/restart", ...gatewayRestartHandler).get("/gateway/run", ...gatewayRunHandler).get("/gateway/logs", ...gatewayLogsHandler).get("/gateway/sql", ...gatewaySqlHandler).get("/gateway/listeners", ...gatewayListenersHandler).get("/debug", ...debugHandler).get("/debug/events", ...debugEventsHandler).get("/debug/dropped", ...debugDroppedHandler).get("/debug/errors", ...debugErrorsHandler).get("/debug/replay", ...debugReplayHandler).get("/debug/raw", ...debugRawHandler).get("/debug/connection", ...debugConnectionHandler).get("/debug/logs", ...debugLogsHandler).get("/docs", ...docsIndexHandler).get("/docs/:topic", ...docsTopicHandler).get("/doctor", ...doctorHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
3219
3410
  //#endregion
3220
- export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClock, FunnelConnectorAdapter, FunnelConnectorListener, FunnelConnectorRegistry, FunnelDiagnostics, FunnelDocs, FunnelDoctor, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelHttpClient, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLogger, FunnelProcessRunner, FunnelRecovery, FunnelSettingsReader, FunnelSettingsStore, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelHttpClient, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelHttpClient, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, baseConnectorConfigSchema, buildServiceRoutes, channelConfigSchema, channelDeliveryModeSchema, channelWsProtocols, channelWsUrl, routes as cliRoutes, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, createSettings, factory, funnelEventSchema, gatewayLoopbackUrl, previewOf, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryRows, queryToCliArgs, resolveFunnelDir, resolveFunnelPort, settingsSchema, toDiagnosticConnectionError, toDiagnosticEvent, toRequest };
3411
+ export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, Funnel, FunnelAuthFailedError, FunnelBroadcaster, FunnelChannelAlreadyExistsError, FunnelChannelNotFoundError, FunnelChannelPublisher, FunnelChannels, FunnelClock, FunnelConnectorAdapter, FunnelConnectorListener, FunnelConnectorNotFoundError, FunnelConnectorRegistry, FunnelConnectorTypeMismatchError, FunnelDiagnostics, FunnelDocs, FunnelDoctor, FunnelError, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayBindError, FunnelGatewayServer, FunnelGatewayToken, FunnelHttpClient, FunnelIdGenerator, FunnelListenerRegistry, FunnelListenersClient, FunnelLogger, FunnelProcessRunner, FunnelRecovery, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenCollisionError, FunnelTokenPrompter, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelHttpClient, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelHttpClient, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, baseConnectorConfigSchema, buildServiceRoutes, channelConfigSchema, channelDeliveryModeSchema, channelWsProtocols, channelWsUrl, routes as cliRoutes, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, createSettings, discordConnectorSchema, factory, funnelEventSchema, gatewayLoopbackUrl, ghConnectorSchema, previewOf, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryRows, queryToCliArgs, resolveFunnelDir, resolveFunnelPort, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, toDiagnosticConnectionError, toDiagnosticEvent, toRequest };