@interactive-inc/claude-funnel 0.10.0 → 0.15.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 +106 -56
- package/dist/bin.js +557 -530
- package/dist/connectors/schedule.d.ts +2 -49
- package/dist/connectors/schedule.js +1 -1
- package/dist/connectors/slack.d.ts +4 -48
- package/dist/connectors/slack.js +1 -1
- package/dist/gateway/daemon.js +213 -211
- package/dist/index.d.ts +465 -173
- package/dist/index.js +692 -154
- package/dist/{schedule-connector-schema-CkuIQ0JQ.js → schedule-connector-schema-FxP7LPlx.js} +11 -0
- package/dist/{file-system-Co60LrmR.d.ts → schedule-listener-BPodvbld.d.ts} +56 -1
- package/dist/{slack-connector-schema-Cd22WiHB.js → slack-connector-schema-B4hsf3AY.js} +10 -1
- package/dist/slack-listener-CHj6uMY-.d.ts +74 -0
- package/package.json +2 -6
- package/schemas/funnel.schema.json +144 -0
- package/dist/slack-connector-schema-D7zAHN8k.d.ts +0 -15
- package/lib/bin.ts +0 -3
- package/lib/cli/factory.ts +0 -10
- package/lib/cli/index.ts +0 -85
- package/lib/cli/router/query-to-cli-args.ts +0 -20
- package/lib/cli/router/to-request.ts +0 -113
- package/lib/cli/router/validator.ts +0 -27
- package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +0 -27
- package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +0 -40
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +0 -41
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +0 -22
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +0 -23
- package/lib/cli/routes/channels.$channel.connectors.$connector.ts +0 -26
- package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +0 -92
- package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +0 -22
- package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +0 -63
- package/lib/cli/routes/channels.$channel.connectors.ts +0 -26
- package/lib/cli/routes/channels.$channel.publish.ts +0 -52
- package/lib/cli/routes/channels.$channel.rename.$newName.ts +0 -22
- package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +0 -34
- package/lib/cli/routes/channels.$channel.ts +0 -34
- package/lib/cli/routes/channels.add.$channel.ts +0 -33
- package/lib/cli/routes/channels.remove.$channel.ts +0 -20
- package/lib/cli/routes/channels.ts +0 -39
- package/lib/cli/routes/claude.ts +0 -70
- package/lib/cli/routes/gateway.listeners.ts +0 -41
- package/lib/cli/routes/gateway.logs.ts +0 -123
- package/lib/cli/routes/gateway.restart.ts +0 -50
- package/lib/cli/routes/gateway.run.ts +0 -41
- package/lib/cli/routes/gateway.start.ts +0 -50
- package/lib/cli/routes/gateway.status.ts +0 -19
- package/lib/cli/routes/gateway.stop.ts +0 -32
- package/lib/cli/routes/gateway.ts +0 -55
- package/lib/cli/routes/index.ts +0 -219
- package/lib/cli/routes/profiles.$profile.as-default.ts +0 -22
- package/lib/cli/routes/profiles.$profile.rename.$newName.ts +0 -22
- package/lib/cli/routes/profiles.$profile.run.ts +0 -36
- package/lib/cli/routes/profiles.add.$profile.ts +0 -49
- package/lib/cli/routes/profiles.remove.$profile.ts +0 -20
- package/lib/cli/routes/profiles.set.$profile.ts +0 -45
- package/lib/cli/routes/profiles.ts +0 -40
- package/lib/cli/routes/status.ts +0 -93
- package/lib/cli/routes/update.ts +0 -27
- package/lib/connectors/connector-adapter.ts +0 -9
- package/lib/connectors/connector-config-schema.ts +0 -16
- package/lib/connectors/connector-factory.ts +0 -94
- package/lib/connectors/connector-listener.ts +0 -20
- package/lib/connectors/discord-adapter.ts +0 -51
- package/lib/connectors/discord-connector-schema.ts +0 -12
- package/lib/connectors/discord-event-processor.ts +0 -48
- package/lib/connectors/discord-listener.ts +0 -111
- package/lib/connectors/discord.ts +0 -4
- package/lib/connectors/gh-adapter.ts +0 -48
- package/lib/connectors/gh-connector-schema.ts +0 -12
- package/lib/connectors/gh-listener.ts +0 -137
- package/lib/connectors/gh.ts +0 -3
- package/lib/connectors/match-cron.ts +0 -78
- package/lib/connectors/schedule-connector-schema.ts +0 -33
- package/lib/connectors/schedule-listener.ts +0 -207
- package/lib/connectors/schedule-state-store.ts +0 -54
- package/lib/connectors/schedule.ts +0 -4
- package/lib/connectors/slack-adapter.ts +0 -36
- package/lib/connectors/slack-connector-schema.ts +0 -13
- package/lib/connectors/slack-event-processor.ts +0 -97
- package/lib/connectors/slack-listener.ts +0 -97
- package/lib/connectors/slack.ts +0 -4
- package/lib/engine/channels/channels.ts +0 -520
- package/lib/engine/claude/claude.ts +0 -205
- package/lib/engine/claude/gateway-controller.ts +0 -4
- package/lib/engine/fs/file-system.ts +0 -23
- package/lib/engine/fs/memory-file-system.ts +0 -102
- package/lib/engine/fs/node-file-system.ts +0 -68
- package/lib/engine/http/http-client.ts +0 -17
- package/lib/engine/http/memory-http-client.ts +0 -36
- package/lib/engine/http/node-http-client.ts +0 -23
- package/lib/engine/id/id-generator.ts +0 -7
- package/lib/engine/id/memory-id-generator.ts +0 -20
- package/lib/engine/id/node-id-generator.ts +0 -7
- package/lib/engine/logger/logger.ts +0 -11
- package/lib/engine/logger/memory-logger.ts +0 -28
- package/lib/engine/logger/node-logger.ts +0 -49
- package/lib/engine/logger/noop-logger.ts +0 -9
- package/lib/engine/mcp/channel-server.ts +0 -123
- package/lib/engine/mcp/channel-subscriber.ts +0 -82
- package/lib/engine/mcp/mcp.ts +0 -126
- package/lib/engine/mcp/read-channel-connectors.ts +0 -34
- package/lib/engine/mcp/read-gateway-token.ts +0 -16
- package/lib/engine/mcp/usage-hint-for-type.ts +0 -15
- package/lib/engine/process/memory-process-runner.ts +0 -88
- package/lib/engine/process/node-process-runner.ts +0 -91
- package/lib/engine/process/process-runner.ts +0 -33
- package/lib/engine/profiles/profile-channel-checker.ts +0 -7
- package/lib/engine/profiles/profiles.ts +0 -126
- package/lib/engine/settings/mock-settings-reader.ts +0 -27
- package/lib/engine/settings/settings-reader.ts +0 -6
- package/lib/engine/settings/settings-schema.ts +0 -48
- package/lib/engine/settings/settings-store.ts +0 -110
- package/lib/engine/time/clock.ts +0 -15
- package/lib/engine/time/memory-clock.ts +0 -26
- package/lib/engine/time/node-clock.ts +0 -7
- package/lib/funnel.ts +0 -294
- package/lib/gateway/auth-middleware.ts +0 -44
- package/lib/gateway/broadcaster.ts +0 -319
- package/lib/gateway/channel-publisher.ts +0 -67
- package/lib/gateway/daemon.ts +0 -47
- package/lib/gateway/factory.ts +0 -10
- package/lib/gateway/funnel-event-store.ts +0 -155
- package/lib/gateway/gateway-server.ts +0 -426
- package/lib/gateway/gateway-token.ts +0 -79
- package/lib/gateway/gateway.ts +0 -209
- package/lib/gateway/kill-competing-slack-gateways.ts +0 -56
- package/lib/gateway/listener-supervisor.ts +0 -339
- package/lib/gateway/listeners-client.ts +0 -128
- package/lib/gateway/publish-schema.ts +0 -27
- package/lib/gateway/resolve-daemon-script.ts +0 -26
- package/lib/gateway/routes/channels.connectors.call.ts +0 -39
- package/lib/gateway/routes/channels.publish.ts +0 -44
- package/lib/gateway/routes/health.ts +0 -13
- package/lib/gateway/routes/index.ts +0 -26
- package/lib/gateway/routes/listeners.list.ts +0 -6
- package/lib/gateway/routes/listeners.restart.ts +0 -15
- package/lib/gateway/routes/listeners.start.ts +0 -15
- package/lib/gateway/routes/listeners.stop.ts +0 -15
- package/lib/gateway/routes/route-deps.ts +0 -19
- package/lib/gateway/routes/status.ts +0 -15
- package/lib/gateway/routes/validator.ts +0 -17
- package/lib/index.ts +0 -67
- package/lib/logger/leuco-human-file-writer.ts +0 -65
- package/lib/logger/leuco-human-logger.ts +0 -98
- package/lib/logger/leuco-human-record.ts +0 -16
- package/lib/logger/leuco-human-stdout-writer.ts +0 -26
- package/lib/logger/leuco-human-writer.ts +0 -14
- package/lib/logger/leuco-logger-memory-sink.ts +0 -67
- package/lib/logger/leuco-logger-record.ts +0 -13
- package/lib/logger/leuco-logger-sink.ts +0 -33
- package/lib/logger/leuco-logger-sqlite-sink.ts +0 -355
- package/lib/logger/leuco-logger.ts +0 -135
- package/lib/tui/app.tsx +0 -357
- package/lib/tui/components/add-row.tsx +0 -18
- package/lib/tui/components/brand.tsx +0 -27
- package/lib/tui/components/card.tsx +0 -44
- package/lib/tui/components/detail-bar.tsx +0 -46
- package/lib/tui/components/editable-field.tsx +0 -33
- package/lib/tui/components/empty-state.tsx +0 -11
- package/lib/tui/components/gateway-status.tsx +0 -66
- package/lib/tui/components/keymap.tsx +0 -29
- package/lib/tui/components/menu-item.tsx +0 -73
- package/lib/tui/components/menu.tsx +0 -26
- package/lib/tui/components/panel-header.tsx +0 -22
- package/lib/tui/components/readonly-field.tsx +0 -18
- package/lib/tui/components/section-header.tsx +0 -25
- package/lib/tui/components/selection-accent.tsx +0 -32
- package/lib/tui/components/session-item.tsx +0 -33
- package/lib/tui/components/session-list.tsx +0 -33
- package/lib/tui/components/ui/hascii/accordion-item.tsx +0 -88
- package/lib/tui/components/ui/hascii/accordion.tsx +0 -96
- package/lib/tui/components/ui/hascii/alert-dialog.tsx +0 -43
- package/lib/tui/components/ui/hascii/badge.tsx +0 -51
- package/lib/tui/components/ui/hascii/breadcrumb.tsx +0 -58
- package/lib/tui/components/ui/hascii/button.tsx +0 -194
- package/lib/tui/components/ui/hascii/card-content.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-description.tsx +0 -13
- package/lib/tui/components/ui/hascii/card-footer.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-title.tsx +0 -13
- package/lib/tui/components/ui/hascii/card.tsx +0 -27
- package/lib/tui/components/ui/hascii/checkbox.tsx +0 -65
- package/lib/tui/components/ui/hascii/command.tsx +0 -159
- package/lib/tui/components/ui/hascii/dialog-content.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-description.tsx +0 -13
- package/lib/tui/components/ui/hascii/dialog-footer.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-title.tsx +0 -13
- package/lib/tui/components/ui/hascii/dialog.tsx +0 -27
- package/lib/tui/components/ui/hascii/file-tree.tsx +0 -142
- package/lib/tui/components/ui/hascii/focus-group.tsx +0 -62
- package/lib/tui/components/ui/hascii/form-item.tsx +0 -43
- package/lib/tui/components/ui/hascii/input-otp.tsx +0 -86
- package/lib/tui/components/ui/hascii/input.tsx +0 -130
- package/lib/tui/components/ui/hascii/pagination.tsx +0 -105
- package/lib/tui/components/ui/hascii/progress.tsx +0 -28
- package/lib/tui/components/ui/hascii/select.tsx +0 -131
- package/lib/tui/components/ui/hascii/separator.tsx +0 -35
- package/lib/tui/components/ui/hascii/sidebar-content.tsx +0 -23
- package/lib/tui/components/ui/hascii/sidebar-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +0 -67
- package/lib/tui/components/ui/hascii/sidebar.tsx +0 -24
- package/lib/tui/components/ui/hascii/skeleton.tsx +0 -60
- package/lib/tui/components/ui/hascii/slider.tsx +0 -91
- package/lib/tui/components/ui/hascii/snackbar.tsx +0 -75
- package/lib/tui/components/ui/hascii/sparkline.tsx +0 -53
- package/lib/tui/components/ui/hascii/spinner.tsx +0 -47
- package/lib/tui/components/ui/hascii/stepper.tsx +0 -54
- package/lib/tui/components/ui/hascii/switch.tsx +0 -66
- package/lib/tui/components/ui/hascii/table.tsx +0 -95
- package/lib/tui/components/ui/hascii/tabs.tsx +0 -59
- package/lib/tui/components/ui/hascii/toggle-group-item.tsx +0 -45
- package/lib/tui/components/ui/hascii/toggle-group.tsx +0 -99
- package/lib/tui/components/ui/hascii/tree.tsx +0 -104
- package/lib/tui/components/view-shell.tsx +0 -44
- package/lib/tui/filter-input.tsx +0 -33
- package/lib/tui/hooks/hascii/use-pressable.ts +0 -54
- package/lib/tui/parse-comma-list.ts +0 -14
- package/lib/tui/profile-launcher.tsx +0 -61
- package/lib/tui/scrollbar-options.ts +0 -19
- package/lib/tui/sidebar.tsx +0 -50
- package/lib/tui/theme.ts +0 -40
- package/lib/tui/tui.tsx +0 -20
- package/lib/tui/types.ts +0 -38
- package/lib/tui/unique-name.ts +0 -18
- package/lib/tui/use-event-stream.ts +0 -133
- package/lib/tui/use-snapshot.ts +0 -99
- package/lib/tui/utils/hascii/form-item-context.tsx +0 -23
- package/lib/tui/utils/hascii/input-focus-context.tsx +0 -31
- package/lib/tui/utils/hascii/theme-context.tsx +0 -26
- package/lib/tui/utils/hascii/theme.ts +0 -176
- package/lib/tui/views/channels-view.tsx +0 -108
- package/lib/tui/views/connectors-view.tsx +0 -164
- package/lib/tui/views/events-view.tsx +0 -160
- package/lib/tui/views/listeners-view.tsx +0 -80
- package/lib/tui/views/profiles-view.tsx +0 -152
package/dist/index.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, t as discordConnectorSchema } from "./discord-connector-schema-ygf5Df-2.js";
|
|
2
2
|
import { n as FunnelLogger, r as FunnelConnectorListener, t as NodeFunnelLogger } from "./node-logger-DQz_BGOD.js";
|
|
3
3
|
import { a as FunnelProcessRunner, i as NodeFunnelProcessRunner, n as FunnelGhListener, r as FunnelGhAdapter, t as ghConnectorSchema } from "./gh-connector-schema-2ml29MBC.js";
|
|
4
|
-
import { a as ScheduleStateStore, i as FunnelScheduleListener, n as scheduleConnectorSchema, o as NodeFunnelFileSystem, r as scheduleEntrySchema, s as FunnelFileSystem, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-
|
|
5
|
-
import { i as FunnelSlackAdapter, n as FunnelSlackListener, t as slackConnectorSchema } from "./slack-connector-schema-
|
|
4
|
+
import { a as ScheduleStateStore, i as FunnelScheduleListener, n as scheduleConnectorSchema, o as NodeFunnelFileSystem, r as scheduleEntrySchema, s as FunnelFileSystem, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-FxP7LPlx.js";
|
|
5
|
+
import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-B4hsf3AY.js";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
|
-
import "
|
|
10
|
+
import { stderr, stdin } from "node:process";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
11
12
|
import { timingSafeEqual } from "node:crypto";
|
|
12
13
|
import { createFactory } from "hono/factory";
|
|
13
14
|
import { Database } from "bun:sqlite";
|
|
14
15
|
import { HTTPException } from "hono/http-exception";
|
|
15
16
|
import { zValidator } from "@hono/zod-validator";
|
|
16
|
-
import { Server
|
|
17
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
17
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
19
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
19
20
|
import { stringify } from "yaml";
|
|
@@ -133,23 +134,33 @@ const defaultLogger$5 = new NodeFunnelLogger();
|
|
|
133
134
|
*
|
|
134
135
|
* `dir` is the funnel home (defaults to ~/.funnel); per-connector state files
|
|
135
136
|
* land at `<dir>/channels/<channel-id>/connectors/<connector-id>/state.json`.
|
|
137
|
+
*
|
|
138
|
+
* Host integrations can supply per-type listener hooks via
|
|
139
|
+
* `slackListenerOptions` / `scheduleListenerOptions` — e.g. to attach a
|
|
140
|
+
* Bolt `app.action` handler or to drop one-shot schedule entries on fire.
|
|
136
141
|
*/
|
|
137
142
|
var FunnelConnectorFactory = class {
|
|
138
143
|
fs;
|
|
139
144
|
process;
|
|
140
145
|
logger;
|
|
141
146
|
dir;
|
|
147
|
+
slackListenerOptions;
|
|
148
|
+
scheduleListenerOptions;
|
|
142
149
|
constructor(deps = {}) {
|
|
143
150
|
this.fs = deps.fs ?? defaultFs$4;
|
|
144
151
|
this.process = deps.process ?? defaultProcess$3;
|
|
145
152
|
this.logger = deps.logger ?? defaultLogger$5;
|
|
146
153
|
this.dir = deps.dir ?? FUNNEL_DIR;
|
|
154
|
+
this.slackListenerOptions = deps.slackListenerOptions ?? {};
|
|
155
|
+
this.scheduleListenerOptions = deps.scheduleListenerOptions ?? {};
|
|
147
156
|
Object.freeze(this);
|
|
148
157
|
}
|
|
149
158
|
createListener(channelId, config) {
|
|
150
159
|
if (config.type === "slack") return new FunnelSlackListener({
|
|
151
160
|
config,
|
|
152
|
-
logger: this.logger
|
|
161
|
+
logger: this.logger,
|
|
162
|
+
onAppCreated: this.slackListenerOptions.onAppCreated,
|
|
163
|
+
preprocessEvent: this.slackListenerOptions.preprocessEvent
|
|
153
164
|
});
|
|
154
165
|
if (config.type === "gh") return new FunnelGhListener({
|
|
155
166
|
config,
|
|
@@ -166,7 +177,8 @@ var FunnelConnectorFactory = class {
|
|
|
166
177
|
path: join(this.connectorDir(channelId, config.id), "state.json"),
|
|
167
178
|
fs: this.fs
|
|
168
179
|
}),
|
|
169
|
-
logger: this.logger
|
|
180
|
+
logger: this.logger,
|
|
181
|
+
onFired: this.scheduleListenerOptions.onFired
|
|
170
182
|
});
|
|
171
183
|
}
|
|
172
184
|
createAdapter(config) {
|
|
@@ -183,6 +195,40 @@ var FunnelConnectorFactory = class {
|
|
|
183
195
|
}
|
|
184
196
|
};
|
|
185
197
|
//#endregion
|
|
198
|
+
//#region lib/engine/channels/connector-tokens.ts
|
|
199
|
+
/**
|
|
200
|
+
* Return every secret token contained in a connector config. Used by token
|
|
201
|
+
* collision detection at add/update time so the same Slack bot or Discord
|
|
202
|
+
* bot cannot be registered under two connectors. Centralizes the per-type
|
|
203
|
+
* switch so the channels facade does not embed type-specific knowledge.
|
|
204
|
+
*/
|
|
205
|
+
function connectorTokens(connector) {
|
|
206
|
+
switch (connector.type) {
|
|
207
|
+
case "slack": return [connector.botToken, connector.appToken];
|
|
208
|
+
case "discord": return [connector.botToken];
|
|
209
|
+
case "gh":
|
|
210
|
+
case "schedule": return [];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region lib/engine/channels/require-connector.ts
|
|
215
|
+
function isConnectorOfType(connector, type) {
|
|
216
|
+
return connector.type === type;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Look up a connector by name and narrow its discriminated union to a single
|
|
220
|
+
* variant via a type predicate. Throws if the connector is missing or has the
|
|
221
|
+
* wrong `type`. Replaces per-type `requireXxxConnector` privates — adding a
|
|
222
|
+
* new connector type only touches the `ConnectorConfig` union, not this
|
|
223
|
+
* helper.
|
|
224
|
+
*/
|
|
225
|
+
function requireConnectorOfType(channel, connectorName, type) {
|
|
226
|
+
const connector = channel.connectors.find((c) => c.name === connectorName);
|
|
227
|
+
if (!connector) throw new Error(`connector "${connectorName}" not found in channel "${channel.name}"`);
|
|
228
|
+
if (!isConnectorOfType(connector, type)) throw new Error(`connector "${connectorName}" is type "${connector.type}", not "${type}"`);
|
|
229
|
+
return connector;
|
|
230
|
+
}
|
|
231
|
+
//#endregion
|
|
186
232
|
//#region lib/engine/time/clock.ts
|
|
187
233
|
/**
|
|
188
234
|
* Time boundary. Default NodeFunnelClock returns `new Date()`; MemoryFunnelClock
|
|
@@ -320,39 +366,41 @@ var FunnelChannels = class {
|
|
|
320
366
|
const now = this.clock.iso();
|
|
321
367
|
const createdAt = now;
|
|
322
368
|
const updatedAt = now;
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
369
|
+
switch (input.type) {
|
|
370
|
+
case "slack": return {
|
|
371
|
+
id,
|
|
372
|
+
type: "slack",
|
|
373
|
+
name: input.name,
|
|
374
|
+
botToken: input.botToken,
|
|
375
|
+
appToken: input.appToken,
|
|
376
|
+
createdAt,
|
|
377
|
+
updatedAt
|
|
378
|
+
};
|
|
379
|
+
case "gh": return {
|
|
380
|
+
id,
|
|
381
|
+
type: "gh",
|
|
382
|
+
name: input.name,
|
|
383
|
+
...input.pollInterval !== void 0 ? { pollInterval: input.pollInterval } : {},
|
|
384
|
+
createdAt,
|
|
385
|
+
updatedAt
|
|
386
|
+
};
|
|
387
|
+
case "discord": return {
|
|
388
|
+
id,
|
|
389
|
+
type: "discord",
|
|
390
|
+
name: input.name,
|
|
391
|
+
botToken: input.botToken,
|
|
392
|
+
createdAt,
|
|
393
|
+
updatedAt
|
|
394
|
+
};
|
|
395
|
+
case "schedule": return {
|
|
396
|
+
id,
|
|
397
|
+
type: "schedule",
|
|
398
|
+
name: input.name,
|
|
399
|
+
entries: input.entries ?? [],
|
|
400
|
+
createdAt,
|
|
401
|
+
updatedAt
|
|
402
|
+
};
|
|
403
|
+
}
|
|
356
404
|
}
|
|
357
405
|
removeConnector(channelName, connectorName) {
|
|
358
406
|
const settings = this.store.read();
|
|
@@ -374,8 +422,7 @@ var FunnelChannels = class {
|
|
|
374
422
|
}
|
|
375
423
|
updateSlackConnector(channelName, connectorName, fields) {
|
|
376
424
|
const settings = this.store.read();
|
|
377
|
-
const
|
|
378
|
-
const connector = this.requireSlackConnector(channel, connectorName);
|
|
425
|
+
const connector = requireConnectorOfType(this.requireChannel(settings, channelName), connectorName, "slack");
|
|
379
426
|
const updated = {
|
|
380
427
|
...connector,
|
|
381
428
|
botToken: fields.botToken ?? connector.botToken,
|
|
@@ -388,16 +435,14 @@ var FunnelChannels = class {
|
|
|
388
435
|
}
|
|
389
436
|
updateGhConnector(channelName, connectorName, fields) {
|
|
390
437
|
const settings = this.store.read();
|
|
391
|
-
const
|
|
392
|
-
const connector = this.requireGhConnector(channel, connectorName);
|
|
438
|
+
const connector = requireConnectorOfType(this.requireChannel(settings, channelName), connectorName, "gh");
|
|
393
439
|
if (fields.pollInterval !== void 0) connector.pollInterval = fields.pollInterval;
|
|
394
440
|
connector.updatedAt = this.clock.iso();
|
|
395
441
|
this.store.write(settings);
|
|
396
442
|
}
|
|
397
443
|
updateDiscordConnector(channelName, connectorName, fields) {
|
|
398
444
|
const settings = this.store.read();
|
|
399
|
-
const
|
|
400
|
-
const connector = this.requireDiscordConnector(channel, connectorName);
|
|
445
|
+
const connector = requireConnectorOfType(this.requireChannel(settings, channelName), connectorName, "discord");
|
|
401
446
|
const updated = {
|
|
402
447
|
...connector,
|
|
403
448
|
botToken: fields.botToken ?? connector.botToken,
|
|
@@ -408,13 +453,11 @@ var FunnelChannels = class {
|
|
|
408
453
|
this.store.write(settings);
|
|
409
454
|
}
|
|
410
455
|
listScheduleEntries(channelName, connectorName) {
|
|
411
|
-
|
|
412
|
-
return this.requireScheduleConnector(channel, connectorName).entries;
|
|
456
|
+
return requireConnectorOfType(this.requireChannel(this.store.read(), channelName), connectorName, "schedule").entries;
|
|
413
457
|
}
|
|
414
458
|
addScheduleEntry(channelName, connectorName, entry) {
|
|
415
459
|
const settings = this.store.read();
|
|
416
|
-
const
|
|
417
|
-
const connector = this.requireScheduleConnector(channel, connectorName);
|
|
460
|
+
const connector = requireConnectorOfType(this.requireChannel(settings, channelName), connectorName, "schedule");
|
|
418
461
|
const persisted = {
|
|
419
462
|
id: entry.id ?? this.idGenerator.generate(),
|
|
420
463
|
cron: entry.cron,
|
|
@@ -429,8 +472,7 @@ var FunnelChannels = class {
|
|
|
429
472
|
}
|
|
430
473
|
removeScheduleEntry(channelName, connectorName, id) {
|
|
431
474
|
const settings = this.store.read();
|
|
432
|
-
const
|
|
433
|
-
const connector = this.requireScheduleConnector(channel, connectorName);
|
|
475
|
+
const connector = requireConnectorOfType(this.requireChannel(settings, channelName), connectorName, "schedule");
|
|
434
476
|
const index = connector.entries.findIndex((e) => e.id === id);
|
|
435
477
|
if (index < 0) throw new Error(`schedule entry "${id}" not found`);
|
|
436
478
|
connector.entries.splice(index, 1);
|
|
@@ -470,44 +512,14 @@ var FunnelChannels = class {
|
|
|
470
512
|
if (!channel) throw new Error(`channel "${name}" not found`);
|
|
471
513
|
return channel;
|
|
472
514
|
}
|
|
473
|
-
requireConnector(channel, connectorName) {
|
|
474
|
-
const connector = channel.connectors.find((c) => c.name === connectorName);
|
|
475
|
-
if (!connector) throw new Error(`connector "${connectorName}" not found in channel "${channel.name}"`);
|
|
476
|
-
return connector;
|
|
477
|
-
}
|
|
478
|
-
requireSlackConnector(channel, connectorName) {
|
|
479
|
-
const connector = this.requireConnector(channel, connectorName);
|
|
480
|
-
if (connector.type !== "slack") throw new Error(`connector "${connectorName}" is type "${connector.type}", not "slack"`);
|
|
481
|
-
return connector;
|
|
482
|
-
}
|
|
483
|
-
requireGhConnector(channel, connectorName) {
|
|
484
|
-
const connector = this.requireConnector(channel, connectorName);
|
|
485
|
-
if (connector.type !== "gh") throw new Error(`connector "${connectorName}" is type "${connector.type}", not "gh"`);
|
|
486
|
-
return connector;
|
|
487
|
-
}
|
|
488
|
-
requireDiscordConnector(channel, connectorName) {
|
|
489
|
-
const connector = this.requireConnector(channel, connectorName);
|
|
490
|
-
if (connector.type !== "discord") throw new Error(`connector "${connectorName}" is type "${connector.type}", not "discord"`);
|
|
491
|
-
return connector;
|
|
492
|
-
}
|
|
493
|
-
requireScheduleConnector(channel, connectorName) {
|
|
494
|
-
const connector = this.requireConnector(channel, connectorName);
|
|
495
|
-
if (connector.type !== "schedule") throw new Error(`connector "${connectorName}" is type "${connector.type}", not "schedule"`);
|
|
496
|
-
return connector;
|
|
497
|
-
}
|
|
498
515
|
assertNoTokenCollision(settings, candidate) {
|
|
499
|
-
const tokens =
|
|
516
|
+
const tokens = connectorTokens(candidate);
|
|
500
517
|
if (tokens.length === 0) return;
|
|
501
518
|
for (const channel of settings.channels) for (const other of channel.connectors) {
|
|
502
519
|
if (other.id === candidate.id) continue;
|
|
503
|
-
for (const token of
|
|
520
|
+
for (const token of connectorTokens(other)) if (tokens.includes(token)) throw new Error(`token already in use by connector "${other.name}" in channel "${channel.name}"`);
|
|
504
521
|
}
|
|
505
522
|
}
|
|
506
|
-
tokensOf(connector) {
|
|
507
|
-
if (connector.type === "slack") return [connector.botToken, connector.appToken];
|
|
508
|
-
if (connector.type === "discord") return [connector.botToken];
|
|
509
|
-
return [];
|
|
510
|
-
}
|
|
511
523
|
};
|
|
512
524
|
//#endregion
|
|
513
525
|
//#region lib/engine/claude/claude.ts
|
|
@@ -556,7 +568,7 @@ var FunnelClaude = class {
|
|
|
556
568
|
this.installCleanup(options.profileName);
|
|
557
569
|
}
|
|
558
570
|
const claudeArgs = this.buildArgs(options, cwd);
|
|
559
|
-
const env = this.buildEnv(channel.id);
|
|
571
|
+
const env = this.buildEnv(channel.id, options.extraEnv);
|
|
560
572
|
this.logger.info(`claude launch`, {
|
|
561
573
|
channel: options.channel,
|
|
562
574
|
channelId: channel.id,
|
|
@@ -624,8 +636,9 @@ var FunnelClaude = class {
|
|
|
624
636
|
if (options.brief && !result.includes("--brief")) result.push("--brief");
|
|
625
637
|
return result;
|
|
626
638
|
}
|
|
627
|
-
buildEnv(channelId) {
|
|
639
|
+
buildEnv(channelId, extraEnv) {
|
|
628
640
|
const env = {};
|
|
641
|
+
if (extraEnv) for (const [key, value] of Object.entries(extraEnv)) env[key] = value;
|
|
629
642
|
for (const [key, value] of Object.entries(globalThis.process.env)) if (typeof value === "string") env[key] = value;
|
|
630
643
|
env.FUNNEL_CHANNEL_ID = channelId;
|
|
631
644
|
return env;
|
|
@@ -720,6 +733,332 @@ var MemoryFunnelIdGenerator = class extends FunnelIdGenerator {
|
|
|
720
733
|
}
|
|
721
734
|
};
|
|
722
735
|
//#endregion
|
|
736
|
+
//#region lib/engine/local-config/local-config-schema.ts
|
|
737
|
+
/**
|
|
738
|
+
* Per-repo launch config (`funnel.json`).
|
|
739
|
+
*
|
|
740
|
+
* `fnl claude` reads this when no --profile / --channel is given and uses it
|
|
741
|
+
* to set the channel binding, sub-agent, and brief flag. When `connectors`
|
|
742
|
+
* is declared, missing channels/connectors are materialized into the local
|
|
743
|
+
* `~/.funnel/settings.json` on launch.
|
|
744
|
+
*
|
|
745
|
+
* Token fields per connector resolve in this order:
|
|
746
|
+
*
|
|
747
|
+
* 1. Literal value at the field itself (e.g. `botToken: "xoxb-..."`)
|
|
748
|
+
* 2. Env-var reference at `env.<field>` (e.g. `env: { botToken: "SLACK_BOT_TOKEN" }`);
|
|
749
|
+
* resolved from process.env first, then ./.env.local
|
|
750
|
+
* 3. Field omitted everywhere → prompted for once on a TTY and persisted to
|
|
751
|
+
* `~/.funnel/settings.json`; non-TTY launches fail fast.
|
|
752
|
+
*
|
|
753
|
+
* `funnel.json` itself is never written to. Only `channel` is required.
|
|
754
|
+
*/
|
|
755
|
+
const slackEnvSchema = z.object({
|
|
756
|
+
botToken: z.string().optional(),
|
|
757
|
+
appToken: z.string().optional()
|
|
758
|
+
}).optional();
|
|
759
|
+
const slackConnectorSpecSchema = z.object({
|
|
760
|
+
type: z.literal("slack"),
|
|
761
|
+
name: z.string(),
|
|
762
|
+
botToken: z.string().optional(),
|
|
763
|
+
appToken: z.string().optional(),
|
|
764
|
+
env: slackEnvSchema
|
|
765
|
+
});
|
|
766
|
+
const discordEnvSchema = z.object({ botToken: z.string().optional() }).optional();
|
|
767
|
+
const discordConnectorSpecSchema = z.object({
|
|
768
|
+
type: z.literal("discord"),
|
|
769
|
+
name: z.string(),
|
|
770
|
+
botToken: z.string().optional(),
|
|
771
|
+
env: discordEnvSchema
|
|
772
|
+
});
|
|
773
|
+
const ghConnectorSpecSchema = z.object({
|
|
774
|
+
type: z.literal("gh"),
|
|
775
|
+
name: z.string(),
|
|
776
|
+
pollInterval: z.number().int().positive().optional()
|
|
777
|
+
});
|
|
778
|
+
const scheduleConnectorSpecSchema = z.object({
|
|
779
|
+
type: z.literal("schedule"),
|
|
780
|
+
name: z.string()
|
|
781
|
+
});
|
|
782
|
+
const connectorSpecSchema = z.discriminatedUnion("type", [
|
|
783
|
+
slackConnectorSpecSchema,
|
|
784
|
+
discordConnectorSpecSchema,
|
|
785
|
+
ghConnectorSpecSchema,
|
|
786
|
+
scheduleConnectorSpecSchema
|
|
787
|
+
]);
|
|
788
|
+
const localConfigSchema = z.object({
|
|
789
|
+
$schema: z.string().optional(),
|
|
790
|
+
channel: z.string(),
|
|
791
|
+
/** Extra args forwarded to the claude CLI. Prepended before user-supplied CLI args so user args still win on collision (e.g. --model, --agent, --brief, --resume, positional session ids). */
|
|
792
|
+
options: z.array(z.string()).optional(),
|
|
793
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
794
|
+
connectors: z.array(connectorSpecSchema).optional()
|
|
795
|
+
});
|
|
796
|
+
const LOCAL_CONFIG_FILENAME = "funnel.json";
|
|
797
|
+
const LOCAL_ENV_FILENAME = ".env.local";
|
|
798
|
+
//#endregion
|
|
799
|
+
//#region lib/engine/local-config/dotenv-reader.ts
|
|
800
|
+
const VARIABLE_LINE = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/;
|
|
801
|
+
const unquote = (value) => {
|
|
802
|
+
if (value.length < 2) return value;
|
|
803
|
+
const first = value[0];
|
|
804
|
+
const last = value[value.length - 1];
|
|
805
|
+
if (first === "\"" && last === "\"") return value.slice(1, -1);
|
|
806
|
+
if (first === "'" && last === "'") return value.slice(1, -1);
|
|
807
|
+
return value;
|
|
808
|
+
};
|
|
809
|
+
/**
|
|
810
|
+
* Minimal `.env.local` parser. Supports `KEY=value` lines, blank lines, and
|
|
811
|
+
* `#` comments. Strips matching surrounding single or double quotes. No
|
|
812
|
+
* interpolation, no `export` prefix — anything fancier should live in a real
|
|
813
|
+
* env file loaded by the shell.
|
|
814
|
+
*/
|
|
815
|
+
var FunnelDotenvReader = class {
|
|
816
|
+
fs;
|
|
817
|
+
constructor(deps) {
|
|
818
|
+
this.fs = deps.fs;
|
|
819
|
+
Object.freeze(this);
|
|
820
|
+
}
|
|
821
|
+
read(cwd) {
|
|
822
|
+
const path = join(cwd, LOCAL_ENV_FILENAME);
|
|
823
|
+
if (!this.fs.existsSync(path)) return {};
|
|
824
|
+
const raw = this.fs.readFileSync(path);
|
|
825
|
+
const out = {};
|
|
826
|
+
for (const line of raw.split("\n")) {
|
|
827
|
+
const trimmed = line.trim();
|
|
828
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
829
|
+
const match = trimmed.match(VARIABLE_LINE);
|
|
830
|
+
if (!match) continue;
|
|
831
|
+
const key = match[1];
|
|
832
|
+
const value = match[2];
|
|
833
|
+
if (!key) continue;
|
|
834
|
+
out[key] = unquote(value ?? "");
|
|
835
|
+
}
|
|
836
|
+
return out;
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
//#endregion
|
|
840
|
+
//#region lib/engine/local-config/local-config.ts
|
|
841
|
+
/**
|
|
842
|
+
* Reads `funnel.json` from a directory. Returns `null` when the file is
|
|
843
|
+
* absent so callers can fall through to other resolution paths (default
|
|
844
|
+
* profile, help). Throws on present-but-invalid files so misconfiguration
|
|
845
|
+
* surfaces loudly instead of silently launching the wrong channel.
|
|
846
|
+
*/
|
|
847
|
+
var FunnelLocalConfig = class {
|
|
848
|
+
fs;
|
|
849
|
+
constructor(deps) {
|
|
850
|
+
this.fs = deps.fs;
|
|
851
|
+
Object.freeze(this);
|
|
852
|
+
}
|
|
853
|
+
read(cwd) {
|
|
854
|
+
const path = join(cwd, LOCAL_CONFIG_FILENAME);
|
|
855
|
+
if (!this.fs.existsSync(path)) return null;
|
|
856
|
+
const raw = this.fs.readFileSync(path);
|
|
857
|
+
const parsed = (() => {
|
|
858
|
+
try {
|
|
859
|
+
return JSON.parse(raw);
|
|
860
|
+
} catch (error) {
|
|
861
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
862
|
+
throw new Error(`${LOCAL_CONFIG_FILENAME} is not valid JSON: ${message}`);
|
|
863
|
+
}
|
|
864
|
+
})();
|
|
865
|
+
const result = localConfigSchema.safeParse(parsed);
|
|
866
|
+
if (!result.success) throw new Error(`${LOCAL_CONFIG_FILENAME} is invalid: ${result.error.message}`);
|
|
867
|
+
return result.data;
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
//#endregion
|
|
871
|
+
//#region lib/engine/token-prompter/token-prompter.ts
|
|
872
|
+
/**
|
|
873
|
+
* Asks the user for a secret value on stdin. Used as a last resort when a
|
|
874
|
+
* funnel.json token field is absent and not present in `~/.funnel`. The Node
|
|
875
|
+
* implementation refuses to prompt when stdin is not a TTY so non-interactive
|
|
876
|
+
* launches (CI, agent spawning agent, daemons) fail fast instead of hanging.
|
|
877
|
+
*/
|
|
878
|
+
var FunnelTokenPrompter = class {};
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region lib/engine/local-config/local-config-sync.ts
|
|
881
|
+
/**
|
|
882
|
+
* Reconciles a `funnel.json` spec with `~/.funnel/settings.json`. The spec
|
|
883
|
+
* is the source of truth for the channel it declares:
|
|
884
|
+
*
|
|
885
|
+
* - missing channel → created
|
|
886
|
+
* - declared connector matched by name → tokens reconciled
|
|
887
|
+
* - declared connector matched by token in the same channel under a
|
|
888
|
+
* different name → renamed in place (then tokens reconciled)
|
|
889
|
+
* - declared connector with no match → added
|
|
890
|
+
* - any connector left in the channel that the spec did not touch → removed
|
|
891
|
+
*
|
|
892
|
+
* Removal only fires when funnel.json has a `connectors` field. An absent
|
|
893
|
+
* field means "do not manage connectors from here" and leaves everything in
|
|
894
|
+
* `~/.funnel` alone.
|
|
895
|
+
*/
|
|
896
|
+
var FunnelLocalConfigSync = class {
|
|
897
|
+
channels;
|
|
898
|
+
dotenv;
|
|
899
|
+
prompter;
|
|
900
|
+
env;
|
|
901
|
+
constructor(deps) {
|
|
902
|
+
this.channels = deps.channels;
|
|
903
|
+
this.dotenv = deps.dotenv;
|
|
904
|
+
this.prompter = deps.prompter;
|
|
905
|
+
this.env = deps.env ?? process.env;
|
|
906
|
+
Object.freeze(this);
|
|
907
|
+
}
|
|
908
|
+
async ensure(local, cwd) {
|
|
909
|
+
if (!this.channels.get(local.channel)) this.channels.add({ name: local.channel });
|
|
910
|
+
if (local.connectors === void 0) return;
|
|
911
|
+
const dotenv = this.dotenv.read(cwd);
|
|
912
|
+
const touched = /* @__PURE__ */ new Set();
|
|
913
|
+
for (const spec of local.connectors) {
|
|
914
|
+
const id = await this.ensureConnector(local.channel, spec, dotenv);
|
|
915
|
+
touched.add(id);
|
|
916
|
+
}
|
|
917
|
+
this.removeExtras(local.channel, touched);
|
|
918
|
+
}
|
|
919
|
+
async ensureConnector(channelName, spec, dotenv) {
|
|
920
|
+
if (spec.type === "slack") return await this.ensureSlack(channelName, spec, dotenv);
|
|
921
|
+
if (spec.type === "discord") return await this.ensureDiscord(channelName, spec, dotenv);
|
|
922
|
+
if (spec.type === "gh") return this.ensureGh(channelName, spec);
|
|
923
|
+
return this.ensureSchedule(channelName, spec);
|
|
924
|
+
}
|
|
925
|
+
async ensureSlack(channelName, spec, dotenv) {
|
|
926
|
+
const byName = this.findExistingSlack(channelName, spec.name);
|
|
927
|
+
const botToken = await this.resolveField({
|
|
928
|
+
literal: spec.botToken,
|
|
929
|
+
envVar: spec.env?.botToken,
|
|
930
|
+
dotenv,
|
|
931
|
+
label: `${spec.name}.botToken`,
|
|
932
|
+
existing: byName?.botToken
|
|
933
|
+
});
|
|
934
|
+
const appToken = await this.resolveField({
|
|
935
|
+
literal: spec.appToken,
|
|
936
|
+
envVar: spec.env?.appToken,
|
|
937
|
+
dotenv,
|
|
938
|
+
label: `${spec.name}.appToken`,
|
|
939
|
+
existing: byName?.appToken
|
|
940
|
+
});
|
|
941
|
+
if (byName) {
|
|
942
|
+
if (byName.botToken !== botToken || byName.appToken !== appToken) this.channels.updateSlackConnector(channelName, spec.name, {
|
|
943
|
+
botToken,
|
|
944
|
+
appToken
|
|
945
|
+
});
|
|
946
|
+
return byName.id;
|
|
947
|
+
}
|
|
948
|
+
const byToken = this.findSlackByToken(channelName, [botToken, appToken]);
|
|
949
|
+
if (byToken) {
|
|
950
|
+
this.channels.renameConnector(channelName, byToken.name, spec.name);
|
|
951
|
+
if (byToken.botToken !== botToken || byToken.appToken !== appToken) this.channels.updateSlackConnector(channelName, spec.name, {
|
|
952
|
+
botToken,
|
|
953
|
+
appToken
|
|
954
|
+
});
|
|
955
|
+
return byToken.id;
|
|
956
|
+
}
|
|
957
|
+
return this.channels.addConnector(channelName, {
|
|
958
|
+
type: "slack",
|
|
959
|
+
name: spec.name,
|
|
960
|
+
botToken,
|
|
961
|
+
appToken
|
|
962
|
+
}).id;
|
|
963
|
+
}
|
|
964
|
+
async ensureDiscord(channelName, spec, dotenv) {
|
|
965
|
+
const byName = this.findExistingDiscord(channelName, spec.name);
|
|
966
|
+
const botToken = await this.resolveField({
|
|
967
|
+
literal: spec.botToken,
|
|
968
|
+
envVar: spec.env?.botToken,
|
|
969
|
+
dotenv,
|
|
970
|
+
label: `${spec.name}.botToken`,
|
|
971
|
+
existing: byName?.botToken
|
|
972
|
+
});
|
|
973
|
+
if (byName) {
|
|
974
|
+
if (byName.botToken !== botToken) this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
|
|
975
|
+
return byName.id;
|
|
976
|
+
}
|
|
977
|
+
const byToken = this.findDiscordByToken(channelName, botToken);
|
|
978
|
+
if (byToken) {
|
|
979
|
+
this.channels.renameConnector(channelName, byToken.name, spec.name);
|
|
980
|
+
if (byToken.botToken !== botToken) this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
|
|
981
|
+
return byToken.id;
|
|
982
|
+
}
|
|
983
|
+
return this.channels.addConnector(channelName, {
|
|
984
|
+
type: "discord",
|
|
985
|
+
name: spec.name,
|
|
986
|
+
botToken
|
|
987
|
+
}).id;
|
|
988
|
+
}
|
|
989
|
+
ensureGh(channelName, spec) {
|
|
990
|
+
const existing = this.channels.getConnector(channelName, spec.name);
|
|
991
|
+
if (existing && existing.type !== "gh") throw new Error(`connector "${spec.name}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "gh"`);
|
|
992
|
+
if (existing && existing.type === "gh") {
|
|
993
|
+
if (spec.pollInterval !== void 0 && existing.pollInterval !== spec.pollInterval) this.channels.updateGhConnector(channelName, spec.name, { pollInterval: spec.pollInterval });
|
|
994
|
+
return existing.id;
|
|
995
|
+
}
|
|
996
|
+
return this.channels.addConnector(channelName, {
|
|
997
|
+
type: "gh",
|
|
998
|
+
name: spec.name,
|
|
999
|
+
...spec.pollInterval !== void 0 ? { pollInterval: spec.pollInterval } : {}
|
|
1000
|
+
}).id;
|
|
1001
|
+
}
|
|
1002
|
+
ensureSchedule(channelName, spec) {
|
|
1003
|
+
const existing = this.channels.getConnector(channelName, spec.name);
|
|
1004
|
+
if (existing && existing.type !== "schedule") throw new Error(`connector "${spec.name}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "schedule"`);
|
|
1005
|
+
if (existing && existing.type === "schedule") return existing.id;
|
|
1006
|
+
return this.channels.addConnector(channelName, {
|
|
1007
|
+
type: "schedule",
|
|
1008
|
+
name: spec.name
|
|
1009
|
+
}).id;
|
|
1010
|
+
}
|
|
1011
|
+
findExistingSlack(channelName, connectorName) {
|
|
1012
|
+
const existing = this.channels.getConnector(channelName, connectorName);
|
|
1013
|
+
if (!existing) return null;
|
|
1014
|
+
if (existing.type !== "slack") throw new Error(`connector "${connectorName}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "slack"`);
|
|
1015
|
+
return existing;
|
|
1016
|
+
}
|
|
1017
|
+
findExistingDiscord(channelName, connectorName) {
|
|
1018
|
+
const existing = this.channels.getConnector(channelName, connectorName);
|
|
1019
|
+
if (!existing) return null;
|
|
1020
|
+
if (existing.type !== "discord") throw new Error(`connector "${connectorName}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "discord"`);
|
|
1021
|
+
return existing;
|
|
1022
|
+
}
|
|
1023
|
+
findSlackByToken(channelName, tokens) {
|
|
1024
|
+
const channel = this.channels.get(channelName);
|
|
1025
|
+
if (!channel) return null;
|
|
1026
|
+
for (const connector of channel.connectors) {
|
|
1027
|
+
if (connector.type !== "slack") continue;
|
|
1028
|
+
if (tokens.includes(connector.botToken) || tokens.includes(connector.appToken)) return connector;
|
|
1029
|
+
}
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
findDiscordByToken(channelName, token) {
|
|
1033
|
+
const channel = this.channels.get(channelName);
|
|
1034
|
+
if (!channel) return null;
|
|
1035
|
+
for (const connector of channel.connectors) {
|
|
1036
|
+
if (connector.type !== "discord") continue;
|
|
1037
|
+
if (connector.botToken === token) return connector;
|
|
1038
|
+
}
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
removeExtras(channelName, touched) {
|
|
1042
|
+
const channel = this.channels.get(channelName);
|
|
1043
|
+
if (!channel) return;
|
|
1044
|
+
const stale = channel.connectors.filter((c) => !touched.has(c.id));
|
|
1045
|
+
for (const connector of stale) this.channels.removeConnector(channelName, connector.name);
|
|
1046
|
+
}
|
|
1047
|
+
async resolveField(input) {
|
|
1048
|
+
if (input.literal !== void 0 && input.envVar !== void 0) throw new Error(`${input.label} is set both as a literal and as env.${input.label.split(".").pop()}; pick one`);
|
|
1049
|
+
if (input.literal !== void 0 && input.literal !== "") return input.literal;
|
|
1050
|
+
if (input.envVar !== void 0 && input.envVar !== "") {
|
|
1051
|
+
const fromProcessEnv = this.env[input.envVar];
|
|
1052
|
+
if (fromProcessEnv) return fromProcessEnv;
|
|
1053
|
+
const fromDotenv = input.dotenv[input.envVar];
|
|
1054
|
+
if (fromDotenv) return fromDotenv;
|
|
1055
|
+
throw new Error(`${input.label} references env var "${input.envVar}" but it is not set in process env or .env.local`);
|
|
1056
|
+
}
|
|
1057
|
+
if (input.existing) return input.existing;
|
|
1058
|
+
return await this.prompter.promptSecret(input.label);
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
//#endregion
|
|
723
1062
|
//#region lib/engine/logger/memory-logger.ts
|
|
724
1063
|
var MemoryFunnelLogger = class extends FunnelLogger {
|
|
725
1064
|
file = null;
|
|
@@ -977,6 +1316,73 @@ var FunnelProfiles = class {
|
|
|
977
1316
|
}
|
|
978
1317
|
};
|
|
979
1318
|
//#endregion
|
|
1319
|
+
//#region lib/engine/token-prompter/node-token-prompter.ts
|
|
1320
|
+
const STAR = "*";
|
|
1321
|
+
const CR = "\r";
|
|
1322
|
+
const LF = "\n";
|
|
1323
|
+
const BACKSPACE = String.fromCharCode(8);
|
|
1324
|
+
const DEL = String.fromCharCode(127);
|
|
1325
|
+
const CTRL_C = String.fromCharCode(3);
|
|
1326
|
+
const CTRL_D = String.fromCharCode(4);
|
|
1327
|
+
/**
|
|
1328
|
+
* Reads a secret from stdin in raw mode. Echoes a `*` per byte so the user
|
|
1329
|
+
* can see progress without exposing the token. Refuses to prompt when stdin
|
|
1330
|
+
* is not a TTY — callers should surface the resulting error with a hint
|
|
1331
|
+
* pointing at the corresponding env var or CLI command.
|
|
1332
|
+
*/
|
|
1333
|
+
var NodeFunnelTokenPrompter = class extends FunnelTokenPrompter {
|
|
1334
|
+
async promptSecret(label) {
|
|
1335
|
+
if (!stdin.isTTY) throw new Error(`cannot prompt for "${label}": stdin is not a TTY. Set the matching env var or run \`fnl channels <ch> connectors add ...\` first.`);
|
|
1336
|
+
stderr.write(`${label}: `);
|
|
1337
|
+
const wasRaw = stdin.isRaw;
|
|
1338
|
+
stdin.setRawMode(true);
|
|
1339
|
+
stdin.resume();
|
|
1340
|
+
try {
|
|
1341
|
+
return await this.readSecret();
|
|
1342
|
+
} finally {
|
|
1343
|
+
stdin.setRawMode(wasRaw);
|
|
1344
|
+
stdin.pause();
|
|
1345
|
+
stderr.write(LF);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
readSecret() {
|
|
1349
|
+
return new Promise((resolve, reject) => {
|
|
1350
|
+
let buffer = "";
|
|
1351
|
+
const onData = (chunk) => {
|
|
1352
|
+
for (const byte of chunk) {
|
|
1353
|
+
const char = String.fromCharCode(byte);
|
|
1354
|
+
if (char === LF || char === CR) {
|
|
1355
|
+
stdin.off("data", onData);
|
|
1356
|
+
resolve(buffer);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
if (char === CTRL_C) {
|
|
1360
|
+
stdin.off("data", onData);
|
|
1361
|
+
reject(/* @__PURE__ */ new Error("prompt cancelled"));
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (char === CTRL_D) {
|
|
1365
|
+
stdin.off("data", onData);
|
|
1366
|
+
if (buffer.length === 0) reject(/* @__PURE__ */ new Error("prompt cancelled"));
|
|
1367
|
+
else resolve(buffer);
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (char === BACKSPACE || char === DEL) {
|
|
1371
|
+
if (buffer.length > 0) {
|
|
1372
|
+
buffer = buffer.slice(0, -1);
|
|
1373
|
+
stderr.write("\b \b");
|
|
1374
|
+
}
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
buffer += char;
|
|
1378
|
+
stderr.write(STAR);
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
stdin.on("data", onData);
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
//#endregion
|
|
980
1386
|
//#region lib/engine/settings/mock-settings-reader.ts
|
|
981
1387
|
const createSettings = (partial = {}) => ({
|
|
982
1388
|
version: 1,
|
|
@@ -1096,14 +1502,18 @@ var FunnelChannelPublisher = class {
|
|
|
1096
1502
|
* The candidates cover:
|
|
1097
1503
|
* 1. dev: this helper lives at lib/gateway/, so daemon.ts is its sibling
|
|
1098
1504
|
* 2. built sibling: dist/gateway/daemon.js if the helper itself ends up at dist/gateway/
|
|
1099
|
-
* 3. bundled: when this helper is inlined into dist/bin.js,
|
|
1505
|
+
* 3. bundled: when this helper is inlined into dist/bin.js, the helper's dir is dist/,
|
|
1100
1506
|
* and daemon.js lives at dist/gateway/daemon.js
|
|
1507
|
+
*
|
|
1508
|
+
* `import.meta.url` works in both Bun and Node test runners; `import.meta.dir`
|
|
1509
|
+
* is Bun-only and breaks vitest.
|
|
1101
1510
|
*/
|
|
1102
1511
|
const resolveDaemonScript = () => {
|
|
1512
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
1103
1513
|
const candidates = [
|
|
1104
|
-
resolve(
|
|
1105
|
-
resolve(
|
|
1106
|
-
resolve(
|
|
1514
|
+
resolve(here, "./daemon.ts"),
|
|
1515
|
+
resolve(here, "./daemon.js"),
|
|
1516
|
+
resolve(here, "./gateway/daemon.js")
|
|
1107
1517
|
];
|
|
1108
1518
|
for (const candidate of candidates) if (existsSync(candidate)) return candidate;
|
|
1109
1519
|
throw new Error(`daemon script not found (looked in ${candidates.join(", ")})`);
|
|
@@ -1131,6 +1541,7 @@ var FunnelGateway = class {
|
|
|
1131
1541
|
process;
|
|
1132
1542
|
fs;
|
|
1133
1543
|
clock;
|
|
1544
|
+
dir;
|
|
1134
1545
|
pidFile;
|
|
1135
1546
|
logDir;
|
|
1136
1547
|
gatewayLog;
|
|
@@ -1141,9 +1552,9 @@ var FunnelGateway = class {
|
|
|
1141
1552
|
this.process = deps.process ?? defaultProcess$1;
|
|
1142
1553
|
this.fs = deps.fs ?? defaultFs$1;
|
|
1143
1554
|
this.clock = deps.clock ?? defaultClock;
|
|
1144
|
-
|
|
1555
|
+
this.dir = deps.dir ?? FUNNEL_DIR;
|
|
1145
1556
|
this.tmpDir = deps.tmpDir ?? DEFAULT_TMP_DIR$1;
|
|
1146
|
-
this.pidFile = join(
|
|
1557
|
+
this.pidFile = join(this.dir, "gateway.pid");
|
|
1147
1558
|
this.logDir = join(this.tmpDir, "events");
|
|
1148
1559
|
this.gatewayLog = join(this.tmpDir, "gateway.log");
|
|
1149
1560
|
this.port = deps.port ?? DEFAULT_PORT$1;
|
|
@@ -1174,15 +1585,15 @@ var FunnelGateway = class {
|
|
|
1174
1585
|
"-c",
|
|
1175
1586
|
command
|
|
1176
1587
|
]);
|
|
1177
|
-
const deadline =
|
|
1178
|
-
while (
|
|
1588
|
+
const deadline = this.clock.millis() + STARTUP_TIMEOUT_MS;
|
|
1589
|
+
while (this.clock.millis() < deadline) {
|
|
1179
1590
|
if (this.isRunning()) return true;
|
|
1180
1591
|
await this.sleep(POLL_INTERVAL_MS$1);
|
|
1181
1592
|
}
|
|
1182
1593
|
return this.isRunning();
|
|
1183
1594
|
}
|
|
1184
1595
|
buildStartCommand(gatewayScript, options = {}) {
|
|
1185
|
-
return `nohup ${options.caffeinate !== false && globalThis.process.platform === "darwin" ? "caffeinate -i " : ""}bun ${gatewayScript} >> ${this.gatewayLog} 2>&1 &`;
|
|
1596
|
+
return `nohup ${options.caffeinate !== false && globalThis.process.platform === "darwin" ? "caffeinate -i " : ""}bun ${gatewayScript} ${`funnel-gateway[${this.dir}]`} >> ${this.gatewayLog} 2>&1 &`;
|
|
1186
1597
|
}
|
|
1187
1598
|
async stop() {
|
|
1188
1599
|
const pid = this.readPid();
|
|
@@ -2083,12 +2494,15 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
2083
2494
|
//#region lib/gateway/kill-competing-slack-gateways.ts
|
|
2084
2495
|
const defaultProcess = new NodeFunnelProcessRunner();
|
|
2085
2496
|
const defaultLogger$1 = new NodeFunnelLogger();
|
|
2086
|
-
const
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2497
|
+
const titleFor = (dir) => `funnel-gateway[${dir}]`;
|
|
2498
|
+
/**
|
|
2499
|
+
* Kills other funnel daemon processes that share the SAME funnel home dir,
|
|
2500
|
+
* which is the only situation that causes a real conflict (duplicate Slack
|
|
2501
|
+
* Socket Mode connections with the same tokens). Daemons rooted at a
|
|
2502
|
+
* different `~/.funnel/` are left alone — they hold different tokens and
|
|
2503
|
+
* speak to different Slack apps. The daemon advertises its dir via
|
|
2504
|
+
* `process.title = "funnel-gateway[<dir>]"`, which this routine matches.
|
|
2505
|
+
*/
|
|
2092
2506
|
const killCompetingSlackGateways = async (props) => {
|
|
2093
2507
|
const runner = props.process ?? defaultProcess;
|
|
2094
2508
|
const logger = props.logger ?? defaultLogger$1;
|
|
@@ -2099,6 +2513,7 @@ const killCompetingSlackGateways = async (props) => {
|
|
|
2099
2513
|
"pid=,args="
|
|
2100
2514
|
]);
|
|
2101
2515
|
if (result.exitCode !== 0) return [];
|
|
2516
|
+
const expectedTitle = titleFor(props.dir);
|
|
2102
2517
|
const killed = [];
|
|
2103
2518
|
for (const raw of result.stdout.split("\n")) {
|
|
2104
2519
|
const line = raw.trim();
|
|
@@ -2109,8 +2524,7 @@ const killCompetingSlackGateways = async (props) => {
|
|
|
2109
2524
|
const args = match[2];
|
|
2110
2525
|
if (!Number.isInteger(pid) || pid <= 0) continue;
|
|
2111
2526
|
if (pid === props.selfPid) continue;
|
|
2112
|
-
if (!
|
|
2113
|
-
if (!looksLikeSlackGateway(args)) continue;
|
|
2527
|
+
if (!args.includes(expectedTitle)) continue;
|
|
2114
2528
|
runner.kill(pid, "SIGTERM");
|
|
2115
2529
|
killed.push(pid);
|
|
2116
2530
|
logger.info("killed competing Slack gateway process", {
|
|
@@ -2302,12 +2716,14 @@ var FunnelGatewayServer = class {
|
|
|
2302
2716
|
process;
|
|
2303
2717
|
logger;
|
|
2304
2718
|
selfPid;
|
|
2719
|
+
dir;
|
|
2305
2720
|
killCompetingSlack;
|
|
2306
2721
|
token;
|
|
2307
2722
|
broadcaster;
|
|
2308
2723
|
eventStore;
|
|
2309
2724
|
supervisor;
|
|
2310
2725
|
nowMs;
|
|
2726
|
+
extraRoutes;
|
|
2311
2727
|
startedAt = null;
|
|
2312
2728
|
server = null;
|
|
2313
2729
|
constructor(deps) {
|
|
@@ -2318,8 +2734,10 @@ var FunnelGatewayServer = class {
|
|
|
2318
2734
|
this.process = deps.process;
|
|
2319
2735
|
this.logger = deps.logger ?? defaultLogger;
|
|
2320
2736
|
this.selfPid = deps.selfPid ?? globalThis.process.pid;
|
|
2737
|
+
this.dir = deps.dir ?? FUNNEL_DIR;
|
|
2321
2738
|
this.killCompetingSlack = deps.killCompetingSlack ?? true;
|
|
2322
2739
|
this.token = deps.token ?? "";
|
|
2740
|
+
this.extraRoutes = deps.extraRoutes ?? null;
|
|
2323
2741
|
const clock = deps.clock;
|
|
2324
2742
|
this.nowMs = clock ? () => clock.millis() : () => Date.now();
|
|
2325
2743
|
if (!existsSync(this.logDir)) mkdirSync(this.logDir, { recursive: true });
|
|
@@ -2481,7 +2899,7 @@ var FunnelGatewayServer = class {
|
|
|
2481
2899
|
base.use("/status", requireBearerToken({ expected: this.token }));
|
|
2482
2900
|
base.use("/channels/*", requireBearerToken({ expected: this.token }));
|
|
2483
2901
|
}
|
|
2484
|
-
return base.route("/", gatewayRoutes);
|
|
2902
|
+
return (this.extraRoutes ? base.route("/", this.extraRoutes) : base).route("/", gatewayRoutes);
|
|
2485
2903
|
}
|
|
2486
2904
|
/**
|
|
2487
2905
|
* Reads the bearer token from the WebSocket upgrade request. Accepts:
|
|
@@ -2511,6 +2929,7 @@ var FunnelGatewayServer = class {
|
|
|
2511
2929
|
if (this.killCompetingSlack && allConnectors.some((c) => c.type === "slack")) {
|
|
2512
2930
|
const killed = await killCompetingSlackGateways({
|
|
2513
2931
|
selfPid: this.selfPid,
|
|
2932
|
+
dir: this.dir,
|
|
2514
2933
|
process: this.process,
|
|
2515
2934
|
logger: this.logger
|
|
2516
2935
|
});
|
|
@@ -2737,7 +3156,7 @@ const SANDBOX_TMP_DIR = "/sandbox/tmp";
|
|
|
2737
3156
|
* ```
|
|
2738
3157
|
*/
|
|
2739
3158
|
var Funnel = class Funnel {
|
|
2740
|
-
|
|
3159
|
+
memos = {};
|
|
2741
3160
|
constructor(props = {}) {
|
|
2742
3161
|
this.props = props;
|
|
2743
3162
|
Object.freeze(this);
|
|
@@ -2759,12 +3178,6 @@ var Funnel = class Funnel {
|
|
|
2759
3178
|
tmpDir: props.tmpDir ?? SANDBOX_TMP_DIR
|
|
2760
3179
|
});
|
|
2761
3180
|
}
|
|
2762
|
-
memo(key, build) {
|
|
2763
|
-
if (this.cache.has(key)) return this.cache.get(key);
|
|
2764
|
-
const value = build();
|
|
2765
|
-
this.cache.set(key, value);
|
|
2766
|
-
return value;
|
|
2767
|
-
}
|
|
2768
3181
|
/** Resolved on-disk paths the facade will read/write when methods are called. Pure compute, not memoized. */
|
|
2769
3182
|
get paths() {
|
|
2770
3183
|
const dir = this.props.dir ?? FUNNEL_DIR;
|
|
@@ -2776,61 +3189,97 @@ var Funnel = class Funnel {
|
|
|
2776
3189
|
}
|
|
2777
3190
|
/** Filesystem boundary. Defaults to NodeFunnelFileSystem. */
|
|
2778
3191
|
get fs() {
|
|
2779
|
-
|
|
3192
|
+
if (!this.memos.fs) this.memos.fs = this.props.fs ?? new NodeFunnelFileSystem();
|
|
3193
|
+
return this.memos.fs;
|
|
2780
3194
|
}
|
|
2781
3195
|
/** Process runner boundary. Defaults to NodeFunnelProcessRunner. */
|
|
2782
3196
|
get process() {
|
|
2783
|
-
|
|
3197
|
+
if (!this.memos.process) this.memos.process = this.props.process ?? new NodeFunnelProcessRunner();
|
|
3198
|
+
return this.memos.process;
|
|
2784
3199
|
}
|
|
2785
3200
|
/** Logger boundary. Defaults to NodeFunnelLogger. */
|
|
2786
3201
|
get logger() {
|
|
2787
|
-
|
|
3202
|
+
if (!this.memos.logger) this.memos.logger = this.props.logger ?? new NodeFunnelLogger();
|
|
3203
|
+
return this.memos.logger;
|
|
2788
3204
|
}
|
|
2789
3205
|
/** Clock boundary. Defaults to NodeFunnelClock. */
|
|
2790
3206
|
get clock() {
|
|
2791
|
-
|
|
3207
|
+
if (!this.memos.clock) this.memos.clock = this.props.clock ?? new NodeFunnelClock();
|
|
3208
|
+
return this.memos.clock;
|
|
2792
3209
|
}
|
|
2793
3210
|
/** ID generator boundary. Defaults to NodeFunnelIdGenerator. */
|
|
2794
3211
|
get idGenerator() {
|
|
2795
|
-
|
|
3212
|
+
if (!this.memos.idGenerator) this.memos.idGenerator = this.props.idGenerator ?? new NodeFunnelIdGenerator();
|
|
3213
|
+
return this.memos.idGenerator;
|
|
2796
3214
|
}
|
|
2797
3215
|
/** Settings reader. If not injected, a FunnelSettingsStore rooted at `dir` is created. */
|
|
2798
3216
|
get store() {
|
|
2799
|
-
|
|
3217
|
+
if (!this.memos.store) this.memos.store = this.props.store ?? new FunnelSettingsStore({
|
|
2800
3218
|
path: this.paths.settings,
|
|
2801
3219
|
fs: this.fs
|
|
2802
|
-
})
|
|
3220
|
+
});
|
|
3221
|
+
return this.memos.store;
|
|
2803
3222
|
}
|
|
2804
3223
|
/** Pure factory that constructs per-type listeners and adapters from connector configs. */
|
|
2805
3224
|
get factory() {
|
|
2806
|
-
|
|
3225
|
+
if (!this.memos.factory) this.memos.factory = new FunnelConnectorFactory({
|
|
2807
3226
|
fs: this.fs,
|
|
2808
3227
|
process: this.process,
|
|
2809
3228
|
logger: this.logger,
|
|
2810
|
-
dir: this.paths.dir
|
|
2811
|
-
|
|
3229
|
+
dir: this.paths.dir,
|
|
3230
|
+
slackListenerOptions: this.props.slackListenerOptions,
|
|
3231
|
+
scheduleListenerOptions: this.props.scheduleListenerOptions
|
|
3232
|
+
});
|
|
3233
|
+
return this.memos.factory;
|
|
2812
3234
|
}
|
|
2813
3235
|
/** Channel CRUD + nested connector CRUD + schedule entries + listener/adapter dispatch. */
|
|
2814
3236
|
get channels() {
|
|
2815
|
-
|
|
3237
|
+
if (!this.memos.channels) this.memos.channels = new FunnelChannels({
|
|
2816
3238
|
store: this.store,
|
|
2817
3239
|
factory: this.factory,
|
|
2818
3240
|
profileChecker: this.profiles,
|
|
2819
3241
|
clock: this.clock,
|
|
2820
3242
|
idGenerator: this.idGenerator
|
|
2821
|
-
})
|
|
3243
|
+
});
|
|
3244
|
+
return this.memos.channels;
|
|
2822
3245
|
}
|
|
2823
3246
|
/** Launch profiles (named presets for `fnl claude`: path + sub-agent + channel id). */
|
|
2824
3247
|
get profiles() {
|
|
2825
|
-
|
|
3248
|
+
if (!this.memos.profiles) this.memos.profiles = new FunnelProfiles({ store: this.store });
|
|
3249
|
+
return this.memos.profiles;
|
|
3250
|
+
}
|
|
3251
|
+
/** Reads `funnel.json` from a cwd. `fnl claude` consults it before falling back to the default profile. */
|
|
3252
|
+
get localConfig() {
|
|
3253
|
+
if (!this.memos.localConfig) this.memos.localConfig = new FunnelLocalConfig({ fs: this.fs });
|
|
3254
|
+
return this.memos.localConfig;
|
|
3255
|
+
}
|
|
3256
|
+
/** Parses `.env.local` from a cwd (used by sync to back $VAR references). */
|
|
3257
|
+
get dotenv() {
|
|
3258
|
+
if (!this.memos.dotenv) this.memos.dotenv = new FunnelDotenvReader({ fs: this.fs });
|
|
3259
|
+
return this.memos.dotenv;
|
|
3260
|
+
}
|
|
3261
|
+
/** Secret prompter. Defaults to a TTY-only stdin reader; tests inject MemoryFunnelTokenPrompter. */
|
|
3262
|
+
get tokenPrompter() {
|
|
3263
|
+
if (!this.memos.tokenPrompter) this.memos.tokenPrompter = this.props.tokenPrompter ?? new NodeFunnelTokenPrompter();
|
|
3264
|
+
return this.memos.tokenPrompter;
|
|
3265
|
+
}
|
|
3266
|
+
/** Reconciles funnel.json's channel + connectors with `~/.funnel/settings.json` on launch. */
|
|
3267
|
+
get localConfigSync() {
|
|
3268
|
+
if (!this.memos.localConfigSync) this.memos.localConfigSync = new FunnelLocalConfigSync({
|
|
3269
|
+
channels: this.channels,
|
|
3270
|
+
dotenv: this.dotenv,
|
|
3271
|
+
prompter: this.tokenPrompter
|
|
3272
|
+
});
|
|
3273
|
+
return this.memos.localConfigSync;
|
|
2826
3274
|
}
|
|
2827
3275
|
/** funnel MCP installer (writes/removes `.mcp.json` entries in target repos). */
|
|
2828
3276
|
get mcp() {
|
|
2829
|
-
|
|
3277
|
+
if (!this.memos.mcp) this.memos.mcp = new FunnelMcp({ fs: this.fs });
|
|
3278
|
+
return this.memos.mcp;
|
|
2830
3279
|
}
|
|
2831
3280
|
/** Launch Claude Code with a channel injected via env, MCP installed, gateway ensured. */
|
|
2832
3281
|
get claude() {
|
|
2833
|
-
|
|
3282
|
+
if (!this.memos.claude) this.memos.claude = new FunnelClaude({
|
|
2834
3283
|
channels: this.channels,
|
|
2835
3284
|
mcp: this.mcp,
|
|
2836
3285
|
gateway: this.gateway,
|
|
@@ -2838,24 +3287,27 @@ var Funnel = class Funnel {
|
|
|
2838
3287
|
process: this.process,
|
|
2839
3288
|
logger: this.logger,
|
|
2840
3289
|
dir: this.paths.dir
|
|
2841
|
-
})
|
|
3290
|
+
});
|
|
3291
|
+
return this.memos.claude;
|
|
2842
3292
|
}
|
|
2843
3293
|
/** Gateway daemon controller (PID-file, start/stop the separate `bun daemon.ts` process). */
|
|
2844
3294
|
get gateway() {
|
|
2845
|
-
|
|
3295
|
+
if (!this.memos.gateway) this.memos.gateway = new FunnelGateway({
|
|
2846
3296
|
fs: this.fs,
|
|
2847
3297
|
process: this.process,
|
|
2848
3298
|
clock: this.clock,
|
|
2849
3299
|
dir: this.paths.dir,
|
|
2850
3300
|
tmpDir: this.paths.tmpDir
|
|
2851
|
-
})
|
|
3301
|
+
});
|
|
3302
|
+
return this.memos.gateway;
|
|
2852
3303
|
}
|
|
2853
3304
|
/** Read / generate the daemon's gateway token (mode 0600 file under `dir`). */
|
|
2854
3305
|
get gatewayToken() {
|
|
2855
|
-
|
|
3306
|
+
if (!this.memos.gatewayToken) this.memos.gatewayToken = new FunnelGatewayToken({
|
|
2856
3307
|
fs: this.fs,
|
|
2857
3308
|
dir: this.paths.dir
|
|
2858
|
-
})
|
|
3309
|
+
});
|
|
3310
|
+
return this.memos.gatewayToken;
|
|
2859
3311
|
}
|
|
2860
3312
|
/**
|
|
2861
3313
|
* HTTP client for `POST /channels/:channel/publish` on the running gateway
|
|
@@ -2863,15 +3315,16 @@ var Funnel = class Funnel {
|
|
|
2863
3315
|
* connector. Returns `{ state: "offline" }` if the daemon isn't up.
|
|
2864
3316
|
*/
|
|
2865
3317
|
get publisher() {
|
|
2866
|
-
|
|
3318
|
+
if (!this.memos.publisher) {
|
|
2867
3319
|
const gateway = this.gateway;
|
|
2868
3320
|
const token = this.gatewayToken;
|
|
2869
|
-
|
|
3321
|
+
this.memos.publisher = new FunnelChannelPublisher({
|
|
2870
3322
|
port: gateway.getPort(),
|
|
2871
3323
|
isDaemonRunning: () => gateway.isRunning(),
|
|
2872
3324
|
getToken: () => token.read()
|
|
2873
3325
|
});
|
|
2874
|
-
}
|
|
3326
|
+
}
|
|
3327
|
+
return this.memos.publisher;
|
|
2875
3328
|
}
|
|
2876
3329
|
/**
|
|
2877
3330
|
* HTTP client for listener operations on the running gateway daemon.
|
|
@@ -2879,15 +3332,16 @@ var Funnel = class Funnel {
|
|
|
2879
3332
|
* paths stay write-only without parsing strings.
|
|
2880
3333
|
*/
|
|
2881
3334
|
get listeners() {
|
|
2882
|
-
|
|
3335
|
+
if (!this.memos.listeners) {
|
|
2883
3336
|
const gateway = this.gateway;
|
|
2884
3337
|
const token = this.gatewayToken;
|
|
2885
|
-
|
|
3338
|
+
this.memos.listeners = new FunnelListenersClient({
|
|
2886
3339
|
port: gateway.getPort(),
|
|
2887
3340
|
isDaemonRunning: () => gateway.isRunning(),
|
|
2888
3341
|
getToken: () => token.read()
|
|
2889
3342
|
});
|
|
2890
|
-
}
|
|
3343
|
+
}
|
|
3344
|
+
return this.memos.listeners;
|
|
2891
3345
|
}
|
|
2892
3346
|
/**
|
|
2893
3347
|
* In-process gateway server. Unlike `gateway.start()` (which spawns a daemon),
|
|
@@ -2904,7 +3358,8 @@ var Funnel = class Funnel {
|
|
|
2904
3358
|
clock: this.clock,
|
|
2905
3359
|
logger: this.logger,
|
|
2906
3360
|
killCompetingSlack: options.killCompetingSlack,
|
|
2907
|
-
token: options.token ?? this.gatewayToken.ensure()
|
|
3361
|
+
token: options.token ?? this.gatewayToken.ensure(),
|
|
3362
|
+
extraRoutes: options.extraRoutes
|
|
2908
3363
|
});
|
|
2909
3364
|
}
|
|
2910
3365
|
};
|
|
@@ -3016,7 +3471,7 @@ const startChannelServer = async (options = {}) => {
|
|
|
3016
3471
|
const channelId = options.channelId ?? process.env.FUNNEL_CHANNEL_ID;
|
|
3017
3472
|
const channel = channelId ? readChannelConnectors(dir, channelId) : null;
|
|
3018
3473
|
const token = options.token ?? readGatewayToken(dir);
|
|
3019
|
-
const server = new Server
|
|
3474
|
+
const server = new Server({
|
|
3020
3475
|
name: FUNNEL_MCP_NAME,
|
|
3021
3476
|
version: "1.0.0"
|
|
3022
3477
|
}, {
|
|
@@ -3091,6 +3546,41 @@ const startChannelServer = async (options = {}) => {
|
|
|
3091
3546
|
}).start();
|
|
3092
3547
|
};
|
|
3093
3548
|
//#endregion
|
|
3549
|
+
//#region lib/engine/local-config/local-config-json-schema.ts
|
|
3550
|
+
/**
|
|
3551
|
+
* Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
|
|
3552
|
+
* `$schema` references in committed `funnel.json` files so editors can give
|
|
3553
|
+
* autocomplete and validation for channel / subAgent / env / connectors[]
|
|
3554
|
+
* without anyone hand-maintaining a separate schema.
|
|
3555
|
+
*/
|
|
3556
|
+
const funnelJsonSchema = () => {
|
|
3557
|
+
return {
|
|
3558
|
+
...z.toJSONSchema(localConfigSchema, { target: "draft-2020-12" }),
|
|
3559
|
+
title: "Funnel per-repo launch config",
|
|
3560
|
+
description: "Used by `fnl claude` when no --profile / --channel is given. Declares the channel to subscribe to, optional sub-agent and brief flag, environment variables to layer under process.env, and optional connectors to materialize into ~/.funnel/settings.json on launch."
|
|
3561
|
+
};
|
|
3562
|
+
};
|
|
3563
|
+
//#endregion
|
|
3564
|
+
//#region lib/engine/token-prompter/memory-token-prompter.ts
|
|
3565
|
+
/**
|
|
3566
|
+
* Pre-seeded answers keyed by prompt label. Tests configure the map up front;
|
|
3567
|
+
* unmapped labels throw so the test surfaces unexpected prompts loudly.
|
|
3568
|
+
*/
|
|
3569
|
+
var MemoryFunnelTokenPrompter = class extends FunnelTokenPrompter {
|
|
3570
|
+
answers;
|
|
3571
|
+
asked = [];
|
|
3572
|
+
constructor(props = {}) {
|
|
3573
|
+
super();
|
|
3574
|
+
this.answers = new Map(Object.entries(props.answers ?? {}));
|
|
3575
|
+
}
|
|
3576
|
+
async promptSecret(label) {
|
|
3577
|
+
this.asked.push(label);
|
|
3578
|
+
const answer = this.answers.get(label);
|
|
3579
|
+
if (answer === void 0) throw new Error(`no answer seeded for prompt "${label}"`);
|
|
3580
|
+
return answer;
|
|
3581
|
+
}
|
|
3582
|
+
};
|
|
3583
|
+
//#endregion
|
|
3094
3584
|
//#region lib/cli/factory.ts
|
|
3095
3585
|
const factory = createFactory();
|
|
3096
3586
|
//#endregion
|
|
@@ -3585,16 +4075,23 @@ examples:
|
|
|
3585
4075
|
const claudeHelp = `funnel claude — launch Claude Code
|
|
3586
4076
|
|
|
3587
4077
|
usage:
|
|
3588
|
-
funnel claude launch
|
|
4078
|
+
funnel claude launch using funnel.json in cwd, or the default profile
|
|
3589
4079
|
funnel claude -p <name> launch a named profile
|
|
3590
4080
|
funnel claude --profile <name> (long form)
|
|
3591
4081
|
funnel claude --channel <name> raw launch (no profile, cwd = current dir)
|
|
4082
|
+
funnel claude [...] any other argument is forwarded to the claude CLI
|
|
3592
4083
|
|
|
3593
|
-
|
|
4084
|
+
resolution order when no --profile / --channel is given:
|
|
4085
|
+
1. ./funnel.json in the current directory
|
|
4086
|
+
2. the default profile (first entry in fnl profiles)
|
|
4087
|
+
|
|
4088
|
+
funnel-specific options (everything else passes through to claude verbatim):
|
|
3594
4089
|
-p, --profile profile name to launch
|
|
3595
4090
|
--channel channel name (raw launch, ignored when --profile is given)
|
|
4091
|
+
-h, --help show this help
|
|
3596
4092
|
|
|
3597
|
-
|
|
4093
|
+
Positional args, unknown short flags (e.g. -c, -r), and claude's own flags
|
|
4094
|
+
(--agent, --resume, --model, --print, --output-format ...) are all forwarded.
|
|
3598
4095
|
On launch the FUNNEL_CHANNEL_ID env var is set and MCP connects to the gateway.`;
|
|
3599
4096
|
const RESERVED_KEYS$1 = ["profile", "channel"];
|
|
3600
4097
|
const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
@@ -3603,25 +4100,48 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
|
3603
4100
|
}).passthrough(), claudeHelp), async (c) => {
|
|
3604
4101
|
const query = c.req.valid("query");
|
|
3605
4102
|
const funnel = c.var.funnel;
|
|
4103
|
+
const userArgs = queryToCliArgs(c.req.url, RESERVED_KEYS$1);
|
|
3606
4104
|
if (query.channel && !query.profile) {
|
|
3607
4105
|
const exitCode = await funnel.claude.launch({
|
|
3608
4106
|
channel: query.channel,
|
|
3609
|
-
userArgs
|
|
4107
|
+
userArgs
|
|
4108
|
+
});
|
|
4109
|
+
process.exit(exitCode);
|
|
4110
|
+
}
|
|
4111
|
+
if (query.profile) {
|
|
4112
|
+
const profile = funnel.profiles.get(query.profile);
|
|
4113
|
+
if (!profile) throw new HTTPException(404, { message: `profile "${query.profile}" not found` });
|
|
4114
|
+
const exitCode = await funnel.claude.launch({
|
|
4115
|
+
channel: profile.channelId,
|
|
4116
|
+
cwd: profile.path,
|
|
4117
|
+
subAgent: profile.subAgent,
|
|
4118
|
+
userArgs,
|
|
4119
|
+
profileName: profile.name,
|
|
4120
|
+
brief: profile.brief
|
|
3610
4121
|
});
|
|
3611
4122
|
process.exit(exitCode);
|
|
3612
4123
|
}
|
|
3613
|
-
const
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
4124
|
+
const cwd = process.cwd();
|
|
4125
|
+
const local = funnel.localConfig.read(cwd);
|
|
4126
|
+
if (local) {
|
|
4127
|
+
await funnel.localConfigSync.ensure(local, cwd);
|
|
4128
|
+
const exitCode = await funnel.claude.launch({
|
|
4129
|
+
channel: local.channel,
|
|
4130
|
+
cwd,
|
|
4131
|
+
userArgs: [...local.options ?? [], ...userArgs],
|
|
4132
|
+
extraEnv: local.env
|
|
4133
|
+
});
|
|
4134
|
+
process.exit(exitCode);
|
|
3617
4135
|
}
|
|
4136
|
+
const defaultProfile = funnel.profiles.getDefault();
|
|
4137
|
+
if (!defaultProfile) return c.text(claudeHelp);
|
|
3618
4138
|
const exitCode = await funnel.claude.launch({
|
|
3619
|
-
channel:
|
|
3620
|
-
cwd:
|
|
3621
|
-
subAgent:
|
|
3622
|
-
userArgs
|
|
3623
|
-
profileName:
|
|
3624
|
-
brief:
|
|
4139
|
+
channel: defaultProfile.channelId,
|
|
4140
|
+
cwd: defaultProfile.path,
|
|
4141
|
+
subAgent: defaultProfile.subAgent,
|
|
4142
|
+
userArgs,
|
|
4143
|
+
profileName: defaultProfile.name,
|
|
4144
|
+
brief: defaultProfile.brief
|
|
3625
4145
|
});
|
|
3626
4146
|
process.exit(exitCode);
|
|
3627
4147
|
});
|
|
@@ -3985,6 +4505,24 @@ examples:
|
|
|
3985
4505
|
});
|
|
3986
4506
|
return c.text(lines.join("\n"));
|
|
3987
4507
|
});
|
|
4508
|
+
const schemaHandler = factory.createHandlers(zValidator$1("query", z.object({}), `funnel schema — print the JSON Schema for funnel.json
|
|
4509
|
+
|
|
4510
|
+
usage: funnel schema
|
|
4511
|
+
|
|
4512
|
+
Outputs the draft 2020-12 JSON Schema describing the per-repo funnel.json
|
|
4513
|
+
file. Pipe it into a local file and reference it from funnel.json so editors
|
|
4514
|
+
can validate and autocomplete the config:
|
|
4515
|
+
|
|
4516
|
+
fnl schema > funnel.schema.json
|
|
4517
|
+
|
|
4518
|
+
# funnel.json
|
|
4519
|
+
{
|
|
4520
|
+
"$schema": "./funnel.schema.json",
|
|
4521
|
+
"channel": "ops"
|
|
4522
|
+
}`), async (c) => {
|
|
4523
|
+
const schema = funnelJsonSchema();
|
|
4524
|
+
return c.text(`${JSON.stringify(schema, null, 2)}\n`);
|
|
4525
|
+
});
|
|
3988
4526
|
//#endregion
|
|
3989
4527
|
//#region lib/cli/routes/status.ts
|
|
3990
4528
|
const statusHelp = `funnel status — show overall connection status
|
|
@@ -4078,7 +4616,7 @@ const createCliApp = (funnel) => {
|
|
|
4078
4616
|
if (error instanceof HTTPException) return c.text(`error: ${error.message}`, error.status);
|
|
4079
4617
|
return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
|
|
4080
4618
|
});
|
|
4081
|
-
return base.get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...helpRoute(addHelp$3)).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...helpRoute(removeHelp$1)).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...helpRoute(renameHelp$1)).post("/channels/:channel/rename", ...helpRoute(renameHelp$1)).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...helpRoute(publishHelp)).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...helpRoute(addHelp$2)).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...helpRoute(removeHelp$3)).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...helpRoute(setHelp$1)).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", ...helpRoute(renameHelp$2)).post("/channels/:channel/connectors/:connector/rename", ...helpRoute(renameHelp$2)).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", ...helpRoute(addHelp$1)).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...helpRoute(removeHelp$2)).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...helpRoute(addHelp)).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...helpRoute(setHelp)).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...helpRoute(removeHelp)).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...helpRoute(renameHelp)).post("/profiles/:profile/rename", ...helpRoute(renameHelp)).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/listeners", ...gatewayListenersHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
|
|
4619
|
+
return base.get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...helpRoute(addHelp$3)).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...helpRoute(removeHelp$1)).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...helpRoute(renameHelp$1)).post("/channels/:channel/rename", ...helpRoute(renameHelp$1)).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...helpRoute(publishHelp)).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...helpRoute(addHelp$2)).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...helpRoute(removeHelp$3)).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...helpRoute(setHelp$1)).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", ...helpRoute(renameHelp$2)).post("/channels/:channel/connectors/:connector/rename", ...helpRoute(renameHelp$2)).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", ...helpRoute(addHelp$1)).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...helpRoute(removeHelp$2)).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...helpRoute(addHelp)).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...helpRoute(setHelp)).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...helpRoute(removeHelp)).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...helpRoute(renameHelp)).post("/profiles/:profile/rename", ...helpRoute(renameHelp)).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/listeners", ...gatewayListenersHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
|
|
4082
4620
|
};
|
|
4083
4621
|
/** CLI Hono app wired to a default `new Funnel()`. For embedding with a custom Funnel use `createCliApp`. */
|
|
4084
4622
|
const app = createCliApp(new Funnel());
|
|
@@ -6293,4 +6831,4 @@ async function launchTui(funnel) {
|
|
|
6293
6831
|
});
|
|
6294
6832
|
}
|
|
6295
6833
|
//#endregion
|
|
6296
|
-
export { DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, channelConfigSchema, channelDeliveryModeSchema, app as cliApp, connectorConfigSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, ghConnectorSchema, launchTui, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
|
|
6834
|
+
export { DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, channelConfigSchema, channelDeliveryModeSchema, app as cliApp, connectorConfigSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
|