@interactive-inc/claude-funnel 0.57.0 → 0.58.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/dist/bin.js +258 -219
- package/dist/claude.d.ts +2 -2
- package/dist/claude.js +1 -1
- package/dist/{connector-adapter-1PxjN-Uk.d.ts → connector-adapter-BkYC6qiK.d.ts} +1 -1
- package/dist/connectors/discord.d.ts +2 -19
- package/dist/connectors/discord.js +1 -1
- package/dist/connectors/gh.d.ts +1 -1
- package/dist/connectors/slack.d.ts +2 -2
- package/dist/connectors/slack.js +1 -1
- package/dist/{discord-connector-schema-CPgcZkXh.d.ts → discord-connector-schema-CWHVNIcB.d.ts} +18 -1
- package/dist/{discord-listener-C0MoKdQO.js → discord-listener-CKsZGTnH.js} +1 -1
- package/dist/gateway/daemon.js +196 -196
- package/dist/gateway.d.ts +1 -1
- package/dist/gateway.js +1 -1
- package/dist/{index-DEeCwhk2.d.ts → index-tP67P1Sy.d.ts} +38 -3
- package/dist/index.d.ts +6 -5
- package/dist/index.js +76 -11
- package/dist/{local-config-sync-E_t5_fjw.d.ts → local-config-sync-BY20ixEV.d.ts} +2 -2
- package/dist/local-config.d.ts +2 -2
- package/dist/{memory-diagnostic-log-BbFVqDzz.js → memory-diagnostic-log-CvqobDDs.js} +134 -50
- package/dist/{memory-token-prompter-DpCC1_Dn.d.ts → memory-token-prompter-DOgptiIb.d.ts} +1 -1
- package/dist/{slack-listener-BDyBqatt.js → slack-listener-C4wlZaOq.js} +18 -5
- package/dist/{slack-listener-DFlAzMc7.d.ts → slack-listener-DFW9vck4.d.ts} +1 -0
- package/dist/{yaml-render-OhUN-qkS.js → yaml-render-C9Hhjk-0.js} +1 -1
- package/package.json +6 -1
|
@@ -360,7 +360,7 @@ const funnelEventSchema = z.object({
|
|
|
360
360
|
*/
|
|
361
361
|
var FunnelEventLog = class {};
|
|
362
362
|
//#endregion
|
|
363
|
-
//#region lib/logger/
|
|
363
|
+
//#region lib/logger/funnel-log-sqlite-sink.ts
|
|
364
364
|
/** Conservative whitelist for column names interpolated into SQL. */
|
|
365
365
|
const COLUMN_NAME_RE = /^[a-z_][a-z0-9_]*$/;
|
|
366
366
|
/** How many inserts between on-disk size checks (see insertsSinceByteCheck). */
|
|
@@ -380,9 +380,14 @@ const RESERVED_COLUMNS = new Set([
|
|
|
380
380
|
* they are configuration, not schema evolution.
|
|
381
381
|
*/
|
|
382
382
|
const MIGRATIONS = [[
|
|
383
|
-
"CREATE TABLE IF NOT EXISTS
|
|
384
|
-
"CREATE INDEX IF NOT EXISTS
|
|
385
|
-
"CREATE INDEX IF NOT EXISTS
|
|
383
|
+
"CREATE TABLE IF NOT EXISTS logs (seq INTEGER PRIMARY KEY, ts INTEGER NOT NULL, type TEXT, event TEXT NOT NULL)",
|
|
384
|
+
"CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs (ts)",
|
|
385
|
+
"CREATE INDEX IF NOT EXISTS idx_logs_type ON logs (type)"
|
|
386
|
+
], [
|
|
387
|
+
"DROP TABLE IF EXISTS leuco_log",
|
|
388
|
+
"CREATE TABLE IF NOT EXISTS logs (seq INTEGER PRIMARY KEY, ts INTEGER NOT NULL, type TEXT, event TEXT NOT NULL)",
|
|
389
|
+
"CREATE INDEX IF NOT EXISTS idx_logs_ts ON logs (ts)",
|
|
390
|
+
"CREATE INDEX IF NOT EXISTS idx_logs_type ON logs (type)"
|
|
386
391
|
]];
|
|
387
392
|
/**
|
|
388
393
|
* SQLite-backed sink built on `bun:sqlite`. Implements both primary and
|
|
@@ -391,7 +396,7 @@ const MIGRATIONS = [[
|
|
|
391
396
|
* from a backup stream).
|
|
392
397
|
*
|
|
393
398
|
* Concurrency model: seq is `INTEGER PRIMARY KEY`, so SQLite assigns it
|
|
394
|
-
* atomically via `lastInsertRowid`. Two `
|
|
399
|
+
* atomically via `lastInsertRowid`. Two `FunnelLog` instances pointed
|
|
395
400
|
* at the same database file therefore see one monotonically increasing
|
|
396
401
|
* seq stream without any bus-level coordination — the database itself is
|
|
397
402
|
* the synchronization point.
|
|
@@ -403,7 +408,7 @@ const MIGRATIONS = [[
|
|
|
403
408
|
* a new index to an existing database is a no-downtime operation.
|
|
404
409
|
*
|
|
405
410
|
* Type safety: the second generic parameter `I` is the literal tuple of
|
|
406
|
-
* index column names. `extractIndexes` and `
|
|
411
|
+
* index column names. `extractIndexes` and `query({ where })` are
|
|
407
412
|
* both type-checked against this tuple, so a typo at the call site is a
|
|
408
413
|
* compile-time error rather than a silent miss at runtime.
|
|
409
414
|
*
|
|
@@ -414,7 +419,7 @@ const MIGRATIONS = [[
|
|
|
414
419
|
* for ~10–100x throughput at the cost of one fsync per batch instead of
|
|
415
420
|
* one per row.
|
|
416
421
|
*/
|
|
417
|
-
var
|
|
422
|
+
var FunnelLogSqliteSink = class {
|
|
418
423
|
db;
|
|
419
424
|
maxRows;
|
|
420
425
|
maxAgeMs;
|
|
@@ -457,15 +462,15 @@ var LeucoLoggerSqliteSink = class {
|
|
|
457
462
|
...this.indexes
|
|
458
463
|
];
|
|
459
464
|
const placeholders = cols.map(() => "?").join(", ");
|
|
460
|
-
this.insertStmt = this.db.prepare(`INSERT INTO
|
|
465
|
+
this.insertStmt = this.db.prepare(`INSERT INTO logs (${cols.join(", ")}) VALUES (${placeholders})`);
|
|
461
466
|
const colsWithSeq = ["seq", ...cols];
|
|
462
467
|
const placeholdersWithSeq = colsWithSeq.map(() => "?").join(", ");
|
|
463
|
-
this.insertWithSeqStmt = this.db.prepare(`INSERT INTO
|
|
464
|
-
this.maxSeqStmt = this.db.prepare("SELECT COALESCE(MAX(seq), 0) AS max FROM
|
|
465
|
-
this.countStmt = this.db.prepare("SELECT COUNT(*) AS n FROM
|
|
466
|
-
this.trimRowsStmt = this.db.prepare("DELETE FROM
|
|
467
|
-
this.trimAgeStmt = this.db.prepare("DELETE FROM
|
|
468
|
-
this.trimOldestStmt = this.db.prepare("DELETE FROM
|
|
468
|
+
this.insertWithSeqStmt = this.db.prepare(`INSERT INTO logs (${colsWithSeq.join(", ")}) VALUES (${placeholdersWithSeq})`);
|
|
469
|
+
this.maxSeqStmt = this.db.prepare("SELECT COALESCE(MAX(seq), 0) AS max FROM logs");
|
|
470
|
+
this.countStmt = this.db.prepare("SELECT COUNT(*) AS n FROM logs");
|
|
471
|
+
this.trimRowsStmt = this.db.prepare("DELETE FROM logs WHERE seq <= (SELECT seq FROM logs ORDER BY seq DESC LIMIT 1 OFFSET ?)");
|
|
472
|
+
this.trimAgeStmt = this.db.prepare("DELETE FROM logs WHERE ts < ?");
|
|
473
|
+
this.trimOldestStmt = this.db.prepare("DELETE FROM logs WHERE seq IN (SELECT seq FROM logs ORDER BY seq ASC LIMIT ?)");
|
|
469
474
|
}
|
|
470
475
|
insert(input) {
|
|
471
476
|
try {
|
|
@@ -516,7 +521,7 @@ var LeucoLoggerSqliteSink = class {
|
|
|
516
521
|
const row = this.maxSeqStmt.get();
|
|
517
522
|
return row ? row.max : 0;
|
|
518
523
|
}
|
|
519
|
-
|
|
524
|
+
query(props = {}) {
|
|
520
525
|
const conditions = ["seq > ?"];
|
|
521
526
|
const params = [props.sinceSeq ?? 0];
|
|
522
527
|
if (typeof props.type === "string") {
|
|
@@ -527,7 +532,7 @@ var LeucoLoggerSqliteSink = class {
|
|
|
527
532
|
const limit = props.limit ?? 1e3;
|
|
528
533
|
params.push(limit);
|
|
529
534
|
const dir = props.order === "desc" ? "DESC" : "ASC";
|
|
530
|
-
const sql = `SELECT seq, ts, type, event FROM
|
|
535
|
+
const sql = `SELECT seq, ts, type, event FROM logs WHERE ${conditions.join(" AND ")} ORDER BY seq ${dir} LIMIT ?`;
|
|
531
536
|
const rows = this.db.prepare(sql).all(...params);
|
|
532
537
|
if (dir === "DESC") rows.reverse();
|
|
533
538
|
return rows.map(toRecord);
|
|
@@ -607,15 +612,15 @@ var LeucoLoggerSqliteSink = class {
|
|
|
607
612
|
}
|
|
608
613
|
/** Drop every row and reclaim the file space. Used by `<log>.clear()`. */
|
|
609
614
|
clear() {
|
|
610
|
-
this.db.run("DELETE FROM
|
|
615
|
+
this.db.run("DELETE FROM logs");
|
|
611
616
|
this.db.run("VACUUM");
|
|
612
617
|
this.insertsSinceByteCheck = 0;
|
|
613
618
|
}
|
|
614
619
|
syncIndexColumns() {
|
|
615
|
-
const existing = new Set(this.db.prepare("PRAGMA table_info(
|
|
620
|
+
const existing = new Set(this.db.prepare("PRAGMA table_info(logs)").all().map((r) => r.name));
|
|
616
621
|
for (const col of this.indexes) {
|
|
617
|
-
if (!existing.has(col)) this.db.run(`ALTER TABLE
|
|
618
|
-
this.db.run(`CREATE INDEX IF NOT EXISTS
|
|
622
|
+
if (!existing.has(col)) this.db.run(`ALTER TABLE logs ADD COLUMN ${col} TEXT`);
|
|
623
|
+
this.db.run(`CREATE INDEX IF NOT EXISTS idx_logs_${col} ON logs (${col})`);
|
|
619
624
|
}
|
|
620
625
|
}
|
|
621
626
|
migrate() {
|
|
@@ -679,7 +684,7 @@ var SqliteFunnelEventLog = class extends FunnelEventLog {
|
|
|
679
684
|
super();
|
|
680
685
|
this.now = props.now ?? (() => Date.now());
|
|
681
686
|
this.logger = props.logger;
|
|
682
|
-
this.sink = new
|
|
687
|
+
this.sink = new FunnelLogSqliteSink({
|
|
683
688
|
path: props.path,
|
|
684
689
|
indexes: ["channel_id", "connector_id"],
|
|
685
690
|
extractIndexes: (event) => ({
|
|
@@ -722,7 +727,7 @@ var SqliteFunnelEventLog = class extends FunnelEventLog {
|
|
|
722
727
|
* so this returns the full slice and lets the caller filter.
|
|
723
728
|
*/
|
|
724
729
|
loadSince(since) {
|
|
725
|
-
const records = this.sink.
|
|
730
|
+
const records = this.sink.query({ sinceSeq: since });
|
|
726
731
|
const out = [];
|
|
727
732
|
for (const record of records) out.push({
|
|
728
733
|
content: record.event.content,
|
|
@@ -739,7 +744,7 @@ var SqliteFunnelEventLog = class extends FunnelEventLog {
|
|
|
739
744
|
loadForChannel(props) {
|
|
740
745
|
const where = { channel_id: props.channelId };
|
|
741
746
|
if (props.connectorId !== void 0) where.connector_id = props.connectorId;
|
|
742
|
-
const records = this.sink.
|
|
747
|
+
const records = this.sink.query({
|
|
743
748
|
where,
|
|
744
749
|
...props.sinceSeq !== void 0 ? { sinceSeq: props.sinceSeq } : {},
|
|
745
750
|
...props.limit !== void 0 ? { limit: props.limit } : {}
|
|
@@ -771,7 +776,8 @@ function truncate(content) {
|
|
|
771
776
|
const defaultOnError$1 = () => {};
|
|
772
777
|
const DEFAULT_HEALTH_INTERVAL_MS = 3e4;
|
|
773
778
|
const DEFAULT_MAX_BACKOFF_MS = 6e4;
|
|
774
|
-
const
|
|
779
|
+
const DEFAULT_START_TIMEOUT_MS = 3e4;
|
|
780
|
+
const defaultSleep$1 = (ms) => new Promise((r) => {
|
|
775
781
|
setTimeout(r, ms);
|
|
776
782
|
});
|
|
777
783
|
/**
|
|
@@ -796,10 +802,13 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
796
802
|
stats = /* @__PURE__ */ new Map();
|
|
797
803
|
healthCheckIntervalMs;
|
|
798
804
|
maxBackoffMs;
|
|
805
|
+
startTimeoutMs;
|
|
799
806
|
sleep;
|
|
800
807
|
now;
|
|
801
808
|
healthCheckTimer = null;
|
|
802
809
|
healthCheckInFlight = false;
|
|
810
|
+
/** Connectors that failed initial start — retried by the health check. */
|
|
811
|
+
pendingRetry = /* @__PURE__ */ new Map();
|
|
803
812
|
constructor(deps) {
|
|
804
813
|
this.channels = deps.channels;
|
|
805
814
|
this.notify = deps.notify;
|
|
@@ -807,7 +816,8 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
807
816
|
this.onError = deps.onError ?? defaultOnError$1;
|
|
808
817
|
this.healthCheckIntervalMs = deps.healthCheckIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS;
|
|
809
818
|
this.maxBackoffMs = deps.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
|
|
810
|
-
this.
|
|
819
|
+
this.startTimeoutMs = deps.startTimeoutMs ?? DEFAULT_START_TIMEOUT_MS;
|
|
820
|
+
this.sleep = deps.sleep ?? defaultSleep$1;
|
|
811
821
|
this.now = deps.now ?? (() => Date.now());
|
|
812
822
|
}
|
|
813
823
|
static keyOf(channelName, connectorName) {
|
|
@@ -853,13 +863,16 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
853
863
|
}
|
|
854
864
|
};
|
|
855
865
|
try {
|
|
856
|
-
await created.listener.start(bind)
|
|
866
|
+
await Promise.race([created.listener.start(bind), this.sleep(this.startTimeoutMs).then(() => {
|
|
867
|
+
throw new Error(`listener start timed out after ${this.startTimeoutMs}ms`);
|
|
868
|
+
})]);
|
|
857
869
|
this.running.set(key, {
|
|
858
870
|
config: created.config,
|
|
859
871
|
channelName,
|
|
860
872
|
channelId: created.channelId,
|
|
861
873
|
listener: created.listener
|
|
862
874
|
});
|
|
875
|
+
this.pendingRetry.delete(key);
|
|
863
876
|
this.ensureStats(key);
|
|
864
877
|
this.logger?.info(`${created.config.type} listener started`, {
|
|
865
878
|
channel: channelName,
|
|
@@ -894,8 +907,6 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
894
907
|
};
|
|
895
908
|
try {
|
|
896
909
|
await entry.listener.stop();
|
|
897
|
-
this.running.delete(key);
|
|
898
|
-
this.failureCounts.delete(key);
|
|
899
910
|
this.logger?.info(`${entry.config.type} listener stopped`, {
|
|
900
911
|
channel: channelName,
|
|
901
912
|
connector: connectorName
|
|
@@ -918,6 +929,9 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
918
929
|
ok: false,
|
|
919
930
|
reason: err.message
|
|
920
931
|
};
|
|
932
|
+
} finally {
|
|
933
|
+
this.running.delete(key);
|
|
934
|
+
this.failureCounts.delete(key);
|
|
921
935
|
}
|
|
922
936
|
}
|
|
923
937
|
async restart(channelName, connectorName) {
|
|
@@ -927,11 +941,23 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
927
941
|
}
|
|
928
942
|
async startAll() {
|
|
929
943
|
const all = this.channels.listAllConnectors();
|
|
930
|
-
|
|
944
|
+
const results = await Promise.allSettled(all.map((view) => this.start(view.channelName, view.name)));
|
|
945
|
+
for (let i = 0; i < results.length; i++) {
|
|
946
|
+
const result = results[i];
|
|
947
|
+
const view = all[i];
|
|
948
|
+
if (result.status === "rejected" || result.status === "fulfilled" && !result.value.ok) {
|
|
949
|
+
const key = FunnelListenerSupervisor.keyOf(view.channelName, view.name);
|
|
950
|
+
this.pendingRetry.set(key, {
|
|
951
|
+
channelName: view.channelName,
|
|
952
|
+
connectorName: view.name
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
}
|
|
931
956
|
this.startHealthCheck();
|
|
932
957
|
}
|
|
933
958
|
async stopAll() {
|
|
934
959
|
this.stopHealthCheck();
|
|
960
|
+
this.pendingRetry.clear();
|
|
935
961
|
for (const [, entry] of [...this.running.entries()]) await this.stop(entry.channelName, entry.config.name);
|
|
936
962
|
}
|
|
937
963
|
ensureStats(key) {
|
|
@@ -966,6 +992,10 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
966
992
|
clearInterval(this.healthCheckTimer);
|
|
967
993
|
this.healthCheckTimer = null;
|
|
968
994
|
}
|
|
995
|
+
/** Run one health-check pass synchronously. Test-only seam. */
|
|
996
|
+
async runHealthCheckForTest() {
|
|
997
|
+
await this.runHealthCheck();
|
|
998
|
+
}
|
|
969
999
|
async runHealthCheck() {
|
|
970
1000
|
if (this.healthCheckInFlight) return;
|
|
971
1001
|
this.healthCheckInFlight = true;
|
|
@@ -977,6 +1007,23 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
977
1007
|
}
|
|
978
1008
|
await this.recoverDead(entry.channelName, entry.config.name, entry.config.type);
|
|
979
1009
|
}
|
|
1010
|
+
for (const [key, pending] of [...this.pendingRetry.entries()]) {
|
|
1011
|
+
if (this.running.has(key)) {
|
|
1012
|
+
this.pendingRetry.delete(key);
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
this.logger?.info("retrying failed listener", {
|
|
1016
|
+
channel: pending.channelName,
|
|
1017
|
+
connector: pending.connectorName
|
|
1018
|
+
});
|
|
1019
|
+
const failureCount = this.failureCounts.get(key) ?? 0;
|
|
1020
|
+
const backoffMs = Math.min(1e3 * 2 ** failureCount, this.maxBackoffMs);
|
|
1021
|
+
await this.sleep(backoffMs);
|
|
1022
|
+
if ((await this.start(pending.channelName, pending.connectorName)).ok) {
|
|
1023
|
+
this.pendingRetry.delete(key);
|
|
1024
|
+
this.failureCounts.delete(key);
|
|
1025
|
+
} else this.failureCounts.set(key, failureCount + 1);
|
|
1026
|
+
}
|
|
980
1027
|
} finally {
|
|
981
1028
|
this.healthCheckInFlight = false;
|
|
982
1029
|
}
|
|
@@ -1005,7 +1052,27 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
1005
1052
|
//#endregion
|
|
1006
1053
|
//#region lib/gateway/kill-competing-slack-gateways.ts
|
|
1007
1054
|
const defaultProcess = new NodeFunnelProcessRunner();
|
|
1055
|
+
const SIGTERM_GRACE_MS = 3e3;
|
|
1056
|
+
const POLL_INTERVAL_MS = 100;
|
|
1057
|
+
const SIGKILL_GRACE_MS = 200;
|
|
1058
|
+
const defaultSleep = (ms) => new Promise((resolve) => {
|
|
1059
|
+
setTimeout(resolve, ms);
|
|
1060
|
+
});
|
|
1008
1061
|
const titleFor = (dir) => `funnel-gateway[${dir}]`;
|
|
1062
|
+
const waitForExit = async (props) => {
|
|
1063
|
+
const deadline = props.now() + SIGTERM_GRACE_MS;
|
|
1064
|
+
while (props.now() < deadline) {
|
|
1065
|
+
if (props.pids.every((pid) => !props.runner.isAlive(pid))) return;
|
|
1066
|
+
await props.sleep(POLL_INTERVAL_MS);
|
|
1067
|
+
}
|
|
1068
|
+
for (const pid of props.pids) {
|
|
1069
|
+
if (!props.runner.isAlive(pid)) continue;
|
|
1070
|
+
try {
|
|
1071
|
+
props.runner.kill(pid, "SIGKILL");
|
|
1072
|
+
} catch {}
|
|
1073
|
+
}
|
|
1074
|
+
await props.sleep(SIGKILL_GRACE_MS);
|
|
1075
|
+
};
|
|
1009
1076
|
/**
|
|
1010
1077
|
* Kills other funnel daemon processes that share the SAME funnel home dir,
|
|
1011
1078
|
* which is the only situation that causes a real conflict (duplicate Slack
|
|
@@ -1015,6 +1082,10 @@ const titleFor = (dir) => `funnel-gateway[${dir}]`;
|
|
|
1015
1082
|
* `funnel-gateway[<dir>]` marker appended to argv (also assigned to
|
|
1016
1083
|
* `process.title` on POSIX). `FunnelProcessRunner.listProcessesContaining`
|
|
1017
1084
|
* absorbs the POSIX/Windows enumeration difference behind the marker match.
|
|
1085
|
+
*
|
|
1086
|
+
* Waits for the killed daemons to actually exit before returning, so the caller
|
|
1087
|
+
* can bind the port and open a fresh Socket Mode connection without overlapping
|
|
1088
|
+
* the old one (the overlap is what makes Slack split inbound events).
|
|
1018
1089
|
*/
|
|
1019
1090
|
const killCompetingSlackGateways = async (props) => {
|
|
1020
1091
|
const runner = props.process ?? defaultProcess;
|
|
@@ -1031,6 +1102,13 @@ const killCompetingSlackGateways = async (props) => {
|
|
|
1031
1102
|
args: snapshot.command.slice(0, 160)
|
|
1032
1103
|
});
|
|
1033
1104
|
}
|
|
1105
|
+
if (killed.length === 0) return killed;
|
|
1106
|
+
await waitForExit({
|
|
1107
|
+
runner,
|
|
1108
|
+
pids: killed,
|
|
1109
|
+
sleep: props.sleep ?? defaultSleep,
|
|
1110
|
+
now: props.now ?? (() => Date.now())
|
|
1111
|
+
});
|
|
1034
1112
|
return killed;
|
|
1035
1113
|
};
|
|
1036
1114
|
//#endregion
|
|
@@ -1290,6 +1368,7 @@ const healthHandler = factory.createHandlers((c) => {
|
|
|
1290
1368
|
return c.json({
|
|
1291
1369
|
ok: true,
|
|
1292
1370
|
pid: deps.selfPid,
|
|
1371
|
+
funnelDir: deps.dir,
|
|
1293
1372
|
clients: deps.broadcaster.getClientCount(),
|
|
1294
1373
|
listeners: deps.supervisor.list()
|
|
1295
1374
|
});
|
|
@@ -1341,6 +1420,7 @@ const statusHandler = factory.createHandlers((c) => {
|
|
|
1341
1420
|
return c.json({
|
|
1342
1421
|
ok: true,
|
|
1343
1422
|
pid: deps.selfPid,
|
|
1423
|
+
funnelDir: deps.dir,
|
|
1344
1424
|
uptimeMs: deps.uptimeMs(),
|
|
1345
1425
|
clients: deps.broadcaster.listChannels(),
|
|
1346
1426
|
listeners: deps.supervisor.list(),
|
|
@@ -1450,6 +1530,7 @@ var FunnelGatewayServer = class {
|
|
|
1450
1530
|
if (this.server) return this.server;
|
|
1451
1531
|
if (!this.token && !LOOPBACK_HOSTS.has(this.hostname) && !this.allowInsecureHost) throw new Error(`refusing to start gateway: hostname "${this.hostname}" is reachable off-box but no token is set. Set a token, bind to loopback (127.0.0.1), or pass allowInsecureHost: true.`);
|
|
1452
1532
|
const app = this.buildApp();
|
|
1533
|
+
await this.killCompetingSlackIfNeeded();
|
|
1453
1534
|
this.startedAt = this.nowMs();
|
|
1454
1535
|
this.server = Bun.serve({
|
|
1455
1536
|
port: this.port,
|
|
@@ -1504,6 +1585,7 @@ var FunnelGatewayServer = class {
|
|
|
1504
1585
|
if (this.token && !this.tokenMatchesUpgrade(request)) return new Response("unauthorized", { status: 401 });
|
|
1505
1586
|
const requestedChannel = url.searchParams.get("channel") ?? "";
|
|
1506
1587
|
const channel = requestedChannel ? this.resolveChannel(requestedChannel) : null;
|
|
1588
|
+
if (requestedChannel && !channel) return new Response(`unknown channel "${requestedChannel}"`, { status: 404 });
|
|
1507
1589
|
const channelId = channel?.id ?? requestedChannel;
|
|
1508
1590
|
const channelName = channel?.name ?? null;
|
|
1509
1591
|
const connectors = channel?.connectors ?? [];
|
|
@@ -1567,6 +1649,7 @@ var FunnelGatewayServer = class {
|
|
|
1567
1649
|
base.use((c, next) => {
|
|
1568
1650
|
c.set("deps", {
|
|
1569
1651
|
selfPid: this.selfPid,
|
|
1652
|
+
dir: this.dir,
|
|
1570
1653
|
broadcaster: this.broadcaster,
|
|
1571
1654
|
supervisor: this.supervisor,
|
|
1572
1655
|
channels: this.channels,
|
|
@@ -1606,21 +1689,22 @@ var FunnelGatewayServer = class {
|
|
|
1606
1689
|
delivery: channel.delivery
|
|
1607
1690
|
};
|
|
1608
1691
|
}
|
|
1692
|
+
async killCompetingSlackIfNeeded() {
|
|
1693
|
+
if (!this.killCompetingSlack) return;
|
|
1694
|
+
if (!this.channels.listAllConnectors().some((c) => c.type === "slack")) return;
|
|
1695
|
+
const killed = await killCompetingSlackGateways({
|
|
1696
|
+
selfPid: this.selfPid,
|
|
1697
|
+
dir: this.dir,
|
|
1698
|
+
process: this.process,
|
|
1699
|
+
logger: this.logger
|
|
1700
|
+
});
|
|
1701
|
+
if (killed.length > 0) this.logger?.info("killed competing Slack gateway processes", {
|
|
1702
|
+
event_type: "system",
|
|
1703
|
+
action: "kill_competing",
|
|
1704
|
+
pids: killed.join(",")
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1609
1707
|
async bootListeners() {
|
|
1610
|
-
const allConnectors = this.channels.listAllConnectors();
|
|
1611
|
-
if (this.killCompetingSlack && allConnectors.some((c) => c.type === "slack")) {
|
|
1612
|
-
const killed = await killCompetingSlackGateways({
|
|
1613
|
-
selfPid: this.selfPid,
|
|
1614
|
-
dir: this.dir,
|
|
1615
|
-
process: this.process,
|
|
1616
|
-
logger: this.logger
|
|
1617
|
-
});
|
|
1618
|
-
if (killed.length > 0) this.logger?.info("killed competing Slack gateway processes", {
|
|
1619
|
-
event_type: "system",
|
|
1620
|
-
action: "kill_competing",
|
|
1621
|
-
pids: killed.join(",")
|
|
1622
|
-
});
|
|
1623
|
-
}
|
|
1624
1708
|
await this.supervisor.startAll();
|
|
1625
1709
|
for (const entry of this.supervisor.list()) this.logger?.info(`${entry.type} listener started: ${entry.name}`, {
|
|
1626
1710
|
event_type: "system",
|
|
@@ -1885,7 +1969,7 @@ var ConnectorDiagnosticLog = class {};
|
|
|
1885
1969
|
*/
|
|
1886
1970
|
const RAW_PAYLOAD_CAP = 256 * 1024;
|
|
1887
1971
|
/**
|
|
1888
|
-
* Default `ConnectorDiagnosticLog`: three independent `
|
|
1972
|
+
* Default `ConnectorDiagnosticLog`: three independent `FunnelLogSqliteSink`s, one
|
|
1889
1973
|
* per table (raw / processed / connection), in separate files. Each sink
|
|
1890
1974
|
* indexes the columns its queries filter on — `event_id` / `connector_id` /
|
|
1891
1975
|
* `channel_id` for raw, plus `outcome` for processed and `status` for
|
|
@@ -1920,7 +2004,7 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
1920
2004
|
...ageCap,
|
|
1921
2005
|
...rawMax !== void 0 ? { maxRows: rawMax } : {}
|
|
1922
2006
|
};
|
|
1923
|
-
this.raw = new
|
|
2007
|
+
this.raw = new FunnelLogSqliteSink({
|
|
1924
2008
|
path: props.rawPath,
|
|
1925
2009
|
indexes: [
|
|
1926
2010
|
"event_id",
|
|
@@ -1934,7 +2018,7 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
1934
2018
|
}),
|
|
1935
2019
|
...rawCap
|
|
1936
2020
|
});
|
|
1937
|
-
this.processed = new
|
|
2021
|
+
this.processed = new FunnelLogSqliteSink({
|
|
1938
2022
|
path: props.processedPath,
|
|
1939
2023
|
indexes: [
|
|
1940
2024
|
"event_id",
|
|
@@ -1950,7 +2034,7 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
1950
2034
|
}),
|
|
1951
2035
|
...verdictCap
|
|
1952
2036
|
});
|
|
1953
|
-
this.connection = new
|
|
2037
|
+
this.connection = new FunnelLogSqliteSink({
|
|
1954
2038
|
path: props.connectionPath,
|
|
1955
2039
|
indexes: [
|
|
1956
2040
|
"connector_id",
|
|
@@ -2016,7 +2100,7 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
2016
2100
|
});
|
|
2017
2101
|
}
|
|
2018
2102
|
queryRaw(query) {
|
|
2019
|
-
return this.raw.
|
|
2103
|
+
return this.raw.query({
|
|
2020
2104
|
...query.type !== void 0 ? { type: query.type } : {},
|
|
2021
2105
|
...query.limit !== void 0 ? { limit: query.limit } : {},
|
|
2022
2106
|
where: buildWhere(query),
|
|
@@ -2034,7 +2118,7 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
2034
2118
|
queryProcessed(query) {
|
|
2035
2119
|
const where = buildWhere(query);
|
|
2036
2120
|
if (query.outcome !== void 0) where.outcome = query.outcome;
|
|
2037
|
-
return this.processed.
|
|
2121
|
+
return this.processed.query({
|
|
2038
2122
|
...query.type !== void 0 ? { type: query.type } : {},
|
|
2039
2123
|
...query.limit !== void 0 ? { limit: query.limit } : {},
|
|
2040
2124
|
where,
|
|
@@ -2053,7 +2137,7 @@ var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
|
2053
2137
|
queryConnection(query) {
|
|
2054
2138
|
const where = buildWhere(query);
|
|
2055
2139
|
if (query.status !== void 0) where.status = query.status;
|
|
2056
|
-
return this.connection.
|
|
2140
|
+
return this.connection.query({
|
|
2057
2141
|
...query.type !== void 0 ? { type: query.type } : {},
|
|
2058
2142
|
...query.limit !== void 0 ? { limit: query.limit } : {},
|
|
2059
2143
|
where,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as FunnelFileSystem } from "./file-system-Wub9Nto4.js";
|
|
2
|
-
import { i as FunnelTokenPrompter } from "./local-config-sync-
|
|
2
|
+
import { i as FunnelTokenPrompter } from "./local-config-sync-BY20ixEV.js";
|
|
3
3
|
|
|
4
4
|
//#region lib/services/local-config/local-config-json-schema.d.ts
|
|
5
5
|
/**
|
|
@@ -3,7 +3,7 @@ import { t as resolveConnectorToken } from "./resolve-connector-token-CczqG_Ig.j
|
|
|
3
3
|
import { t as FunnelConnectorListener } from "./connector-listener-CpHBecCj.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { WebClient } from "@slack/web-api";
|
|
6
|
-
import { App, LogLevel } from "@slack/bolt";
|
|
6
|
+
import { App, LogLevel, SocketModeReceiver } from "@slack/bolt";
|
|
7
7
|
//#region lib/engine/connectors/slack-adapter.ts
|
|
8
8
|
const toRecord = (value) => {
|
|
9
9
|
const result = {};
|
|
@@ -272,6 +272,7 @@ var FunnelSlackListener = class extends FunnelConnectorListener {
|
|
|
272
272
|
onAppCreated;
|
|
273
273
|
preprocessEvent;
|
|
274
274
|
app = null;
|
|
275
|
+
connected = false;
|
|
275
276
|
constructor(deps) {
|
|
276
277
|
super();
|
|
277
278
|
this.config = deps.config;
|
|
@@ -290,15 +291,25 @@ var FunnelSlackListener = class extends FunnelConnectorListener {
|
|
|
290
291
|
env: this.env,
|
|
291
292
|
label: `${this.config.name}.botToken`
|
|
292
293
|
});
|
|
293
|
-
const
|
|
294
|
-
token: botToken,
|
|
294
|
+
const receiver = new SocketModeReceiver({
|
|
295
295
|
appToken: resolveConnectorToken({
|
|
296
296
|
literal: this.config.appToken,
|
|
297
297
|
envVar: this.config.appTokenEnv,
|
|
298
298
|
env: this.env,
|
|
299
299
|
label: `${this.config.name}.appToken`
|
|
300
300
|
}),
|
|
301
|
-
|
|
301
|
+
logLevel: LogLevel.ERROR,
|
|
302
|
+
autoReconnectEnabled: false
|
|
303
|
+
});
|
|
304
|
+
receiver.client.on("connected", () => {
|
|
305
|
+
this.connected = true;
|
|
306
|
+
});
|
|
307
|
+
receiver.client.on("disconnected", () => {
|
|
308
|
+
this.connected = false;
|
|
309
|
+
});
|
|
310
|
+
const app = new App({
|
|
311
|
+
token: botToken,
|
|
312
|
+
receiver,
|
|
302
313
|
logLevel: LogLevel.ERROR
|
|
303
314
|
});
|
|
304
315
|
let authResult;
|
|
@@ -362,6 +373,7 @@ var FunnelSlackListener = class extends FunnelConnectorListener {
|
|
|
362
373
|
throw error;
|
|
363
374
|
}
|
|
364
375
|
this.app = app;
|
|
376
|
+
this.connected = true;
|
|
365
377
|
this.recordConnection("connected", "");
|
|
366
378
|
}
|
|
367
379
|
async stop() {
|
|
@@ -374,11 +386,12 @@ var FunnelSlackListener = class extends FunnelConnectorListener {
|
|
|
374
386
|
this.logger?.error("Slack stop error", { error: messageOf(error) });
|
|
375
387
|
} finally {
|
|
376
388
|
this.app = null;
|
|
389
|
+
this.connected = false;
|
|
377
390
|
this.recordConnection("stopped", "");
|
|
378
391
|
}
|
|
379
392
|
}
|
|
380
393
|
isAlive() {
|
|
381
|
-
return this.app !== null;
|
|
394
|
+
return this.app !== null && this.connected;
|
|
382
395
|
}
|
|
383
396
|
recordRaw(eventId, event) {
|
|
384
397
|
this.diagnosticLog?.recordRaw({
|
|
@@ -123,6 +123,7 @@ declare class FunnelSlackListener extends FunnelConnectorListener {
|
|
|
123
123
|
private readonly onAppCreated;
|
|
124
124
|
private readonly preprocessEvent;
|
|
125
125
|
private app;
|
|
126
|
+
private connected;
|
|
126
127
|
constructor(deps: Deps);
|
|
127
128
|
start(notify: NotifyFn): Promise<void>;
|
|
128
129
|
stop(): Promise<void>;
|
|
@@ -43,7 +43,7 @@ var FunnelClaude = class {
|
|
|
43
43
|
}
|
|
44
44
|
if (!this.gateway.isRunning()) {
|
|
45
45
|
this.logger?.info(`starting gateway automatically`);
|
|
46
|
-
await this.gateway.start();
|
|
46
|
+
if (!await this.gateway.start()) throw new Error(`gateway failed to start on port ${resolveFunnelPort()}; another funnel daemon (a different repo/scope) may already hold it. See 'fnl gateway logs' and 'ps -o pid,args= | grep funnel-gateway'.`);
|
|
47
47
|
}
|
|
48
48
|
if (options.profileId) this.guard.acquire(options.profileId);
|
|
49
49
|
const session = (options.resume ?? false) && options.profileId ? this.resolveSession(options.profileId, cwd, options.userArgs ?? [], options.env ?? {}) : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interactive-inc/claude-funnel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"description": "Hub CLI that routes external events (Slack / GitHub / Discord) to Claude Code agents through subscription channels over MCP.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -102,6 +102,11 @@
|
|
|
102
102
|
"import": "./dist/connectors/schedule.js",
|
|
103
103
|
"default": "./dist/connectors/schedule.js"
|
|
104
104
|
},
|
|
105
|
+
"./logger": {
|
|
106
|
+
"types": "./dist/logger.d.ts",
|
|
107
|
+
"import": "./dist/logger.js",
|
|
108
|
+
"default": "./dist/logger.js"
|
|
109
|
+
},
|
|
105
110
|
"./bin": "./dist/bin.js",
|
|
106
111
|
"./package.json": "./package.json"
|
|
107
112
|
},
|