@interactive-inc/claude-funnel 0.59.0 → 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 +524 -449
  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-B3IFCj_G.d.ts → file-process-guard-DOlCr4GF.d.ts} +5 -6
  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 +282 -209
  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-Dsx6AmhH.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 +383 -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-5LzwJ_F7.js → memory-diagnostic-log-CI60kNfB.js} +33 -18
  47. package/dist/{memory-token-prompter-BlFwK9k7.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-DUcZgN2Q.js → node-process-runner-DxTvycoK.js} +35 -13
  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-6foMXfFf.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-C9Hhjk-0.js → yaml-render-qW34NlYz.js} +43 -10
  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-Conbxl5O.d.ts +0 -3595
  70. package/dist/local-config-sync--f739oCJ.d.ts +0 -401
  71. package/dist/process-runner-Cx5O_fTf.d.ts +0 -49
  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-DUcZgN2Q.js";
6
- import { n as FunnelGhAdapter, t as FunnelGhListener } from "./gh-listener-Dsx6AmhH.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-6foMXfFf.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-C9Hhjk-0.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-5LzwJ_F7.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
  };
@@ -596,6 +487,7 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
596
487
  syncHandler = () => empty;
597
488
  aliveStub = null;
598
489
  listStub = null;
490
+ startTimeStub = null;
599
491
  on(handler) {
600
492
  this.handler = handler;
601
493
  return this;
@@ -612,6 +504,10 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
612
504
  this.listStub = stub;
613
505
  return this;
614
506
  }
507
+ onGetStartTime(stub) {
508
+ this.startTimeStub = stub;
509
+ return this;
510
+ }
615
511
  async run(command, options = {}) {
616
512
  this.calls.push({
617
513
  kind: "run",
@@ -681,6 +577,10 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
681
577
  if (this.listStub) return this.listStub(marker);
682
578
  return [];
683
579
  }
580
+ getStartTime(pid) {
581
+ if (this.startTimeStub) return this.startTimeStub(pid);
582
+ return null;
583
+ }
684
584
  };
685
585
  //#endregion
686
586
  //#region lib/engine/settings/mock-settings-reader.ts
@@ -1017,7 +917,9 @@ const noopOnError = () => {};
1017
917
  *
1018
918
  * @example
1019
919
  * ```ts
1020
- * const funnel = new Funnel({})
920
+ * import { slackConnector } from "@interactive-inc/claude-funnel/connectors/slack"
921
+ *
922
+ * const funnel = new Funnel({ connectors: [slackConnector()] })
1021
923
  * const channel = funnel.channels.add({ name: "inbox" })
1022
924
  * funnel.channels.addConnector("inbox", { type: "slack", name: "ops", botToken, appToken })
1023
925
  * await funnel.gatewayServer({ port: 9742 }).start()
@@ -1065,20 +967,25 @@ var Funnel = class Funnel {
1065
967
  fs,
1066
968
  idGenerator
1067
969
  });
1068
- const factory = new FunnelConnectorFactory({
970
+ const registry = new FunnelConnectorRegistry({
971
+ descriptors: props.connectors ?? [],
1069
972
  fs,
1070
973
  process,
1071
974
  logger: this.logger,
1072
975
  diagnosticLog: props.diagnosticLog,
1073
- dir,
1074
- slackListenerOptions: props.slackListenerOptions,
1075
- scheduleListenerOptions: props.scheduleListenerOptions
976
+ dir
977
+ });
978
+ this.profiles = new FunnelProfiles({
979
+ store,
980
+ idGenerator,
981
+ fs
1076
982
  });
1077
983
  this.channels = new FunnelChannels({
1078
984
  store,
1079
- factory,
985
+ registry,
1080
986
  clock,
1081
- idGenerator
987
+ idGenerator,
988
+ profileChecker: this.profiles
1082
989
  });
1083
990
  this.gateway = new FunnelGateway({
1084
991
  fs,
@@ -1103,11 +1010,6 @@ var Funnel = class Funnel {
1103
1010
  getToken: () => this.gatewayToken.read()
1104
1011
  });
1105
1012
  const mcp = new FunnelMcp({ fs });
1106
- this.profiles = new FunnelProfiles({
1107
- store,
1108
- idGenerator,
1109
- fs
1110
- });
1111
1013
  this.localConfig = new FunnelLocalConfig({ fs });
1112
1014
  this.localConfigSync = new FunnelLocalConfigSync({
1113
1015
  channels: this.channels,
@@ -1213,10 +1115,32 @@ var Funnel = class Funnel {
1213
1115
  }
1214
1116
  gatewayClient() {
1215
1117
  const { port } = this.gateway.getStatus();
1216
- return hc(`http://127.0.0.1:${port}`);
1118
+ return hc(gatewayLoopbackUrl(port));
1217
1119
  }
1218
1120
  };
1219
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
1220
1144
  //#region lib/engine/logger/node-logger.ts
1221
1145
  const defaultLogFile = () => join(funnelTmpDir(), "funnel.log");
1222
1146
  var NodeFunnelLogger = class extends FunnelLogger {
@@ -1245,7 +1169,7 @@ var NodeFunnelLogger = class extends FunnelLogger {
1245
1169
  message,
1246
1170
  ...meta ? { meta } : {}
1247
1171
  };
1248
- appendFileSync(this.file, `${JSON.stringify(entry)}\n`);
1172
+ appendFileSync(this.file, `${redactSecrets(JSON.stringify(entry))}\n`);
1249
1173
  }
1250
1174
  };
1251
1175
  //#endregion
@@ -1390,13 +1314,19 @@ const toRequest = (args) => {
1390
1314
  while (i < args.length) {
1391
1315
  const arg = args[i];
1392
1316
  if (arg.startsWith("--")) {
1393
- 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
+ }
1394
1324
  const next = args[i + 1];
1395
1325
  if (isValue(next)) {
1396
- params.set(key, next);
1326
+ params.set(body, next);
1397
1327
  i += 2;
1398
1328
  } else {
1399
- params.set(key, "true");
1329
+ params.set(body, "true");
1400
1330
  i++;
1401
1331
  }
1402
1332
  continue;
@@ -1462,7 +1392,7 @@ const queryToCliArgs = (url, reservedKeys = []) => {
1462
1392
  };
1463
1393
  //#endregion
1464
1394
  //#region lib/cli/routes/channels.add.ts
1465
- const help$17 = `funnel channels add — add a channel
1395
+ const help$15 = `funnel channels add — add a channel
1466
1396
 
1467
1397
  usage: funnel channels add <name> [--delivery fanout|exclusive]
1468
1398
 
@@ -1480,7 +1410,29 @@ examples:
1480
1410
  funnel channels add ci-events --delivery exclusive
1481
1411
 
1482
1412
  see also: funnel channels, funnel channels <name> connectors add`;
1483
- 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
+ });
1484
1436
  //#endregion
1485
1437
  //#region lib/cli/routes/channels.add.$channel.ts
1486
1438
  const channelsAddHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({ delivery: channelDeliveryModeSchema.optional() })), (c) => {
@@ -1493,6 +1445,17 @@ const channelsAddHandler = factory.createHandlers(zValidator$1("param", z.object
1493
1445
  return c.text(`added channel "${created.name}" (id: ${created.id})`);
1494
1446
  });
1495
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
1496
1459
  //#region lib/cli/router/help-guard.ts
1497
1460
  function helpGuard(help) {
1498
1461
  return async (c, next) => {
@@ -1517,14 +1480,20 @@ subcommands:
1517
1480
  <c> schedules add <id> --cron=... --prompt=... / add a schedule entry
1518
1481
  <c> schedules remove <id> / remove a schedule entry`), (c) => {
1519
1482
  const param = c.req.valid("param");
1520
- const channel = c.env.funnel.channels.get(param.channel);
1521
- 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
+ }) });
1522
1491
  if (channel.connectors.length === 0) return c.text(`no connectors in channel "${channel.name}"`);
1523
1492
  return c.text(channel.connectors.map((c) => `${c.name} (${c.type}, id: ${c.id})`).join("\n"));
1524
1493
  });
1525
1494
  //#endregion
1526
1495
  //#region lib/cli/routes/channels.$channel.connectors.add.ts
1527
- 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
1528
1497
 
1529
1498
  usage:
1530
1499
  funnel channels <channel> connectors add <name> --type=slack --bot-token=xoxb-... --app-token=xapp-...
@@ -1547,7 +1516,7 @@ examples:
1547
1516
  funnel channels alerts connectors add daily --type=schedule
1548
1517
 
1549
1518
  see also: funnel channels <channel> connectors, funnel channels <channel> connectors remove`;
1550
- const channelsConnectorsAddHelpHandler = factory.createHandlers((c) => c.text(help$16));
1519
+ const channelsConnectorsAddHelpHandler = factory.createHandlers((c) => c.text(help$14));
1551
1520
  //#endregion
1552
1521
  //#region lib/cli/routes/channels.$channel.connectors.add.$connector.ts
1553
1522
  const slackBody = z.object({
@@ -1615,7 +1584,7 @@ const channelsConnectorsAddHandler = factory.createHandlers(zValidator$1("param"
1615
1584
  });
1616
1585
  //#endregion
1617
1586
  //#region lib/cli/routes/channels.$channel.connectors.remove.ts
1618
- 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
1619
1588
 
1620
1589
  usage: funnel channels <channel> connectors remove <connector>
1621
1590
 
@@ -1627,7 +1596,7 @@ examples:
1627
1596
  funnel channels production connectors remove slack-main
1628
1597
 
1629
1598
  see also: funnel channels <channel> connectors, funnel channels <channel> connectors add`;
1630
- const channelsConnectorsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$15));
1599
+ const channelsConnectorsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$13));
1631
1600
  //#endregion
1632
1601
  //#region lib/cli/routes/channels.$channel.connectors.remove.$connector.ts
1633
1602
  const channelsConnectorsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1642,13 +1611,13 @@ const channelsConnectorsRemoveHandler = factory.createHandlers(zValidator$1("par
1642
1611
  });
1643
1612
  //#endregion
1644
1613
  //#region lib/cli/routes/channels.$channel.connectors.set.ts
1645
- 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
1646
1615
 
1647
1616
  usage:
1648
1617
  funnel channels <ch> connectors set <conn> [--bot-token=...] [--app-token=...] # slack
1649
1618
  funnel channels <ch> connectors set <conn> [--bot-token=...] # discord
1650
1619
  funnel channels <ch> connectors set <conn> [--poll-interval=N] # gh`;
1651
- const channelsConnectorsSetHelpHandler = factory.createHandlers((c) => c.text(help$14));
1620
+ const channelsConnectorsSetHelpHandler = factory.createHandlers((c) => c.text(help$12));
1652
1621
  //#endregion
1653
1622
  //#region lib/cli/routes/channels.$channel.connectors.set.$connector.ts
1654
1623
  const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1663,7 +1632,12 @@ const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param"
1663
1632
  const query = c.req.valid("query");
1664
1633
  const funnel = c.env.funnel;
1665
1634
  const existing = funnel.channels.getConnector(param.channel, param.connector);
1666
- 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
+ }) });
1667
1641
  if (existing.type === "slack") funnel.channels.updateSlackConnector(param.channel, param.connector, {
1668
1642
  ...query["bot-token"] !== void 0 ? { botToken: query["bot-token"] } : {},
1669
1643
  ...query["app-token"] !== void 0 ? { appToken: query["app-token"] } : {}
@@ -1686,15 +1660,21 @@ subcommands:
1686
1660
 
1687
1661
  output / valid YAML`), (c) => {
1688
1662
  const param = c.req.valid("param");
1689
- const connector = c.env.funnel.channels.getConnector(param.channel, param.connector);
1690
- 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
+ }) });
1691
1671
  return c.text(renderYaml(connector));
1692
1672
  });
1693
1673
  //#endregion
1694
1674
  //#region lib/cli/routes/channels.$channel.connectors.rename.ts
1695
- 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
1696
1676
 
1697
- usage: funnel channels <channel> connectors rename <old> <new>
1677
+ usage: funnel channels <channel> connectors rename <old-connector-name> <new-connector-name>
1698
1678
 
1699
1679
  Renames the connector in the configuration file. Tokens, type, and
1700
1680
  schedules are preserved. The gateway picks up the new name on the
@@ -1704,7 +1684,7 @@ examples:
1704
1684
  funnel channels production connectors rename slack-1 slack-main
1705
1685
 
1706
1686
  see also: funnel channels <channel> connectors`;
1707
- const channelsConnectorsRenameHelpHandler = factory.createHandlers((c) => c.text(help$13));
1687
+ const channelsConnectorsRenameHelpHandler = factory.createHandlers((c) => c.text(help$11));
1708
1688
  //#endregion
1709
1689
  //#region lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts
1710
1690
  const channelsConnectorsRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1721,10 +1701,10 @@ const channelsConnectorsRenameHandler = factory.createHandlers(zValidator$1("par
1721
1701
  });
1722
1702
  //#endregion
1723
1703
  //#region lib/cli/routes/channels.$channel.connectors.$connector.rename.ts
1724
- const help$12 = `funnel channels <channel> connectors rename <connector> <new-name>
1704
+ const help$10 = `funnel channels <channel> connectors rename <connector> <new-name>
1725
1705
 
1726
1706
  usage: funnel channels <channel> connectors rename <connector> <new-name>`;
1727
- const channelsConnectorRenameHelpHandler = factory.createHandlers((c) => c.text(help$12));
1707
+ const channelsConnectorRenameHelpHandler = factory.createHandlers((c) => c.text(help$10));
1728
1708
  const channelsConnectorsRequestHandler = factory.createHandlers(zValidator$1("param", z.object({
1729
1709
  channel: z.string(),
1730
1710
  connector: z.string()
@@ -1767,16 +1747,39 @@ subcommands:
1767
1747
  add <id> --cron=... --prompt=... [--enabled=true] [--catchup-policy=latest|all|skip] / add entry
1768
1748
  remove <id> / remove entry`), (c) => {
1769
1749
  const param = c.req.valid("param");
1770
- 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));
1771
1752
  if (entries.length === 0) return c.text("no schedule entries");
1772
1753
  return c.text(entries.map((e) => `${e.id}\t${e.cron}\t${e.enabled ? "on" : "off"}\t${e.prompt}`).join("\n"));
1773
1754
  });
1774
1755
  //#endregion
1775
1756
  //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.ts
1776
- 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
1777
1758
 
1778
- usage: funnel channels <ch> connectors <conn> schedules add <id> --cron="*/5 * * * *" --prompt="..." [--enabled=true] [--catchup-policy=latest|all|skip]`;
1779
- 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
+ });
1780
1783
  //#endregion
1781
1784
  //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts
1782
1785
  const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1786,28 +1789,28 @@ const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$
1786
1789
  })), zValidator$1("query", z.object({
1787
1790
  cron: z.string(),
1788
1791
  prompt: z.string(),
1789
- enabled: z.enum(["true", "false"]).optional(),
1792
+ enabled: booleanFlag,
1790
1793
  "catchup-policy": scheduleCatchupPolicySchema.optional()
1791
1794
  })), async (c) => {
1792
1795
  const param = c.req.valid("param");
1793
1796
  const query = c.req.valid("query");
1794
1797
  const funnel = c.env.funnel;
1795
- const entry = funnel.channels.addScheduleEntry(param.channel, param.connector, {
1798
+ const entry = scheduleEntrySchema.parse(funnel.channels.connectorOp(param.channel, param.connector, "addEntry", {
1796
1799
  id: param.id,
1797
1800
  cron: query.cron,
1798
1801
  prompt: query.prompt,
1799
- ...query.enabled !== void 0 ? { enabled: query.enabled === "true" } : {},
1802
+ ...query.enabled !== void 0 ? { enabled: query.enabled } : {},
1800
1803
  ...query["catchup-policy"] !== void 0 ? { catchupPolicy: query["catchup-policy"] } : {}
1801
- });
1804
+ }));
1802
1805
  await funnel.listeners.restart(param.channel, param.connector);
1803
1806
  return c.text(`added schedule entry "${entry.id}"`);
1804
1807
  });
1805
1808
  //#endregion
1806
1809
  //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.ts
1807
- const help$10 = `funnel channels <ch> connectors <conn> schedules remove <id>
1810
+ const help$8 = `funnel channels <ch> connectors <conn> schedules remove <id>
1808
1811
 
1809
1812
  usage: funnel channels <ch> connectors <conn> schedules remove <id>`;
1810
- const channelsConnectorSchedulesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$10));
1813
+ const channelsConnectorSchedulesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$8));
1811
1814
  //#endregion
1812
1815
  //#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts
1813
1816
  const channelsConnectorsSchedulesRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1817,13 +1820,13 @@ const channelsConnectorsSchedulesRemoveHandler = factory.createHandlers(zValidat
1817
1820
  })), zValidator$1("query", z.object({})), async (c) => {
1818
1821
  const param = c.req.valid("param");
1819
1822
  const funnel = c.env.funnel;
1820
- funnel.channels.removeScheduleEntry(param.channel, param.connector, param.id);
1823
+ funnel.channels.connectorOp(param.channel, param.connector, "removeEntry", { id: param.id });
1821
1824
  await funnel.listeners.restart(param.channel, param.connector);
1822
1825
  return c.text(`removed schedule entry "${param.id}"`);
1823
1826
  });
1824
1827
  //#endregion
1825
1828
  //#region lib/cli/routes/channels.publish.ts
1826
- 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
1827
1830
 
1828
1831
  usage: funnel channels <channel> publish --content="<text>" [--connector=<name>] [--meta-<key>=<value> ...]
1829
1832
 
@@ -1831,7 +1834,7 @@ options:
1831
1834
  --content Required. The event body delivered to subscribers.
1832
1835
  --connector Optional. Stamp the event with a connector name (resolved to id when found).
1833
1836
  --meta-<key> Optional. Repeatable. Added to meta. Example: --meta-source=cron`;
1834
- const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$9));
1837
+ const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$7));
1835
1838
  //#endregion
1836
1839
  //#region lib/cli/routes/channels.$channel.publish.ts
1837
1840
  const querySchema = z.object({
@@ -1855,7 +1858,7 @@ const channelsPublishHandler = factory.createHandlers(zValidator$1("param", z.ob
1855
1858
  });
1856
1859
  //#endregion
1857
1860
  //#region lib/cli/routes/channels.remove.ts
1858
- 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
1859
1862
 
1860
1863
  usage: funnel channels remove <name>
1861
1864
 
@@ -1867,21 +1870,28 @@ examples:
1867
1870
  funnel channels remove staging
1868
1871
 
1869
1872
  see also: funnel channels, funnel channels add`;
1870
- const channelsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$8));
1873
+ const channelsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$6));
1871
1874
  //#endregion
1872
1875
  //#region lib/cli/routes/channels.remove.$channel.ts
1873
1876
  const channelsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({})), (c) => {
1874
1877
  const param = c.req.valid("param");
1875
- 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);
1876
1886
  return c.text(`removed channel "${param.channel}"`);
1877
1887
  });
1878
1888
  //#endregion
1879
1889
  //#region lib/cli/routes/channels.rename.ts
1880
- const help$7 = `funnel channels rename — rename a channel
1890
+ const channelsRenameHelp = `funnel channels rename — rename a channel
1881
1891
 
1882
1892
  usage:
1883
- funnel channels rename <old> <new>
1884
- 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>
1885
1895
 
1886
1896
  Renames the channel in the configuration file. Connectors, schedules,
1887
1897
  and delivery mode are preserved. The gateway picks up the new name on
@@ -1892,15 +1902,10 @@ examples:
1892
1902
  funnel channels staging rename production
1893
1903
 
1894
1904
  see also: funnel channels, funnel channels <name>`;
1895
- const channelsRenameHelpHandler = factory.createHandlers((c) => c.text(help$7));
1905
+ const channelsRenameHelpHandler = factory.createHandlers((c) => c.text(channelsRenameHelp));
1896
1906
  //#endregion
1897
1907
  //#region lib/cli/routes/channels.$channel.rename.ts
1898
- const help$6 = `funnel channels rename — rename a channel
1899
-
1900
- usage:
1901
- funnel channels rename <old> <new>
1902
- funnel channels <old> rename <new>`;
1903
- const channelsChannelRenameHelpHandler = factory.createHandlers((c) => c.text(help$6));
1908
+ const channelsChannelRenameHelpHandler = factory.createHandlers((c) => c.text(channelsRenameHelp));
1904
1909
  //#endregion
1905
1910
  //#region lib/cli/routes/channels.$channel.rename.$newName.ts
1906
1911
  const channelsRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
@@ -1908,7 +1913,14 @@ const channelsRenameHandler = factory.createHandlers(zValidator$1("param", z.obj
1908
1913
  newName: z.string()
1909
1914
  })), zValidator$1("query", z.object({})), (c) => {
1910
1915
  const param = c.req.valid("param");
1911
- 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);
1912
1924
  return c.text(`renamed channel "${param.channel}" to "${param.newName}"`);
1913
1925
  });
1914
1926
  const channelsSetDeliveryHandler = factory.createHandlers(helpGuard(`funnel channels <name> set delivery <mode> — change a channel's routing mode
@@ -1938,8 +1950,14 @@ subcommands:
1938
1950
 
1939
1951
  output / valid YAML`), (c) => {
1940
1952
  const param = c.req.valid("param");
1941
- const channel = c.env.funnel.channels.get(param.channel);
1942
- 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
+ }) });
1943
1961
  return c.text(renderYaml({
1944
1962
  id: channel.id,
1945
1963
  name: channel.name,
@@ -2060,8 +2078,14 @@ const validateConnector = (connector) => {
2060
2078
  };
2061
2079
  const channelsValidateHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({})), (c) => {
2062
2080
  const param = c.req.valid("param");
2063
- const channel = c.env.funnel.channels.get(param.channel);
2064
- 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
+ }) });
2065
2089
  if (channel.connectors.length === 0) return c.text(renderYaml({
2066
2090
  channel: channel.name,
2067
2091
  valid: false,
@@ -2132,7 +2156,12 @@ const claudeHandler = factory.createHandlers(helpGuard(claudeHelp), zValidator$1
2132
2156
  }
2133
2157
  if (query.profile) {
2134
2158
  const profile = profiles.get(query.profile);
2135
- 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
+ }) });
2136
2165
  const exitCode = await claude.launch({
2137
2166
  channel: profile.channelId,
2138
2167
  cwd: profile.path,
@@ -2265,16 +2294,12 @@ const resolveTargetChannel = (c, channelArg) => {
2265
2294
  };
2266
2295
  const debugHandler = factory.createHandlers(helpGuard(debugHelp), zValidator$1("query", z.object({
2267
2296
  channel: z.string().optional(),
2268
- all: z.enum([
2269
- "true",
2270
- "false",
2271
- ""
2272
- ]).optional(),
2297
+ all: booleanFlag,
2273
2298
  limit: z.string().optional()
2274
2299
  })), async (c) => {
2275
2300
  const query = c.req.valid("query");
2276
2301
  const funnel = c.env.funnel;
2277
- if (query.all === "true" || query.all === "") {
2302
+ if (query.all === true) {
2278
2303
  const report = await funnel.diagnostics.diagnoseAll();
2279
2304
  return c.text(renderYaml(report));
2280
2305
  }
@@ -2385,20 +2410,12 @@ examples:
2385
2410
  funnel doctor
2386
2411
  funnel doctor --fix
2387
2412
  funnel doctor --fix --aggressive`), zValidator$1("query", z.object({
2388
- fix: z.enum([
2389
- "true",
2390
- "false",
2391
- ""
2392
- ]).optional(),
2393
- aggressive: z.enum([
2394
- "true",
2395
- "false",
2396
- ""
2397
- ]).optional()
2413
+ fix: booleanFlag,
2414
+ aggressive: booleanFlag
2398
2415
  })), async (c) => {
2399
2416
  const query = c.req.valid("query");
2400
- const wantsFix = query.fix === "true" || query.fix === "";
2401
- const wantsAggressive = query.aggressive === "true" || query.aggressive === "";
2417
+ const wantsFix = query.fix === true;
2418
+ const wantsAggressive = query.aggressive === true;
2402
2419
  const mode = wantsFix ? wantsAggressive ? "aggressive" : "safe" : "off";
2403
2420
  const report = await c.env.funnel.doctor.run(mode);
2404
2421
  return c.text(renderYaml(report));
@@ -2437,7 +2454,7 @@ examples:
2437
2454
  const renderGatewayStatus = async (c) => {
2438
2455
  const status = c.env.funnel.gateway.getStatus();
2439
2456
  if (!status.running) return c.text(renderYaml({ running: false }), 503);
2440
- 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);
2441
2458
  if (!res) return c.text(renderYaml({
2442
2459
  running: true,
2443
2460
  pid: status.pid,
@@ -2747,6 +2764,16 @@ examples:
2747
2764
  funnel gateway start --no-caffeine
2748
2765
 
2749
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
+ };
2750
2777
  const gatewayStartHandler = factory.createHandlers(helpGuard(startHelp), zValidator$1("query", z.object({ "no-caffeine": z.string().optional() })), async (c) => {
2751
2778
  const query = c.req.valid("query");
2752
2779
  const funnel = c.env.funnel;
@@ -2754,8 +2781,10 @@ const gatewayStartHandler = factory.createHandlers(helpGuard(startHelp), zValida
2754
2781
  const status = funnel.gateway.getStatus();
2755
2782
  return c.text(`funnel gateway: already running (pid ${status.pid})`);
2756
2783
  }
2757
- if (!await funnel.gateway.start({ caffeinate: query["no-caffeine"] !== "true" })) throw new HTTPException(500, { message: "funnel gateway: failed to start" });
2758
- 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})`);
2759
2788
  });
2760
2789
  const gatewayStatusHandler = factory.createHandlers(helpGuard(`funnel gateway status / show gateway running status
2761
2790
 
@@ -2845,7 +2874,12 @@ const profilesAddHandler = factory.createHandlers(zValidator$1("param", z.object
2845
2874
  const funnel = c.env.funnel;
2846
2875
  const { profiles, claude } = c.env;
2847
2876
  const channel = funnel.channels.get(query.channel);
2848
- 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
+ }) });
2849
2883
  const recipe = parseProfileRecipe(query);
2850
2884
  profiles.add({
2851
2885
  name: param.profile,
@@ -2908,7 +2942,12 @@ const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.obj
2908
2942
  c.env.funnel;
2909
2943
  const { profiles, claude } = c.env;
2910
2944
  const profile = profiles.get(param.profile);
2911
- 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
+ }) });
2912
2951
  const exitCode = await claude.launch({
2913
2952
  channel: profile.channelId,
2914
2953
  cwd: profile.path,
@@ -2968,7 +3007,12 @@ const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object
2968
3007
  const funnel = c.env.funnel;
2969
3008
  const { profiles, claude } = c.env;
2970
3009
  const channel = query.channel !== void 0 ? funnel.channels.get(query.channel) : null;
2971
- 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
+ }) });
2972
3016
  const recipe = parseProfileRecipe(query);
2973
3017
  profiles.update(param.profile, {
2974
3018
  path: query.path,
@@ -3044,7 +3088,7 @@ const statusHelp = `funnel status / overall health snapshot
3044
3088
  usage / funnel status [--watch] [--interval <N>]
3045
3089
 
3046
3090
  options:
3047
- --watch / continuously refresh (Ctrl+C to stop)
3091
+ --watch / continuously refresh (default: off; Ctrl+C to stop)
3048
3092
  --interval <N> / polling interval in seconds (default 3)
3049
3093
 
3050
3094
  output / valid YAML
@@ -3069,7 +3113,7 @@ const buildStatusReport = async (funnel, profiles) => {
3069
3113
  const gatewayStatus = funnel.gateway.getStatus();
3070
3114
  let gatewayData = null;
3071
3115
  if (gatewayStatus.running) {
3072
- 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);
3073
3117
  if (res && res.ok) {
3074
3118
  const body = await res.json();
3075
3119
  if (isGatewayStatus(body)) gatewayData = body;
@@ -3090,6 +3134,7 @@ const buildStatusReport = async (funnel, profiles) => {
3090
3134
  return {
3091
3135
  gateway: gatewayStatus.running ? {
3092
3136
  running: true,
3137
+ responsive: gatewayData !== null,
3093
3138
  pid: gatewayStatus.pid,
3094
3139
  port: gatewayStatus.port,
3095
3140
  uptimeMs: gatewayData?.uptimeMs ?? null
@@ -3116,16 +3161,12 @@ const buildStatusReport = async (funnel, profiles) => {
3116
3161
  };
3117
3162
  };
3118
3163
  const statusHandler = factory.createHandlers(helpGuard(statusHelp), zValidator$1("query", z.object({
3119
- watch: z.enum([
3120
- "true",
3121
- "false",
3122
- ""
3123
- ]).optional(),
3164
+ watch: booleanFlag,
3124
3165
  interval: z.string().optional()
3125
3166
  })), async (c) => {
3126
3167
  const query = c.req.valid("query");
3127
3168
  const funnel = c.env.funnel;
3128
- const isWatch = query.watch === "true" || query.watch === "";
3169
+ const isWatch = query.watch === true;
3129
3170
  const intervalSec = Math.min(60, Math.max(1, query.interval ? Number(query.interval) : 3));
3130
3171
  if (!isWatch) {
3131
3172
  const report = await buildStatusReport(funnel, c.env.profiles);
@@ -3176,4 +3217,4 @@ const routes = factory.createApp().onError((error, c) => {
3176
3217
  return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
3177
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);
3178
3219
  //#endregion
3179
- 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 };