@interactive-inc/claude-funnel 0.59.1 → 0.60.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 (85) hide show
  1. package/README.md +9 -3
  2. package/dist/bin.js +549 -487
  3. package/dist/channels-2g_BU1N0.d.ts +174 -0
  4. package/dist/claude.d.ts +9 -5
  5. package/dist/claude.js +54 -17
  6. package/dist/{diagnostic-log-Cb3v8P7p.d.ts → connector-descriptor-6SXJoszo.d.ts} +158 -2
  7. package/dist/connectors/discord.d.ts +30 -4
  8. package/dist/connectors/discord.js +2 -2
  9. package/dist/connectors/gh.d.ts +21 -5
  10. package/dist/connectors/gh.js +3 -3
  11. package/dist/connectors/schedule.d.ts +124 -2
  12. package/dist/connectors/schedule.js +3 -3
  13. package/dist/connectors/slack.d.ts +149 -5
  14. package/dist/connectors/slack.js +2 -2
  15. package/dist/{diagnostic-sql-reader-CzYgZpq2.js → diagnostic-sql-reader-C9zR-Csp.js} +5 -5
  16. package/dist/diagnostics.d.ts +1 -1
  17. package/dist/diagnostics.js +1 -1
  18. package/dist/{discord-listener-CKsZGTnH.js → discord-connector-BL36yvbL.js} +60 -37
  19. package/dist/docs.d.ts +1 -1
  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/error-message-of-Byi4y0Uf.js +9 -0
  24. package/dist/{file-process-guard-JhFpmHYo.d.ts → file-process-guard-DOlCr4GF.d.ts} +4 -5
  25. package/dist/{funnel-diagnostics-BpKYrMSu.js → funnel-diagnostics-CSiJmPlZ.js} +19 -2
  26. package/dist/{funnel-diagnostics-K-wON25Y.d.ts → funnel-diagnostics-DpXOsCty.d.ts} +3 -3
  27. package/dist/{funnel-docs-ng5K8w4j.js → funnel-docs-BxXZ9Ksx.js} +76 -3
  28. package/dist/{funnel-docs-DYBs1-H_.d.ts → funnel-docs-CNklHvbt.d.ts} +1 -1
  29. package/dist/{funnel-doctor-vxO96TCA.d.ts → funnel-doctor-CZf_0Luq.d.ts} +2 -2
  30. package/dist/{funnel-recovery-COExL9MD.d.ts → funnel-recovery-DnLrdWO9.d.ts} +1 -1
  31. package/dist/gateway/daemon.js +326 -266
  32. package/dist/gateway-base-url-Dy4Ykuoh.js +14 -0
  33. package/dist/gateway.d.ts +2 -2
  34. package/dist/gateway.js +2 -2
  35. package/dist/{gh-listener-B2I4s8qh.js → gh-connector-DpiixfQZ.js} +53 -5
  36. package/dist/gh-connector-schema-Rzwc1c1N.js +12 -0
  37. package/dist/http-client-oICicjuO.d.ts +18 -0
  38. package/dist/index-CgY8NdMz.d.ts +1057 -0
  39. package/dist/index.d.ts +1558 -17
  40. package/dist/index.js +374 -342
  41. package/dist/{local-config-json-schema-DE1zkMcb.js → local-config-json-schema-JyLqOQNX.js} +9 -5
  42. package/dist/local-config-sync-Dh1Croqe.d.ts +169 -0
  43. package/dist/local-config.d.ts +2 -2
  44. package/dist/local-config.js +2 -2
  45. package/dist/logger.js +1 -1
  46. package/dist/{memory-diagnostic-log-B9Us7X05.js → memory-diagnostic-log-CI60kNfB.js} +33 -18
  47. package/dist/{memory-token-prompter-CcShtF8B.d.ts → memory-token-prompter-B4sjyaAq.d.ts} +2 -2
  48. package/dist/{memory-token-prompter-C7vREzCL.js → memory-token-prompter-CZde7e6y.js} +1 -1
  49. package/dist/{node-file-system-BcrmWN9I.js → node-file-system-Blr8pAir.js} +1 -1
  50. package/dist/node-http-client-lowp60Oa.js +25 -0
  51. package/dist/{gh-connector-schema-ClPLSYD9.js → node-process-runner-DxTvycoK.js} +1 -12
  52. package/dist/{profiles-g2qGVOWv.d.ts → profiles-Cy5wXQ0L.d.ts} +3 -3
  53. package/dist/{profiles-MnXvYfZF.js → profiles-DSzTeKQw.js} +1 -1
  54. package/dist/profiles.d.ts +1 -1
  55. package/dist/profiles.js +1 -1
  56. package/dist/recovery.d.ts +1 -1
  57. package/dist/recovery.js +1 -1
  58. package/dist/{schedule-listener-DP9Jhc6U.js → schedule-connector-L4uzg5M8.js} +109 -9
  59. package/dist/{settings-reader-DPwqOVUm.d.ts → settings-reader-BIFB_j2f.d.ts} +1 -1
  60. package/dist/settings-schema-D1xcOqRu.d.ts +78 -0
  61. package/dist/{gateway-base-url-DxVjjDoW.js → settings-store-CUKSeTXC.js} +27 -29
  62. package/dist/{slack-listener-C4wlZaOq.js → slack-connector-DQIFPdBF.js} +67 -12
  63. package/dist/slot-fields-CMoRpwuy.js +45 -0
  64. package/dist/{yaml-render-cZu6CxkE.js → yaml-render-qW34NlYz.js} +4 -4
  65. package/package.json +1 -1
  66. package/dist/connector-adapter-DGacCppE.d.ts +0 -25
  67. package/dist/discord-connector-schema-CQyfDkLD.d.ts +0 -39
  68. package/dist/gh-connector-schema-CZzwzvqY.d.ts +0 -14
  69. package/dist/index-D7mjirUL.d.ts +0 -3602
  70. package/dist/local-config-sync-BGPAS9Be.d.ts +0 -401
  71. package/dist/process-runner-DIm1cy95.d.ts +0 -52
  72. package/dist/resolve-connector-token-CczqG_Ig.js +0 -22
  73. package/dist/schedule-listener-DoMPjHZj.d.ts +0 -112
  74. package/dist/settings-schema-1hh11jnN.d.ts +0 -152
  75. package/dist/slack-listener-Dj9NFbAJ.d.ts +0 -136
  76. /package/dist/{connector-adapter-qwXLjQId.js → connector-adapter-DU9Rvyec.js} +0 -0
  77. /package/dist/{connector-listener-CpHBecCj.js → connector-listener-DR3aKOuK.js} +0 -0
  78. /package/dist/{file-system-PWKKU7lA.js → file-system-Wvzc2ePY.js} +0 -0
  79. /package/dist/{file-system-DxpnnUVb.d.ts → file-system-o51IsM0W.d.ts} +0 -0
  80. /package/dist/{funnel-doctor-CApCezTq.js → funnel-doctor-DiJCjHsg.js} +0 -0
  81. /package/dist/{funnel-log-sqlite-sink-B_5_4ybn.js → funnel-log-sqlite-sink-kqJbx2H7.js} +0 -0
  82. /package/dist/{funnel-recovery-D9CxD5Zs.js → funnel-recovery-BFdPjL6Z.js} +0 -0
  83. /package/dist/{logger-BP6SisKt.js → logger-B6iyNbxM.js} +0 -0
  84. /package/dist/{schedule-connector-schema-B_xO5z5B.js → schedule-connector-schema-CfyuMCMh.js} +0 -0
  85. /package/dist/{settings-reader-DPqrpV7s.js → settings-reader-CtQ-Ix8_.js} +0 -0
package/dist/index.js CHANGED
@@ -1,27 +1,23 @@
1
- import { t as FunnelConnectorAdapter } from "./connector-adapter-qwXLjQId.js";
2
- import { a as FunnelHttpClient, i as NodeFunnelHttpClient, r as FunnelDiscordAdapter, t as FunnelDiscordListener } from "./discord-listener-CKsZGTnH.js";
3
- import { t as FunnelConnectorListener } from "./connector-listener-CpHBecCj.js";
4
- import { t as FunnelLogger } from "./logger-BP6SisKt.js";
5
- import { n as NodeFunnelProcessRunner, r as FunnelProcessRunner, t as ghConnectorSchema } from "./gh-connector-schema-ClPLSYD9.js";
6
- import { n as FunnelGhAdapter, t as FunnelGhListener } from "./gh-listener-B2I4s8qh.js";
7
- import { n as ScheduleStateStore, t as FunnelScheduleListener } from "./schedule-listener-DP9Jhc6U.js";
8
- import { t as FunnelFileSystem } from "./file-system-PWKKU7lA.js";
9
- import { t as NodeFunnelFileSystem } from "./node-file-system-BcrmWN9I.js";
10
- import { n as FunnelSlackEventProcessor, r as FunnelSlackAdapter, t as FunnelSlackListener } from "./slack-listener-C4wlZaOq.js";
11
- import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-DPqrpV7s.js";
12
- import { a as SETTINGS_PATH, c as SETTINGS_VERSION, d as profileConfigSchema, f as settingsSchema, i as FunnelSettingsStore, l as channelConfigSchema, m as NodeFunnelIdGenerator, n as DEFAULT_GATEWAY_PORT, o as resolveFunnelDir, p as connectorConfigSchema, r as FUNNEL_DIR, s as resolveFunnelPort, t as gatewayLoopbackUrl, u as channelDeliveryModeSchema } from "./gateway-base-url-DxVjjDoW.js";
13
- import { t as discordConnectorSchema } from "./discord-connector-schema-B_N6IXLz.js";
14
- import { n as scheduleConnectorSchema, r as scheduleEntrySchema, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-B_xO5z5B.js";
15
- import { t as slackConnectorSchema } from "./slack-connector-schema-C1zEf4TG.js";
16
- import { a as FunnelMcp, o as FileProcessGuard, s as FunnelClaude, t as renderYaml } from "./yaml-render-cZu6CxkE.js";
17
- import { a as toDiagnosticEvent, i as toDiagnosticConnectionError, n as previewOf, r as queryRows, t as FunnelDiagnostics } from "./funnel-diagnostics-BpKYrMSu.js";
18
- import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-CzYgZpq2.js";
19
- import { t as FunnelDoctor } from "./funnel-doctor-CApCezTq.js";
20
- import { t as FunnelDocs } from "./funnel-docs-ng5K8w4j.js";
21
- import { a as FunnelLocalConfig, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigSync, t as funnelJsonSchema } from "./local-config-json-schema-DE1zkMcb.js";
22
- import { t as FunnelProfiles } from "./profiles-MnXvYfZF.js";
23
- import { t as FunnelRecovery } from "./funnel-recovery-D9CxD5Zs.js";
24
- 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-B9Us7X05.js";
1
+ import { t as gatewayLoopbackUrl } from "./gateway-base-url-Dy4Ykuoh.js";
2
+ import { t as FunnelFileSystem } from "./file-system-Wvzc2ePY.js";
3
+ import { t as NodeFunnelFileSystem } from "./node-file-system-Blr8pAir.js";
4
+ import { t as FunnelLogger } from "./logger-B6iyNbxM.js";
5
+ import { n as FunnelProcessRunner, t as NodeFunnelProcessRunner } from "./node-process-runner-DxTvycoK.js";
6
+ import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-CtQ-Ix8_.js";
7
+ import { a as resolveFunnelDir, c as channelConfigSchema, d as settingsSchema, f as baseConnectorConfigSchema, i as SETTINGS_PATH, l as channelDeliveryModeSchema, n as FUNNEL_DIR, o as resolveFunnelPort, p as NodeFunnelIdGenerator, r as FunnelSettingsStore, s as SETTINGS_VERSION, t as DEFAULT_GATEWAY_PORT, u as profileConfigSchema } from "./settings-store-CUKSeTXC.js";
8
+ import { a as FunnelMcp, o as FileProcessGuard, s as FunnelClaude, t as renderYaml } from "./yaml-render-qW34NlYz.js";
9
+ import { a as toDiagnosticEvent, i as toDiagnosticConnectionError, n as previewOf, r as queryRows, t as FunnelDiagnostics } from "./funnel-diagnostics-CSiJmPlZ.js";
10
+ import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-C9zR-Csp.js";
11
+ import { t as FunnelDoctor } from "./funnel-doctor-DiJCjHsg.js";
12
+ import { t as FunnelDocs } from "./funnel-docs-BxXZ9Ksx.js";
13
+ import { a as FunnelLocalConfig, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigSync, t as funnelJsonSchema } from "./local-config-json-schema-JyLqOQNX.js";
14
+ import { t as FunnelProfiles } from "./profiles-DSzTeKQw.js";
15
+ import { t as FunnelRecovery } from "./funnel-recovery-BFdPjL6Z.js";
16
+ import { C as funnelTmpDir, S as publishResponseSchema, _ as funnelEventSchema, a as connectorConnectionEventSchema, b as FunnelChannelPublisher, c as MemoryFunnelEventLog, d as DEFAULT_GATEWAY_TOKEN_PATH, f as FunnelGatewayToken, g as FunnelEventLog, h as SqliteFunnelEventLog, i as ConnectorDiagnosticLog, l as channelWsProtocols, m as FunnelListenerSupervisor, n as SqliteConnectorDiagnosticLog, o as connectorProcessedEventSchema, p as FunnelGatewayServer, r as CONNECTOR_CONNECTION_STATUSES, s as connectorRawEventSchema, t as MemoryConnectorDiagnosticLog, u as channelWsUrl, v as FunnelBroadcaster, x as publishRequestSchema, y as requireBearerToken } from "./memory-diagnostic-log-CI60kNfB.js";
17
+ import { n as FunnelHttpClient, t as NodeFunnelHttpClient } from "./node-http-client-lowp60Oa.js";
18
+ import { t as FunnelConnectorAdapter } from "./connector-adapter-DU9Rvyec.js";
19
+ import { t as FunnelConnectorListener } from "./connector-listener-DR3aKOuK.js";
20
+ import { r as scheduleEntrySchema, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-CfyuMCMh.js";
25
21
  import { dirname, join, resolve } from "node:path";
26
22
  import { hc } from "hono/client";
27
23
  import { appendFileSync, existsSync, mkdirSync } from "node:fs";
@@ -29,80 +25,68 @@ import { z } from "zod";
29
25
  import { fileURLToPath } from "node:url";
30
26
  import { createFactory } from "hono/factory";
31
27
  import { HTTPException } from "hono/http-exception";
32
- import { zValidator as zValidator$1 } from "@hono/zod-validator";
28
+ import { zValidator } from "@hono/zod-validator";
33
29
  import { Hono } from "hono";
34
- //#region lib/engine/connectors/connector-factory.ts
30
+ //#region lib/engine/connectors/connector-registry.ts
35
31
  const defaultFs$1 = new NodeFunnelFileSystem();
36
32
  const defaultProcess$1 = new NodeFunnelProcessRunner();
37
33
  /**
38
- * Pure factory for per-type listeners and adapters. The factory has no CRUD
39
- * responsibility connector configs live inside settings.json under their
40
- * channel, and FunnelChannels passes them in by value.
34
+ * Dispatches connector work to injected descriptors by `type`. Replaces the old
35
+ * hard-coded factory: core never imports a concrete connector, so listener and
36
+ * adapter code (and their SDKs) is bundled only when the host passes that type's
37
+ * descriptor to `new Funnel({ connectors: [...] })`.
41
38
  *
42
- * `dir` is the funnel home (defaults to ~/.funnel); per-connector state files
43
- * land at `<dir>/channels/<channel-id>/connectors/<connector-id>/state.json`.
44
- *
45
- * Host integrations can supply per-type listener hooks via
46
- * `slackListenerOptions` / `scheduleListenerOptions` — e.g. to attach a
47
- * Bolt `app.action` handler or to drop one-shot schedule entries on fire.
39
+ * `dir` is the funnel home; per-connector state files land at
40
+ * `<dir>/channels/<channel-id>/connectors/<connector-id>/`.
48
41
  */
49
- var FunnelConnectorFactory = class {
42
+ var FunnelConnectorRegistry = class {
43
+ descriptors;
50
44
  fs;
51
45
  process;
52
46
  logger;
53
47
  diagnosticLog;
54
48
  dir;
55
- slackListenerOptions;
56
- scheduleListenerOptions;
57
- constructor(deps = {}) {
49
+ constructor(deps) {
50
+ this.descriptors = new Map(deps.descriptors.map((descriptor) => [descriptor.type, descriptor]));
58
51
  this.fs = deps.fs ?? defaultFs$1;
59
52
  this.process = deps.process ?? defaultProcess$1;
60
53
  this.logger = deps.logger;
61
54
  this.diagnosticLog = deps.diagnosticLog;
62
55
  this.dir = deps.dir ?? FUNNEL_DIR;
63
- this.slackListenerOptions = deps.slackListenerOptions ?? {};
64
- this.scheduleListenerOptions = deps.scheduleListenerOptions ?? {};
65
56
  Object.freeze(this);
66
57
  }
58
+ has(type) {
59
+ return this.descriptors.has(type);
60
+ }
61
+ types() {
62
+ return [...this.descriptors.keys()];
63
+ }
67
64
  createListener(channelId, config) {
68
- if (config.type === "slack") return new FunnelSlackListener({
69
- config,
70
- channelId,
71
- logger: this.logger,
72
- diagnosticLog: this.diagnosticLog,
73
- onAppCreated: this.slackListenerOptions.onAppCreated,
74
- preprocessEvent: this.slackListenerOptions.preprocessEvent
75
- });
76
- if (config.type === "gh") return new FunnelGhListener({
77
- config,
78
- channelId,
79
- process: this.process,
80
- logger: this.logger,
81
- diagnosticLog: this.diagnosticLog
82
- });
83
- if (config.type === "discord") return new FunnelDiscordListener({
84
- config,
85
- channelId,
86
- logger: this.logger,
87
- diagnosticLog: this.diagnosticLog
88
- });
89
- return new FunnelScheduleListener({
90
- config,
91
- lastFiredStore: new ScheduleStateStore({
92
- path: join(this.connectorDir(channelId, config.id), "state.json"),
93
- fs: this.fs
94
- }),
95
- channelId,
96
- logger: this.logger,
97
- diagnosticLog: this.diagnosticLog,
98
- onFired: this.scheduleListenerOptions.onFired
99
- });
65
+ return this.require(config.type).createListener(config, this.listenerDeps(channelId));
100
66
  }
101
67
  createAdapter(config) {
102
- if (config.type === "slack") return new FunnelSlackAdapter({ config });
103
- if (config.type === "gh") return new FunnelGhAdapter({ process: this.process });
104
- if (config.type === "discord") return new FunnelDiscordAdapter({ config });
105
- return null;
68
+ const descriptor = this.require(config.type);
69
+ if (!descriptor.createAdapter) return null;
70
+ return descriptor.createAdapter(config, this.adapterDeps());
71
+ }
72
+ secretTokens(config) {
73
+ return this.require(config.type).secretTokens(config);
74
+ }
75
+ buildConfig(input, context) {
76
+ const type = typeof input.type === "string" ? input.type : "";
77
+ return this.require(type).buildConfig(input, context);
78
+ }
79
+ applyUpdate(config, fields, context) {
80
+ return this.require(config.type).applyUpdate(config, fields, context);
81
+ }
82
+ runOperation(config, name, args, context) {
83
+ const operation = this.require(config.type).operations[name];
84
+ if (!operation) throw new Error(`connector type "${config.type}" has no operation "${name}"`);
85
+ return operation({
86
+ config,
87
+ args,
88
+ context
89
+ });
106
90
  }
107
91
  connectorDir(channelId, connectorId) {
108
92
  return join(this.dir, "channels", channelId, "connectors", connectorId);
@@ -110,43 +94,29 @@ var FunnelConnectorFactory = class {
110
94
  channelDir(channelId) {
111
95
  return join(this.dir, "channels", channelId);
112
96
  }
113
- };
114
- //#endregion
115
- //#region lib/engine/channels/connector-tokens.ts
116
- /**
117
- * Return every literal secret token contained in a connector config. Used by
118
- * token collision detection at add/update time so the same Slack bot or
119
- * Discord bot cannot be registered under two connectors. Connectors that hold
120
- * an env *reference* instead of a literal contribute nothing here — two
121
- * connectors naming the same env var is not a secret collision, and the secret
122
- * is not in settings.json to compare anyway.
123
- */
124
- function connectorTokens(connector) {
125
- switch (connector.type) {
126
- case "slack": return [connector.botToken, connector.appToken].filter((token) => token !== void 0);
127
- case "discord": return [connector.botToken].filter((token) => token !== void 0);
128
- case "gh":
129
- case "schedule": return [];
97
+ require(type) {
98
+ const descriptor = this.descriptors.get(type);
99
+ if (!descriptor) throw new Error(`unknown connector type "${type}". Pass its descriptor to new Funnel({ connectors: [...] }).`);
100
+ return descriptor;
130
101
  }
131
- }
132
- //#endregion
133
- //#region lib/engine/channels/require-connector.ts
134
- function isConnectorOfType(connector, type) {
135
- return connector.type === type;
136
- }
137
- /**
138
- * Look up a connector by name and narrow its discriminated union to a single
139
- * variant via a type predicate. Throws if the connector is missing or has the
140
- * wrong `type`. Replaces per-type `requireXxxConnector` privates — adding a
141
- * new connector type only touches the `ConnectorConfig` union, not this
142
- * helper.
143
- */
144
- function requireConnectorOfType(channel, connectorName, type) {
145
- const connector = channel.connectors.find((c) => c.name === connectorName);
146
- if (!connector) throw new Error(`connector "${connectorName}" not found in channel "${channel.name}"`);
147
- if (!isConnectorOfType(connector, type)) throw new Error(`connector "${connectorName}" is type "${connector.type}", not "${type}"`);
148
- return connector;
149
- }
102
+ listenerDeps(channelId) {
103
+ return {
104
+ channelId,
105
+ fs: this.fs,
106
+ process: this.process,
107
+ logger: this.logger,
108
+ diagnosticLog: this.diagnosticLog,
109
+ connectorDir: (channel, connector) => this.connectorDir(channel, connector)
110
+ };
111
+ }
112
+ adapterDeps() {
113
+ return {
114
+ fs: this.fs,
115
+ process: this.process,
116
+ logger: this.logger
117
+ };
118
+ }
119
+ };
150
120
  //#endregion
151
121
  //#region lib/engine/time/clock.ts
152
122
  /**
@@ -170,26 +140,6 @@ var NodeFunnelClock = class extends FunnelClock {
170
140
  };
171
141
  //#endregion
172
142
  //#region lib/engine/channels/channels.ts
173
- /**
174
- * Resolves one token slot (e.g. botToken/botTokenEnv) for an update. The
175
- * literal and the env-ref form are mutually exclusive: if `fields` supplies
176
- * either, that form wins and the other key is omitted entirely; if it supplies
177
- * neither, the connector's current slot is carried over unchanged. Returns a
178
- * partial object spread into the rebuilt connector, so an omitted key is truly
179
- * absent rather than set to undefined.
180
- */
181
- const slotFields = (literalKey, envKey, fields, current) => {
182
- const literal = fields[literalKey];
183
- if (literal !== void 0) return { [literalKey]: literal };
184
- const envVar = fields[envKey];
185
- if (envVar !== void 0) return { [envKey]: envVar };
186
- const result = {};
187
- const currentLiteral = current[literalKey];
188
- const currentEnv = current[envKey];
189
- if (typeof currentLiteral === "string") result[literalKey] = currentLiteral;
190
- if (typeof currentEnv === "string") result[envKey] = currentEnv;
191
- return result;
192
- };
193
143
  const defaultClock$1 = new NodeFunnelClock();
194
144
  const defaultIdGenerator = new NodeFunnelIdGenerator();
195
145
  /**
@@ -199,16 +149,20 @@ const defaultIdGenerator = new NodeFunnelIdGenerator();
199
149
  * global connector namespace exists. Token uniqueness is enforced across all
200
150
  * channels at add/update time so the same Slack/Discord credentials cannot
201
151
  * be registered twice.
152
+ *
153
+ * Connector type knowledge lives entirely in the injected registry (descriptors):
154
+ * this class builds, updates, and runs operations on connectors generically and
155
+ * never imports a concrete connector type.
202
156
  */
203
157
  var FunnelChannels = class {
204
158
  store;
205
- factory;
159
+ registry;
206
160
  profileChecker;
207
161
  clock;
208
162
  idGenerator;
209
163
  constructor(deps) {
210
164
  this.store = deps.store;
211
- this.factory = deps.factory;
165
+ this.registry = deps.registry;
212
166
  this.profileChecker = deps.profileChecker ?? null;
213
167
  this.clock = deps.clock ?? defaultClock$1;
214
168
  this.idGenerator = deps.idGenerator ?? defaultIdGenerator;
@@ -280,57 +234,15 @@ var FunnelChannels = class {
280
234
  const settings = this.store.read();
281
235
  const channel = this.requireChannel(settings, channelName);
282
236
  if (channel.connectors.some((c) => c.name === input.name)) throw new Error(`connector "${input.name}" already exists in channel "${channelName}"`);
283
- const candidate = this.fromInput(input);
237
+ const candidate = this.registry.buildConfig(input, {
238
+ id: this.idGenerator.generate(),
239
+ now: this.clock.iso()
240
+ });
284
241
  this.assertNoTokenCollision(settings, candidate);
285
242
  channel.connectors.push(candidate);
286
243
  this.store.write(settings);
287
244
  return candidate;
288
245
  }
289
- fromInput(input) {
290
- const id = this.idGenerator.generate();
291
- const now = this.clock.iso();
292
- const createdAt = now;
293
- const updatedAt = now;
294
- switch (input.type) {
295
- case "slack": return {
296
- id,
297
- type: "slack",
298
- name: input.name,
299
- ...input.botToken !== void 0 ? { botToken: input.botToken } : {},
300
- ...input.appToken !== void 0 ? { appToken: input.appToken } : {},
301
- ...input.botTokenEnv !== void 0 ? { botTokenEnv: input.botTokenEnv } : {},
302
- ...input.appTokenEnv !== void 0 ? { appTokenEnv: input.appTokenEnv } : {},
303
- minify: input.minify ?? true,
304
- createdAt,
305
- updatedAt
306
- };
307
- case "gh": return {
308
- id,
309
- type: "gh",
310
- name: input.name,
311
- ...input.pollInterval !== void 0 ? { pollInterval: input.pollInterval } : {},
312
- createdAt,
313
- updatedAt
314
- };
315
- case "discord": return {
316
- id,
317
- type: "discord",
318
- name: input.name,
319
- ...input.botToken !== void 0 ? { botToken: input.botToken } : {},
320
- ...input.botTokenEnv !== void 0 ? { botTokenEnv: input.botTokenEnv } : {},
321
- createdAt,
322
- updatedAt
323
- };
324
- case "schedule": return {
325
- id,
326
- type: "schedule",
327
- name: input.name,
328
- entries: input.entries ?? [],
329
- createdAt,
330
- updatedAt
331
- };
332
- }
333
- }
334
246
  removeConnector(channelName, connectorName) {
335
247
  const settings = this.store.read();
336
248
  const channel = this.requireChannel(settings, channelName);
@@ -349,78 +261,57 @@ var FunnelChannels = class {
349
261
  connector.updatedAt = this.clock.iso();
350
262
  this.store.write(settings);
351
263
  }
352
- updateSlackConnector(channelName, connectorName, fields) {
264
+ /**
265
+ * Update a connector's mutable fields generically. The connector's descriptor
266
+ * rebuilds the config from `fields` (e.g. Slack/Discord token slots are rebuilt
267
+ * so a slot can move between a literal and an env reference cleanly).
268
+ */
269
+ updateConnector(channelName, connectorName, fields) {
353
270
  const settings = this.store.read();
354
271
  const channel = this.requireChannel(settings, channelName);
355
- const connector = requireConnectorOfType(channel, connectorName, "slack");
356
- const updated = {
357
- id: connector.id,
358
- name: connector.name,
359
- type: "slack",
360
- minify: connector.minify,
361
- createdAt: connector.createdAt,
362
- updatedAt: this.clock.iso(),
363
- ...slotFields("botToken", "botTokenEnv", fields, connector),
364
- ...slotFields("appToken", "appTokenEnv", fields, connector)
365
- };
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() });
366
275
  this.assertNoTokenCollision(settings, updated);
367
276
  this.replaceConnector(channel, connector.name, updated);
368
277
  this.store.write(settings);
369
278
  }
279
+ /** Back-compat wrapper for `updateConnector` on a slack connector. */
280
+ updateSlackConnector(channelName, connectorName, fields) {
281
+ this.updateConnector(channelName, connectorName, fields);
282
+ }
283
+ /** Back-compat wrapper for `updateConnector` on a gh connector. */
370
284
  updateGhConnector(channelName, connectorName, fields) {
371
- const settings = this.store.read();
372
- const connector = requireConnectorOfType(this.requireChannel(settings, channelName), connectorName, "gh");
373
- if (fields.pollInterval !== void 0) connector.pollInterval = fields.pollInterval;
374
- connector.updatedAt = this.clock.iso();
375
- this.store.write(settings);
285
+ this.updateConnector(channelName, connectorName, fields);
376
286
  }
287
+ /** Back-compat wrapper for `updateConnector` on a discord connector. */
377
288
  updateDiscordConnector(channelName, connectorName, fields) {
378
- const settings = this.store.read();
379
- const channel = this.requireChannel(settings, channelName);
380
- const connector = requireConnectorOfType(channel, connectorName, "discord");
381
- const updated = {
382
- id: connector.id,
383
- name: connector.name,
384
- type: "discord",
385
- createdAt: connector.createdAt,
386
- updatedAt: this.clock.iso(),
387
- ...slotFields("botToken", "botTokenEnv", fields, connector)
388
- };
389
- this.assertNoTokenCollision(settings, updated);
390
- this.replaceConnector(channel, connector.name, updated);
391
- this.store.write(settings);
392
- }
393
- listScheduleEntries(channelName, connectorName) {
394
- return requireConnectorOfType(this.requireChannel(this.store.read(), channelName), connectorName, "schedule").entries;
289
+ this.updateConnector(channelName, connectorName, fields);
395
290
  }
396
- addScheduleEntry(channelName, connectorName, entry) {
397
- const settings = this.store.read();
398
- const connector = requireConnectorOfType(this.requireChannel(settings, channelName), connectorName, "schedule");
399
- const persisted = {
400
- id: entry.id ?? this.idGenerator.generate(),
401
- cron: entry.cron,
402
- prompt: entry.prompt,
403
- enabled: entry.enabled ?? true,
404
- catchupPolicy: entry.catchupPolicy ?? "latest"
405
- };
406
- connector.entries.push(persisted);
407
- connector.updatedAt = this.clock.iso();
408
- this.store.write(settings);
409
- return persisted;
410
- }
411
- removeScheduleEntry(channelName, connectorName, id) {
291
+ /**
292
+ * Run a connector-type-specific operation (e.g. schedule `addEntry` /
293
+ * `removeEntry` / `listEntries`). The descriptor returns the next config and a
294
+ * result; the config is persisted only when the operation actually mutated it.
295
+ */
296
+ connectorOp(channelName, connectorName, operation, args) {
412
297
  const settings = this.store.read();
413
- const connector = requireConnectorOfType(this.requireChannel(settings, channelName), connectorName, "schedule");
414
- const index = connector.entries.findIndex((e) => e.id === id);
415
- if (index < 0) throw new Error(`schedule entry "${id}" not found`);
416
- connector.entries.splice(index, 1);
417
- connector.updatedAt = this.clock.iso();
418
- this.store.write(settings);
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()
304
+ });
305
+ if (outcome.config !== connector) {
306
+ this.replaceConnector(channel, connector.name, outcome.config);
307
+ this.store.write(settings);
308
+ }
309
+ return outcome.result;
419
310
  }
420
311
  async call(channelName, connectorName, input) {
421
312
  const connector = this.getConnector(channelName, connectorName);
422
313
  if (!connector) throw new Error(`connector "${connectorName}" not found in channel "${channelName}"`);
423
- const adapter = this.factory.createAdapter(connector);
314
+ const adapter = this.registry.createAdapter(connector);
424
315
  if (!adapter) throw new Error(`connector type "${connector.type}" does not support outbound calls`);
425
316
  return await adapter.call(input);
426
317
  }
@@ -432,7 +323,7 @@ var FunnelChannels = class {
432
323
  return {
433
324
  config: connector,
434
325
  channelId: channel.id,
435
- listener: this.factory.createListener(channel.id, connector)
326
+ listener: this.registry.createListener(channel.id, connector)
436
327
  };
437
328
  }
438
329
  createAllListeners() {
@@ -441,7 +332,7 @@ var FunnelChannels = class {
441
332
  config: connector,
442
333
  channelId: channel.id,
443
334
  channelName: channel.name,
444
- listener: this.factory.createListener(channel.id, connector)
335
+ listener: this.registry.createListener(channel.id, connector)
445
336
  });
446
337
  return out;
447
338
  }
@@ -456,11 +347,11 @@ var FunnelChannels = class {
456
347
  channel.connectors[index] = next;
457
348
  }
458
349
  assertNoTokenCollision(settings, candidate) {
459
- const tokens = connectorTokens(candidate);
350
+ const tokens = this.registry.secretTokens(candidate);
460
351
  if (tokens.length === 0) return;
461
352
  for (const channel of settings.channels) for (const other of channel.connectors) {
462
353
  if (other.id === candidate.id) continue;
463
- for (const token of connectorTokens(other)) if (tokens.includes(token)) throw new Error(`token already in use by connector "${other.name}" in channel "${channel.name}"`);
354
+ for (const token of this.registry.secretTokens(other)) if (tokens.includes(token)) throw new Error(`token already in use by connector "${other.name}" in channel "${channel.name}"`);
464
355
  }
465
356
  }
466
357
  };
@@ -1026,7 +917,9 @@ const noopOnError = () => {};
1026
917
  *
1027
918
  * @example
1028
919
  * ```ts
1029
- * const funnel = new Funnel({})
920
+ * import { slackConnector } from "@interactive-inc/claude-funnel/connectors/slack"
921
+ *
922
+ * const funnel = new Funnel({ connectors: [slackConnector()] })
1030
923
  * const channel = funnel.channels.add({ name: "inbox" })
1031
924
  * funnel.channels.addConnector("inbox", { type: "slack", name: "ops", botToken, appToken })
1032
925
  * await funnel.gatewayServer({ port: 9742 }).start()
@@ -1074,20 +967,25 @@ var Funnel = class Funnel {
1074
967
  fs,
1075
968
  idGenerator
1076
969
  });
1077
- const factory = new FunnelConnectorFactory({
970
+ const registry = new FunnelConnectorRegistry({
971
+ descriptors: props.connectors ?? [],
1078
972
  fs,
1079
973
  process,
1080
974
  logger: this.logger,
1081
975
  diagnosticLog: props.diagnosticLog,
1082
- dir,
1083
- slackListenerOptions: props.slackListenerOptions,
1084
- scheduleListenerOptions: props.scheduleListenerOptions
976
+ dir
977
+ });
978
+ this.profiles = new FunnelProfiles({
979
+ store,
980
+ idGenerator,
981
+ fs
1085
982
  });
1086
983
  this.channels = new FunnelChannels({
1087
984
  store,
1088
- factory,
985
+ registry,
1089
986
  clock,
1090
- idGenerator
987
+ idGenerator,
988
+ profileChecker: this.profiles
1091
989
  });
1092
990
  this.gateway = new FunnelGateway({
1093
991
  fs,
@@ -1112,11 +1010,6 @@ var Funnel = class Funnel {
1112
1010
  getToken: () => this.gatewayToken.read()
1113
1011
  });
1114
1012
  const mcp = new FunnelMcp({ fs });
1115
- this.profiles = new FunnelProfiles({
1116
- store,
1117
- idGenerator,
1118
- fs
1119
- });
1120
1013
  this.localConfig = new FunnelLocalConfig({ fs });
1121
1014
  this.localConfigSync = new FunnelLocalConfigSync({
1122
1015
  channels: this.channels,
@@ -1222,10 +1115,32 @@ var Funnel = class Funnel {
1222
1115
  }
1223
1116
  gatewayClient() {
1224
1117
  const { port } = this.gateway.getStatus();
1225
- return hc(`http://127.0.0.1:${port}`);
1118
+ return hc(gatewayLoopbackUrl(port));
1226
1119
  }
1227
1120
  };
1228
1121
  //#endregion
1122
+ //#region lib/engine/logger/redact-secrets.ts
1123
+ /**
1124
+ * Mask credential-shaped substrings before a log line is persisted. Matching
1125
+ * is prefix-anchored (Slack xoxb-/xapp-, GitHub ghp_/github_pat_, Discord
1126
+ * Bot tokens, HTTP bearer values) so ordinary identifiers never trip it —
1127
+ * a false negative only weakens defense in depth, a false positive destroys
1128
+ * a legitimate log line.
1129
+ */
1130
+ const SECRET_PATTERNS = [
1131
+ /xox[abprs]-[A-Za-z0-9-]{10,}/g,
1132
+ /xapp-[A-Za-z0-9-]{10,}/g,
1133
+ /gh[pousr]_[A-Za-z0-9]{20,}/g,
1134
+ /github_pat_[A-Za-z0-9_]{20,}/g,
1135
+ /Bot [A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{20,}/g,
1136
+ /Bearer [A-Za-z0-9._~+/-]{16,}/g
1137
+ ];
1138
+ const redactSecrets = (text) => {
1139
+ let redacted = text;
1140
+ for (const pattern of SECRET_PATTERNS) redacted = redacted.replace(pattern, "[redacted]");
1141
+ return redacted;
1142
+ };
1143
+ //#endregion
1229
1144
  //#region lib/engine/logger/node-logger.ts
1230
1145
  const defaultLogFile = () => join(funnelTmpDir(), "funnel.log");
1231
1146
  var NodeFunnelLogger = class extends FunnelLogger {
@@ -1254,7 +1169,7 @@ var NodeFunnelLogger = class extends FunnelLogger {
1254
1169
  message,
1255
1170
  ...meta ? { meta } : {}
1256
1171
  };
1257
- appendFileSync(this.file, `${JSON.stringify(entry)}\n`);
1172
+ appendFileSync(this.file, `${redactSecrets(JSON.stringify(entry))}\n`);
1258
1173
  }
1259
1174
  };
1260
1175
  //#endregion
@@ -1399,13 +1314,19 @@ const toRequest = (args) => {
1399
1314
  while (i < args.length) {
1400
1315
  const arg = args[i];
1401
1316
  if (arg.startsWith("--")) {
1402
- const key = arg.slice(2);
1317
+ const body = arg.slice(2);
1318
+ const equalsAt = body.indexOf("=");
1319
+ if (equalsAt >= 0) {
1320
+ params.set(body.slice(0, equalsAt), body.slice(equalsAt + 1));
1321
+ i++;
1322
+ continue;
1323
+ }
1403
1324
  const next = args[i + 1];
1404
1325
  if (isValue(next)) {
1405
- params.set(key, next);
1326
+ params.set(body, next);
1406
1327
  i += 2;
1407
1328
  } else {
1408
- params.set(key, "true");
1329
+ params.set(body, "true");
1409
1330
  i++;
1410
1331
  }
1411
1332
  continue;
@@ -1471,7 +1392,7 @@ const queryToCliArgs = (url, reservedKeys = []) => {
1471
1392
  };
1472
1393
  //#endregion
1473
1394
  //#region lib/cli/routes/channels.add.ts
1474
- const help$17 = `funnel channels add — add a channel
1395
+ const help$15 = `funnel channels add — add a channel
1475
1396
 
1476
1397
  usage: funnel channels add <name> [--delivery fanout|exclusive]
1477
1398
 
@@ -1489,7 +1410,29 @@ examples:
1489
1410
  funnel channels add ci-events --delivery exclusive
1490
1411
 
1491
1412
  see also: funnel channels, funnel channels <name> connectors add`;
1492
- const channelsAddHelpHandler = factory.createHandlers((c) => c.text(help$17));
1413
+ const channelsAddHelpHandler = factory.createHandlers((c) => c.text(help$15));
1414
+ //#endregion
1415
+ //#region lib/cli/router/validator.ts
1416
+ const labelFor = (target, key) => {
1417
+ if (target === "query") return `--${key}`;
1418
+ return `<${key}>`;
1419
+ };
1420
+ const formatIssues = (target, issues) => {
1421
+ return `invalid arguments — ${issues.map((issue) => {
1422
+ const key = issue.path.map(String).join(".");
1423
+ if (!key) return issue.message;
1424
+ return `${labelFor(target, key)}: ${issue.message}`;
1425
+ }).join("; ")} (run with --help for usage)`;
1426
+ };
1427
+ /**
1428
+ * CLI-flavored zValidator: every route imports this instead of the raw
1429
+ * @hono/zod-validator so a validation failure renders as one readable line
1430
+ * naming the offending flag, not a raw ZodError JSON dump.
1431
+ */
1432
+ const zValidator$1 = (target, schema) => zValidator(target, schema, (result) => {
1433
+ if (result.success) return;
1434
+ throw new HTTPException(400, { message: formatIssues(target, result.error.issues) });
1435
+ });
1493
1436
  //#endregion
1494
1437
  //#region lib/cli/routes/channels.add.$channel.ts
1495
1438
  const channelsAddHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({ delivery: channelDeliveryModeSchema.optional() })), (c) => {
@@ -1502,6 +1445,17 @@ const channelsAddHandler = factory.createHandlers(zValidator$1("param", z.object
1502
1445
  return c.text(`added channel "${created.name}" (id: ${created.id})`);
1503
1446
  });
1504
1447
  //#endregion
1448
+ //#region lib/cli/routes/not-found-message.ts
1449
+ /**
1450
+ * One error shape for every name-resolution miss: what was asked, what exists,
1451
+ * and the command that creates it — so a Claude (or human) can self-correct
1452
+ * without a follow-up listing call.
1453
+ */
1454
+ const notFoundMessage = (props) => {
1455
+ const listed = props.available.length > 0 ? props.available.join(", ") : "none";
1456
+ return `${props.kind} "${props.name}" not found (available: ${listed}); to create one: ${props.nextAction}`;
1457
+ };
1458
+ //#endregion
1505
1459
  //#region lib/cli/router/help-guard.ts
1506
1460
  function helpGuard(help) {
1507
1461
  return async (c, next) => {
@@ -1526,14 +1480,20 @@ subcommands:
1526
1480
  <c> schedules add <id> --cron=... --prompt=... / add a schedule entry
1527
1481
  <c> schedules remove <id> / remove a schedule entry`), (c) => {
1528
1482
  const param = c.req.valid("param");
1529
- const channel = c.env.funnel.channels.get(param.channel);
1530
- if (!channel) throw new HTTPException(404, { message: `channel "${param.channel}" not found` });
1483
+ const funnel = c.env.funnel;
1484
+ const channel = funnel.channels.get(param.channel);
1485
+ if (!channel) throw new HTTPException(404, { message: notFoundMessage({
1486
+ kind: "channel",
1487
+ name: param.channel,
1488
+ available: funnel.channels.list().map((ch) => ch.name),
1489
+ nextAction: "fnl channels add <name>"
1490
+ }) });
1531
1491
  if (channel.connectors.length === 0) return c.text(`no connectors in channel "${channel.name}"`);
1532
1492
  return c.text(channel.connectors.map((c) => `${c.name} (${c.type}, id: ${c.id})`).join("\n"));
1533
1493
  });
1534
1494
  //#endregion
1535
1495
  //#region lib/cli/routes/channels.$channel.connectors.add.ts
1536
- const help$16 = `funnel channels <channel> connectors add <name> — add a connector to a channel
1496
+ const help$14 = `funnel channels <channel> connectors add <name> — add a connector to a channel
1537
1497
 
1538
1498
  usage:
1539
1499
  funnel channels <channel> connectors add <name> --type=slack --bot-token=xoxb-... --app-token=xapp-...
@@ -1556,7 +1516,7 @@ examples:
1556
1516
  funnel channels alerts connectors add daily --type=schedule
1557
1517
 
1558
1518
  see also: funnel channels <channel> connectors, funnel channels <channel> connectors remove`;
1559
- const channelsConnectorsAddHelpHandler = factory.createHandlers((c) => c.text(help$16));
1519
+ const channelsConnectorsAddHelpHandler = factory.createHandlers((c) => c.text(help$14));
1560
1520
  //#endregion
1561
1521
  //#region lib/cli/routes/channels.$channel.connectors.add.$connector.ts
1562
1522
  const slackBody = z.object({
@@ -1624,7 +1584,7 @@ const channelsConnectorsAddHandler = factory.createHandlers(zValidator$1("param"
1624
1584
  });
1625
1585
  //#endregion
1626
1586
  //#region lib/cli/routes/channels.$channel.connectors.remove.ts
1627
- const help$15 = `funnel channels <channel> connectors remove <connector> — remove a connector
1587
+ const help$13 = `funnel channels <channel> connectors remove <connector> — remove a connector
1628
1588
 
1629
1589
  usage: funnel channels <channel> connectors remove <connector>
1630
1590
 
@@ -1636,7 +1596,7 @@ examples:
1636
1596
  funnel channels production connectors remove slack-main
1637
1597
 
1638
1598
  see also: funnel channels <channel> connectors, funnel channels <channel> connectors add`;
1639
- const channelsConnectorsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$15));
1599
+ const channelsConnectorsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$13));
1640
1600
  //#endregion
1641
1601
  //#region lib/cli/routes/channels.$channel.connectors.remove.$connector.ts
1642
1602
  const channelsConnectorsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1651,13 +1611,13 @@ const channelsConnectorsRemoveHandler = factory.createHandlers(zValidator$1("par
1651
1611
  });
1652
1612
  //#endregion
1653
1613
  //#region lib/cli/routes/channels.$channel.connectors.set.ts
1654
- const help$14 = `funnel channels <channel> connectors set <connector> — update connector fields
1614
+ const help$12 = `funnel channels <channel> connectors set <connector> — update connector fields
1655
1615
 
1656
1616
  usage:
1657
1617
  funnel channels <ch> connectors set <conn> [--bot-token=...] [--app-token=...] # slack
1658
1618
  funnel channels <ch> connectors set <conn> [--bot-token=...] # discord
1659
1619
  funnel channels <ch> connectors set <conn> [--poll-interval=N] # gh`;
1660
- const channelsConnectorsSetHelpHandler = factory.createHandlers((c) => c.text(help$14));
1620
+ const channelsConnectorsSetHelpHandler = factory.createHandlers((c) => c.text(help$12));
1661
1621
  //#endregion
1662
1622
  //#region lib/cli/routes/channels.$channel.connectors.set.$connector.ts
1663
1623
  const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1672,7 +1632,12 @@ const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param"
1672
1632
  const query = c.req.valid("query");
1673
1633
  const funnel = c.env.funnel;
1674
1634
  const existing = funnel.channels.getConnector(param.channel, param.connector);
1675
- if (!existing) throw new HTTPException(404, { message: `connector "${param.connector}" not found in channel "${param.channel}"` });
1635
+ if (!existing) throw new HTTPException(404, { message: notFoundMessage({
1636
+ kind: "connector",
1637
+ name: param.connector,
1638
+ available: (funnel.channels.get(param.channel)?.connectors ?? []).map((conn) => conn.name),
1639
+ nextAction: `fnl channels ${param.channel} connectors add <name> --type=slack|gh|discord|schedule ...`
1640
+ }) });
1676
1641
  if (existing.type === "slack") funnel.channels.updateSlackConnector(param.channel, param.connector, {
1677
1642
  ...query["bot-token"] !== void 0 ? { botToken: query["bot-token"] } : {},
1678
1643
  ...query["app-token"] !== void 0 ? { appToken: query["app-token"] } : {}
@@ -1695,15 +1660,21 @@ subcommands:
1695
1660
 
1696
1661
  output / valid YAML`), (c) => {
1697
1662
  const param = c.req.valid("param");
1698
- const connector = c.env.funnel.channels.getConnector(param.channel, param.connector);
1699
- if (!connector) throw new HTTPException(404, { message: `connector "${param.connector}" not found in channel "${param.channel}"` });
1663
+ const funnel = c.env.funnel;
1664
+ const connector = funnel.channels.getConnector(param.channel, param.connector);
1665
+ if (!connector) throw new HTTPException(404, { message: notFoundMessage({
1666
+ kind: "connector",
1667
+ name: param.connector,
1668
+ available: (funnel.channels.get(param.channel)?.connectors ?? []).map((conn) => conn.name),
1669
+ nextAction: `fnl channels ${param.channel} connectors add <name> --type=slack|gh|discord|schedule ...`
1670
+ }) });
1700
1671
  return c.text(renderYaml(connector));
1701
1672
  });
1702
1673
  //#endregion
1703
1674
  //#region lib/cli/routes/channels.$channel.connectors.rename.ts
1704
- const help$13 = `funnel channels <channel> connectors rename <old> <new> — rename a connector
1675
+ const help$11 = `funnel channels <channel> connectors rename <old-connector-name> <new-connector-name> — rename a connector
1705
1676
 
1706
- usage: funnel channels <channel> connectors rename <old> <new>
1677
+ usage: funnel channels <channel> connectors rename <old-connector-name> <new-connector-name>
1707
1678
 
1708
1679
  Renames the connector in the configuration file. Tokens, type, and
1709
1680
  schedules are preserved. The gateway picks up the new name on the
@@ -1713,7 +1684,7 @@ examples:
1713
1684
  funnel channels production connectors rename slack-1 slack-main
1714
1685
 
1715
1686
  see also: funnel channels <channel> connectors`;
1716
- const channelsConnectorsRenameHelpHandler = factory.createHandlers((c) => c.text(help$13));
1687
+ const channelsConnectorsRenameHelpHandler = factory.createHandlers((c) => c.text(help$11));
1717
1688
  //#endregion
1718
1689
  //#region lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts
1719
1690
  const channelsConnectorsRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1730,10 +1701,10 @@ const channelsConnectorsRenameHandler = factory.createHandlers(zValidator$1("par
1730
1701
  });
1731
1702
  //#endregion
1732
1703
  //#region lib/cli/routes/channels.$channel.connectors.$connector.rename.ts
1733
- const help$12 = `funnel channels <channel> connectors rename <connector> <new-name>
1704
+ const help$10 = `funnel channels <channel> connectors rename <connector> <new-name>
1734
1705
 
1735
1706
  usage: funnel channels <channel> connectors rename <connector> <new-name>`;
1736
- const channelsConnectorRenameHelpHandler = factory.createHandlers((c) => c.text(help$12));
1707
+ const channelsConnectorRenameHelpHandler = factory.createHandlers((c) => c.text(help$10));
1737
1708
  const channelsConnectorsRequestHandler = factory.createHandlers(zValidator$1("param", z.object({
1738
1709
  channel: z.string(),
1739
1710
  connector: z.string()
@@ -1776,16 +1747,39 @@ subcommands:
1776
1747
  add <id> --cron=... --prompt=... [--enabled=true] [--catchup-policy=latest|all|skip] / add entry
1777
1748
  remove <id> / remove entry`), (c) => {
1778
1749
  const param = c.req.valid("param");
1779
- const entries = c.env.funnel.channels.listScheduleEntries(param.channel, param.connector);
1750
+ const funnel = c.env.funnel;
1751
+ const entries = z.array(scheduleEntrySchema).parse(funnel.channels.connectorOp(param.channel, param.connector, "listEntries", void 0));
1780
1752
  if (entries.length === 0) return c.text("no schedule entries");
1781
1753
  return c.text(entries.map((e) => `${e.id}\t${e.cron}\t${e.enabled ? "on" : "off"}\t${e.prompt}`).join("\n"));
1782
1754
  });
1783
1755
  //#endregion
1784
1756
  //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.ts
1785
- const help$11 = `funnel channels <ch> connectors <conn> schedules add <id> — add a schedule entry
1757
+ const help$9 = `funnel channels <ch> connectors <conn> schedules add <id> — add a schedule entry
1786
1758
 
1787
- usage: funnel channels <ch> connectors <conn> schedules add <id> --cron="*/5 * * * *" --prompt="..." [--enabled=true] [--catchup-policy=latest|all|skip]`;
1788
- const channelsConnectorSchedulesAddHelpHandler = factory.createHandlers((c) => c.text(help$11));
1759
+ usage: funnel channels <ch> connectors <conn> schedules add <id> --cron="*/5 * * * *" --prompt="..." [--enabled|--enabled=false] [--catchup-policy=latest|all|skip]
1760
+
1761
+ options:
1762
+ --cron <expr> / 5-field cron expression (required)
1763
+ --prompt <text> / prompt delivered on each fire (required)
1764
+ --enabled / fire on schedule (default: true; --enabled=false stores it disabled)
1765
+ --catchup-policy latest|all|skip / how missed fires are replayed after downtime (default: latest)`;
1766
+ const channelsConnectorSchedulesAddHelpHandler = factory.createHandlers((c) => c.text(help$9));
1767
+ //#endregion
1768
+ //#region lib/cli/router/boolean-flag.ts
1769
+ /**
1770
+ * One parser for every CLI boolean flag: bare `--flag` (and `--flag=true`)
1771
+ * mean true, `--flag=false` means false, absent stays undefined so routes can
1772
+ * distinguish "not given" from "explicitly off". `""` survives for callers
1773
+ * that hit the HTTP surface directly with `?flag=`.
1774
+ */
1775
+ const booleanFlag = z.enum([
1776
+ "true",
1777
+ "false",
1778
+ ""
1779
+ ]).optional().transform((value) => {
1780
+ if (value === void 0) return void 0;
1781
+ return value !== "false";
1782
+ });
1789
1783
  //#endregion
1790
1784
  //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts
1791
1785
  const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1795,28 +1789,28 @@ const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$
1795
1789
  })), zValidator$1("query", z.object({
1796
1790
  cron: z.string(),
1797
1791
  prompt: z.string(),
1798
- enabled: z.enum(["true", "false"]).optional(),
1792
+ enabled: booleanFlag,
1799
1793
  "catchup-policy": scheduleCatchupPolicySchema.optional()
1800
1794
  })), async (c) => {
1801
1795
  const param = c.req.valid("param");
1802
1796
  const query = c.req.valid("query");
1803
1797
  const funnel = c.env.funnel;
1804
- const entry = funnel.channels.addScheduleEntry(param.channel, param.connector, {
1798
+ const entry = scheduleEntrySchema.parse(funnel.channels.connectorOp(param.channel, param.connector, "addEntry", {
1805
1799
  id: param.id,
1806
1800
  cron: query.cron,
1807
1801
  prompt: query.prompt,
1808
- ...query.enabled !== void 0 ? { enabled: query.enabled === "true" } : {},
1802
+ ...query.enabled !== void 0 ? { enabled: query.enabled } : {},
1809
1803
  ...query["catchup-policy"] !== void 0 ? { catchupPolicy: query["catchup-policy"] } : {}
1810
- });
1804
+ }));
1811
1805
  await funnel.listeners.restart(param.channel, param.connector);
1812
1806
  return c.text(`added schedule entry "${entry.id}"`);
1813
1807
  });
1814
1808
  //#endregion
1815
1809
  //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.ts
1816
- const help$10 = `funnel channels <ch> connectors <conn> schedules remove <id>
1810
+ const help$8 = `funnel channels <ch> connectors <conn> schedules remove <id>
1817
1811
 
1818
1812
  usage: funnel channels <ch> connectors <conn> schedules remove <id>`;
1819
- const channelsConnectorSchedulesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$10));
1813
+ const channelsConnectorSchedulesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$8));
1820
1814
  //#endregion
1821
1815
  //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts
1822
1816
  const channelsConnectorsSchedulesRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1826,13 +1820,13 @@ const channelsConnectorsSchedulesRemoveHandler = factory.createHandlers(zValidat
1826
1820
  })), zValidator$1("query", z.object({})), async (c) => {
1827
1821
  const param = c.req.valid("param");
1828
1822
  const funnel = c.env.funnel;
1829
- funnel.channels.removeScheduleEntry(param.channel, param.connector, param.id);
1823
+ funnel.channels.connectorOp(param.channel, param.connector, "removeEntry", { id: param.id });
1830
1824
  await funnel.listeners.restart(param.channel, param.connector);
1831
1825
  return c.text(`removed schedule entry "${param.id}"`);
1832
1826
  });
1833
1827
  //#endregion
1834
1828
  //#region lib/cli/routes/channels.publish.ts
1835
- const help$9 = `funnel channels <channel> publish — push arbitrary content into a channel
1829
+ const help$7 = `funnel channels <channel> publish — push arbitrary content into a channel
1836
1830
 
1837
1831
  usage: funnel channels <channel> publish --content="<text>" [--connector=<name>] [--meta-<key>=<value> ...]
1838
1832
 
@@ -1840,7 +1834,7 @@ options:
1840
1834
  --content Required. The event body delivered to subscribers.
1841
1835
  --connector Optional. Stamp the event with a connector name (resolved to id when found).
1842
1836
  --meta-<key> Optional. Repeatable. Added to meta. Example: --meta-source=cron`;
1843
- const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$9));
1837
+ const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$7));
1844
1838
  //#endregion
1845
1839
  //#region lib/cli/routes/channels.$channel.publish.ts
1846
1840
  const querySchema = z.object({
@@ -1864,7 +1858,7 @@ const channelsPublishHandler = factory.createHandlers(zValidator$1("param", z.ob
1864
1858
  });
1865
1859
  //#endregion
1866
1860
  //#region lib/cli/routes/channels.remove.ts
1867
- const help$8 = `funnel channels remove — remove a channel and all its connectors
1861
+ const help$6 = `funnel channels remove — remove a channel and all its connectors
1868
1862
 
1869
1863
  usage: funnel channels remove <name>
1870
1864
 
@@ -1876,21 +1870,28 @@ examples:
1876
1870
  funnel channels remove staging
1877
1871
 
1878
1872
  see also: funnel channels, funnel channels add`;
1879
- const channelsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$8));
1873
+ const channelsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$6));
1880
1874
  //#endregion
1881
1875
  //#region lib/cli/routes/channels.remove.$channel.ts
1882
1876
  const channelsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({})), (c) => {
1883
1877
  const param = c.req.valid("param");
1884
- c.env.funnel.channels.remove(param.channel);
1878
+ const funnel = c.env.funnel;
1879
+ if (!funnel.channels.get(param.channel)) throw new HTTPException(404, { message: notFoundMessage({
1880
+ kind: "channel",
1881
+ name: param.channel,
1882
+ available: funnel.channels.list().map((ch) => ch.name),
1883
+ nextAction: "fnl channels add <name>"
1884
+ }) });
1885
+ funnel.channels.remove(param.channel);
1885
1886
  return c.text(`removed channel "${param.channel}"`);
1886
1887
  });
1887
1888
  //#endregion
1888
1889
  //#region lib/cli/routes/channels.rename.ts
1889
- const help$7 = `funnel channels rename — rename a channel
1890
+ const channelsRenameHelp = `funnel channels rename — rename a channel
1890
1891
 
1891
1892
  usage:
1892
- funnel channels rename <old> <new>
1893
- funnel channels <old> rename <new>
1893
+ funnel channels rename <old-channel-name> <new-channel-name>
1894
+ funnel channels <old-channel-name> rename <new-channel-name>
1894
1895
 
1895
1896
  Renames the channel in the configuration file. Connectors, schedules,
1896
1897
  and delivery mode are preserved. The gateway picks up the new name on
@@ -1901,15 +1902,10 @@ examples:
1901
1902
  funnel channels staging rename production
1902
1903
 
1903
1904
  see also: funnel channels, funnel channels <name>`;
1904
- const channelsRenameHelpHandler = factory.createHandlers((c) => c.text(help$7));
1905
+ const channelsRenameHelpHandler = factory.createHandlers((c) => c.text(channelsRenameHelp));
1905
1906
  //#endregion
1906
1907
  //#region lib/cli/routes/channels.$channel.rename.ts
1907
- const help$6 = `funnel channels rename — rename a channel
1908
-
1909
- usage:
1910
- funnel channels rename <old> <new>
1911
- funnel channels <old> rename <new>`;
1912
- const channelsChannelRenameHelpHandler = factory.createHandlers((c) => c.text(help$6));
1908
+ const channelsChannelRenameHelpHandler = factory.createHandlers((c) => c.text(channelsRenameHelp));
1913
1909
  //#endregion
1914
1910
  //#region lib/cli/routes/channels.$channel.rename.$newName.ts
1915
1911
  const channelsRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1917,7 +1913,14 @@ const channelsRenameHandler = factory.createHandlers(zValidator$1("param", z.obj
1917
1913
  newName: z.string()
1918
1914
  })), zValidator$1("query", z.object({})), (c) => {
1919
1915
  const param = c.req.valid("param");
1920
- c.env.funnel.channels.rename(param.channel, param.newName);
1916
+ const funnel = c.env.funnel;
1917
+ if (!funnel.channels.get(param.channel)) throw new HTTPException(404, { message: notFoundMessage({
1918
+ kind: "channel",
1919
+ name: param.channel,
1920
+ available: funnel.channels.list().map((ch) => ch.name),
1921
+ nextAction: "fnl channels add <name>"
1922
+ }) });
1923
+ funnel.channels.rename(param.channel, param.newName);
1921
1924
  return c.text(`renamed channel "${param.channel}" to "${param.newName}"`);
1922
1925
  });
1923
1926
  const channelsSetDeliveryHandler = factory.createHandlers(helpGuard(`funnel channels <name> set delivery <mode> — change a channel's routing mode
@@ -1947,8 +1950,14 @@ subcommands:
1947
1950
 
1948
1951
  output / valid YAML`), (c) => {
1949
1952
  const param = c.req.valid("param");
1950
- const channel = c.env.funnel.channels.get(param.channel);
1951
- if (!channel) throw new HTTPException(404, { message: `channel "${param.channel}" not found` });
1953
+ const funnel = c.env.funnel;
1954
+ const channel = funnel.channels.get(param.channel);
1955
+ if (!channel) throw new HTTPException(404, { message: notFoundMessage({
1956
+ kind: "channel",
1957
+ name: param.channel,
1958
+ available: funnel.channels.list().map((ch) => ch.name),
1959
+ nextAction: "fnl channels add <name>"
1960
+ }) });
1952
1961
  return c.text(renderYaml({
1953
1962
  id: channel.id,
1954
1963
  name: channel.name,
@@ -2069,8 +2078,14 @@ const validateConnector = (connector) => {
2069
2078
  };
2070
2079
  const channelsValidateHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({})), (c) => {
2071
2080
  const param = c.req.valid("param");
2072
- const channel = c.env.funnel.channels.get(param.channel);
2073
- if (!channel) throw new HTTPException(404, { message: `channel "${param.channel}" not found` });
2081
+ const funnel = c.env.funnel;
2082
+ const channel = funnel.channels.get(param.channel);
2083
+ if (!channel) throw new HTTPException(404, { message: notFoundMessage({
2084
+ kind: "channel",
2085
+ name: param.channel,
2086
+ available: funnel.channels.list().map((ch) => ch.name),
2087
+ nextAction: "fnl channels add <name>"
2088
+ }) });
2074
2089
  if (channel.connectors.length === 0) return c.text(renderYaml({
2075
2090
  channel: channel.name,
2076
2091
  valid: false,
@@ -2141,7 +2156,12 @@ const claudeHandler = factory.createHandlers(helpGuard(claudeHelp), zValidator$1
2141
2156
  }
2142
2157
  if (query.profile) {
2143
2158
  const profile = profiles.get(query.profile);
2144
- if (!profile) throw new HTTPException(404, { message: `profile "${query.profile}" not found` });
2159
+ if (!profile) throw new HTTPException(404, { message: notFoundMessage({
2160
+ kind: "profile",
2161
+ name: query.profile,
2162
+ available: profiles.list().map((p) => p.name),
2163
+ nextAction: "fnl profiles add <name> --path=<repo> --channel=<channel>"
2164
+ }) });
2145
2165
  const exitCode = await claude.launch({
2146
2166
  channel: profile.channelId,
2147
2167
  cwd: profile.path,
@@ -2274,16 +2294,12 @@ const resolveTargetChannel = (c, channelArg) => {
2274
2294
  };
2275
2295
  const debugHandler = factory.createHandlers(helpGuard(debugHelp), zValidator$1("query", z.object({
2276
2296
  channel: z.string().optional(),
2277
- all: z.enum([
2278
- "true",
2279
- "false",
2280
- ""
2281
- ]).optional(),
2297
+ all: booleanFlag,
2282
2298
  limit: z.string().optional()
2283
2299
  })), async (c) => {
2284
2300
  const query = c.req.valid("query");
2285
2301
  const funnel = c.env.funnel;
2286
- if (query.all === "true" || query.all === "") {
2302
+ if (query.all === true) {
2287
2303
  const report = await funnel.diagnostics.diagnoseAll();
2288
2304
  return c.text(renderYaml(report));
2289
2305
  }
@@ -2394,20 +2410,12 @@ examples:
2394
2410
  funnel doctor
2395
2411
  funnel doctor --fix
2396
2412
  funnel doctor --fix --aggressive`), zValidator$1("query", z.object({
2397
- fix: z.enum([
2398
- "true",
2399
- "false",
2400
- ""
2401
- ]).optional(),
2402
- aggressive: z.enum([
2403
- "true",
2404
- "false",
2405
- ""
2406
- ]).optional()
2413
+ fix: booleanFlag,
2414
+ aggressive: booleanFlag
2407
2415
  })), async (c) => {
2408
2416
  const query = c.req.valid("query");
2409
- const wantsFix = query.fix === "true" || query.fix === "";
2410
- const wantsAggressive = query.aggressive === "true" || query.aggressive === "";
2417
+ const wantsFix = query.fix === true;
2418
+ const wantsAggressive = query.aggressive === true;
2411
2419
  const mode = wantsFix ? wantsAggressive ? "aggressive" : "safe" : "off";
2412
2420
  const report = await c.env.funnel.doctor.run(mode);
2413
2421
  return c.text(renderYaml(report));
@@ -2446,7 +2454,7 @@ examples:
2446
2454
  const renderGatewayStatus = async (c) => {
2447
2455
  const status = c.env.funnel.gateway.getStatus();
2448
2456
  if (!status.running) return c.text(renderYaml({ running: false }), 503);
2449
- const res = await fetch(`http://127.0.0.1:${status.port}/status`).catch(() => null);
2457
+ const res = await fetch(`${gatewayLoopbackUrl(status.port)}/status`).catch(() => null);
2450
2458
  if (!res) return c.text(renderYaml({
2451
2459
  running: true,
2452
2460
  pid: status.pid,
@@ -2756,6 +2764,16 @@ examples:
2756
2764
  funnel gateway start --no-caffeine
2757
2765
 
2758
2766
  programmable: funnel.gateway.start({ caffeinate })`;
2767
+ const HEALTH_TIMEOUT_MS = 5e3;
2768
+ const HEALTH_POLL_INTERVAL_MS = 100;
2769
+ const waitForHealth = async (port) => {
2770
+ const deadline = Date.now() + HEALTH_TIMEOUT_MS;
2771
+ while (Date.now() < deadline) {
2772
+ if ((await fetch(`${gatewayLoopbackUrl(port)}/health`).catch(() => null))?.ok) return true;
2773
+ await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_INTERVAL_MS));
2774
+ }
2775
+ return false;
2776
+ };
2759
2777
  const gatewayStartHandler = factory.createHandlers(helpGuard(startHelp), zValidator$1("query", z.object({ "no-caffeine": z.string().optional() })), async (c) => {
2760
2778
  const query = c.req.valid("query");
2761
2779
  const funnel = c.env.funnel;
@@ -2763,8 +2781,10 @@ const gatewayStartHandler = factory.createHandlers(helpGuard(startHelp), zValida
2763
2781
  const status = funnel.gateway.getStatus();
2764
2782
  return c.text(`funnel gateway: already running (pid ${status.pid})`);
2765
2783
  }
2766
- if (!await funnel.gateway.start({ caffeinate: query["no-caffeine"] !== "true" })) throw new HTTPException(500, { message: "funnel gateway: failed to start" });
2767
- return c.text("funnel gateway: started");
2784
+ if (!await funnel.gateway.start({ caffeinate: query["no-caffeine"] !== "true" })) throw new HTTPException(500, { message: "funnel gateway: failed to start — inspect the daemon log with `fnl gateway logs`" });
2785
+ const status = funnel.gateway.getStatus();
2786
+ if (!await waitForHealth(status.port)) return c.text(`funnel gateway: started (pid ${status.pid}, port ${status.port}) but /health did not respond within ${HEALTH_TIMEOUT_MS / 1e3}s — check \`fnl gateway logs\``);
2787
+ return c.text(`funnel gateway: started (pid ${status.pid}, port ${status.port})`);
2768
2788
  });
2769
2789
  const gatewayStatusHandler = factory.createHandlers(helpGuard(`funnel gateway status / show gateway running status
2770
2790
 
@@ -2854,7 +2874,12 @@ const profilesAddHandler = factory.createHandlers(zValidator$1("param", z.object
2854
2874
  const funnel = c.env.funnel;
2855
2875
  const { profiles, claude } = c.env;
2856
2876
  const channel = funnel.channels.get(query.channel);
2857
- if (!channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
2877
+ if (!channel) throw new HTTPException(400, { message: notFoundMessage({
2878
+ kind: "channel",
2879
+ name: query.channel,
2880
+ available: funnel.channels.list().map((ch) => ch.name),
2881
+ nextAction: "fnl channels add <name>"
2882
+ }) });
2858
2883
  const recipe = parseProfileRecipe(query);
2859
2884
  profiles.add({
2860
2885
  name: param.profile,
@@ -2917,7 +2942,12 @@ const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.obj
2917
2942
  c.env.funnel;
2918
2943
  const { profiles, claude } = c.env;
2919
2944
  const profile = profiles.get(param.profile);
2920
- if (!profile) throw new HTTPException(404, { message: `profile "${param.profile}" not found` });
2945
+ if (!profile) throw new HTTPException(404, { message: notFoundMessage({
2946
+ kind: "profile",
2947
+ name: param.profile,
2948
+ available: profiles.list().map((p) => p.name),
2949
+ nextAction: "fnl profiles add <name> --path=<repo> --channel=<channel>"
2950
+ }) });
2921
2951
  const exitCode = await claude.launch({
2922
2952
  channel: profile.channelId,
2923
2953
  cwd: profile.path,
@@ -2977,7 +3007,12 @@ const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object
2977
3007
  const funnel = c.env.funnel;
2978
3008
  const { profiles, claude } = c.env;
2979
3009
  const channel = query.channel !== void 0 ? funnel.channels.get(query.channel) : null;
2980
- if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
3010
+ if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message: notFoundMessage({
3011
+ kind: "channel",
3012
+ name: query.channel,
3013
+ available: funnel.channels.list().map((ch) => ch.name),
3014
+ nextAction: "fnl channels add <name>"
3015
+ }) });
2981
3016
  const recipe = parseProfileRecipe(query);
2982
3017
  profiles.update(param.profile, {
2983
3018
  path: query.path,
@@ -3053,7 +3088,7 @@ const statusHelp = `funnel status / overall health snapshot
3053
3088
  usage / funnel status [--watch] [--interval <N>]
3054
3089
 
3055
3090
  options:
3056
- --watch / continuously refresh (Ctrl+C to stop)
3091
+ --watch / continuously refresh (default: off; Ctrl+C to stop)
3057
3092
  --interval <N> / polling interval in seconds (default 3)
3058
3093
 
3059
3094
  output / valid YAML
@@ -3078,7 +3113,7 @@ const buildStatusReport = async (funnel, profiles) => {
3078
3113
  const gatewayStatus = funnel.gateway.getStatus();
3079
3114
  let gatewayData = null;
3080
3115
  if (gatewayStatus.running) {
3081
- const res = await fetch(`http://127.0.0.1:${gatewayStatus.port}/status`).catch(() => null);
3116
+ const res = await fetch(`${gatewayLoopbackUrl(gatewayStatus.port)}/status`).catch(() => null);
3082
3117
  if (res && res.ok) {
3083
3118
  const body = await res.json();
3084
3119
  if (isGatewayStatus(body)) gatewayData = body;
@@ -3099,6 +3134,7 @@ const buildStatusReport = async (funnel, profiles) => {
3099
3134
  return {
3100
3135
  gateway: gatewayStatus.running ? {
3101
3136
  running: true,
3137
+ responsive: gatewayData !== null,
3102
3138
  pid: gatewayStatus.pid,
3103
3139
  port: gatewayStatus.port,
3104
3140
  uptimeMs: gatewayData?.uptimeMs ?? null
@@ -3125,16 +3161,12 @@ const buildStatusReport = async (funnel, profiles) => {
3125
3161
  };
3126
3162
  };
3127
3163
  const statusHandler = factory.createHandlers(helpGuard(statusHelp), zValidator$1("query", z.object({
3128
- watch: z.enum([
3129
- "true",
3130
- "false",
3131
- ""
3132
- ]).optional(),
3164
+ watch: booleanFlag,
3133
3165
  interval: z.string().optional()
3134
3166
  })), async (c) => {
3135
3167
  const query = c.req.valid("query");
3136
3168
  const funnel = c.env.funnel;
3137
- const isWatch = query.watch === "true" || query.watch === "";
3169
+ const isWatch = query.watch === true;
3138
3170
  const intervalSec = Math.min(60, Math.max(1, query.interval ? Number(query.interval) : 3));
3139
3171
  if (!isWatch) {
3140
3172
  const report = await buildStatusReport(funnel, c.env.profiles);
@@ -3185,4 +3217,4 @@ const routes = factory.createApp().onError((error, c) => {
3185
3217
  return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
3186
3218
  }).get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...channelsAddHelpHandler).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...channelsRemoveHelpHandler).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...channelsRenameHelpHandler).post("/channels/:channel/rename", ...channelsChannelRenameHelpHandler).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...channelsPublishHelpHandler).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel/validate", ...channelsValidateHandler).get("/channels/validate", ...channelsValidateHelpHandler).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...channelsConnectorsAddHelpHandler).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...channelsConnectorsRemoveHelpHandler).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...channelsConnectorsSetHelpHandler).post("/channels/:channel/connectors/set/:connector", ...channelsConnectorsSetHandler).post("/channels/:channel/connectors/rename/:connector/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/:connector/rename/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/rename", ...channelsConnectorsRenameHelpHandler).post("/channels/:channel/connectors/:connector/rename", ...channelsConnectorRenameHelpHandler).post("/channels/:channel/connectors/:connector/request", ...channelsConnectorsRequestHandler).get("/channels/:channel/connectors/:connector", ...channelsConnectorsShowHandler).get("/channels/:channel/connectors/:connector/schedules", ...channelsConnectorsSchedulesGroupHandler).post("/channels/:channel/connectors/:connector/schedules/add", ...channelsConnectorSchedulesAddHelpHandler).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...channelsConnectorSchedulesRemoveHelpHandler).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...profilesAddHelpHandler).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...profilesSetHelpHandler).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...profilesRemoveHelpHandler).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...profilesRenameHelpHandler).post("/profiles/:profile/rename", ...profilesProfileRenameHelpHandler).post("/profiles/:profile/as-default", ...profilesAsDefaultHandler).get("/profiles/:profile/run", ...profilesLaunchHandler).get("/profiles/:profile", ...profilesLaunchHandler).get("/gateway", ...gatewayGroupHandler).get("/gateway/status", ...gatewayStatusHandler).get("/gateway/start", ...gatewayStartHandler).get("/gateway/stop", ...gatewayStopHandler).get("/gateway/restart", ...gatewayRestartHandler).get("/gateway/run", ...gatewayRunHandler).get("/gateway/logs", ...gatewayLogsHandler).get("/gateway/sql", ...gatewaySqlHandler).get("/gateway/listeners", ...gatewayListenersHandler).get("/debug", ...debugHandler).get("/debug/events", ...debugEventsHandler).get("/debug/dropped", ...debugDroppedHandler).get("/debug/errors", ...debugErrorsHandler).get("/debug/replay", ...debugReplayHandler).get("/docs", ...docsIndexHandler).get("/docs/:topic", ...docsTopicHandler).get("/doctor", ...doctorHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
3187
3219
  //#endregion
3188
- export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClock, FunnelConnectorAdapter, FunnelConnectorFactory, FunnelConnectorListener, FunnelDiagnostics, FunnelDocs, FunnelDoctor, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelHttpClient, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLogger, FunnelProcessRunner, FunnelRecovery, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelHttpClient, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelHttpClient, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, buildServiceRoutes, channelConfigSchema, channelDeliveryModeSchema, channelWsProtocols, channelWsUrl, routes as cliRoutes, connectorConfigSchema, 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 };
3220
+ export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClock, FunnelConnectorAdapter, FunnelConnectorListener, FunnelConnectorRegistry, FunnelDiagnostics, FunnelDocs, FunnelDoctor, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelHttpClient, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLogger, FunnelProcessRunner, FunnelRecovery, FunnelSettingsReader, FunnelSettingsStore, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelHttpClient, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelHttpClient, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, baseConnectorConfigSchema, buildServiceRoutes, channelConfigSchema, channelDeliveryModeSchema, channelWsProtocols, channelWsUrl, routes as cliRoutes, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, createSettings, factory, funnelEventSchema, gatewayLoopbackUrl, previewOf, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryRows, queryToCliArgs, resolveFunnelDir, resolveFunnelPort, settingsSchema, toDiagnosticConnectionError, toDiagnosticEvent, toRequest };