@interactive-inc/claude-funnel 0.59.1 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/bin.js +549 -487
- package/dist/channels-2g_BU1N0.d.ts +174 -0
- package/dist/claude.d.ts +9 -5
- package/dist/claude.js +54 -17
- package/dist/{diagnostic-log-Cb3v8P7p.d.ts → connector-descriptor-6SXJoszo.d.ts} +158 -2
- package/dist/connectors/discord.d.ts +30 -4
- package/dist/connectors/discord.js +2 -2
- package/dist/connectors/gh.d.ts +21 -5
- package/dist/connectors/gh.js +3 -3
- package/dist/connectors/schedule.d.ts +124 -2
- package/dist/connectors/schedule.js +3 -3
- package/dist/connectors/slack.d.ts +149 -5
- package/dist/connectors/slack.js +2 -2
- package/dist/{diagnostic-sql-reader-CzYgZpq2.js → diagnostic-sql-reader-C9zR-Csp.js} +5 -5
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +1 -1
- package/dist/{discord-listener-CKsZGTnH.js → discord-connector-BL36yvbL.js} +60 -37
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +1 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +1 -1
- package/dist/error-message-of-Byi4y0Uf.js +9 -0
- package/dist/{file-process-guard-JhFpmHYo.d.ts → file-process-guard-DOlCr4GF.d.ts} +4 -5
- package/dist/{funnel-diagnostics-BpKYrMSu.js → funnel-diagnostics-CSiJmPlZ.js} +19 -2
- package/dist/{funnel-diagnostics-K-wON25Y.d.ts → funnel-diagnostics-DpXOsCty.d.ts} +3 -3
- package/dist/{funnel-docs-ng5K8w4j.js → funnel-docs-BxXZ9Ksx.js} +76 -3
- package/dist/{funnel-docs-DYBs1-H_.d.ts → funnel-docs-CNklHvbt.d.ts} +1 -1
- package/dist/{funnel-doctor-vxO96TCA.d.ts → funnel-doctor-CZf_0Luq.d.ts} +2 -2
- package/dist/{funnel-recovery-COExL9MD.d.ts → funnel-recovery-DnLrdWO9.d.ts} +1 -1
- package/dist/gateway/daemon.js +326 -266
- package/dist/gateway-base-url-Dy4Ykuoh.js +14 -0
- package/dist/gateway.d.ts +2 -2
- package/dist/gateway.js +2 -2
- package/dist/{gh-listener-B2I4s8qh.js → gh-connector-DpiixfQZ.js} +53 -5
- package/dist/gh-connector-schema-Rzwc1c1N.js +12 -0
- package/dist/http-client-oICicjuO.d.ts +18 -0
- package/dist/index-CgY8NdMz.d.ts +1057 -0
- package/dist/index.d.ts +1558 -17
- package/dist/index.js +374 -342
- package/dist/{local-config-json-schema-DE1zkMcb.js → local-config-json-schema-JyLqOQNX.js} +9 -5
- package/dist/local-config-sync-Dh1Croqe.d.ts +169 -0
- package/dist/local-config.d.ts +2 -2
- package/dist/local-config.js +2 -2
- package/dist/logger.js +1 -1
- package/dist/{memory-diagnostic-log-B9Us7X05.js → memory-diagnostic-log-CI60kNfB.js} +33 -18
- package/dist/{memory-token-prompter-CcShtF8B.d.ts → memory-token-prompter-B4sjyaAq.d.ts} +2 -2
- package/dist/{memory-token-prompter-C7vREzCL.js → memory-token-prompter-CZde7e6y.js} +1 -1
- package/dist/{node-file-system-BcrmWN9I.js → node-file-system-Blr8pAir.js} +1 -1
- package/dist/node-http-client-lowp60Oa.js +25 -0
- package/dist/{gh-connector-schema-ClPLSYD9.js → node-process-runner-DxTvycoK.js} +1 -12
- package/dist/{profiles-g2qGVOWv.d.ts → profiles-Cy5wXQ0L.d.ts} +3 -3
- package/dist/{profiles-MnXvYfZF.js → profiles-DSzTeKQw.js} +1 -1
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +1 -1
- package/dist/recovery.d.ts +1 -1
- package/dist/recovery.js +1 -1
- package/dist/{schedule-listener-DP9Jhc6U.js → schedule-connector-L4uzg5M8.js} +109 -9
- package/dist/{settings-reader-DPwqOVUm.d.ts → settings-reader-BIFB_j2f.d.ts} +1 -1
- package/dist/settings-schema-D1xcOqRu.d.ts +78 -0
- package/dist/{gateway-base-url-DxVjjDoW.js → settings-store-CUKSeTXC.js} +27 -29
- package/dist/{slack-listener-C4wlZaOq.js → slack-connector-DQIFPdBF.js} +67 -12
- package/dist/slot-fields-CMoRpwuy.js +45 -0
- package/dist/{yaml-render-cZu6CxkE.js → yaml-render-qW34NlYz.js} +4 -4
- package/package.json +1 -1
- package/dist/connector-adapter-DGacCppE.d.ts +0 -25
- package/dist/discord-connector-schema-CQyfDkLD.d.ts +0 -39
- package/dist/gh-connector-schema-CZzwzvqY.d.ts +0 -14
- package/dist/index-D7mjirUL.d.ts +0 -3602
- package/dist/local-config-sync-BGPAS9Be.d.ts +0 -401
- package/dist/process-runner-DIm1cy95.d.ts +0 -52
- package/dist/resolve-connector-token-CczqG_Ig.js +0 -22
- package/dist/schedule-listener-DoMPjHZj.d.ts +0 -112
- package/dist/settings-schema-1hh11jnN.d.ts +0 -152
- package/dist/slack-listener-Dj9NFbAJ.d.ts +0 -136
- /package/dist/{connector-adapter-qwXLjQId.js → connector-adapter-DU9Rvyec.js} +0 -0
- /package/dist/{connector-listener-CpHBecCj.js → connector-listener-DR3aKOuK.js} +0 -0
- /package/dist/{file-system-PWKKU7lA.js → file-system-Wvzc2ePY.js} +0 -0
- /package/dist/{file-system-DxpnnUVb.d.ts → file-system-o51IsM0W.d.ts} +0 -0
- /package/dist/{funnel-doctor-CApCezTq.js → funnel-doctor-DiJCjHsg.js} +0 -0
- /package/dist/{funnel-log-sqlite-sink-B_5_4ybn.js → funnel-log-sqlite-sink-kqJbx2H7.js} +0 -0
- /package/dist/{funnel-recovery-D9CxD5Zs.js → funnel-recovery-BFdPjL6Z.js} +0 -0
- /package/dist/{logger-BP6SisKt.js → logger-B6iyNbxM.js} +0 -0
- /package/dist/{schedule-connector-schema-B_xO5z5B.js → schedule-connector-schema-CfyuMCMh.js} +0 -0
- /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
|
|
2
|
-
import {
|
|
3
|
-
import { t as
|
|
4
|
-
import { t as FunnelLogger } from "./logger-
|
|
5
|
-
import { n as
|
|
6
|
-
import { n as
|
|
7
|
-
import { n as
|
|
8
|
-
import { t as
|
|
9
|
-
import { t as
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { t as
|
|
14
|
-
import {
|
|
15
|
-
import { t as
|
|
16
|
-
import { a as
|
|
17
|
-
import {
|
|
18
|
-
import { t as
|
|
19
|
-
import { t as
|
|
20
|
-
import { t as
|
|
21
|
-
import { a as FunnelLocalConfig, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigSync, t as funnelJsonSchema } from "./local-config-json-schema-DE1zkMcb.js";
|
|
22
|
-
import { t as FunnelProfiles } from "./profiles-MnXvYfZF.js";
|
|
23
|
-
import { t as FunnelRecovery } from "./funnel-recovery-D9CxD5Zs.js";
|
|
24
|
-
import { C as funnelTmpDir, S as publishResponseSchema, _ as funnelEventSchema, a as connectorConnectionEventSchema, b as FunnelChannelPublisher, c as MemoryFunnelEventLog, d as DEFAULT_GATEWAY_TOKEN_PATH, f as FunnelGatewayToken, g as FunnelEventLog, h as SqliteFunnelEventLog, i as ConnectorDiagnosticLog, l as channelWsProtocols, m as FunnelListenerSupervisor, n as SqliteConnectorDiagnosticLog, o as connectorProcessedEventSchema, p as FunnelGatewayServer, r as CONNECTOR_CONNECTION_STATUSES, s as connectorRawEventSchema, t as MemoryConnectorDiagnosticLog, u as channelWsUrl, v as FunnelBroadcaster, x as publishRequestSchema, y as requireBearerToken } from "./memory-diagnostic-log-B9Us7X05.js";
|
|
1
|
+
import { t as gatewayLoopbackUrl } from "./gateway-base-url-Dy4Ykuoh.js";
|
|
2
|
+
import { t as FunnelFileSystem } from "./file-system-Wvzc2ePY.js";
|
|
3
|
+
import { t as NodeFunnelFileSystem } from "./node-file-system-Blr8pAir.js";
|
|
4
|
+
import { t as FunnelLogger } from "./logger-B6iyNbxM.js";
|
|
5
|
+
import { n as FunnelProcessRunner, t as NodeFunnelProcessRunner } from "./node-process-runner-DxTvycoK.js";
|
|
6
|
+
import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-CtQ-Ix8_.js";
|
|
7
|
+
import { a as resolveFunnelDir, c as channelConfigSchema, d as settingsSchema, f as baseConnectorConfigSchema, i as SETTINGS_PATH, l as channelDeliveryModeSchema, n as FUNNEL_DIR, o as resolveFunnelPort, p as NodeFunnelIdGenerator, r as FunnelSettingsStore, s as SETTINGS_VERSION, t as DEFAULT_GATEWAY_PORT, u as profileConfigSchema } from "./settings-store-CUKSeTXC.js";
|
|
8
|
+
import { a as FunnelMcp, o as FileProcessGuard, s as FunnelClaude, t as renderYaml } from "./yaml-render-qW34NlYz.js";
|
|
9
|
+
import { a as toDiagnosticEvent, i as toDiagnosticConnectionError, n as previewOf, r as queryRows, t as FunnelDiagnostics } from "./funnel-diagnostics-CSiJmPlZ.js";
|
|
10
|
+
import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-C9zR-Csp.js";
|
|
11
|
+
import { t as FunnelDoctor } from "./funnel-doctor-DiJCjHsg.js";
|
|
12
|
+
import { t as FunnelDocs } from "./funnel-docs-BxXZ9Ksx.js";
|
|
13
|
+
import { a as FunnelLocalConfig, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigSync, t as funnelJsonSchema } from "./local-config-json-schema-JyLqOQNX.js";
|
|
14
|
+
import { t as FunnelProfiles } from "./profiles-DSzTeKQw.js";
|
|
15
|
+
import { t as FunnelRecovery } from "./funnel-recovery-BFdPjL6Z.js";
|
|
16
|
+
import { C as funnelTmpDir, S as publishResponseSchema, _ as funnelEventSchema, a as connectorConnectionEventSchema, b as FunnelChannelPublisher, c as MemoryFunnelEventLog, d as DEFAULT_GATEWAY_TOKEN_PATH, f as FunnelGatewayToken, g as FunnelEventLog, h as SqliteFunnelEventLog, i as ConnectorDiagnosticLog, l as channelWsProtocols, m as FunnelListenerSupervisor, n as SqliteConnectorDiagnosticLog, o as connectorProcessedEventSchema, p as FunnelGatewayServer, r as CONNECTOR_CONNECTION_STATUSES, s as connectorRawEventSchema, t as MemoryConnectorDiagnosticLog, u as channelWsUrl, v as FunnelBroadcaster, x as publishRequestSchema, y as requireBearerToken } from "./memory-diagnostic-log-CI60kNfB.js";
|
|
17
|
+
import { n as FunnelHttpClient, t as NodeFunnelHttpClient } from "./node-http-client-lowp60Oa.js";
|
|
18
|
+
import { t as FunnelConnectorAdapter } from "./connector-adapter-DU9Rvyec.js";
|
|
19
|
+
import { t as FunnelConnectorListener } from "./connector-listener-DR3aKOuK.js";
|
|
20
|
+
import { r as scheduleEntrySchema, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-CfyuMCMh.js";
|
|
25
21
|
import { dirname, join, resolve } from "node:path";
|
|
26
22
|
import { hc } from "hono/client";
|
|
27
23
|
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
@@ -29,80 +25,68 @@ import { z } from "zod";
|
|
|
29
25
|
import { fileURLToPath } from "node:url";
|
|
30
26
|
import { createFactory } from "hono/factory";
|
|
31
27
|
import { HTTPException } from "hono/http-exception";
|
|
32
|
-
import { zValidator
|
|
28
|
+
import { zValidator } from "@hono/zod-validator";
|
|
33
29
|
import { Hono } from "hono";
|
|
34
|
-
//#region lib/engine/connectors/connector-
|
|
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
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
|
43
|
-
*
|
|
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
|
|
42
|
+
var FunnelConnectorRegistry = class {
|
|
43
|
+
descriptors;
|
|
50
44
|
fs;
|
|
51
45
|
process;
|
|
52
46
|
logger;
|
|
53
47
|
diagnosticLog;
|
|
54
48
|
dir;
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
159
|
+
registry;
|
|
206
160
|
profileChecker;
|
|
207
161
|
clock;
|
|
208
162
|
idGenerator;
|
|
209
163
|
constructor(deps) {
|
|
210
164
|
this.store = deps.store;
|
|
211
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
414
|
-
const
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
1084
|
-
|
|
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
|
-
|
|
985
|
+
registry,
|
|
1089
986
|
clock,
|
|
1090
|
-
idGenerator
|
|
987
|
+
idGenerator,
|
|
988
|
+
profileChecker: this.profiles
|
|
1091
989
|
});
|
|
1092
990
|
this.gateway = new FunnelGateway({
|
|
1093
991
|
fs,
|
|
@@ -1112,11 +1010,6 @@ var Funnel = class Funnel {
|
|
|
1112
1010
|
getToken: () => this.gatewayToken.read()
|
|
1113
1011
|
});
|
|
1114
1012
|
const mcp = new FunnelMcp({ fs });
|
|
1115
|
-
this.profiles = new FunnelProfiles({
|
|
1116
|
-
store,
|
|
1117
|
-
idGenerator,
|
|
1118
|
-
fs
|
|
1119
|
-
});
|
|
1120
1013
|
this.localConfig = new FunnelLocalConfig({ fs });
|
|
1121
1014
|
this.localConfigSync = new FunnelLocalConfigSync({
|
|
1122
1015
|
channels: this.channels,
|
|
@@ -1222,10 +1115,32 @@ var Funnel = class Funnel {
|
|
|
1222
1115
|
}
|
|
1223
1116
|
gatewayClient() {
|
|
1224
1117
|
const { port } = this.gateway.getStatus();
|
|
1225
|
-
return hc(
|
|
1118
|
+
return hc(gatewayLoopbackUrl(port));
|
|
1226
1119
|
}
|
|
1227
1120
|
};
|
|
1228
1121
|
//#endregion
|
|
1122
|
+
//#region lib/engine/logger/redact-secrets.ts
|
|
1123
|
+
/**
|
|
1124
|
+
* Mask credential-shaped substrings before a log line is persisted. Matching
|
|
1125
|
+
* is prefix-anchored (Slack xoxb-/xapp-, GitHub ghp_/github_pat_, Discord
|
|
1126
|
+
* Bot tokens, HTTP bearer values) so ordinary identifiers never trip it —
|
|
1127
|
+
* a false negative only weakens defense in depth, a false positive destroys
|
|
1128
|
+
* a legitimate log line.
|
|
1129
|
+
*/
|
|
1130
|
+
const SECRET_PATTERNS = [
|
|
1131
|
+
/xox[abprs]-[A-Za-z0-9-]{10,}/g,
|
|
1132
|
+
/xapp-[A-Za-z0-9-]{10,}/g,
|
|
1133
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
1134
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
1135
|
+
/Bot [A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{20,}/g,
|
|
1136
|
+
/Bearer [A-Za-z0-9._~+/-]{16,}/g
|
|
1137
|
+
];
|
|
1138
|
+
const redactSecrets = (text) => {
|
|
1139
|
+
let redacted = text;
|
|
1140
|
+
for (const pattern of SECRET_PATTERNS) redacted = redacted.replace(pattern, "[redacted]");
|
|
1141
|
+
return redacted;
|
|
1142
|
+
};
|
|
1143
|
+
//#endregion
|
|
1229
1144
|
//#region lib/engine/logger/node-logger.ts
|
|
1230
1145
|
const defaultLogFile = () => join(funnelTmpDir(), "funnel.log");
|
|
1231
1146
|
var NodeFunnelLogger = class extends FunnelLogger {
|
|
@@ -1254,7 +1169,7 @@ var NodeFunnelLogger = class extends FunnelLogger {
|
|
|
1254
1169
|
message,
|
|
1255
1170
|
...meta ? { meta } : {}
|
|
1256
1171
|
};
|
|
1257
|
-
appendFileSync(this.file, `${JSON.stringify(entry)}\n`);
|
|
1172
|
+
appendFileSync(this.file, `${redactSecrets(JSON.stringify(entry))}\n`);
|
|
1258
1173
|
}
|
|
1259
1174
|
};
|
|
1260
1175
|
//#endregion
|
|
@@ -1399,13 +1314,19 @@ const toRequest = (args) => {
|
|
|
1399
1314
|
while (i < args.length) {
|
|
1400
1315
|
const arg = args[i];
|
|
1401
1316
|
if (arg.startsWith("--")) {
|
|
1402
|
-
const
|
|
1317
|
+
const body = arg.slice(2);
|
|
1318
|
+
const equalsAt = body.indexOf("=");
|
|
1319
|
+
if (equalsAt >= 0) {
|
|
1320
|
+
params.set(body.slice(0, equalsAt), body.slice(equalsAt + 1));
|
|
1321
|
+
i++;
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1403
1324
|
const next = args[i + 1];
|
|
1404
1325
|
if (isValue(next)) {
|
|
1405
|
-
params.set(
|
|
1326
|
+
params.set(body, next);
|
|
1406
1327
|
i += 2;
|
|
1407
1328
|
} else {
|
|
1408
|
-
params.set(
|
|
1329
|
+
params.set(body, "true");
|
|
1409
1330
|
i++;
|
|
1410
1331
|
}
|
|
1411
1332
|
continue;
|
|
@@ -1471,7 +1392,7 @@ const queryToCliArgs = (url, reservedKeys = []) => {
|
|
|
1471
1392
|
};
|
|
1472
1393
|
//#endregion
|
|
1473
1394
|
//#region lib/cli/routes/channels.add.ts
|
|
1474
|
-
const help$
|
|
1395
|
+
const help$15 = `funnel channels add — add a channel
|
|
1475
1396
|
|
|
1476
1397
|
usage: funnel channels add <name> [--delivery fanout|exclusive]
|
|
1477
1398
|
|
|
@@ -1489,7 +1410,29 @@ examples:
|
|
|
1489
1410
|
funnel channels add ci-events --delivery exclusive
|
|
1490
1411
|
|
|
1491
1412
|
see also: funnel channels, funnel channels <name> connectors add`;
|
|
1492
|
-
const channelsAddHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1413
|
+
const channelsAddHelpHandler = factory.createHandlers((c) => c.text(help$15));
|
|
1414
|
+
//#endregion
|
|
1415
|
+
//#region lib/cli/router/validator.ts
|
|
1416
|
+
const labelFor = (target, key) => {
|
|
1417
|
+
if (target === "query") return `--${key}`;
|
|
1418
|
+
return `<${key}>`;
|
|
1419
|
+
};
|
|
1420
|
+
const formatIssues = (target, issues) => {
|
|
1421
|
+
return `invalid arguments — ${issues.map((issue) => {
|
|
1422
|
+
const key = issue.path.map(String).join(".");
|
|
1423
|
+
if (!key) return issue.message;
|
|
1424
|
+
return `${labelFor(target, key)}: ${issue.message}`;
|
|
1425
|
+
}).join("; ")} (run with --help for usage)`;
|
|
1426
|
+
};
|
|
1427
|
+
/**
|
|
1428
|
+
* CLI-flavored zValidator: every route imports this instead of the raw
|
|
1429
|
+
* @hono/zod-validator so a validation failure renders as one readable line
|
|
1430
|
+
* naming the offending flag, not a raw ZodError JSON dump.
|
|
1431
|
+
*/
|
|
1432
|
+
const zValidator$1 = (target, schema) => zValidator(target, schema, (result) => {
|
|
1433
|
+
if (result.success) return;
|
|
1434
|
+
throw new HTTPException(400, { message: formatIssues(target, result.error.issues) });
|
|
1435
|
+
});
|
|
1493
1436
|
//#endregion
|
|
1494
1437
|
//#region lib/cli/routes/channels.add.$channel.ts
|
|
1495
1438
|
const channelsAddHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({ delivery: channelDeliveryModeSchema.optional() })), (c) => {
|
|
@@ -1502,6 +1445,17 @@ const channelsAddHandler = factory.createHandlers(zValidator$1("param", z.object
|
|
|
1502
1445
|
return c.text(`added channel "${created.name}" (id: ${created.id})`);
|
|
1503
1446
|
});
|
|
1504
1447
|
//#endregion
|
|
1448
|
+
//#region lib/cli/routes/not-found-message.ts
|
|
1449
|
+
/**
|
|
1450
|
+
* One error shape for every name-resolution miss: what was asked, what exists,
|
|
1451
|
+
* and the command that creates it — so a Claude (or human) can self-correct
|
|
1452
|
+
* without a follow-up listing call.
|
|
1453
|
+
*/
|
|
1454
|
+
const notFoundMessage = (props) => {
|
|
1455
|
+
const listed = props.available.length > 0 ? props.available.join(", ") : "none";
|
|
1456
|
+
return `${props.kind} "${props.name}" not found (available: ${listed}); to create one: ${props.nextAction}`;
|
|
1457
|
+
};
|
|
1458
|
+
//#endregion
|
|
1505
1459
|
//#region lib/cli/router/help-guard.ts
|
|
1506
1460
|
function helpGuard(help) {
|
|
1507
1461
|
return async (c, next) => {
|
|
@@ -1526,14 +1480,20 @@ subcommands:
|
|
|
1526
1480
|
<c> schedules add <id> --cron=... --prompt=... / add a schedule entry
|
|
1527
1481
|
<c> schedules remove <id> / remove a schedule entry`), (c) => {
|
|
1528
1482
|
const param = c.req.valid("param");
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1483
|
+
const funnel = c.env.funnel;
|
|
1484
|
+
const channel = funnel.channels.get(param.channel);
|
|
1485
|
+
if (!channel) throw new HTTPException(404, { message: notFoundMessage({
|
|
1486
|
+
kind: "channel",
|
|
1487
|
+
name: param.channel,
|
|
1488
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
1489
|
+
nextAction: "fnl channels add <name>"
|
|
1490
|
+
}) });
|
|
1531
1491
|
if (channel.connectors.length === 0) return c.text(`no connectors in channel "${channel.name}"`);
|
|
1532
1492
|
return c.text(channel.connectors.map((c) => `${c.name} (${c.type}, id: ${c.id})`).join("\n"));
|
|
1533
1493
|
});
|
|
1534
1494
|
//#endregion
|
|
1535
1495
|
//#region lib/cli/routes/channels.$channel.connectors.add.ts
|
|
1536
|
-
const help$
|
|
1496
|
+
const help$14 = `funnel channels <channel> connectors add <name> — add a connector to a channel
|
|
1537
1497
|
|
|
1538
1498
|
usage:
|
|
1539
1499
|
funnel channels <channel> connectors add <name> --type=slack --bot-token=xoxb-... --app-token=xapp-...
|
|
@@ -1556,7 +1516,7 @@ examples:
|
|
|
1556
1516
|
funnel channels alerts connectors add daily --type=schedule
|
|
1557
1517
|
|
|
1558
1518
|
see also: funnel channels <channel> connectors, funnel channels <channel> connectors remove`;
|
|
1559
|
-
const channelsConnectorsAddHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1519
|
+
const channelsConnectorsAddHelpHandler = factory.createHandlers((c) => c.text(help$14));
|
|
1560
1520
|
//#endregion
|
|
1561
1521
|
//#region lib/cli/routes/channels.$channel.connectors.add.$connector.ts
|
|
1562
1522
|
const slackBody = z.object({
|
|
@@ -1624,7 +1584,7 @@ const channelsConnectorsAddHandler = factory.createHandlers(zValidator$1("param"
|
|
|
1624
1584
|
});
|
|
1625
1585
|
//#endregion
|
|
1626
1586
|
//#region lib/cli/routes/channels.$channel.connectors.remove.ts
|
|
1627
|
-
const help$
|
|
1587
|
+
const help$13 = `funnel channels <channel> connectors remove <connector> — remove a connector
|
|
1628
1588
|
|
|
1629
1589
|
usage: funnel channels <channel> connectors remove <connector>
|
|
1630
1590
|
|
|
@@ -1636,7 +1596,7 @@ examples:
|
|
|
1636
1596
|
funnel channels production connectors remove slack-main
|
|
1637
1597
|
|
|
1638
1598
|
see also: funnel channels <channel> connectors, funnel channels <channel> connectors add`;
|
|
1639
|
-
const channelsConnectorsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1599
|
+
const channelsConnectorsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$13));
|
|
1640
1600
|
//#endregion
|
|
1641
1601
|
//#region lib/cli/routes/channels.$channel.connectors.remove.$connector.ts
|
|
1642
1602
|
const channelsConnectorsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
@@ -1651,13 +1611,13 @@ const channelsConnectorsRemoveHandler = factory.createHandlers(zValidator$1("par
|
|
|
1651
1611
|
});
|
|
1652
1612
|
//#endregion
|
|
1653
1613
|
//#region lib/cli/routes/channels.$channel.connectors.set.ts
|
|
1654
|
-
const help$
|
|
1614
|
+
const help$12 = `funnel channels <channel> connectors set <connector> — update connector fields
|
|
1655
1615
|
|
|
1656
1616
|
usage:
|
|
1657
1617
|
funnel channels <ch> connectors set <conn> [--bot-token=...] [--app-token=...] # slack
|
|
1658
1618
|
funnel channels <ch> connectors set <conn> [--bot-token=...] # discord
|
|
1659
1619
|
funnel channels <ch> connectors set <conn> [--poll-interval=N] # gh`;
|
|
1660
|
-
const channelsConnectorsSetHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1620
|
+
const channelsConnectorsSetHelpHandler = factory.createHandlers((c) => c.text(help$12));
|
|
1661
1621
|
//#endregion
|
|
1662
1622
|
//#region lib/cli/routes/channels.$channel.connectors.set.$connector.ts
|
|
1663
1623
|
const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
@@ -1672,7 +1632,12 @@ const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param"
|
|
|
1672
1632
|
const query = c.req.valid("query");
|
|
1673
1633
|
const funnel = c.env.funnel;
|
|
1674
1634
|
const existing = funnel.channels.getConnector(param.channel, param.connector);
|
|
1675
|
-
if (!existing) throw new HTTPException(404, { message:
|
|
1635
|
+
if (!existing) throw new HTTPException(404, { message: notFoundMessage({
|
|
1636
|
+
kind: "connector",
|
|
1637
|
+
name: param.connector,
|
|
1638
|
+
available: (funnel.channels.get(param.channel)?.connectors ?? []).map((conn) => conn.name),
|
|
1639
|
+
nextAction: `fnl channels ${param.channel} connectors add <name> --type=slack|gh|discord|schedule ...`
|
|
1640
|
+
}) });
|
|
1676
1641
|
if (existing.type === "slack") funnel.channels.updateSlackConnector(param.channel, param.connector, {
|
|
1677
1642
|
...query["bot-token"] !== void 0 ? { botToken: query["bot-token"] } : {},
|
|
1678
1643
|
...query["app-token"] !== void 0 ? { appToken: query["app-token"] } : {}
|
|
@@ -1695,15 +1660,21 @@ subcommands:
|
|
|
1695
1660
|
|
|
1696
1661
|
output / valid YAML`), (c) => {
|
|
1697
1662
|
const param = c.req.valid("param");
|
|
1698
|
-
const
|
|
1699
|
-
|
|
1663
|
+
const funnel = c.env.funnel;
|
|
1664
|
+
const connector = funnel.channels.getConnector(param.channel, param.connector);
|
|
1665
|
+
if (!connector) throw new HTTPException(404, { message: notFoundMessage({
|
|
1666
|
+
kind: "connector",
|
|
1667
|
+
name: param.connector,
|
|
1668
|
+
available: (funnel.channels.get(param.channel)?.connectors ?? []).map((conn) => conn.name),
|
|
1669
|
+
nextAction: `fnl channels ${param.channel} connectors add <name> --type=slack|gh|discord|schedule ...`
|
|
1670
|
+
}) });
|
|
1700
1671
|
return c.text(renderYaml(connector));
|
|
1701
1672
|
});
|
|
1702
1673
|
//#endregion
|
|
1703
1674
|
//#region lib/cli/routes/channels.$channel.connectors.rename.ts
|
|
1704
|
-
const help$
|
|
1675
|
+
const help$11 = `funnel channels <channel> connectors rename <old-connector-name> <new-connector-name> — rename a connector
|
|
1705
1676
|
|
|
1706
|
-
usage: funnel channels <channel> connectors rename <old> <new>
|
|
1677
|
+
usage: funnel channels <channel> connectors rename <old-connector-name> <new-connector-name>
|
|
1707
1678
|
|
|
1708
1679
|
Renames the connector in the configuration file. Tokens, type, and
|
|
1709
1680
|
schedules are preserved. The gateway picks up the new name on the
|
|
@@ -1713,7 +1684,7 @@ examples:
|
|
|
1713
1684
|
funnel channels production connectors rename slack-1 slack-main
|
|
1714
1685
|
|
|
1715
1686
|
see also: funnel channels <channel> connectors`;
|
|
1716
|
-
const channelsConnectorsRenameHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1687
|
+
const channelsConnectorsRenameHelpHandler = factory.createHandlers((c) => c.text(help$11));
|
|
1717
1688
|
//#endregion
|
|
1718
1689
|
//#region lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts
|
|
1719
1690
|
const channelsConnectorsRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
@@ -1730,10 +1701,10 @@ const channelsConnectorsRenameHandler = factory.createHandlers(zValidator$1("par
|
|
|
1730
1701
|
});
|
|
1731
1702
|
//#endregion
|
|
1732
1703
|
//#region lib/cli/routes/channels.$channel.connectors.$connector.rename.ts
|
|
1733
|
-
const help$
|
|
1704
|
+
const help$10 = `funnel channels <channel> connectors rename <connector> <new-name>
|
|
1734
1705
|
|
|
1735
1706
|
usage: funnel channels <channel> connectors rename <connector> <new-name>`;
|
|
1736
|
-
const channelsConnectorRenameHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1707
|
+
const channelsConnectorRenameHelpHandler = factory.createHandlers((c) => c.text(help$10));
|
|
1737
1708
|
const channelsConnectorsRequestHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
1738
1709
|
channel: z.string(),
|
|
1739
1710
|
connector: z.string()
|
|
@@ -1776,16 +1747,39 @@ subcommands:
|
|
|
1776
1747
|
add <id> --cron=... --prompt=... [--enabled=true] [--catchup-policy=latest|all|skip] / add entry
|
|
1777
1748
|
remove <id> / remove entry`), (c) => {
|
|
1778
1749
|
const param = c.req.valid("param");
|
|
1779
|
-
const
|
|
1750
|
+
const funnel = c.env.funnel;
|
|
1751
|
+
const entries = z.array(scheduleEntrySchema).parse(funnel.channels.connectorOp(param.channel, param.connector, "listEntries", void 0));
|
|
1780
1752
|
if (entries.length === 0) return c.text("no schedule entries");
|
|
1781
1753
|
return c.text(entries.map((e) => `${e.id}\t${e.cron}\t${e.enabled ? "on" : "off"}\t${e.prompt}`).join("\n"));
|
|
1782
1754
|
});
|
|
1783
1755
|
//#endregion
|
|
1784
1756
|
//#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.ts
|
|
1785
|
-
const help$
|
|
1757
|
+
const help$9 = `funnel channels <ch> connectors <conn> schedules add <id> — add a schedule entry
|
|
1786
1758
|
|
|
1787
|
-
usage: funnel channels <ch> connectors <conn> schedules add <id> --cron="*/5 * * * *" --prompt="..." [--enabled=
|
|
1788
|
-
|
|
1759
|
+
usage: funnel channels <ch> connectors <conn> schedules add <id> --cron="*/5 * * * *" --prompt="..." [--enabled|--enabled=false] [--catchup-policy=latest|all|skip]
|
|
1760
|
+
|
|
1761
|
+
options:
|
|
1762
|
+
--cron <expr> / 5-field cron expression (required)
|
|
1763
|
+
--prompt <text> / prompt delivered on each fire (required)
|
|
1764
|
+
--enabled / fire on schedule (default: true; --enabled=false stores it disabled)
|
|
1765
|
+
--catchup-policy latest|all|skip / how missed fires are replayed after downtime (default: latest)`;
|
|
1766
|
+
const channelsConnectorSchedulesAddHelpHandler = factory.createHandlers((c) => c.text(help$9));
|
|
1767
|
+
//#endregion
|
|
1768
|
+
//#region lib/cli/router/boolean-flag.ts
|
|
1769
|
+
/**
|
|
1770
|
+
* One parser for every CLI boolean flag: bare `--flag` (and `--flag=true`)
|
|
1771
|
+
* mean true, `--flag=false` means false, absent stays undefined so routes can
|
|
1772
|
+
* distinguish "not given" from "explicitly off". `""` survives for callers
|
|
1773
|
+
* that hit the HTTP surface directly with `?flag=`.
|
|
1774
|
+
*/
|
|
1775
|
+
const booleanFlag = z.enum([
|
|
1776
|
+
"true",
|
|
1777
|
+
"false",
|
|
1778
|
+
""
|
|
1779
|
+
]).optional().transform((value) => {
|
|
1780
|
+
if (value === void 0) return void 0;
|
|
1781
|
+
return value !== "false";
|
|
1782
|
+
});
|
|
1789
1783
|
//#endregion
|
|
1790
1784
|
//#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts
|
|
1791
1785
|
const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
@@ -1795,28 +1789,28 @@ const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$
|
|
|
1795
1789
|
})), zValidator$1("query", z.object({
|
|
1796
1790
|
cron: z.string(),
|
|
1797
1791
|
prompt: z.string(),
|
|
1798
|
-
enabled:
|
|
1792
|
+
enabled: booleanFlag,
|
|
1799
1793
|
"catchup-policy": scheduleCatchupPolicySchema.optional()
|
|
1800
1794
|
})), async (c) => {
|
|
1801
1795
|
const param = c.req.valid("param");
|
|
1802
1796
|
const query = c.req.valid("query");
|
|
1803
1797
|
const funnel = c.env.funnel;
|
|
1804
|
-
const entry = funnel.channels.
|
|
1798
|
+
const entry = scheduleEntrySchema.parse(funnel.channels.connectorOp(param.channel, param.connector, "addEntry", {
|
|
1805
1799
|
id: param.id,
|
|
1806
1800
|
cron: query.cron,
|
|
1807
1801
|
prompt: query.prompt,
|
|
1808
|
-
...query.enabled !== void 0 ? { enabled: query.enabled
|
|
1802
|
+
...query.enabled !== void 0 ? { enabled: query.enabled } : {},
|
|
1809
1803
|
...query["catchup-policy"] !== void 0 ? { catchupPolicy: query["catchup-policy"] } : {}
|
|
1810
|
-
});
|
|
1804
|
+
}));
|
|
1811
1805
|
await funnel.listeners.restart(param.channel, param.connector);
|
|
1812
1806
|
return c.text(`added schedule entry "${entry.id}"`);
|
|
1813
1807
|
});
|
|
1814
1808
|
//#endregion
|
|
1815
1809
|
//#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.ts
|
|
1816
|
-
const help$
|
|
1810
|
+
const help$8 = `funnel channels <ch> connectors <conn> schedules remove <id>
|
|
1817
1811
|
|
|
1818
1812
|
usage: funnel channels <ch> connectors <conn> schedules remove <id>`;
|
|
1819
|
-
const channelsConnectorSchedulesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1813
|
+
const channelsConnectorSchedulesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$8));
|
|
1820
1814
|
//#endregion
|
|
1821
1815
|
//#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts
|
|
1822
1816
|
const channelsConnectorsSchedulesRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
@@ -1826,13 +1820,13 @@ const channelsConnectorsSchedulesRemoveHandler = factory.createHandlers(zValidat
|
|
|
1826
1820
|
})), zValidator$1("query", z.object({})), async (c) => {
|
|
1827
1821
|
const param = c.req.valid("param");
|
|
1828
1822
|
const funnel = c.env.funnel;
|
|
1829
|
-
funnel.channels.
|
|
1823
|
+
funnel.channels.connectorOp(param.channel, param.connector, "removeEntry", { id: param.id });
|
|
1830
1824
|
await funnel.listeners.restart(param.channel, param.connector);
|
|
1831
1825
|
return c.text(`removed schedule entry "${param.id}"`);
|
|
1832
1826
|
});
|
|
1833
1827
|
//#endregion
|
|
1834
1828
|
//#region lib/cli/routes/channels.publish.ts
|
|
1835
|
-
const help$
|
|
1829
|
+
const help$7 = `funnel channels <channel> publish — push arbitrary content into a channel
|
|
1836
1830
|
|
|
1837
1831
|
usage: funnel channels <channel> publish --content="<text>" [--connector=<name>] [--meta-<key>=<value> ...]
|
|
1838
1832
|
|
|
@@ -1840,7 +1834,7 @@ options:
|
|
|
1840
1834
|
--content Required. The event body delivered to subscribers.
|
|
1841
1835
|
--connector Optional. Stamp the event with a connector name (resolved to id when found).
|
|
1842
1836
|
--meta-<key> Optional. Repeatable. Added to meta. Example: --meta-source=cron`;
|
|
1843
|
-
const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1837
|
+
const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$7));
|
|
1844
1838
|
//#endregion
|
|
1845
1839
|
//#region lib/cli/routes/channels.$channel.publish.ts
|
|
1846
1840
|
const querySchema = z.object({
|
|
@@ -1864,7 +1858,7 @@ const channelsPublishHandler = factory.createHandlers(zValidator$1("param", z.ob
|
|
|
1864
1858
|
});
|
|
1865
1859
|
//#endregion
|
|
1866
1860
|
//#region lib/cli/routes/channels.remove.ts
|
|
1867
|
-
const help$
|
|
1861
|
+
const help$6 = `funnel channels remove — remove a channel and all its connectors
|
|
1868
1862
|
|
|
1869
1863
|
usage: funnel channels remove <name>
|
|
1870
1864
|
|
|
@@ -1876,21 +1870,28 @@ examples:
|
|
|
1876
1870
|
funnel channels remove staging
|
|
1877
1871
|
|
|
1878
1872
|
see also: funnel channels, funnel channels add`;
|
|
1879
|
-
const channelsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1873
|
+
const channelsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$6));
|
|
1880
1874
|
//#endregion
|
|
1881
1875
|
//#region lib/cli/routes/channels.remove.$channel.ts
|
|
1882
1876
|
const channelsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({})), (c) => {
|
|
1883
1877
|
const param = c.req.valid("param");
|
|
1884
|
-
c.env.funnel
|
|
1878
|
+
const funnel = c.env.funnel;
|
|
1879
|
+
if (!funnel.channels.get(param.channel)) throw new HTTPException(404, { message: notFoundMessage({
|
|
1880
|
+
kind: "channel",
|
|
1881
|
+
name: param.channel,
|
|
1882
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
1883
|
+
nextAction: "fnl channels add <name>"
|
|
1884
|
+
}) });
|
|
1885
|
+
funnel.channels.remove(param.channel);
|
|
1885
1886
|
return c.text(`removed channel "${param.channel}"`);
|
|
1886
1887
|
});
|
|
1887
1888
|
//#endregion
|
|
1888
1889
|
//#region lib/cli/routes/channels.rename.ts
|
|
1889
|
-
const
|
|
1890
|
+
const channelsRenameHelp = `funnel channels rename — rename a channel
|
|
1890
1891
|
|
|
1891
1892
|
usage:
|
|
1892
|
-
funnel channels rename <old> <new>
|
|
1893
|
-
funnel channels <old> rename <new>
|
|
1893
|
+
funnel channels rename <old-channel-name> <new-channel-name>
|
|
1894
|
+
funnel channels <old-channel-name> rename <new-channel-name>
|
|
1894
1895
|
|
|
1895
1896
|
Renames the channel in the configuration file. Connectors, schedules,
|
|
1896
1897
|
and delivery mode are preserved. The gateway picks up the new name on
|
|
@@ -1901,15 +1902,10 @@ examples:
|
|
|
1901
1902
|
funnel channels staging rename production
|
|
1902
1903
|
|
|
1903
1904
|
see also: funnel channels, funnel channels <name>`;
|
|
1904
|
-
const channelsRenameHelpHandler = factory.createHandlers((c) => c.text(
|
|
1905
|
+
const channelsRenameHelpHandler = factory.createHandlers((c) => c.text(channelsRenameHelp));
|
|
1905
1906
|
//#endregion
|
|
1906
1907
|
//#region lib/cli/routes/channels.$channel.rename.ts
|
|
1907
|
-
const
|
|
1908
|
-
|
|
1909
|
-
usage:
|
|
1910
|
-
funnel channels rename <old> <new>
|
|
1911
|
-
funnel channels <old> rename <new>`;
|
|
1912
|
-
const channelsChannelRenameHelpHandler = factory.createHandlers((c) => c.text(help$6));
|
|
1908
|
+
const channelsChannelRenameHelpHandler = factory.createHandlers((c) => c.text(channelsRenameHelp));
|
|
1913
1909
|
//#endregion
|
|
1914
1910
|
//#region lib/cli/routes/channels.$channel.rename.$newName.ts
|
|
1915
1911
|
const channelsRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
@@ -1917,7 +1913,14 @@ const channelsRenameHandler = factory.createHandlers(zValidator$1("param", z.obj
|
|
|
1917
1913
|
newName: z.string()
|
|
1918
1914
|
})), zValidator$1("query", z.object({})), (c) => {
|
|
1919
1915
|
const param = c.req.valid("param");
|
|
1920
|
-
c.env.funnel
|
|
1916
|
+
const funnel = c.env.funnel;
|
|
1917
|
+
if (!funnel.channels.get(param.channel)) throw new HTTPException(404, { message: notFoundMessage({
|
|
1918
|
+
kind: "channel",
|
|
1919
|
+
name: param.channel,
|
|
1920
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
1921
|
+
nextAction: "fnl channels add <name>"
|
|
1922
|
+
}) });
|
|
1923
|
+
funnel.channels.rename(param.channel, param.newName);
|
|
1921
1924
|
return c.text(`renamed channel "${param.channel}" to "${param.newName}"`);
|
|
1922
1925
|
});
|
|
1923
1926
|
const channelsSetDeliveryHandler = factory.createHandlers(helpGuard(`funnel channels <name> set delivery <mode> — change a channel's routing mode
|
|
@@ -1947,8 +1950,14 @@ subcommands:
|
|
|
1947
1950
|
|
|
1948
1951
|
output / valid YAML`), (c) => {
|
|
1949
1952
|
const param = c.req.valid("param");
|
|
1950
|
-
const
|
|
1951
|
-
|
|
1953
|
+
const funnel = c.env.funnel;
|
|
1954
|
+
const channel = funnel.channels.get(param.channel);
|
|
1955
|
+
if (!channel) throw new HTTPException(404, { message: notFoundMessage({
|
|
1956
|
+
kind: "channel",
|
|
1957
|
+
name: param.channel,
|
|
1958
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
1959
|
+
nextAction: "fnl channels add <name>"
|
|
1960
|
+
}) });
|
|
1952
1961
|
return c.text(renderYaml({
|
|
1953
1962
|
id: channel.id,
|
|
1954
1963
|
name: channel.name,
|
|
@@ -2069,8 +2078,14 @@ const validateConnector = (connector) => {
|
|
|
2069
2078
|
};
|
|
2070
2079
|
const channelsValidateHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({})), (c) => {
|
|
2071
2080
|
const param = c.req.valid("param");
|
|
2072
|
-
const
|
|
2073
|
-
|
|
2081
|
+
const funnel = c.env.funnel;
|
|
2082
|
+
const channel = funnel.channels.get(param.channel);
|
|
2083
|
+
if (!channel) throw new HTTPException(404, { message: notFoundMessage({
|
|
2084
|
+
kind: "channel",
|
|
2085
|
+
name: param.channel,
|
|
2086
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
2087
|
+
nextAction: "fnl channels add <name>"
|
|
2088
|
+
}) });
|
|
2074
2089
|
if (channel.connectors.length === 0) return c.text(renderYaml({
|
|
2075
2090
|
channel: channel.name,
|
|
2076
2091
|
valid: false,
|
|
@@ -2141,7 +2156,12 @@ const claudeHandler = factory.createHandlers(helpGuard(claudeHelp), zValidator$1
|
|
|
2141
2156
|
}
|
|
2142
2157
|
if (query.profile) {
|
|
2143
2158
|
const profile = profiles.get(query.profile);
|
|
2144
|
-
if (!profile) throw new HTTPException(404, { message:
|
|
2159
|
+
if (!profile) throw new HTTPException(404, { message: notFoundMessage({
|
|
2160
|
+
kind: "profile",
|
|
2161
|
+
name: query.profile,
|
|
2162
|
+
available: profiles.list().map((p) => p.name),
|
|
2163
|
+
nextAction: "fnl profiles add <name> --path=<repo> --channel=<channel>"
|
|
2164
|
+
}) });
|
|
2145
2165
|
const exitCode = await claude.launch({
|
|
2146
2166
|
channel: profile.channelId,
|
|
2147
2167
|
cwd: profile.path,
|
|
@@ -2274,16 +2294,12 @@ const resolveTargetChannel = (c, channelArg) => {
|
|
|
2274
2294
|
};
|
|
2275
2295
|
const debugHandler = factory.createHandlers(helpGuard(debugHelp), zValidator$1("query", z.object({
|
|
2276
2296
|
channel: z.string().optional(),
|
|
2277
|
-
all:
|
|
2278
|
-
"true",
|
|
2279
|
-
"false",
|
|
2280
|
-
""
|
|
2281
|
-
]).optional(),
|
|
2297
|
+
all: booleanFlag,
|
|
2282
2298
|
limit: z.string().optional()
|
|
2283
2299
|
})), async (c) => {
|
|
2284
2300
|
const query = c.req.valid("query");
|
|
2285
2301
|
const funnel = c.env.funnel;
|
|
2286
|
-
if (query.all ===
|
|
2302
|
+
if (query.all === true) {
|
|
2287
2303
|
const report = await funnel.diagnostics.diagnoseAll();
|
|
2288
2304
|
return c.text(renderYaml(report));
|
|
2289
2305
|
}
|
|
@@ -2394,20 +2410,12 @@ examples:
|
|
|
2394
2410
|
funnel doctor
|
|
2395
2411
|
funnel doctor --fix
|
|
2396
2412
|
funnel doctor --fix --aggressive`), zValidator$1("query", z.object({
|
|
2397
|
-
fix:
|
|
2398
|
-
|
|
2399
|
-
"false",
|
|
2400
|
-
""
|
|
2401
|
-
]).optional(),
|
|
2402
|
-
aggressive: z.enum([
|
|
2403
|
-
"true",
|
|
2404
|
-
"false",
|
|
2405
|
-
""
|
|
2406
|
-
]).optional()
|
|
2413
|
+
fix: booleanFlag,
|
|
2414
|
+
aggressive: booleanFlag
|
|
2407
2415
|
})), async (c) => {
|
|
2408
2416
|
const query = c.req.valid("query");
|
|
2409
|
-
const wantsFix = query.fix ===
|
|
2410
|
-
const wantsAggressive = query.aggressive ===
|
|
2417
|
+
const wantsFix = query.fix === true;
|
|
2418
|
+
const wantsAggressive = query.aggressive === true;
|
|
2411
2419
|
const mode = wantsFix ? wantsAggressive ? "aggressive" : "safe" : "off";
|
|
2412
2420
|
const report = await c.env.funnel.doctor.run(mode);
|
|
2413
2421
|
return c.text(renderYaml(report));
|
|
@@ -2446,7 +2454,7 @@ examples:
|
|
|
2446
2454
|
const renderGatewayStatus = async (c) => {
|
|
2447
2455
|
const status = c.env.funnel.gateway.getStatus();
|
|
2448
2456
|
if (!status.running) return c.text(renderYaml({ running: false }), 503);
|
|
2449
|
-
const res = await fetch(
|
|
2457
|
+
const res = await fetch(`${gatewayLoopbackUrl(status.port)}/status`).catch(() => null);
|
|
2450
2458
|
if (!res) return c.text(renderYaml({
|
|
2451
2459
|
running: true,
|
|
2452
2460
|
pid: status.pid,
|
|
@@ -2756,6 +2764,16 @@ examples:
|
|
|
2756
2764
|
funnel gateway start --no-caffeine
|
|
2757
2765
|
|
|
2758
2766
|
programmable: funnel.gateway.start({ caffeinate })`;
|
|
2767
|
+
const HEALTH_TIMEOUT_MS = 5e3;
|
|
2768
|
+
const HEALTH_POLL_INTERVAL_MS = 100;
|
|
2769
|
+
const waitForHealth = async (port) => {
|
|
2770
|
+
const deadline = Date.now() + HEALTH_TIMEOUT_MS;
|
|
2771
|
+
while (Date.now() < deadline) {
|
|
2772
|
+
if ((await fetch(`${gatewayLoopbackUrl(port)}/health`).catch(() => null))?.ok) return true;
|
|
2773
|
+
await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_INTERVAL_MS));
|
|
2774
|
+
}
|
|
2775
|
+
return false;
|
|
2776
|
+
};
|
|
2759
2777
|
const gatewayStartHandler = factory.createHandlers(helpGuard(startHelp), zValidator$1("query", z.object({ "no-caffeine": z.string().optional() })), async (c) => {
|
|
2760
2778
|
const query = c.req.valid("query");
|
|
2761
2779
|
const funnel = c.env.funnel;
|
|
@@ -2763,8 +2781,10 @@ const gatewayStartHandler = factory.createHandlers(helpGuard(startHelp), zValida
|
|
|
2763
2781
|
const status = funnel.gateway.getStatus();
|
|
2764
2782
|
return c.text(`funnel gateway: already running (pid ${status.pid})`);
|
|
2765
2783
|
}
|
|
2766
|
-
if (!await funnel.gateway.start({ caffeinate: query["no-caffeine"] !== "true" })) throw new HTTPException(500, { message: "funnel gateway: failed to start" });
|
|
2767
|
-
|
|
2784
|
+
if (!await funnel.gateway.start({ caffeinate: query["no-caffeine"] !== "true" })) throw new HTTPException(500, { message: "funnel gateway: failed to start — inspect the daemon log with `fnl gateway logs`" });
|
|
2785
|
+
const status = funnel.gateway.getStatus();
|
|
2786
|
+
if (!await waitForHealth(status.port)) return c.text(`funnel gateway: started (pid ${status.pid}, port ${status.port}) but /health did not respond within ${HEALTH_TIMEOUT_MS / 1e3}s — check \`fnl gateway logs\``);
|
|
2787
|
+
return c.text(`funnel gateway: started (pid ${status.pid}, port ${status.port})`);
|
|
2768
2788
|
});
|
|
2769
2789
|
const gatewayStatusHandler = factory.createHandlers(helpGuard(`funnel gateway status / show gateway running status
|
|
2770
2790
|
|
|
@@ -2854,7 +2874,12 @@ const profilesAddHandler = factory.createHandlers(zValidator$1("param", z.object
|
|
|
2854
2874
|
const funnel = c.env.funnel;
|
|
2855
2875
|
const { profiles, claude } = c.env;
|
|
2856
2876
|
const channel = funnel.channels.get(query.channel);
|
|
2857
|
-
if (!channel) throw new HTTPException(400, { message:
|
|
2877
|
+
if (!channel) throw new HTTPException(400, { message: notFoundMessage({
|
|
2878
|
+
kind: "channel",
|
|
2879
|
+
name: query.channel,
|
|
2880
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
2881
|
+
nextAction: "fnl channels add <name>"
|
|
2882
|
+
}) });
|
|
2858
2883
|
const recipe = parseProfileRecipe(query);
|
|
2859
2884
|
profiles.add({
|
|
2860
2885
|
name: param.profile,
|
|
@@ -2917,7 +2942,12 @@ const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.obj
|
|
|
2917
2942
|
c.env.funnel;
|
|
2918
2943
|
const { profiles, claude } = c.env;
|
|
2919
2944
|
const profile = profiles.get(param.profile);
|
|
2920
|
-
if (!profile) throw new HTTPException(404, { message:
|
|
2945
|
+
if (!profile) throw new HTTPException(404, { message: notFoundMessage({
|
|
2946
|
+
kind: "profile",
|
|
2947
|
+
name: param.profile,
|
|
2948
|
+
available: profiles.list().map((p) => p.name),
|
|
2949
|
+
nextAction: "fnl profiles add <name> --path=<repo> --channel=<channel>"
|
|
2950
|
+
}) });
|
|
2921
2951
|
const exitCode = await claude.launch({
|
|
2922
2952
|
channel: profile.channelId,
|
|
2923
2953
|
cwd: profile.path,
|
|
@@ -2977,7 +3007,12 @@ const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object
|
|
|
2977
3007
|
const funnel = c.env.funnel;
|
|
2978
3008
|
const { profiles, claude } = c.env;
|
|
2979
3009
|
const channel = query.channel !== void 0 ? funnel.channels.get(query.channel) : null;
|
|
2980
|
-
if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message:
|
|
3010
|
+
if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message: notFoundMessage({
|
|
3011
|
+
kind: "channel",
|
|
3012
|
+
name: query.channel,
|
|
3013
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
3014
|
+
nextAction: "fnl channels add <name>"
|
|
3015
|
+
}) });
|
|
2981
3016
|
const recipe = parseProfileRecipe(query);
|
|
2982
3017
|
profiles.update(param.profile, {
|
|
2983
3018
|
path: query.path,
|
|
@@ -3053,7 +3088,7 @@ const statusHelp = `funnel status / overall health snapshot
|
|
|
3053
3088
|
usage / funnel status [--watch] [--interval <N>]
|
|
3054
3089
|
|
|
3055
3090
|
options:
|
|
3056
|
-
--watch / continuously refresh (Ctrl+C to stop)
|
|
3091
|
+
--watch / continuously refresh (default: off; Ctrl+C to stop)
|
|
3057
3092
|
--interval <N> / polling interval in seconds (default 3)
|
|
3058
3093
|
|
|
3059
3094
|
output / valid YAML
|
|
@@ -3078,7 +3113,7 @@ const buildStatusReport = async (funnel, profiles) => {
|
|
|
3078
3113
|
const gatewayStatus = funnel.gateway.getStatus();
|
|
3079
3114
|
let gatewayData = null;
|
|
3080
3115
|
if (gatewayStatus.running) {
|
|
3081
|
-
const res = await fetch(
|
|
3116
|
+
const res = await fetch(`${gatewayLoopbackUrl(gatewayStatus.port)}/status`).catch(() => null);
|
|
3082
3117
|
if (res && res.ok) {
|
|
3083
3118
|
const body = await res.json();
|
|
3084
3119
|
if (isGatewayStatus(body)) gatewayData = body;
|
|
@@ -3099,6 +3134,7 @@ const buildStatusReport = async (funnel, profiles) => {
|
|
|
3099
3134
|
return {
|
|
3100
3135
|
gateway: gatewayStatus.running ? {
|
|
3101
3136
|
running: true,
|
|
3137
|
+
responsive: gatewayData !== null,
|
|
3102
3138
|
pid: gatewayStatus.pid,
|
|
3103
3139
|
port: gatewayStatus.port,
|
|
3104
3140
|
uptimeMs: gatewayData?.uptimeMs ?? null
|
|
@@ -3125,16 +3161,12 @@ const buildStatusReport = async (funnel, profiles) => {
|
|
|
3125
3161
|
};
|
|
3126
3162
|
};
|
|
3127
3163
|
const statusHandler = factory.createHandlers(helpGuard(statusHelp), zValidator$1("query", z.object({
|
|
3128
|
-
watch:
|
|
3129
|
-
"true",
|
|
3130
|
-
"false",
|
|
3131
|
-
""
|
|
3132
|
-
]).optional(),
|
|
3164
|
+
watch: booleanFlag,
|
|
3133
3165
|
interval: z.string().optional()
|
|
3134
3166
|
})), async (c) => {
|
|
3135
3167
|
const query = c.req.valid("query");
|
|
3136
3168
|
const funnel = c.env.funnel;
|
|
3137
|
-
const isWatch = query.watch ===
|
|
3169
|
+
const isWatch = query.watch === true;
|
|
3138
3170
|
const intervalSec = Math.min(60, Math.max(1, query.interval ? Number(query.interval) : 3));
|
|
3139
3171
|
if (!isWatch) {
|
|
3140
3172
|
const report = await buildStatusReport(funnel, c.env.profiles);
|
|
@@ -3185,4 +3217,4 @@ const routes = factory.createApp().onError((error, c) => {
|
|
|
3185
3217
|
return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
|
|
3186
3218
|
}).get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...channelsAddHelpHandler).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...channelsRemoveHelpHandler).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...channelsRenameHelpHandler).post("/channels/:channel/rename", ...channelsChannelRenameHelpHandler).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...channelsPublishHelpHandler).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel/validate", ...channelsValidateHandler).get("/channels/validate", ...channelsValidateHelpHandler).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...channelsConnectorsAddHelpHandler).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...channelsConnectorsRemoveHelpHandler).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...channelsConnectorsSetHelpHandler).post("/channels/:channel/connectors/set/:connector", ...channelsConnectorsSetHandler).post("/channels/:channel/connectors/rename/:connector/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/:connector/rename/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/rename", ...channelsConnectorsRenameHelpHandler).post("/channels/:channel/connectors/:connector/rename", ...channelsConnectorRenameHelpHandler).post("/channels/:channel/connectors/:connector/request", ...channelsConnectorsRequestHandler).get("/channels/:channel/connectors/:connector", ...channelsConnectorsShowHandler).get("/channels/:channel/connectors/:connector/schedules", ...channelsConnectorsSchedulesGroupHandler).post("/channels/:channel/connectors/:connector/schedules/add", ...channelsConnectorSchedulesAddHelpHandler).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...channelsConnectorSchedulesRemoveHelpHandler).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...profilesAddHelpHandler).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...profilesSetHelpHandler).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...profilesRemoveHelpHandler).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...profilesRenameHelpHandler).post("/profiles/:profile/rename", ...profilesProfileRenameHelpHandler).post("/profiles/:profile/as-default", ...profilesAsDefaultHandler).get("/profiles/:profile/run", ...profilesLaunchHandler).get("/profiles/:profile", ...profilesLaunchHandler).get("/gateway", ...gatewayGroupHandler).get("/gateway/status", ...gatewayStatusHandler).get("/gateway/start", ...gatewayStartHandler).get("/gateway/stop", ...gatewayStopHandler).get("/gateway/restart", ...gatewayRestartHandler).get("/gateway/run", ...gatewayRunHandler).get("/gateway/logs", ...gatewayLogsHandler).get("/gateway/sql", ...gatewaySqlHandler).get("/gateway/listeners", ...gatewayListenersHandler).get("/debug", ...debugHandler).get("/debug/events", ...debugEventsHandler).get("/debug/dropped", ...debugDroppedHandler).get("/debug/errors", ...debugErrorsHandler).get("/debug/replay", ...debugReplayHandler).get("/docs", ...docsIndexHandler).get("/docs/:topic", ...docsTopicHandler).get("/doctor", ...doctorHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
|
|
3187
3219
|
//#endregion
|
|
3188
|
-
export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClock, FunnelConnectorAdapter,
|
|
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 };
|