@interactive-inc/claude-funnel 0.59.1 → 0.60.1

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-C_PLxfUX.d.ts} +6 -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 +376 -343
  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-93pX7EF7.js} +7 -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-93pX7EF7.js";
9
+ import { a as toDiagnosticEvent, i as toDiagnosticConnectionError, n as previewOf, r as queryRows, t as FunnelDiagnostics } from "./funnel-diagnostics-CSiJmPlZ.js";
10
+ import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-C9zR-Csp.js";
11
+ import { t as FunnelDoctor } from "./funnel-doctor-DiJCjHsg.js";
12
+ import { t as FunnelDocs } from "./funnel-docs-BxXZ9Ksx.js";
13
+ import { a as FunnelLocalConfig, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigSync, t as funnelJsonSchema } from "./local-config-json-schema-JyLqOQNX.js";
14
+ import { t as FunnelProfiles } from "./profiles-DSzTeKQw.js";
15
+ import { t as FunnelRecovery } from "./funnel-recovery-BFdPjL6Z.js";
16
+ import { C as funnelTmpDir, S as publishResponseSchema, _ as funnelEventSchema, a as connectorConnectionEventSchema, b as FunnelChannelPublisher, c as MemoryFunnelEventLog, d as DEFAULT_GATEWAY_TOKEN_PATH, f as FunnelGatewayToken, g as FunnelEventLog, h as SqliteFunnelEventLog, i as ConnectorDiagnosticLog, l as channelWsProtocols, m as FunnelListenerSupervisor, n as SqliteConnectorDiagnosticLog, o as connectorProcessedEventSchema, p as FunnelGatewayServer, r as CONNECTOR_CONNECTION_STATUSES, s as connectorRawEventSchema, t as MemoryConnectorDiagnosticLog, u as channelWsUrl, v as FunnelBroadcaster, x as publishRequestSchema, y as requireBearerToken } from "./memory-diagnostic-log-CI60kNfB.js";
17
+ import { n as FunnelHttpClient, t as NodeFunnelHttpClient } from "./node-http-client-lowp60Oa.js";
18
+ import { t as FunnelConnectorAdapter } from "./connector-adapter-DU9Rvyec.js";
19
+ import { t as FunnelConnectorListener } from "./connector-listener-DR3aKOuK.js";
20
+ import { r as scheduleEntrySchema, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-CfyuMCMh.js";
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,
@@ -1133,7 +1026,8 @@ var Funnel = class Funnel {
1133
1026
  dir
1134
1027
  }),
1135
1028
  process,
1136
- logger: this.logger
1029
+ logger: this.logger,
1030
+ dir
1137
1031
  });
1138
1032
  this.diagnostics = new FunnelDiagnostics({
1139
1033
  gateway: this.gateway,
@@ -1222,10 +1116,32 @@ var Funnel = class Funnel {
1222
1116
  }
1223
1117
  gatewayClient() {
1224
1118
  const { port } = this.gateway.getStatus();
1225
- return hc(`http://127.0.0.1:${port}`);
1119
+ return hc(gatewayLoopbackUrl(port));
1226
1120
  }
1227
1121
  };
1228
1122
  //#endregion
1123
+ //#region lib/engine/logger/redact-secrets.ts
1124
+ /**
1125
+ * Mask credential-shaped substrings before a log line is persisted. Matching
1126
+ * is prefix-anchored (Slack xoxb-/xapp-, GitHub ghp_/github_pat_, Discord
1127
+ * Bot tokens, HTTP bearer values) so ordinary identifiers never trip it —
1128
+ * a false negative only weakens defense in depth, a false positive destroys
1129
+ * a legitimate log line.
1130
+ */
1131
+ const SECRET_PATTERNS = [
1132
+ /xox[abprs]-[A-Za-z0-9-]{10,}/g,
1133
+ /xapp-[A-Za-z0-9-]{10,}/g,
1134
+ /gh[pousr]_[A-Za-z0-9]{20,}/g,
1135
+ /github_pat_[A-Za-z0-9_]{20,}/g,
1136
+ /Bot [A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{20,}/g,
1137
+ /Bearer [A-Za-z0-9._~+/-]{16,}/g
1138
+ ];
1139
+ const redactSecrets = (text) => {
1140
+ let redacted = text;
1141
+ for (const pattern of SECRET_PATTERNS) redacted = redacted.replace(pattern, "[redacted]");
1142
+ return redacted;
1143
+ };
1144
+ //#endregion
1229
1145
  //#region lib/engine/logger/node-logger.ts
1230
1146
  const defaultLogFile = () => join(funnelTmpDir(), "funnel.log");
1231
1147
  var NodeFunnelLogger = class extends FunnelLogger {
@@ -1254,7 +1170,7 @@ var NodeFunnelLogger = class extends FunnelLogger {
1254
1170
  message,
1255
1171
  ...meta ? { meta } : {}
1256
1172
  };
1257
- appendFileSync(this.file, `${JSON.stringify(entry)}\n`);
1173
+ appendFileSync(this.file, `${redactSecrets(JSON.stringify(entry))}\n`);
1258
1174
  }
1259
1175
  };
1260
1176
  //#endregion
@@ -1399,13 +1315,19 @@ const toRequest = (args) => {
1399
1315
  while (i < args.length) {
1400
1316
  const arg = args[i];
1401
1317
  if (arg.startsWith("--")) {
1402
- const key = arg.slice(2);
1318
+ const body = arg.slice(2);
1319
+ const equalsAt = body.indexOf("=");
1320
+ if (equalsAt >= 0) {
1321
+ params.set(body.slice(0, equalsAt), body.slice(equalsAt + 1));
1322
+ i++;
1323
+ continue;
1324
+ }
1403
1325
  const next = args[i + 1];
1404
1326
  if (isValue(next)) {
1405
- params.set(key, next);
1327
+ params.set(body, next);
1406
1328
  i += 2;
1407
1329
  } else {
1408
- params.set(key, "true");
1330
+ params.set(body, "true");
1409
1331
  i++;
1410
1332
  }
1411
1333
  continue;
@@ -1471,7 +1393,7 @@ const queryToCliArgs = (url, reservedKeys = []) => {
1471
1393
  };
1472
1394
  //#endregion
1473
1395
  //#region lib/cli/routes/channels.add.ts
1474
- const help$17 = `funnel channels add — add a channel
1396
+ const help$15 = `funnel channels add — add a channel
1475
1397
 
1476
1398
  usage: funnel channels add <name> [--delivery fanout|exclusive]
1477
1399
 
@@ -1489,7 +1411,29 @@ examples:
1489
1411
  funnel channels add ci-events --delivery exclusive
1490
1412
 
1491
1413
  see also: funnel channels, funnel channels <name> connectors add`;
1492
- const channelsAddHelpHandler = factory.createHandlers((c) => c.text(help$17));
1414
+ const channelsAddHelpHandler = factory.createHandlers((c) => c.text(help$15));
1415
+ //#endregion
1416
+ //#region lib/cli/router/validator.ts
1417
+ const labelFor = (target, key) => {
1418
+ if (target === "query") return `--${key}`;
1419
+ return `<${key}>`;
1420
+ };
1421
+ const formatIssues = (target, issues) => {
1422
+ return `invalid arguments — ${issues.map((issue) => {
1423
+ const key = issue.path.map(String).join(".");
1424
+ if (!key) return issue.message;
1425
+ return `${labelFor(target, key)}: ${issue.message}`;
1426
+ }).join("; ")} (run with --help for usage)`;
1427
+ };
1428
+ /**
1429
+ * CLI-flavored zValidator: every route imports this instead of the raw
1430
+ * @hono/zod-validator so a validation failure renders as one readable line
1431
+ * naming the offending flag, not a raw ZodError JSON dump.
1432
+ */
1433
+ const zValidator$1 = (target, schema) => zValidator(target, schema, (result) => {
1434
+ if (result.success) return;
1435
+ throw new HTTPException(400, { message: formatIssues(target, result.error.issues) });
1436
+ });
1493
1437
  //#endregion
1494
1438
  //#region lib/cli/routes/channels.add.$channel.ts
1495
1439
  const channelsAddHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({ delivery: channelDeliveryModeSchema.optional() })), (c) => {
@@ -1502,6 +1446,17 @@ const channelsAddHandler = factory.createHandlers(zValidator$1("param", z.object
1502
1446
  return c.text(`added channel "${created.name}" (id: ${created.id})`);
1503
1447
  });
1504
1448
  //#endregion
1449
+ //#region lib/cli/routes/not-found-message.ts
1450
+ /**
1451
+ * One error shape for every name-resolution miss: what was asked, what exists,
1452
+ * and the command that creates it — so a Claude (or human) can self-correct
1453
+ * without a follow-up listing call.
1454
+ */
1455
+ const notFoundMessage = (props) => {
1456
+ const listed = props.available.length > 0 ? props.available.join(", ") : "none";
1457
+ return `${props.kind} "${props.name}" not found (available: ${listed}); to create one: ${props.nextAction}`;
1458
+ };
1459
+ //#endregion
1505
1460
  //#region lib/cli/router/help-guard.ts
1506
1461
  function helpGuard(help) {
1507
1462
  return async (c, next) => {
@@ -1526,14 +1481,20 @@ subcommands:
1526
1481
  <c> schedules add <id> --cron=... --prompt=... / add a schedule entry
1527
1482
  <c> schedules remove <id> / remove a schedule entry`), (c) => {
1528
1483
  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` });
1484
+ const funnel = c.env.funnel;
1485
+ const channel = funnel.channels.get(param.channel);
1486
+ if (!channel) throw new HTTPException(404, { message: notFoundMessage({
1487
+ kind: "channel",
1488
+ name: param.channel,
1489
+ available: funnel.channels.list().map((ch) => ch.name),
1490
+ nextAction: "fnl channels add <name>"
1491
+ }) });
1531
1492
  if (channel.connectors.length === 0) return c.text(`no connectors in channel "${channel.name}"`);
1532
1493
  return c.text(channel.connectors.map((c) => `${c.name} (${c.type}, id: ${c.id})`).join("\n"));
1533
1494
  });
1534
1495
  //#endregion
1535
1496
  //#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
1497
+ const help$14 = `funnel channels <channel> connectors add <name> — add a connector to a channel
1537
1498
 
1538
1499
  usage:
1539
1500
  funnel channels <channel> connectors add <name> --type=slack --bot-token=xoxb-... --app-token=xapp-...
@@ -1556,7 +1517,7 @@ examples:
1556
1517
  funnel channels alerts connectors add daily --type=schedule
1557
1518
 
1558
1519
  see also: funnel channels <channel> connectors, funnel channels <channel> connectors remove`;
1559
- const channelsConnectorsAddHelpHandler = factory.createHandlers((c) => c.text(help$16));
1520
+ const channelsConnectorsAddHelpHandler = factory.createHandlers((c) => c.text(help$14));
1560
1521
  //#endregion
1561
1522
  //#region lib/cli/routes/channels.$channel.connectors.add.$connector.ts
1562
1523
  const slackBody = z.object({
@@ -1624,7 +1585,7 @@ const channelsConnectorsAddHandler = factory.createHandlers(zValidator$1("param"
1624
1585
  });
1625
1586
  //#endregion
1626
1587
  //#region lib/cli/routes/channels.$channel.connectors.remove.ts
1627
- const help$15 = `funnel channels <channel> connectors remove <connector> — remove a connector
1588
+ const help$13 = `funnel channels <channel> connectors remove <connector> — remove a connector
1628
1589
 
1629
1590
  usage: funnel channels <channel> connectors remove <connector>
1630
1591
 
@@ -1636,7 +1597,7 @@ examples:
1636
1597
  funnel channels production connectors remove slack-main
1637
1598
 
1638
1599
  see also: funnel channels <channel> connectors, funnel channels <channel> connectors add`;
1639
- const channelsConnectorsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$15));
1600
+ const channelsConnectorsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$13));
1640
1601
  //#endregion
1641
1602
  //#region lib/cli/routes/channels.$channel.connectors.remove.$connector.ts
1642
1603
  const channelsConnectorsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1651,13 +1612,13 @@ const channelsConnectorsRemoveHandler = factory.createHandlers(zValidator$1("par
1651
1612
  });
1652
1613
  //#endregion
1653
1614
  //#region lib/cli/routes/channels.$channel.connectors.set.ts
1654
- const help$14 = `funnel channels <channel> connectors set <connector> — update connector fields
1615
+ const help$12 = `funnel channels <channel> connectors set <connector> — update connector fields
1655
1616
 
1656
1617
  usage:
1657
1618
  funnel channels <ch> connectors set <conn> [--bot-token=...] [--app-token=...] # slack
1658
1619
  funnel channels <ch> connectors set <conn> [--bot-token=...] # discord
1659
1620
  funnel channels <ch> connectors set <conn> [--poll-interval=N] # gh`;
1660
- const channelsConnectorsSetHelpHandler = factory.createHandlers((c) => c.text(help$14));
1621
+ const channelsConnectorsSetHelpHandler = factory.createHandlers((c) => c.text(help$12));
1661
1622
  //#endregion
1662
1623
  //#region lib/cli/routes/channels.$channel.connectors.set.$connector.ts
1663
1624
  const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1672,7 +1633,12 @@ const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param"
1672
1633
  const query = c.req.valid("query");
1673
1634
  const funnel = c.env.funnel;
1674
1635
  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}"` });
1636
+ if (!existing) throw new HTTPException(404, { message: notFoundMessage({
1637
+ kind: "connector",
1638
+ name: param.connector,
1639
+ available: (funnel.channels.get(param.channel)?.connectors ?? []).map((conn) => conn.name),
1640
+ nextAction: `fnl channels ${param.channel} connectors add <name> --type=slack|gh|discord|schedule ...`
1641
+ }) });
1676
1642
  if (existing.type === "slack") funnel.channels.updateSlackConnector(param.channel, param.connector, {
1677
1643
  ...query["bot-token"] !== void 0 ? { botToken: query["bot-token"] } : {},
1678
1644
  ...query["app-token"] !== void 0 ? { appToken: query["app-token"] } : {}
@@ -1695,15 +1661,21 @@ subcommands:
1695
1661
 
1696
1662
  output / valid YAML`), (c) => {
1697
1663
  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}"` });
1664
+ const funnel = c.env.funnel;
1665
+ const connector = funnel.channels.getConnector(param.channel, param.connector);
1666
+ if (!connector) throw new HTTPException(404, { message: notFoundMessage({
1667
+ kind: "connector",
1668
+ name: param.connector,
1669
+ available: (funnel.channels.get(param.channel)?.connectors ?? []).map((conn) => conn.name),
1670
+ nextAction: `fnl channels ${param.channel} connectors add <name> --type=slack|gh|discord|schedule ...`
1671
+ }) });
1700
1672
  return c.text(renderYaml(connector));
1701
1673
  });
1702
1674
  //#endregion
1703
1675
  //#region lib/cli/routes/channels.$channel.connectors.rename.ts
1704
- const help$13 = `funnel channels <channel> connectors rename <old> <new> — rename a connector
1676
+ const help$11 = `funnel channels <channel> connectors rename <old-connector-name> <new-connector-name> — rename a connector
1705
1677
 
1706
- usage: funnel channels <channel> connectors rename <old> <new>
1678
+ usage: funnel channels <channel> connectors rename <old-connector-name> <new-connector-name>
1707
1679
 
1708
1680
  Renames the connector in the configuration file. Tokens, type, and
1709
1681
  schedules are preserved. The gateway picks up the new name on the
@@ -1713,7 +1685,7 @@ examples:
1713
1685
  funnel channels production connectors rename slack-1 slack-main
1714
1686
 
1715
1687
  see also: funnel channels <channel> connectors`;
1716
- const channelsConnectorsRenameHelpHandler = factory.createHandlers((c) => c.text(help$13));
1688
+ const channelsConnectorsRenameHelpHandler = factory.createHandlers((c) => c.text(help$11));
1717
1689
  //#endregion
1718
1690
  //#region lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts
1719
1691
  const channelsConnectorsRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1730,10 +1702,10 @@ const channelsConnectorsRenameHandler = factory.createHandlers(zValidator$1("par
1730
1702
  });
1731
1703
  //#endregion
1732
1704
  //#region lib/cli/routes/channels.$channel.connectors.$connector.rename.ts
1733
- const help$12 = `funnel channels <channel> connectors rename <connector> <new-name>
1705
+ const help$10 = `funnel channels <channel> connectors rename <connector> <new-name>
1734
1706
 
1735
1707
  usage: funnel channels <channel> connectors rename <connector> <new-name>`;
1736
- const channelsConnectorRenameHelpHandler = factory.createHandlers((c) => c.text(help$12));
1708
+ const channelsConnectorRenameHelpHandler = factory.createHandlers((c) => c.text(help$10));
1737
1709
  const channelsConnectorsRequestHandler = factory.createHandlers(zValidator$1("param", z.object({
1738
1710
  channel: z.string(),
1739
1711
  connector: z.string()
@@ -1776,16 +1748,39 @@ subcommands:
1776
1748
  add <id> --cron=... --prompt=... [--enabled=true] [--catchup-policy=latest|all|skip] / add entry
1777
1749
  remove <id> / remove entry`), (c) => {
1778
1750
  const param = c.req.valid("param");
1779
- const entries = c.env.funnel.channels.listScheduleEntries(param.channel, param.connector);
1751
+ const funnel = c.env.funnel;
1752
+ const entries = z.array(scheduleEntrySchema).parse(funnel.channels.connectorOp(param.channel, param.connector, "listEntries", void 0));
1780
1753
  if (entries.length === 0) return c.text("no schedule entries");
1781
1754
  return c.text(entries.map((e) => `${e.id}\t${e.cron}\t${e.enabled ? "on" : "off"}\t${e.prompt}`).join("\n"));
1782
1755
  });
1783
1756
  //#endregion
1784
1757
  //#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
1758
+ const help$9 = `funnel channels <ch> connectors <conn> schedules add <id> — add a schedule entry
1786
1759
 
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));
1760
+ usage: funnel channels <ch> connectors <conn> schedules add <id> --cron="*/5 * * * *" --prompt="..." [--enabled|--enabled=false] [--catchup-policy=latest|all|skip]
1761
+
1762
+ options:
1763
+ --cron <expr> / 5-field cron expression (required)
1764
+ --prompt <text> / prompt delivered on each fire (required)
1765
+ --enabled / fire on schedule (default: true; --enabled=false stores it disabled)
1766
+ --catchup-policy latest|all|skip / how missed fires are replayed after downtime (default: latest)`;
1767
+ const channelsConnectorSchedulesAddHelpHandler = factory.createHandlers((c) => c.text(help$9));
1768
+ //#endregion
1769
+ //#region lib/cli/router/boolean-flag.ts
1770
+ /**
1771
+ * One parser for every CLI boolean flag: bare `--flag` (and `--flag=true`)
1772
+ * mean true, `--flag=false` means false, absent stays undefined so routes can
1773
+ * distinguish "not given" from "explicitly off". `""` survives for callers
1774
+ * that hit the HTTP surface directly with `?flag=`.
1775
+ */
1776
+ const booleanFlag = z.enum([
1777
+ "true",
1778
+ "false",
1779
+ ""
1780
+ ]).optional().transform((value) => {
1781
+ if (value === void 0) return void 0;
1782
+ return value !== "false";
1783
+ });
1789
1784
  //#endregion
1790
1785
  //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts
1791
1786
  const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1795,28 +1790,28 @@ const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$
1795
1790
  })), zValidator$1("query", z.object({
1796
1791
  cron: z.string(),
1797
1792
  prompt: z.string(),
1798
- enabled: z.enum(["true", "false"]).optional(),
1793
+ enabled: booleanFlag,
1799
1794
  "catchup-policy": scheduleCatchupPolicySchema.optional()
1800
1795
  })), async (c) => {
1801
1796
  const param = c.req.valid("param");
1802
1797
  const query = c.req.valid("query");
1803
1798
  const funnel = c.env.funnel;
1804
- const entry = funnel.channels.addScheduleEntry(param.channel, param.connector, {
1799
+ const entry = scheduleEntrySchema.parse(funnel.channels.connectorOp(param.channel, param.connector, "addEntry", {
1805
1800
  id: param.id,
1806
1801
  cron: query.cron,
1807
1802
  prompt: query.prompt,
1808
- ...query.enabled !== void 0 ? { enabled: query.enabled === "true" } : {},
1803
+ ...query.enabled !== void 0 ? { enabled: query.enabled } : {},
1809
1804
  ...query["catchup-policy"] !== void 0 ? { catchupPolicy: query["catchup-policy"] } : {}
1810
- });
1805
+ }));
1811
1806
  await funnel.listeners.restart(param.channel, param.connector);
1812
1807
  return c.text(`added schedule entry "${entry.id}"`);
1813
1808
  });
1814
1809
  //#endregion
1815
1810
  //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.ts
1816
- const help$10 = `funnel channels <ch> connectors <conn> schedules remove <id>
1811
+ const help$8 = `funnel channels <ch> connectors <conn> schedules remove <id>
1817
1812
 
1818
1813
  usage: funnel channels <ch> connectors <conn> schedules remove <id>`;
1819
- const channelsConnectorSchedulesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$10));
1814
+ const channelsConnectorSchedulesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$8));
1820
1815
  //#endregion
1821
1816
  //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts
1822
1817
  const channelsConnectorsSchedulesRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1826,13 +1821,13 @@ const channelsConnectorsSchedulesRemoveHandler = factory.createHandlers(zValidat
1826
1821
  })), zValidator$1("query", z.object({})), async (c) => {
1827
1822
  const param = c.req.valid("param");
1828
1823
  const funnel = c.env.funnel;
1829
- funnel.channels.removeScheduleEntry(param.channel, param.connector, param.id);
1824
+ funnel.channels.connectorOp(param.channel, param.connector, "removeEntry", { id: param.id });
1830
1825
  await funnel.listeners.restart(param.channel, param.connector);
1831
1826
  return c.text(`removed schedule entry "${param.id}"`);
1832
1827
  });
1833
1828
  //#endregion
1834
1829
  //#region lib/cli/routes/channels.publish.ts
1835
- const help$9 = `funnel channels <channel> publish — push arbitrary content into a channel
1830
+ const help$7 = `funnel channels <channel> publish — push arbitrary content into a channel
1836
1831
 
1837
1832
  usage: funnel channels <channel> publish --content="<text>" [--connector=<name>] [--meta-<key>=<value> ...]
1838
1833
 
@@ -1840,7 +1835,7 @@ options:
1840
1835
  --content Required. The event body delivered to subscribers.
1841
1836
  --connector Optional. Stamp the event with a connector name (resolved to id when found).
1842
1837
  --meta-<key> Optional. Repeatable. Added to meta. Example: --meta-source=cron`;
1843
- const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$9));
1838
+ const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$7));
1844
1839
  //#endregion
1845
1840
  //#region lib/cli/routes/channels.$channel.publish.ts
1846
1841
  const querySchema = z.object({
@@ -1864,7 +1859,7 @@ const channelsPublishHandler = factory.createHandlers(zValidator$1("param", z.ob
1864
1859
  });
1865
1860
  //#endregion
1866
1861
  //#region lib/cli/routes/channels.remove.ts
1867
- const help$8 = `funnel channels remove — remove a channel and all its connectors
1862
+ const help$6 = `funnel channels remove — remove a channel and all its connectors
1868
1863
 
1869
1864
  usage: funnel channels remove <name>
1870
1865
 
@@ -1876,21 +1871,28 @@ examples:
1876
1871
  funnel channels remove staging
1877
1872
 
1878
1873
  see also: funnel channels, funnel channels add`;
1879
- const channelsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$8));
1874
+ const channelsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$6));
1880
1875
  //#endregion
1881
1876
  //#region lib/cli/routes/channels.remove.$channel.ts
1882
1877
  const channelsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({})), (c) => {
1883
1878
  const param = c.req.valid("param");
1884
- c.env.funnel.channels.remove(param.channel);
1879
+ const funnel = c.env.funnel;
1880
+ if (!funnel.channels.get(param.channel)) throw new HTTPException(404, { message: notFoundMessage({
1881
+ kind: "channel",
1882
+ name: param.channel,
1883
+ available: funnel.channels.list().map((ch) => ch.name),
1884
+ nextAction: "fnl channels add <name>"
1885
+ }) });
1886
+ funnel.channels.remove(param.channel);
1885
1887
  return c.text(`removed channel "${param.channel}"`);
1886
1888
  });
1887
1889
  //#endregion
1888
1890
  //#region lib/cli/routes/channels.rename.ts
1889
- const help$7 = `funnel channels rename — rename a channel
1891
+ const channelsRenameHelp = `funnel channels rename — rename a channel
1890
1892
 
1891
1893
  usage:
1892
- funnel channels rename <old> <new>
1893
- funnel channels <old> rename <new>
1894
+ funnel channels rename <old-channel-name> <new-channel-name>
1895
+ funnel channels <old-channel-name> rename <new-channel-name>
1894
1896
 
1895
1897
  Renames the channel in the configuration file. Connectors, schedules,
1896
1898
  and delivery mode are preserved. The gateway picks up the new name on
@@ -1901,15 +1903,10 @@ examples:
1901
1903
  funnel channels staging rename production
1902
1904
 
1903
1905
  see also: funnel channels, funnel channels <name>`;
1904
- const channelsRenameHelpHandler = factory.createHandlers((c) => c.text(help$7));
1906
+ const channelsRenameHelpHandler = factory.createHandlers((c) => c.text(channelsRenameHelp));
1905
1907
  //#endregion
1906
1908
  //#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));
1909
+ const channelsChannelRenameHelpHandler = factory.createHandlers((c) => c.text(channelsRenameHelp));
1913
1910
  //#endregion
1914
1911
  //#region lib/cli/routes/channels.$channel.rename.$newName.ts
1915
1912
  const channelsRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1917,7 +1914,14 @@ const channelsRenameHandler = factory.createHandlers(zValidator$1("param", z.obj
1917
1914
  newName: z.string()
1918
1915
  })), zValidator$1("query", z.object({})), (c) => {
1919
1916
  const param = c.req.valid("param");
1920
- c.env.funnel.channels.rename(param.channel, param.newName);
1917
+ const funnel = c.env.funnel;
1918
+ if (!funnel.channels.get(param.channel)) throw new HTTPException(404, { message: notFoundMessage({
1919
+ kind: "channel",
1920
+ name: param.channel,
1921
+ available: funnel.channels.list().map((ch) => ch.name),
1922
+ nextAction: "fnl channels add <name>"
1923
+ }) });
1924
+ funnel.channels.rename(param.channel, param.newName);
1921
1925
  return c.text(`renamed channel "${param.channel}" to "${param.newName}"`);
1922
1926
  });
1923
1927
  const channelsSetDeliveryHandler = factory.createHandlers(helpGuard(`funnel channels <name> set delivery <mode> — change a channel's routing mode
@@ -1947,8 +1951,14 @@ subcommands:
1947
1951
 
1948
1952
  output / valid YAML`), (c) => {
1949
1953
  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` });
1954
+ const funnel = c.env.funnel;
1955
+ const channel = funnel.channels.get(param.channel);
1956
+ if (!channel) throw new HTTPException(404, { message: notFoundMessage({
1957
+ kind: "channel",
1958
+ name: param.channel,
1959
+ available: funnel.channels.list().map((ch) => ch.name),
1960
+ nextAction: "fnl channels add <name>"
1961
+ }) });
1952
1962
  return c.text(renderYaml({
1953
1963
  id: channel.id,
1954
1964
  name: channel.name,
@@ -2069,8 +2079,14 @@ const validateConnector = (connector) => {
2069
2079
  };
2070
2080
  const channelsValidateHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({})), (c) => {
2071
2081
  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` });
2082
+ const funnel = c.env.funnel;
2083
+ const channel = funnel.channels.get(param.channel);
2084
+ if (!channel) throw new HTTPException(404, { message: notFoundMessage({
2085
+ kind: "channel",
2086
+ name: param.channel,
2087
+ available: funnel.channels.list().map((ch) => ch.name),
2088
+ nextAction: "fnl channels add <name>"
2089
+ }) });
2074
2090
  if (channel.connectors.length === 0) return c.text(renderYaml({
2075
2091
  channel: channel.name,
2076
2092
  valid: false,
@@ -2141,7 +2157,12 @@ const claudeHandler = factory.createHandlers(helpGuard(claudeHelp), zValidator$1
2141
2157
  }
2142
2158
  if (query.profile) {
2143
2159
  const profile = profiles.get(query.profile);
2144
- if (!profile) throw new HTTPException(404, { message: `profile "${query.profile}" not found` });
2160
+ if (!profile) throw new HTTPException(404, { message: notFoundMessage({
2161
+ kind: "profile",
2162
+ name: query.profile,
2163
+ available: profiles.list().map((p) => p.name),
2164
+ nextAction: "fnl profiles add <name> --path=<repo> --channel=<channel>"
2165
+ }) });
2145
2166
  const exitCode = await claude.launch({
2146
2167
  channel: profile.channelId,
2147
2168
  cwd: profile.path,
@@ -2274,16 +2295,12 @@ const resolveTargetChannel = (c, channelArg) => {
2274
2295
  };
2275
2296
  const debugHandler = factory.createHandlers(helpGuard(debugHelp), zValidator$1("query", z.object({
2276
2297
  channel: z.string().optional(),
2277
- all: z.enum([
2278
- "true",
2279
- "false",
2280
- ""
2281
- ]).optional(),
2298
+ all: booleanFlag,
2282
2299
  limit: z.string().optional()
2283
2300
  })), async (c) => {
2284
2301
  const query = c.req.valid("query");
2285
2302
  const funnel = c.env.funnel;
2286
- if (query.all === "true" || query.all === "") {
2303
+ if (query.all === true) {
2287
2304
  const report = await funnel.diagnostics.diagnoseAll();
2288
2305
  return c.text(renderYaml(report));
2289
2306
  }
@@ -2394,20 +2411,12 @@ examples:
2394
2411
  funnel doctor
2395
2412
  funnel doctor --fix
2396
2413
  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()
2414
+ fix: booleanFlag,
2415
+ aggressive: booleanFlag
2407
2416
  })), async (c) => {
2408
2417
  const query = c.req.valid("query");
2409
- const wantsFix = query.fix === "true" || query.fix === "";
2410
- const wantsAggressive = query.aggressive === "true" || query.aggressive === "";
2418
+ const wantsFix = query.fix === true;
2419
+ const wantsAggressive = query.aggressive === true;
2411
2420
  const mode = wantsFix ? wantsAggressive ? "aggressive" : "safe" : "off";
2412
2421
  const report = await c.env.funnel.doctor.run(mode);
2413
2422
  return c.text(renderYaml(report));
@@ -2446,7 +2455,7 @@ examples:
2446
2455
  const renderGatewayStatus = async (c) => {
2447
2456
  const status = c.env.funnel.gateway.getStatus();
2448
2457
  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);
2458
+ const res = await fetch(`${gatewayLoopbackUrl(status.port)}/status`).catch(() => null);
2450
2459
  if (!res) return c.text(renderYaml({
2451
2460
  running: true,
2452
2461
  pid: status.pid,
@@ -2756,6 +2765,16 @@ examples:
2756
2765
  funnel gateway start --no-caffeine
2757
2766
 
2758
2767
  programmable: funnel.gateway.start({ caffeinate })`;
2768
+ const HEALTH_TIMEOUT_MS = 5e3;
2769
+ const HEALTH_POLL_INTERVAL_MS = 100;
2770
+ const waitForHealth = async (port) => {
2771
+ const deadline = Date.now() + HEALTH_TIMEOUT_MS;
2772
+ while (Date.now() < deadline) {
2773
+ if ((await fetch(`${gatewayLoopbackUrl(port)}/health`).catch(() => null))?.ok) return true;
2774
+ await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_INTERVAL_MS));
2775
+ }
2776
+ return false;
2777
+ };
2759
2778
  const gatewayStartHandler = factory.createHandlers(helpGuard(startHelp), zValidator$1("query", z.object({ "no-caffeine": z.string().optional() })), async (c) => {
2760
2779
  const query = c.req.valid("query");
2761
2780
  const funnel = c.env.funnel;
@@ -2763,8 +2782,10 @@ const gatewayStartHandler = factory.createHandlers(helpGuard(startHelp), zValida
2763
2782
  const status = funnel.gateway.getStatus();
2764
2783
  return c.text(`funnel gateway: already running (pid ${status.pid})`);
2765
2784
  }
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");
2785
+ 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`" });
2786
+ const status = funnel.gateway.getStatus();
2787
+ 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\``);
2788
+ return c.text(`funnel gateway: started (pid ${status.pid}, port ${status.port})`);
2768
2789
  });
2769
2790
  const gatewayStatusHandler = factory.createHandlers(helpGuard(`funnel gateway status / show gateway running status
2770
2791
 
@@ -2854,7 +2875,12 @@ const profilesAddHandler = factory.createHandlers(zValidator$1("param", z.object
2854
2875
  const funnel = c.env.funnel;
2855
2876
  const { profiles, claude } = c.env;
2856
2877
  const channel = funnel.channels.get(query.channel);
2857
- if (!channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
2878
+ if (!channel) throw new HTTPException(400, { message: notFoundMessage({
2879
+ kind: "channel",
2880
+ name: query.channel,
2881
+ available: funnel.channels.list().map((ch) => ch.name),
2882
+ nextAction: "fnl channels add <name>"
2883
+ }) });
2858
2884
  const recipe = parseProfileRecipe(query);
2859
2885
  profiles.add({
2860
2886
  name: param.profile,
@@ -2917,7 +2943,12 @@ const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.obj
2917
2943
  c.env.funnel;
2918
2944
  const { profiles, claude } = c.env;
2919
2945
  const profile = profiles.get(param.profile);
2920
- if (!profile) throw new HTTPException(404, { message: `profile "${param.profile}" not found` });
2946
+ if (!profile) throw new HTTPException(404, { message: notFoundMessage({
2947
+ kind: "profile",
2948
+ name: param.profile,
2949
+ available: profiles.list().map((p) => p.name),
2950
+ nextAction: "fnl profiles add <name> --path=<repo> --channel=<channel>"
2951
+ }) });
2921
2952
  const exitCode = await claude.launch({
2922
2953
  channel: profile.channelId,
2923
2954
  cwd: profile.path,
@@ -2977,7 +3008,12 @@ const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object
2977
3008
  const funnel = c.env.funnel;
2978
3009
  const { profiles, claude } = c.env;
2979
3010
  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` });
3011
+ if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message: notFoundMessage({
3012
+ kind: "channel",
3013
+ name: query.channel,
3014
+ available: funnel.channels.list().map((ch) => ch.name),
3015
+ nextAction: "fnl channels add <name>"
3016
+ }) });
2981
3017
  const recipe = parseProfileRecipe(query);
2982
3018
  profiles.update(param.profile, {
2983
3019
  path: query.path,
@@ -3053,7 +3089,7 @@ const statusHelp = `funnel status / overall health snapshot
3053
3089
  usage / funnel status [--watch] [--interval <N>]
3054
3090
 
3055
3091
  options:
3056
- --watch / continuously refresh (Ctrl+C to stop)
3092
+ --watch / continuously refresh (default: off; Ctrl+C to stop)
3057
3093
  --interval <N> / polling interval in seconds (default 3)
3058
3094
 
3059
3095
  output / valid YAML
@@ -3078,7 +3114,7 @@ const buildStatusReport = async (funnel, profiles) => {
3078
3114
  const gatewayStatus = funnel.gateway.getStatus();
3079
3115
  let gatewayData = null;
3080
3116
  if (gatewayStatus.running) {
3081
- const res = await fetch(`http://127.0.0.1:${gatewayStatus.port}/status`).catch(() => null);
3117
+ const res = await fetch(`${gatewayLoopbackUrl(gatewayStatus.port)}/status`).catch(() => null);
3082
3118
  if (res && res.ok) {
3083
3119
  const body = await res.json();
3084
3120
  if (isGatewayStatus(body)) gatewayData = body;
@@ -3099,6 +3135,7 @@ const buildStatusReport = async (funnel, profiles) => {
3099
3135
  return {
3100
3136
  gateway: gatewayStatus.running ? {
3101
3137
  running: true,
3138
+ responsive: gatewayData !== null,
3102
3139
  pid: gatewayStatus.pid,
3103
3140
  port: gatewayStatus.port,
3104
3141
  uptimeMs: gatewayData?.uptimeMs ?? null
@@ -3125,16 +3162,12 @@ const buildStatusReport = async (funnel, profiles) => {
3125
3162
  };
3126
3163
  };
3127
3164
  const statusHandler = factory.createHandlers(helpGuard(statusHelp), zValidator$1("query", z.object({
3128
- watch: z.enum([
3129
- "true",
3130
- "false",
3131
- ""
3132
- ]).optional(),
3165
+ watch: booleanFlag,
3133
3166
  interval: z.string().optional()
3134
3167
  })), async (c) => {
3135
3168
  const query = c.req.valid("query");
3136
3169
  const funnel = c.env.funnel;
3137
- const isWatch = query.watch === "true" || query.watch === "";
3170
+ const isWatch = query.watch === true;
3138
3171
  const intervalSec = Math.min(60, Math.max(1, query.interval ? Number(query.interval) : 3));
3139
3172
  if (!isWatch) {
3140
3173
  const report = await buildStatusReport(funnel, c.env.profiles);
@@ -3185,4 +3218,4 @@ const routes = factory.createApp().onError((error, c) => {
3185
3218
  return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
3186
3219
  }).get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...channelsAddHelpHandler).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...channelsRemoveHelpHandler).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...channelsRenameHelpHandler).post("/channels/:channel/rename", ...channelsChannelRenameHelpHandler).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...channelsPublishHelpHandler).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel/validate", ...channelsValidateHandler).get("/channels/validate", ...channelsValidateHelpHandler).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...channelsConnectorsAddHelpHandler).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...channelsConnectorsRemoveHelpHandler).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...channelsConnectorsSetHelpHandler).post("/channels/:channel/connectors/set/:connector", ...channelsConnectorsSetHandler).post("/channels/:channel/connectors/rename/:connector/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/:connector/rename/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/rename", ...channelsConnectorsRenameHelpHandler).post("/channels/:channel/connectors/:connector/rename", ...channelsConnectorRenameHelpHandler).post("/channels/:channel/connectors/:connector/request", ...channelsConnectorsRequestHandler).get("/channels/:channel/connectors/:connector", ...channelsConnectorsShowHandler).get("/channels/:channel/connectors/:connector/schedules", ...channelsConnectorsSchedulesGroupHandler).post("/channels/:channel/connectors/:connector/schedules/add", ...channelsConnectorSchedulesAddHelpHandler).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...channelsConnectorSchedulesRemoveHelpHandler).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...profilesAddHelpHandler).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...profilesSetHelpHandler).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...profilesRemoveHelpHandler).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...profilesRenameHelpHandler).post("/profiles/:profile/rename", ...profilesProfileRenameHelpHandler).post("/profiles/:profile/as-default", ...profilesAsDefaultHandler).get("/profiles/:profile/run", ...profilesLaunchHandler).get("/profiles/:profile", ...profilesLaunchHandler).get("/gateway", ...gatewayGroupHandler).get("/gateway/status", ...gatewayStatusHandler).get("/gateway/start", ...gatewayStartHandler).get("/gateway/stop", ...gatewayStopHandler).get("/gateway/restart", ...gatewayRestartHandler).get("/gateway/run", ...gatewayRunHandler).get("/gateway/logs", ...gatewayLogsHandler).get("/gateway/sql", ...gatewaySqlHandler).get("/gateway/listeners", ...gatewayListenersHandler).get("/debug", ...debugHandler).get("/debug/events", ...debugEventsHandler).get("/debug/dropped", ...debugDroppedHandler).get("/debug/errors", ...debugErrorsHandler).get("/debug/replay", ...debugReplayHandler).get("/docs", ...docsIndexHandler).get("/docs/:topic", ...docsTopicHandler).get("/doctor", ...doctorHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
3187
3220
  //#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 };
3221
+ export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClock, FunnelConnectorAdapter, FunnelConnectorListener, FunnelConnectorRegistry, FunnelDiagnostics, FunnelDocs, FunnelDoctor, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelHttpClient, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLogger, FunnelProcessRunner, FunnelRecovery, FunnelSettingsReader, FunnelSettingsStore, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelHttpClient, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelHttpClient, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, baseConnectorConfigSchema, buildServiceRoutes, channelConfigSchema, channelDeliveryModeSchema, channelWsProtocols, channelWsUrl, routes as cliRoutes, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, createSettings, factory, funnelEventSchema, gatewayLoopbackUrl, previewOf, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryRows, queryToCliArgs, resolveFunnelDir, resolveFunnelPort, settingsSchema, toDiagnosticConnectionError, toDiagnosticEvent, toRequest };