@interactive-inc/claude-funnel 0.59.0 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/bin.js +524 -449
- 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-B3IFCj_G.d.ts → file-process-guard-DOlCr4GF.d.ts} +5 -6
- 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 +282 -209
- 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-Dsx6AmhH.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 +383 -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-5LzwJ_F7.js → memory-diagnostic-log-CI60kNfB.js} +33 -18
- package/dist/{memory-token-prompter-BlFwK9k7.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-DUcZgN2Q.js → node-process-runner-DxTvycoK.js} +35 -13
- 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-6foMXfFf.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-C9Hhjk-0.js → yaml-render-qW34NlYz.js} +43 -10
- 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-Conbxl5O.d.ts +0 -3595
- package/dist/local-config-sync--f739oCJ.d.ts +0 -401
- package/dist/process-runner-Cx5O_fTf.d.ts +0 -49
- 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-5LzwJ_F7.js";
|
|
1
|
+
import { t as gatewayLoopbackUrl } from "./gateway-base-url-Dy4Ykuoh.js";
|
|
2
|
+
import { t as FunnelFileSystem } from "./file-system-Wvzc2ePY.js";
|
|
3
|
+
import { t as NodeFunnelFileSystem } from "./node-file-system-Blr8pAir.js";
|
|
4
|
+
import { t as FunnelLogger } from "./logger-B6iyNbxM.js";
|
|
5
|
+
import { n as FunnelProcessRunner, t as NodeFunnelProcessRunner } from "./node-process-runner-DxTvycoK.js";
|
|
6
|
+
import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-CtQ-Ix8_.js";
|
|
7
|
+
import { a as resolveFunnelDir, c as channelConfigSchema, d as settingsSchema, f as baseConnectorConfigSchema, i as SETTINGS_PATH, l as channelDeliveryModeSchema, n as FUNNEL_DIR, o as resolveFunnelPort, p as NodeFunnelIdGenerator, r as FunnelSettingsStore, s as SETTINGS_VERSION, t as DEFAULT_GATEWAY_PORT, u as profileConfigSchema } from "./settings-store-CUKSeTXC.js";
|
|
8
|
+
import { a as FunnelMcp, o as FileProcessGuard, s as FunnelClaude, t as renderYaml } from "./yaml-render-qW34NlYz.js";
|
|
9
|
+
import { a as toDiagnosticEvent, i as toDiagnosticConnectionError, n as previewOf, r as queryRows, t as FunnelDiagnostics } from "./funnel-diagnostics-CSiJmPlZ.js";
|
|
10
|
+
import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-C9zR-Csp.js";
|
|
11
|
+
import { t as FunnelDoctor } from "./funnel-doctor-DiJCjHsg.js";
|
|
12
|
+
import { t as FunnelDocs } from "./funnel-docs-BxXZ9Ksx.js";
|
|
13
|
+
import { a as FunnelLocalConfig, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigSync, t as funnelJsonSchema } from "./local-config-json-schema-JyLqOQNX.js";
|
|
14
|
+
import { t as FunnelProfiles } from "./profiles-DSzTeKQw.js";
|
|
15
|
+
import { t as FunnelRecovery } from "./funnel-recovery-BFdPjL6Z.js";
|
|
16
|
+
import { C as funnelTmpDir, S as publishResponseSchema, _ as funnelEventSchema, a as connectorConnectionEventSchema, b as FunnelChannelPublisher, c as MemoryFunnelEventLog, d as DEFAULT_GATEWAY_TOKEN_PATH, f as FunnelGatewayToken, g as FunnelEventLog, h as SqliteFunnelEventLog, i as ConnectorDiagnosticLog, l as channelWsProtocols, m as FunnelListenerSupervisor, n as SqliteConnectorDiagnosticLog, o as connectorProcessedEventSchema, p as FunnelGatewayServer, r as CONNECTOR_CONNECTION_STATUSES, s as connectorRawEventSchema, t as MemoryConnectorDiagnosticLog, u as channelWsUrl, v as FunnelBroadcaster, x as publishRequestSchema, y as requireBearerToken } from "./memory-diagnostic-log-CI60kNfB.js";
|
|
17
|
+
import { n as FunnelHttpClient, t as NodeFunnelHttpClient } from "./node-http-client-lowp60Oa.js";
|
|
18
|
+
import { t as FunnelConnectorAdapter } from "./connector-adapter-DU9Rvyec.js";
|
|
19
|
+
import { t as FunnelConnectorListener } from "./connector-listener-DR3aKOuK.js";
|
|
20
|
+
import { r as scheduleEntrySchema, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-CfyuMCMh.js";
|
|
25
21
|
import { dirname, join, resolve } from "node:path";
|
|
26
22
|
import { hc } from "hono/client";
|
|
27
23
|
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
@@ -29,80 +25,68 @@ import { z } from "zod";
|
|
|
29
25
|
import { fileURLToPath } from "node:url";
|
|
30
26
|
import { createFactory } from "hono/factory";
|
|
31
27
|
import { HTTPException } from "hono/http-exception";
|
|
32
|
-
import { zValidator
|
|
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
|
};
|
|
@@ -596,6 +487,7 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
|
|
|
596
487
|
syncHandler = () => empty;
|
|
597
488
|
aliveStub = null;
|
|
598
489
|
listStub = null;
|
|
490
|
+
startTimeStub = null;
|
|
599
491
|
on(handler) {
|
|
600
492
|
this.handler = handler;
|
|
601
493
|
return this;
|
|
@@ -612,6 +504,10 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
|
|
|
612
504
|
this.listStub = stub;
|
|
613
505
|
return this;
|
|
614
506
|
}
|
|
507
|
+
onGetStartTime(stub) {
|
|
508
|
+
this.startTimeStub = stub;
|
|
509
|
+
return this;
|
|
510
|
+
}
|
|
615
511
|
async run(command, options = {}) {
|
|
616
512
|
this.calls.push({
|
|
617
513
|
kind: "run",
|
|
@@ -681,6 +577,10 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
|
|
|
681
577
|
if (this.listStub) return this.listStub(marker);
|
|
682
578
|
return [];
|
|
683
579
|
}
|
|
580
|
+
getStartTime(pid) {
|
|
581
|
+
if (this.startTimeStub) return this.startTimeStub(pid);
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
684
584
|
};
|
|
685
585
|
//#endregion
|
|
686
586
|
//#region lib/engine/settings/mock-settings-reader.ts
|
|
@@ -1017,7 +917,9 @@ const noopOnError = () => {};
|
|
|
1017
917
|
*
|
|
1018
918
|
* @example
|
|
1019
919
|
* ```ts
|
|
1020
|
-
*
|
|
920
|
+
* import { slackConnector } from "@interactive-inc/claude-funnel/connectors/slack"
|
|
921
|
+
*
|
|
922
|
+
* const funnel = new Funnel({ connectors: [slackConnector()] })
|
|
1021
923
|
* const channel = funnel.channels.add({ name: "inbox" })
|
|
1022
924
|
* funnel.channels.addConnector("inbox", { type: "slack", name: "ops", botToken, appToken })
|
|
1023
925
|
* await funnel.gatewayServer({ port: 9742 }).start()
|
|
@@ -1065,20 +967,25 @@ var Funnel = class Funnel {
|
|
|
1065
967
|
fs,
|
|
1066
968
|
idGenerator
|
|
1067
969
|
});
|
|
1068
|
-
const
|
|
970
|
+
const registry = new FunnelConnectorRegistry({
|
|
971
|
+
descriptors: props.connectors ?? [],
|
|
1069
972
|
fs,
|
|
1070
973
|
process,
|
|
1071
974
|
logger: this.logger,
|
|
1072
975
|
diagnosticLog: props.diagnosticLog,
|
|
1073
|
-
dir
|
|
1074
|
-
|
|
1075
|
-
|
|
976
|
+
dir
|
|
977
|
+
});
|
|
978
|
+
this.profiles = new FunnelProfiles({
|
|
979
|
+
store,
|
|
980
|
+
idGenerator,
|
|
981
|
+
fs
|
|
1076
982
|
});
|
|
1077
983
|
this.channels = new FunnelChannels({
|
|
1078
984
|
store,
|
|
1079
|
-
|
|
985
|
+
registry,
|
|
1080
986
|
clock,
|
|
1081
|
-
idGenerator
|
|
987
|
+
idGenerator,
|
|
988
|
+
profileChecker: this.profiles
|
|
1082
989
|
});
|
|
1083
990
|
this.gateway = new FunnelGateway({
|
|
1084
991
|
fs,
|
|
@@ -1103,11 +1010,6 @@ var Funnel = class Funnel {
|
|
|
1103
1010
|
getToken: () => this.gatewayToken.read()
|
|
1104
1011
|
});
|
|
1105
1012
|
const mcp = new FunnelMcp({ fs });
|
|
1106
|
-
this.profiles = new FunnelProfiles({
|
|
1107
|
-
store,
|
|
1108
|
-
idGenerator,
|
|
1109
|
-
fs
|
|
1110
|
-
});
|
|
1111
1013
|
this.localConfig = new FunnelLocalConfig({ fs });
|
|
1112
1014
|
this.localConfigSync = new FunnelLocalConfigSync({
|
|
1113
1015
|
channels: this.channels,
|
|
@@ -1213,10 +1115,32 @@ var Funnel = class Funnel {
|
|
|
1213
1115
|
}
|
|
1214
1116
|
gatewayClient() {
|
|
1215
1117
|
const { port } = this.gateway.getStatus();
|
|
1216
|
-
return hc(
|
|
1118
|
+
return hc(gatewayLoopbackUrl(port));
|
|
1217
1119
|
}
|
|
1218
1120
|
};
|
|
1219
1121
|
//#endregion
|
|
1122
|
+
//#region lib/engine/logger/redact-secrets.ts
|
|
1123
|
+
/**
|
|
1124
|
+
* Mask credential-shaped substrings before a log line is persisted. Matching
|
|
1125
|
+
* is prefix-anchored (Slack xoxb-/xapp-, GitHub ghp_/github_pat_, Discord
|
|
1126
|
+
* Bot tokens, HTTP bearer values) so ordinary identifiers never trip it —
|
|
1127
|
+
* a false negative only weakens defense in depth, a false positive destroys
|
|
1128
|
+
* a legitimate log line.
|
|
1129
|
+
*/
|
|
1130
|
+
const SECRET_PATTERNS = [
|
|
1131
|
+
/xox[abprs]-[A-Za-z0-9-]{10,}/g,
|
|
1132
|
+
/xapp-[A-Za-z0-9-]{10,}/g,
|
|
1133
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
1134
|
+
/github_pat_[A-Za-z0-9_]{20,}/g,
|
|
1135
|
+
/Bot [A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{20,}/g,
|
|
1136
|
+
/Bearer [A-Za-z0-9._~+/-]{16,}/g
|
|
1137
|
+
];
|
|
1138
|
+
const redactSecrets = (text) => {
|
|
1139
|
+
let redacted = text;
|
|
1140
|
+
for (const pattern of SECRET_PATTERNS) redacted = redacted.replace(pattern, "[redacted]");
|
|
1141
|
+
return redacted;
|
|
1142
|
+
};
|
|
1143
|
+
//#endregion
|
|
1220
1144
|
//#region lib/engine/logger/node-logger.ts
|
|
1221
1145
|
const defaultLogFile = () => join(funnelTmpDir(), "funnel.log");
|
|
1222
1146
|
var NodeFunnelLogger = class extends FunnelLogger {
|
|
@@ -1245,7 +1169,7 @@ var NodeFunnelLogger = class extends FunnelLogger {
|
|
|
1245
1169
|
message,
|
|
1246
1170
|
...meta ? { meta } : {}
|
|
1247
1171
|
};
|
|
1248
|
-
appendFileSync(this.file, `${JSON.stringify(entry)}\n`);
|
|
1172
|
+
appendFileSync(this.file, `${redactSecrets(JSON.stringify(entry))}\n`);
|
|
1249
1173
|
}
|
|
1250
1174
|
};
|
|
1251
1175
|
//#endregion
|
|
@@ -1390,13 +1314,19 @@ const toRequest = (args) => {
|
|
|
1390
1314
|
while (i < args.length) {
|
|
1391
1315
|
const arg = args[i];
|
|
1392
1316
|
if (arg.startsWith("--")) {
|
|
1393
|
-
const
|
|
1317
|
+
const body = arg.slice(2);
|
|
1318
|
+
const equalsAt = body.indexOf("=");
|
|
1319
|
+
if (equalsAt >= 0) {
|
|
1320
|
+
params.set(body.slice(0, equalsAt), body.slice(equalsAt + 1));
|
|
1321
|
+
i++;
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1394
1324
|
const next = args[i + 1];
|
|
1395
1325
|
if (isValue(next)) {
|
|
1396
|
-
params.set(
|
|
1326
|
+
params.set(body, next);
|
|
1397
1327
|
i += 2;
|
|
1398
1328
|
} else {
|
|
1399
|
-
params.set(
|
|
1329
|
+
params.set(body, "true");
|
|
1400
1330
|
i++;
|
|
1401
1331
|
}
|
|
1402
1332
|
continue;
|
|
@@ -1462,7 +1392,7 @@ const queryToCliArgs = (url, reservedKeys = []) => {
|
|
|
1462
1392
|
};
|
|
1463
1393
|
//#endregion
|
|
1464
1394
|
//#region lib/cli/routes/channels.add.ts
|
|
1465
|
-
const help$
|
|
1395
|
+
const help$15 = `funnel channels add — add a channel
|
|
1466
1396
|
|
|
1467
1397
|
usage: funnel channels add <name> [--delivery fanout|exclusive]
|
|
1468
1398
|
|
|
@@ -1480,7 +1410,29 @@ examples:
|
|
|
1480
1410
|
funnel channels add ci-events --delivery exclusive
|
|
1481
1411
|
|
|
1482
1412
|
see also: funnel channels, funnel channels <name> connectors add`;
|
|
1483
|
-
const channelsAddHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1413
|
+
const channelsAddHelpHandler = factory.createHandlers((c) => c.text(help$15));
|
|
1414
|
+
//#endregion
|
|
1415
|
+
//#region lib/cli/router/validator.ts
|
|
1416
|
+
const labelFor = (target, key) => {
|
|
1417
|
+
if (target === "query") return `--${key}`;
|
|
1418
|
+
return `<${key}>`;
|
|
1419
|
+
};
|
|
1420
|
+
const formatIssues = (target, issues) => {
|
|
1421
|
+
return `invalid arguments — ${issues.map((issue) => {
|
|
1422
|
+
const key = issue.path.map(String).join(".");
|
|
1423
|
+
if (!key) return issue.message;
|
|
1424
|
+
return `${labelFor(target, key)}: ${issue.message}`;
|
|
1425
|
+
}).join("; ")} (run with --help for usage)`;
|
|
1426
|
+
};
|
|
1427
|
+
/**
|
|
1428
|
+
* CLI-flavored zValidator: every route imports this instead of the raw
|
|
1429
|
+
* @hono/zod-validator so a validation failure renders as one readable line
|
|
1430
|
+
* naming the offending flag, not a raw ZodError JSON dump.
|
|
1431
|
+
*/
|
|
1432
|
+
const zValidator$1 = (target, schema) => zValidator(target, schema, (result) => {
|
|
1433
|
+
if (result.success) return;
|
|
1434
|
+
throw new HTTPException(400, { message: formatIssues(target, result.error.issues) });
|
|
1435
|
+
});
|
|
1484
1436
|
//#endregion
|
|
1485
1437
|
//#region lib/cli/routes/channels.add.$channel.ts
|
|
1486
1438
|
const channelsAddHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({ delivery: channelDeliveryModeSchema.optional() })), (c) => {
|
|
@@ -1493,6 +1445,17 @@ const channelsAddHandler = factory.createHandlers(zValidator$1("param", z.object
|
|
|
1493
1445
|
return c.text(`added channel "${created.name}" (id: ${created.id})`);
|
|
1494
1446
|
});
|
|
1495
1447
|
//#endregion
|
|
1448
|
+
//#region lib/cli/routes/not-found-message.ts
|
|
1449
|
+
/**
|
|
1450
|
+
* One error shape for every name-resolution miss: what was asked, what exists,
|
|
1451
|
+
* and the command that creates it — so a Claude (or human) can self-correct
|
|
1452
|
+
* without a follow-up listing call.
|
|
1453
|
+
*/
|
|
1454
|
+
const notFoundMessage = (props) => {
|
|
1455
|
+
const listed = props.available.length > 0 ? props.available.join(", ") : "none";
|
|
1456
|
+
return `${props.kind} "${props.name}" not found (available: ${listed}); to create one: ${props.nextAction}`;
|
|
1457
|
+
};
|
|
1458
|
+
//#endregion
|
|
1496
1459
|
//#region lib/cli/router/help-guard.ts
|
|
1497
1460
|
function helpGuard(help) {
|
|
1498
1461
|
return async (c, next) => {
|
|
@@ -1517,14 +1480,20 @@ subcommands:
|
|
|
1517
1480
|
<c> schedules add <id> --cron=... --prompt=... / add a schedule entry
|
|
1518
1481
|
<c> schedules remove <id> / remove a schedule entry`), (c) => {
|
|
1519
1482
|
const param = c.req.valid("param");
|
|
1520
|
-
const
|
|
1521
|
-
|
|
1483
|
+
const funnel = c.env.funnel;
|
|
1484
|
+
const channel = funnel.channels.get(param.channel);
|
|
1485
|
+
if (!channel) throw new HTTPException(404, { message: notFoundMessage({
|
|
1486
|
+
kind: "channel",
|
|
1487
|
+
name: param.channel,
|
|
1488
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
1489
|
+
nextAction: "fnl channels add <name>"
|
|
1490
|
+
}) });
|
|
1522
1491
|
if (channel.connectors.length === 0) return c.text(`no connectors in channel "${channel.name}"`);
|
|
1523
1492
|
return c.text(channel.connectors.map((c) => `${c.name} (${c.type}, id: ${c.id})`).join("\n"));
|
|
1524
1493
|
});
|
|
1525
1494
|
//#endregion
|
|
1526
1495
|
//#region lib/cli/routes/channels.$channel.connectors.add.ts
|
|
1527
|
-
const help$
|
|
1496
|
+
const help$14 = `funnel channels <channel> connectors add <name> — add a connector to a channel
|
|
1528
1497
|
|
|
1529
1498
|
usage:
|
|
1530
1499
|
funnel channels <channel> connectors add <name> --type=slack --bot-token=xoxb-... --app-token=xapp-...
|
|
@@ -1547,7 +1516,7 @@ examples:
|
|
|
1547
1516
|
funnel channels alerts connectors add daily --type=schedule
|
|
1548
1517
|
|
|
1549
1518
|
see also: funnel channels <channel> connectors, funnel channels <channel> connectors remove`;
|
|
1550
|
-
const channelsConnectorsAddHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1519
|
+
const channelsConnectorsAddHelpHandler = factory.createHandlers((c) => c.text(help$14));
|
|
1551
1520
|
//#endregion
|
|
1552
1521
|
//#region lib/cli/routes/channels.$channel.connectors.add.$connector.ts
|
|
1553
1522
|
const slackBody = z.object({
|
|
@@ -1615,7 +1584,7 @@ const channelsConnectorsAddHandler = factory.createHandlers(zValidator$1("param"
|
|
|
1615
1584
|
});
|
|
1616
1585
|
//#endregion
|
|
1617
1586
|
//#region lib/cli/routes/channels.$channel.connectors.remove.ts
|
|
1618
|
-
const help$
|
|
1587
|
+
const help$13 = `funnel channels <channel> connectors remove <connector> — remove a connector
|
|
1619
1588
|
|
|
1620
1589
|
usage: funnel channels <channel> connectors remove <connector>
|
|
1621
1590
|
|
|
@@ -1627,7 +1596,7 @@ examples:
|
|
|
1627
1596
|
funnel channels production connectors remove slack-main
|
|
1628
1597
|
|
|
1629
1598
|
see also: funnel channels <channel> connectors, funnel channels <channel> connectors add`;
|
|
1630
|
-
const channelsConnectorsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1599
|
+
const channelsConnectorsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$13));
|
|
1631
1600
|
//#endregion
|
|
1632
1601
|
//#region lib/cli/routes/channels.$channel.connectors.remove.$connector.ts
|
|
1633
1602
|
const channelsConnectorsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
@@ -1642,13 +1611,13 @@ const channelsConnectorsRemoveHandler = factory.createHandlers(zValidator$1("par
|
|
|
1642
1611
|
});
|
|
1643
1612
|
//#endregion
|
|
1644
1613
|
//#region lib/cli/routes/channels.$channel.connectors.set.ts
|
|
1645
|
-
const help$
|
|
1614
|
+
const help$12 = `funnel channels <channel> connectors set <connector> — update connector fields
|
|
1646
1615
|
|
|
1647
1616
|
usage:
|
|
1648
1617
|
funnel channels <ch> connectors set <conn> [--bot-token=...] [--app-token=...] # slack
|
|
1649
1618
|
funnel channels <ch> connectors set <conn> [--bot-token=...] # discord
|
|
1650
1619
|
funnel channels <ch> connectors set <conn> [--poll-interval=N] # gh`;
|
|
1651
|
-
const channelsConnectorsSetHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1620
|
+
const channelsConnectorsSetHelpHandler = factory.createHandlers((c) => c.text(help$12));
|
|
1652
1621
|
//#endregion
|
|
1653
1622
|
//#region lib/cli/routes/channels.$channel.connectors.set.$connector.ts
|
|
1654
1623
|
const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
@@ -1663,7 +1632,12 @@ const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param"
|
|
|
1663
1632
|
const query = c.req.valid("query");
|
|
1664
1633
|
const funnel = c.env.funnel;
|
|
1665
1634
|
const existing = funnel.channels.getConnector(param.channel, param.connector);
|
|
1666
|
-
if (!existing) throw new HTTPException(404, { message:
|
|
1635
|
+
if (!existing) throw new HTTPException(404, { message: notFoundMessage({
|
|
1636
|
+
kind: "connector",
|
|
1637
|
+
name: param.connector,
|
|
1638
|
+
available: (funnel.channels.get(param.channel)?.connectors ?? []).map((conn) => conn.name),
|
|
1639
|
+
nextAction: `fnl channels ${param.channel} connectors add <name> --type=slack|gh|discord|schedule ...`
|
|
1640
|
+
}) });
|
|
1667
1641
|
if (existing.type === "slack") funnel.channels.updateSlackConnector(param.channel, param.connector, {
|
|
1668
1642
|
...query["bot-token"] !== void 0 ? { botToken: query["bot-token"] } : {},
|
|
1669
1643
|
...query["app-token"] !== void 0 ? { appToken: query["app-token"] } : {}
|
|
@@ -1686,15 +1660,21 @@ subcommands:
|
|
|
1686
1660
|
|
|
1687
1661
|
output / valid YAML`), (c) => {
|
|
1688
1662
|
const param = c.req.valid("param");
|
|
1689
|
-
const
|
|
1690
|
-
|
|
1663
|
+
const funnel = c.env.funnel;
|
|
1664
|
+
const connector = funnel.channels.getConnector(param.channel, param.connector);
|
|
1665
|
+
if (!connector) throw new HTTPException(404, { message: notFoundMessage({
|
|
1666
|
+
kind: "connector",
|
|
1667
|
+
name: param.connector,
|
|
1668
|
+
available: (funnel.channels.get(param.channel)?.connectors ?? []).map((conn) => conn.name),
|
|
1669
|
+
nextAction: `fnl channels ${param.channel} connectors add <name> --type=slack|gh|discord|schedule ...`
|
|
1670
|
+
}) });
|
|
1691
1671
|
return c.text(renderYaml(connector));
|
|
1692
1672
|
});
|
|
1693
1673
|
//#endregion
|
|
1694
1674
|
//#region lib/cli/routes/channels.$channel.connectors.rename.ts
|
|
1695
|
-
const help$
|
|
1675
|
+
const help$11 = `funnel channels <channel> connectors rename <old-connector-name> <new-connector-name> — rename a connector
|
|
1696
1676
|
|
|
1697
|
-
usage: funnel channels <channel> connectors rename <old> <new>
|
|
1677
|
+
usage: funnel channels <channel> connectors rename <old-connector-name> <new-connector-name>
|
|
1698
1678
|
|
|
1699
1679
|
Renames the connector in the configuration file. Tokens, type, and
|
|
1700
1680
|
schedules are preserved. The gateway picks up the new name on the
|
|
@@ -1704,7 +1684,7 @@ examples:
|
|
|
1704
1684
|
funnel channels production connectors rename slack-1 slack-main
|
|
1705
1685
|
|
|
1706
1686
|
see also: funnel channels <channel> connectors`;
|
|
1707
|
-
const channelsConnectorsRenameHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1687
|
+
const channelsConnectorsRenameHelpHandler = factory.createHandlers((c) => c.text(help$11));
|
|
1708
1688
|
//#endregion
|
|
1709
1689
|
//#region lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts
|
|
1710
1690
|
const channelsConnectorsRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
@@ -1721,10 +1701,10 @@ const channelsConnectorsRenameHandler = factory.createHandlers(zValidator$1("par
|
|
|
1721
1701
|
});
|
|
1722
1702
|
//#endregion
|
|
1723
1703
|
//#region lib/cli/routes/channels.$channel.connectors.$connector.rename.ts
|
|
1724
|
-
const help$
|
|
1704
|
+
const help$10 = `funnel channels <channel> connectors rename <connector> <new-name>
|
|
1725
1705
|
|
|
1726
1706
|
usage: funnel channels <channel> connectors rename <connector> <new-name>`;
|
|
1727
|
-
const channelsConnectorRenameHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1707
|
+
const channelsConnectorRenameHelpHandler = factory.createHandlers((c) => c.text(help$10));
|
|
1728
1708
|
const channelsConnectorsRequestHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
1729
1709
|
channel: z.string(),
|
|
1730
1710
|
connector: z.string()
|
|
@@ -1767,16 +1747,39 @@ subcommands:
|
|
|
1767
1747
|
add <id> --cron=... --prompt=... [--enabled=true] [--catchup-policy=latest|all|skip] / add entry
|
|
1768
1748
|
remove <id> / remove entry`), (c) => {
|
|
1769
1749
|
const param = c.req.valid("param");
|
|
1770
|
-
const
|
|
1750
|
+
const funnel = c.env.funnel;
|
|
1751
|
+
const entries = z.array(scheduleEntrySchema).parse(funnel.channels.connectorOp(param.channel, param.connector, "listEntries", void 0));
|
|
1771
1752
|
if (entries.length === 0) return c.text("no schedule entries");
|
|
1772
1753
|
return c.text(entries.map((e) => `${e.id}\t${e.cron}\t${e.enabled ? "on" : "off"}\t${e.prompt}`).join("\n"));
|
|
1773
1754
|
});
|
|
1774
1755
|
//#endregion
|
|
1775
1756
|
//#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.ts
|
|
1776
|
-
const help$
|
|
1757
|
+
const help$9 = `funnel channels <ch> connectors <conn> schedules add <id> — add a schedule entry
|
|
1777
1758
|
|
|
1778
|
-
usage: funnel channels <ch> connectors <conn> schedules add <id> --cron="*/5 * * * *" --prompt="..." [--enabled=
|
|
1779
|
-
|
|
1759
|
+
usage: funnel channels <ch> connectors <conn> schedules add <id> --cron="*/5 * * * *" --prompt="..." [--enabled|--enabled=false] [--catchup-policy=latest|all|skip]
|
|
1760
|
+
|
|
1761
|
+
options:
|
|
1762
|
+
--cron <expr> / 5-field cron expression (required)
|
|
1763
|
+
--prompt <text> / prompt delivered on each fire (required)
|
|
1764
|
+
--enabled / fire on schedule (default: true; --enabled=false stores it disabled)
|
|
1765
|
+
--catchup-policy latest|all|skip / how missed fires are replayed after downtime (default: latest)`;
|
|
1766
|
+
const channelsConnectorSchedulesAddHelpHandler = factory.createHandlers((c) => c.text(help$9));
|
|
1767
|
+
//#endregion
|
|
1768
|
+
//#region lib/cli/router/boolean-flag.ts
|
|
1769
|
+
/**
|
|
1770
|
+
* One parser for every CLI boolean flag: bare `--flag` (and `--flag=true`)
|
|
1771
|
+
* mean true, `--flag=false` means false, absent stays undefined so routes can
|
|
1772
|
+
* distinguish "not given" from "explicitly off". `""` survives for callers
|
|
1773
|
+
* that hit the HTTP surface directly with `?flag=`.
|
|
1774
|
+
*/
|
|
1775
|
+
const booleanFlag = z.enum([
|
|
1776
|
+
"true",
|
|
1777
|
+
"false",
|
|
1778
|
+
""
|
|
1779
|
+
]).optional().transform((value) => {
|
|
1780
|
+
if (value === void 0) return void 0;
|
|
1781
|
+
return value !== "false";
|
|
1782
|
+
});
|
|
1780
1783
|
//#endregion
|
|
1781
1784
|
//#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts
|
|
1782
1785
|
const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
@@ -1786,28 +1789,28 @@ const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$
|
|
|
1786
1789
|
})), zValidator$1("query", z.object({
|
|
1787
1790
|
cron: z.string(),
|
|
1788
1791
|
prompt: z.string(),
|
|
1789
|
-
enabled:
|
|
1792
|
+
enabled: booleanFlag,
|
|
1790
1793
|
"catchup-policy": scheduleCatchupPolicySchema.optional()
|
|
1791
1794
|
})), async (c) => {
|
|
1792
1795
|
const param = c.req.valid("param");
|
|
1793
1796
|
const query = c.req.valid("query");
|
|
1794
1797
|
const funnel = c.env.funnel;
|
|
1795
|
-
const entry = funnel.channels.
|
|
1798
|
+
const entry = scheduleEntrySchema.parse(funnel.channels.connectorOp(param.channel, param.connector, "addEntry", {
|
|
1796
1799
|
id: param.id,
|
|
1797
1800
|
cron: query.cron,
|
|
1798
1801
|
prompt: query.prompt,
|
|
1799
|
-
...query.enabled !== void 0 ? { enabled: query.enabled
|
|
1802
|
+
...query.enabled !== void 0 ? { enabled: query.enabled } : {},
|
|
1800
1803
|
...query["catchup-policy"] !== void 0 ? { catchupPolicy: query["catchup-policy"] } : {}
|
|
1801
|
-
});
|
|
1804
|
+
}));
|
|
1802
1805
|
await funnel.listeners.restart(param.channel, param.connector);
|
|
1803
1806
|
return c.text(`added schedule entry "${entry.id}"`);
|
|
1804
1807
|
});
|
|
1805
1808
|
//#endregion
|
|
1806
1809
|
//#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.ts
|
|
1807
|
-
const help$
|
|
1810
|
+
const help$8 = `funnel channels <ch> connectors <conn> schedules remove <id>
|
|
1808
1811
|
|
|
1809
1812
|
usage: funnel channels <ch> connectors <conn> schedules remove <id>`;
|
|
1810
|
-
const channelsConnectorSchedulesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1813
|
+
const channelsConnectorSchedulesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$8));
|
|
1811
1814
|
//#endregion
|
|
1812
1815
|
//#region lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts
|
|
1813
1816
|
const channelsConnectorsSchedulesRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
@@ -1817,13 +1820,13 @@ const channelsConnectorsSchedulesRemoveHandler = factory.createHandlers(zValidat
|
|
|
1817
1820
|
})), zValidator$1("query", z.object({})), async (c) => {
|
|
1818
1821
|
const param = c.req.valid("param");
|
|
1819
1822
|
const funnel = c.env.funnel;
|
|
1820
|
-
funnel.channels.
|
|
1823
|
+
funnel.channels.connectorOp(param.channel, param.connector, "removeEntry", { id: param.id });
|
|
1821
1824
|
await funnel.listeners.restart(param.channel, param.connector);
|
|
1822
1825
|
return c.text(`removed schedule entry "${param.id}"`);
|
|
1823
1826
|
});
|
|
1824
1827
|
//#endregion
|
|
1825
1828
|
//#region lib/cli/routes/channels.publish.ts
|
|
1826
|
-
const help$
|
|
1829
|
+
const help$7 = `funnel channels <channel> publish — push arbitrary content into a channel
|
|
1827
1830
|
|
|
1828
1831
|
usage: funnel channels <channel> publish --content="<text>" [--connector=<name>] [--meta-<key>=<value> ...]
|
|
1829
1832
|
|
|
@@ -1831,7 +1834,7 @@ options:
|
|
|
1831
1834
|
--content Required. The event body delivered to subscribers.
|
|
1832
1835
|
--connector Optional. Stamp the event with a connector name (resolved to id when found).
|
|
1833
1836
|
--meta-<key> Optional. Repeatable. Added to meta. Example: --meta-source=cron`;
|
|
1834
|
-
const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1837
|
+
const channelsPublishHelpHandler = factory.createHandlers((c) => c.text(help$7));
|
|
1835
1838
|
//#endregion
|
|
1836
1839
|
//#region lib/cli/routes/channels.$channel.publish.ts
|
|
1837
1840
|
const querySchema = z.object({
|
|
@@ -1855,7 +1858,7 @@ const channelsPublishHandler = factory.createHandlers(zValidator$1("param", z.ob
|
|
|
1855
1858
|
});
|
|
1856
1859
|
//#endregion
|
|
1857
1860
|
//#region lib/cli/routes/channels.remove.ts
|
|
1858
|
-
const help$
|
|
1861
|
+
const help$6 = `funnel channels remove — remove a channel and all its connectors
|
|
1859
1862
|
|
|
1860
1863
|
usage: funnel channels remove <name>
|
|
1861
1864
|
|
|
@@ -1867,21 +1870,28 @@ examples:
|
|
|
1867
1870
|
funnel channels remove staging
|
|
1868
1871
|
|
|
1869
1872
|
see also: funnel channels, funnel channels add`;
|
|
1870
|
-
const channelsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$
|
|
1873
|
+
const channelsRemoveHelpHandler = factory.createHandlers((c) => c.text(help$6));
|
|
1871
1874
|
//#endregion
|
|
1872
1875
|
//#region lib/cli/routes/channels.remove.$channel.ts
|
|
1873
1876
|
const channelsRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({})), (c) => {
|
|
1874
1877
|
const param = c.req.valid("param");
|
|
1875
|
-
c.env.funnel
|
|
1878
|
+
const funnel = c.env.funnel;
|
|
1879
|
+
if (!funnel.channels.get(param.channel)) throw new HTTPException(404, { message: notFoundMessage({
|
|
1880
|
+
kind: "channel",
|
|
1881
|
+
name: param.channel,
|
|
1882
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
1883
|
+
nextAction: "fnl channels add <name>"
|
|
1884
|
+
}) });
|
|
1885
|
+
funnel.channels.remove(param.channel);
|
|
1876
1886
|
return c.text(`removed channel "${param.channel}"`);
|
|
1877
1887
|
});
|
|
1878
1888
|
//#endregion
|
|
1879
1889
|
//#region lib/cli/routes/channels.rename.ts
|
|
1880
|
-
const
|
|
1890
|
+
const channelsRenameHelp = `funnel channels rename — rename a channel
|
|
1881
1891
|
|
|
1882
1892
|
usage:
|
|
1883
|
-
funnel channels rename <old> <new>
|
|
1884
|
-
funnel channels <old> rename <new>
|
|
1893
|
+
funnel channels rename <old-channel-name> <new-channel-name>
|
|
1894
|
+
funnel channels <old-channel-name> rename <new-channel-name>
|
|
1885
1895
|
|
|
1886
1896
|
Renames the channel in the configuration file. Connectors, schedules,
|
|
1887
1897
|
and delivery mode are preserved. The gateway picks up the new name on
|
|
@@ -1892,15 +1902,10 @@ examples:
|
|
|
1892
1902
|
funnel channels staging rename production
|
|
1893
1903
|
|
|
1894
1904
|
see also: funnel channels, funnel channels <name>`;
|
|
1895
|
-
const channelsRenameHelpHandler = factory.createHandlers((c) => c.text(
|
|
1905
|
+
const channelsRenameHelpHandler = factory.createHandlers((c) => c.text(channelsRenameHelp));
|
|
1896
1906
|
//#endregion
|
|
1897
1907
|
//#region lib/cli/routes/channels.$channel.rename.ts
|
|
1898
|
-
const
|
|
1899
|
-
|
|
1900
|
-
usage:
|
|
1901
|
-
funnel channels rename <old> <new>
|
|
1902
|
-
funnel channels <old> rename <new>`;
|
|
1903
|
-
const channelsChannelRenameHelpHandler = factory.createHandlers((c) => c.text(help$6));
|
|
1908
|
+
const channelsChannelRenameHelpHandler = factory.createHandlers((c) => c.text(channelsRenameHelp));
|
|
1904
1909
|
//#endregion
|
|
1905
1910
|
//#region lib/cli/routes/channels.$channel.rename.$newName.ts
|
|
1906
1911
|
const channelsRenameHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
@@ -1908,7 +1913,14 @@ const channelsRenameHandler = factory.createHandlers(zValidator$1("param", z.obj
|
|
|
1908
1913
|
newName: z.string()
|
|
1909
1914
|
})), zValidator$1("query", z.object({})), (c) => {
|
|
1910
1915
|
const param = c.req.valid("param");
|
|
1911
|
-
c.env.funnel
|
|
1916
|
+
const funnel = c.env.funnel;
|
|
1917
|
+
if (!funnel.channels.get(param.channel)) throw new HTTPException(404, { message: notFoundMessage({
|
|
1918
|
+
kind: "channel",
|
|
1919
|
+
name: param.channel,
|
|
1920
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
1921
|
+
nextAction: "fnl channels add <name>"
|
|
1922
|
+
}) });
|
|
1923
|
+
funnel.channels.rename(param.channel, param.newName);
|
|
1912
1924
|
return c.text(`renamed channel "${param.channel}" to "${param.newName}"`);
|
|
1913
1925
|
});
|
|
1914
1926
|
const channelsSetDeliveryHandler = factory.createHandlers(helpGuard(`funnel channels <name> set delivery <mode> — change a channel's routing mode
|
|
@@ -1938,8 +1950,14 @@ subcommands:
|
|
|
1938
1950
|
|
|
1939
1951
|
output / valid YAML`), (c) => {
|
|
1940
1952
|
const param = c.req.valid("param");
|
|
1941
|
-
const
|
|
1942
|
-
|
|
1953
|
+
const funnel = c.env.funnel;
|
|
1954
|
+
const channel = funnel.channels.get(param.channel);
|
|
1955
|
+
if (!channel) throw new HTTPException(404, { message: notFoundMessage({
|
|
1956
|
+
kind: "channel",
|
|
1957
|
+
name: param.channel,
|
|
1958
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
1959
|
+
nextAction: "fnl channels add <name>"
|
|
1960
|
+
}) });
|
|
1943
1961
|
return c.text(renderYaml({
|
|
1944
1962
|
id: channel.id,
|
|
1945
1963
|
name: channel.name,
|
|
@@ -2060,8 +2078,14 @@ const validateConnector = (connector) => {
|
|
|
2060
2078
|
};
|
|
2061
2079
|
const channelsValidateHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({})), (c) => {
|
|
2062
2080
|
const param = c.req.valid("param");
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2081
|
+
const funnel = c.env.funnel;
|
|
2082
|
+
const channel = funnel.channels.get(param.channel);
|
|
2083
|
+
if (!channel) throw new HTTPException(404, { message: notFoundMessage({
|
|
2084
|
+
kind: "channel",
|
|
2085
|
+
name: param.channel,
|
|
2086
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
2087
|
+
nextAction: "fnl channels add <name>"
|
|
2088
|
+
}) });
|
|
2065
2089
|
if (channel.connectors.length === 0) return c.text(renderYaml({
|
|
2066
2090
|
channel: channel.name,
|
|
2067
2091
|
valid: false,
|
|
@@ -2132,7 +2156,12 @@ const claudeHandler = factory.createHandlers(helpGuard(claudeHelp), zValidator$1
|
|
|
2132
2156
|
}
|
|
2133
2157
|
if (query.profile) {
|
|
2134
2158
|
const profile = profiles.get(query.profile);
|
|
2135
|
-
if (!profile) throw new HTTPException(404, { message:
|
|
2159
|
+
if (!profile) throw new HTTPException(404, { message: notFoundMessage({
|
|
2160
|
+
kind: "profile",
|
|
2161
|
+
name: query.profile,
|
|
2162
|
+
available: profiles.list().map((p) => p.name),
|
|
2163
|
+
nextAction: "fnl profiles add <name> --path=<repo> --channel=<channel>"
|
|
2164
|
+
}) });
|
|
2136
2165
|
const exitCode = await claude.launch({
|
|
2137
2166
|
channel: profile.channelId,
|
|
2138
2167
|
cwd: profile.path,
|
|
@@ -2265,16 +2294,12 @@ const resolveTargetChannel = (c, channelArg) => {
|
|
|
2265
2294
|
};
|
|
2266
2295
|
const debugHandler = factory.createHandlers(helpGuard(debugHelp), zValidator$1("query", z.object({
|
|
2267
2296
|
channel: z.string().optional(),
|
|
2268
|
-
all:
|
|
2269
|
-
"true",
|
|
2270
|
-
"false",
|
|
2271
|
-
""
|
|
2272
|
-
]).optional(),
|
|
2297
|
+
all: booleanFlag,
|
|
2273
2298
|
limit: z.string().optional()
|
|
2274
2299
|
})), async (c) => {
|
|
2275
2300
|
const query = c.req.valid("query");
|
|
2276
2301
|
const funnel = c.env.funnel;
|
|
2277
|
-
if (query.all ===
|
|
2302
|
+
if (query.all === true) {
|
|
2278
2303
|
const report = await funnel.diagnostics.diagnoseAll();
|
|
2279
2304
|
return c.text(renderYaml(report));
|
|
2280
2305
|
}
|
|
@@ -2385,20 +2410,12 @@ examples:
|
|
|
2385
2410
|
funnel doctor
|
|
2386
2411
|
funnel doctor --fix
|
|
2387
2412
|
funnel doctor --fix --aggressive`), zValidator$1("query", z.object({
|
|
2388
|
-
fix:
|
|
2389
|
-
|
|
2390
|
-
"false",
|
|
2391
|
-
""
|
|
2392
|
-
]).optional(),
|
|
2393
|
-
aggressive: z.enum([
|
|
2394
|
-
"true",
|
|
2395
|
-
"false",
|
|
2396
|
-
""
|
|
2397
|
-
]).optional()
|
|
2413
|
+
fix: booleanFlag,
|
|
2414
|
+
aggressive: booleanFlag
|
|
2398
2415
|
})), async (c) => {
|
|
2399
2416
|
const query = c.req.valid("query");
|
|
2400
|
-
const wantsFix = query.fix ===
|
|
2401
|
-
const wantsAggressive = query.aggressive ===
|
|
2417
|
+
const wantsFix = query.fix === true;
|
|
2418
|
+
const wantsAggressive = query.aggressive === true;
|
|
2402
2419
|
const mode = wantsFix ? wantsAggressive ? "aggressive" : "safe" : "off";
|
|
2403
2420
|
const report = await c.env.funnel.doctor.run(mode);
|
|
2404
2421
|
return c.text(renderYaml(report));
|
|
@@ -2437,7 +2454,7 @@ examples:
|
|
|
2437
2454
|
const renderGatewayStatus = async (c) => {
|
|
2438
2455
|
const status = c.env.funnel.gateway.getStatus();
|
|
2439
2456
|
if (!status.running) return c.text(renderYaml({ running: false }), 503);
|
|
2440
|
-
const res = await fetch(
|
|
2457
|
+
const res = await fetch(`${gatewayLoopbackUrl(status.port)}/status`).catch(() => null);
|
|
2441
2458
|
if (!res) return c.text(renderYaml({
|
|
2442
2459
|
running: true,
|
|
2443
2460
|
pid: status.pid,
|
|
@@ -2747,6 +2764,16 @@ examples:
|
|
|
2747
2764
|
funnel gateway start --no-caffeine
|
|
2748
2765
|
|
|
2749
2766
|
programmable: funnel.gateway.start({ caffeinate })`;
|
|
2767
|
+
const HEALTH_TIMEOUT_MS = 5e3;
|
|
2768
|
+
const HEALTH_POLL_INTERVAL_MS = 100;
|
|
2769
|
+
const waitForHealth = async (port) => {
|
|
2770
|
+
const deadline = Date.now() + HEALTH_TIMEOUT_MS;
|
|
2771
|
+
while (Date.now() < deadline) {
|
|
2772
|
+
if ((await fetch(`${gatewayLoopbackUrl(port)}/health`).catch(() => null))?.ok) return true;
|
|
2773
|
+
await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_INTERVAL_MS));
|
|
2774
|
+
}
|
|
2775
|
+
return false;
|
|
2776
|
+
};
|
|
2750
2777
|
const gatewayStartHandler = factory.createHandlers(helpGuard(startHelp), zValidator$1("query", z.object({ "no-caffeine": z.string().optional() })), async (c) => {
|
|
2751
2778
|
const query = c.req.valid("query");
|
|
2752
2779
|
const funnel = c.env.funnel;
|
|
@@ -2754,8 +2781,10 @@ const gatewayStartHandler = factory.createHandlers(helpGuard(startHelp), zValida
|
|
|
2754
2781
|
const status = funnel.gateway.getStatus();
|
|
2755
2782
|
return c.text(`funnel gateway: already running (pid ${status.pid})`);
|
|
2756
2783
|
}
|
|
2757
|
-
if (!await funnel.gateway.start({ caffeinate: query["no-caffeine"] !== "true" })) throw new HTTPException(500, { message: "funnel gateway: failed to start" });
|
|
2758
|
-
|
|
2784
|
+
if (!await funnel.gateway.start({ caffeinate: query["no-caffeine"] !== "true" })) throw new HTTPException(500, { message: "funnel gateway: failed to start — inspect the daemon log with `fnl gateway logs`" });
|
|
2785
|
+
const status = funnel.gateway.getStatus();
|
|
2786
|
+
if (!await waitForHealth(status.port)) return c.text(`funnel gateway: started (pid ${status.pid}, port ${status.port}) but /health did not respond within ${HEALTH_TIMEOUT_MS / 1e3}s — check \`fnl gateway logs\``);
|
|
2787
|
+
return c.text(`funnel gateway: started (pid ${status.pid}, port ${status.port})`);
|
|
2759
2788
|
});
|
|
2760
2789
|
const gatewayStatusHandler = factory.createHandlers(helpGuard(`funnel gateway status / show gateway running status
|
|
2761
2790
|
|
|
@@ -2845,7 +2874,12 @@ const profilesAddHandler = factory.createHandlers(zValidator$1("param", z.object
|
|
|
2845
2874
|
const funnel = c.env.funnel;
|
|
2846
2875
|
const { profiles, claude } = c.env;
|
|
2847
2876
|
const channel = funnel.channels.get(query.channel);
|
|
2848
|
-
if (!channel) throw new HTTPException(400, { message:
|
|
2877
|
+
if (!channel) throw new HTTPException(400, { message: notFoundMessage({
|
|
2878
|
+
kind: "channel",
|
|
2879
|
+
name: query.channel,
|
|
2880
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
2881
|
+
nextAction: "fnl channels add <name>"
|
|
2882
|
+
}) });
|
|
2849
2883
|
const recipe = parseProfileRecipe(query);
|
|
2850
2884
|
profiles.add({
|
|
2851
2885
|
name: param.profile,
|
|
@@ -2908,7 +2942,12 @@ const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.obj
|
|
|
2908
2942
|
c.env.funnel;
|
|
2909
2943
|
const { profiles, claude } = c.env;
|
|
2910
2944
|
const profile = profiles.get(param.profile);
|
|
2911
|
-
if (!profile) throw new HTTPException(404, { message:
|
|
2945
|
+
if (!profile) throw new HTTPException(404, { message: notFoundMessage({
|
|
2946
|
+
kind: "profile",
|
|
2947
|
+
name: param.profile,
|
|
2948
|
+
available: profiles.list().map((p) => p.name),
|
|
2949
|
+
nextAction: "fnl profiles add <name> --path=<repo> --channel=<channel>"
|
|
2950
|
+
}) });
|
|
2912
2951
|
const exitCode = await claude.launch({
|
|
2913
2952
|
channel: profile.channelId,
|
|
2914
2953
|
cwd: profile.path,
|
|
@@ -2968,7 +3007,12 @@ const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object
|
|
|
2968
3007
|
const funnel = c.env.funnel;
|
|
2969
3008
|
const { profiles, claude } = c.env;
|
|
2970
3009
|
const channel = query.channel !== void 0 ? funnel.channels.get(query.channel) : null;
|
|
2971
|
-
if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message:
|
|
3010
|
+
if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message: notFoundMessage({
|
|
3011
|
+
kind: "channel",
|
|
3012
|
+
name: query.channel,
|
|
3013
|
+
available: funnel.channels.list().map((ch) => ch.name),
|
|
3014
|
+
nextAction: "fnl channels add <name>"
|
|
3015
|
+
}) });
|
|
2972
3016
|
const recipe = parseProfileRecipe(query);
|
|
2973
3017
|
profiles.update(param.profile, {
|
|
2974
3018
|
path: query.path,
|
|
@@ -3044,7 +3088,7 @@ const statusHelp = `funnel status / overall health snapshot
|
|
|
3044
3088
|
usage / funnel status [--watch] [--interval <N>]
|
|
3045
3089
|
|
|
3046
3090
|
options:
|
|
3047
|
-
--watch / continuously refresh (Ctrl+C to stop)
|
|
3091
|
+
--watch / continuously refresh (default: off; Ctrl+C to stop)
|
|
3048
3092
|
--interval <N> / polling interval in seconds (default 3)
|
|
3049
3093
|
|
|
3050
3094
|
output / valid YAML
|
|
@@ -3069,7 +3113,7 @@ const buildStatusReport = async (funnel, profiles) => {
|
|
|
3069
3113
|
const gatewayStatus = funnel.gateway.getStatus();
|
|
3070
3114
|
let gatewayData = null;
|
|
3071
3115
|
if (gatewayStatus.running) {
|
|
3072
|
-
const res = await fetch(
|
|
3116
|
+
const res = await fetch(`${gatewayLoopbackUrl(gatewayStatus.port)}/status`).catch(() => null);
|
|
3073
3117
|
if (res && res.ok) {
|
|
3074
3118
|
const body = await res.json();
|
|
3075
3119
|
if (isGatewayStatus(body)) gatewayData = body;
|
|
@@ -3090,6 +3134,7 @@ const buildStatusReport = async (funnel, profiles) => {
|
|
|
3090
3134
|
return {
|
|
3091
3135
|
gateway: gatewayStatus.running ? {
|
|
3092
3136
|
running: true,
|
|
3137
|
+
responsive: gatewayData !== null,
|
|
3093
3138
|
pid: gatewayStatus.pid,
|
|
3094
3139
|
port: gatewayStatus.port,
|
|
3095
3140
|
uptimeMs: gatewayData?.uptimeMs ?? null
|
|
@@ -3116,16 +3161,12 @@ const buildStatusReport = async (funnel, profiles) => {
|
|
|
3116
3161
|
};
|
|
3117
3162
|
};
|
|
3118
3163
|
const statusHandler = factory.createHandlers(helpGuard(statusHelp), zValidator$1("query", z.object({
|
|
3119
|
-
watch:
|
|
3120
|
-
"true",
|
|
3121
|
-
"false",
|
|
3122
|
-
""
|
|
3123
|
-
]).optional(),
|
|
3164
|
+
watch: booleanFlag,
|
|
3124
3165
|
interval: z.string().optional()
|
|
3125
3166
|
})), async (c) => {
|
|
3126
3167
|
const query = c.req.valid("query");
|
|
3127
3168
|
const funnel = c.env.funnel;
|
|
3128
|
-
const isWatch = query.watch ===
|
|
3169
|
+
const isWatch = query.watch === true;
|
|
3129
3170
|
const intervalSec = Math.min(60, Math.max(1, query.interval ? Number(query.interval) : 3));
|
|
3130
3171
|
if (!isWatch) {
|
|
3131
3172
|
const report = await buildStatusReport(funnel, c.env.profiles);
|
|
@@ -3176,4 +3217,4 @@ const routes = factory.createApp().onError((error, c) => {
|
|
|
3176
3217
|
return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
|
|
3177
3218
|
}).get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...channelsAddHelpHandler).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...channelsRemoveHelpHandler).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...channelsRenameHelpHandler).post("/channels/:channel/rename", ...channelsChannelRenameHelpHandler).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...channelsPublishHelpHandler).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel/validate", ...channelsValidateHandler).get("/channels/validate", ...channelsValidateHelpHandler).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...channelsConnectorsAddHelpHandler).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...channelsConnectorsRemoveHelpHandler).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...channelsConnectorsSetHelpHandler).post("/channels/:channel/connectors/set/:connector", ...channelsConnectorsSetHandler).post("/channels/:channel/connectors/rename/:connector/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/:connector/rename/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/rename", ...channelsConnectorsRenameHelpHandler).post("/channels/:channel/connectors/:connector/rename", ...channelsConnectorRenameHelpHandler).post("/channels/:channel/connectors/:connector/request", ...channelsConnectorsRequestHandler).get("/channels/:channel/connectors/:connector", ...channelsConnectorsShowHandler).get("/channels/:channel/connectors/:connector/schedules", ...channelsConnectorsSchedulesGroupHandler).post("/channels/:channel/connectors/:connector/schedules/add", ...channelsConnectorSchedulesAddHelpHandler).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...channelsConnectorSchedulesRemoveHelpHandler).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...profilesAddHelpHandler).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...profilesSetHelpHandler).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...profilesRemoveHelpHandler).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...profilesRenameHelpHandler).post("/profiles/:profile/rename", ...profilesProfileRenameHelpHandler).post("/profiles/:profile/as-default", ...profilesAsDefaultHandler).get("/profiles/:profile/run", ...profilesLaunchHandler).get("/profiles/:profile", ...profilesLaunchHandler).get("/gateway", ...gatewayGroupHandler).get("/gateway/status", ...gatewayStatusHandler).get("/gateway/start", ...gatewayStartHandler).get("/gateway/stop", ...gatewayStopHandler).get("/gateway/restart", ...gatewayRestartHandler).get("/gateway/run", ...gatewayRunHandler).get("/gateway/logs", ...gatewayLogsHandler).get("/gateway/sql", ...gatewaySqlHandler).get("/gateway/listeners", ...gatewayListenersHandler).get("/debug", ...debugHandler).get("/debug/events", ...debugEventsHandler).get("/debug/dropped", ...debugDroppedHandler).get("/debug/errors", ...debugErrorsHandler).get("/debug/replay", ...debugReplayHandler).get("/docs", ...docsIndexHandler).get("/docs/:topic", ...docsTopicHandler).get("/doctor", ...doctorHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
|
|
3178
3219
|
//#endregion
|
|
3179
|
-
export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClock, FunnelConnectorAdapter,
|
|
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 };
|