@interactive-inc/claude-funnel 0.60.1 → 0.64.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-CRGb6B5_.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-BFIhyTfa.d.ts} +49 -10
  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 +106 -132
  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-C_PLxfUX.d.ts → file-process-guard-tVcgckH6.d.ts} +6 -6
  24. package/dist/{file-system-o51IsM0W.d.ts → file-system-VhwwXZbm.d.ts} +8 -0
  25. package/dist/flume-source-listener-BNyAII7N.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-Ds6sHhA-.d.ts} +37 -19
  40. package/dist/index.d.ts +182 -22
  41. package/dist/index.js +363 -173
  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-BoV8Hf-n.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-CxpWagbT.js +388 -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-93pX7EF7.js → yaml-render--J1_3BSA.js} +25 -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-93pX7EF7.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,7 +1078,7 @@ 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
@@ -1049,9 +1107,20 @@ var Funnel = class Funnel {
1049
1107
  Object.freeze(this);
1050
1108
  }
1051
1109
  /**
1052
- * Sandboxed Funnel wired with in-memory implementations for every IO boundary.
1053
- * Touches no real disk, processes, wall-clock time, or UUIDs — safe for tests
1054
- * 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.
1055
1124
  */
1056
1125
  static inMemory(props = {}) {
1057
1126
  return new Funnel({
@@ -1062,6 +1131,9 @@ var Funnel = class Funnel {
1062
1131
  logger: props.logger ?? new MemoryFunnelLogger(),
1063
1132
  clock: props.clock ?? new MemoryFunnelClock(),
1064
1133
  idGenerator: props.idGenerator ?? new MemoryFunnelIdGenerator(),
1134
+ http: props.http ?? new MemoryFunnelHttpClient(),
1135
+ tokenPrompter: props.tokenPrompter ?? new MemoryFunnelTokenPrompter(),
1136
+ diagnosticLog: props.diagnosticLog ?? new MemoryConnectorDiagnosticLog(),
1065
1137
  dir: props.dir ?? SANDBOX_DIR,
1066
1138
  tmpDir: props.tmpDir ?? SANDBOX_TMP_DIR
1067
1139
  });
@@ -1094,7 +1166,7 @@ var Funnel = class Funnel {
1094
1166
  * independently of FunnelClaude (e.g. checking if a named profile is running).
1095
1167
  */
1096
1168
  createProcessGuard() {
1097
- return new FileProcessGuard({
1169
+ return new FunnelFileProcessGuard({
1098
1170
  fs: this.fs,
1099
1171
  process: this.process,
1100
1172
  dir: this.paths.dir
@@ -1182,31 +1254,6 @@ var NoopFunnelLogger = class extends FunnelLogger {
1182
1254
  error() {}
1183
1255
  };
1184
1256
  //#endregion
1185
- //#region lib/engine/http/memory-http-client.ts
1186
- var MemoryFunnelHttpClient = class extends FunnelHttpClient {
1187
- calls = [];
1188
- handler = () => ({
1189
- status: 200,
1190
- body: ""
1191
- });
1192
- on(handler) {
1193
- this.handler = handler;
1194
- return this;
1195
- }
1196
- async fetch(request) {
1197
- this.calls.push(request);
1198
- const response = await this.handler(request);
1199
- const status = response.status ?? 200;
1200
- const body = response.body ?? "";
1201
- return {
1202
- status,
1203
- ok: status >= 200 && status < 300,
1204
- text: async () => body,
1205
- json: async () => JSON.parse(body)
1206
- };
1207
- }
1208
- };
1209
- //#endregion
1210
1257
  //#region lib/gateway/service-routes.ts
1211
1258
  /**
1212
1259
  * Mountable Hono app that exposes the service layer (`FunnelDiagnostics` +
@@ -1235,24 +1282,65 @@ const buildServiceRoutes = (deps) => {
1235
1282
  });
1236
1283
  app.get("/diagnostics/events", async (c) => {
1237
1284
  const channel = c.req.query("channel") ?? null;
1285
+ const connector = c.req.query("connector");
1238
1286
  const limit = Number(c.req.query("limit") ?? "20");
1239
- const events = await deps.diagnostics.recentEvents(channel, limit);
1287
+ const events = await deps.diagnostics.recentEvents(channel, {
1288
+ connector,
1289
+ limit
1290
+ });
1240
1291
  return c.json(events);
1241
1292
  });
1242
1293
  app.get("/diagnostics/dropped", async (c) => {
1243
1294
  const channel = c.req.query("channel") ?? null;
1295
+ const connector = c.req.query("connector");
1244
1296
  const limit = Number(c.req.query("limit") ?? "20");
1245
- const events = await deps.diagnostics.droppedEvents(channel, limit);
1297
+ const events = await deps.diagnostics.droppedEvents(channel, {
1298
+ connector,
1299
+ limit
1300
+ });
1246
1301
  return c.json(events);
1247
1302
  });
1248
1303
  app.get("/diagnostics/errors", async (c) => {
1249
1304
  const channel = c.req.query("channel") ?? null;
1305
+ const connector = c.req.query("connector");
1250
1306
  const limit = Number(c.req.query("limit") ?? "20");
1251
- const errors = await deps.diagnostics.connectionErrors(channel, limit);
1307
+ const errors = await deps.diagnostics.connectionErrors(channel, {
1308
+ connector,
1309
+ limit
1310
+ });
1252
1311
  return c.json(errors);
1253
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
+ });
1254
1342
  app.post("/diagnostics/replay", async (c) => {
1255
- const body = await c.req.json().catch(() => ({}));
1343
+ const body = await readJsonBody(c);
1256
1344
  const channel = typeof body.channel === "string" ? body.channel : null;
1257
1345
  const seq = typeof body.seq === "number" ? body.seq : void 0;
1258
1346
  if (!channel) return c.json({ error: "channel is required" }, 400);
@@ -1260,13 +1348,22 @@ const buildServiceRoutes = (deps) => {
1260
1348
  return c.json(result);
1261
1349
  });
1262
1350
  app.post("/doctor", async (c) => {
1263
- const body = await c.req.json().catch(() => ({}));
1351
+ const body = await readJsonBody(c);
1264
1352
  const mode = body.mode === "safe" || body.mode === "aggressive" || body.mode === "off" ? body.mode : "off";
1265
1353
  const report = await deps.doctor.run(mode);
1266
1354
  return c.json(report);
1267
1355
  });
1268
1356
  return app;
1269
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
+ };
1270
1367
  //#endregion
1271
1368
  //#region lib/cli/factory.ts
1272
1369
  const factory = createFactory();
@@ -1628,7 +1725,7 @@ const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param"
1628
1725
  "bot-token": z.string().optional(),
1629
1726
  "app-token": z.string().optional(),
1630
1727
  "poll-interval": z.coerce.number().int().positive().optional()
1631
- }).passthrough()), async (c) => {
1728
+ }).loose()), async (c) => {
1632
1729
  const param = c.req.valid("param");
1633
1730
  const query = c.req.valid("query");
1634
1731
  const funnel = c.env.funnel;
@@ -1720,7 +1817,7 @@ options:
1720
1817
  output / valid YAML (or raw text when the adapter returns text)`), zValidator$1("query", z.object({
1721
1818
  method: z.string(),
1722
1819
  path: z.string().optional()
1723
- }).passthrough()), async (c) => {
1820
+ }).loose()), async (c) => {
1724
1821
  const param = c.req.valid("param");
1725
1822
  const query = c.req.valid("query");
1726
1823
  const funnel = c.env.funnel;
@@ -1841,7 +1938,7 @@ const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$7))
1841
1938
  const querySchema = z.object({
1842
1939
  content: z.string().min(1, { message: "--content is required" }),
1843
1940
  connector: z.string().min(1).optional()
1844
- }).passthrough();
1941
+ }).loose();
1845
1942
  const channelsPublishHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", querySchema), async (c) => {
1846
1943
  const param = c.req.valid("param");
1847
1944
  const query = c.req.valid("query");
@@ -2144,7 +2241,7 @@ const RESERVED_KEYS$1 = ["profile", "channel"];
2144
2241
  const claudeHandler = factory.createHandlers(helpGuard(claudeHelp), zValidator$1("query", z.object({
2145
2242
  profile: z.string().optional(),
2146
2243
  channel: z.string().optional()
2147
- }).passthrough()), async (c) => {
2244
+ }).loose()), async (c) => {
2148
2245
  const query = c.req.valid("query");
2149
2246
  const { funnel, claude, profiles, localConfig, localConfigSync } = c.env;
2150
2247
  const userArgs = queryToCliArgs(c.req.url, RESERVED_KEYS$1);
@@ -2205,35 +2302,41 @@ const claudeHandler = factory.createHandlers(helpGuard(claudeHelp), zValidator$1
2205
2302
  });
2206
2303
  //#endregion
2207
2304
  //#region lib/cli/routes/debug.ts
2208
- 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)
2209
2306
 
2210
- usage / funnel debug [subcommand] [--channel <name>] [--all] [--limit <N>]
2307
+ usage / funnel debug [subcommand] [--channel <name>] [--connector <name>] [--all] [--limit <N>]
2211
2308
 
2212
2309
  subcommands:
2213
2310
  (none) / full diagnosis for one channel (or --all for every channel)
2214
2311
  events / last N processed events with outcome
2215
2312
  dropped / events filtered out (skip:*) with payload
2216
- 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)
2217
2317
  replay / re-send a past event into a channel
2218
2318
 
2219
2319
  options:
2220
2320
  --channel <name> / channel to inspect (auto-selected when only one exists)
2321
+ --connector <name> / restrict events/dropped/errors/connection/raw to one connector
2221
2322
  --all / diagnose every channel
2222
- --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)
2223
2324
 
2224
2325
  For the common case, prefer fnl doctor — it runs a full diagnosis and can apply
2225
2326
  safe fixes in one shot. fnl debug is the lower-level view.
2226
2327
 
2227
2328
  output / valid YAML
2228
2329
 
2229
- programmable / funnel.diagnostics.diagnose() / .diagnoseAll() / .recentEvents() / .droppedEvents() / .connectionErrors() / .replay()
2330
+ programmable / funnel.diagnostics.diagnose() / .diagnoseAll() / .recentEvents() / .droppedEvents() / .rawEvents() / .connectionErrors() / .connectionTimeline() / .recentLogs() / .replay()
2230
2331
 
2231
2332
  examples:
2232
2333
  funnel debug
2233
2334
  funnel debug --all
2234
2335
  funnel debug --channel ops
2235
2336
  funnel debug events --channel ops --limit 50
2236
- 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`;
2237
2340
  const debugEventsHelp = `funnel debug events / last N processed events
2238
2341
 
2239
2342
  usage / funnel debug events [--channel <name>] [--limit <N>]
@@ -2257,8 +2360,30 @@ const debugReplayHelp = `funnel debug replay / re-publish a past event into a ch
2257
2360
  usage / funnel debug replay --channel <name> [--seq <N>]
2258
2361
 
2259
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 })`;
2260
2384
  const channelLimitQuery = z.object({
2261
2385
  channel: z.string().optional(),
2386
+ connector: z.string().optional(),
2262
2387
  limit: z.string().optional()
2263
2388
  });
2264
2389
  const resolveTargetChannel = (c, channelArg) => {
@@ -2333,7 +2458,10 @@ const debugEventsHandler = factory.createHandlers(helpGuard(debugEventsHelp), zV
2333
2458
  const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
2334
2459
  const resolved = resolveTargetChannel(c, query.channel);
2335
2460
  if (resolved.kind === "error") return c.text(renderYaml(resolved.payload));
2336
- 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
+ });
2337
2465
  return c.text(renderYaml({ events }));
2338
2466
  });
2339
2467
  const debugDroppedHandler = factory.createHandlers(helpGuard(debugDroppedHelp), zValidator$1("query", channelLimitQuery), async (c) => {
@@ -2342,7 +2470,10 @@ const debugDroppedHandler = factory.createHandlers(helpGuard(debugDroppedHelp),
2342
2470
  const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
2343
2471
  const resolved = resolveTargetChannel(c, query.channel);
2344
2472
  if (resolved.kind === "error") return c.text(renderYaml(resolved.payload));
2345
- 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
+ });
2346
2477
  return c.text(renderYaml({ dropped: events }));
2347
2478
  });
2348
2479
  const debugErrorsHandler = factory.createHandlers(helpGuard(debugErrorsHelp), zValidator$1("query", channelLimitQuery), async (c) => {
@@ -2351,7 +2482,10 @@ const debugErrorsHandler = factory.createHandlers(helpGuard(debugErrorsHelp), zV
2351
2482
  const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
2352
2483
  const resolved = resolveTargetChannel(c, query.channel);
2353
2484
  if (resolved.kind === "error") return c.text(renderYaml(resolved.payload));
2354
- 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
+ });
2355
2489
  return c.text(renderYaml({ errors }));
2356
2490
  });
2357
2491
  const debugReplayHandler = factory.createHandlers(helpGuard(debugReplayHelp), zValidator$1("query", z.object({
@@ -2367,6 +2501,43 @@ const debugReplayHandler = factory.createHandlers(helpGuard(debugReplayHelp), zV
2367
2501
  const result = await funnel.diagnostics.replay(resolved.name, seq);
2368
2502
  return c.text(renderYaml(result));
2369
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
+ });
2370
2541
  const docsIndexHandler = factory.createHandlers(helpGuard(`funnel docs / embedded documentation
2371
2542
 
2372
2543
  usage / funnel docs [topic]
@@ -2453,14 +2624,21 @@ examples:
2453
2624
  funnel gateway logs
2454
2625
  funnel gateway sql --preset recent`;
2455
2626
  const renderGatewayStatus = async (c) => {
2456
- const status = c.env.funnel.gateway.getStatus();
2627
+ const funnel = c.env.funnel;
2628
+ const status = funnel.gateway.getStatus();
2457
2629
  if (!status.running) return c.text(renderYaml({ running: false }), 503);
2458
- const res = await fetch(`${gatewayLoopbackUrl(status.port)}/status`).catch(() => null);
2459
- 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({
2460
2638
  running: true,
2461
2639
  pid: status.pid,
2462
2640
  port: status.port,
2463
- error: "health check failed"
2641
+ error: res ? `status check failed (${res.status})` : "status check failed"
2464
2642
  }));
2465
2643
  const data = await res.json();
2466
2644
  return c.text(renderYaml({
@@ -2770,7 +2948,13 @@ const HEALTH_POLL_INTERVAL_MS = 100;
2770
2948
  const waitForHealth = async (port) => {
2771
2949
  const deadline = Date.now() + HEALTH_TIMEOUT_MS;
2772
2950
  while (Date.now() < deadline) {
2773
- 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;
2774
2958
  await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_INTERVAL_MS));
2775
2959
  }
2776
2960
  return false;
@@ -2938,7 +3122,7 @@ const launchHelp = `funnel profiles <name> run — launch a profile (sugar for f
2938
3122
  usage: funnel profiles <name> run [additional claude args...]
2939
3123
  funnel profiles <name> (alias)`;
2940
3124
  const RESERVED_KEYS = [];
2941
- 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) => {
2942
3126
  const param = c.req.valid("param");
2943
3127
  c.env.funnel;
2944
3128
  const { profiles, claude } = c.env;
@@ -3114,7 +3298,13 @@ const buildStatusReport = async (funnel, profiles) => {
3114
3298
  const gatewayStatus = funnel.gateway.getStatus();
3115
3299
  let gatewayData = null;
3116
3300
  if (gatewayStatus.running) {
3117
- 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
+ }
3118
3308
  if (res && res.ok) {
3119
3309
  const body = await res.json();
3120
3310
  if (isGatewayStatus(body)) gatewayData = body;
@@ -3216,6 +3406,6 @@ const updateHandler = factory.createHandlers(helpGuard(updateHelp), async (c) =>
3216
3406
  const routes = factory.createApp().onError((error, c) => {
3217
3407
  if (error instanceof HTTPException) return c.text(error.message, error.status);
3218
3408
  return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
3219
- }).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);
3220
3410
  //#endregion
3221
- 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 };