@interactive-inc/claude-funnel 0.41.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 +155 -286
- package/dist/index.js +520 -982
- 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,184 +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
|
-
port: this.props.port
|
|
4047
|
-
});
|
|
4048
|
-
return this.memos.gateway;
|
|
4049
|
-
}
|
|
4050
|
-
/** Read / generate the daemon's gateway token (mode 0600 file under `dir`). */
|
|
4051
|
-
get gatewayToken() {
|
|
4052
|
-
if (!this.memos.gatewayToken) this.memos.gatewayToken = new FunnelGatewayToken({
|
|
4053
|
-
fs: this.fs,
|
|
4054
|
-
dir: this.paths.dir
|
|
4055
|
-
});
|
|
4056
|
-
return this.memos.gatewayToken;
|
|
4057
|
-
}
|
|
4058
|
-
/**
|
|
4059
|
-
* HTTP client for `POST /channels/:channel/publish` on the running gateway
|
|
4060
|
-
* daemon. Use it to push arbitrary content into a channel from outside any
|
|
4061
|
-
* connector. Returns `{ state: "offline" }` if the daemon isn't up.
|
|
4062
|
-
*/
|
|
4063
|
-
get publisher() {
|
|
4064
|
-
if (!this.memos.publisher) {
|
|
4065
|
-
const gateway = this.gateway;
|
|
4066
|
-
const token = this.gatewayToken;
|
|
4067
|
-
this.memos.publisher = new FunnelChannelPublisher({
|
|
4068
|
-
port: gateway.getPort(),
|
|
4069
|
-
isDaemonRunning: () => gateway.isRunning(),
|
|
4070
|
-
getToken: () => token.read()
|
|
4071
|
-
});
|
|
4072
|
-
}
|
|
4073
|
-
return this.memos.publisher;
|
|
4074
|
-
}
|
|
4075
|
-
/**
|
|
4076
|
-
* HTTP client for listener operations on the running gateway daemon.
|
|
4077
|
-
* Returns `{ state: "offline" }` when the daemon is offline so hot-reload
|
|
4078
|
-
* paths stay write-only without parsing strings.
|
|
4079
|
-
*/
|
|
4080
|
-
get listeners() {
|
|
4081
|
-
if (!this.memos.listeners) {
|
|
4082
|
-
const gateway = this.gateway;
|
|
4083
|
-
const token = this.gatewayToken;
|
|
4084
|
-
this.memos.listeners = new FunnelListenersClient({
|
|
4085
|
-
port: gateway.getPort(),
|
|
4086
|
-
isDaemonRunning: () => gateway.isRunning(),
|
|
4087
|
-
getToken: () => token.read()
|
|
4088
|
-
});
|
|
4089
|
-
}
|
|
4090
|
-
return this.memos.listeners;
|
|
4091
|
-
}
|
|
4092
3946
|
/**
|
|
4093
3947
|
* In-process gateway server. Unlike `gateway.start()` (which spawns a daemon),
|
|
4094
3948
|
* this returns a class that runs `Bun.serve` + listeners inside the current process —
|
|
@@ -4097,7 +3951,6 @@ var Funnel = class Funnel {
|
|
|
4097
3951
|
gatewayServer(options = {}) {
|
|
4098
3952
|
return new FunnelGatewayServer({
|
|
4099
3953
|
channels: this.channels,
|
|
4100
|
-
settings: this.store,
|
|
4101
3954
|
port: options.port,
|
|
4102
3955
|
hostname: options.hostname,
|
|
4103
3956
|
dbPath: options.dbPath,
|
|
@@ -4112,6 +3965,20 @@ var Funnel = class Funnel {
|
|
|
4112
3965
|
extraRoutes: options.extraRoutes
|
|
4113
3966
|
});
|
|
4114
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
|
+
}
|
|
4115
3982
|
async debug(channelName) {
|
|
4116
3983
|
return buildFunnelDebugReport({
|
|
4117
3984
|
gateway: this.gateway,
|
|
@@ -4125,331 +3992,6 @@ var Funnel = class Funnel {
|
|
|
4125
3992
|
}
|
|
4126
3993
|
};
|
|
4127
3994
|
//#endregion
|
|
4128
|
-
//#region lib/engine/mcp/channel-subscriber.ts
|
|
4129
|
-
const RECONNECT_DELAY = 1e3;
|
|
4130
|
-
const MAX_RECONNECT_DELAY = 1e4;
|
|
4131
|
-
/**
|
|
4132
|
-
* Subscribes to the gateway WebSocket for a single channel and forwards
|
|
4133
|
-
* incoming events to the MCP server as `notifications/claude/channel`.
|
|
4134
|
-
* Reconnects with exponential backoff and replays missed events via `?since=<offset>`.
|
|
4135
|
-
*/
|
|
4136
|
-
var FunnelChannelSubscriber = class {
|
|
4137
|
-
state = {
|
|
4138
|
-
reconnectDelay: RECONNECT_DELAY,
|
|
4139
|
-
lastOffset: 0
|
|
4140
|
-
};
|
|
4141
|
-
constructor(props) {
|
|
4142
|
-
this.props = props;
|
|
4143
|
-
Object.freeze(this);
|
|
4144
|
-
}
|
|
4145
|
-
start() {
|
|
4146
|
-
this.connect();
|
|
4147
|
-
}
|
|
4148
|
-
connect() {
|
|
4149
|
-
const sinceQuery = this.state.lastOffset > 0 ? `&since=${this.state.lastOffset}` : "";
|
|
4150
|
-
const wsUrl = `${this.props.baseUrl}${sinceQuery}`;
|
|
4151
|
-
const ws = new WebSocket(wsUrl, this.props.protocols);
|
|
4152
|
-
ws.addEventListener("open", () => {
|
|
4153
|
-
this.state.reconnectDelay = RECONNECT_DELAY;
|
|
4154
|
-
process.stderr.write(`funnel: connected (${wsUrl})\n`);
|
|
4155
|
-
});
|
|
4156
|
-
ws.addEventListener("message", (event) => this.handleMessage(event));
|
|
4157
|
-
ws.addEventListener("close", () => {
|
|
4158
|
-
process.stderr.write(`funnel: disconnected, reconnecting in ${this.state.reconnectDelay}ms\n`);
|
|
4159
|
-
setTimeout(() => this.connect(), this.state.reconnectDelay);
|
|
4160
|
-
this.state.reconnectDelay = Math.min(this.state.reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
|
4161
|
-
});
|
|
4162
|
-
ws.addEventListener("error", () => {});
|
|
4163
|
-
}
|
|
4164
|
-
async handleMessage(event) {
|
|
4165
|
-
try {
|
|
4166
|
-
const payload = JSON.parse(String(event.data));
|
|
4167
|
-
const eventType = payload.meta?.event_type ?? "unknown";
|
|
4168
|
-
if (typeof payload.offset === "number" && payload.offset > this.state.lastOffset) this.state.lastOffset = payload.offset;
|
|
4169
|
-
process.stderr.write(`funnel: received event (${eventType})\n`);
|
|
4170
|
-
await this.props.server.notification({
|
|
4171
|
-
method: "notifications/claude/channel",
|
|
4172
|
-
params: {
|
|
4173
|
-
content: payload.content,
|
|
4174
|
-
meta: payload.meta
|
|
4175
|
-
}
|
|
4176
|
-
});
|
|
4177
|
-
} catch (error) {
|
|
4178
|
-
process.stderr.write(`funnel: error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
4179
|
-
}
|
|
4180
|
-
}
|
|
4181
|
-
};
|
|
4182
|
-
//#endregion
|
|
4183
|
-
//#region lib/engine/mcp/read-channel-connectors.ts
|
|
4184
|
-
const TOOL_CONNECTOR_TYPES = new Set([
|
|
4185
|
-
"slack",
|
|
4186
|
-
"gh",
|
|
4187
|
-
"discord"
|
|
4188
|
-
]);
|
|
4189
|
-
const readChannelConnectors = (dir, channelId) => {
|
|
4190
|
-
const settingsPath = join(dir, "settings.json");
|
|
4191
|
-
if (!existsSync(settingsPath)) return null;
|
|
4192
|
-
const raw = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
4193
|
-
const parsed = settingsSchema.safeParse(raw);
|
|
4194
|
-
if (!parsed.success) return null;
|
|
4195
|
-
const channel = parsed.data.channels.find((c) => c.id === channelId);
|
|
4196
|
-
if (!channel) return null;
|
|
4197
|
-
const connectors = channel.connectors.filter((c) => TOOL_CONNECTOR_TYPES.has(c.type)).map((c) => ({
|
|
4198
|
-
name: c.name,
|
|
4199
|
-
type: c.type
|
|
4200
|
-
}));
|
|
4201
|
-
return {
|
|
4202
|
-
channelName: channel.name,
|
|
4203
|
-
connectors
|
|
4204
|
-
};
|
|
4205
|
-
};
|
|
4206
|
-
//#endregion
|
|
4207
|
-
//#region lib/engine/mcp/read-gateway-token.ts
|
|
4208
|
-
const readGatewayToken = (dir) => {
|
|
4209
|
-
const fromEnv = process.env.FUNNEL_GATEWAY_TOKEN;
|
|
4210
|
-
if (fromEnv && fromEnv.length > 0) return fromEnv;
|
|
4211
|
-
const path = join(dir, "gateway.token");
|
|
4212
|
-
if (!existsSync(path)) return null;
|
|
4213
|
-
const value = readFileSync(path, "utf-8").trim();
|
|
4214
|
-
return value.length > 0 ? value : null;
|
|
4215
|
-
};
|
|
4216
|
-
//#endregion
|
|
4217
|
-
//#region lib/engine/mcp/usage-hint-for-type.ts
|
|
4218
|
-
const usageHintForType = (type) => {
|
|
4219
|
-
if (type === "slack") return [
|
|
4220
|
-
"Slack Web API.",
|
|
4221
|
-
"To reply in the same thread: method=POST path=chat.postMessage body={ channel: meta.channel_id, text: \"...\", thread_ts: meta.thread_ts }",
|
|
4222
|
-
"To react: method=POST path=reactions.add body={ channel: meta.channel_id, timestamp: meta.thread_ts, name: \"thumbsup\" }",
|
|
4223
|
-
"Use meta fields from the incoming event: channel_id (Slack channel), thread_ts (thread anchor), user_id (sender)."
|
|
4224
|
-
].join(" ");
|
|
4225
|
-
if (type === "discord") return [
|
|
4226
|
-
"Discord REST API.",
|
|
4227
|
-
"To reply: method=POST path=/channels/<meta.channel_id>/messages body={ content: \"...\" }",
|
|
4228
|
-
"Use meta fields: channel_id (Discord channel), user_id (sender), guild_id."
|
|
4229
|
-
].join(" ");
|
|
4230
|
-
if (type === "gh") return [
|
|
4231
|
-
"GitHub REST via gh CLI.",
|
|
4232
|
-
"To comment: method=POST path=repos/<meta.repository>/issues/<number>/comments body={ body: \"...\" }",
|
|
4233
|
-
"Parse <number> from meta.subject_url. meta fields: repository (owner/repo), subject_type, subject_url, reason."
|
|
4234
|
-
].join(" ");
|
|
4235
|
-
return "Generic adapter call.";
|
|
4236
|
-
};
|
|
4237
|
-
//#endregion
|
|
4238
|
-
//#region lib/engine/mcp/channel-server.ts
|
|
4239
|
-
const DEFAULT_FUNNEL_DIR = join(homedir(), ".funnel");
|
|
4240
|
-
const BUILTIN_TOOL_NAMES = ["fnl_status", "fnl_debug"];
|
|
4241
|
-
const isBuiltinTool = (name) => BUILTIN_TOOL_NAMES.includes(name);
|
|
4242
|
-
const readAllChannels = (dir) => {
|
|
4243
|
-
const settingsPath = join(dir, "settings.json");
|
|
4244
|
-
if (!existsSync(settingsPath)) return [];
|
|
4245
|
-
try {
|
|
4246
|
-
const raw = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
4247
|
-
const parsed = settingsSchema.safeParse(raw);
|
|
4248
|
-
if (!parsed.success) return [];
|
|
4249
|
-
return parsed.data.channels.map((c) => ({
|
|
4250
|
-
id: c.id,
|
|
4251
|
-
name: c.name
|
|
4252
|
-
}));
|
|
4253
|
-
} catch {
|
|
4254
|
-
return [];
|
|
4255
|
-
}
|
|
4256
|
-
};
|
|
4257
|
-
const startChannelServer = async (options = {}) => {
|
|
4258
|
-
const dir = options.dir ?? DEFAULT_FUNNEL_DIR;
|
|
4259
|
-
const gatewayBaseUrl = options.gatewayUrl ?? process.env.FUNNEL_GATEWAY_URL ?? `http://127.0.0.1:${resolveFunnelPort()}`;
|
|
4260
|
-
const gatewayWsUrl = `${gatewayBaseUrl.replace(/^http/, "ws")}/ws`;
|
|
4261
|
-
const channelId = options.channelId ?? process.env.FUNNEL_CHANNEL_ID;
|
|
4262
|
-
const channel = channelId ? readChannelConnectors(dir, channelId) : null;
|
|
4263
|
-
const token = options.token ?? readGatewayToken(dir);
|
|
4264
|
-
const allChannels = readAllChannels(dir);
|
|
4265
|
-
const currentChannelName = channel?.channelName ?? null;
|
|
4266
|
-
const channelContext = allChannels.length > 0 ? [
|
|
4267
|
-
"",
|
|
4268
|
-
"Configured channels (use as the `channel` argument to fnl_debug):",
|
|
4269
|
-
...allChannels.map((ch) => ` ${ch.name}${ch.name === currentChannelName ? " ← this session" : ""}`)
|
|
4270
|
-
].join("\n") : "";
|
|
4271
|
-
const server = new Server({
|
|
4272
|
-
name: FUNNEL_MCP_NAME,
|
|
4273
|
-
version: "1.0.0"
|
|
4274
|
-
}, {
|
|
4275
|
-
capabilities: {
|
|
4276
|
-
experimental: { "claude/channel": {} },
|
|
4277
|
-
tools: {}
|
|
4278
|
-
},
|
|
4279
|
-
instructions: [
|
|
4280
|
-
`Events arrive as notifications (method: notifications/claude/channel) with two fields:`,
|
|
4281
|
-
` content — the event payload as a JSON string (parse it to read the message)`,
|
|
4282
|
-
` meta — key/value strings describing the event`,
|
|
4283
|
-
"",
|
|
4284
|
-
"meta fields by event_type:",
|
|
4285
|
-
" slack: event_type=slack channel_id=C… thread_ts=1234.5678 user_id=U… mentioned=true|false",
|
|
4286
|
-
" gh: event_type=gh repository=owner/repo subject_type=Issue|PullRequest subject_url=… reason=…",
|
|
4287
|
-
" discord: event_type=discord channel_id=… user_id=… guild_id=… mentioned=true|false",
|
|
4288
|
-
" schedule: event_type=schedule entry_id=…",
|
|
4289
|
-
"",
|
|
4290
|
-
"To reply to a Slack message in the same thread, call the connector tool with:",
|
|
4291
|
-
` method: POST`,
|
|
4292
|
-
` path: chat.postMessage`,
|
|
4293
|
-
` body: { channel: meta.channel_id, text: "your reply", thread_ts: meta.thread_ts }`,
|
|
4294
|
-
"",
|
|
4295
|
-
"To comment on a GitHub issue/PR (extract from subject_url in meta):",
|
|
4296
|
-
` method: POST`,
|
|
4297
|
-
` path: repos/<meta.repository>/issues/<number>/comments (parse number from meta.subject_url)`,
|
|
4298
|
-
` body: { body: "your reply" }`,
|
|
4299
|
-
"",
|
|
4300
|
-
"Built-in diagnostic tools — call proactively when events seem missing or delayed:",
|
|
4301
|
-
" fnl_status — gateway running state, all listeners alive/dead, Claude WS clients",
|
|
4302
|
-
" fnl_debug — per-channel diagnosis with last 10 events, rootCause, suggestedActions",
|
|
4303
|
-
" omit channel arg to diagnose all channels; check summary.suggestedActions first",
|
|
4304
|
-
channelContext
|
|
4305
|
-
].join("\n")
|
|
4306
|
-
});
|
|
4307
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
4308
|
-
const connectorTools = (channel?.connectors ?? []).map((c) => ({
|
|
4309
|
-
name: c.name,
|
|
4310
|
-
description: `Call the "${c.name}" (${c.type}) connector. ${usageHintForType(c.type)}`,
|
|
4311
|
-
inputSchema: {
|
|
4312
|
-
type: "object",
|
|
4313
|
-
properties: {
|
|
4314
|
-
method: {
|
|
4315
|
-
type: "string",
|
|
4316
|
-
description: "HTTP verb or API method (e.g. POST, chat.postMessage)"
|
|
4317
|
-
},
|
|
4318
|
-
path: {
|
|
4319
|
-
type: "string",
|
|
4320
|
-
description: "API path or method name (adapter-specific)"
|
|
4321
|
-
},
|
|
4322
|
-
body: {
|
|
4323
|
-
type: "object",
|
|
4324
|
-
description: "Request body / params (adapter-specific)"
|
|
4325
|
-
}
|
|
4326
|
-
},
|
|
4327
|
-
required: ["method", "path"]
|
|
4328
|
-
}
|
|
4329
|
-
}));
|
|
4330
|
-
const channelEnum = allChannels.length > 0 ? allChannels.map((ch) => ch.name) : void 0;
|
|
4331
|
-
const builtinTools = [{
|
|
4332
|
-
name: "fnl_status",
|
|
4333
|
-
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.",
|
|
4334
|
-
inputSchema: {
|
|
4335
|
-
type: "object",
|
|
4336
|
-
properties: {}
|
|
4337
|
-
}
|
|
4338
|
-
}, {
|
|
4339
|
-
name: "fnl_debug",
|
|
4340
|
-
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.",
|
|
4341
|
-
inputSchema: {
|
|
4342
|
-
type: "object",
|
|
4343
|
-
properties: { channel: channelEnum ? {
|
|
4344
|
-
type: "string",
|
|
4345
|
-
description: `Channel name to inspect. One of: ${channelEnum.join(", ")}. Omit to get all channels.`,
|
|
4346
|
-
enum: channelEnum
|
|
4347
|
-
} : {
|
|
4348
|
-
type: "string",
|
|
4349
|
-
description: "Channel name to inspect. Omit to get all channels."
|
|
4350
|
-
} }
|
|
4351
|
-
}
|
|
4352
|
-
}];
|
|
4353
|
-
return { tools: [...connectorTools, ...builtinTools] };
|
|
4354
|
-
});
|
|
4355
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
4356
|
-
const toolName = request.params.name;
|
|
4357
|
-
if (isBuiltinTool(toolName)) return handleBuiltinTool(toolName, request.params.arguments, gatewayBaseUrl, token, allChannels);
|
|
4358
|
-
if (!channel) throw new Error("FUNNEL_CHANNEL_ID is not set or channel not found in settings.json");
|
|
4359
|
-
const args = request.params.arguments ?? {};
|
|
4360
|
-
const method = typeof args.method === "string" ? args.method : "";
|
|
4361
|
-
const path = typeof args.path === "string" ? args.path : "";
|
|
4362
|
-
const body = args.body ?? {};
|
|
4363
|
-
if (!method || !path) throw new Error("`method` and `path` are required");
|
|
4364
|
-
const url = `${gatewayBaseUrl}/channels/${encodeURIComponent(channel.channelName)}/connectors/${encodeURIComponent(toolName)}/call`;
|
|
4365
|
-
const headers = { "content-type": "application/json" };
|
|
4366
|
-
if (token) headers.authorization = `Bearer ${token}`;
|
|
4367
|
-
const res = await fetch(url, {
|
|
4368
|
-
method: "POST",
|
|
4369
|
-
headers,
|
|
4370
|
-
body: JSON.stringify({
|
|
4371
|
-
method,
|
|
4372
|
-
path,
|
|
4373
|
-
body
|
|
4374
|
-
})
|
|
4375
|
-
});
|
|
4376
|
-
const text = await res.text();
|
|
4377
|
-
if (!res.ok) throw new Error(`gateway call failed (${res.status}): ${text}`);
|
|
4378
|
-
return { content: [{
|
|
4379
|
-
type: "text",
|
|
4380
|
-
text
|
|
4381
|
-
}] };
|
|
4382
|
-
});
|
|
4383
|
-
const transport = new StdioServerTransport();
|
|
4384
|
-
await server.connect(transport);
|
|
4385
|
-
if (!channelId) return;
|
|
4386
|
-
new FunnelChannelSubscriber({
|
|
4387
|
-
server,
|
|
4388
|
-
baseUrl: `${gatewayWsUrl}?channel=${encodeURIComponent(channelId)}`,
|
|
4389
|
-
protocols: token ? [`funnel.token.${token}`] : void 0
|
|
4390
|
-
}).start();
|
|
4391
|
-
};
|
|
4392
|
-
const handleBuiltinTool = async (name, args, gatewayBaseUrl, token, allChannels) => {
|
|
4393
|
-
const headers = {};
|
|
4394
|
-
if (token) headers.authorization = `Bearer ${token}`;
|
|
4395
|
-
if (name === "fnl_status") {
|
|
4396
|
-
const res = await fetch(`${gatewayBaseUrl}/status`, { headers }).catch(() => null);
|
|
4397
|
-
if (!res) return { content: [{
|
|
4398
|
-
type: "text",
|
|
4399
|
-
text: JSON.stringify({
|
|
4400
|
-
running: false,
|
|
4401
|
-
error: "gateway unreachable",
|
|
4402
|
-
hint: "run: fnl gateway start",
|
|
4403
|
-
knownChannels: allChannels.map((ch) => ch.name)
|
|
4404
|
-
})
|
|
4405
|
-
}] };
|
|
4406
|
-
const body = await res.json();
|
|
4407
|
-
return { content: [{
|
|
4408
|
-
type: "text",
|
|
4409
|
-
text: JSON.stringify(body)
|
|
4410
|
-
}] };
|
|
4411
|
-
}
|
|
4412
|
-
const channelArg = typeof args?.channel === "string" ? args.channel : null;
|
|
4413
|
-
const url = channelArg ? `${gatewayBaseUrl}/debug?channel=${encodeURIComponent(channelArg)}` : `${gatewayBaseUrl}/debug`;
|
|
4414
|
-
const res = await fetch(url, { headers }).catch(() => null);
|
|
4415
|
-
if (!res) return { content: [{
|
|
4416
|
-
type: "text",
|
|
4417
|
-
text: JSON.stringify({
|
|
4418
|
-
gateway: { running: false },
|
|
4419
|
-
channels: allChannels.map((ch) => ({
|
|
4420
|
-
id: ch.id,
|
|
4421
|
-
name: ch.name,
|
|
4422
|
-
diagnosis: {
|
|
4423
|
-
status: "error",
|
|
4424
|
-
message: "gateway is not running",
|
|
4425
|
-
nextAction: "fnl gateway start",
|
|
4426
|
-
rootCause: null
|
|
4427
|
-
}
|
|
4428
|
-
}))
|
|
4429
|
-
})
|
|
4430
|
-
}] };
|
|
4431
|
-
const body = await res.json();
|
|
4432
|
-
return { content: [{
|
|
4433
|
-
type: "text",
|
|
4434
|
-
text: JSON.stringify(body)
|
|
4435
|
-
}] };
|
|
4436
|
-
};
|
|
4437
|
-
//#endregion
|
|
4438
|
-
//#region lib/engine/local-config/local-config-json-schema.ts
|
|
4439
|
-
/**
|
|
4440
|
-
* Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
|
|
4441
|
-
* `$schema` references in committed `funnel.json` files so editors can give
|
|
4442
|
-
* autocomplete and validation for channels[] (transport) and profiles[]
|
|
4443
|
-
* (launch recipe) without anyone hand-maintaining a separate schema.
|
|
4444
|
-
*/
|
|
4445
|
-
const funnelJsonSchema = () => {
|
|
4446
|
-
return {
|
|
4447
|
-
...z.toJSONSchema(localConfigSchema, { target: "draft-2020-12" }),
|
|
4448
|
-
title: "Funnel per-repo launch config",
|
|
4449
|
-
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."
|
|
4450
|
-
};
|
|
4451
|
-
};
|
|
4452
|
-
//#endregion
|
|
4453
3995
|
//#region lib/engine/logger/node-logger.ts
|
|
4454
3996
|
const defaultLogFile = () => join(funnelTmpDir(), "funnel.log");
|
|
4455
3997
|
var NodeFunnelLogger = class extends FunnelLogger {
|
|
@@ -4490,26 +4032,6 @@ var NoopFunnelLogger = class extends FunnelLogger {
|
|
|
4490
4032
|
error() {}
|
|
4491
4033
|
};
|
|
4492
4034
|
//#endregion
|
|
4493
|
-
//#region lib/engine/token-prompter/memory-token-prompter.ts
|
|
4494
|
-
/**
|
|
4495
|
-
* Pre-seeded answers keyed by prompt label. Tests configure the map up front;
|
|
4496
|
-
* unmapped labels throw so the test surfaces unexpected prompts loudly.
|
|
4497
|
-
*/
|
|
4498
|
-
var MemoryFunnelTokenPrompter = class extends FunnelTokenPrompter {
|
|
4499
|
-
answers;
|
|
4500
|
-
asked = [];
|
|
4501
|
-
constructor(props = {}) {
|
|
4502
|
-
super();
|
|
4503
|
-
this.answers = new Map(Object.entries(props.answers ?? {}));
|
|
4504
|
-
}
|
|
4505
|
-
async promptSecret(label) {
|
|
4506
|
-
this.asked.push(label);
|
|
4507
|
-
const answer = this.answers.get(label);
|
|
4508
|
-
if (answer === void 0) throw new Error(`no answer seeded for prompt "${label}"`);
|
|
4509
|
-
return answer;
|
|
4510
|
-
}
|
|
4511
|
-
};
|
|
4512
|
-
//#endregion
|
|
4513
4035
|
//#region lib/gateway/memory-funnel-event-log.ts
|
|
4514
4036
|
/**
|
|
4515
4037
|
* In-process `FunnelEventLog` backed by a plain array. Used by tests and by
|
|
@@ -5465,7 +4987,6 @@ modes:
|
|
|
5465
4987
|
fanout every connected WS client receives every event (default)
|
|
5466
4988
|
exclusive each event is delivered to exactly one connected client (round-robin)
|
|
5467
4989
|
|
|
5468
|
-
tap=all clients (TUI dashboard, debugging) always receive regardless of mode.
|
|
5469
4990
|
`), (c) => {
|
|
5470
4991
|
const param = c.req.valid("param");
|
|
5471
4992
|
c.env.funnel.channels.setDelivery(param.channel, param.mode);
|
|
@@ -5662,19 +5183,19 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
|
5662
5183
|
channel: z.string().optional()
|
|
5663
5184
|
}).passthrough(), claudeHelp), async (c) => {
|
|
5664
5185
|
const query = c.req.valid("query");
|
|
5665
|
-
const funnel = c.env
|
|
5186
|
+
const { funnel, claude, profiles, localConfig, localConfigSync } = c.env;
|
|
5666
5187
|
const userArgs = queryToCliArgs(c.req.url, RESERVED_KEYS$1);
|
|
5667
5188
|
if (query.channel && !query.profile) {
|
|
5668
|
-
const exitCode = await
|
|
5189
|
+
const exitCode = await claude.launch({
|
|
5669
5190
|
channel: query.channel,
|
|
5670
5191
|
userArgs
|
|
5671
5192
|
});
|
|
5672
5193
|
process.exit(exitCode);
|
|
5673
5194
|
}
|
|
5674
5195
|
if (query.profile) {
|
|
5675
|
-
const profile =
|
|
5196
|
+
const profile = profiles.get(query.profile);
|
|
5676
5197
|
if (!profile) throw new HTTPException(404, { message: `profile "${query.profile}" not found` });
|
|
5677
|
-
const exitCode = await
|
|
5198
|
+
const exitCode = await claude.launch({
|
|
5678
5199
|
channel: profile.channelId,
|
|
5679
5200
|
cwd: profile.path,
|
|
5680
5201
|
userArgs,
|
|
@@ -5686,24 +5207,24 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
|
5686
5207
|
process.exit(exitCode);
|
|
5687
5208
|
}
|
|
5688
5209
|
const cwd = process.cwd();
|
|
5689
|
-
const local =
|
|
5210
|
+
const local = localConfig.read(cwd);
|
|
5690
5211
|
if (local) {
|
|
5691
5212
|
const picked = query.channel !== void 0 ? local.channels.find((c) => c.name === query.channel) : local.channels[0];
|
|
5692
5213
|
if (!picked) throw new HTTPException(404, { message: query.channel ? `channel "${query.channel}" is not declared in funnel.json` : `funnel.json declares no channels` });
|
|
5693
|
-
const synced = await
|
|
5214
|
+
const synced = await localConfigSync.ensure(picked);
|
|
5694
5215
|
for (const outcome of synced.touched) if (outcome.changed) await funnel.listeners.restart(picked.name, outcome.name);
|
|
5695
5216
|
else await funnel.listeners.start(picked.name, outcome.name);
|
|
5696
5217
|
for (const name of synced.removed) await funnel.listeners.stop(picked.name, name);
|
|
5697
|
-
const exitCode = await
|
|
5218
|
+
const exitCode = await claude.launch({
|
|
5698
5219
|
channel: picked.name,
|
|
5699
5220
|
cwd,
|
|
5700
5221
|
userArgs
|
|
5701
5222
|
});
|
|
5702
5223
|
process.exit(exitCode);
|
|
5703
5224
|
}
|
|
5704
|
-
const defaultProfile =
|
|
5225
|
+
const defaultProfile = profiles.getDefault();
|
|
5705
5226
|
if (!defaultProfile) return c.text(claudeHelp);
|
|
5706
|
-
const exitCode = await
|
|
5227
|
+
const exitCode = await claude.launch({
|
|
5707
5228
|
channel: defaultProfile.channelId,
|
|
5708
5229
|
cwd: defaultProfile.path,
|
|
5709
5230
|
userArgs,
|
|
@@ -6196,7 +5717,7 @@ const buildChannelReport = async (targetChannel, gatewayStatus, gatewayBodyOrNul
|
|
|
6196
5717
|
errors: l.errors,
|
|
6197
5718
|
lastEventAt: l.lastEventAt
|
|
6198
5719
|
}));
|
|
6199
|
-
baseReport.claudeClients = gatewayBodyOrNull.clients.filter((cl) =>
|
|
5720
|
+
baseReport.claudeClients = gatewayBodyOrNull.clients.filter((cl) => cl.channelName === targetChannelName).length;
|
|
6200
5721
|
}
|
|
6201
5722
|
if (store) {
|
|
6202
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]);
|
|
@@ -6706,15 +6227,7 @@ examples:
|
|
|
6706
6227
|
funnel gateway run
|
|
6707
6228
|
funnel gateway run --no-caffeine`), async (c) => {
|
|
6708
6229
|
const query = c.req.valid("query");
|
|
6709
|
-
const
|
|
6710
|
-
const gatewayScript = resolveDaemonScript();
|
|
6711
|
-
const command = query["no-caffeine"] !== "true" && process.platform === "darwin" ? [
|
|
6712
|
-
"caffeinate",
|
|
6713
|
-
"-is",
|
|
6714
|
-
"bun",
|
|
6715
|
-
gatewayScript
|
|
6716
|
-
] : ["bun", gatewayScript];
|
|
6717
|
-
const exitCode = await funnel.process.attach(command);
|
|
6230
|
+
const exitCode = await c.env.funnel.runGatewayForeground({ caffeinate: query["no-caffeine"] !== "true" });
|
|
6718
6231
|
process.exit(exitCode);
|
|
6719
6232
|
});
|
|
6720
6233
|
//#endregion
|
|
@@ -6858,10 +6371,11 @@ const profilesAddHandler = factory.createHandlers(zValidator$1("param", z.object
|
|
|
6858
6371
|
const param = c.req.valid("param");
|
|
6859
6372
|
const query = c.req.valid("query");
|
|
6860
6373
|
const funnel = c.env.funnel;
|
|
6374
|
+
const { profiles, claude } = c.env;
|
|
6861
6375
|
const channel = funnel.channels.get(query.channel);
|
|
6862
6376
|
if (!channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
|
|
6863
6377
|
const recipe = parseProfileRecipe(query);
|
|
6864
|
-
|
|
6378
|
+
profiles.add({
|
|
6865
6379
|
name: param.profile,
|
|
6866
6380
|
path: query.path,
|
|
6867
6381
|
channelId: channel.id,
|
|
@@ -6877,7 +6391,9 @@ usage: funnel profiles <name> as-default
|
|
|
6877
6391
|
|
|
6878
6392
|
the first profile in the list is treated as the default for fnl claude.`), (c) => {
|
|
6879
6393
|
const param = c.req.valid("param");
|
|
6880
|
-
c.env.funnel
|
|
6394
|
+
c.env.funnel;
|
|
6395
|
+
const { profiles, claude } = c.env;
|
|
6396
|
+
profiles.asDefault(param.profile);
|
|
6881
6397
|
return c.text(`profile "${param.profile}" is now the default`);
|
|
6882
6398
|
});
|
|
6883
6399
|
//#endregion
|
|
@@ -6903,7 +6419,9 @@ const profilesRenameHandler = factory.createHandlers(zValidator$1("param", z.obj
|
|
|
6903
6419
|
newName: z.string()
|
|
6904
6420
|
})), zValidator$1("query", z.object({})), (c) => {
|
|
6905
6421
|
const param = c.req.valid("param");
|
|
6906
|
-
c.env.funnel
|
|
6422
|
+
c.env.funnel;
|
|
6423
|
+
const { profiles, claude } = c.env;
|
|
6424
|
+
profiles.rename(param.profile, param.newName);
|
|
6907
6425
|
return c.text(`renamed profile "${param.profile}" to "${param.newName}"`);
|
|
6908
6426
|
});
|
|
6909
6427
|
//#endregion
|
|
@@ -6915,10 +6433,11 @@ usage: funnel profiles <name> run [additional claude args...]
|
|
|
6915
6433
|
const RESERVED_KEYS = [];
|
|
6916
6434
|
const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({}).passthrough(), launchHelp), async (c) => {
|
|
6917
6435
|
const param = c.req.valid("param");
|
|
6918
|
-
|
|
6919
|
-
const
|
|
6436
|
+
c.env.funnel;
|
|
6437
|
+
const { profiles, claude } = c.env;
|
|
6438
|
+
const profile = profiles.get(param.profile);
|
|
6920
6439
|
if (!profile) throw new HTTPException(404, { message: `profile "${param.profile}" not found` });
|
|
6921
|
-
const exitCode = await
|
|
6440
|
+
const exitCode = await claude.launch({
|
|
6922
6441
|
channel: profile.channelId,
|
|
6923
6442
|
cwd: profile.path,
|
|
6924
6443
|
userArgs: queryToCliArgs(c.req.url, RESERVED_KEYS),
|
|
@@ -6939,7 +6458,9 @@ const profilesRemoveHelpHandler = factory.createHandlers((c) => c.text(help$1));
|
|
|
6939
6458
|
//#region lib/cli/routes/profiles.remove.$profile.ts
|
|
6940
6459
|
const profilesRemoveHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({})), (c) => {
|
|
6941
6460
|
const param = c.req.valid("param");
|
|
6942
|
-
c.env.funnel
|
|
6461
|
+
c.env.funnel;
|
|
6462
|
+
const { profiles, claude } = c.env;
|
|
6463
|
+
profiles.remove(param.profile);
|
|
6943
6464
|
return c.text(`removed profile "${param.profile}"`);
|
|
6944
6465
|
});
|
|
6945
6466
|
//#endregion
|
|
@@ -6973,10 +6494,11 @@ const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object
|
|
|
6973
6494
|
const param = c.req.valid("param");
|
|
6974
6495
|
const query = c.req.valid("query");
|
|
6975
6496
|
const funnel = c.env.funnel;
|
|
6497
|
+
const { profiles, claude } = c.env;
|
|
6976
6498
|
const channel = query.channel !== void 0 ? funnel.channels.get(query.channel) : null;
|
|
6977
6499
|
if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
|
|
6978
6500
|
const recipe = parseProfileRecipe(query);
|
|
6979
|
-
|
|
6501
|
+
profiles.update(param.profile, {
|
|
6980
6502
|
path: query.path,
|
|
6981
6503
|
channelId: channel?.id,
|
|
6982
6504
|
options: recipe.options,
|
|
@@ -7007,9 +6529,11 @@ examples:
|
|
|
7007
6529
|
funnel profiles add cto --path /repo/myapp --channel prod-inbox --agent pm --options "--brief"
|
|
7008
6530
|
funnel profiles cto as-default
|
|
7009
6531
|
funnel profiles cto run`), (c) => {
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
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) => {
|
|
7013
6537
|
const tag = index === 0 ? " (default)" : "";
|
|
7014
6538
|
const recipe = profile.options.length > 0 ? `, options=${profile.options.join(" ")}` : "";
|
|
7015
6539
|
const session = profile.resume ? "" : ", resume=false";
|
|
@@ -7017,6 +6541,21 @@ examples:
|
|
|
7017
6541
|
});
|
|
7018
6542
|
return c.text(lines.join("\n"));
|
|
7019
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
|
+
};
|
|
7020
6559
|
const schemaHandler = factory.createHandlers(zValidator$1("query", z.object({}), `funnel schema — print the JSON Schema for funnel.json
|
|
7021
6560
|
|
|
7022
6561
|
usage: funnel schema
|
|
@@ -7061,9 +6600,9 @@ const isGatewayStatus = (value) => {
|
|
|
7061
6600
|
if (!("listeners" in value) || !Array.isArray(value.listeners)) return false;
|
|
7062
6601
|
return true;
|
|
7063
6602
|
};
|
|
7064
|
-
const buildStatusLines = async (funnel) => {
|
|
6603
|
+
const buildStatusLines = async (funnel, profiles) => {
|
|
7065
6604
|
const channels = funnel.channels.list();
|
|
7066
|
-
const
|
|
6605
|
+
const profileList = profiles.list();
|
|
7067
6606
|
const gatewayStatus = funnel.gateway.getStatus();
|
|
7068
6607
|
const lines = [];
|
|
7069
6608
|
lines.push("= funnel status =");
|
|
@@ -7089,7 +6628,6 @@ const buildStatusLines = async (funnel) => {
|
|
|
7089
6628
|
const listenerAliveByChannel = /* @__PURE__ */ new Map();
|
|
7090
6629
|
if (gatewayData) {
|
|
7091
6630
|
for (const client of gatewayData.clients) {
|
|
7092
|
-
if (client.tapAll) continue;
|
|
7093
6631
|
const key = client.channelName ?? client.channel;
|
|
7094
6632
|
clientsByChannel.set(key, (clientsByChannel.get(key) ?? 0) + 1);
|
|
7095
6633
|
}
|
|
@@ -7110,8 +6648,8 @@ const buildStatusLines = async (funnel) => {
|
|
|
7110
6648
|
lines.push(` ${indicator} ${paddedName} [${connectorLabel}]${claudeLabel}`);
|
|
7111
6649
|
}
|
|
7112
6650
|
lines.push("");
|
|
7113
|
-
lines.push(`profiles: ${
|
|
7114
|
-
for (const [index, profile] of
|
|
6651
|
+
lines.push(`profiles: ${profileList.length}`);
|
|
6652
|
+
for (const [index, profile] of profileList.entries()) {
|
|
7115
6653
|
const tag = index === 0 ? " (default)" : "";
|
|
7116
6654
|
const channel = funnel.channels.getById(profile.channelId);
|
|
7117
6655
|
const channelLabel = channel ? channel.name : `id:${profile.channelId}`;
|
|
@@ -7132,11 +6670,11 @@ const statusHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
|
7132
6670
|
const isWatch = query.watch === "true" || query.watch === "";
|
|
7133
6671
|
const intervalSec = Math.min(60, Math.max(1, query.interval ? Number(query.interval) : 3));
|
|
7134
6672
|
if (!isWatch) {
|
|
7135
|
-
const lines = await buildStatusLines(funnel);
|
|
6673
|
+
const lines = await buildStatusLines(funnel, c.env.profiles);
|
|
7136
6674
|
return c.text(lines.join("\n"));
|
|
7137
6675
|
}
|
|
7138
6676
|
const render = async () => {
|
|
7139
|
-
const lines = await buildStatusLines(funnel);
|
|
6677
|
+
const lines = await buildStatusLines(funnel, c.env.profiles);
|
|
7140
6678
|
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
7141
6679
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
7142
6680
|
process.stdout.write(lines.join("\n"));
|
|
@@ -7178,4 +6716,4 @@ const routes = factory.createApp().onError((error, c) => {
|
|
|
7178
6716
|
return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
|
|
7179
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);
|
|
7180
6718
|
//#endregion
|
|
7181
|
-
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 };
|