@interactive-inc/claude-funnel 0.52.0 → 0.55.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 (72) hide show
  1. package/README.md +25 -3
  2. package/dist/bin.js +1276 -520
  3. package/dist/claude.d.ts +22 -5
  4. package/dist/claude.js +456 -169
  5. package/dist/connector-adapter-1PxjN-Uk.d.ts +25 -0
  6. package/dist/{connector-adapter-D5Utumgz.js → connector-adapter-qwXLjQId.js} +1 -1
  7. package/dist/{connector-listener-DU54DN-f.js → connector-listener-CpHBecCj.js} +1 -1
  8. package/dist/connectors/discord.d.ts +6 -6
  9. package/dist/connectors/discord.js +2 -2
  10. package/dist/connectors/gh.d.ts +6 -6
  11. package/dist/connectors/gh.js +2 -2
  12. package/dist/connectors/schedule.d.ts +12 -2
  13. package/dist/connectors/schedule.js +2 -2
  14. package/dist/connectors/slack.d.ts +3 -3
  15. package/dist/connectors/slack.js +2 -2
  16. package/dist/{connector-diagnostic-log-yTOojKUR.d.ts → diagnostic-log-Bxe7Bbvw.d.ts} +2 -2
  17. package/dist/diagnostic-sql-reader-CzYgZpq2.js +83 -0
  18. package/dist/diagnostics.d.ts +2 -0
  19. package/dist/diagnostics.js +2 -0
  20. package/dist/{discord-connector-schema-CBDyGdOI.js → discord-connector-schema-B_N6IXLz.js} +1 -1
  21. package/dist/{discord-connector-schema-R0Uu-3ns.d.ts → discord-connector-schema-CPgcZkXh.d.ts} +1 -1
  22. package/dist/{discord-listener-_jSE3HsQ.js → discord-listener-C0MoKdQO.js} +6 -6
  23. package/dist/docs.d.ts +2 -0
  24. package/dist/docs.js +2 -0
  25. package/dist/doctor.d.ts +2 -0
  26. package/dist/doctor.js +2 -0
  27. package/dist/{file-process-guard-BgrVHe9I.d.ts → file-process-guard-DI1742H5.d.ts} +31 -15
  28. package/dist/funnel-diagnostics-BpKYrMSu.js +300 -0
  29. package/dist/funnel-diagnostics-qWy5tPSq.d.ts +176 -0
  30. package/dist/funnel-docs-dXPokzr5.d.ts +18 -0
  31. package/dist/funnel-docs-ng5K8w4j.js +653 -0
  32. package/dist/funnel-doctor-BF3Rdgk0.d.ts +34 -0
  33. package/dist/funnel-doctor-CApCezTq.js +82 -0
  34. package/dist/funnel-recovery-BUBsu7WX.d.ts +101 -0
  35. package/dist/funnel-recovery-D9CxD5Zs.js +134 -0
  36. package/dist/gateway/daemon.js +810 -211
  37. package/dist/{settings-store-D2XSXTyt.js → gateway-base-url-6foMXfFf.js} +19 -6
  38. package/dist/gateway.d.ts +3 -3
  39. package/dist/gateway.js +3 -2
  40. package/dist/{gh-connector-schema-eoTtHbY6.d.ts → gh-connector-schema-CU1ojfIF.d.ts} +1 -1
  41. package/dist/{gh-connector-schema-o3Q1-ojL.js → gh-connector-schema-DUcZgN2Q.js} +1 -1
  42. package/dist/{gh-listener-DH-fClQm.js → gh-listener-Dsx6AmhH.js} +5 -5
  43. package/dist/{index-NFs2jzCa.d.ts → index-CrngHrne.d.ts} +187 -619
  44. package/dist/index.d.ts +16 -11
  45. package/dist/index.js +512 -976
  46. package/dist/{local-config-json-schema-8IHjS4Q7.js → local-config-json-schema-DE1zkMcb.js} +35 -9
  47. package/dist/{local-config-sync-BdsrDZOu.d.ts → local-config-sync-B8b04LrZ.d.ts} +45 -25
  48. package/dist/local-config.d.ts +2 -2
  49. package/dist/local-config.js +2 -2
  50. package/dist/{memory-connector-diagnostic-log-CrW1ltLM.js → memory-diagnostic-log-BZ1VD80X.js} +61 -99
  51. package/dist/{memory-token-prompter-B5FFCsGP.d.ts → memory-token-prompter-Lo3YRDzq.d.ts} +4 -4
  52. package/dist/{memory-token-prompter-CLerGsgM.js → memory-token-prompter-vBXxY20-.js} +2 -2
  53. package/dist/{profiles-f0mNmEyP.d.ts → profiles-EHTeCOqB.d.ts} +3 -2
  54. package/dist/profiles.d.ts +1 -1
  55. package/dist/profiles.js +1 -1
  56. package/dist/recovery.d.ts +2 -0
  57. package/dist/recovery.js +2 -0
  58. package/dist/{resolve-connector-token-BHmZLRrV.js → resolve-connector-token-CczqG_Ig.js} +1 -1
  59. package/dist/{schedule-connector-schema-iCI61gzU.js → schedule-connector-schema-B_xO5z5B.js} +1 -1
  60. package/dist/{schedule-listener-CUyUFFR1.d.ts → schedule-listener-DKh0hnkK.d.ts} +5 -5
  61. package/dist/{schedule-listener-ePAjians.js → schedule-listener-DP9Jhc6U.js} +14 -4
  62. package/dist/settings-reader-CBrgz01o.d.ts +18 -0
  63. package/dist/{settings-reader-BSU6JyvM.d.ts → settings-schema-zhnMIa8I.d.ts} +1 -16
  64. package/dist/{slack-connector-schema-BCNWluHM.js → slack-connector-schema-C1zEf4TG.js} +1 -1
  65. package/dist/{slack-listener-Bv5xI9gC.d.ts → slack-listener-COQA8wAZ.d.ts} +4 -4
  66. package/dist/{slack-listener-ClQuHhEF.js → slack-listener-DUKPcpJH.js} +7 -7
  67. package/dist/{mcp-Dr-nIBwN.js → yaml-render-OhUN-qkS.js} +52 -34
  68. package/package.json +21 -1
  69. package/dist/connector-adapter-DKgsVuMH.d.ts +0 -11
  70. /package/dist/{file-system-BeOKXjlV.d.ts → file-system-Wub9Nto4.d.ts} +0 -0
  71. /package/dist/{process-runner-DfniuWVU.d.ts → process-runner-D5I_jhYQ.d.ts} +0 -0
  72. /package/dist/{profiles-wMRnjSid.js → profiles-MnXvYfZF.js} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { join } from "node:path";
2
2
  import { z } from "zod";
3
3
  import { stderr, stdin } from "node:process";
4
- //#region lib/engine/local-config/local-config-schema.ts
4
+ //#region lib/services/local-config/local-config-schema.ts
5
5
  /**
6
6
  * Per-repo launch config (`funnel.json`).
7
7
  *
@@ -79,7 +79,7 @@ const localConfigSchema = z.object({
79
79
  });
80
80
  const LOCAL_CONFIG_FILENAME = "funnel.json";
81
81
  //#endregion
82
- //#region lib/engine/local-config/local-config.ts
82
+ //#region lib/services/local-config/local-config.ts
83
83
  /**
84
84
  * Reads `funnel.json` from a directory. Returns `null` when the file is
85
85
  * absent so callers can fall through to other resolution paths (default
@@ -122,6 +122,18 @@ var FunnelLocalConfig = class {
122
122
  }
123
123
  };
124
124
  //#endregion
125
+ //#region lib/engine/connectors/either-token.ts
126
+ function botTokenSlot(slot) {
127
+ if (slot.env !== void 0) return { botTokenEnv: slot.env };
128
+ if (slot.literal !== void 0) return { botToken: slot.literal };
129
+ return {};
130
+ }
131
+ function appTokenSlot(slot) {
132
+ if (slot.env !== void 0) return { appTokenEnv: slot.env };
133
+ if (slot.literal !== void 0) return { appToken: slot.literal };
134
+ return {};
135
+ }
136
+ //#endregion
125
137
  //#region lib/engine/token-prompter/token-prompter.ts
126
138
  /**
127
139
  * Asks the user for a secret value on stdin. Used as a last resort when a
@@ -131,18 +143,22 @@ var FunnelLocalConfig = class {
131
143
  */
132
144
  var FunnelTokenPrompter = class {};
133
145
  //#endregion
134
- //#region lib/engine/local-config/local-config-sync.ts
146
+ //#region lib/services/local-config/local-config-sync.ts
135
147
  /**
136
148
  * Reconciles a single funnel.json channel spec with `~/.funnel/settings.json`.
137
149
  * The spec is the source of truth for the channel it declares:
138
150
  *
139
151
  * - missing channel → created
140
152
  * - declared connector matched by name → tokens reconciled
141
- * - declared connector matched by token in the same channel under a
142
- * different name → renamed in place (then tokens reconciled)
143
- * - declared connector with no match → added
153
+ * - declared connector with no name match added (prompting for its tokens)
144
154
  * - any connector left in the channel that the spec did not touch → removed
145
155
  *
156
+ * Connectors are matched by NAME only — there is no rename-by-token path. A spec
157
+ * that renames a connector (same token, new name) is reconciled as "add the new
158
+ * name, remove the old one". Because the collision check runs at add time while
159
+ * the old connector is still present, re-using its token at the new name throws
160
+ * a token-collision error; remove the old connector via the CLI first.
161
+ *
146
162
  * Removal only fires when the channel spec has a `connectors` field. An
147
163
  * absent field means "do not manage connectors from here" and leaves
148
164
  * everything in `~/.funnel` alone. Other channels in funnel.json (not
@@ -223,7 +239,14 @@ var FunnelLocalConfigSync = class {
223
239
  id: this.channels.addConnector(channelName, {
224
240
  type: "slack",
225
241
  name: spec.name,
226
- ...update,
242
+ ...botTokenSlot({
243
+ literal: bot.token,
244
+ env: bot.tokenEnv
245
+ }),
246
+ ...appTokenSlot({
247
+ literal: app.token,
248
+ env: app.tokenEnv
249
+ }),
227
250
  ...spec.minify !== void 0 ? { minify: spec.minify } : {}
228
251
  }).id,
229
252
  name: spec.name,
@@ -260,7 +283,10 @@ var FunnelLocalConfigSync = class {
260
283
  id: this.channels.addConnector(channelName, {
261
284
  type: "discord",
262
285
  name: spec.name,
263
- ...update
286
+ ...botTokenSlot({
287
+ literal: bot.token,
288
+ env: bot.tokenEnv
289
+ })
264
290
  }).id,
265
291
  name: spec.name,
266
292
  changed: true
@@ -421,7 +447,7 @@ var NodeFunnelTokenPrompter = class extends FunnelTokenPrompter {
421
447
  }
422
448
  };
423
449
  //#endregion
424
- //#region lib/engine/local-config/local-config-json-schema.ts
450
+ //#region lib/services/local-config/local-config-json-schema.ts
425
451
  /**
426
452
  * Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
427
453
  * `$schema` references in committed `funnel.json` files so editors can give
@@ -1,13 +1,14 @@
1
- import { i as ChannelDeliveryMode, n as FunnelIdGenerator, r as ChannelConfig, t as FunnelSettingsReader } from "./settings-reader-BSU6JyvM.js";
2
- import { S as FunnelLogger, b as FunnelConnectorListener, o as ConnectorDiagnosticLog } from "./connector-diagnostic-log-yTOojKUR.js";
3
- import { r as FunnelProcessRunner } from "./process-runner-DfniuWVU.js";
4
- import { n as FunnelFileSystem } from "./file-system-BeOKXjlV.js";
5
- import { n as FunnelConnectorAdapter, t as CallInput } from "./connector-adapter-DKgsVuMH.js";
6
- import { a as ScheduleEntry, n as ScheduleOnFired } from "./schedule-listener-CUyUFFR1.js";
7
- import { n as SlackOnAppCreated, r as SlackPreprocessEvent } from "./slack-listener-Bv5xI9gC.js";
1
+ import { n as ChannelDeliveryMode, t as ChannelConfig } from "./settings-schema-zhnMIa8I.js";
2
+ import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-CBrgz01o.js";
3
+ import { S as FunnelLogger, b as FunnelConnectorListener, o as ConnectorDiagnosticLog } from "./diagnostic-log-Bxe7Bbvw.js";
4
+ import { r as FunnelProcessRunner } from "./process-runner-D5I_jhYQ.js";
5
+ import { n as FunnelFileSystem } from "./file-system-Wub9Nto4.js";
6
+ import { n as FunnelConnectorAdapter, t as CallInput } from "./connector-adapter-1PxjN-Uk.js";
7
+ import { a as ScheduleEntry, n as ScheduleOnFired } from "./schedule-listener-DKh0hnkK.js";
8
+ import { n as SlackOnAppCreated, r as SlackPreprocessEvent } from "./slack-listener-COQA8wAZ.js";
8
9
  import { z } from "zod";
9
10
 
10
- //#region lib/engine/local-config/local-config-schema.d.ts
11
+ //#region lib/services/local-config/local-config-schema.d.ts
11
12
  declare const connectorSpecSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
12
13
  type: z.ZodLiteral<"slack">;
13
14
  name: z.ZodString;
@@ -83,7 +84,7 @@ declare const localConfigSchema: z.ZodObject<{
83
84
  type LocalConfig = z.infer<typeof localConfigSchema>;
84
85
  declare const LOCAL_CONFIG_FILENAME = "funnel.json";
85
86
  //#endregion
86
- //#region lib/engine/local-config/local-config.d.ts
87
+ //#region lib/services/local-config/local-config.d.ts
87
88
  type Deps$3 = {
88
89
  fs: FunnelFileSystem;
89
90
  };
@@ -100,7 +101,7 @@ declare class FunnelLocalConfig {
100
101
  private assertProfilesValid;
101
102
  }
102
103
  //#endregion
103
- //#region lib/connectors/connector-config-schema.d.ts
104
+ //#region lib/engine/connectors/connector-config-schema.d.ts
104
105
  declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
105
106
  id: z.ZodString;
106
107
  name: z.ZodString;
@@ -148,7 +149,28 @@ declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
148
149
  type ConnectorConfig = z.infer<typeof connectorConfigSchema>;
149
150
  type ConnectorType = ConnectorConfig["type"];
150
151
  //#endregion
151
- //#region lib/connectors/connector-factory.d.ts
152
+ //#region lib/engine/connectors/either-token.d.ts
153
+ /**
154
+ * A single connector token slot is supplied one of two non-empty ways, which
155
+ * are mutually exclusive, or left empty:
156
+ *
157
+ * - the literal secret (`botToken: "xoxb-…"`)
158
+ * - the *name* of an env var holding it (`botTokenEnv: "SLACK_BOT_TOKEN"`)
159
+ * - neither — left for the CLI / TTY prompt to fill in at launch
160
+ *
161
+ * `EitherToken<"botToken", "botTokenEnv">` makes "both set at once" a compile
162
+ * error while still allowing "neither". Compose multiple slots with `&` (a slack
163
+ * connector intersects a bot slot and an app slot); the intersection keeps each
164
+ * slot independently exclusive without enumerating the cross-product.
165
+ *
166
+ * To build a value, use the slot helpers below (`botTokenSlot` etc.). They take
167
+ * a resolved `{ literal, env }` and return the exclusive shape. A generic
168
+ * builder can't: TS can't prove a `Record<Env, …>` omits the `Literal` key when
169
+ * both are free type params, so each helper fixes concrete key names instead.
170
+ */
171
+ type EitherToken<Literal extends string, Env extends string> = (Partial<Record<Literal, string>> & Partial<Record<Env, never>>) | (Partial<Record<Literal, never>> & Partial<Record<Env, string>>);
172
+ //#endregion
173
+ //#region lib/engine/connectors/connector-factory.d.ts
152
174
  type SlackListenerOptions = {
153
175
  onAppCreated?: SlackOnAppCreated;
154
176
  preprocessEvent?: SlackPreprocessEvent;
@@ -224,24 +246,18 @@ type ChannelConnectorView = ConnectorConfig & {
224
246
  channelId: string;
225
247
  channelName: string;
226
248
  };
227
- type AddConnectorInput = {
249
+ type AddConnectorInput = ({
228
250
  type: "slack";
229
251
  name: string;
230
- botToken?: string;
231
- appToken?: string;
232
- botTokenEnv?: string;
233
- appTokenEnv?: string;
234
252
  minify?: boolean;
235
- } | {
253
+ } & EitherToken<"botToken", "botTokenEnv"> & EitherToken<"appToken", "appTokenEnv">) | {
236
254
  type: "gh";
237
255
  name: string;
238
256
  pollInterval?: number;
239
- } | {
257
+ } | ({
240
258
  type: "discord";
241
259
  name: string;
242
- botToken?: string;
243
- botTokenEnv?: string;
244
- } | {
260
+ } & EitherToken<"botToken", "botTokenEnv">) | {
245
261
  type: "schedule";
246
262
  name: string;
247
263
  entries?: ScheduleEntry[];
@@ -322,7 +338,7 @@ declare abstract class FunnelTokenPrompter {
322
338
  abstract promptSecret(label: string): Promise<string>;
323
339
  }
324
340
  //#endregion
325
- //#region lib/engine/local-config/local-config-sync.d.ts
341
+ //#region lib/services/local-config/local-config-sync.d.ts
326
342
  type Deps = {
327
343
  channels: FunnelChannels;
328
344
  prompter: FunnelTokenPrompter;
@@ -341,11 +357,15 @@ type LocalConfigSyncResult = {
341
357
  *
342
358
  * - missing channel → created
343
359
  * - declared connector matched by name → tokens reconciled
344
- * - declared connector matched by token in the same channel under a
345
- * different name → renamed in place (then tokens reconciled)
346
- * - declared connector with no match → added
360
+ * - declared connector with no name match added (prompting for its tokens)
347
361
  * - any connector left in the channel that the spec did not touch → removed
348
362
  *
363
+ * Connectors are matched by NAME only — there is no rename-by-token path. A spec
364
+ * that renames a connector (same token, new name) is reconciled as "add the new
365
+ * name, remove the old one". Because the collision check runs at add time while
366
+ * the old connector is still present, re-using its token at the new name throws
367
+ * a token-collision error; remove the old connector via the CLI first.
368
+ *
349
369
  * Removal only fires when the channel spec has a `connectors` field. An
350
370
  * absent field means "do not manage connectors from here" and leaves
351
371
  * everything in `~/.funnel` alone. Other channels in funnel.json (not
@@ -1,3 +1,3 @@
1
- import { C as profileSpecSchema, S as localConfigSchema, _ as LOCAL_CONFIG_FILENAME, b as channelSpecSchema, g as ConnectorSpec, h as ChannelSpec, i as FunnelTokenPrompter, m as FunnelLocalConfig, n as FunnelLocalConfigSync, r as LocalConfigSyncResult, t as ConnectorSyncOutcome, v as LocalConfig, x as connectorSpecSchema, y as ProfileSpec } from "./local-config-sync-BdsrDZOu.js";
2
- import { i as funnelJsonSchema, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigWriter, t as MemoryFunnelTokenPrompter } from "./memory-token-prompter-B5FFCsGP.js";
1
+ import { C as profileSpecSchema, S as localConfigSchema, _ as LOCAL_CONFIG_FILENAME, b as channelSpecSchema, g as ConnectorSpec, h as ChannelSpec, i as FunnelTokenPrompter, m as FunnelLocalConfig, n as FunnelLocalConfigSync, r as LocalConfigSyncResult, t as ConnectorSyncOutcome, v as LocalConfig, x as connectorSpecSchema, y as ProfileSpec } from "./local-config-sync-B8b04LrZ.js";
2
+ import { i as funnelJsonSchema, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigWriter, t as MemoryFunnelTokenPrompter } from "./memory-token-prompter-Lo3YRDzq.js";
3
3
  export { ChannelSpec, ConnectorSpec, ConnectorSyncOutcome, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, LocalConfig, LocalConfigSyncResult, MemoryFunnelTokenPrompter, NodeFunnelTokenPrompter, ProfileSpec, channelSpecSchema, connectorSpecSchema, funnelJsonSchema, localConfigSchema, profileSpecSchema };
@@ -1,3 +1,3 @@
1
- import { a as FunnelLocalConfig, c as connectorSpecSchema, i as FunnelTokenPrompter, l as localConfigSchema, n as NodeFunnelTokenPrompter, o as LOCAL_CONFIG_FILENAME, r as FunnelLocalConfigSync, s as channelSpecSchema, t as funnelJsonSchema, u as profileSpecSchema } from "./local-config-json-schema-8IHjS4Q7.js";
2
- import { n as FunnelLocalConfigWriter, t as MemoryFunnelTokenPrompter } from "./memory-token-prompter-CLerGsgM.js";
1
+ import { a as FunnelLocalConfig, c as connectorSpecSchema, i as FunnelTokenPrompter, l as localConfigSchema, n as NodeFunnelTokenPrompter, o as LOCAL_CONFIG_FILENAME, r as FunnelLocalConfigSync, s as channelSpecSchema, t as funnelJsonSchema, u as profileSpecSchema } from "./local-config-json-schema-DE1zkMcb.js";
2
+ import { n as FunnelLocalConfigWriter, t as MemoryFunnelTokenPrompter } from "./memory-token-prompter-vBXxY20-.js";
3
3
  export { FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, MemoryFunnelTokenPrompter, NodeFunnelTokenPrompter, channelSpecSchema, connectorSpecSchema, funnelJsonSchema, localConfigSchema, profileSpecSchema };
@@ -1,13 +1,14 @@
1
- import { n as NodeFunnelProcessRunner } from "./gh-connector-schema-o3Q1-ojL.js";
1
+ import { n as NodeFunnelProcessRunner } from "./gh-connector-schema-DUcZgN2Q.js";
2
2
  import { t as NodeFunnelFileSystem } from "./node-file-system-BcrmWN9I.js";
3
- import { n as FUNNEL_DIR, o as resolveFunnelPort } from "./settings-store-D2XSXTyt.js";
3
+ import { r as FUNNEL_DIR, s as resolveFunnelPort, t as gatewayLoopbackUrl } from "./gateway-base-url-6foMXfFf.js";
4
+ import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-CzYgZpq2.js";
4
5
  import { dirname, join } from "node:path";
5
6
  import { chmodSync, existsSync, mkdirSync } from "node:fs";
6
7
  import { z } from "zod";
7
8
  import { homedir, tmpdir } from "node:os";
9
+ import { Database } from "bun:sqlite";
8
10
  import { timingSafeEqual } from "node:crypto";
9
11
  import { createFactory } from "hono/factory";
10
- import { Database } from "bun:sqlite";
11
12
  import { HTTPException } from "hono/http-exception";
12
13
  import { zValidator } from "@hono/zod-validator";
13
14
  //#region lib/engine/settings/tmp-dir.ts
@@ -65,7 +66,7 @@ var FunnelChannelPublisher = class {
65
66
  async publish(channelName, request) {
66
67
  if (!this.isDaemonRunning()) return OFFLINE;
67
68
  try {
68
- const url = `http://127.0.0.1:${this.port}/channels/${encodeURIComponent(channelName)}/publish`;
69
+ const url = `${gatewayLoopbackUrl(this.port)}/channels/${encodeURIComponent(channelName)}/publish`;
69
70
  const res = await fetch(url, {
70
71
  method: "POST",
71
72
  headers: {
@@ -328,7 +329,7 @@ var FunnelBroadcaster = class {
328
329
  }
329
330
  };
330
331
  //#endregion
331
- //#region lib/gateway/funnel-event-log.ts
332
+ //#region lib/gateway/event-log/event-log.ts
332
333
  /**
333
334
  * Replayable event payload persisted by the gateway. Domain events the
334
335
  * broadcaster emits to WS clients land here so reconnects across daemon
@@ -647,7 +648,7 @@ function toRecord(row) {
647
648
  };
648
649
  }
649
650
  //#endregion
650
- //#region lib/gateway/sqlite-funnel-event-log.ts
651
+ //#region lib/gateway/event-log/sqlite-event-log.ts
651
652
  const MAX_CONTENT_CHARS = 2e3;
652
653
  /**
653
654
  * SQLite-backed `FunnelEventLog`. One indexed table holds every broadcaster
@@ -669,9 +670,11 @@ const MAX_CONTENT_CHARS = 2e3;
669
670
  var SqliteFunnelEventLog = class extends FunnelEventLog {
670
671
  sink;
671
672
  now;
673
+ logger;
672
674
  constructor(props) {
673
675
  super();
674
676
  this.now = props.now ?? (() => Date.now());
677
+ this.logger = props.logger;
675
678
  this.sink = new LeucoLoggerSqliteSink({
676
679
  path: props.path,
677
680
  indexes: ["channel_id", "connector_id"],
@@ -699,11 +702,15 @@ var SqliteFunnelEventLog = class extends FunnelEventLog {
699
702
  connector_id: record.connectorId,
700
703
  meta: record.meta
701
704
  };
702
- this.sink.write({
705
+ const result = this.sink.write({
703
706
  seq: record.offset,
704
707
  ts: this.now(),
705
708
  event
706
709
  });
710
+ if (result instanceof Error) this.logger?.error("event log write failed", {
711
+ offset: record.offset,
712
+ error: result.message
713
+ });
707
714
  }
708
715
  /**
709
716
  * Returns events with offset > since. Filtering by channel/connector is
@@ -1040,10 +1047,18 @@ const zParam = (schema) => zValidator("param", schema, (result, c) => {
1040
1047
  });
1041
1048
  //#endregion
1042
1049
  //#region lib/gateway/routes/channels.connectors.call.ts
1050
+ const jsonValueSchema = z.lazy(() => z.union([
1051
+ z.string(),
1052
+ z.number(),
1053
+ z.boolean(),
1054
+ z.null(),
1055
+ z.array(jsonValueSchema),
1056
+ z.record(z.string(), jsonValueSchema)
1057
+ ]));
1043
1058
  const bodySchema = z.object({
1044
1059
  method: z.string().min(1),
1045
1060
  path: z.string().min(1),
1046
- body: z.unknown().optional()
1061
+ body: jsonValueSchema.optional()
1047
1062
  });
1048
1063
  /**
1049
1064
  * POST /channels/:channel/connectors/:connector/call
@@ -1064,7 +1079,7 @@ const channelsConnectorsCallHandler = factory.createHandlers(zParam(z.object({
1064
1079
  const result = await c.var.deps.channels.call(param.channel, param.connector, {
1065
1080
  method: parsed.data.method,
1066
1081
  path: parsed.data.path,
1067
- body: parsed.data.body ?? {}
1082
+ body: parsed.data.body
1068
1083
  });
1069
1084
  return c.json({
1070
1085
  ok: true,
@@ -1111,87 +1126,6 @@ const channelsPublishHandler = factory.createHandlers(zParam(z.object({ channel:
1111
1126
  return c.json(response);
1112
1127
  });
1113
1128
  //#endregion
1114
- //#region lib/gateway/connector-diagnostic-sql-reader.ts
1115
- /**
1116
- * Read-only SQL surface over the three diagnostic tables, for Claude to query
1117
- * the log with arbitrary `SELECT`s. It opens all files read-only and exposes
1118
- * three views — `raw`, `processed`, `connection` — that hide the storage
1119
- * details (the physical table is `leuco_log` and each row's columns live
1120
- * inside a JSON `event` blob): the views surface the columns as plain fields,
1121
- * with `payload` already pulled out of the nested JSON.
1122
- *
1123
- * The tables are separate files. `raw` and `processed` share an `event_id`,
1124
- * so a `JOIN` answers "the event arrived, but what verdict did it get?";
1125
- * `connection` answers the other half — "did the listener ever connect at
1126
- * all?". Writes are impossible: the connection is read-only and `query`
1127
- * rejects anything but a single `SELECT`.
1128
- */
1129
- var ConnectorDiagnosticSqlReader = class {
1130
- db;
1131
- constructor(props) {
1132
- const db = new Database(props.rawPath, { readonly: true });
1133
- try {
1134
- db.run("PRAGMA busy_timeout = 500");
1135
- db.prepare("ATTACH DATABASE ? AS processeddb").run(props.processedPath);
1136
- db.prepare("ATTACH DATABASE ? AS connectiondb").run(props.connectionPath);
1137
- db.run(rawViewSql);
1138
- db.run(processedViewSql);
1139
- db.run(connectionViewSql);
1140
- } catch (error) {
1141
- db.close();
1142
- throw error;
1143
- }
1144
- this.db = db;
1145
- Object.freeze(this);
1146
- }
1147
- /**
1148
- * Run one read-only `SELECT` and return the rows. Returns an `Error` (rather
1149
- * than throwing) for a non-SELECT statement or a SQL error, so the caller
1150
- * can surface the message without a stack trace.
1151
- */
1152
- query(sql, params = []) {
1153
- const trimmed = sql.trim().replace(/;$/, "").trim();
1154
- if (!/^select\b/i.test(trimmed)) return /* @__PURE__ */ new Error("only a single SELECT statement is allowed");
1155
- if (trimmed.includes(";")) return /* @__PURE__ */ new Error("only a single statement is allowed (remove the ';')");
1156
- try {
1157
- return this.db.prepare(trimmed).all(...params);
1158
- } catch (error) {
1159
- return error instanceof Error ? error : new Error(String(error));
1160
- }
1161
- }
1162
- close() {
1163
- this.db.close();
1164
- }
1165
- };
1166
- const rawViewSql = `CREATE TEMP VIEW raw AS SELECT
1167
- seq,
1168
- ts,
1169
- json_extract(event, '$.event_id') AS event_id,
1170
- json_extract(event, '$.type') AS type,
1171
- json_extract(event, '$.connector_id') AS connector_id,
1172
- json_extract(event, '$.channel_id') AS channel_id,
1173
- json_extract(event, '$.payload') AS payload
1174
- FROM main.leuco_log`;
1175
- const processedViewSql = `CREATE TEMP VIEW processed AS SELECT
1176
- seq,
1177
- ts,
1178
- json_extract(event, '$.event_id') AS event_id,
1179
- json_extract(event, '$.type') AS type,
1180
- json_extract(event, '$.connector_id') AS connector_id,
1181
- json_extract(event, '$.channel_id') AS channel_id,
1182
- json_extract(event, '$.outcome') AS outcome,
1183
- json_extract(event, '$.payload') AS payload
1184
- FROM processeddb.leuco_log`;
1185
- const connectionViewSql = `CREATE TEMP VIEW connection AS SELECT
1186
- seq,
1187
- ts,
1188
- json_extract(event, '$.type') AS type,
1189
- json_extract(event, '$.connector_id') AS connector_id,
1190
- json_extract(event, '$.channel_id') AS channel_id,
1191
- json_extract(event, '$.status') AS status,
1192
- json_extract(event, '$.detail') AS detail
1193
- FROM connectiondb.leuco_log`;
1194
- //#endregion
1195
1129
  //#region lib/gateway/routes/debug.ts
1196
1130
  const extractPreview = (payload) => {
1197
1131
  if (typeof payload !== "string" || payload.length === 0) return null;
@@ -1412,7 +1346,11 @@ const statusHandler = factory.createHandlers((c) => {
1412
1346
  //#endregion
1413
1347
  //#region lib/gateway/routes/index.ts
1414
1348
  function buildGatewayRoutes() {
1415
- return factory.createApp().get("/health", ...healthHandler).get("/status", ...statusHandler).get("/debug", ...debugHandler).get("/listeners", ...listenersListHandler).post("/listeners/:channel/:connector/start", ...listenersStartHandler).delete("/listeners/:channel/:connector", ...listenersStopHandler).post("/listeners/:channel/:connector/restart", ...listenersRestartHandler).post("/channels/:channel/connectors/:connector/call", ...channelsConnectorsCallHandler).post("/channels/:channel/publish", ...channelsPublishHandler);
1349
+ return factory.createApp().onError((error, c) => {
1350
+ if (error instanceof HTTPException) return error.getResponse();
1351
+ const message = error instanceof Error ? error.message : String(error);
1352
+ return c.json({ error: message }, 500);
1353
+ }).get("/health", ...healthHandler).get("/status", ...statusHandler).get("/debug", ...debugHandler).get("/listeners", ...listenersListHandler).post("/listeners/:channel/:connector/start", ...listenersStartHandler).delete("/listeners/:channel/:connector", ...listenersStopHandler).post("/listeners/:channel/:connector/restart", ...listenersRestartHandler).post("/channels/:channel/connectors/:connector/call", ...channelsConnectorsCallHandler).post("/channels/:channel/publish", ...channelsPublishHandler);
1416
1354
  }
1417
1355
  const gatewayRoutes = buildGatewayRoutes();
1418
1356
  //#endregion
@@ -1448,6 +1386,7 @@ var FunnelGatewayServer = class {
1448
1386
  dir;
1449
1387
  killCompetingSlack;
1450
1388
  token;
1389
+ allowInsecureHost;
1451
1390
  broadcaster;
1452
1391
  eventLog;
1453
1392
  supervisor;
@@ -1467,6 +1406,7 @@ var FunnelGatewayServer = class {
1467
1406
  this.dir = deps.dir ?? FUNNEL_DIR;
1468
1407
  this.killCompetingSlack = deps.killCompetingSlack ?? true;
1469
1408
  this.token = deps.token ?? "";
1409
+ this.allowInsecureHost = deps.allowInsecureHost ?? false;
1470
1410
  this.extraRoutes = deps.extraRoutes ?? null;
1471
1411
  const clock = deps.clock;
1472
1412
  this.nowMs = clock ? () => clock.millis() : () => Date.now();
@@ -1476,7 +1416,8 @@ var FunnelGatewayServer = class {
1476
1416
  if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
1477
1417
  this.eventLog = new SqliteFunnelEventLog({
1478
1418
  path: this.dbPath,
1479
- now: this.nowMs
1419
+ now: this.nowMs,
1420
+ logger: this.logger
1480
1421
  });
1481
1422
  }
1482
1423
  this.broadcaster = new FunnelBroadcaster({
@@ -1503,7 +1444,7 @@ var FunnelGatewayServer = class {
1503
1444
  }
1504
1445
  async start() {
1505
1446
  if (this.server) return this.server;
1506
- if (!this.token && !LOOPBACK_HOSTS.has(this.hostname)) this.logger?.warn("gateway auth is disabled on a non-loopback bind every endpoint is reachable without a token", { hostname: this.hostname });
1447
+ if (!this.token && !LOOPBACK_HOSTS.has(this.hostname) && !this.allowInsecureHost) throw new Error(`refusing to start gateway: hostname "${this.hostname}" is reachable off-box but no token is set. Set a token, bind to loopback (127.0.0.1), or pass allowInsecureHost: true.`);
1507
1448
  const app = this.buildApp();
1508
1449
  this.startedAt = this.nowMs();
1509
1450
  this.server = Bun.serve({
@@ -1714,7 +1655,7 @@ var FunnelGatewayServer = class {
1714
1655
  return { offset: event.offset };
1715
1656
  }
1716
1657
  lookupChannelId(channelName) {
1717
- return this.channels.get(channelName)?.id ?? null;
1658
+ return this.channels.get(channelName)?.id ?? this.channels.getById(channelName)?.id ?? null;
1718
1659
  }
1719
1660
  lookupConnectorId(channelId, connectorName) {
1720
1661
  return this.channels.getById(channelId)?.connectors.find((c) => c.name === connectorName)?.id ?? null;
@@ -1775,7 +1716,28 @@ var FunnelGatewayToken = class {
1775
1716
  };
1776
1717
  const DEFAULT_GATEWAY_TOKEN_PATH = join(homedir(), ".funnel", TOKEN_FILE_NAME);
1777
1718
  //#endregion
1778
- //#region lib/gateway/memory-funnel-event-log.ts
1719
+ //#region lib/gateway/channel-ws-url.ts
1720
+ function channelWsUrl(input) {
1721
+ const url = new URL(input.base);
1722
+ url.searchParams.set("channel", input.channel);
1723
+ if (input.subscriberId !== void 0) url.searchParams.set("id", input.subscriberId);
1724
+ if (input.since !== void 0) url.searchParams.set("since", String(input.since));
1725
+ return url.toString();
1726
+ }
1727
+ /**
1728
+ * Builds the `Sec-WebSocket-Protocol` values that authenticate a gateway WS
1729
+ * upgrade. Browser `WebSocket` cannot set an `Authorization` header, so the
1730
+ * gateway also accepts the token as a `funnel.token.<token>` subprotocol.
1731
+ * Returns an empty array when no token is given (auth disabled / loopback).
1732
+ *
1733
+ * Usage: `new WebSocket(channelWsUrl({ base, channel }), channelWsProtocols(token))`
1734
+ */
1735
+ function channelWsProtocols(token) {
1736
+ if (!token) return [];
1737
+ return [`funnel.token.${token}`];
1738
+ }
1739
+ //#endregion
1740
+ //#region lib/gateway/event-log/memory-event-log.ts
1779
1741
  /**
1780
1742
  * In-process `FunnelEventLog` backed by a plain array. Used by tests and by
1781
1743
  * embedders that do not need durability — replay works within the process
@@ -1818,7 +1780,7 @@ var MemoryFunnelEventLog = class extends FunnelEventLog {
1818
1780
  close() {}
1819
1781
  };
1820
1782
  //#endregion
1821
- //#region lib/gateway/connector-diagnostic-log.ts
1783
+ //#region lib/gateway/diagnostic-log/diagnostic-log.ts
1822
1784
  /**
1823
1785
  * Points in the listener's connection lifecycle. The single source of truth
1824
1786
  * for the value set: the `status` column schema, the `ConnectorConnectionStatus`
@@ -1909,7 +1871,7 @@ const connectorConnectionEventSchema = z.object({
1909
1871
  */
1910
1872
  var ConnectorDiagnosticLog = class {};
1911
1873
  //#endregion
1912
- //#region lib/gateway/sqlite-connector-diagnostic-log.ts
1874
+ //#region lib/gateway/diagnostic-log/sqlite-diagnostic-log.ts
1913
1875
  /**
1914
1876
  * Cap on a raw payload kept verbatim. The point of the raw table is to see
1915
1877
  * what Slack/Discord actually sent, and a typical event is a few KB — so 256
@@ -2165,7 +2127,7 @@ const headFields = (payload) => {
2165
2127
  }
2166
2128
  };
2167
2129
  //#endregion
2168
- //#region lib/gateway/memory-connector-diagnostic-log.ts
2130
+ //#region lib/gateway/diagnostic-log/memory-diagnostic-log.ts
2169
2131
  /**
2170
2132
  * In-process `ConnectorDiagnosticLog` backed by one array per table. Used by tests
2171
2133
  * and embedders that do not need durability. Like the SQLite log it keeps
@@ -2242,4 +2204,4 @@ const takeRecent = (events, limit) => {
2242
2204
  return events.slice(-limit);
2243
2205
  };
2244
2206
  //#endregion
2245
- export { FunnelBroadcaster as _, connectorConnectionEventSchema as a, publishResponseSchema as b, MemoryFunnelEventLog as c, FunnelGatewayServer as d, ConnectorDiagnosticSqlReader as f, funnelEventSchema as g, FunnelEventLog as h, ConnectorDiagnosticLog as i, DEFAULT_GATEWAY_TOKEN_PATH as l, SqliteFunnelEventLog as m, SqliteConnectorDiagnosticLog as n, connectorProcessedEventSchema as o, FunnelListenerSupervisor as p, CONNECTOR_CONNECTION_STATUSES as r, connectorRawEventSchema as s, MemoryConnectorDiagnosticLog as t, FunnelGatewayToken as u, FunnelChannelPublisher as v, funnelTmpDir as x, publishRequestSchema as y };
2207
+ export { funnelTmpDir as C, publishResponseSchema as S, funnelEventSchema as _, connectorConnectionEventSchema as a, FunnelChannelPublisher as b, MemoryFunnelEventLog as c, DEFAULT_GATEWAY_TOKEN_PATH as d, FunnelGatewayToken as f, FunnelEventLog as g, SqliteFunnelEventLog as h, ConnectorDiagnosticLog as i, channelWsProtocols as l, FunnelListenerSupervisor as m, SqliteConnectorDiagnosticLog as n, connectorProcessedEventSchema as o, FunnelGatewayServer as p, CONNECTOR_CONNECTION_STATUSES as r, connectorRawEventSchema as s, MemoryConnectorDiagnosticLog as t, channelWsUrl as u, FunnelBroadcaster as v, publishRequestSchema as x, requireBearerToken as y };
@@ -1,7 +1,7 @@
1
- import { n as FunnelFileSystem } from "./file-system-BeOKXjlV.js";
2
- import { i as FunnelTokenPrompter } from "./local-config-sync-BdsrDZOu.js";
1
+ import { n as FunnelFileSystem } from "./file-system-Wub9Nto4.js";
2
+ import { i as FunnelTokenPrompter } from "./local-config-sync-B8b04LrZ.js";
3
3
 
4
- //#region lib/engine/local-config/local-config-json-schema.d.ts
4
+ //#region lib/services/local-config/local-config-json-schema.d.ts
5
5
  /**
6
6
  * Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
7
7
  * `$schema` references in committed `funnel.json` files so editors can give
@@ -10,7 +10,7 @@ import { i as FunnelTokenPrompter } from "./local-config-sync-BdsrDZOu.js";
10
10
  */
11
11
  declare const funnelJsonSchema: () => Record<string, unknown>;
12
12
  //#endregion
13
- //#region lib/engine/local-config/local-config-writer.d.ts
13
+ //#region lib/services/local-config/local-config-writer.d.ts
14
14
  type Deps = {
15
15
  fs: FunnelFileSystem;
16
16
  };
@@ -1,6 +1,6 @@
1
- import { i as FunnelTokenPrompter, o as LOCAL_CONFIG_FILENAME } from "./local-config-json-schema-8IHjS4Q7.js";
1
+ import { i as FunnelTokenPrompter, o as LOCAL_CONFIG_FILENAME } from "./local-config-json-schema-DE1zkMcb.js";
2
2
  import { join } from "node:path";
3
- //#region lib/engine/local-config/local-config-writer.ts
3
+ //#region lib/services/local-config/local-config-writer.ts
4
4
  const isRecord = (value) => {
5
5
  return typeof value === "object" && value !== null && !Array.isArray(value);
6
6
  };
@@ -1,5 +1,6 @@
1
- import { a as ProfileConfig, n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-BSU6JyvM.js";
2
- import { n as FunnelFileSystem } from "./file-system-BeOKXjlV.js";
1
+ import { r as ProfileConfig } from "./settings-schema-zhnMIa8I.js";
2
+ import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-CBrgz01o.js";
3
+ import { n as FunnelFileSystem } from "./file-system-Wub9Nto4.js";
3
4
 
4
5
  //#region lib/engine/profiles/profiles.d.ts
5
6
  type Deps = {
@@ -1,2 +1,2 @@
1
- import { t as FunnelProfiles } from "./profiles-f0mNmEyP.js";
1
+ import { t as FunnelProfiles } from "./profiles-EHTeCOqB.js";
2
2
  export { FunnelProfiles };
package/dist/profiles.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as FunnelProfiles } from "./profiles-wMRnjSid.js";
1
+ import { t as FunnelProfiles } from "./profiles-MnXvYfZF.js";
2
2
  export { FunnelProfiles };
@@ -0,0 +1,2 @@
1
+ import { a as RecoveryListenerControl, i as RecoveryGatewayControl, n as RecoveryAction, o as RecoveryResult, r as RecoveryChannelSource, t as FunnelRecovery } from "./funnel-recovery-BUBsu7WX.js";
2
+ export { FunnelRecovery, RecoveryAction, RecoveryChannelSource, RecoveryGatewayControl, RecoveryListenerControl, RecoveryResult };
@@ -0,0 +1,2 @@
1
+ import { t as FunnelRecovery } from "./funnel-recovery-D9CxD5Zs.js";
2
+ export { FunnelRecovery };
@@ -1,4 +1,4 @@
1
- //#region lib/connectors/resolve-connector-token.ts
1
+ //#region lib/engine/connectors/resolve-connector-token.ts
2
2
  /**
3
3
  * Resolves a connector token from either a literal value or the name of an env
4
4
  * var. A connector config carries one or the other per slot (see
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- //#region lib/connectors/schedule-connector-schema.ts
2
+ //#region lib/engine/connectors/schedule-connector-schema.ts
3
3
  /**
4
4
  * Catch-up behavior when the daemon was down past one or more matching minutes.
5
5
  *