@interactive-inc/claude-funnel 0.28.0 → 0.30.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 +19 -21
- package/dist/bin.js +875 -880
- package/dist/gateway/daemon.js +225 -260
- package/dist/index.d.ts +39 -71
- package/dist/index.js +89 -107
- package/funnel.schema.json +3 -30
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -570,40 +570,14 @@ declare class FunnelClaude {
|
|
|
570
570
|
*/
|
|
571
571
|
type OnFunnelError = (error: Error, context?: Record<string, unknown>) => void;
|
|
572
572
|
//#endregion
|
|
573
|
-
//#region lib/engine/local-config/dotenv-reader.d.ts
|
|
574
|
-
type Deps$10 = {
|
|
575
|
-
fs: FunnelFileSystem;
|
|
576
|
-
};
|
|
577
|
-
/**
|
|
578
|
-
* Minimal `.env.local` parser. Supports `KEY=value` lines, blank lines, and
|
|
579
|
-
* `#` comments. Strips matching surrounding single or double quotes. No
|
|
580
|
-
* interpolation, no `export` prefix — anything fancier should live in a real
|
|
581
|
-
* env file loaded by the shell.
|
|
582
|
-
*/
|
|
583
|
-
declare class FunnelDotenvReader {
|
|
584
|
-
private readonly fs;
|
|
585
|
-
constructor(deps: Deps$10);
|
|
586
|
-
read(cwd: string): Record<string, string>;
|
|
587
|
-
}
|
|
588
|
-
//#endregion
|
|
589
573
|
//#region lib/engine/local-config/local-config-schema.d.ts
|
|
590
574
|
declare const connectorSpecSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
591
575
|
type: z.ZodLiteral<"slack">;
|
|
592
576
|
name: z.ZodString;
|
|
593
|
-
botToken: z.ZodOptional<z.ZodString>;
|
|
594
|
-
appToken: z.ZodOptional<z.ZodString>;
|
|
595
577
|
minify: z.ZodOptional<z.ZodBoolean>;
|
|
596
|
-
env: z.ZodOptional<z.ZodObject<{
|
|
597
|
-
botToken: z.ZodOptional<z.ZodString>;
|
|
598
|
-
appToken: z.ZodOptional<z.ZodString>;
|
|
599
|
-
}, z.core.$strip>>;
|
|
600
578
|
}, z.core.$strip>, z.ZodObject<{
|
|
601
579
|
type: z.ZodLiteral<"discord">;
|
|
602
580
|
name: z.ZodString;
|
|
603
|
-
botToken: z.ZodOptional<z.ZodString>;
|
|
604
|
-
env: z.ZodOptional<z.ZodObject<{
|
|
605
|
-
botToken: z.ZodOptional<z.ZodString>;
|
|
606
|
-
}, z.core.$strip>>;
|
|
607
581
|
}, z.core.$strip>, z.ZodObject<{
|
|
608
582
|
type: z.ZodLiteral<"gh">;
|
|
609
583
|
name: z.ZodString;
|
|
@@ -618,20 +592,10 @@ declare const channelSpecSchema: z.ZodObject<{
|
|
|
618
592
|
connectors: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
619
593
|
type: z.ZodLiteral<"slack">;
|
|
620
594
|
name: z.ZodString;
|
|
621
|
-
botToken: z.ZodOptional<z.ZodString>;
|
|
622
|
-
appToken: z.ZodOptional<z.ZodString>;
|
|
623
595
|
minify: z.ZodOptional<z.ZodBoolean>;
|
|
624
|
-
env: z.ZodOptional<z.ZodObject<{
|
|
625
|
-
botToken: z.ZodOptional<z.ZodString>;
|
|
626
|
-
appToken: z.ZodOptional<z.ZodString>;
|
|
627
|
-
}, z.core.$strip>>;
|
|
628
596
|
}, z.core.$strip>, z.ZodObject<{
|
|
629
597
|
type: z.ZodLiteral<"discord">;
|
|
630
598
|
name: z.ZodString;
|
|
631
|
-
botToken: z.ZodOptional<z.ZodString>;
|
|
632
|
-
env: z.ZodOptional<z.ZodObject<{
|
|
633
|
-
botToken: z.ZodOptional<z.ZodString>;
|
|
634
|
-
}, z.core.$strip>>;
|
|
635
599
|
}, z.core.$strip>, z.ZodObject<{
|
|
636
600
|
type: z.ZodLiteral<"gh">;
|
|
637
601
|
name: z.ZodString;
|
|
@@ -651,25 +615,16 @@ declare const profileSpecSchema: z.ZodObject<{
|
|
|
651
615
|
type ProfileSpec = z.infer<typeof profileSpecSchema>;
|
|
652
616
|
declare const localConfigSchema: z.ZodObject<{
|
|
653
617
|
$schema: z.ZodOptional<z.ZodString>;
|
|
618
|
+
id: z.ZodOptional<z.ZodString>;
|
|
654
619
|
channels: z.ZodArray<z.ZodObject<{
|
|
655
620
|
name: z.ZodString;
|
|
656
621
|
connectors: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
657
622
|
type: z.ZodLiteral<"slack">;
|
|
658
623
|
name: z.ZodString;
|
|
659
|
-
botToken: z.ZodOptional<z.ZodString>;
|
|
660
|
-
appToken: z.ZodOptional<z.ZodString>;
|
|
661
624
|
minify: z.ZodOptional<z.ZodBoolean>;
|
|
662
|
-
env: z.ZodOptional<z.ZodObject<{
|
|
663
|
-
botToken: z.ZodOptional<z.ZodString>;
|
|
664
|
-
appToken: z.ZodOptional<z.ZodString>;
|
|
665
|
-
}, z.core.$strip>>;
|
|
666
625
|
}, z.core.$strip>, z.ZodObject<{
|
|
667
626
|
type: z.ZodLiteral<"discord">;
|
|
668
627
|
name: z.ZodString;
|
|
669
|
-
botToken: z.ZodOptional<z.ZodString>;
|
|
670
|
-
env: z.ZodOptional<z.ZodObject<{
|
|
671
|
-
botToken: z.ZodOptional<z.ZodString>;
|
|
672
|
-
}, z.core.$strip>>;
|
|
673
628
|
}, z.core.$strip>, z.ZodObject<{
|
|
674
629
|
type: z.ZodLiteral<"gh">;
|
|
675
630
|
name: z.ZodString;
|
|
@@ -688,10 +643,9 @@ declare const localConfigSchema: z.ZodObject<{
|
|
|
688
643
|
}, z.core.$strip>;
|
|
689
644
|
type LocalConfig = z.infer<typeof localConfigSchema>;
|
|
690
645
|
declare const LOCAL_CONFIG_FILENAME = "funnel.json";
|
|
691
|
-
declare const LOCAL_ENV_FILENAME = ".env.local";
|
|
692
646
|
//#endregion
|
|
693
647
|
//#region lib/engine/local-config/local-config.d.ts
|
|
694
|
-
type Deps$
|
|
648
|
+
type Deps$10 = {
|
|
695
649
|
fs: FunnelFileSystem;
|
|
696
650
|
};
|
|
697
651
|
/**
|
|
@@ -702,7 +656,7 @@ type Deps$9 = {
|
|
|
702
656
|
*/
|
|
703
657
|
declare class FunnelLocalConfig {
|
|
704
658
|
private readonly fs;
|
|
705
|
-
constructor(deps: Deps$
|
|
659
|
+
constructor(deps: Deps$10);
|
|
706
660
|
read(cwd: string): LocalConfig | null;
|
|
707
661
|
private assertProfilesValid;
|
|
708
662
|
}
|
|
@@ -719,11 +673,9 @@ declare abstract class FunnelTokenPrompter {
|
|
|
719
673
|
}
|
|
720
674
|
//#endregion
|
|
721
675
|
//#region lib/engine/local-config/local-config-sync.d.ts
|
|
722
|
-
type Deps$
|
|
676
|
+
type Deps$9 = {
|
|
723
677
|
channels: FunnelChannels;
|
|
724
|
-
dotenv: FunnelDotenvReader;
|
|
725
678
|
prompter: FunnelTokenPrompter;
|
|
726
|
-
env?: NodeJS.ProcessEnv;
|
|
727
679
|
};
|
|
728
680
|
type ConnectorSyncOutcome = {
|
|
729
681
|
name: string;
|
|
@@ -754,11 +706,9 @@ type LocalConfigSyncResult = {
|
|
|
754
706
|
*/
|
|
755
707
|
declare class FunnelLocalConfigSync {
|
|
756
708
|
private readonly channels;
|
|
757
|
-
private readonly dotenv;
|
|
758
709
|
private readonly prompter;
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
ensure(channel: ChannelSpec, cwd: string): Promise<LocalConfigSyncResult>;
|
|
710
|
+
constructor(deps: Deps$9);
|
|
711
|
+
ensure(channel: ChannelSpec): Promise<LocalConfigSyncResult>;
|
|
762
712
|
private ensureConnector;
|
|
763
713
|
private ensureSlack;
|
|
764
714
|
private ensureDiscord;
|
|
@@ -768,20 +718,33 @@ declare class FunnelLocalConfigSync {
|
|
|
768
718
|
private findExistingDiscord;
|
|
769
719
|
private removeExtras;
|
|
770
720
|
/**
|
|
771
|
-
* Decides how a single token slot is stored in settings.json
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
* - literal → `{ token: "<secret>" }`.
|
|
778
|
-
* - neither, but a prior value exists → carry it over verbatim (whichever
|
|
779
|
-
* form it already was), so a tokenless re-sync is a no-op.
|
|
780
|
-
* - nothing at all → prompt for a literal (TTY only; throws otherwise).
|
|
721
|
+
* Decides how a single token slot is stored in settings.json. funnel.json
|
|
722
|
+
* never carries tokens, so the only sources are a value already in
|
|
723
|
+
* settings.json (carried over verbatim, whichever form it was — literal or an
|
|
724
|
+
* `env`-var reference set via the CLI) or, on first sync, a TTY prompt for a
|
|
725
|
+
* literal (throws when stdin is not a TTY). Either way the secret lands in the
|
|
726
|
+
* repo-scoped settings, never in the repo itself.
|
|
781
727
|
*/
|
|
782
728
|
private resolveSlot;
|
|
783
729
|
}
|
|
784
730
|
//#endregion
|
|
731
|
+
//#region lib/engine/local-config/local-config-writer.d.ts
|
|
732
|
+
type Deps$8 = {
|
|
733
|
+
fs: FunnelFileSystem;
|
|
734
|
+
};
|
|
735
|
+
/**
|
|
736
|
+
* The one path that mutates the repo-committed funnel.json, and it only ever
|
|
737
|
+
* inserts `id`. On first launch a repo has no `id`; funnel generates one and
|
|
738
|
+
* writes it back here so future launches resolve the same `~/.funnel/projects/<id>/`.
|
|
739
|
+
* Idempotent — a no-op once `id` is present. Kept separate from the read-only
|
|
740
|
+
* FunnelLocalConfig so reads stay side-effect free.
|
|
741
|
+
*/
|
|
742
|
+
declare class FunnelLocalConfigWriter {
|
|
743
|
+
private readonly fs;
|
|
744
|
+
constructor(deps: Deps$8);
|
|
745
|
+
ensureId(cwd: string, id: string): void;
|
|
746
|
+
}
|
|
747
|
+
//#endregion
|
|
785
748
|
//#region lib/gateway/publish-schema.d.ts
|
|
786
749
|
/**
|
|
787
750
|
* Shared schema for `POST /channels/:channel/publish` — used by both the
|
|
@@ -1159,7 +1122,8 @@ declare class FunnelGateway {
|
|
|
1159
1122
|
type Deps$3 = {
|
|
1160
1123
|
channels: FunnelChannels;
|
|
1161
1124
|
settings: FunnelSettingsReader;
|
|
1162
|
-
port?: number; /**
|
|
1125
|
+
port?: number; /** Bind address for `Bun.serve`. Defaults to `127.0.0.1` (loopback only). Set to `0.0.0.0` to expose on the network. */
|
|
1126
|
+
hostname?: string; /** SQLite event store file path. Parent directory is created on demand. Defaults to `<os.tmpdir()>/funnel/events.db`. Ignored when `eventLog` is supplied. */
|
|
1163
1127
|
dbPath?: string; /** Durable replay log. Defaults to a `SqliteFunnelEventLog` at `dbPath`. Inject a `MemoryFunnelEventLog` (or any `FunnelEventLog`) to swap or disable persistence. */
|
|
1164
1128
|
eventLog?: FunnelEventLog;
|
|
1165
1129
|
process?: FunnelProcessRunner;
|
|
@@ -1200,6 +1164,7 @@ declare class FunnelGatewayServer {
|
|
|
1200
1164
|
private readonly channels;
|
|
1201
1165
|
private readonly settings;
|
|
1202
1166
|
private readonly port;
|
|
1167
|
+
private readonly hostname;
|
|
1203
1168
|
private readonly dbPath;
|
|
1204
1169
|
private readonly process?;
|
|
1205
1170
|
private readonly logger;
|
|
@@ -1450,8 +1415,8 @@ declare class Funnel {
|
|
|
1450
1415
|
get profiles(): FunnelProfiles;
|
|
1451
1416
|
/** Reads `funnel.json` from a cwd. `fnl claude` consults it before falling back to the default profile. */
|
|
1452
1417
|
get localConfig(): FunnelLocalConfig;
|
|
1453
|
-
/**
|
|
1454
|
-
get
|
|
1418
|
+
/** Writes the stable `id` into funnel.json on first launch so state can be scoped to `~/.funnel/projects/<id>/`. */
|
|
1419
|
+
get localConfigWriter(): FunnelLocalConfigWriter;
|
|
1455
1420
|
/** Secret prompter. Defaults to a TTY-only stdin reader; tests inject MemoryFunnelTokenPrompter. */
|
|
1456
1421
|
get tokenPrompter(): FunnelTokenPrompter;
|
|
1457
1422
|
/** Reconciles funnel.json's channel + connectors with `~/.funnel/settings.json` on launch. */
|
|
@@ -1482,7 +1447,8 @@ declare class Funnel {
|
|
|
1482
1447
|
* useful for tests, embedding, or custom hosts.
|
|
1483
1448
|
*/
|
|
1484
1449
|
gatewayServer(options?: {
|
|
1485
|
-
port?: number;
|
|
1450
|
+
port?: number; /** Bind address. Defaults to `127.0.0.1` (loopback only). Set to `0.0.0.0` to expose on the network. */
|
|
1451
|
+
hostname?: string;
|
|
1486
1452
|
dbPath?: string;
|
|
1487
1453
|
killCompetingSlack?: boolean; /** Override the auth token. Defaults to the persisted gateway.token. Pass "" to disable auth (tests). */
|
|
1488
1454
|
token?: string; /** Durable replay log. Defaults to a SqliteFunnelEventLog at dbPath; inject a MemoryFunnelEventLog (or any FunnelEventLog) to swap or disable persistence. */
|
|
@@ -1843,6 +1809,7 @@ declare class MemoryFunnelEventLog extends FunnelEventLog {
|
|
|
1843
1809
|
record(record: FunnelEventRecord): void;
|
|
1844
1810
|
loadSince(since: number): ReplayableEvent[];
|
|
1845
1811
|
findMaxOffset(): number;
|
|
1812
|
+
clear(): void;
|
|
1846
1813
|
close(): void;
|
|
1847
1814
|
}
|
|
1848
1815
|
//#endregion
|
|
@@ -1917,6 +1884,7 @@ declare class MemoryConnectorDiagnosticLog extends ConnectorDiagnosticLog {
|
|
|
1917
1884
|
queryRaw(query: ConnectorRawQuery): StoredRawEvent[];
|
|
1918
1885
|
queryProcessed(query: ConnectorProcessedQuery): StoredProcessedEvent[];
|
|
1919
1886
|
queryConnection(query: ConnectorConnectionQuery): StoredConnectionEvent[];
|
|
1887
|
+
clear(): void;
|
|
1920
1888
|
close(): void;
|
|
1921
1889
|
}
|
|
1922
1890
|
//#endregion
|
|
@@ -4613,4 +4581,4 @@ ${string}`;
|
|
|
4613
4581
|
//#region lib/tui/tui.d.ts
|
|
4614
4582
|
declare function launchTui(funnel: Funnel): Promise<void>;
|
|
4615
4583
|
//#endregion
|
|
4616
|
-
export { AliveStub, AttachOptions, BroadcastEvent, BroadcastSubscriber, CONNECTOR_CONNECTION_STATUSES, ChannelConfig, ChannelConnectorView, ChannelDeliveryMode, ChannelServerOptions, ChannelSpec, ConnectorConfig, ConnectorConnectionEvent, ConnectorConnectionQuery, ConnectorConnectionRecord, ConnectorConnectionStatus, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, ConnectorProcessedEvent, ConnectorProcessedQuery, ConnectorProcessedRecord, ConnectorQuery, ConnectorRawEvent, ConnectorRawQuery, ConnectorRawRecord, ConnectorSpec, ConnectorSyncOutcome, ConnectorType, DEFAULT_GATEWAY_TOKEN_PATH, DetachOptions, DiscordConnectorConfig, Env, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileStat, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener,
|
|
4584
|
+
export { AliveStub, AttachOptions, BroadcastEvent, BroadcastSubscriber, CONNECTOR_CONNECTION_STATUSES, ChannelConfig, ChannelConnectorView, ChannelDeliveryMode, ChannelServerOptions, ChannelSpec, ConnectorConfig, ConnectorConnectionEvent, ConnectorConnectionQuery, ConnectorConnectionRecord, ConnectorConnectionStatus, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, ConnectorProcessedEvent, ConnectorProcessedQuery, ConnectorProcessedRecord, ConnectorQuery, ConnectorRawEvent, ConnectorRawQuery, ConnectorRawRecord, ConnectorSpec, ConnectorSyncOutcome, ConnectorType, DEFAULT_GATEWAY_TOKEN_PATH, DetachOptions, DiscordConnectorConfig, Env, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileStat, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelEvent, FunnelEventLog, FunnelEventRecord, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, type GatewayEmitInput, type GatewayRouteDeps, type Env$1 as GatewayServerEnv, GhConnectorConfig, LOCAL_CONFIG_FILENAME, LaunchOptions, ListListenersResult, ListenerEntry, ListenerOpResult, LocalConfig, LocalConfigSyncResult, LogEntry, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MemoryProcessCall, MemoryProcessHandler, MemoryProcessResponse, MemoryProcessSyncHandler, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, NotifyFn, OnFunnelError, ProcessListStub, ProcessSnapshot, ProfileConfig, ProfileSpec, PublishRequest, PublishResponse, PublishResult, ReplayableEvent, RunOptions, RunResult, SETTINGS_PATH, SETTINGS_VERSION, ScheduleCatchupPolicy, ScheduleConnectorConfig, ScheduleEntry, ScheduleListenerOptions, Settings, SlackConnectorConfig, SlackListenerOptions, SlackProcessed, SlackProcessedEmit, SlackProcessedSkip, SlackRawEvent, SlackSkipReason, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, StoredConnectionEvent, StoredProcessedEvent, StoredRawEvent, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, profileSpecSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, resolveFunnelDir, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
|
package/dist/index.js
CHANGED
|
@@ -187,7 +187,7 @@ var FunnelSettingsStore = class extends FunnelSettingsReader {
|
|
|
187
187
|
...settings,
|
|
188
188
|
version: 1
|
|
189
189
|
};
|
|
190
|
-
this.fs.
|
|
190
|
+
this.fs.writeSecretFileSync(this.path, `${JSON.stringify(versioned, null, 2)}\n`);
|
|
191
191
|
}
|
|
192
192
|
};
|
|
193
193
|
//#endregion
|
|
@@ -887,8 +887,9 @@ var MemoryFunnelIdGenerator = class extends FunnelIdGenerator {
|
|
|
887
887
|
* `fnl claude` reads this when no global --profile preset is used. It picks one
|
|
888
888
|
* of the declared channels (`--channel <name>` selects by name; otherwise the
|
|
889
889
|
* first entry wins) and materializes its transport (connectors / delivery) into
|
|
890
|
-
* `~/.funnel/settings.json` on launch
|
|
891
|
-
*
|
|
890
|
+
* the repo's scoped settings (`~/.funnel/projects/<id>/settings.json`) on launch.
|
|
891
|
+
* Connectors carry no tokens here — a token absent from settings is prompted for
|
|
892
|
+
* at launch (TTY) and saved there, never in the repo.
|
|
892
893
|
*
|
|
893
894
|
* The launch recipe (`options` / `env` / `resume`) lives on `profiles[]`, not on
|
|
894
895
|
* the channel: a channel only describes where events come from. `fnl claude`
|
|
@@ -896,25 +897,15 @@ var MemoryFunnelIdGenerator = class extends FunnelIdGenerator {
|
|
|
896
897
|
* straight to the launcher and is not persisted into the global profile list.
|
|
897
898
|
* These profiles are selected by their `channel` binding, not by name.
|
|
898
899
|
*/
|
|
899
|
-
const slackEnvSchema = z.object({
|
|
900
|
-
botToken: z.string().optional(),
|
|
901
|
-
appToken: z.string().optional()
|
|
902
|
-
}).optional();
|
|
903
900
|
const slackConnectorSpecSchema = z.object({
|
|
904
901
|
type: z.literal("slack"),
|
|
905
902
|
name: z.string(),
|
|
906
|
-
botToken: z.string().optional(),
|
|
907
|
-
appToken: z.string().optional(),
|
|
908
903
|
/** Shrink raw Slack events before fanout. Defaults to true. */
|
|
909
|
-
minify: z.boolean().optional()
|
|
910
|
-
env: slackEnvSchema
|
|
904
|
+
minify: z.boolean().optional()
|
|
911
905
|
});
|
|
912
|
-
const discordEnvSchema = z.object({ botToken: z.string().optional() }).optional();
|
|
913
906
|
const discordConnectorSpecSchema = z.object({
|
|
914
907
|
type: z.literal("discord"),
|
|
915
|
-
name: z.string()
|
|
916
|
-
botToken: z.string().optional(),
|
|
917
|
-
env: discordEnvSchema
|
|
908
|
+
name: z.string()
|
|
918
909
|
});
|
|
919
910
|
const ghConnectorSpecSchema = z.object({
|
|
920
911
|
type: z.literal("gh"),
|
|
@@ -952,54 +943,18 @@ const profileSpecSchema = z.object({
|
|
|
952
943
|
});
|
|
953
944
|
const localConfigSchema = z.object({
|
|
954
945
|
$schema: z.string().optional(),
|
|
946
|
+
/**
|
|
947
|
+
* Stable per-repo identifier. funnel writes this on first launch when absent;
|
|
948
|
+
* all funnel state for this repo lives under `~/.funnel/projects/<id>/`, so the
|
|
949
|
+
* repo itself never holds settings or tokens. Committed alongside funnel.json.
|
|
950
|
+
*/
|
|
951
|
+
id: z.string().optional(),
|
|
955
952
|
/** Declared channels (transport only). First entry is the default; --channel <name> selects by name. */
|
|
956
953
|
channels: z.array(channelSpecSchema).min(1),
|
|
957
954
|
/** Launch presets bound to a channel. First entry bound to the chosen channel is the default. */
|
|
958
955
|
profiles: z.array(profileSpecSchema).optional()
|
|
959
956
|
});
|
|
960
957
|
const LOCAL_CONFIG_FILENAME = "funnel.json";
|
|
961
|
-
const LOCAL_ENV_FILENAME = ".env.local";
|
|
962
|
-
//#endregion
|
|
963
|
-
//#region lib/engine/local-config/dotenv-reader.ts
|
|
964
|
-
const VARIABLE_LINE = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/;
|
|
965
|
-
const unquote = (value) => {
|
|
966
|
-
if (value.length < 2) return value;
|
|
967
|
-
const first = value[0];
|
|
968
|
-
const last = value[value.length - 1];
|
|
969
|
-
if (first === "\"" && last === "\"") return value.slice(1, -1);
|
|
970
|
-
if (first === "'" && last === "'") return value.slice(1, -1);
|
|
971
|
-
return value;
|
|
972
|
-
};
|
|
973
|
-
/**
|
|
974
|
-
* Minimal `.env.local` parser. Supports `KEY=value` lines, blank lines, and
|
|
975
|
-
* `#` comments. Strips matching surrounding single or double quotes. No
|
|
976
|
-
* interpolation, no `export` prefix — anything fancier should live in a real
|
|
977
|
-
* env file loaded by the shell.
|
|
978
|
-
*/
|
|
979
|
-
var FunnelDotenvReader = class {
|
|
980
|
-
fs;
|
|
981
|
-
constructor(deps) {
|
|
982
|
-
this.fs = deps.fs;
|
|
983
|
-
Object.freeze(this);
|
|
984
|
-
}
|
|
985
|
-
read(cwd) {
|
|
986
|
-
const path = join(cwd, LOCAL_ENV_FILENAME);
|
|
987
|
-
if (!this.fs.existsSync(path)) return {};
|
|
988
|
-
const raw = this.fs.readFileSync(path);
|
|
989
|
-
const out = {};
|
|
990
|
-
for (const line of raw.split("\n")) {
|
|
991
|
-
const trimmed = line.trim();
|
|
992
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
993
|
-
const match = trimmed.match(VARIABLE_LINE);
|
|
994
|
-
if (!match) continue;
|
|
995
|
-
const key = match[1];
|
|
996
|
-
const value = match[2];
|
|
997
|
-
if (!key) continue;
|
|
998
|
-
out[key] = unquote(value ?? "");
|
|
999
|
-
}
|
|
1000
|
-
return out;
|
|
1001
|
-
}
|
|
1002
|
-
};
|
|
1003
958
|
//#endregion
|
|
1004
959
|
//#region lib/engine/local-config/local-config.ts
|
|
1005
960
|
/**
|
|
@@ -1075,27 +1030,22 @@ var FunnelTokenPrompter = class {};
|
|
|
1075
1030
|
*/
|
|
1076
1031
|
var FunnelLocalConfigSync = class {
|
|
1077
1032
|
channels;
|
|
1078
|
-
dotenv;
|
|
1079
1033
|
prompter;
|
|
1080
|
-
env;
|
|
1081
1034
|
constructor(deps) {
|
|
1082
1035
|
this.channels = deps.channels;
|
|
1083
|
-
this.dotenv = deps.dotenv;
|
|
1084
1036
|
this.prompter = deps.prompter;
|
|
1085
|
-
this.env = deps.env ?? process.env;
|
|
1086
1037
|
Object.freeze(this);
|
|
1087
1038
|
}
|
|
1088
|
-
async ensure(channel
|
|
1039
|
+
async ensure(channel) {
|
|
1089
1040
|
if (!this.channels.get(channel.name)) this.channels.add({ name: channel.name });
|
|
1090
1041
|
if (channel.connectors === void 0) return {
|
|
1091
1042
|
touched: [],
|
|
1092
1043
|
removed: []
|
|
1093
1044
|
};
|
|
1094
|
-
const dotenv = this.dotenv.read(cwd);
|
|
1095
1045
|
const touched = [];
|
|
1096
1046
|
const touchedIds = /* @__PURE__ */ new Set();
|
|
1097
1047
|
for (const spec of channel.connectors) {
|
|
1098
|
-
const outcome = await this.ensureConnector(channel.name, spec
|
|
1048
|
+
const outcome = await this.ensureConnector(channel.name, spec);
|
|
1099
1049
|
touched.push({
|
|
1100
1050
|
name: outcome.name,
|
|
1101
1051
|
changed: outcome.changed
|
|
@@ -1107,26 +1057,20 @@ var FunnelLocalConfigSync = class {
|
|
|
1107
1057
|
removed: this.removeExtras(channel.name, touchedIds)
|
|
1108
1058
|
};
|
|
1109
1059
|
}
|
|
1110
|
-
async ensureConnector(channelName, spec
|
|
1111
|
-
if (spec.type === "slack") return await this.ensureSlack(channelName, spec
|
|
1112
|
-
if (spec.type === "discord") return await this.ensureDiscord(channelName, spec
|
|
1060
|
+
async ensureConnector(channelName, spec) {
|
|
1061
|
+
if (spec.type === "slack") return await this.ensureSlack(channelName, spec);
|
|
1062
|
+
if (spec.type === "discord") return await this.ensureDiscord(channelName, spec);
|
|
1113
1063
|
if (spec.type === "gh") return this.ensureGh(channelName, spec);
|
|
1114
1064
|
return this.ensureSchedule(channelName, spec);
|
|
1115
1065
|
}
|
|
1116
|
-
async ensureSlack(channelName, spec
|
|
1066
|
+
async ensureSlack(channelName, spec) {
|
|
1117
1067
|
const byName = this.findExistingSlack(channelName, spec.name);
|
|
1118
1068
|
const bot = await this.resolveSlot({
|
|
1119
|
-
literal: spec.botToken,
|
|
1120
|
-
envVar: spec.env?.botToken,
|
|
1121
|
-
dotenv,
|
|
1122
1069
|
label: `${spec.name}.botToken`,
|
|
1123
1070
|
existingLiteral: byName?.botToken,
|
|
1124
1071
|
existingEnv: byName?.botTokenEnv
|
|
1125
1072
|
});
|
|
1126
1073
|
const app = await this.resolveSlot({
|
|
1127
|
-
literal: spec.appToken,
|
|
1128
|
-
envVar: spec.env?.appToken,
|
|
1129
|
-
dotenv,
|
|
1130
1074
|
label: `${spec.name}.appToken`,
|
|
1131
1075
|
existingLiteral: byName?.appToken,
|
|
1132
1076
|
existingEnv: byName?.appTokenEnv
|
|
@@ -1163,12 +1107,9 @@ var FunnelLocalConfigSync = class {
|
|
|
1163
1107
|
changed: true
|
|
1164
1108
|
};
|
|
1165
1109
|
}
|
|
1166
|
-
async ensureDiscord(channelName, spec
|
|
1110
|
+
async ensureDiscord(channelName, spec) {
|
|
1167
1111
|
const byName = this.findExistingDiscord(channelName, spec.name);
|
|
1168
1112
|
const bot = await this.resolveSlot({
|
|
1169
|
-
literal: spec.botToken,
|
|
1170
|
-
envVar: spec.env?.botToken,
|
|
1171
|
-
dotenv,
|
|
1172
1113
|
label: `${spec.name}.botToken`,
|
|
1173
1114
|
existingLiteral: byName?.botToken,
|
|
1174
1115
|
existingEnv: byName?.botTokenEnv
|
|
@@ -1267,30 +1208,14 @@ var FunnelLocalConfigSync = class {
|
|
|
1267
1208
|
return stale.map((c) => c.name);
|
|
1268
1209
|
}
|
|
1269
1210
|
/**
|
|
1270
|
-
* Decides how a single token slot is stored in settings.json
|
|
1271
|
-
*
|
|
1272
|
-
*
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
* - literal → `{ token: "<secret>" }`.
|
|
1277
|
-
* - neither, but a prior value exists → carry it over verbatim (whichever
|
|
1278
|
-
* form it already was), so a tokenless re-sync is a no-op.
|
|
1279
|
-
* - nothing at all → prompt for a literal (TTY only; throws otherwise).
|
|
1211
|
+
* Decides how a single token slot is stored in settings.json. funnel.json
|
|
1212
|
+
* never carries tokens, so the only sources are a value already in
|
|
1213
|
+
* settings.json (carried over verbatim, whichever form it was — literal or an
|
|
1214
|
+
* `env`-var reference set via the CLI) or, on first sync, a TTY prompt for a
|
|
1215
|
+
* literal (throws when stdin is not a TTY). Either way the secret lands in the
|
|
1216
|
+
* repo-scoped settings, never in the repo itself.
|
|
1280
1217
|
*/
|
|
1281
1218
|
async resolveSlot(input) {
|
|
1282
|
-
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`);
|
|
1283
|
-
if (input.envVar !== void 0 && input.envVar !== "") {
|
|
1284
|
-
if (!this.env[input.envVar] && !input.dotenv[input.envVar]) throw new Error(`${input.label} references env var "${input.envVar}" but it is not set in process env or .env.local`);
|
|
1285
|
-
return {
|
|
1286
|
-
token: void 0,
|
|
1287
|
-
tokenEnv: input.envVar
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1290
|
-
if (input.literal !== void 0 && input.literal !== "") return {
|
|
1291
|
-
token: input.literal,
|
|
1292
|
-
tokenEnv: void 0
|
|
1293
|
-
};
|
|
1294
1219
|
if (input.existingEnv !== void 0) return {
|
|
1295
1220
|
token: void 0,
|
|
1296
1221
|
tokenEnv: input.existingEnv
|
|
@@ -1306,6 +1231,44 @@ var FunnelLocalConfigSync = class {
|
|
|
1306
1231
|
}
|
|
1307
1232
|
};
|
|
1308
1233
|
//#endregion
|
|
1234
|
+
//#region lib/engine/local-config/local-config-writer.ts
|
|
1235
|
+
const isRecord = (value) => {
|
|
1236
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1237
|
+
};
|
|
1238
|
+
const withIdFirst = (config, id) => {
|
|
1239
|
+
const ordered = {};
|
|
1240
|
+
if (config.$schema !== void 0) ordered.$schema = config.$schema;
|
|
1241
|
+
ordered.id = id;
|
|
1242
|
+
for (const key of Object.keys(config)) {
|
|
1243
|
+
if (key === "$schema" || key === "id") continue;
|
|
1244
|
+
ordered[key] = config[key];
|
|
1245
|
+
}
|
|
1246
|
+
return ordered;
|
|
1247
|
+
};
|
|
1248
|
+
/**
|
|
1249
|
+
* The one path that mutates the repo-committed funnel.json, and it only ever
|
|
1250
|
+
* inserts `id`. On first launch a repo has no `id`; funnel generates one and
|
|
1251
|
+
* writes it back here so future launches resolve the same `~/.funnel/projects/<id>/`.
|
|
1252
|
+
* Idempotent — a no-op once `id` is present. Kept separate from the read-only
|
|
1253
|
+
* FunnelLocalConfig so reads stay side-effect free.
|
|
1254
|
+
*/
|
|
1255
|
+
var FunnelLocalConfigWriter = class {
|
|
1256
|
+
fs;
|
|
1257
|
+
constructor(deps) {
|
|
1258
|
+
this.fs = deps.fs;
|
|
1259
|
+
Object.freeze(this);
|
|
1260
|
+
}
|
|
1261
|
+
ensureId(cwd, id) {
|
|
1262
|
+
const path = join(cwd, LOCAL_CONFIG_FILENAME);
|
|
1263
|
+
if (!this.fs.existsSync(path)) return;
|
|
1264
|
+
const parsed = JSON.parse(this.fs.readFileSync(path));
|
|
1265
|
+
if (!isRecord(parsed)) return;
|
|
1266
|
+
if (typeof parsed.id === "string" && parsed.id !== "") return;
|
|
1267
|
+
const ordered = withIdFirst(parsed, id);
|
|
1268
|
+
this.fs.writeFileSync(path, `${JSON.stringify(ordered, null, 2)}\n`);
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
//#endregion
|
|
1309
1272
|
//#region lib/engine/logger/memory-logger.ts
|
|
1310
1273
|
var MemoryFunnelLogger = class extends FunnelLogger {
|
|
1311
1274
|
file = null;
|
|
@@ -3084,6 +3047,13 @@ const gatewayRoutes = factory$1.createApp().get("/health", ...healthHandler).get
|
|
|
3084
3047
|
//#endregion
|
|
3085
3048
|
//#region lib/gateway/gateway-server.ts
|
|
3086
3049
|
const DEFAULT_PORT = 9742;
|
|
3050
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
3051
|
+
const LOOPBACK_HOSTS = new Set([
|
|
3052
|
+
"127.0.0.1",
|
|
3053
|
+
"localhost",
|
|
3054
|
+
"::1",
|
|
3055
|
+
"::ffff:127.0.0.1"
|
|
3056
|
+
]);
|
|
3087
3057
|
const defaultDbPath = () => join(funnelTmpDir(), "events.db");
|
|
3088
3058
|
const defaultOnError = () => {};
|
|
3089
3059
|
/**
|
|
@@ -3100,6 +3070,7 @@ var FunnelGatewayServer = class {
|
|
|
3100
3070
|
channels;
|
|
3101
3071
|
settings;
|
|
3102
3072
|
port;
|
|
3073
|
+
hostname;
|
|
3103
3074
|
dbPath;
|
|
3104
3075
|
process;
|
|
3105
3076
|
logger;
|
|
@@ -3119,6 +3090,7 @@ var FunnelGatewayServer = class {
|
|
|
3119
3090
|
this.channels = deps.channels;
|
|
3120
3091
|
this.settings = deps.settings;
|
|
3121
3092
|
this.port = deps.port ?? DEFAULT_PORT;
|
|
3093
|
+
this.hostname = deps.hostname ?? DEFAULT_HOST;
|
|
3122
3094
|
this.dbPath = deps.dbPath ?? defaultDbPath();
|
|
3123
3095
|
this.process = deps.process;
|
|
3124
3096
|
this.logger = deps.logger;
|
|
@@ -3163,10 +3135,12 @@ var FunnelGatewayServer = class {
|
|
|
3163
3135
|
}
|
|
3164
3136
|
async start() {
|
|
3165
3137
|
if (this.server) return this.server;
|
|
3138
|
+
if (!this.token && !LOOPBACK_HOSTS.has(this.hostname)) this.logger?.warn("gateway auth is disabled on a non-loopback bind — every endpoint is reachable without a token", { hostname: this.hostname });
|
|
3166
3139
|
const app = this.buildApp();
|
|
3167
3140
|
this.startedAt = this.nowMs();
|
|
3168
3141
|
this.server = Bun.serve({
|
|
3169
3142
|
port: this.port,
|
|
3143
|
+
hostname: this.hostname,
|
|
3170
3144
|
development: false,
|
|
3171
3145
|
fetch: (request, server) => this.handleFetch(request, server, app),
|
|
3172
3146
|
websocket: {
|
|
@@ -3670,10 +3644,10 @@ var Funnel = class Funnel {
|
|
|
3670
3644
|
if (!this.memos.localConfig) this.memos.localConfig = new FunnelLocalConfig({ fs: this.fs });
|
|
3671
3645
|
return this.memos.localConfig;
|
|
3672
3646
|
}
|
|
3673
|
-
/**
|
|
3674
|
-
get
|
|
3675
|
-
if (!this.memos.
|
|
3676
|
-
return this.memos.
|
|
3647
|
+
/** Writes the stable `id` into funnel.json on first launch so state can be scoped to `~/.funnel/projects/<id>/`. */
|
|
3648
|
+
get localConfigWriter() {
|
|
3649
|
+
if (!this.memos.localConfigWriter) this.memos.localConfigWriter = new FunnelLocalConfigWriter({ fs: this.fs });
|
|
3650
|
+
return this.memos.localConfigWriter;
|
|
3677
3651
|
}
|
|
3678
3652
|
/** Secret prompter. Defaults to a TTY-only stdin reader; tests inject MemoryFunnelTokenPrompter. */
|
|
3679
3653
|
get tokenPrompter() {
|
|
@@ -3684,7 +3658,6 @@ var Funnel = class Funnel {
|
|
|
3684
3658
|
get localConfigSync() {
|
|
3685
3659
|
if (!this.memos.localConfigSync) this.memos.localConfigSync = new FunnelLocalConfigSync({
|
|
3686
3660
|
channels: this.channels,
|
|
3687
|
-
dotenv: this.dotenv,
|
|
3688
3661
|
prompter: this.tokenPrompter
|
|
3689
3662
|
});
|
|
3690
3663
|
return this.memos.localConfigSync;
|
|
@@ -3772,6 +3745,7 @@ var Funnel = class Funnel {
|
|
|
3772
3745
|
channels: this.channels,
|
|
3773
3746
|
settings: this.store,
|
|
3774
3747
|
port: options.port,
|
|
3748
|
+
hostname: options.hostname,
|
|
3775
3749
|
dbPath: options.dbPath,
|
|
3776
3750
|
eventLog: options.eventLog,
|
|
3777
3751
|
process: this.process,
|
|
@@ -4080,6 +4054,9 @@ var MemoryFunnelEventLog = class extends FunnelEventLog {
|
|
|
4080
4054
|
for (const event of this.events) if (event.offset > max) max = event.offset;
|
|
4081
4055
|
return max;
|
|
4082
4056
|
}
|
|
4057
|
+
clear() {
|
|
4058
|
+
this.events.length = 0;
|
|
4059
|
+
}
|
|
4083
4060
|
close() {}
|
|
4084
4061
|
};
|
|
4085
4062
|
//#endregion
|
|
@@ -4488,6 +4465,11 @@ var MemoryConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
4488
4465
|
return true;
|
|
4489
4466
|
}), query.limit);
|
|
4490
4467
|
}
|
|
4468
|
+
clear() {
|
|
4469
|
+
this.raws.length = 0;
|
|
4470
|
+
this.processeds.length = 0;
|
|
4471
|
+
this.connections.length = 0;
|
|
4472
|
+
}
|
|
4491
4473
|
close() {}
|
|
4492
4474
|
};
|
|
4493
4475
|
const matches$1 = (event, query) => {
|
|
@@ -5132,7 +5114,7 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
|
5132
5114
|
if (local) {
|
|
5133
5115
|
const picked = query.channel !== void 0 ? local.channels.find((c) => c.name === query.channel) : local.channels[0];
|
|
5134
5116
|
if (!picked) throw new HTTPException(404, { message: query.channel ? `channel "${query.channel}" is not declared in funnel.json` : `funnel.json declares no channels` });
|
|
5135
|
-
const synced = await funnel.localConfigSync.ensure(picked
|
|
5117
|
+
const synced = await funnel.localConfigSync.ensure(picked);
|
|
5136
5118
|
for (const outcome of synced.touched) if (outcome.changed) await funnel.listeners.restart(picked.name, outcome.name);
|
|
5137
5119
|
else await funnel.listeners.start(picked.name, outcome.name);
|
|
5138
5120
|
for (const name of synced.removed) await funnel.listeners.stop(picked.name, name);
|
|
@@ -7972,4 +7954,4 @@ async function launchTui(funnel) {
|
|
|
7972
7954
|
});
|
|
7973
7955
|
}
|
|
7974
7956
|
//#endregion
|
|
7975
|
-
export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener,
|
|
7957
|
+
export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, profileSpecSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, resolveFunnelDir, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
|