@interactive-inc/claude-funnel 0.40.0 → 0.49.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 +34 -9
- package/dist/bin.js +255 -256
- package/dist/gateway/daemon.js +151 -152
- package/dist/index.d.ts +160 -286
- package/dist/index.js +520 -981
- package/package.json +16 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { a as ScheduleStateStore, i as FunnelScheduleListener, n as scheduleConn
|
|
|
5
5
|
import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-DDbSGPZn.js";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { hc } from "hono/client";
|
|
8
|
-
import { appendFileSync, chmodSync, existsSync, mkdirSync
|
|
8
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { homedir, tmpdir } from "node:os";
|
|
11
11
|
import { stderr, stdin } from "node:process";
|
|
@@ -15,9 +15,6 @@ import { createFactory } from "hono/factory";
|
|
|
15
15
|
import { Database } from "bun:sqlite";
|
|
16
16
|
import { HTTPException } from "hono/http-exception";
|
|
17
17
|
import { zValidator } from "@hono/zod-validator";
|
|
18
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
19
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
21
18
|
//#region lib/engine/id/id-generator.ts
|
|
22
19
|
/**
|
|
23
20
|
* ID generator boundary. Default NodeFunnelIdGenerator wraps `crypto.randomUUID()`;
|
|
@@ -125,7 +122,7 @@ function resolveFunnelPort() {
|
|
|
125
122
|
}
|
|
126
123
|
const FUNNEL_DIR = join(homedir(), ".funnel");
|
|
127
124
|
const SETTINGS_PATH = join(FUNNEL_DIR, "settings.json");
|
|
128
|
-
const defaultFs$
|
|
125
|
+
const defaultFs$6 = new NodeFunnelFileSystem();
|
|
129
126
|
const defaultIdGenerator$2 = new NodeFunnelIdGenerator();
|
|
130
127
|
var FunnelSettingsStore = class extends FunnelSettingsReader {
|
|
131
128
|
path;
|
|
@@ -134,7 +131,7 @@ var FunnelSettingsStore = class extends FunnelSettingsReader {
|
|
|
134
131
|
constructor(deps = {}) {
|
|
135
132
|
super();
|
|
136
133
|
this.path = deps.path ?? SETTINGS_PATH;
|
|
137
|
-
this.fs = deps.fs ?? defaultFs$
|
|
134
|
+
this.fs = deps.fs ?? defaultFs$6;
|
|
138
135
|
this.idGenerator = deps.idGenerator ?? defaultIdGenerator$2;
|
|
139
136
|
Object.freeze(this);
|
|
140
137
|
}
|
|
@@ -205,8 +202,8 @@ var FunnelSettingsStore = class extends FunnelSettingsReader {
|
|
|
205
202
|
};
|
|
206
203
|
//#endregion
|
|
207
204
|
//#region lib/connectors/connector-factory.ts
|
|
208
|
-
const defaultFs$
|
|
209
|
-
const defaultProcess$
|
|
205
|
+
const defaultFs$5 = new NodeFunnelFileSystem();
|
|
206
|
+
const defaultProcess$4 = new NodeFunnelProcessRunner();
|
|
210
207
|
/**
|
|
211
208
|
* Pure factory for per-type listeners and adapters. The factory has no CRUD
|
|
212
209
|
* responsibility — connector configs live inside settings.json under their
|
|
@@ -228,8 +225,8 @@ var FunnelConnectorFactory = class {
|
|
|
228
225
|
slackListenerOptions;
|
|
229
226
|
scheduleListenerOptions;
|
|
230
227
|
constructor(deps = {}) {
|
|
231
|
-
this.fs = deps.fs ?? defaultFs$
|
|
232
|
-
this.process = deps.process ?? defaultProcess$
|
|
228
|
+
this.fs = deps.fs ?? defaultFs$5;
|
|
229
|
+
this.process = deps.process ?? defaultProcess$4;
|
|
233
230
|
this.logger = deps.logger;
|
|
234
231
|
this.diagnosticLog = deps.diagnosticLog;
|
|
235
232
|
this.dir = deps.dir ?? FUNNEL_DIR;
|
|
@@ -382,7 +379,7 @@ var FunnelChannels = class {
|
|
|
382
379
|
constructor(deps) {
|
|
383
380
|
this.store = deps.store;
|
|
384
381
|
this.factory = deps.factory;
|
|
385
|
-
this.profileChecker = deps.profileChecker;
|
|
382
|
+
this.profileChecker = deps.profileChecker ?? null;
|
|
386
383
|
this.clock = deps.clock ?? defaultClock$1;
|
|
387
384
|
this.idGenerator = deps.idGenerator ?? defaultIdGenerator$1;
|
|
388
385
|
Object.freeze(this);
|
|
@@ -420,7 +417,7 @@ var FunnelChannels = class {
|
|
|
420
417
|
const index = settings.channels.findIndex((c) => c.name === name);
|
|
421
418
|
if (index < 0) throw new Error(`channel "${name}" not found`);
|
|
422
419
|
const channel = settings.channels[index];
|
|
423
|
-
if (channel && this.profileChecker
|
|
420
|
+
if (channel && this.profileChecker?.hasChannelRef(channel.id)) throw new Error(`channel "${name}" is referenced by a profile`);
|
|
424
421
|
settings.channels.splice(index, 1);
|
|
425
422
|
this.store.write(settings);
|
|
426
423
|
}
|
|
@@ -639,41 +636,38 @@ var FunnelChannels = class {
|
|
|
639
636
|
};
|
|
640
637
|
//#endregion
|
|
641
638
|
//#region lib/engine/claude/claude.ts
|
|
642
|
-
const defaultProcess$
|
|
643
|
-
const defaultFs$3 = new NodeFunnelFileSystem();
|
|
639
|
+
const defaultProcess$3 = new NodeFunnelProcessRunner();
|
|
644
640
|
const defaultIdGenerator = new NodeFunnelIdGenerator();
|
|
645
641
|
/**
|
|
646
642
|
* Launches Claude Code with funnel pre-wired: ensures the gateway is running,
|
|
647
643
|
* installs the funnel MCP into the target repo's `.mcp.json` if missing,
|
|
648
|
-
* injects `FUNNEL_CHANNEL_ID` into the child env, and
|
|
649
|
-
*
|
|
644
|
+
* injects `FUNNEL_CHANNEL_ID` into the child env, and delegates singleton
|
|
645
|
+
* enforcement to a ProcessGuard.
|
|
650
646
|
*/
|
|
651
647
|
var FunnelClaude = class {
|
|
652
648
|
channels;
|
|
653
649
|
mcp;
|
|
654
650
|
gateway;
|
|
655
|
-
|
|
651
|
+
sessions;
|
|
652
|
+
guard;
|
|
656
653
|
process;
|
|
657
|
-
fs;
|
|
658
654
|
idGenerator;
|
|
659
655
|
logger;
|
|
660
|
-
pidDir;
|
|
661
656
|
constructor(deps) {
|
|
662
657
|
this.channels = deps.channels;
|
|
663
658
|
this.mcp = deps.mcp;
|
|
664
659
|
this.gateway = deps.gateway;
|
|
665
|
-
this.
|
|
666
|
-
this.
|
|
667
|
-
this.
|
|
660
|
+
this.sessions = deps.sessions;
|
|
661
|
+
this.guard = deps.guard;
|
|
662
|
+
this.process = deps.process ?? defaultProcess$3;
|
|
668
663
|
this.idGenerator = deps.idGenerator ?? defaultIdGenerator;
|
|
669
664
|
this.logger = deps.logger;
|
|
670
|
-
this.pidDir = join(deps.dir ?? FUNNEL_DIR, "claude");
|
|
671
665
|
Object.freeze(this);
|
|
672
666
|
}
|
|
673
667
|
async launch(options) {
|
|
674
668
|
const channel = this.channels.get(options.channel) ?? this.channels.getById(options.channel);
|
|
675
669
|
if (!channel) throw new Error(`channel "${options.channel}" not found`);
|
|
676
|
-
if (options.profileId && this.isRunning(options.profileId)) throw new Error(`profile "${options.profileId}" is already running`);
|
|
670
|
+
if (options.profileId && this.guard.isRunning(options.profileId)) throw new Error(`profile "${options.profileId}" is already running`);
|
|
677
671
|
const cwd = options.cwd ?? globalThis.process.cwd();
|
|
678
672
|
if ((options.installMcp ?? true) && !this.mcp.findInstalledName(cwd)) {
|
|
679
673
|
this.mcp.install(cwd);
|
|
@@ -683,10 +677,7 @@ var FunnelClaude = class {
|
|
|
683
677
|
this.logger?.info(`starting gateway automatically`);
|
|
684
678
|
await this.gateway.start();
|
|
685
679
|
}
|
|
686
|
-
if (options.profileId)
|
|
687
|
-
this.writePidFile(options.profileId);
|
|
688
|
-
this.installCleanup(options.profileId);
|
|
689
|
-
}
|
|
680
|
+
if (options.profileId) this.guard.acquire(options.profileId);
|
|
690
681
|
const session = (options.resume ?? false) && options.profileId ? this.resolveSession(options.profileId, cwd, options.userArgs ?? [], options.env ?? {}) : null;
|
|
691
682
|
const claudeArgs = this.buildArgs(options.options ?? [], options.userArgs ?? [], cwd, session);
|
|
692
683
|
const env = this.buildEnv(channel.id, options.env ?? {});
|
|
@@ -702,43 +693,9 @@ var FunnelClaude = class {
|
|
|
702
693
|
onSpawned: options.onSpawned
|
|
703
694
|
});
|
|
704
695
|
} finally {
|
|
705
|
-
if (options.profileId) this.
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
isRunning(profileId) {
|
|
709
|
-
const pid = this.readPid(profileId);
|
|
710
|
-
if (!pid) return false;
|
|
711
|
-
return this.isProcessAlive(pid);
|
|
712
|
-
}
|
|
713
|
-
pidPath(profileId) {
|
|
714
|
-
return join(this.pidDir, `${profileId}.pid`);
|
|
715
|
-
}
|
|
716
|
-
readPid(profileId) {
|
|
717
|
-
const path = this.pidPath(profileId);
|
|
718
|
-
if (!this.fs.existsSync(path)) return null;
|
|
719
|
-
try {
|
|
720
|
-
const content = this.fs.readFileSync(path).trim();
|
|
721
|
-
const pid = Number(content);
|
|
722
|
-
if (!pid || pid <= 0) return null;
|
|
723
|
-
return pid;
|
|
724
|
-
} catch {
|
|
725
|
-
return null;
|
|
696
|
+
if (options.profileId) this.guard.release(options.profileId);
|
|
726
697
|
}
|
|
727
698
|
}
|
|
728
|
-
writePidFile(profileId) {
|
|
729
|
-
this.fs.mkdirSync(this.pidDir, { recursive: true });
|
|
730
|
-
this.fs.writeFileSync(this.pidPath(profileId), String(globalThis.process.pid));
|
|
731
|
-
}
|
|
732
|
-
removePidFile(profileId) {
|
|
733
|
-
const path = this.pidPath(profileId);
|
|
734
|
-
if (this.fs.existsSync(path)) this.fs.unlink(path);
|
|
735
|
-
}
|
|
736
|
-
installCleanup(profileId) {
|
|
737
|
-
globalThis.process.once("exit", () => this.removePidFile(profileId));
|
|
738
|
-
}
|
|
739
|
-
isProcessAlive(pid) {
|
|
740
|
-
return this.process.isAlive(pid);
|
|
741
|
-
}
|
|
742
699
|
buildArgs(recipeOptions, userArgs, cwd, session) {
|
|
743
700
|
const result = [...recipeOptions, ...userArgs];
|
|
744
701
|
if (session !== null) if (session.mode === "resume") result.push("--resume", session.id);
|
|
@@ -772,31 +729,18 @@ var FunnelClaude = class {
|
|
|
772
729
|
if (arg === "--resume" || arg.startsWith("--resume=")) return null;
|
|
773
730
|
if (arg === "--session-id" || arg.startsWith("--session-id=")) return null;
|
|
774
731
|
}
|
|
775
|
-
const existing = this.
|
|
776
|
-
if (existing !== null && this.sessionFileExists(cwd, existing, recipeEnv)) return {
|
|
732
|
+
const existing = this.sessions.getSessionId(profileId);
|
|
733
|
+
if (existing !== null && this.sessions.sessionFileExists(cwd, existing, recipeEnv)) return {
|
|
777
734
|
id: existing,
|
|
778
735
|
mode: "resume"
|
|
779
736
|
};
|
|
780
737
|
const fresh = this.idGenerator.generate();
|
|
781
|
-
this.
|
|
738
|
+
this.sessions.setSessionId(profileId, fresh);
|
|
782
739
|
return {
|
|
783
740
|
id: fresh,
|
|
784
741
|
mode: "new"
|
|
785
742
|
};
|
|
786
743
|
}
|
|
787
|
-
/**
|
|
788
|
-
* Mirrors claude's session storage path
|
|
789
|
-
* (`<config-dir>/projects/<cwd-with-slashes-as-dashes>/<id>.jsonl`) to check
|
|
790
|
-
* whether a recorded session still exists AND is non-empty. Reads the same
|
|
791
|
-
* `CLAUDE_CONFIG_DIR` the child will run under so the check matches reality; a
|
|
792
|
-
* wrong guess can only ever produce a false negative (start fresh), never a
|
|
793
|
-
* bad resume.
|
|
794
|
-
*/
|
|
795
|
-
sessionFileExists(cwd, sessionId, recipeEnv) {
|
|
796
|
-
const path = join(recipeEnv.CLAUDE_CONFIG_DIR ?? globalThis.process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"), "projects", cwd.replace(/\//g, "-"), `${sessionId}.jsonl`);
|
|
797
|
-
if (!this.fs.existsSync(path)) return false;
|
|
798
|
-
return this.fs.readFileSync(path).trim().length > 0;
|
|
799
|
-
}
|
|
800
744
|
buildEnv(channelId, recipeEnv) {
|
|
801
745
|
const env = {};
|
|
802
746
|
for (const [key, value] of Object.entries(recipeEnv)) env[key] = value;
|
|
@@ -807,91 +751,47 @@ var FunnelClaude = class {
|
|
|
807
751
|
}
|
|
808
752
|
};
|
|
809
753
|
//#endregion
|
|
810
|
-
//#region lib/engine/
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
this.
|
|
821
|
-
|
|
822
|
-
this.mtimes = new Map(Object.entries(props.mtimes ?? {}));
|
|
823
|
-
this.modes = new Map(Object.entries(props.modes ?? {}));
|
|
824
|
-
this.now = props.now ?? (() => Date.now());
|
|
825
|
-
}
|
|
826
|
-
existsSync(path) {
|
|
827
|
-
return this.dirs.has(path) || this.files.has(path);
|
|
828
|
-
}
|
|
829
|
-
readFileSync(path) {
|
|
830
|
-
return this.files.get(path) ?? "";
|
|
831
|
-
}
|
|
832
|
-
writeFileSync(path, data) {
|
|
833
|
-
this.files.set(path, data);
|
|
834
|
-
this.touch(path);
|
|
754
|
+
//#region lib/engine/claude/file-process-guard.ts
|
|
755
|
+
const defaultFs$4 = new NodeFunnelFileSystem();
|
|
756
|
+
const defaultProcess$2 = new NodeFunnelProcessRunner();
|
|
757
|
+
var FileProcessGuard = class {
|
|
758
|
+
fs;
|
|
759
|
+
process;
|
|
760
|
+
pidDir;
|
|
761
|
+
constructor(deps = {}) {
|
|
762
|
+
this.fs = deps.fs ?? defaultFs$4;
|
|
763
|
+
this.process = deps.process ?? defaultProcess$2;
|
|
764
|
+
this.pidDir = join(deps.dir ?? FUNNEL_DIR, "claude");
|
|
765
|
+
Object.freeze(this);
|
|
835
766
|
}
|
|
836
|
-
|
|
837
|
-
this.
|
|
838
|
-
|
|
839
|
-
this.
|
|
767
|
+
isRunning(profileId) {
|
|
768
|
+
const pid = this.readPid(profileId);
|
|
769
|
+
if (!pid) return false;
|
|
770
|
+
return this.process.isAlive(pid);
|
|
840
771
|
}
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
this.
|
|
844
|
-
this.
|
|
772
|
+
acquire(profileId) {
|
|
773
|
+
this.fs.mkdirSync(this.pidDir, { recursive: true });
|
|
774
|
+
this.fs.writeFileSync(this.pidPath(profileId), String(globalThis.process.pid));
|
|
775
|
+
globalThis.process.once("exit", () => this.release(profileId));
|
|
845
776
|
}
|
|
846
|
-
|
|
847
|
-
this.
|
|
848
|
-
this.
|
|
849
|
-
this.modes.delete(path);
|
|
777
|
+
release(profileId) {
|
|
778
|
+
const path = this.pidPath(profileId);
|
|
779
|
+
if (this.fs.existsSync(path)) this.fs.unlink(path);
|
|
850
780
|
}
|
|
851
|
-
|
|
852
|
-
this.
|
|
781
|
+
pidPath(profileId) {
|
|
782
|
+
return join(this.pidDir, `${profileId}.pid`);
|
|
853
783
|
}
|
|
854
|
-
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
if (!
|
|
784
|
+
readPid(profileId) {
|
|
785
|
+
const path = this.pidPath(profileId);
|
|
786
|
+
if (!this.fs.existsSync(path)) return null;
|
|
787
|
+
try {
|
|
788
|
+
const content = this.fs.readFileSync(path).trim();
|
|
789
|
+
const pid = Number(content);
|
|
790
|
+
if (!pid || pid <= 0) return null;
|
|
791
|
+
return pid;
|
|
792
|
+
} catch {
|
|
793
|
+
return null;
|
|
861
794
|
}
|
|
862
|
-
return names;
|
|
863
|
-
}
|
|
864
|
-
statSync(path) {
|
|
865
|
-
const mtimeMs = this.mtimes.get(path);
|
|
866
|
-
if (mtimeMs === void 0) throw new Error(`not found: ${path}`);
|
|
867
|
-
return {
|
|
868
|
-
mtimeMs,
|
|
869
|
-
mode: this.modes.get(path) ?? null
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
setMtime(path, mtimeMs) {
|
|
873
|
-
this.mtimes.set(path, mtimeMs);
|
|
874
|
-
}
|
|
875
|
-
setMode(path, mode) {
|
|
876
|
-
this.modes.set(path, mode);
|
|
877
|
-
}
|
|
878
|
-
touch(path) {
|
|
879
|
-
if (!this.mtimes.has(path)) this.mtimes.set(path, this.now());
|
|
880
|
-
else this.mtimes.set(path, this.now());
|
|
881
|
-
}
|
|
882
|
-
};
|
|
883
|
-
//#endregion
|
|
884
|
-
//#region lib/engine/id/memory-id-generator.ts
|
|
885
|
-
var MemoryFunnelIdGenerator = class extends FunnelIdGenerator {
|
|
886
|
-
counter = 0;
|
|
887
|
-
prefix;
|
|
888
|
-
constructor(props = {}) {
|
|
889
|
-
super();
|
|
890
|
-
this.prefix = props.prefix ?? "id";
|
|
891
|
-
}
|
|
892
|
-
generate() {
|
|
893
|
-
this.counter++;
|
|
894
|
-
return `${this.prefix}-${this.counter}`;
|
|
895
795
|
}
|
|
896
796
|
};
|
|
897
797
|
//#endregion
|
|
@@ -1247,85 +1147,13 @@ var FunnelLocalConfigSync = class {
|
|
|
1247
1147
|
};
|
|
1248
1148
|
}
|
|
1249
1149
|
};
|
|
1250
|
-
//#endregion
|
|
1251
|
-
//#region lib/engine/local-config/local-config-writer.ts
|
|
1252
|
-
const isRecord = (value) => {
|
|
1253
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1254
|
-
};
|
|
1255
|
-
const withIdFirst = (config, id) => {
|
|
1256
|
-
const ordered = {};
|
|
1257
|
-
if (config.$schema !== void 0) ordered.$schema = config.$schema;
|
|
1258
|
-
ordered.id = id;
|
|
1259
|
-
for (const key of Object.keys(config)) {
|
|
1260
|
-
if (key === "$schema" || key === "id") continue;
|
|
1261
|
-
ordered[key] = config[key];
|
|
1262
|
-
}
|
|
1263
|
-
return ordered;
|
|
1264
|
-
};
|
|
1265
|
-
/**
|
|
1266
|
-
* The one path that mutates the repo-committed funnel.json, and it only ever
|
|
1267
|
-
* inserts `id`. On first launch a repo has no `id`; funnel generates one and
|
|
1268
|
-
* writes it back here so future launches resolve the same `~/.funnel/projects/<id>/`.
|
|
1269
|
-
* Idempotent — a no-op once `id` is present. Kept separate from the read-only
|
|
1270
|
-
* FunnelLocalConfig so reads stay side-effect free.
|
|
1271
|
-
*/
|
|
1272
|
-
var FunnelLocalConfigWriter = class {
|
|
1273
|
-
fs;
|
|
1274
|
-
constructor(deps) {
|
|
1275
|
-
this.fs = deps.fs;
|
|
1276
|
-
Object.freeze(this);
|
|
1277
|
-
}
|
|
1278
|
-
ensureId(cwd, id) {
|
|
1279
|
-
const path = join(cwd, LOCAL_CONFIG_FILENAME);
|
|
1280
|
-
if (!this.fs.existsSync(path)) return;
|
|
1281
|
-
const parsed = JSON.parse(this.fs.readFileSync(path));
|
|
1282
|
-
if (!isRecord(parsed)) return;
|
|
1283
|
-
if (typeof parsed.id === "string" && parsed.id !== "") return;
|
|
1284
|
-
const ordered = withIdFirst(parsed, id);
|
|
1285
|
-
this.fs.writeFileSync(path, `${JSON.stringify(ordered, null, 2)}\n`);
|
|
1286
|
-
}
|
|
1287
|
-
};
|
|
1288
|
-
//#endregion
|
|
1289
|
-
//#region lib/engine/logger/memory-logger.ts
|
|
1290
|
-
var MemoryFunnelLogger = class extends FunnelLogger {
|
|
1291
|
-
file = null;
|
|
1292
|
-
entries = [];
|
|
1293
|
-
info(message, meta) {
|
|
1294
|
-
this.entries.push({
|
|
1295
|
-
level: "info",
|
|
1296
|
-
message,
|
|
1297
|
-
meta
|
|
1298
|
-
});
|
|
1299
|
-
}
|
|
1300
|
-
warn(message, meta) {
|
|
1301
|
-
this.entries.push({
|
|
1302
|
-
level: "warn",
|
|
1303
|
-
message,
|
|
1304
|
-
meta
|
|
1305
|
-
});
|
|
1306
|
-
}
|
|
1307
|
-
error(message, meta) {
|
|
1308
|
-
this.entries.push({
|
|
1309
|
-
level: "error",
|
|
1310
|
-
message,
|
|
1311
|
-
meta
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
clear() {
|
|
1315
|
-
this.entries.length = 0;
|
|
1316
|
-
}
|
|
1317
|
-
};
|
|
1318
|
-
//#endregion
|
|
1319
|
-
//#region lib/engine/mcp/mcp.ts
|
|
1320
|
-
const FUNNEL_MCP_COMMAND = "bun";
|
|
1321
1150
|
const FUNNEL_MCP_ARGS = ["funnel", "mcp"];
|
|
1322
|
-
const FUNNEL_MCP_NAME = "funnel";
|
|
1323
1151
|
const mcpEntrySchema = z.object({
|
|
1324
1152
|
command: z.string().optional(),
|
|
1325
1153
|
args: z.array(z.string()).optional()
|
|
1326
1154
|
});
|
|
1327
1155
|
const mcpConfigSchema = z.object({ mcpServers: z.record(z.string(), mcpEntrySchema).optional() });
|
|
1328
|
-
const defaultFs$
|
|
1156
|
+
const defaultFs$3 = new NodeFunnelFileSystem();
|
|
1329
1157
|
/**
|
|
1330
1158
|
* Installs/uninstalls the funnel MCP entry into a target repository's
|
|
1331
1159
|
* `.mcp.json`. Detects an existing entry by command match so renaming is
|
|
@@ -1334,7 +1162,7 @@ const defaultFs$2 = new NodeFunnelFileSystem();
|
|
|
1334
1162
|
var FunnelMcp = class {
|
|
1335
1163
|
fs;
|
|
1336
1164
|
constructor(deps = {}) {
|
|
1337
|
-
this.fs = deps.fs ?? defaultFs$
|
|
1165
|
+
this.fs = deps.fs ?? defaultFs$3;
|
|
1338
1166
|
Object.freeze(this);
|
|
1339
1167
|
}
|
|
1340
1168
|
install(repoPath) {
|
|
@@ -1403,107 +1231,8 @@ var FunnelMcp = class {
|
|
|
1403
1231
|
}
|
|
1404
1232
|
};
|
|
1405
1233
|
//#endregion
|
|
1406
|
-
//#region lib/engine/process/memory-process-runner.ts
|
|
1407
|
-
const empty = {
|
|
1408
|
-
exitCode: 0,
|
|
1409
|
-
stdout: "",
|
|
1410
|
-
stderr: ""
|
|
1411
|
-
};
|
|
1412
|
-
var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
|
|
1413
|
-
calls = [];
|
|
1414
|
-
killed = [];
|
|
1415
|
-
handler = () => empty;
|
|
1416
|
-
syncHandler = () => empty;
|
|
1417
|
-
aliveStub = null;
|
|
1418
|
-
listStub = null;
|
|
1419
|
-
on(handler) {
|
|
1420
|
-
this.handler = handler;
|
|
1421
|
-
return this;
|
|
1422
|
-
}
|
|
1423
|
-
onSync(handler) {
|
|
1424
|
-
this.syncHandler = handler;
|
|
1425
|
-
return this;
|
|
1426
|
-
}
|
|
1427
|
-
onIsAlive(stub) {
|
|
1428
|
-
this.aliveStub = stub;
|
|
1429
|
-
return this;
|
|
1430
|
-
}
|
|
1431
|
-
onListProcessesContaining(stub) {
|
|
1432
|
-
this.listStub = stub;
|
|
1433
|
-
return this;
|
|
1434
|
-
}
|
|
1435
|
-
async run(command, options = {}) {
|
|
1436
|
-
this.calls.push({
|
|
1437
|
-
kind: "run",
|
|
1438
|
-
command,
|
|
1439
|
-
options
|
|
1440
|
-
});
|
|
1441
|
-
const result = await this.handler(command);
|
|
1442
|
-
return {
|
|
1443
|
-
exitCode: result.exitCode ?? 0,
|
|
1444
|
-
stdout: result.stdout ?? "",
|
|
1445
|
-
stderr: result.stderr ?? ""
|
|
1446
|
-
};
|
|
1447
|
-
}
|
|
1448
|
-
runSync(command) {
|
|
1449
|
-
this.calls.push({
|
|
1450
|
-
kind: "runSync",
|
|
1451
|
-
command
|
|
1452
|
-
});
|
|
1453
|
-
const result = this.syncHandler(command);
|
|
1454
|
-
return {
|
|
1455
|
-
exitCode: result.exitCode ?? 0,
|
|
1456
|
-
stdout: result.stdout ?? "",
|
|
1457
|
-
stderr: result.stderr ?? ""
|
|
1458
|
-
};
|
|
1459
|
-
}
|
|
1460
|
-
async attach(command, options = {}) {
|
|
1461
|
-
this.calls.push({
|
|
1462
|
-
kind: "attach",
|
|
1463
|
-
command,
|
|
1464
|
-
options
|
|
1465
|
-
});
|
|
1466
|
-
if (options.onSpawned) options.onSpawned(1);
|
|
1467
|
-
return (await this.handler(command)).exitCode ?? 0;
|
|
1468
|
-
}
|
|
1469
|
-
detach(command, options = {}) {
|
|
1470
|
-
this.calls.push({
|
|
1471
|
-
kind: "detach",
|
|
1472
|
-
command,
|
|
1473
|
-
options
|
|
1474
|
-
});
|
|
1475
|
-
}
|
|
1476
|
-
kill(pid, signal = "SIGTERM") {
|
|
1477
|
-
this.calls.push({
|
|
1478
|
-
kind: "kill",
|
|
1479
|
-
command: [String(pid), signal]
|
|
1480
|
-
});
|
|
1481
|
-
this.killed.push({
|
|
1482
|
-
pid,
|
|
1483
|
-
signal
|
|
1484
|
-
});
|
|
1485
|
-
}
|
|
1486
|
-
isAlive(pid) {
|
|
1487
|
-
if (this.aliveStub) return this.aliveStub(pid);
|
|
1488
|
-
const result = this.syncHandler([
|
|
1489
|
-
"ps",
|
|
1490
|
-
"-p",
|
|
1491
|
-
String(pid),
|
|
1492
|
-
"-o",
|
|
1493
|
-
"state="
|
|
1494
|
-
]);
|
|
1495
|
-
if ((result.exitCode ?? 0) !== 0) return false;
|
|
1496
|
-
const state = (result.stdout ?? "").trim();
|
|
1497
|
-
if (!state) return false;
|
|
1498
|
-
return !state.startsWith("Z");
|
|
1499
|
-
}
|
|
1500
|
-
listProcessesContaining(marker) {
|
|
1501
|
-
if (this.listStub) return this.listStub(marker);
|
|
1502
|
-
return [];
|
|
1503
|
-
}
|
|
1504
|
-
};
|
|
1505
|
-
//#endregion
|
|
1506
1234
|
//#region lib/engine/profiles/profiles.ts
|
|
1235
|
+
const defaultFs$2 = new NodeFunnelFileSystem();
|
|
1507
1236
|
/**
|
|
1508
1237
|
* Named launch presets for `fnl claude`. Each profile bundles a working
|
|
1509
1238
|
* directory, the channel id its Claude instance subscribes to, and the launch
|
|
@@ -1524,9 +1253,11 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
|
|
|
1524
1253
|
var FunnelProfiles = class {
|
|
1525
1254
|
store;
|
|
1526
1255
|
idGenerator;
|
|
1256
|
+
fs;
|
|
1527
1257
|
constructor(deps) {
|
|
1528
1258
|
this.store = deps.store;
|
|
1529
1259
|
this.idGenerator = deps.idGenerator;
|
|
1260
|
+
this.fs = deps.fs ?? defaultFs$2;
|
|
1530
1261
|
Object.freeze(this);
|
|
1531
1262
|
}
|
|
1532
1263
|
list() {
|
|
@@ -1596,6 +1327,19 @@ var FunnelProfiles = class {
|
|
|
1596
1327
|
profile.sessionId = sessionId;
|
|
1597
1328
|
this.store.write(settings);
|
|
1598
1329
|
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Mirrors claude's session storage path
|
|
1332
|
+
* (`<config-dir>/projects/<cwd-with-slashes-as-dashes>/<id>.jsonl`) to check
|
|
1333
|
+
* whether a recorded session still exists AND is non-empty. Reads the same
|
|
1334
|
+
* `CLAUDE_CONFIG_DIR` the child will run under so the check matches reality; a
|
|
1335
|
+
* wrong guess can only ever produce a false negative (start fresh), never a
|
|
1336
|
+
* bad resume.
|
|
1337
|
+
*/
|
|
1338
|
+
sessionFileExists(cwd, sessionId, env) {
|
|
1339
|
+
const path = join(env.CLAUDE_CONFIG_DIR ?? globalThis.process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"), "projects", cwd.replace(/\//g, "-"), `${sessionId}.jsonl`);
|
|
1340
|
+
if (!this.fs.existsSync(path)) return false;
|
|
1341
|
+
return this.fs.readFileSync(path).trim().length > 0;
|
|
1342
|
+
}
|
|
1599
1343
|
update(name, fields) {
|
|
1600
1344
|
const settings = this.store.read();
|
|
1601
1345
|
const profile = settings.profiles.find((p) => p.name === name);
|
|
@@ -1641,42 +1385,260 @@ var NodeFunnelTokenPrompter = class extends FunnelTokenPrompter {
|
|
|
1641
1385
|
stderr.write(LF);
|
|
1642
1386
|
}
|
|
1643
1387
|
}
|
|
1644
|
-
readSecret() {
|
|
1645
|
-
return new Promise((resolve, reject) => {
|
|
1646
|
-
let buffer = "";
|
|
1647
|
-
const onData = (chunk) => {
|
|
1648
|
-
for (const byte of chunk) {
|
|
1649
|
-
const char = String.fromCharCode(byte);
|
|
1650
|
-
if (char === LF || char === CR) {
|
|
1651
|
-
stdin.off("data", onData);
|
|
1652
|
-
resolve(buffer);
|
|
1653
|
-
return;
|
|
1654
|
-
}
|
|
1655
|
-
if (char === CTRL_C) {
|
|
1656
|
-
stdin.off("data", onData);
|
|
1657
|
-
reject(/* @__PURE__ */ new Error("prompt cancelled"));
|
|
1658
|
-
return;
|
|
1659
|
-
}
|
|
1660
|
-
if (char === CTRL_D) {
|
|
1661
|
-
stdin.off("data", onData);
|
|
1662
|
-
if (buffer.length === 0) reject(/* @__PURE__ */ new Error("prompt cancelled"));
|
|
1663
|
-
else resolve(buffer);
|
|
1664
|
-
return;
|
|
1665
|
-
}
|
|
1666
|
-
if (char === BACKSPACE || char === DEL) {
|
|
1667
|
-
if (buffer.length > 0) {
|
|
1668
|
-
buffer = buffer.slice(0, -1);
|
|
1669
|
-
stderr.write("\b \b");
|
|
1670
|
-
}
|
|
1671
|
-
continue;
|
|
1672
|
-
}
|
|
1673
|
-
buffer += char;
|
|
1674
|
-
stderr.write(STAR);
|
|
1675
|
-
}
|
|
1676
|
-
};
|
|
1677
|
-
stdin.on("data", onData);
|
|
1388
|
+
readSecret() {
|
|
1389
|
+
return new Promise((resolve, reject) => {
|
|
1390
|
+
let buffer = "";
|
|
1391
|
+
const onData = (chunk) => {
|
|
1392
|
+
for (const byte of chunk) {
|
|
1393
|
+
const char = String.fromCharCode(byte);
|
|
1394
|
+
if (char === LF || char === CR) {
|
|
1395
|
+
stdin.off("data", onData);
|
|
1396
|
+
resolve(buffer);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (char === CTRL_C) {
|
|
1400
|
+
stdin.off("data", onData);
|
|
1401
|
+
reject(/* @__PURE__ */ new Error("prompt cancelled"));
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
if (char === CTRL_D) {
|
|
1405
|
+
stdin.off("data", onData);
|
|
1406
|
+
if (buffer.length === 0) reject(/* @__PURE__ */ new Error("prompt cancelled"));
|
|
1407
|
+
else resolve(buffer);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
if (char === BACKSPACE || char === DEL) {
|
|
1411
|
+
if (buffer.length > 0) {
|
|
1412
|
+
buffer = buffer.slice(0, -1);
|
|
1413
|
+
stderr.write("\b \b");
|
|
1414
|
+
}
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
buffer += char;
|
|
1418
|
+
stderr.write(STAR);
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
stdin.on("data", onData);
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
//#endregion
|
|
1426
|
+
//#region lib/engine/fs/memory-file-system.ts
|
|
1427
|
+
const SECRET_MODE = 384;
|
|
1428
|
+
var MemoryFunnelFileSystem = class extends FunnelFileSystem {
|
|
1429
|
+
dirs;
|
|
1430
|
+
files;
|
|
1431
|
+
mtimes;
|
|
1432
|
+
modes;
|
|
1433
|
+
now;
|
|
1434
|
+
constructor(props = {}) {
|
|
1435
|
+
super();
|
|
1436
|
+
this.dirs = new Set(props.dirs ?? []);
|
|
1437
|
+
this.files = new Map(Object.entries(props.files ?? {}));
|
|
1438
|
+
this.mtimes = new Map(Object.entries(props.mtimes ?? {}));
|
|
1439
|
+
this.modes = new Map(Object.entries(props.modes ?? {}));
|
|
1440
|
+
this.now = props.now ?? (() => Date.now());
|
|
1441
|
+
}
|
|
1442
|
+
existsSync(path) {
|
|
1443
|
+
return this.dirs.has(path) || this.files.has(path);
|
|
1444
|
+
}
|
|
1445
|
+
readFileSync(path) {
|
|
1446
|
+
return this.files.get(path) ?? "";
|
|
1447
|
+
}
|
|
1448
|
+
writeFileSync(path, data) {
|
|
1449
|
+
this.files.set(path, data);
|
|
1450
|
+
this.touch(path);
|
|
1451
|
+
}
|
|
1452
|
+
writeSecretFileSync(path, data) {
|
|
1453
|
+
this.files.set(path, data);
|
|
1454
|
+
this.modes.set(path, SECRET_MODE);
|
|
1455
|
+
this.touch(path);
|
|
1456
|
+
}
|
|
1457
|
+
appendFileSync(path, data) {
|
|
1458
|
+
const prev = this.files.get(path) ?? "";
|
|
1459
|
+
this.files.set(path, prev + data);
|
|
1460
|
+
this.touch(path);
|
|
1461
|
+
}
|
|
1462
|
+
unlink(path) {
|
|
1463
|
+
this.files.delete(path);
|
|
1464
|
+
this.mtimes.delete(path);
|
|
1465
|
+
this.modes.delete(path);
|
|
1466
|
+
}
|
|
1467
|
+
mkdirSync(path, options) {
|
|
1468
|
+
this.dirs.add(path);
|
|
1469
|
+
}
|
|
1470
|
+
readdirSync(path) {
|
|
1471
|
+
const prefix = path.endsWith("/") ? path : `${path}/`;
|
|
1472
|
+
const names = [];
|
|
1473
|
+
for (const file of this.files.keys()) {
|
|
1474
|
+
if (!file.startsWith(prefix)) continue;
|
|
1475
|
+
const rest = file.slice(prefix.length);
|
|
1476
|
+
if (!rest.includes("/")) names.push(rest);
|
|
1477
|
+
}
|
|
1478
|
+
return names;
|
|
1479
|
+
}
|
|
1480
|
+
statSync(path) {
|
|
1481
|
+
const mtimeMs = this.mtimes.get(path);
|
|
1482
|
+
if (mtimeMs === void 0) throw new Error(`not found: ${path}`);
|
|
1483
|
+
return {
|
|
1484
|
+
mtimeMs,
|
|
1485
|
+
mode: this.modes.get(path) ?? null
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
setMtime(path, mtimeMs) {
|
|
1489
|
+
this.mtimes.set(path, mtimeMs);
|
|
1490
|
+
}
|
|
1491
|
+
setMode(path, mode) {
|
|
1492
|
+
this.modes.set(path, mode);
|
|
1493
|
+
}
|
|
1494
|
+
touch(path) {
|
|
1495
|
+
if (!this.mtimes.has(path)) this.mtimes.set(path, this.now());
|
|
1496
|
+
else this.mtimes.set(path, this.now());
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
//#endregion
|
|
1500
|
+
//#region lib/engine/id/memory-id-generator.ts
|
|
1501
|
+
var MemoryFunnelIdGenerator = class extends FunnelIdGenerator {
|
|
1502
|
+
counter = 0;
|
|
1503
|
+
prefix;
|
|
1504
|
+
constructor(props = {}) {
|
|
1505
|
+
super();
|
|
1506
|
+
this.prefix = props.prefix ?? "id";
|
|
1507
|
+
}
|
|
1508
|
+
generate() {
|
|
1509
|
+
this.counter++;
|
|
1510
|
+
return `${this.prefix}-${this.counter}`;
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
//#endregion
|
|
1514
|
+
//#region lib/engine/logger/memory-logger.ts
|
|
1515
|
+
var MemoryFunnelLogger = class extends FunnelLogger {
|
|
1516
|
+
file = null;
|
|
1517
|
+
entries = [];
|
|
1518
|
+
info(message, meta) {
|
|
1519
|
+
this.entries.push({
|
|
1520
|
+
level: "info",
|
|
1521
|
+
message,
|
|
1522
|
+
meta
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
warn(message, meta) {
|
|
1526
|
+
this.entries.push({
|
|
1527
|
+
level: "warn",
|
|
1528
|
+
message,
|
|
1529
|
+
meta
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
error(message, meta) {
|
|
1533
|
+
this.entries.push({
|
|
1534
|
+
level: "error",
|
|
1535
|
+
message,
|
|
1536
|
+
meta
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
clear() {
|
|
1540
|
+
this.entries.length = 0;
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
//#endregion
|
|
1544
|
+
//#region lib/engine/process/memory-process-runner.ts
|
|
1545
|
+
const empty = {
|
|
1546
|
+
exitCode: 0,
|
|
1547
|
+
stdout: "",
|
|
1548
|
+
stderr: ""
|
|
1549
|
+
};
|
|
1550
|
+
var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
|
|
1551
|
+
calls = [];
|
|
1552
|
+
killed = [];
|
|
1553
|
+
handler = () => empty;
|
|
1554
|
+
syncHandler = () => empty;
|
|
1555
|
+
aliveStub = null;
|
|
1556
|
+
listStub = null;
|
|
1557
|
+
on(handler) {
|
|
1558
|
+
this.handler = handler;
|
|
1559
|
+
return this;
|
|
1560
|
+
}
|
|
1561
|
+
onSync(handler) {
|
|
1562
|
+
this.syncHandler = handler;
|
|
1563
|
+
return this;
|
|
1564
|
+
}
|
|
1565
|
+
onIsAlive(stub) {
|
|
1566
|
+
this.aliveStub = stub;
|
|
1567
|
+
return this;
|
|
1568
|
+
}
|
|
1569
|
+
onListProcessesContaining(stub) {
|
|
1570
|
+
this.listStub = stub;
|
|
1571
|
+
return this;
|
|
1572
|
+
}
|
|
1573
|
+
async run(command, options = {}) {
|
|
1574
|
+
this.calls.push({
|
|
1575
|
+
kind: "run",
|
|
1576
|
+
command,
|
|
1577
|
+
options
|
|
1578
|
+
});
|
|
1579
|
+
const result = await this.handler(command);
|
|
1580
|
+
return {
|
|
1581
|
+
exitCode: result.exitCode ?? 0,
|
|
1582
|
+
stdout: result.stdout ?? "",
|
|
1583
|
+
stderr: result.stderr ?? ""
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
runSync(command) {
|
|
1587
|
+
this.calls.push({
|
|
1588
|
+
kind: "runSync",
|
|
1589
|
+
command
|
|
1590
|
+
});
|
|
1591
|
+
const result = this.syncHandler(command);
|
|
1592
|
+
return {
|
|
1593
|
+
exitCode: result.exitCode ?? 0,
|
|
1594
|
+
stdout: result.stdout ?? "",
|
|
1595
|
+
stderr: result.stderr ?? ""
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
async attach(command, options = {}) {
|
|
1599
|
+
this.calls.push({
|
|
1600
|
+
kind: "attach",
|
|
1601
|
+
command,
|
|
1602
|
+
options
|
|
1603
|
+
});
|
|
1604
|
+
if (options.onSpawned) options.onSpawned(1);
|
|
1605
|
+
return (await this.handler(command)).exitCode ?? 0;
|
|
1606
|
+
}
|
|
1607
|
+
detach(command, options = {}) {
|
|
1608
|
+
this.calls.push({
|
|
1609
|
+
kind: "detach",
|
|
1610
|
+
command,
|
|
1611
|
+
options
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
kill(pid, signal = "SIGTERM") {
|
|
1615
|
+
this.calls.push({
|
|
1616
|
+
kind: "kill",
|
|
1617
|
+
command: [String(pid), signal]
|
|
1618
|
+
});
|
|
1619
|
+
this.killed.push({
|
|
1620
|
+
pid,
|
|
1621
|
+
signal
|
|
1678
1622
|
});
|
|
1679
1623
|
}
|
|
1624
|
+
isAlive(pid) {
|
|
1625
|
+
if (this.aliveStub) return this.aliveStub(pid);
|
|
1626
|
+
const result = this.syncHandler([
|
|
1627
|
+
"ps",
|
|
1628
|
+
"-p",
|
|
1629
|
+
String(pid),
|
|
1630
|
+
"-o",
|
|
1631
|
+
"state="
|
|
1632
|
+
]);
|
|
1633
|
+
if ((result.exitCode ?? 0) !== 0) return false;
|
|
1634
|
+
const state = (result.stdout ?? "").trim();
|
|
1635
|
+
if (!state) return false;
|
|
1636
|
+
return !state.startsWith("Z");
|
|
1637
|
+
}
|
|
1638
|
+
listProcessesContaining(marker) {
|
|
1639
|
+
if (this.listStub) return this.listStub(marker);
|
|
1640
|
+
return [];
|
|
1641
|
+
}
|
|
1680
1642
|
};
|
|
1681
1643
|
//#endregion
|
|
1682
1644
|
//#region lib/engine/settings/mock-settings-reader.ts
|
|
@@ -1743,9 +1705,8 @@ const publishRequestSchema = z.object({
|
|
|
1743
1705
|
connector: z.string().min(1).optional(),
|
|
1744
1706
|
/**
|
|
1745
1707
|
* Address the event to a single subscriber. When set, only the WS client that
|
|
1746
|
-
* declared this id at upgrade time (`?id=<subscriberId>`) receives it
|
|
1747
|
-
*
|
|
1748
|
-
* default fanout. The route surfaces it to subscribers as `meta.target`.
|
|
1708
|
+
* declared this id at upgrade time (`?id=<subscriberId>`) receives it. Omit for
|
|
1709
|
+
* the default fanout. The route surfaces it to subscribers as `meta.target`.
|
|
1749
1710
|
*/
|
|
1750
1711
|
target: z.string().min(1).optional()
|
|
1751
1712
|
});
|
|
@@ -2107,7 +2068,6 @@ var FunnelBroadcaster = class {
|
|
|
2107
2068
|
return [...persisted.filter((event) => event.offset < cutoff), ...fromMemory];
|
|
2108
2069
|
}
|
|
2109
2070
|
matchesClient(event, data) {
|
|
2110
|
-
if (data.tapAll) return true;
|
|
2111
2071
|
const target = event.meta?.target;
|
|
2112
2072
|
if (target && target !== data.subscriberId) return false;
|
|
2113
2073
|
const channelId = event.meta?.channelId;
|
|
@@ -2117,25 +2077,18 @@ var FunnelBroadcaster = class {
|
|
|
2117
2077
|
return data.connectors.includes(connector);
|
|
2118
2078
|
}
|
|
2119
2079
|
/**
|
|
2120
|
-
* Returns the list of WS clients that should receive `event`.
|
|
2121
|
-
* receive (passive observation). For each per-channel group:
|
|
2080
|
+
* Returns the list of WS clients that should receive `event`. For each per-channel group:
|
|
2122
2081
|
* - fanout → every matching client receives
|
|
2123
2082
|
* - exclusive → exactly one client receives, picked round-robin per channel
|
|
2124
2083
|
*
|
|
2125
|
-
* `meta.target` narrows the
|
|
2126
|
-
*
|
|
2127
|
-
* stays in the running, so a targeted event reaches one named instance while
|
|
2128
|
-
* still being observable by tap=all clients.
|
|
2084
|
+
* `meta.target` narrows the recipient set via `matchesClient`: only the subscriber
|
|
2085
|
+
* whose `subscriberId` equals `target` receives a targeted event.
|
|
2129
2086
|
*/
|
|
2130
2087
|
pickRecipients(event) {
|
|
2131
2088
|
const exclusiveByChannel = /* @__PURE__ */ new Map();
|
|
2132
2089
|
const recipients = [];
|
|
2133
2090
|
for (const [ws, data] of this.clients) {
|
|
2134
2091
|
if (!this.matchesClient(event, data)) continue;
|
|
2135
|
-
if (data.tapAll) {
|
|
2136
|
-
recipients.push(ws);
|
|
2137
|
-
continue;
|
|
2138
|
-
}
|
|
2139
2092
|
if (data.delivery === "exclusive") {
|
|
2140
2093
|
const list = exclusiveByChannel.get(data.channel) ?? [];
|
|
2141
2094
|
list.push(ws);
|
|
@@ -3340,7 +3293,6 @@ const defaultOnError = () => {};
|
|
|
3340
3293
|
*/
|
|
3341
3294
|
var FunnelGatewayServer = class {
|
|
3342
3295
|
channels;
|
|
3343
|
-
settings;
|
|
3344
3296
|
port;
|
|
3345
3297
|
hostname;
|
|
3346
3298
|
dbPath;
|
|
@@ -3360,7 +3312,6 @@ var FunnelGatewayServer = class {
|
|
|
3360
3312
|
server = null;
|
|
3361
3313
|
constructor(deps) {
|
|
3362
3314
|
this.channels = deps.channels;
|
|
3363
|
-
this.settings = deps.settings;
|
|
3364
3315
|
this.port = deps.port ?? resolveFunnelPort();
|
|
3365
3316
|
this.hostname = deps.hostname ?? DEFAULT_HOST;
|
|
3366
3317
|
this.dbPath = deps.dbPath ?? defaultDbPath();
|
|
@@ -3461,11 +3412,10 @@ var FunnelGatewayServer = class {
|
|
|
3461
3412
|
const url = new URL(request.url);
|
|
3462
3413
|
if (url.pathname === "/ws" && request.headers.get("upgrade") === "websocket") {
|
|
3463
3414
|
if (this.token && !this.tokenMatchesUpgrade(request)) return new Response("unauthorized", { status: 401 });
|
|
3464
|
-
const
|
|
3465
|
-
const
|
|
3466
|
-
const
|
|
3467
|
-
const
|
|
3468
|
-
const channelName = tapAll ? null : channel?.name ?? null;
|
|
3415
|
+
const requestedChannel = url.searchParams.get("channel") ?? "";
|
|
3416
|
+
const channel = requestedChannel ? this.resolveChannel(requestedChannel) : null;
|
|
3417
|
+
const channelId = channel?.id ?? requestedChannel;
|
|
3418
|
+
const channelName = channel?.name ?? null;
|
|
3469
3419
|
const connectors = channel?.connectors ?? [];
|
|
3470
3420
|
const delivery = channel?.delivery ?? "fanout";
|
|
3471
3421
|
const sinceRaw = url.searchParams.get("since");
|
|
@@ -3476,7 +3426,6 @@ var FunnelGatewayServer = class {
|
|
|
3476
3426
|
channel: channelId,
|
|
3477
3427
|
channelName,
|
|
3478
3428
|
connectors,
|
|
3479
|
-
tapAll,
|
|
3480
3429
|
delivery,
|
|
3481
3430
|
subscriberId,
|
|
3482
3431
|
since
|
|
@@ -3491,36 +3440,24 @@ var FunnelGatewayServer = class {
|
|
|
3491
3440
|
for (const event of replay) ws.send(JSON.stringify(event));
|
|
3492
3441
|
}
|
|
3493
3442
|
this.broadcaster.addClient(ws, ws.data);
|
|
3494
|
-
|
|
3495
|
-
const meta = {
|
|
3496
|
-
event_type: "system",
|
|
3497
|
-
action: "channel_connect",
|
|
3498
|
-
channel: ws.data.channelName,
|
|
3499
|
-
channelId: ws.data.channel,
|
|
3500
|
-
connectors: ws.data.connectors.join(","),
|
|
3501
|
-
total: String(this.broadcaster.getClientCount())
|
|
3502
|
-
};
|
|
3503
|
-
this.logger?.info("channel connected", meta);
|
|
3504
|
-
} else this.logger?.info("tap-all client connected", {
|
|
3443
|
+
this.logger?.info("channel connected", {
|
|
3505
3444
|
event_type: "system",
|
|
3506
|
-
action: "
|
|
3445
|
+
action: "channel_connect",
|
|
3446
|
+
channel: ws.data.channelName ?? "",
|
|
3447
|
+
channelId: ws.data.channel,
|
|
3448
|
+
connectors: ws.data.connectors.join(","),
|
|
3507
3449
|
total: String(this.broadcaster.getClientCount())
|
|
3508
3450
|
});
|
|
3509
3451
|
}
|
|
3510
3452
|
handleWsClose(ws) {
|
|
3511
3453
|
this.broadcaster.removeClient(ws);
|
|
3512
|
-
|
|
3454
|
+
this.logger?.info("channel disconnected", {
|
|
3513
3455
|
event_type: "system",
|
|
3514
3456
|
action: "channel_disconnect",
|
|
3515
|
-
channel: ws.data.channelName,
|
|
3457
|
+
channel: ws.data.channelName ?? "",
|
|
3516
3458
|
channelId: ws.data.channel,
|
|
3517
3459
|
total: String(this.broadcaster.getClientCount())
|
|
3518
3460
|
});
|
|
3519
|
-
else this.logger?.info("tap-all client disconnected", {
|
|
3520
|
-
event_type: "system",
|
|
3521
|
-
action: "tap_disconnect",
|
|
3522
|
-
total: String(this.broadcaster.getClientCount())
|
|
3523
|
-
});
|
|
3524
3461
|
}
|
|
3525
3462
|
logServerStarted() {
|
|
3526
3463
|
this.logger?.info("gateway started", {
|
|
@@ -3570,7 +3507,7 @@ var FunnelGatewayServer = class {
|
|
|
3570
3507
|
return false;
|
|
3571
3508
|
}
|
|
3572
3509
|
resolveChannel(requested) {
|
|
3573
|
-
const channel = this.
|
|
3510
|
+
const channel = this.channels.get(requested) ?? this.channels.getById(requested);
|
|
3574
3511
|
if (!channel) return null;
|
|
3575
3512
|
return {
|
|
3576
3513
|
id: channel.id,
|
|
@@ -3632,10 +3569,10 @@ var FunnelGatewayServer = class {
|
|
|
3632
3569
|
return { offset: event.offset };
|
|
3633
3570
|
}
|
|
3634
3571
|
lookupChannelId(channelName) {
|
|
3635
|
-
return this.
|
|
3572
|
+
return this.channels.get(channelName)?.id ?? null;
|
|
3636
3573
|
}
|
|
3637
3574
|
lookupConnectorId(channelId, connectorName) {
|
|
3638
|
-
return
|
|
3575
|
+
return this.channels.getById(channelId)?.connectors.find((c) => c.name === connectorName)?.id ?? null;
|
|
3639
3576
|
}
|
|
3640
3577
|
};
|
|
3641
3578
|
//#endregion
|
|
@@ -3827,7 +3764,7 @@ const buildFunnelDebugReport = async (deps, channelFilter) => {
|
|
|
3827
3764
|
errors: listenerEntry.errors,
|
|
3828
3765
|
lastEventAt: listenerEntry.lastEventAt
|
|
3829
3766
|
} : null;
|
|
3830
|
-
const claudeClients = (gatewayData?.clients ?? []).filter((cl) =>
|
|
3767
|
+
const claudeClients = (gatewayData?.clients ?? []).filter((cl) => cl.channelName === ch.name || cl.channel === ch.name);
|
|
3831
3768
|
report.channels.push({
|
|
3832
3769
|
name: ch.name,
|
|
3833
3770
|
connectors: ch.connectors.map((conn) => conn.name),
|
|
@@ -3870,15 +3807,14 @@ const SANDBOX_DIR = "/sandbox/.funnel";
|
|
|
3870
3807
|
const SANDBOX_TMP_DIR = "/sandbox/tmp";
|
|
3871
3808
|
const noopOnError = () => {};
|
|
3872
3809
|
/**
|
|
3873
|
-
* Facade
|
|
3810
|
+
* Facade that wires every funnel facet together and exposes the public surface.
|
|
3874
3811
|
*
|
|
3875
|
-
*
|
|
3876
|
-
*
|
|
3877
|
-
* injectable via `Props` — passing memory implementations gives a fully sandboxed
|
|
3812
|
+
* All side-effecting boundaries (filesystem, process, logger, clock, id, paths)
|
|
3813
|
+
* are injected via Props — passing memory implementations gives a fully sandboxed
|
|
3878
3814
|
* Funnel that touches no real disk, processes, or wall-clock time.
|
|
3879
3815
|
*
|
|
3880
|
-
*
|
|
3881
|
-
*
|
|
3816
|
+
* Fully immutable: all fields are resolved in the constructor and frozen.
|
|
3817
|
+
* No lazy initialisation — every dependency is wired at construction time.
|
|
3882
3818
|
*
|
|
3883
3819
|
* @example
|
|
3884
3820
|
* ```ts
|
|
@@ -3889,9 +3825,104 @@ const noopOnError = () => {};
|
|
|
3889
3825
|
* ```
|
|
3890
3826
|
*/
|
|
3891
3827
|
var Funnel = class Funnel {
|
|
3892
|
-
|
|
3828
|
+
paths;
|
|
3829
|
+
channels;
|
|
3830
|
+
gateway;
|
|
3831
|
+
gatewayToken;
|
|
3832
|
+
publisher;
|
|
3833
|
+
listeners;
|
|
3834
|
+
claude;
|
|
3835
|
+
profiles;
|
|
3836
|
+
localConfig;
|
|
3837
|
+
localConfigSync;
|
|
3838
|
+
fs;
|
|
3839
|
+
process;
|
|
3840
|
+
logger;
|
|
3841
|
+
clock;
|
|
3842
|
+
onError;
|
|
3893
3843
|
constructor(props = {}) {
|
|
3894
|
-
|
|
3844
|
+
const dir = props.dir ?? resolveFunnelDir();
|
|
3845
|
+
const tmpDir = props.tmpDir ?? funnelTmpDir();
|
|
3846
|
+
const fs = props.fs ?? new NodeFunnelFileSystem();
|
|
3847
|
+
const process = props.process ?? new NodeFunnelProcessRunner();
|
|
3848
|
+
const clock = props.clock ?? new NodeFunnelClock();
|
|
3849
|
+
const idGenerator = props.idGenerator ?? new NodeFunnelIdGenerator();
|
|
3850
|
+
this.paths = {
|
|
3851
|
+
dir,
|
|
3852
|
+
tmpDir,
|
|
3853
|
+
settings: join(dir, "settings.json")
|
|
3854
|
+
};
|
|
3855
|
+
this.fs = fs;
|
|
3856
|
+
this.process = process;
|
|
3857
|
+
this.logger = props.logger;
|
|
3858
|
+
this.clock = clock;
|
|
3859
|
+
this.onError = props.onError ?? noopOnError;
|
|
3860
|
+
const store = props.store ?? new FunnelSettingsStore({
|
|
3861
|
+
path: this.paths.settings,
|
|
3862
|
+
fs,
|
|
3863
|
+
idGenerator
|
|
3864
|
+
});
|
|
3865
|
+
const factory = new FunnelConnectorFactory({
|
|
3866
|
+
fs,
|
|
3867
|
+
process,
|
|
3868
|
+
logger: this.logger,
|
|
3869
|
+
diagnosticLog: props.diagnosticLog,
|
|
3870
|
+
dir,
|
|
3871
|
+
slackListenerOptions: props.slackListenerOptions,
|
|
3872
|
+
scheduleListenerOptions: props.scheduleListenerOptions
|
|
3873
|
+
});
|
|
3874
|
+
this.channels = new FunnelChannels({
|
|
3875
|
+
store,
|
|
3876
|
+
factory,
|
|
3877
|
+
clock,
|
|
3878
|
+
idGenerator
|
|
3879
|
+
});
|
|
3880
|
+
this.gateway = new FunnelGateway({
|
|
3881
|
+
fs,
|
|
3882
|
+
process,
|
|
3883
|
+
clock,
|
|
3884
|
+
dir,
|
|
3885
|
+
tmpDir,
|
|
3886
|
+
port: props.port
|
|
3887
|
+
});
|
|
3888
|
+
this.gatewayToken = new FunnelGatewayToken({
|
|
3889
|
+
fs,
|
|
3890
|
+
dir
|
|
3891
|
+
});
|
|
3892
|
+
this.publisher = new FunnelChannelPublisher({
|
|
3893
|
+
port: this.gateway.getPort(),
|
|
3894
|
+
isDaemonRunning: () => this.gateway.isRunning(),
|
|
3895
|
+
getToken: () => this.gatewayToken.read()
|
|
3896
|
+
});
|
|
3897
|
+
this.listeners = new FunnelListenersClient({
|
|
3898
|
+
port: this.gateway.getPort(),
|
|
3899
|
+
isDaemonRunning: () => this.gateway.isRunning(),
|
|
3900
|
+
getToken: () => this.gatewayToken.read()
|
|
3901
|
+
});
|
|
3902
|
+
const mcp = new FunnelMcp({ fs });
|
|
3903
|
+
this.profiles = new FunnelProfiles({
|
|
3904
|
+
store,
|
|
3905
|
+
idGenerator,
|
|
3906
|
+
fs
|
|
3907
|
+
});
|
|
3908
|
+
this.localConfig = new FunnelLocalConfig({ fs });
|
|
3909
|
+
this.localConfigSync = new FunnelLocalConfigSync({
|
|
3910
|
+
channels: this.channels,
|
|
3911
|
+
prompter: props.tokenPrompter ?? new NodeFunnelTokenPrompter()
|
|
3912
|
+
});
|
|
3913
|
+
this.claude = new FunnelClaude({
|
|
3914
|
+
channels: this.channels,
|
|
3915
|
+
mcp,
|
|
3916
|
+
gateway: this.gateway,
|
|
3917
|
+
sessions: this.profiles,
|
|
3918
|
+
guard: new FileProcessGuard({
|
|
3919
|
+
fs,
|
|
3920
|
+
process,
|
|
3921
|
+
dir
|
|
3922
|
+
}),
|
|
3923
|
+
process,
|
|
3924
|
+
logger: this.logger
|
|
3925
|
+
});
|
|
3895
3926
|
Object.freeze(this);
|
|
3896
3927
|
}
|
|
3897
3928
|
/**
|
|
@@ -3901,6 +3932,7 @@ var Funnel = class Funnel {
|
|
|
3901
3932
|
*/
|
|
3902
3933
|
static inMemory(props = {}) {
|
|
3903
3934
|
return new Funnel({
|
|
3935
|
+
...props,
|
|
3904
3936
|
store: props.store ?? new MockFunnelSettingsReader(),
|
|
3905
3937
|
fs: props.fs ?? new MemoryFunnelFileSystem(),
|
|
3906
3938
|
process: props.process ?? new MemoryFunnelProcessRunner(),
|
|
@@ -3911,183 +3943,6 @@ var Funnel = class Funnel {
|
|
|
3911
3943
|
tmpDir: props.tmpDir ?? SANDBOX_TMP_DIR
|
|
3912
3944
|
});
|
|
3913
3945
|
}
|
|
3914
|
-
/** Resolved on-disk paths the facade will read/write when methods are called. Pure compute, not memoized. */
|
|
3915
|
-
get paths() {
|
|
3916
|
-
const dir = this.props.dir ?? resolveFunnelDir();
|
|
3917
|
-
return {
|
|
3918
|
-
dir,
|
|
3919
|
-
tmpDir: this.props.tmpDir ?? funnelTmpDir(),
|
|
3920
|
-
settings: join(dir, "settings.json")
|
|
3921
|
-
};
|
|
3922
|
-
}
|
|
3923
|
-
/** Filesystem boundary. Defaults to NodeFunnelFileSystem. */
|
|
3924
|
-
get fs() {
|
|
3925
|
-
if (!this.memos.fs) this.memos.fs = this.props.fs ?? new NodeFunnelFileSystem();
|
|
3926
|
-
return this.memos.fs;
|
|
3927
|
-
}
|
|
3928
|
-
/** Process runner boundary. Defaults to NodeFunnelProcessRunner. */
|
|
3929
|
-
get process() {
|
|
3930
|
-
if (!this.memos.process) this.memos.process = this.props.process ?? new NodeFunnelProcessRunner();
|
|
3931
|
-
return this.memos.process;
|
|
3932
|
-
}
|
|
3933
|
-
/** Logger boundary. Optional — when no logger is injected, every facet's `this.logger?.x` call is a silent no-op. Production entry points (cli, daemon) inject a NodeFunnelLogger. */
|
|
3934
|
-
get logger() {
|
|
3935
|
-
return this.props.logger;
|
|
3936
|
-
}
|
|
3937
|
-
/** Clock boundary. Defaults to NodeFunnelClock. */
|
|
3938
|
-
get clock() {
|
|
3939
|
-
if (!this.memos.clock) this.memos.clock = this.props.clock ?? new NodeFunnelClock();
|
|
3940
|
-
return this.memos.clock;
|
|
3941
|
-
}
|
|
3942
|
-
/**
|
|
3943
|
-
* Error hook. Forwards Funnel-internal exceptions that would otherwise be
|
|
3944
|
-
* swallowed. Defaults to a no-op when no host hook was passed.
|
|
3945
|
-
*/
|
|
3946
|
-
get onError() {
|
|
3947
|
-
return this.props.onError ?? noopOnError;
|
|
3948
|
-
}
|
|
3949
|
-
/** ID generator boundary. Defaults to NodeFunnelIdGenerator. */
|
|
3950
|
-
get idGenerator() {
|
|
3951
|
-
if (!this.memos.idGenerator) this.memos.idGenerator = this.props.idGenerator ?? new NodeFunnelIdGenerator();
|
|
3952
|
-
return this.memos.idGenerator;
|
|
3953
|
-
}
|
|
3954
|
-
/** Settings reader. If not injected, a FunnelSettingsStore rooted at `dir` is created. */
|
|
3955
|
-
get store() {
|
|
3956
|
-
if (!this.memos.store) this.memos.store = this.props.store ?? new FunnelSettingsStore({
|
|
3957
|
-
path: this.paths.settings,
|
|
3958
|
-
fs: this.fs,
|
|
3959
|
-
idGenerator: this.idGenerator
|
|
3960
|
-
});
|
|
3961
|
-
return this.memos.store;
|
|
3962
|
-
}
|
|
3963
|
-
/** Pure factory that constructs per-type listeners and adapters from connector configs. */
|
|
3964
|
-
get factory() {
|
|
3965
|
-
if (!this.memos.factory) this.memos.factory = new FunnelConnectorFactory({
|
|
3966
|
-
fs: this.fs,
|
|
3967
|
-
process: this.process,
|
|
3968
|
-
logger: this.logger,
|
|
3969
|
-
diagnosticLog: this.props.diagnosticLog,
|
|
3970
|
-
dir: this.paths.dir,
|
|
3971
|
-
slackListenerOptions: this.props.slackListenerOptions,
|
|
3972
|
-
scheduleListenerOptions: this.props.scheduleListenerOptions
|
|
3973
|
-
});
|
|
3974
|
-
return this.memos.factory;
|
|
3975
|
-
}
|
|
3976
|
-
/** Channel CRUD + nested connector CRUD + schedule entries + listener/adapter dispatch. */
|
|
3977
|
-
get channels() {
|
|
3978
|
-
if (!this.memos.channels) this.memos.channels = new FunnelChannels({
|
|
3979
|
-
store: this.store,
|
|
3980
|
-
factory: this.factory,
|
|
3981
|
-
profileChecker: this.profiles,
|
|
3982
|
-
clock: this.clock,
|
|
3983
|
-
idGenerator: this.idGenerator
|
|
3984
|
-
});
|
|
3985
|
-
return this.memos.channels;
|
|
3986
|
-
}
|
|
3987
|
-
/** Launch profiles (named presets for `fnl claude`: path + sub-agent + channel id). */
|
|
3988
|
-
get profiles() {
|
|
3989
|
-
if (!this.memos.profiles) this.memos.profiles = new FunnelProfiles({
|
|
3990
|
-
store: this.store,
|
|
3991
|
-
idGenerator: this.idGenerator
|
|
3992
|
-
});
|
|
3993
|
-
return this.memos.profiles;
|
|
3994
|
-
}
|
|
3995
|
-
/** Reads `funnel.json` from a cwd. `fnl claude` consults it before falling back to the default profile. */
|
|
3996
|
-
get localConfig() {
|
|
3997
|
-
if (!this.memos.localConfig) this.memos.localConfig = new FunnelLocalConfig({ fs: this.fs });
|
|
3998
|
-
return this.memos.localConfig;
|
|
3999
|
-
}
|
|
4000
|
-
/** Writes the stable `id` into funnel.json on first launch so state can be scoped to `~/.funnel/projects/<id>/`. */
|
|
4001
|
-
get localConfigWriter() {
|
|
4002
|
-
if (!this.memos.localConfigWriter) this.memos.localConfigWriter = new FunnelLocalConfigWriter({ fs: this.fs });
|
|
4003
|
-
return this.memos.localConfigWriter;
|
|
4004
|
-
}
|
|
4005
|
-
/** Secret prompter. Defaults to a TTY-only stdin reader; tests inject MemoryFunnelTokenPrompter. */
|
|
4006
|
-
get tokenPrompter() {
|
|
4007
|
-
if (!this.memos.tokenPrompter) this.memos.tokenPrompter = this.props.tokenPrompter ?? new NodeFunnelTokenPrompter();
|
|
4008
|
-
return this.memos.tokenPrompter;
|
|
4009
|
-
}
|
|
4010
|
-
/** Reconciles funnel.json's channel + connectors with `~/.funnel/settings.json` on launch. */
|
|
4011
|
-
get localConfigSync() {
|
|
4012
|
-
if (!this.memos.localConfigSync) this.memos.localConfigSync = new FunnelLocalConfigSync({
|
|
4013
|
-
channels: this.channels,
|
|
4014
|
-
prompter: this.tokenPrompter
|
|
4015
|
-
});
|
|
4016
|
-
return this.memos.localConfigSync;
|
|
4017
|
-
}
|
|
4018
|
-
/** funnel MCP installer (writes/removes `.mcp.json` entries in target repos). */
|
|
4019
|
-
get mcp() {
|
|
4020
|
-
if (!this.memos.mcp) this.memos.mcp = new FunnelMcp({ fs: this.fs });
|
|
4021
|
-
return this.memos.mcp;
|
|
4022
|
-
}
|
|
4023
|
-
/** Launch Claude Code with a channel injected via env, MCP installed, gateway ensured. */
|
|
4024
|
-
get claude() {
|
|
4025
|
-
if (!this.memos.claude) this.memos.claude = new FunnelClaude({
|
|
4026
|
-
channels: this.channels,
|
|
4027
|
-
mcp: this.mcp,
|
|
4028
|
-
gateway: this.gateway,
|
|
4029
|
-
profiles: this.profiles,
|
|
4030
|
-
fs: this.fs,
|
|
4031
|
-
process: this.process,
|
|
4032
|
-
idGenerator: this.idGenerator,
|
|
4033
|
-
logger: this.logger,
|
|
4034
|
-
dir: this.paths.dir
|
|
4035
|
-
});
|
|
4036
|
-
return this.memos.claude;
|
|
4037
|
-
}
|
|
4038
|
-
/** Gateway daemon controller (PID-file, start/stop the separate `bun daemon.ts` process). */
|
|
4039
|
-
get gateway() {
|
|
4040
|
-
if (!this.memos.gateway) this.memos.gateway = new FunnelGateway({
|
|
4041
|
-
fs: this.fs,
|
|
4042
|
-
process: this.process,
|
|
4043
|
-
clock: this.clock,
|
|
4044
|
-
dir: this.paths.dir,
|
|
4045
|
-
tmpDir: this.paths.tmpDir
|
|
4046
|
-
});
|
|
4047
|
-
return this.memos.gateway;
|
|
4048
|
-
}
|
|
4049
|
-
/** Read / generate the daemon's gateway token (mode 0600 file under `dir`). */
|
|
4050
|
-
get gatewayToken() {
|
|
4051
|
-
if (!this.memos.gatewayToken) this.memos.gatewayToken = new FunnelGatewayToken({
|
|
4052
|
-
fs: this.fs,
|
|
4053
|
-
dir: this.paths.dir
|
|
4054
|
-
});
|
|
4055
|
-
return this.memos.gatewayToken;
|
|
4056
|
-
}
|
|
4057
|
-
/**
|
|
4058
|
-
* HTTP client for `POST /channels/:channel/publish` on the running gateway
|
|
4059
|
-
* daemon. Use it to push arbitrary content into a channel from outside any
|
|
4060
|
-
* connector. Returns `{ state: "offline" }` if the daemon isn't up.
|
|
4061
|
-
*/
|
|
4062
|
-
get publisher() {
|
|
4063
|
-
if (!this.memos.publisher) {
|
|
4064
|
-
const gateway = this.gateway;
|
|
4065
|
-
const token = this.gatewayToken;
|
|
4066
|
-
this.memos.publisher = new FunnelChannelPublisher({
|
|
4067
|
-
port: gateway.getPort(),
|
|
4068
|
-
isDaemonRunning: () => gateway.isRunning(),
|
|
4069
|
-
getToken: () => token.read()
|
|
4070
|
-
});
|
|
4071
|
-
}
|
|
4072
|
-
return this.memos.publisher;
|
|
4073
|
-
}
|
|
4074
|
-
/**
|
|
4075
|
-
* HTTP client for listener operations on the running gateway daemon.
|
|
4076
|
-
* Returns `{ state: "offline" }` when the daemon is offline so hot-reload
|
|
4077
|
-
* paths stay write-only without parsing strings.
|
|
4078
|
-
*/
|
|
4079
|
-
get listeners() {
|
|
4080
|
-
if (!this.memos.listeners) {
|
|
4081
|
-
const gateway = this.gateway;
|
|
4082
|
-
const token = this.gatewayToken;
|
|
4083
|
-
this.memos.listeners = new FunnelListenersClient({
|
|
4084
|
-
port: gateway.getPort(),
|
|
4085
|
-
isDaemonRunning: () => gateway.isRunning(),
|
|
4086
|
-
getToken: () => token.read()
|
|
4087
|
-
});
|
|
4088
|
-
}
|
|
4089
|
-
return this.memos.listeners;
|
|
4090
|
-
}
|
|
4091
3946
|
/**
|
|
4092
3947
|
* In-process gateway server. Unlike `gateway.start()` (which spawns a daemon),
|
|
4093
3948
|
* this returns a class that runs `Bun.serve` + listeners inside the current process —
|
|
@@ -4096,7 +3951,6 @@ var Funnel = class Funnel {
|
|
|
4096
3951
|
gatewayServer(options = {}) {
|
|
4097
3952
|
return new FunnelGatewayServer({
|
|
4098
3953
|
channels: this.channels,
|
|
4099
|
-
settings: this.store,
|
|
4100
3954
|
port: options.port,
|
|
4101
3955
|
hostname: options.hostname,
|
|
4102
3956
|
dbPath: options.dbPath,
|
|
@@ -4111,6 +3965,20 @@ var Funnel = class Funnel {
|
|
|
4111
3965
|
extraRoutes: options.extraRoutes
|
|
4112
3966
|
});
|
|
4113
3967
|
}
|
|
3968
|
+
/**
|
|
3969
|
+
* Run the gateway daemon in the foreground (tied to this terminal).
|
|
3970
|
+
* For background daemon management, use `funnel.gateway.start()` instead.
|
|
3971
|
+
*/
|
|
3972
|
+
async runGatewayForeground(options = {}) {
|
|
3973
|
+
const gatewayScript = resolveDaemonScript();
|
|
3974
|
+
const command = options.caffeinate !== false && globalThis.process.platform === "darwin" ? [
|
|
3975
|
+
"caffeinate",
|
|
3976
|
+
"-is",
|
|
3977
|
+
"bun",
|
|
3978
|
+
gatewayScript
|
|
3979
|
+
] : ["bun", gatewayScript];
|
|
3980
|
+
return this.process.attach(command);
|
|
3981
|
+
}
|
|
4114
3982
|
async debug(channelName) {
|
|
4115
3983
|
return buildFunnelDebugReport({
|
|
4116
3984
|
gateway: this.gateway,
|
|
@@ -4124,331 +3992,6 @@ var Funnel = class Funnel {
|
|
|
4124
3992
|
}
|
|
4125
3993
|
};
|
|
4126
3994
|
//#endregion
|
|
4127
|
-
//#region lib/engine/mcp/channel-subscriber.ts
|
|
4128
|
-
const RECONNECT_DELAY = 1e3;
|
|
4129
|
-
const MAX_RECONNECT_DELAY = 1e4;
|
|
4130
|
-
/**
|
|
4131
|
-
* Subscribes to the gateway WebSocket for a single channel and forwards
|
|
4132
|
-
* incoming events to the MCP server as `notifications/claude/channel`.
|
|
4133
|
-
* Reconnects with exponential backoff and replays missed events via `?since=<offset>`.
|
|
4134
|
-
*/
|
|
4135
|
-
var FunnelChannelSubscriber = class {
|
|
4136
|
-
state = {
|
|
4137
|
-
reconnectDelay: RECONNECT_DELAY,
|
|
4138
|
-
lastOffset: 0
|
|
4139
|
-
};
|
|
4140
|
-
constructor(props) {
|
|
4141
|
-
this.props = props;
|
|
4142
|
-
Object.freeze(this);
|
|
4143
|
-
}
|
|
4144
|
-
start() {
|
|
4145
|
-
this.connect();
|
|
4146
|
-
}
|
|
4147
|
-
connect() {
|
|
4148
|
-
const sinceQuery = this.state.lastOffset > 0 ? `&since=${this.state.lastOffset}` : "";
|
|
4149
|
-
const wsUrl = `${this.props.baseUrl}${sinceQuery}`;
|
|
4150
|
-
const ws = new WebSocket(wsUrl, this.props.protocols);
|
|
4151
|
-
ws.addEventListener("open", () => {
|
|
4152
|
-
this.state.reconnectDelay = RECONNECT_DELAY;
|
|
4153
|
-
process.stderr.write(`funnel: connected (${wsUrl})\n`);
|
|
4154
|
-
});
|
|
4155
|
-
ws.addEventListener("message", (event) => this.handleMessage(event));
|
|
4156
|
-
ws.addEventListener("close", () => {
|
|
4157
|
-
process.stderr.write(`funnel: disconnected, reconnecting in ${this.state.reconnectDelay}ms\n`);
|
|
4158
|
-
setTimeout(() => this.connect(), this.state.reconnectDelay);
|
|
4159
|
-
this.state.reconnectDelay = Math.min(this.state.reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
|
4160
|
-
});
|
|
4161
|
-
ws.addEventListener("error", () => {});
|
|
4162
|
-
}
|
|
4163
|
-
async handleMessage(event) {
|
|
4164
|
-
try {
|
|
4165
|
-
const payload = JSON.parse(String(event.data));
|
|
4166
|
-
const eventType = payload.meta?.event_type ?? "unknown";
|
|
4167
|
-
if (typeof payload.offset === "number" && payload.offset > this.state.lastOffset) this.state.lastOffset = payload.offset;
|
|
4168
|
-
process.stderr.write(`funnel: received event (${eventType})\n`);
|
|
4169
|
-
await this.props.server.notification({
|
|
4170
|
-
method: "notifications/claude/channel",
|
|
4171
|
-
params: {
|
|
4172
|
-
content: payload.content,
|
|
4173
|
-
meta: payload.meta
|
|
4174
|
-
}
|
|
4175
|
-
});
|
|
4176
|
-
} catch (error) {
|
|
4177
|
-
process.stderr.write(`funnel: error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
4178
|
-
}
|
|
4179
|
-
}
|
|
4180
|
-
};
|
|
4181
|
-
//#endregion
|
|
4182
|
-
//#region lib/engine/mcp/read-channel-connectors.ts
|
|
4183
|
-
const TOOL_CONNECTOR_TYPES = new Set([
|
|
4184
|
-
"slack",
|
|
4185
|
-
"gh",
|
|
4186
|
-
"discord"
|
|
4187
|
-
]);
|
|
4188
|
-
const readChannelConnectors = (dir, channelId) => {
|
|
4189
|
-
const settingsPath = join(dir, "settings.json");
|
|
4190
|
-
if (!existsSync(settingsPath)) return null;
|
|
4191
|
-
const raw = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
4192
|
-
const parsed = settingsSchema.safeParse(raw);
|
|
4193
|
-
if (!parsed.success) return null;
|
|
4194
|
-
const channel = parsed.data.channels.find((c) => c.id === channelId);
|
|
4195
|
-
if (!channel) return null;
|
|
4196
|
-
const connectors = channel.connectors.filter((c) => TOOL_CONNECTOR_TYPES.has(c.type)).map((c) => ({
|
|
4197
|
-
name: c.name,
|
|
4198
|
-
type: c.type
|
|
4199
|
-
}));
|
|
4200
|
-
return {
|
|
4201
|
-
channelName: channel.name,
|
|
4202
|
-
connectors
|
|
4203
|
-
};
|
|
4204
|
-
};
|
|
4205
|
-
//#endregion
|
|
4206
|
-
//#region lib/engine/mcp/read-gateway-token.ts
|
|
4207
|
-
const readGatewayToken = (dir) => {
|
|
4208
|
-
const fromEnv = process.env.FUNNEL_GATEWAY_TOKEN;
|
|
4209
|
-
if (fromEnv && fromEnv.length > 0) return fromEnv;
|
|
4210
|
-
const path = join(dir, "gateway.token");
|
|
4211
|
-
if (!existsSync(path)) return null;
|
|
4212
|
-
const value = readFileSync(path, "utf-8").trim();
|
|
4213
|
-
return value.length > 0 ? value : null;
|
|
4214
|
-
};
|
|
4215
|
-
//#endregion
|
|
4216
|
-
//#region lib/engine/mcp/usage-hint-for-type.ts
|
|
4217
|
-
const usageHintForType = (type) => {
|
|
4218
|
-
if (type === "slack") return [
|
|
4219
|
-
"Slack Web API.",
|
|
4220
|
-
"To reply in the same thread: method=POST path=chat.postMessage body={ channel: meta.channel_id, text: \"...\", thread_ts: meta.thread_ts }",
|
|
4221
|
-
"To react: method=POST path=reactions.add body={ channel: meta.channel_id, timestamp: meta.thread_ts, name: \"thumbsup\" }",
|
|
4222
|
-
"Use meta fields from the incoming event: channel_id (Slack channel), thread_ts (thread anchor), user_id (sender)."
|
|
4223
|
-
].join(" ");
|
|
4224
|
-
if (type === "discord") return [
|
|
4225
|
-
"Discord REST API.",
|
|
4226
|
-
"To reply: method=POST path=/channels/<meta.channel_id>/messages body={ content: \"...\" }",
|
|
4227
|
-
"Use meta fields: channel_id (Discord channel), user_id (sender), guild_id."
|
|
4228
|
-
].join(" ");
|
|
4229
|
-
if (type === "gh") return [
|
|
4230
|
-
"GitHub REST via gh CLI.",
|
|
4231
|
-
"To comment: method=POST path=repos/<meta.repository>/issues/<number>/comments body={ body: \"...\" }",
|
|
4232
|
-
"Parse <number> from meta.subject_url. meta fields: repository (owner/repo), subject_type, subject_url, reason."
|
|
4233
|
-
].join(" ");
|
|
4234
|
-
return "Generic adapter call.";
|
|
4235
|
-
};
|
|
4236
|
-
//#endregion
|
|
4237
|
-
//#region lib/engine/mcp/channel-server.ts
|
|
4238
|
-
const DEFAULT_FUNNEL_DIR = join(homedir(), ".funnel");
|
|
4239
|
-
const BUILTIN_TOOL_NAMES = ["fnl_status", "fnl_debug"];
|
|
4240
|
-
const isBuiltinTool = (name) => BUILTIN_TOOL_NAMES.includes(name);
|
|
4241
|
-
const readAllChannels = (dir) => {
|
|
4242
|
-
const settingsPath = join(dir, "settings.json");
|
|
4243
|
-
if (!existsSync(settingsPath)) return [];
|
|
4244
|
-
try {
|
|
4245
|
-
const raw = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
4246
|
-
const parsed = settingsSchema.safeParse(raw);
|
|
4247
|
-
if (!parsed.success) return [];
|
|
4248
|
-
return parsed.data.channels.map((c) => ({
|
|
4249
|
-
id: c.id,
|
|
4250
|
-
name: c.name
|
|
4251
|
-
}));
|
|
4252
|
-
} catch {
|
|
4253
|
-
return [];
|
|
4254
|
-
}
|
|
4255
|
-
};
|
|
4256
|
-
const startChannelServer = async (options = {}) => {
|
|
4257
|
-
const dir = options.dir ?? DEFAULT_FUNNEL_DIR;
|
|
4258
|
-
const gatewayBaseUrl = options.gatewayUrl ?? process.env.FUNNEL_GATEWAY_URL ?? `http://127.0.0.1:${resolveFunnelPort()}`;
|
|
4259
|
-
const gatewayWsUrl = `${gatewayBaseUrl.replace(/^http/, "ws")}/ws`;
|
|
4260
|
-
const channelId = options.channelId ?? process.env.FUNNEL_CHANNEL_ID;
|
|
4261
|
-
const channel = channelId ? readChannelConnectors(dir, channelId) : null;
|
|
4262
|
-
const token = options.token ?? readGatewayToken(dir);
|
|
4263
|
-
const allChannels = readAllChannels(dir);
|
|
4264
|
-
const currentChannelName = channel?.channelName ?? null;
|
|
4265
|
-
const channelContext = allChannels.length > 0 ? [
|
|
4266
|
-
"",
|
|
4267
|
-
"Configured channels (use as the `channel` argument to fnl_debug):",
|
|
4268
|
-
...allChannels.map((ch) => ` ${ch.name}${ch.name === currentChannelName ? " ← this session" : ""}`)
|
|
4269
|
-
].join("\n") : "";
|
|
4270
|
-
const server = new Server({
|
|
4271
|
-
name: FUNNEL_MCP_NAME,
|
|
4272
|
-
version: "1.0.0"
|
|
4273
|
-
}, {
|
|
4274
|
-
capabilities: {
|
|
4275
|
-
experimental: { "claude/channel": {} },
|
|
4276
|
-
tools: {}
|
|
4277
|
-
},
|
|
4278
|
-
instructions: [
|
|
4279
|
-
`Events arrive as notifications (method: notifications/claude/channel) with two fields:`,
|
|
4280
|
-
` content — the event payload as a JSON string (parse it to read the message)`,
|
|
4281
|
-
` meta — key/value strings describing the event`,
|
|
4282
|
-
"",
|
|
4283
|
-
"meta fields by event_type:",
|
|
4284
|
-
" slack: event_type=slack channel_id=C… thread_ts=1234.5678 user_id=U… mentioned=true|false",
|
|
4285
|
-
" gh: event_type=gh repository=owner/repo subject_type=Issue|PullRequest subject_url=… reason=…",
|
|
4286
|
-
" discord: event_type=discord channel_id=… user_id=… guild_id=… mentioned=true|false",
|
|
4287
|
-
" schedule: event_type=schedule entry_id=…",
|
|
4288
|
-
"",
|
|
4289
|
-
"To reply to a Slack message in the same thread, call the connector tool with:",
|
|
4290
|
-
` method: POST`,
|
|
4291
|
-
` path: chat.postMessage`,
|
|
4292
|
-
` body: { channel: meta.channel_id, text: "your reply", thread_ts: meta.thread_ts }`,
|
|
4293
|
-
"",
|
|
4294
|
-
"To comment on a GitHub issue/PR (extract from subject_url in meta):",
|
|
4295
|
-
` method: POST`,
|
|
4296
|
-
` path: repos/<meta.repository>/issues/<number>/comments (parse number from meta.subject_url)`,
|
|
4297
|
-
` body: { body: "your reply" }`,
|
|
4298
|
-
"",
|
|
4299
|
-
"Built-in diagnostic tools — call proactively when events seem missing or delayed:",
|
|
4300
|
-
" fnl_status — gateway running state, all listeners alive/dead, Claude WS clients",
|
|
4301
|
-
" fnl_debug — per-channel diagnosis with last 10 events, rootCause, suggestedActions",
|
|
4302
|
-
" omit channel arg to diagnose all channels; check summary.suggestedActions first",
|
|
4303
|
-
channelContext
|
|
4304
|
-
].join("\n")
|
|
4305
|
-
});
|
|
4306
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
4307
|
-
const connectorTools = (channel?.connectors ?? []).map((c) => ({
|
|
4308
|
-
name: c.name,
|
|
4309
|
-
description: `Call the "${c.name}" (${c.type}) connector. ${usageHintForType(c.type)}`,
|
|
4310
|
-
inputSchema: {
|
|
4311
|
-
type: "object",
|
|
4312
|
-
properties: {
|
|
4313
|
-
method: {
|
|
4314
|
-
type: "string",
|
|
4315
|
-
description: "HTTP verb or API method (e.g. POST, chat.postMessage)"
|
|
4316
|
-
},
|
|
4317
|
-
path: {
|
|
4318
|
-
type: "string",
|
|
4319
|
-
description: "API path or method name (adapter-specific)"
|
|
4320
|
-
},
|
|
4321
|
-
body: {
|
|
4322
|
-
type: "object",
|
|
4323
|
-
description: "Request body / params (adapter-specific)"
|
|
4324
|
-
}
|
|
4325
|
-
},
|
|
4326
|
-
required: ["method", "path"]
|
|
4327
|
-
}
|
|
4328
|
-
}));
|
|
4329
|
-
const channelEnum = allChannels.length > 0 ? allChannels.map((ch) => ch.name) : void 0;
|
|
4330
|
-
const builtinTools = [{
|
|
4331
|
-
name: "fnl_status",
|
|
4332
|
-
description: "Return the current funnel gateway status as JSON — gateway running state, listener alive/dead per channel, and connected Claude WS clients. Call this when you need to check whether the gateway is up or why events stopped arriving.",
|
|
4333
|
-
inputSchema: {
|
|
4334
|
-
type: "object",
|
|
4335
|
-
properties: {}
|
|
4336
|
-
}
|
|
4337
|
-
}, {
|
|
4338
|
-
name: "fnl_debug",
|
|
4339
|
-
description: "Return a full channel diagnosis as JSON — gateway health, listener state, Claude WS connection, last 10 inbound events with outcome, connectionErrors (when listener is dead), and diagnosis.rootCause. Call this first when debugging missing events. Omit `channel` to diagnose all channels at once.",
|
|
4340
|
-
inputSchema: {
|
|
4341
|
-
type: "object",
|
|
4342
|
-
properties: { channel: channelEnum ? {
|
|
4343
|
-
type: "string",
|
|
4344
|
-
description: `Channel name to inspect. One of: ${channelEnum.join(", ")}. Omit to get all channels.`,
|
|
4345
|
-
enum: channelEnum
|
|
4346
|
-
} : {
|
|
4347
|
-
type: "string",
|
|
4348
|
-
description: "Channel name to inspect. Omit to get all channels."
|
|
4349
|
-
} }
|
|
4350
|
-
}
|
|
4351
|
-
}];
|
|
4352
|
-
return { tools: [...connectorTools, ...builtinTools] };
|
|
4353
|
-
});
|
|
4354
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
4355
|
-
const toolName = request.params.name;
|
|
4356
|
-
if (isBuiltinTool(toolName)) return handleBuiltinTool(toolName, request.params.arguments, gatewayBaseUrl, token, allChannels);
|
|
4357
|
-
if (!channel) throw new Error("FUNNEL_CHANNEL_ID is not set or channel not found in settings.json");
|
|
4358
|
-
const args = request.params.arguments ?? {};
|
|
4359
|
-
const method = typeof args.method === "string" ? args.method : "";
|
|
4360
|
-
const path = typeof args.path === "string" ? args.path : "";
|
|
4361
|
-
const body = args.body ?? {};
|
|
4362
|
-
if (!method || !path) throw new Error("`method` and `path` are required");
|
|
4363
|
-
const url = `${gatewayBaseUrl}/channels/${encodeURIComponent(channel.channelName)}/connectors/${encodeURIComponent(toolName)}/call`;
|
|
4364
|
-
const headers = { "content-type": "application/json" };
|
|
4365
|
-
if (token) headers.authorization = `Bearer ${token}`;
|
|
4366
|
-
const res = await fetch(url, {
|
|
4367
|
-
method: "POST",
|
|
4368
|
-
headers,
|
|
4369
|
-
body: JSON.stringify({
|
|
4370
|
-
method,
|
|
4371
|
-
path,
|
|
4372
|
-
body
|
|
4373
|
-
})
|
|
4374
|
-
});
|
|
4375
|
-
const text = await res.text();
|
|
4376
|
-
if (!res.ok) throw new Error(`gateway call failed (${res.status}): ${text}`);
|
|
4377
|
-
return { content: [{
|
|
4378
|
-
type: "text",
|
|
4379
|
-
text
|
|
4380
|
-
}] };
|
|
4381
|
-
});
|
|
4382
|
-
const transport = new StdioServerTransport();
|
|
4383
|
-
await server.connect(transport);
|
|
4384
|
-
if (!channelId) return;
|
|
4385
|
-
new FunnelChannelSubscriber({
|
|
4386
|
-
server,
|
|
4387
|
-
baseUrl: `${gatewayWsUrl}?channel=${encodeURIComponent(channelId)}`,
|
|
4388
|
-
protocols: token ? [`funnel.token.${token}`] : void 0
|
|
4389
|
-
}).start();
|
|
4390
|
-
};
|
|
4391
|
-
const handleBuiltinTool = async (name, args, gatewayBaseUrl, token, allChannels) => {
|
|
4392
|
-
const headers = {};
|
|
4393
|
-
if (token) headers.authorization = `Bearer ${token}`;
|
|
4394
|
-
if (name === "fnl_status") {
|
|
4395
|
-
const res = await fetch(`${gatewayBaseUrl}/status`, { headers }).catch(() => null);
|
|
4396
|
-
if (!res) return { content: [{
|
|
4397
|
-
type: "text",
|
|
4398
|
-
text: JSON.stringify({
|
|
4399
|
-
running: false,
|
|
4400
|
-
error: "gateway unreachable",
|
|
4401
|
-
hint: "run: fnl gateway start",
|
|
4402
|
-
knownChannels: allChannels.map((ch) => ch.name)
|
|
4403
|
-
})
|
|
4404
|
-
}] };
|
|
4405
|
-
const body = await res.json();
|
|
4406
|
-
return { content: [{
|
|
4407
|
-
type: "text",
|
|
4408
|
-
text: JSON.stringify(body)
|
|
4409
|
-
}] };
|
|
4410
|
-
}
|
|
4411
|
-
const channelArg = typeof args?.channel === "string" ? args.channel : null;
|
|
4412
|
-
const url = channelArg ? `${gatewayBaseUrl}/debug?channel=${encodeURIComponent(channelArg)}` : `${gatewayBaseUrl}/debug`;
|
|
4413
|
-
const res = await fetch(url, { headers }).catch(() => null);
|
|
4414
|
-
if (!res) return { content: [{
|
|
4415
|
-
type: "text",
|
|
4416
|
-
text: JSON.stringify({
|
|
4417
|
-
gateway: { running: false },
|
|
4418
|
-
channels: allChannels.map((ch) => ({
|
|
4419
|
-
id: ch.id,
|
|
4420
|
-
name: ch.name,
|
|
4421
|
-
diagnosis: {
|
|
4422
|
-
status: "error",
|
|
4423
|
-
message: "gateway is not running",
|
|
4424
|
-
nextAction: "fnl gateway start",
|
|
4425
|
-
rootCause: null
|
|
4426
|
-
}
|
|
4427
|
-
}))
|
|
4428
|
-
})
|
|
4429
|
-
}] };
|
|
4430
|
-
const body = await res.json();
|
|
4431
|
-
return { content: [{
|
|
4432
|
-
type: "text",
|
|
4433
|
-
text: JSON.stringify(body)
|
|
4434
|
-
}] };
|
|
4435
|
-
};
|
|
4436
|
-
//#endregion
|
|
4437
|
-
//#region lib/engine/local-config/local-config-json-schema.ts
|
|
4438
|
-
/**
|
|
4439
|
-
* Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
|
|
4440
|
-
* `$schema` references in committed `funnel.json` files so editors can give
|
|
4441
|
-
* autocomplete and validation for channels[] (transport) and profiles[]
|
|
4442
|
-
* (launch recipe) without anyone hand-maintaining a separate schema.
|
|
4443
|
-
*/
|
|
4444
|
-
const funnelJsonSchema = () => {
|
|
4445
|
-
return {
|
|
4446
|
-
...z.toJSONSchema(localConfigSchema, { target: "draft-2020-12" }),
|
|
4447
|
-
title: "Funnel per-repo launch config",
|
|
4448
|
-
description: "Used by `fnl claude` to declare channels (transport: connectors to materialize into ~/.funnel/settings.json on launch) and profiles (launch recipe: options / env / resume) bound to those channels."
|
|
4449
|
-
};
|
|
4450
|
-
};
|
|
4451
|
-
//#endregion
|
|
4452
3995
|
//#region lib/engine/logger/node-logger.ts
|
|
4453
3996
|
const defaultLogFile = () => join(funnelTmpDir(), "funnel.log");
|
|
4454
3997
|
var NodeFunnelLogger = class extends FunnelLogger {
|
|
@@ -4489,26 +4032,6 @@ var NoopFunnelLogger = class extends FunnelLogger {
|
|
|
4489
4032
|
error() {}
|
|
4490
4033
|
};
|
|
4491
4034
|
//#endregion
|
|
4492
|
-
//#region lib/engine/token-prompter/memory-token-prompter.ts
|
|
4493
|
-
/**
|
|
4494
|
-
* Pre-seeded answers keyed by prompt label. Tests configure the map up front;
|
|
4495
|
-
* unmapped labels throw so the test surfaces unexpected prompts loudly.
|
|
4496
|
-
*/
|
|
4497
|
-
var MemoryFunnelTokenPrompter = class extends FunnelTokenPrompter {
|
|
4498
|
-
answers;
|
|
4499
|
-
asked = [];
|
|
4500
|
-
constructor(props = {}) {
|
|
4501
|
-
super();
|
|
4502
|
-
this.answers = new Map(Object.entries(props.answers ?? {}));
|
|
4503
|
-
}
|
|
4504
|
-
async promptSecret(label) {
|
|
4505
|
-
this.asked.push(label);
|
|
4506
|
-
const answer = this.answers.get(label);
|
|
4507
|
-
if (answer === void 0) throw new Error(`no answer seeded for prompt "${label}"`);
|
|
4508
|
-
return answer;
|
|
4509
|
-
}
|
|
4510
|
-
};
|
|
4511
|
-
//#endregion
|
|
4512
4035
|
//#region lib/gateway/memory-funnel-event-log.ts
|
|
4513
4036
|
/**
|
|
4514
4037
|
* In-process `FunnelEventLog` backed by a plain array. Used by tests and by
|
|
@@ -5464,7 +4987,6 @@ modes:
|
|
|
5464
4987
|
fanout every connected WS client receives every event (default)
|
|
5465
4988
|
exclusive each event is delivered to exactly one connected client (round-robin)
|
|
5466
4989
|
|
|
5467
|
-
tap=all clients (TUI dashboard, debugging) always receive regardless of mode.
|
|
5468
4990
|
`), (c) => {
|
|
5469
4991
|
const param = c.req.valid("param");
|
|
5470
4992
|
c.env.funnel.channels.setDelivery(param.channel, param.mode);
|
|
@@ -5661,19 +5183,19 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
|
5661
5183
|
channel: z.string().optional()
|
|
5662
5184
|
}).passthrough(), claudeHelp), async (c) => {
|
|
5663
5185
|
const query = c.req.valid("query");
|
|
5664
|
-
const funnel = c.env
|
|
5186
|
+
const { funnel, claude, profiles, localConfig, localConfigSync } = c.env;
|
|
5665
5187
|
const userArgs = queryToCliArgs(c.req.url, RESERVED_KEYS$1);
|
|
5666
5188
|
if (query.channel && !query.profile) {
|
|
5667
|
-
const exitCode = await
|
|
5189
|
+
const exitCode = await claude.launch({
|
|
5668
5190
|
channel: query.channel,
|
|
5669
5191
|
userArgs
|
|
5670
5192
|
});
|
|
5671
5193
|
process.exit(exitCode);
|
|
5672
5194
|
}
|
|
5673
5195
|
if (query.profile) {
|
|
5674
|
-
const profile =
|
|
5196
|
+
const profile = profiles.get(query.profile);
|
|
5675
5197
|
if (!profile) throw new HTTPException(404, { message: `profile "${query.profile}" not found` });
|
|
5676
|
-
const exitCode = await
|
|
5198
|
+
const exitCode = await claude.launch({
|
|
5677
5199
|
channel: profile.channelId,
|
|
5678
5200
|
cwd: profile.path,
|
|
5679
5201
|
userArgs,
|
|
@@ -5685,24 +5207,24 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
|
5685
5207
|
process.exit(exitCode);
|
|
5686
5208
|
}
|
|
5687
5209
|
const cwd = process.cwd();
|
|
5688
|
-
const local =
|
|
5210
|
+
const local = localConfig.read(cwd);
|
|
5689
5211
|
if (local) {
|
|
5690
5212
|
const picked = query.channel !== void 0 ? local.channels.find((c) => c.name === query.channel) : local.channels[0];
|
|
5691
5213
|
if (!picked) throw new HTTPException(404, { message: query.channel ? `channel "${query.channel}" is not declared in funnel.json` : `funnel.json declares no channels` });
|
|
5692
|
-
const synced = await
|
|
5214
|
+
const synced = await localConfigSync.ensure(picked);
|
|
5693
5215
|
for (const outcome of synced.touched) if (outcome.changed) await funnel.listeners.restart(picked.name, outcome.name);
|
|
5694
5216
|
else await funnel.listeners.start(picked.name, outcome.name);
|
|
5695
5217
|
for (const name of synced.removed) await funnel.listeners.stop(picked.name, name);
|
|
5696
|
-
const exitCode = await
|
|
5218
|
+
const exitCode = await claude.launch({
|
|
5697
5219
|
channel: picked.name,
|
|
5698
5220
|
cwd,
|
|
5699
5221
|
userArgs
|
|
5700
5222
|
});
|
|
5701
5223
|
process.exit(exitCode);
|
|
5702
5224
|
}
|
|
5703
|
-
const defaultProfile =
|
|
5225
|
+
const defaultProfile = profiles.getDefault();
|
|
5704
5226
|
if (!defaultProfile) return c.text(claudeHelp);
|
|
5705
|
-
const exitCode = await
|
|
5227
|
+
const exitCode = await claude.launch({
|
|
5706
5228
|
channel: defaultProfile.channelId,
|
|
5707
5229
|
cwd: defaultProfile.path,
|
|
5708
5230
|
userArgs,
|
|
@@ -6195,7 +5717,7 @@ const buildChannelReport = async (targetChannel, gatewayStatus, gatewayBodyOrNul
|
|
|
6195
5717
|
errors: l.errors,
|
|
6196
5718
|
lastEventAt: l.lastEventAt
|
|
6197
5719
|
}));
|
|
6198
|
-
baseReport.claudeClients = gatewayBodyOrNull.clients.filter((cl) =>
|
|
5720
|
+
baseReport.claudeClients = gatewayBodyOrNull.clients.filter((cl) => cl.channelName === targetChannelName).length;
|
|
6199
5721
|
}
|
|
6200
5722
|
if (store) {
|
|
6201
5723
|
const evRows = queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT seq, ts, type, outcome, payload FROM processed WHERE channel_id = ? ORDER BY seq DESC LIMIT ?", [targetChannel.id, limit]);
|
|
@@ -6705,15 +6227,7 @@ examples:
|
|
|
6705
6227
|
funnel gateway run
|
|
6706
6228
|
funnel gateway run --no-caffeine`), async (c) => {
|
|
6707
6229
|
const query = c.req.valid("query");
|
|
6708
|
-
const
|
|
6709
|
-
const gatewayScript = resolveDaemonScript();
|
|
6710
|
-
const command = query["no-caffeine"] !== "true" && process.platform === "darwin" ? [
|
|
6711
|
-
"caffeinate",
|
|
6712
|
-
"-is",
|
|
6713
|
-
"bun",
|
|
6714
|
-
gatewayScript
|
|
6715
|
-
] : ["bun", gatewayScript];
|
|
6716
|
-
const exitCode = await funnel.process.attach(command);
|
|
6230
|
+
const exitCode = await c.env.funnel.runGatewayForeground({ caffeinate: query["no-caffeine"] !== "true" });
|
|
6717
6231
|
process.exit(exitCode);
|
|
6718
6232
|
});
|
|
6719
6233
|
//#endregion
|
|
@@ -6857,10 +6371,11 @@ const profilesAddHandler = factory.createHandlers(zValidator$1("param", z.object
|
|
|
6857
6371
|
const param = c.req.valid("param");
|
|
6858
6372
|
const query = c.req.valid("query");
|
|
6859
6373
|
const funnel = c.env.funnel;
|
|
6374
|
+
const { profiles, claude } = c.env;
|
|
6860
6375
|
const channel = funnel.channels.get(query.channel);
|
|
6861
6376
|
if (!channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
|
|
6862
6377
|
const recipe = parseProfileRecipe(query);
|
|
6863
|
-
|
|
6378
|
+
profiles.add({
|
|
6864
6379
|
name: param.profile,
|
|
6865
6380
|
path: query.path,
|
|
6866
6381
|
channelId: channel.id,
|
|
@@ -6876,7 +6391,9 @@ usage: funnel profiles <name> as-default
|
|
|
6876
6391
|
|
|
6877
6392
|
the first profile in the list is treated as the default for fnl claude.`), (c) => {
|
|
6878
6393
|
const param = c.req.valid("param");
|
|
6879
|
-
c.env.funnel
|
|
6394
|
+
c.env.funnel;
|
|
6395
|
+
const { profiles, claude } = c.env;
|
|
6396
|
+
profiles.asDefault(param.profile);
|
|
6880
6397
|
return c.text(`profile "${param.profile}" is now the default`);
|
|
6881
6398
|
});
|
|
6882
6399
|
//#endregion
|
|
@@ -6902,7 +6419,9 @@ const profilesRenameHandler = factory.createHandlers(zValidator$1("param", z.obj
|
|
|
6902
6419
|
newName: z.string()
|
|
6903
6420
|
})), zValidator$1("query", z.object({})), (c) => {
|
|
6904
6421
|
const param = c.req.valid("param");
|
|
6905
|
-
c.env.funnel
|
|
6422
|
+
c.env.funnel;
|
|
6423
|
+
const { profiles, claude } = c.env;
|
|
6424
|
+
profiles.rename(param.profile, param.newName);
|
|
6906
6425
|
return c.text(`renamed profile "${param.profile}" to "${param.newName}"`);
|
|
6907
6426
|
});
|
|
6908
6427
|
//#endregion
|
|
@@ -6914,10 +6433,11 @@ usage: funnel profiles <name> run [additional claude args...]
|
|
|
6914
6433
|
const RESERVED_KEYS = [];
|
|
6915
6434
|
const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({}).passthrough(), launchHelp), async (c) => {
|
|
6916
6435
|
const param = c.req.valid("param");
|
|
6917
|
-
|
|
6918
|
-
const
|
|
6436
|
+
c.env.funnel;
|
|
6437
|
+
const { profiles, claude } = c.env;
|
|
6438
|
+
const profile = profiles.get(param.profile);
|
|
6919
6439
|
if (!profile) throw new HTTPException(404, { message: `profile "${param.profile}" not found` });
|
|
6920
|
-
const exitCode = await
|
|
6440
|
+
const exitCode = await claude.launch({
|
|
6921
6441
|
channel: profile.channelId,
|
|
6922
6442
|
cwd: profile.path,
|
|
6923
6443
|
userArgs: queryToCliArgs(c.req.url, RESERVED_KEYS),
|
|
@@ -6938,7 +6458,9 @@ const profilesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$1));
|
|
|
6938
6458
|
//#region lib/cli/routes/profiles.remove.$profile.ts
|
|
6939
6459
|
const profilesRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({})), (c) => {
|
|
6940
6460
|
const param = c.req.valid("param");
|
|
6941
|
-
c.env.funnel
|
|
6461
|
+
c.env.funnel;
|
|
6462
|
+
const { profiles, claude } = c.env;
|
|
6463
|
+
profiles.remove(param.profile);
|
|
6942
6464
|
return c.text(`removed profile "${param.profile}"`);
|
|
6943
6465
|
});
|
|
6944
6466
|
//#endregion
|
|
@@ -6972,10 +6494,11 @@ const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object
|
|
|
6972
6494
|
const param = c.req.valid("param");
|
|
6973
6495
|
const query = c.req.valid("query");
|
|
6974
6496
|
const funnel = c.env.funnel;
|
|
6497
|
+
const { profiles, claude } = c.env;
|
|
6975
6498
|
const channel = query.channel !== void 0 ? funnel.channels.get(query.channel) : null;
|
|
6976
6499
|
if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
|
|
6977
6500
|
const recipe = parseProfileRecipe(query);
|
|
6978
|
-
|
|
6501
|
+
profiles.update(param.profile, {
|
|
6979
6502
|
path: query.path,
|
|
6980
6503
|
channelId: channel?.id,
|
|
6981
6504
|
options: recipe.options,
|
|
@@ -7006,9 +6529,11 @@ examples:
|
|
|
7006
6529
|
funnel profiles add cto --path /repo/myapp --channel prod-inbox --agent pm --options "--brief"
|
|
7007
6530
|
funnel profiles cto as-default
|
|
7008
6531
|
funnel profiles cto run`), (c) => {
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
const
|
|
6532
|
+
c.env.funnel;
|
|
6533
|
+
const { profiles } = c.env;
|
|
6534
|
+
const profileList = profiles.list();
|
|
6535
|
+
if (profileList.length === 0) return c.text("no profiles");
|
|
6536
|
+
const lines = profileList.map((profile, index) => {
|
|
7012
6537
|
const tag = index === 0 ? " (default)" : "";
|
|
7013
6538
|
const recipe = profile.options.length > 0 ? `, options=${profile.options.join(" ")}` : "";
|
|
7014
6539
|
const session = profile.resume ? "" : ", resume=false";
|
|
@@ -7016,6 +6541,21 @@ examples:
|
|
|
7016
6541
|
});
|
|
7017
6542
|
return c.text(lines.join("\n"));
|
|
7018
6543
|
});
|
|
6544
|
+
//#endregion
|
|
6545
|
+
//#region lib/engine/local-config/local-config-json-schema.ts
|
|
6546
|
+
/**
|
|
6547
|
+
* Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
|
|
6548
|
+
* `$schema` references in committed `funnel.json` files so editors can give
|
|
6549
|
+
* autocomplete and validation for channels[] (transport) and profiles[]
|
|
6550
|
+
* (launch recipe) without anyone hand-maintaining a separate schema.
|
|
6551
|
+
*/
|
|
6552
|
+
const funnelJsonSchema = () => {
|
|
6553
|
+
return {
|
|
6554
|
+
...z.toJSONSchema(localConfigSchema, { target: "draft-2020-12" }),
|
|
6555
|
+
title: "Funnel per-repo launch config",
|
|
6556
|
+
description: "Used by `fnl claude` to declare channels (transport: connectors to materialize into ~/.funnel/settings.json on launch) and profiles (launch recipe: options / env / resume) bound to those channels."
|
|
6557
|
+
};
|
|
6558
|
+
};
|
|
7019
6559
|
const schemaHandler = factory.createHandlers(zValidator$1("query", z.object({}), `funnel schema — print the JSON Schema for funnel.json
|
|
7020
6560
|
|
|
7021
6561
|
usage: funnel schema
|
|
@@ -7060,9 +6600,9 @@ const isGatewayStatus = (value) => {
|
|
|
7060
6600
|
if (!("listeners" in value) || !Array.isArray(value.listeners)) return false;
|
|
7061
6601
|
return true;
|
|
7062
6602
|
};
|
|
7063
|
-
const buildStatusLines = async (funnel) => {
|
|
6603
|
+
const buildStatusLines = async (funnel, profiles) => {
|
|
7064
6604
|
const channels = funnel.channels.list();
|
|
7065
|
-
const
|
|
6605
|
+
const profileList = profiles.list();
|
|
7066
6606
|
const gatewayStatus = funnel.gateway.getStatus();
|
|
7067
6607
|
const lines = [];
|
|
7068
6608
|
lines.push("= funnel status =");
|
|
@@ -7088,7 +6628,6 @@ const buildStatusLines = async (funnel) => {
|
|
|
7088
6628
|
const listenerAliveByChannel = /* @__PURE__ */ new Map();
|
|
7089
6629
|
if (gatewayData) {
|
|
7090
6630
|
for (const client of gatewayData.clients) {
|
|
7091
|
-
if (client.tapAll) continue;
|
|
7092
6631
|
const key = client.channelName ?? client.channel;
|
|
7093
6632
|
clientsByChannel.set(key, (clientsByChannel.get(key) ?? 0) + 1);
|
|
7094
6633
|
}
|
|
@@ -7109,8 +6648,8 @@ const buildStatusLines = async (funnel) => {
|
|
|
7109
6648
|
lines.push(` ${indicator} ${paddedName} [${connectorLabel}]${claudeLabel}`);
|
|
7110
6649
|
}
|
|
7111
6650
|
lines.push("");
|
|
7112
|
-
lines.push(`profiles: ${
|
|
7113
|
-
for (const [index, profile] of
|
|
6651
|
+
lines.push(`profiles: ${profileList.length}`);
|
|
6652
|
+
for (const [index, profile] of profileList.entries()) {
|
|
7114
6653
|
const tag = index === 0 ? " (default)" : "";
|
|
7115
6654
|
const channel = funnel.channels.getById(profile.channelId);
|
|
7116
6655
|
const channelLabel = channel ? channel.name : `id:${profile.channelId}`;
|
|
@@ -7131,11 +6670,11 @@ const statusHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
|
7131
6670
|
const isWatch = query.watch === "true" || query.watch === "";
|
|
7132
6671
|
const intervalSec = Math.min(60, Math.max(1, query.interval ? Number(query.interval) : 3));
|
|
7133
6672
|
if (!isWatch) {
|
|
7134
|
-
const lines = await buildStatusLines(funnel);
|
|
6673
|
+
const lines = await buildStatusLines(funnel, c.env.profiles);
|
|
7135
6674
|
return c.text(lines.join("\n"));
|
|
7136
6675
|
}
|
|
7137
6676
|
const render = async () => {
|
|
7138
|
-
const lines = await buildStatusLines(funnel);
|
|
6677
|
+
const lines = await buildStatusLines(funnel, c.env.profiles);
|
|
7139
6678
|
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
7140
6679
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
7141
6680
|
process.stdout.write(lines.join("\n"));
|
|
@@ -7177,4 +6716,4 @@ const routes = factory.createApp().onError((error, c) => {
|
|
|
7177
6716
|
return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
|
|
7178
6717
|
}).get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...channelsAddHelpHandler).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...channelsRemoveHelpHandler).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...channelsRenameHelpHandler).post("/channels/:channel/rename", ...channelsChannelRenameHelpHandler).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...channelsPublishHelpHandler).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel/validate", ...channelsValidateHandler).get("/channels/validate", ...channelsValidateHelpHandler).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...channelsConnectorsAddHelpHandler).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...channelsConnectorsRemoveHelpHandler).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...channelsConnectorsSetHelpHandler).post("/channels/:channel/connectors/set/:connector", ...channelsConnectorsSetHandler).post("/channels/:channel/connectors/rename/:connector/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/:connector/rename/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/rename", ...channelsConnectorsRenameHelpHandler).post("/channels/:channel/connectors/:connector/rename", ...channelsConnectorRenameHelpHandler).post("/channels/:channel/connectors/:connector/request", ...channelsConnectorsRequestHandler).get("/channels/:channel/connectors/:connector", ...channelsConnectorsShowHandler).get("/channels/:channel/connectors/:connector/schedules", ...channelsConnectorsSchedulesGroupHandler).post("/channels/:channel/connectors/:connector/schedules/add", ...channelsConnectorSchedulesAddHelpHandler).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...channelsConnectorSchedulesRemoveHelpHandler).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...profilesAddHelpHandler).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...profilesSetHelpHandler).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...profilesRemoveHelpHandler).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...profilesRenameHelpHandler).post("/profiles/:profile/rename", ...profilesProfileRenameHelpHandler).post("/profiles/:profile/as-default", ...profilesAsDefaultHandler).get("/profiles/:profile/run", ...profilesLaunchHandler).get("/profiles/:profile", ...profilesLaunchHandler).get("/gateway", ...gatewayGroupHandler).get("/gateway/status", ...gatewayStatusHandler).get("/gateway/start", ...gatewayStartHandler).get("/gateway/stop", ...gatewayStopHandler).get("/gateway/restart", ...gatewayRestartHandler).get("/gateway/run", ...gatewayRunHandler).get("/gateway/logs", ...gatewayLogsHandler).get("/gateway/sql", ...gatewaySqlHandler).get("/gateway/listeners", ...gatewayListenersHandler).get("/debug", ...debugHandler).get("/debug/events", ...debugEventsHandler).get("/debug/dropped", ...debugDroppedHandler).get("/debug/errors", ...debugErrorsHandler).get("/debug/replay", ...debugReplayHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
|
|
7179
6718
|
//#endregion
|
|
7180
|
-
export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR,
|
|
6719
|
+
export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLogger, FunnelProcessRunner, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, channelConfigSchema, channelDeliveryModeSchema, routes as cliRoutes, connectorConfigSchema, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, createSettings, discordConnectorSchema, factory, funnelEventSchema, ghConnectorSchema, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, resolveFunnelDir, resolveFunnelPort, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, toRequest };
|