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