@interactive-inc/claude-funnel 0.53.0 → 0.56.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 +3 -3
- package/dist/bin.js +1229 -486
- package/dist/claude.d.ts +22 -5
- package/dist/claude.js +455 -168
- package/dist/{connector-adapter-CePYBTgW.d.ts → connector-adapter-1PxjN-Uk.d.ts} +1 -1
- package/dist/{connector-adapter-D5Utumgz.js → connector-adapter-qwXLjQId.js} +1 -1
- package/dist/{connector-listener-DU54DN-f.js → connector-listener-CpHBecCj.js} +1 -1
- package/dist/connectors/discord.d.ts +6 -6
- package/dist/connectors/discord.js +2 -2
- package/dist/connectors/gh.d.ts +6 -6
- package/dist/connectors/gh.js +2 -2
- package/dist/connectors/schedule.d.ts +12 -2
- package/dist/connectors/schedule.js +2 -2
- package/dist/connectors/slack.d.ts +3 -3
- package/dist/connectors/slack.js +2 -2
- package/dist/{connector-diagnostic-log-yTOojKUR.d.ts → diagnostic-log-Bxe7Bbvw.d.ts} +2 -2
- package/dist/diagnostic-sql-reader-CzYgZpq2.js +83 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +2 -0
- package/dist/{discord-connector-schema-CBDyGdOI.js → discord-connector-schema-B_N6IXLz.js} +1 -1
- package/dist/{discord-connector-schema-R0Uu-3ns.d.ts → discord-connector-schema-CPgcZkXh.d.ts} +1 -1
- package/dist/{discord-listener-_jSE3HsQ.js → discord-listener-C0MoKdQO.js} +6 -6
- package/dist/docs.d.ts +2 -0
- package/dist/docs.js +2 -0
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +2 -0
- package/dist/{file-process-guard-DMeLB6Zd.d.ts → file-process-guard-DI1742H5.d.ts} +5 -4
- package/dist/funnel-diagnostics-BpKYrMSu.js +300 -0
- package/dist/funnel-diagnostics-qWy5tPSq.d.ts +176 -0
- package/dist/funnel-docs-dXPokzr5.d.ts +18 -0
- package/dist/funnel-docs-ng5K8w4j.js +653 -0
- package/dist/funnel-doctor-BF3Rdgk0.d.ts +34 -0
- package/dist/funnel-doctor-CApCezTq.js +82 -0
- package/dist/funnel-recovery-BUBsu7WX.d.ts +101 -0
- package/dist/funnel-recovery-D9CxD5Zs.js +134 -0
- package/dist/gateway/daemon.js +838 -252
- package/dist/{gateway-base-url-ssk_He5G.js → gateway-base-url-6foMXfFf.js} +5 -5
- package/dist/gateway.d.ts +2 -2
- package/dist/gateway.js +2 -2
- package/dist/{gh-connector-schema-eoTtHbY6.d.ts → gh-connector-schema-CU1ojfIF.d.ts} +1 -1
- package/dist/{gh-connector-schema-o3Q1-ojL.js → gh-connector-schema-DUcZgN2Q.js} +1 -1
- package/dist/{gh-listener-DH-fClQm.js → gh-listener-Dsx6AmhH.js} +5 -5
- package/dist/{index-DF5VmCPJ.d.ts → index-CrngHrne.d.ts} +104 -607
- package/dist/index.d.ts +16 -11
- package/dist/index.js +509 -973
- package/dist/{local-config-json-schema-D8i-BogY.js → local-config-json-schema-DE1zkMcb.js} +12 -8
- package/dist/{local-config-sync-Cq39mT6p.d.ts → local-config-sync-B8b04LrZ.d.ts} +21 -16
- package/dist/local-config.d.ts +2 -2
- package/dist/local-config.js +2 -2
- package/dist/{memory-connector-diagnostic-log-COUWCsT_.js → memory-diagnostic-log-BbFVqDzz.js} +30 -95
- package/dist/{memory-token-prompter-CKV7VBM5.d.ts → memory-token-prompter-Lo3YRDzq.d.ts} +4 -4
- package/dist/{memory-token-prompter-Q7Snwsv2.js → memory-token-prompter-vBXxY20-.js} +2 -2
- package/dist/{profiles-f0mNmEyP.d.ts → profiles-EHTeCOqB.d.ts} +3 -2
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +1 -1
- package/dist/recovery.d.ts +2 -0
- package/dist/recovery.js +2 -0
- package/dist/{resolve-connector-token-BHmZLRrV.js → resolve-connector-token-CczqG_Ig.js} +1 -1
- package/dist/{schedule-connector-schema-iCI61gzU.js → schedule-connector-schema-B_xO5z5B.js} +1 -1
- package/dist/{schedule-listener-CUyUFFR1.d.ts → schedule-listener-DKh0hnkK.d.ts} +5 -5
- package/dist/{schedule-listener-ePAjians.js → schedule-listener-DP9Jhc6U.js} +14 -4
- package/dist/settings-reader-CBrgz01o.d.ts +18 -0
- package/dist/{settings-reader-BSU6JyvM.d.ts → settings-schema-zhnMIa8I.d.ts} +1 -16
- package/dist/{slack-connector-schema-BCNWluHM.js → slack-connector-schema-C1zEf4TG.js} +1 -1
- package/dist/{slack-listener-Bv5xI9gC.d.ts → slack-listener-COQA8wAZ.d.ts} +4 -4
- package/dist/{slack-listener-ClQuHhEF.js → slack-listener-DUKPcpJH.js} +7 -7
- package/dist/{mcp-QeNCBhOD.js → yaml-render-OhUN-qkS.js} +52 -34
- package/package.json +21 -1
- /package/dist/{file-system-BeOKXjlV.d.ts → file-system-Wub9Nto4.d.ts} +0 -0
- /package/dist/{process-runner-DfniuWVU.d.ts → process-runner-D5I_jhYQ.d.ts} +0 -0
- /package/dist/{profiles-wMRnjSid.js → profiles-MnXvYfZF.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
import { r as FunnelDiscordAdapter, t as FunnelDiscordListener } from "./discord-listener-
|
|
2
|
-
import { t as FunnelConnectorListener } from "./connector-listener-
|
|
1
|
+
import { r as FunnelDiscordAdapter, t as FunnelDiscordListener } from "./discord-listener-C0MoKdQO.js";
|
|
2
|
+
import { t as FunnelConnectorListener } from "./connector-listener-CpHBecCj.js";
|
|
3
3
|
import { t as FunnelLogger } from "./logger-BP6SisKt.js";
|
|
4
|
-
import { n as NodeFunnelProcessRunner, r as FunnelProcessRunner, t as ghConnectorSchema } from "./gh-connector-schema-
|
|
5
|
-
import { n as FunnelGhAdapter, t as FunnelGhListener } from "./gh-listener-
|
|
6
|
-
import { n as ScheduleStateStore, t as FunnelScheduleListener } from "./schedule-listener-
|
|
4
|
+
import { n as NodeFunnelProcessRunner, r as FunnelProcessRunner, t as ghConnectorSchema } from "./gh-connector-schema-DUcZgN2Q.js";
|
|
5
|
+
import { n as FunnelGhAdapter, t as FunnelGhListener } from "./gh-listener-Dsx6AmhH.js";
|
|
6
|
+
import { n as ScheduleStateStore, t as FunnelScheduleListener } from "./schedule-listener-DP9Jhc6U.js";
|
|
7
7
|
import { t as FunnelFileSystem } from "./file-system-PWKKU7lA.js";
|
|
8
8
|
import { t as NodeFunnelFileSystem } from "./node-file-system-BcrmWN9I.js";
|
|
9
|
-
import { n as FunnelSlackEventProcessor, r as FunnelSlackAdapter, t as FunnelSlackListener } from "./slack-listener-
|
|
9
|
+
import { n as FunnelSlackEventProcessor, r as FunnelSlackAdapter, t as FunnelSlackListener } from "./slack-listener-DUKPcpJH.js";
|
|
10
10
|
import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-DPqrpV7s.js";
|
|
11
|
-
import { a as SETTINGS_PATH, c as SETTINGS_VERSION, d as profileConfigSchema, f as settingsSchema, i as FunnelSettingsStore, l as channelConfigSchema, m as NodeFunnelIdGenerator, n as DEFAULT_GATEWAY_PORT, o as resolveFunnelDir, p as connectorConfigSchema, r as FUNNEL_DIR, s as resolveFunnelPort, t as gatewayLoopbackUrl, u as channelDeliveryModeSchema } from "./gateway-base-url-
|
|
12
|
-
import { t as discordConnectorSchema } from "./discord-connector-schema-
|
|
13
|
-
import { n as scheduleConnectorSchema, r as scheduleEntrySchema, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-
|
|
14
|
-
import { t as slackConnectorSchema } from "./slack-connector-schema-
|
|
15
|
-
import { a as
|
|
16
|
-
import { a as
|
|
17
|
-
import { t as
|
|
18
|
-
import {
|
|
11
|
+
import { a as SETTINGS_PATH, c as SETTINGS_VERSION, d as profileConfigSchema, f as settingsSchema, i as FunnelSettingsStore, l as channelConfigSchema, m as NodeFunnelIdGenerator, n as DEFAULT_GATEWAY_PORT, o as resolveFunnelDir, p as connectorConfigSchema, r as FUNNEL_DIR, s as resolveFunnelPort, t as gatewayLoopbackUrl, u as channelDeliveryModeSchema } from "./gateway-base-url-6foMXfFf.js";
|
|
12
|
+
import { t as discordConnectorSchema } from "./discord-connector-schema-B_N6IXLz.js";
|
|
13
|
+
import { n as scheduleConnectorSchema, r as scheduleEntrySchema, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-B_xO5z5B.js";
|
|
14
|
+
import { t as slackConnectorSchema } from "./slack-connector-schema-C1zEf4TG.js";
|
|
15
|
+
import { a as FunnelMcp, o as FileProcessGuard, s as FunnelClaude, t as renderYaml } from "./yaml-render-OhUN-qkS.js";
|
|
16
|
+
import { a as toDiagnosticEvent, i as toDiagnosticConnectionError, n as previewOf, r as queryRows, t as FunnelDiagnostics } from "./funnel-diagnostics-BpKYrMSu.js";
|
|
17
|
+
import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-CzYgZpq2.js";
|
|
18
|
+
import { t as FunnelDoctor } from "./funnel-doctor-CApCezTq.js";
|
|
19
|
+
import { t as FunnelDocs } from "./funnel-docs-ng5K8w4j.js";
|
|
20
|
+
import { a as FunnelLocalConfig, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigSync, t as funnelJsonSchema } from "./local-config-json-schema-DE1zkMcb.js";
|
|
21
|
+
import { t as FunnelProfiles } from "./profiles-MnXvYfZF.js";
|
|
22
|
+
import { t as FunnelRecovery } from "./funnel-recovery-D9CxD5Zs.js";
|
|
23
|
+
import { C as funnelTmpDir, S as publishResponseSchema, _ as funnelEventSchema, a as connectorConnectionEventSchema, b as FunnelChannelPublisher, c as MemoryFunnelEventLog, d as DEFAULT_GATEWAY_TOKEN_PATH, f as FunnelGatewayToken, g as FunnelEventLog, h as SqliteFunnelEventLog, i as ConnectorDiagnosticLog, l as channelWsProtocols, m as FunnelListenerSupervisor, n as SqliteConnectorDiagnosticLog, o as connectorProcessedEventSchema, p as FunnelGatewayServer, r as CONNECTOR_CONNECTION_STATUSES, s as connectorRawEventSchema, t as MemoryConnectorDiagnosticLog, u as channelWsUrl, v as FunnelBroadcaster, x as publishRequestSchema, y as requireBearerToken } from "./memory-diagnostic-log-BbFVqDzz.js";
|
|
19
24
|
import { dirname, join, resolve } from "node:path";
|
|
20
25
|
import { hc } from "hono/client";
|
|
21
26
|
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
@@ -24,7 +29,8 @@ import { fileURLToPath } from "node:url";
|
|
|
24
29
|
import { createFactory } from "hono/factory";
|
|
25
30
|
import { HTTPException } from "hono/http-exception";
|
|
26
31
|
import { zValidator } from "@hono/zod-validator";
|
|
27
|
-
|
|
32
|
+
import { Hono } from "hono";
|
|
33
|
+
//#region lib/engine/connectors/connector-factory.ts
|
|
28
34
|
const defaultFs$1 = new NodeFunnelFileSystem();
|
|
29
35
|
const defaultProcess$1 = new NodeFunnelProcessRunner();
|
|
30
36
|
/**
|
|
@@ -994,84 +1000,6 @@ var FunnelListenersClient = class {
|
|
|
994
1000
|
}
|
|
995
1001
|
};
|
|
996
1002
|
//#endregion
|
|
997
|
-
//#region lib/gateway/funnel-debug.ts
|
|
998
|
-
const isGatewayStatusResponse$1 = (value) => {
|
|
999
|
-
if (value === null || typeof value !== "object") return false;
|
|
1000
|
-
if (!("clients" in value) || !Array.isArray(value.clients)) return false;
|
|
1001
|
-
if (!("listeners" in value) || !Array.isArray(value.listeners)) return false;
|
|
1002
|
-
return true;
|
|
1003
|
-
};
|
|
1004
|
-
const buildFunnelDebugReport = async (deps, channelFilter) => {
|
|
1005
|
-
const gatewayStatus = deps.gateway.getStatus();
|
|
1006
|
-
const report = {
|
|
1007
|
-
gateway: {
|
|
1008
|
-
running: gatewayStatus.running,
|
|
1009
|
-
pid: gatewayStatus.pid,
|
|
1010
|
-
port: gatewayStatus.running ? gatewayStatus.port : null,
|
|
1011
|
-
uptimeMs: null
|
|
1012
|
-
},
|
|
1013
|
-
channels: [],
|
|
1014
|
-
recentEvents: null
|
|
1015
|
-
};
|
|
1016
|
-
const allChannels = deps.channels.list();
|
|
1017
|
-
const filteredChannels = channelFilter ? allChannels.filter((ch) => ch.name === channelFilter) : allChannels;
|
|
1018
|
-
let gatewayData = null;
|
|
1019
|
-
if (gatewayStatus.running) {
|
|
1020
|
-
const res = await fetch(`http://127.0.0.1:${gatewayStatus.port}/status`).catch(() => null);
|
|
1021
|
-
if (res && res.ok) {
|
|
1022
|
-
const body = await res.json();
|
|
1023
|
-
if (isGatewayStatusResponse$1(body)) {
|
|
1024
|
-
gatewayData = body;
|
|
1025
|
-
report.gateway.uptimeMs = body.uptimeMs;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
for (const ch of filteredChannels) {
|
|
1030
|
-
const listenerEntry = gatewayData?.listeners.find((l) => l.channelName === ch.name) ?? null;
|
|
1031
|
-
const listener = listenerEntry ? {
|
|
1032
|
-
alive: listenerEntry.alive,
|
|
1033
|
-
events: listenerEntry.events,
|
|
1034
|
-
errors: listenerEntry.errors,
|
|
1035
|
-
lastEventAt: listenerEntry.lastEventAt
|
|
1036
|
-
} : null;
|
|
1037
|
-
const claudeClients = (gatewayData?.clients ?? []).filter((cl) => cl.channelName === ch.name || cl.channel === ch.name);
|
|
1038
|
-
report.channels.push({
|
|
1039
|
-
name: ch.name,
|
|
1040
|
-
connectors: ch.connectors.map((conn) => conn.name),
|
|
1041
|
-
listener,
|
|
1042
|
-
claudeConnected: claudeClients.length > 0,
|
|
1043
|
-
claudeClientCount: claudeClients.length
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
const rawPath = join(deps.tmpDir, "connector-raw.db");
|
|
1047
|
-
const processedPath = join(deps.tmpDir, "connector-processed.db");
|
|
1048
|
-
const connectionPath = join(deps.tmpDir, "connector-connection.db");
|
|
1049
|
-
if (existsSync(rawPath) && existsSync(processedPath) && existsSync(connectionPath)) {
|
|
1050
|
-
const reader = new ConnectorDiagnosticSqlReader({
|
|
1051
|
-
rawPath,
|
|
1052
|
-
processedPath,
|
|
1053
|
-
connectionPath
|
|
1054
|
-
});
|
|
1055
|
-
const filteredChannelId = channelFilter ? allChannels.find((ch) => ch.name === channelFilter)?.id ?? null : null;
|
|
1056
|
-
const sql = filteredChannelId ? "SELECT ts, outcome, payload FROM processed WHERE channel_id = ? ORDER BY seq DESC LIMIT 20" : "SELECT ts, outcome, payload FROM processed ORDER BY seq DESC LIMIT 20";
|
|
1057
|
-
const params = filteredChannelId ? [filteredChannelId] : [];
|
|
1058
|
-
const rows = reader.query(sql, params);
|
|
1059
|
-
reader.close();
|
|
1060
|
-
if (!(rows instanceof Error)) report.recentEvents = rows.map((row) => {
|
|
1061
|
-
const ts = typeof row.ts === "number" ? row.ts : 0;
|
|
1062
|
-
const outcome = typeof row.outcome === "string" ? row.outcome : "";
|
|
1063
|
-
const payload = typeof row.payload === "string" ? row.payload : null;
|
|
1064
|
-
return {
|
|
1065
|
-
ts,
|
|
1066
|
-
outcome,
|
|
1067
|
-
payload,
|
|
1068
|
-
preview: payload ? payload.slice(0, 120) : null
|
|
1069
|
-
};
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
return report;
|
|
1073
|
-
};
|
|
1074
|
-
//#endregion
|
|
1075
1003
|
//#region lib/funnel.ts
|
|
1076
1004
|
const SANDBOX_DIR = "/sandbox/.funnel";
|
|
1077
1005
|
const SANDBOX_TMP_DIR = "/sandbox/tmp";
|
|
@@ -1105,6 +1033,10 @@ var Funnel = class Funnel {
|
|
|
1105
1033
|
profiles;
|
|
1106
1034
|
localConfig;
|
|
1107
1035
|
localConfigSync;
|
|
1036
|
+
diagnostics;
|
|
1037
|
+
recovery;
|
|
1038
|
+
doctor;
|
|
1039
|
+
docs;
|
|
1108
1040
|
fs;
|
|
1109
1041
|
process;
|
|
1110
1042
|
logger;
|
|
@@ -1193,6 +1125,23 @@ var Funnel = class Funnel {
|
|
|
1193
1125
|
process,
|
|
1194
1126
|
logger: this.logger
|
|
1195
1127
|
});
|
|
1128
|
+
this.diagnostics = new FunnelDiagnostics({
|
|
1129
|
+
gateway: this.gateway,
|
|
1130
|
+
gatewayToken: this.gatewayToken,
|
|
1131
|
+
channels: this.channels,
|
|
1132
|
+
publisher: this.publisher,
|
|
1133
|
+
tmpDir
|
|
1134
|
+
});
|
|
1135
|
+
this.recovery = new FunnelRecovery({
|
|
1136
|
+
gateway: this.gateway,
|
|
1137
|
+
listeners: this.listeners,
|
|
1138
|
+
channels: this.channels
|
|
1139
|
+
});
|
|
1140
|
+
this.doctor = new FunnelDoctor({
|
|
1141
|
+
diagnostics: this.diagnostics,
|
|
1142
|
+
recovery: this.recovery
|
|
1143
|
+
});
|
|
1144
|
+
this.docs = new FunnelDocs();
|
|
1196
1145
|
Object.freeze(this);
|
|
1197
1146
|
}
|
|
1198
1147
|
/**
|
|
@@ -1261,13 +1210,6 @@ var Funnel = class Funnel {
|
|
|
1261
1210
|
] : ["bun", gatewayScript];
|
|
1262
1211
|
return this.process.attach(command);
|
|
1263
1212
|
}
|
|
1264
|
-
async debug(channelName) {
|
|
1265
|
-
return buildFunnelDebugReport({
|
|
1266
|
-
gateway: this.gateway,
|
|
1267
|
-
channels: this.channels,
|
|
1268
|
-
tmpDir: this.paths.tmpDir
|
|
1269
|
-
}, channelName ?? null);
|
|
1270
|
-
}
|
|
1271
1213
|
gatewayClient() {
|
|
1272
1214
|
const { port } = this.gateway.getStatus();
|
|
1273
1215
|
return hc(`http://127.0.0.1:${port}`);
|
|
@@ -1314,6 +1256,67 @@ var NoopFunnelLogger = class extends FunnelLogger {
|
|
|
1314
1256
|
error() {}
|
|
1315
1257
|
};
|
|
1316
1258
|
//#endregion
|
|
1259
|
+
//#region lib/gateway/service-routes.ts
|
|
1260
|
+
/**
|
|
1261
|
+
* Mountable Hono app that exposes the service layer (`FunnelDiagnostics` +
|
|
1262
|
+
* `FunnelDoctor`) over loopback HTTP. The MCP server, which lives in a
|
|
1263
|
+
* different process, calls these endpoints to drive the autonomous
|
|
1264
|
+
* troubleshooting loop. The CLI bypasses HTTP and calls the same services
|
|
1265
|
+
* directly through the in-process funnel facade, so CLI and MCP share one
|
|
1266
|
+
* code path.
|
|
1267
|
+
*/
|
|
1268
|
+
const buildServiceRoutes = (deps) => {
|
|
1269
|
+
const app = new Hono();
|
|
1270
|
+
if (deps.token) {
|
|
1271
|
+
app.use("/diagnostics", requireBearerToken({ expected: deps.token }));
|
|
1272
|
+
app.use("/diagnostics/*", requireBearerToken({ expected: deps.token }));
|
|
1273
|
+
app.use("/doctor", requireBearerToken({ expected: deps.token }));
|
|
1274
|
+
}
|
|
1275
|
+
app.get("/diagnostics", async (c) => {
|
|
1276
|
+
const channel = c.req.query("channel");
|
|
1277
|
+
if (c.req.query("all") !== void 0) {
|
|
1278
|
+
const report = await deps.diagnostics.diagnoseAll();
|
|
1279
|
+
return c.json(report);
|
|
1280
|
+
}
|
|
1281
|
+
const report = await deps.diagnostics.diagnose(channel ?? void 0);
|
|
1282
|
+
if (!report) return c.json({ error: "channel not found" }, 404);
|
|
1283
|
+
return c.json(report);
|
|
1284
|
+
});
|
|
1285
|
+
app.get("/diagnostics/events", async (c) => {
|
|
1286
|
+
const channel = c.req.query("channel") ?? null;
|
|
1287
|
+
const limit = Number(c.req.query("limit") ?? "20");
|
|
1288
|
+
const events = await deps.diagnostics.recentEvents(channel, limit);
|
|
1289
|
+
return c.json(events);
|
|
1290
|
+
});
|
|
1291
|
+
app.get("/diagnostics/dropped", async (c) => {
|
|
1292
|
+
const channel = c.req.query("channel") ?? null;
|
|
1293
|
+
const limit = Number(c.req.query("limit") ?? "20");
|
|
1294
|
+
const events = await deps.diagnostics.droppedEvents(channel, limit);
|
|
1295
|
+
return c.json(events);
|
|
1296
|
+
});
|
|
1297
|
+
app.get("/diagnostics/errors", async (c) => {
|
|
1298
|
+
const channel = c.req.query("channel") ?? null;
|
|
1299
|
+
const limit = Number(c.req.query("limit") ?? "20");
|
|
1300
|
+
const errors = await deps.diagnostics.connectionErrors(channel, limit);
|
|
1301
|
+
return c.json(errors);
|
|
1302
|
+
});
|
|
1303
|
+
app.post("/diagnostics/replay", async (c) => {
|
|
1304
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1305
|
+
const channel = typeof body.channel === "string" ? body.channel : null;
|
|
1306
|
+
const seq = typeof body.seq === "number" ? body.seq : void 0;
|
|
1307
|
+
if (!channel) return c.json({ error: "channel is required" }, 400);
|
|
1308
|
+
const result = await deps.diagnostics.replay(channel, seq);
|
|
1309
|
+
return c.json(result);
|
|
1310
|
+
});
|
|
1311
|
+
app.post("/doctor", async (c) => {
|
|
1312
|
+
const body = await c.req.json().catch(() => ({}));
|
|
1313
|
+
const mode = body.mode === "safe" || body.mode === "aggressive" || body.mode === "off" ? body.mode : "off";
|
|
1314
|
+
const report = await deps.doctor.run(mode);
|
|
1315
|
+
return c.json(report);
|
|
1316
|
+
});
|
|
1317
|
+
return app;
|
|
1318
|
+
};
|
|
1319
|
+
//#endregion
|
|
1317
1320
|
//#region lib/cli/factory.ts
|
|
1318
1321
|
const factory = createFactory();
|
|
1319
1322
|
//#endregion
|
|
@@ -1604,13 +1607,15 @@ const channelsConnectorsSetHandler = factory.createHandlers(zValidator$1("param"
|
|
|
1604
1607
|
const channelsConnectorsShowHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
1605
1608
|
channel: z.string(),
|
|
1606
1609
|
connector: z.string()
|
|
1607
|
-
})), zValidator$1("query", z.object({}), `funnel channels <channel> connectors show <connector>
|
|
1610
|
+
})), zValidator$1("query", z.object({}), `funnel channels <channel> connectors show <connector> / show connector config
|
|
1611
|
+
|
|
1612
|
+
usage / funnel channels <channel> connectors show <connector>
|
|
1608
1613
|
|
|
1609
|
-
|
|
1614
|
+
output / valid YAML`), (c) => {
|
|
1610
1615
|
const param = c.req.valid("param");
|
|
1611
1616
|
const connector = c.env.funnel.channels.getConnector(param.channel, param.connector);
|
|
1612
1617
|
if (!connector) throw new HTTPException(404, { message: `connector "${param.connector}" not found in channel "${param.channel}"` });
|
|
1613
|
-
return c.text(
|
|
1618
|
+
return c.text(renderYaml(connector));
|
|
1614
1619
|
});
|
|
1615
1620
|
//#endregion
|
|
1616
1621
|
//#region lib/cli/routes/channels.$channel.connectors.rename.ts
|
|
@@ -1641,23 +1646,32 @@ const channelsConnectorRenameHelpHandler = factory.createHandlers((c) => c.text(
|
|
|
1641
1646
|
const channelsConnectorsRequestHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
1642
1647
|
channel: z.string(),
|
|
1643
1648
|
connector: z.string()
|
|
1644
|
-
})), zValidator$1("query", z.object({
|
|
1649
|
+
})), zValidator$1("query", z.object({
|
|
1650
|
+
method: z.string(),
|
|
1651
|
+
path: z.string().optional()
|
|
1652
|
+
}).passthrough(), `funnel channels <channel> connectors <connector> request / call a connector's outbound API
|
|
1645
1653
|
|
|
1646
|
-
usage
|
|
1654
|
+
usage / funnel channels <channel> connectors <connector> request --method=<m> [--path=<p>] [--key=value ...]
|
|
1655
|
+
|
|
1656
|
+
options:
|
|
1657
|
+
--method / slack: API method (e.g. chat.postMessage). gh/discord: HTTP verb (GET/POST/...).
|
|
1658
|
+
--path / gh/discord: endpoint (e.g. repos/o/r/issues). Omit for slack (defaults to --method).
|
|
1659
|
+
|
|
1660
|
+
output / valid YAML (or raw text when the adapter returns text)`), async (c) => {
|
|
1647
1661
|
const param = c.req.valid("param");
|
|
1648
1662
|
const query = c.req.valid("query");
|
|
1649
1663
|
const funnel = c.env.funnel;
|
|
1650
1664
|
const passthrough = {};
|
|
1651
1665
|
for (const [k, v] of new URL(c.req.url).searchParams) {
|
|
1652
|
-
if (k === "method") continue;
|
|
1666
|
+
if (k === "method" || k === "path") continue;
|
|
1653
1667
|
passthrough[k] = v;
|
|
1654
1668
|
}
|
|
1655
1669
|
const response = await funnel.channels.call(param.channel, param.connector, {
|
|
1656
1670
|
method: query.method,
|
|
1657
|
-
path: query.method,
|
|
1671
|
+
path: query.path ?? query.method,
|
|
1658
1672
|
body: passthrough
|
|
1659
1673
|
});
|
|
1660
|
-
return c.text(typeof response === "string" ? response :
|
|
1674
|
+
return c.text(typeof response === "string" ? response : renderYaml(response));
|
|
1661
1675
|
});
|
|
1662
1676
|
const channelsConnectorsSchedulesGroupHandler = factory.createHandlers(zValidator$1("param", z.object({
|
|
1663
1677
|
channel: z.string(),
|
|
@@ -1685,7 +1699,7 @@ const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$
|
|
|
1685
1699
|
})), zValidator$1("query", z.object({
|
|
1686
1700
|
cron: z.string(),
|
|
1687
1701
|
prompt: z.string(),
|
|
1688
|
-
enabled: z.
|
|
1702
|
+
enabled: z.enum(["true", "false"]).optional(),
|
|
1689
1703
|
"catchup-policy": scheduleCatchupPolicySchema.optional()
|
|
1690
1704
|
})), async (c) => {
|
|
1691
1705
|
const param = c.req.valid("param");
|
|
@@ -1695,7 +1709,7 @@ const channelsConnectorsSchedulesAddHandler = factory.createHandlers(zValidator$
|
|
|
1695
1709
|
id: param.id,
|
|
1696
1710
|
cron: query.cron,
|
|
1697
1711
|
prompt: query.prompt,
|
|
1698
|
-
...query.enabled !== void 0 ? { enabled: query.enabled } : {},
|
|
1712
|
+
...query.enabled !== void 0 ? { enabled: query.enabled === "true" } : {},
|
|
1699
1713
|
...query["catchup-policy"] !== void 0 ? { catchupPolicy: query["catchup-policy"] } : {}
|
|
1700
1714
|
});
|
|
1701
1715
|
await funnel.listeners.restart(param.channel, param.connector);
|
|
@@ -1807,48 +1821,46 @@ modes:
|
|
|
1807
1821
|
c.env.funnel.channels.setDelivery(param.channel, param.mode);
|
|
1808
1822
|
return c.text(`channel "${param.channel}" delivery set to ${param.mode}`);
|
|
1809
1823
|
});
|
|
1810
|
-
const channelsShowHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({}), `funnel channels <name>
|
|
1824
|
+
const channelsShowHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({}), `funnel channels <name> / show channel details
|
|
1825
|
+
|
|
1826
|
+
output / valid YAML`), (c) => {
|
|
1811
1827
|
const param = c.req.valid("param");
|
|
1812
1828
|
const channel = c.env.funnel.channels.get(param.channel);
|
|
1813
1829
|
if (!channel) throw new HTTPException(404, { message: `channel "${param.channel}" not found` });
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1830
|
+
return c.text(renderYaml({
|
|
1831
|
+
id: channel.id,
|
|
1832
|
+
name: channel.name,
|
|
1833
|
+
delivery: channel.delivery,
|
|
1834
|
+
connectors: channel.connectors.map((conn) => ({
|
|
1835
|
+
id: conn.id,
|
|
1836
|
+
name: conn.name,
|
|
1837
|
+
type: conn.type
|
|
1838
|
+
}))
|
|
1839
|
+
}));
|
|
1823
1840
|
});
|
|
1824
|
-
const channelsGroupHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
1825
|
-
"true",
|
|
1826
|
-
"false",
|
|
1827
|
-
""
|
|
1828
|
-
]).optional() }), `funnel channels — manage subscription boxes
|
|
1829
|
-
|
|
1830
|
-
usage: funnel channels [--json]
|
|
1841
|
+
const channelsGroupHandler = factory.createHandlers(zValidator$1("query", z.object({}), `funnel channels / manage subscription boxes
|
|
1831
1842
|
|
|
1832
|
-
|
|
1833
|
-
--json output as JSON array (machine-readable, useful for Claude)
|
|
1843
|
+
usage / funnel channels [subcommand]
|
|
1834
1844
|
|
|
1835
1845
|
subcommands:
|
|
1836
|
-
(none)
|
|
1837
|
-
add <name>
|
|
1838
|
-
remove <name>
|
|
1839
|
-
<name>
|
|
1840
|
-
<name> connectors
|
|
1841
|
-
<name> connectors add <c> --type=...
|
|
1846
|
+
(none) / list every channel with its connectors
|
|
1847
|
+
add <name> / create a channel
|
|
1848
|
+
remove <name> / delete a channel
|
|
1849
|
+
<name> / show one channel
|
|
1850
|
+
<name> connectors / list connectors
|
|
1851
|
+
<name> connectors add <c> --type=... / add a connector
|
|
1852
|
+
|
|
1853
|
+
output / valid YAML
|
|
1854
|
+
|
|
1855
|
+
programmable / funnel.channels.list() / .add() / .remove() / .addConnector() / .removeConnector()
|
|
1842
1856
|
|
|
1843
1857
|
examples:
|
|
1844
1858
|
funnel channels
|
|
1845
|
-
funnel channels --json
|
|
1846
1859
|
funnel channels add prod-inbox
|
|
1847
1860
|
funnel channels prod-inbox connectors add prod-slack --type=slack --bot-token=xoxb-... --app-token=xapp-...
|
|
1848
1861
|
funnel channels prod-inbox`), (c) => {
|
|
1849
|
-
const query = c.req.valid("query");
|
|
1850
1862
|
const channels = c.env.funnel.channels.list();
|
|
1851
|
-
|
|
1863
|
+
return c.text(renderYaml({ channels: channels.map((ch) => ({
|
|
1852
1864
|
id: ch.id,
|
|
1853
1865
|
name: ch.name,
|
|
1854
1866
|
delivery: ch.delivery,
|
|
@@ -1857,14 +1869,7 @@ examples:
|
|
|
1857
1869
|
name: conn.name,
|
|
1858
1870
|
type: conn.type
|
|
1859
1871
|
}))
|
|
1860
|
-
})));
|
|
1861
|
-
if (channels.length === 0) return c.text("no channels");
|
|
1862
|
-
const lines = channels.map((ch) => {
|
|
1863
|
-
const names = ch.connectors.map((conn) => conn.name);
|
|
1864
|
-
const connectors = names.length > 0 ? names.join(", ") : "(none)";
|
|
1865
|
-
return `${ch.name} [${connectors}]`;
|
|
1866
|
-
});
|
|
1867
|
-
return c.text(lines.join("\n"));
|
|
1872
|
+
})) }));
|
|
1868
1873
|
});
|
|
1869
1874
|
//#endregion
|
|
1870
1875
|
//#region lib/cli/routes/channels.validate.ts
|
|
@@ -1929,42 +1934,29 @@ const validateConnector = (connector) => {
|
|
|
1929
1934
|
}
|
|
1930
1935
|
return issues;
|
|
1931
1936
|
};
|
|
1932
|
-
const channelsValidateHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({
|
|
1933
|
-
"true",
|
|
1934
|
-
"false",
|
|
1935
|
-
""
|
|
1936
|
-
]).optional() })), (c) => {
|
|
1937
|
+
const channelsValidateHandler = factory.createHandlers(zValidator$1("param", z.object({ channel: z.string() })), zValidator$1("query", z.object({})), (c) => {
|
|
1937
1938
|
const param = c.req.valid("param");
|
|
1938
|
-
const
|
|
1939
|
-
const funnel = c.env.funnel;
|
|
1940
|
-
const isJson = query.json === "true" || query.json === "";
|
|
1941
|
-
const channel = funnel.channels.get(param.channel);
|
|
1939
|
+
const channel = c.env.funnel.channels.get(param.channel);
|
|
1942
1940
|
if (!channel) throw new HTTPException(404, { message: `channel "${param.channel}" not found` });
|
|
1943
|
-
if (channel.connectors.length === 0) {
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
});
|
|
1953
|
-
return c.text(`⚠ ${channel.name}: no connectors configured`);
|
|
1954
|
-
}
|
|
1941
|
+
if (channel.connectors.length === 0) return c.text(renderYaml({
|
|
1942
|
+
channel: channel.name,
|
|
1943
|
+
valid: false,
|
|
1944
|
+
issues: [{
|
|
1945
|
+
connector: null,
|
|
1946
|
+
field: "connectors",
|
|
1947
|
+
message: "no connectors configured"
|
|
1948
|
+
}]
|
|
1949
|
+
}));
|
|
1955
1950
|
const allIssues = [];
|
|
1956
1951
|
for (const connector of channel.connectors) {
|
|
1957
1952
|
const issues = validateConnector(connector);
|
|
1958
1953
|
allIssues.push(...issues);
|
|
1959
1954
|
}
|
|
1960
|
-
|
|
1955
|
+
return c.text(renderYaml({
|
|
1961
1956
|
channel: channel.name,
|
|
1962
1957
|
valid: allIssues.length === 0,
|
|
1963
1958
|
issues: allIssues
|
|
1964
|
-
});
|
|
1965
|
-
if (allIssues.length === 0) return c.text(`✓ ${channel.name}: all connectors valid`);
|
|
1966
|
-
const lines = allIssues.map((issue) => `✗ ${channel.name}/${issue.connector}: ${issue.message}`);
|
|
1967
|
-
return c.text(lines.join("\n"));
|
|
1959
|
+
}));
|
|
1968
1960
|
});
|
|
1969
1961
|
//#endregion
|
|
1970
1962
|
//#region lib/cli/routes/claude.ts
|
|
@@ -1991,7 +1983,14 @@ funnel-specific options (everything else passes through to claude verbatim):
|
|
|
1991
1983
|
|
|
1992
1984
|
Positional args, unknown short flags (e.g. -c, -r), and claude's own flags
|
|
1993
1985
|
(--agent, --resume, --model, --print, --output-format ...) are all forwarded.
|
|
1994
|
-
On launch the FUNNEL_CHANNEL_ID env var is set and MCP connects to the gateway
|
|
1986
|
+
On launch the FUNNEL_CHANNEL_ID env var is set and MCP connects to the gateway.
|
|
1987
|
+
|
|
1988
|
+
see also:
|
|
1989
|
+
fnl docs claude full resolution order, side effects, double-launch guard
|
|
1990
|
+
fnl docs mcp what the MCP server exposes once Claude is up
|
|
1991
|
+
fnl docs debugging the diagnose → recover → verify loop
|
|
1992
|
+
|
|
1993
|
+
programmable: funnel.claude.launch({ profileId | channelId, options, env, resume })`;
|
|
1995
1994
|
const RESERVED_KEYS$1 = ["profile", "channel"];
|
|
1996
1995
|
const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
1997
1996
|
profile: z.string().optional(),
|
|
@@ -2051,768 +2050,310 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
|
2051
2050
|
process.exit(exitCode);
|
|
2052
2051
|
});
|
|
2053
2052
|
//#endregion
|
|
2054
|
-
//#region lib/cli/routes/debug-row.ts
|
|
2055
|
-
const stringOrNull = (value) => typeof value === "string" && value.length > 0 ? value : null;
|
|
2056
|
-
const numberOrNull = (value) => typeof value === "number" ? value : null;
|
|
2057
|
-
const stringOr = (value, fallback) => typeof value === "string" ? value : fallback;
|
|
2058
|
-
/**
|
|
2059
|
-
* Parse a payload string as a JSON object. Returns null for non-strings,
|
|
2060
|
-
* malformed JSON, or any non-object JSON (arrays, primitives) — the callers
|
|
2061
|
-
* only ever want the object form.
|
|
2062
|
-
*/
|
|
2063
|
-
const parsePayloadObject = (payload) => {
|
|
2064
|
-
if (payload === null) return null;
|
|
2065
|
-
try {
|
|
2066
|
-
const parsed = JSON.parse(payload);
|
|
2067
|
-
if (isStringKeyedObject(parsed)) return parsed;
|
|
2068
|
-
} catch {
|
|
2069
|
-
return null;
|
|
2070
|
-
}
|
|
2071
|
-
return null;
|
|
2072
|
-
};
|
|
2073
|
-
const isStringKeyedObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2074
|
-
const truncate = (text, max) => text.length <= max ? text : `${text.slice(0, max)}…`;
|
|
2075
|
-
/**
|
|
2076
|
-
* A short human preview of a payload: the `text` field when the payload is a
|
|
2077
|
-
* JSON object that has one, otherwise the raw payload, both truncated.
|
|
2078
|
-
*/
|
|
2079
|
-
const previewOf = (payload) => {
|
|
2080
|
-
if (typeof payload !== "string" || payload.length === 0) return null;
|
|
2081
|
-
const parsed = parsePayloadObject(payload);
|
|
2082
|
-
if (parsed !== null && "text" in parsed) return truncate(String(parsed.text), 60);
|
|
2083
|
-
return truncate(payload, 60);
|
|
2084
|
-
};
|
|
2085
|
-
/** Narrow one processed-table row into a `DebugEvent`. */
|
|
2086
|
-
const toDebugEvent = (row) => {
|
|
2087
|
-
const payload = stringOrNull(row.payload);
|
|
2088
|
-
return {
|
|
2089
|
-
seq: numberOrNull(row.seq),
|
|
2090
|
-
ts: numberOrNull(row.ts),
|
|
2091
|
-
type: stringOr(row.type, "?"),
|
|
2092
|
-
outcome: stringOr(row.outcome, "?"),
|
|
2093
|
-
eventId: stringOrNull(row.event_id),
|
|
2094
|
-
payload,
|
|
2095
|
-
payloadParsed: parsePayloadObject(payload),
|
|
2096
|
-
preview: previewOf(row.payload)
|
|
2097
|
-
};
|
|
2098
|
-
};
|
|
2099
|
-
/** Narrow one connection-table row into a `DebugConnectionError`. */
|
|
2100
|
-
const toDebugConnectionError = (row) => ({
|
|
2101
|
-
seq: numberOrNull(row.seq),
|
|
2102
|
-
ts: numberOrNull(row.ts),
|
|
2103
|
-
type: stringOr(row.type, "?"),
|
|
2104
|
-
status: stringOr(row.status, "?"),
|
|
2105
|
-
detail: stringOrNull(row.detail)
|
|
2106
|
-
});
|
|
2107
|
-
/**
|
|
2108
|
-
* Open a reader, run one query, and always close — the shared shape behind
|
|
2109
|
-
* every diagnostic lookup. Returns the rows or the reader's `Error`.
|
|
2110
|
-
*/
|
|
2111
|
-
const queryRows = (reader, sql, params) => {
|
|
2112
|
-
try {
|
|
2113
|
-
return reader.query(sql, params);
|
|
2114
|
-
} finally {
|
|
2115
|
-
reader.close();
|
|
2116
|
-
}
|
|
2117
|
-
};
|
|
2118
|
-
//#endregion
|
|
2119
2053
|
//#region lib/cli/routes/debug.ts
|
|
2120
|
-
const debugHelp = `funnel debug
|
|
2054
|
+
const debugHelp = `funnel debug / per-channel inspection (events, drops, connection errors, replay)
|
|
2121
2055
|
|
|
2122
|
-
usage
|
|
2056
|
+
usage / funnel debug [subcommand] [--channel <name>] [--all] [--limit <N>]
|
|
2123
2057
|
|
|
2124
2058
|
subcommands:
|
|
2125
|
-
(none)
|
|
2126
|
-
events
|
|
2127
|
-
dropped
|
|
2128
|
-
errors
|
|
2059
|
+
(none) / full diagnosis for one channel (or --all for every channel)
|
|
2060
|
+
events / last N processed events with outcome
|
|
2061
|
+
dropped / events filtered out (skip:*) with payload
|
|
2062
|
+
errors / listener auth-failed and error events
|
|
2063
|
+
replay / re-send a past event into a channel
|
|
2129
2064
|
|
|
2130
2065
|
options:
|
|
2131
|
-
--channel <name>
|
|
2132
|
-
--all
|
|
2133
|
-
--limit <N>
|
|
2134
|
-
--json output as JSON (machine-readable, useful for Claude)
|
|
2066
|
+
--channel <name> / channel to inspect (auto-selected when only one exists)
|
|
2067
|
+
--all / diagnose every channel
|
|
2068
|
+
--limit <N> / number of rows (default 5 for diagnosis, 20 for subcommands)
|
|
2135
2069
|
|
|
2136
|
-
|
|
2137
|
-
|
|
2070
|
+
For the common case, prefer fnl doctor — it runs a full diagnosis and can apply
|
|
2071
|
+
safe fixes in one shot. fnl debug is the lower-level view.
|
|
2138
2072
|
|
|
2139
|
-
|
|
2140
|
-
|
|
2073
|
+
output / valid YAML
|
|
2074
|
+
|
|
2075
|
+
programmable / funnel.diagnostics.diagnose() / .diagnoseAll() / .recentEvents() / .droppedEvents() / .connectionErrors() / .replay()
|
|
2141
2076
|
|
|
2142
2077
|
examples:
|
|
2143
2078
|
funnel debug
|
|
2144
|
-
funnel debug --all
|
|
2145
|
-
funnel debug --channel
|
|
2146
|
-
funnel debug --channel
|
|
2147
|
-
funnel debug
|
|
2148
|
-
|
|
2149
|
-
funnel debug errors`;
|
|
2150
|
-
const debugEventsHelp = `funnel debug events — last N processed events with outcome and preview
|
|
2079
|
+
funnel debug --all
|
|
2080
|
+
funnel debug --channel ops
|
|
2081
|
+
funnel debug events --channel ops --limit 50
|
|
2082
|
+
funnel debug dropped --channel ops`;
|
|
2083
|
+
const debugEventsHelp = `funnel debug events / last N processed events
|
|
2151
2084
|
|
|
2152
|
-
usage
|
|
2085
|
+
usage / funnel debug events [--channel <name>] [--limit <N>]
|
|
2153
2086
|
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
--limit <N> number of rows (default: 20)
|
|
2157
|
-
--json output as JSON
|
|
2087
|
+
programmable / funnel.diagnostics.recentEvents(channel, limit)`;
|
|
2088
|
+
const debugDroppedHelp = `funnel debug dropped / events filtered out (skip:*)
|
|
2158
2089
|
|
|
2159
|
-
|
|
2160
|
-
funnel debug events
|
|
2161
|
-
funnel debug events --channel open-karte --limit 50
|
|
2162
|
-
funnel debug events --json`;
|
|
2163
|
-
const debugDroppedHelp = `funnel debug dropped — events filtered out (skip:*)
|
|
2164
|
-
|
|
2165
|
-
usage: funnel debug dropped [--channel <name>] [--limit <N>] [--json]
|
|
2090
|
+
usage / funnel debug dropped [--channel <name>] [--limit <N>]
|
|
2166
2091
|
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
--limit <N> number of rows (default: 20)
|
|
2170
|
-
--json output as JSON
|
|
2092
|
+
shows skip reasons: skip:type / skip:subtype / skip:dedup / skip:self-user /
|
|
2093
|
+
skip:self-bot / skip:preprocess
|
|
2171
2094
|
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
examples:
|
|
2176
|
-
funnel debug dropped
|
|
2177
|
-
funnel debug dropped --channel open-karte --json`;
|
|
2178
|
-
const debugErrorsHelp = `funnel debug errors — listener connection errors
|
|
2095
|
+
programmable / funnel.diagnostics.droppedEvents(channel, limit)`;
|
|
2096
|
+
const debugErrorsHelp = `funnel debug errors / listener auth-failed and error events
|
|
2179
2097
|
|
|
2180
|
-
usage
|
|
2098
|
+
usage / funnel debug errors [--channel <name>] [--limit <N>]
|
|
2181
2099
|
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
--limit <N> number of rows (default: 20)
|
|
2185
|
-
--json output as JSON
|
|
2100
|
+
programmable / funnel.diagnostics.connectionErrors(channel, limit)`;
|
|
2101
|
+
const debugReplayHelp = `funnel debug replay / re-publish a past event into a channel
|
|
2186
2102
|
|
|
2187
|
-
|
|
2188
|
-
use this when a listener never connects or keeps disconnecting.
|
|
2103
|
+
usage / funnel debug replay --channel <name> [--seq <N>]
|
|
2189
2104
|
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
};
|
|
2206
|
-
const formatTs = (epochMs) => {
|
|
2207
|
-
if (typeof epochMs !== "number") return "?";
|
|
2208
|
-
return new Date(epochMs).toISOString().slice(11, 19);
|
|
2209
|
-
};
|
|
2210
|
-
const buildDiagnosis = (report) => {
|
|
2211
|
-
const rootCause = (report.connectionErrors[report.connectionErrors.length - 1] ?? null)?.detail ?? null;
|
|
2212
|
-
if (!report.gateway.running) return {
|
|
2213
|
-
status: "error",
|
|
2214
|
-
message: "gateway is not running",
|
|
2215
|
-
nextActions: ["fnl gateway start"],
|
|
2216
|
-
rootCause: null
|
|
2217
|
-
};
|
|
2218
|
-
const channel = report.channel;
|
|
2219
|
-
if (!(report.listeners.length > 0)) return {
|
|
2220
|
-
status: "warn",
|
|
2221
|
-
message: "no connectors configured on this channel",
|
|
2222
|
-
nextActions: [`fnl channels ${channel} connectors add <name> --type=slack ...`],
|
|
2223
|
-
rootCause: null
|
|
2224
|
-
};
|
|
2225
|
-
const allDead = report.listeners.every((l) => !l.alive);
|
|
2226
|
-
const someDead = report.listeners.some((l) => !l.alive);
|
|
2227
|
-
if (allDead) return {
|
|
2228
|
-
status: "error",
|
|
2229
|
-
message: "all listeners are dead",
|
|
2230
|
-
nextActions: ["fnl gateway logs", "fnl gateway restart"],
|
|
2231
|
-
rootCause
|
|
2232
|
-
};
|
|
2233
|
-
if (someDead) return {
|
|
2234
|
-
status: "warn",
|
|
2235
|
-
message: "some listeners are dead",
|
|
2236
|
-
nextActions: ["fnl gateway logs"],
|
|
2237
|
-
rootCause
|
|
2238
|
-
};
|
|
2239
|
-
const hasErrors = report.listeners.some((l) => l.errors > 0);
|
|
2240
|
-
if (report.claudeClients === 0) return {
|
|
2241
|
-
status: "warn",
|
|
2242
|
-
message: "no Claude connected to this channel",
|
|
2243
|
-
nextActions: [`fnl claude --channel ${channel}`],
|
|
2244
|
-
rootCause: null
|
|
2245
|
-
};
|
|
2246
|
-
if (hasErrors) return {
|
|
2247
|
-
status: "warn",
|
|
2248
|
-
message: "listeners have errors",
|
|
2249
|
-
nextActions: ["fnl gateway logs"],
|
|
2250
|
-
rootCause
|
|
2251
|
-
};
|
|
2252
|
-
return {
|
|
2253
|
-
status: "ok",
|
|
2254
|
-
message: "everything looks healthy",
|
|
2255
|
-
nextActions: [],
|
|
2256
|
-
rootCause: null
|
|
2257
|
-
};
|
|
2258
|
-
};
|
|
2259
|
-
const renderText = (report) => {
|
|
2260
|
-
const lines = [];
|
|
2261
|
-
lines.push(`= funnel debug: ${report.channel} =`);
|
|
2262
|
-
lines.push("");
|
|
2263
|
-
const gw = report.gateway;
|
|
2264
|
-
if (!gw.running) lines.push("[gateway] ○ not running");
|
|
2265
|
-
else {
|
|
2266
|
-
const uptime = gw.uptimeMs !== null ? ` · up ${formatUptime(gw.uptimeMs)}` : "";
|
|
2267
|
-
lines.push(`[gateway] ● running pid ${gw.pid} · port ${gw.port}${uptime}`);
|
|
2268
|
-
}
|
|
2269
|
-
if (report.listeners.length === 0) lines.push("[listener] - no listener");
|
|
2270
|
-
else for (const listener of report.listeners) {
|
|
2271
|
-
const indicator = listener.alive ? "●" : "○";
|
|
2272
|
-
const state = listener.alive ? "alive " : "dead ";
|
|
2273
|
-
const eventsStr = `${listener.events} events`;
|
|
2274
|
-
const lastStr = listener.lastEventAt ? ` · last ${listener.lastEventAt.slice(11, 19)}` : "";
|
|
2275
|
-
const errStr = listener.errors > 0 ? ` · ⚠ ${listener.errors} errors` : "";
|
|
2276
|
-
lines.push(`[listener] ${indicator} ${state} ${eventsStr}${lastStr}${errStr}`);
|
|
2277
|
-
}
|
|
2278
|
-
const claudeCount = report.claudeClients;
|
|
2279
|
-
if (claudeCount === 0) lines.push("[claude] ○ not connected");
|
|
2280
|
-
else lines.push(`[claude] ● connected (${claudeCount} WS client${claudeCount > 1 ? "s" : ""})`);
|
|
2281
|
-
if (report.recentEvents.length === 0) lines.push("[events] no events recorded");
|
|
2282
|
-
else {
|
|
2283
|
-
lines.push(`[events] last ${report.recentEvents.length} event${report.recentEvents.length > 1 ? "s" : ""}:`);
|
|
2284
|
-
for (const event of report.recentEvents) {
|
|
2285
|
-
const time = formatTs(event.ts);
|
|
2286
|
-
const type = event.type.padEnd(8);
|
|
2287
|
-
const outcome = event.outcome.padEnd(20);
|
|
2288
|
-
const preview = event.preview ? ` "${event.preview}"` : "";
|
|
2289
|
-
const seq = event.seq !== null ? ` (seq=${event.seq})` : "";
|
|
2290
|
-
lines.push(` ${time} ${type} ${outcome}${preview}${seq}`);
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
if (report.connectionErrors.length > 0) {
|
|
2294
|
-
lines.push("[conn errs] recent connection errors:");
|
|
2295
|
-
for (const err of report.connectionErrors) {
|
|
2296
|
-
const time = formatTs(err.ts);
|
|
2297
|
-
const status = err.status.padEnd(14);
|
|
2298
|
-
const detail = err.detail ? ` "${err.detail}"` : "";
|
|
2299
|
-
lines.push(` ${time} ${err.type.padEnd(8)} ${status}${detail}`);
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
lines.push("");
|
|
2303
|
-
const diag = report.diagnosis;
|
|
2304
|
-
const icon = diag.status === "ok" ? "✓" : diag.status === "warn" ? "⚠" : "✗";
|
|
2305
|
-
lines.push(`diagnosis: ${icon} ${diag.message}`);
|
|
2306
|
-
if (diag.rootCause) lines.push(` root cause: ${diag.rootCause}`);
|
|
2307
|
-
for (const action of diag.nextActions) lines.push(` → ${action}`);
|
|
2308
|
-
return lines.join("\n");
|
|
2309
|
-
};
|
|
2310
|
-
const resolveStoreOrNull = () => {
|
|
2311
|
-
const tmpDir = funnelTmpDir();
|
|
2312
|
-
const rawPath = join(tmpDir, "connector-raw.db");
|
|
2313
|
-
const processedPath = join(tmpDir, "connector-processed.db");
|
|
2314
|
-
const connectionPath = join(tmpDir, "connector-connection.db");
|
|
2315
|
-
if (!existsSync(rawPath) || !existsSync(processedPath) || !existsSync(connectionPath)) return null;
|
|
2316
|
-
return {
|
|
2317
|
-
rawPath,
|
|
2318
|
-
processedPath,
|
|
2319
|
-
connectionPath
|
|
2320
|
-
};
|
|
2321
|
-
};
|
|
2322
|
-
/**
|
|
2323
|
-
* Resolve the connector name for a connector id on a channel, used to attribute
|
|
2324
|
-
* a replayed event back to its source connector. Returns undefined when the id
|
|
2325
|
-
* is null or no longer present (connectors can be removed after an event was
|
|
2326
|
-
* logged).
|
|
2327
|
-
*/
|
|
2328
|
-
const connectorOf = (channel, connectorId) => {
|
|
2329
|
-
if (connectorId === null) return void 0;
|
|
2330
|
-
return channel.connectors?.find((connector) => connector.id === connectorId)?.name;
|
|
2331
|
-
};
|
|
2332
|
-
const resolveChannelId = (channels, channelName) => {
|
|
2333
|
-
if (channelName) {
|
|
2334
|
-
const match = channels.find((ch) => ch.name === channelName);
|
|
2335
|
-
if (match) return {
|
|
2336
|
-
found: true,
|
|
2337
|
-
channel: match
|
|
2105
|
+
programmable / funnel.diagnostics.replay(channel, seq?)`;
|
|
2106
|
+
const channelLimitQuery = z.object({
|
|
2107
|
+
channel: z.string().optional(),
|
|
2108
|
+
limit: z.string().optional()
|
|
2109
|
+
});
|
|
2110
|
+
const resolveTargetChannel = (c, channelArg) => {
|
|
2111
|
+
const channels = c.env.funnel.channels.list();
|
|
2112
|
+
if (channelArg) {
|
|
2113
|
+
const match = channels.find((ch) => ch.name === channelArg);
|
|
2114
|
+
if (!match) return {
|
|
2115
|
+
kind: "error",
|
|
2116
|
+
payload: {
|
|
2117
|
+
error: `channel not found: ${channelArg}`,
|
|
2118
|
+
availableChannels: channels.map((ch) => ch.name)
|
|
2119
|
+
}
|
|
2338
2120
|
};
|
|
2339
2121
|
return {
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
name: channelName
|
|
2122
|
+
kind: "ok",
|
|
2123
|
+
name: match.name
|
|
2343
2124
|
};
|
|
2344
2125
|
}
|
|
2345
|
-
if (channels.length === 1 && channels[0]) return {
|
|
2346
|
-
found: true,
|
|
2347
|
-
channel: channels[0]
|
|
2348
|
-
};
|
|
2349
2126
|
if (channels.length === 0) return {
|
|
2350
|
-
|
|
2351
|
-
|
|
2127
|
+
kind: "ok",
|
|
2128
|
+
name: null
|
|
2129
|
+
};
|
|
2130
|
+
if (channels.length === 1) return {
|
|
2131
|
+
kind: "ok",
|
|
2132
|
+
name: channels[0]?.name ?? null
|
|
2352
2133
|
};
|
|
2353
2134
|
return {
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2135
|
+
kind: "error",
|
|
2136
|
+
payload: {
|
|
2137
|
+
error: "multiple channels — specify one with --channel",
|
|
2138
|
+
channels: channels.map((ch) => ch.name)
|
|
2139
|
+
}
|
|
2357
2140
|
};
|
|
2358
2141
|
};
|
|
2359
|
-
const
|
|
2142
|
+
const debugHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
2360
2143
|
channel: z.string().optional(),
|
|
2361
|
-
|
|
2362
|
-
json: z.enum([
|
|
2144
|
+
all: z.enum([
|
|
2363
2145
|
"true",
|
|
2364
2146
|
"false",
|
|
2365
2147
|
""
|
|
2366
|
-
]).optional()
|
|
2367
|
-
|
|
2148
|
+
]).optional(),
|
|
2149
|
+
limit: z.string().optional()
|
|
2150
|
+
}), debugHelp), async (c) => {
|
|
2368
2151
|
const query = c.req.valid("query");
|
|
2369
|
-
const
|
|
2370
|
-
|
|
2152
|
+
const funnel = c.env.funnel;
|
|
2153
|
+
if (query.all === "true" || query.all === "") {
|
|
2154
|
+
const report = await funnel.diagnostics.diagnoseAll();
|
|
2155
|
+
return c.text(renderYaml(report));
|
|
2156
|
+
}
|
|
2157
|
+
const allChannels = funnel.channels.list();
|
|
2158
|
+
if (allChannels.length === 0) return c.text(renderYaml({
|
|
2159
|
+
error: "no channels configured",
|
|
2160
|
+
nextAction: "fnl channels add <name>"
|
|
2161
|
+
}));
|
|
2162
|
+
let targetName = null;
|
|
2163
|
+
if (query.channel) {
|
|
2164
|
+
const match = allChannels.find((ch) => ch.name === query.channel);
|
|
2165
|
+
if (!match) return c.text(renderYaml({
|
|
2166
|
+
error: `channel not found: ${query.channel}`,
|
|
2167
|
+
availableChannels: allChannels.map((ch) => ch.name)
|
|
2168
|
+
}));
|
|
2169
|
+
targetName = match.name;
|
|
2170
|
+
} else if (allChannels.length === 1) targetName = allChannels[0]?.name ?? null;
|
|
2171
|
+
else return c.text(renderYaml({
|
|
2172
|
+
error: "multiple channels — specify one with --channel or use --all",
|
|
2173
|
+
channels: allChannels.map((ch) => ch.name),
|
|
2174
|
+
hint: "use --all for all channels at once"
|
|
2175
|
+
}));
|
|
2176
|
+
const report = await funnel.diagnostics.diagnose(targetName ?? void 0);
|
|
2177
|
+
if (!report) return c.text(renderYaml({ error: "channel not resolvable" }));
|
|
2178
|
+
return c.text(renderYaml(report));
|
|
2179
|
+
});
|
|
2180
|
+
const debugEventsHandler = factory.createHandlers(zValidator$1("query", channelLimitQuery, debugEventsHelp), async (c) => {
|
|
2181
|
+
const query = c.req.valid("query");
|
|
2182
|
+
const funnel = c.env.funnel;
|
|
2371
2183
|
const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
|
|
2372
|
-
const
|
|
2373
|
-
if (
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
}
|
|
2377
|
-
const resolved = resolveChannelId(channels, query.channel);
|
|
2378
|
-
if (!resolved.found) {
|
|
2379
|
-
if (resolved.reason === "not-found") {
|
|
2380
|
-
if (isJson) return c.json({
|
|
2381
|
-
error: `channel not found: ${resolved.name}`,
|
|
2382
|
-
availableChannels: channels.map((ch) => ch.name)
|
|
2383
|
-
});
|
|
2384
|
-
return c.text(`channel not found: ${resolved.name}`);
|
|
2385
|
-
}
|
|
2386
|
-
if (resolved.reason === "ambiguous") {
|
|
2387
|
-
if (isJson) return c.json({
|
|
2388
|
-
error: "multiple channels — specify one with --channel",
|
|
2389
|
-
channels: resolved.names
|
|
2390
|
-
});
|
|
2391
|
-
return c.text(`multiple channels — specify one with --channel:\n${resolved.names.map((n) => ` - ${n}`).join("\n")}`);
|
|
2392
|
-
}
|
|
2393
|
-
if (isJson) return c.json([]);
|
|
2394
|
-
return c.text("no channels configured");
|
|
2395
|
-
}
|
|
2396
|
-
const channel = resolved.channel;
|
|
2397
|
-
const reader = new ConnectorDiagnosticSqlReader(store);
|
|
2398
|
-
const rows = channel ? queryRows(reader, "SELECT seq, ts, type, outcome, payload FROM processed WHERE channel_id = ? ORDER BY seq DESC LIMIT ?", [channel.id, limit]) : queryRows(reader, "SELECT seq, ts, type, outcome, payload FROM processed ORDER BY seq DESC LIMIT ?", [limit]);
|
|
2399
|
-
if (rows instanceof Error) return c.text(`error: ${rows.message}`);
|
|
2400
|
-
const events = rows.reverse().map(toDebugEvent);
|
|
2401
|
-
if (isJson) return c.json(events);
|
|
2402
|
-
if (events.length === 0) return c.text("no events recorded");
|
|
2403
|
-
const lines = events.map((ev) => {
|
|
2404
|
-
const time = formatTs(ev.ts);
|
|
2405
|
-
const type = ev.type.padEnd(8);
|
|
2406
|
-
const outcome = ev.outcome.padEnd(20);
|
|
2407
|
-
const preview = ev.preview ? ` "${ev.preview}"` : "";
|
|
2408
|
-
return `${time} ${type} ${outcome}${ev.seq !== null ? ` seq=${ev.seq}` : ""}${preview}`;
|
|
2409
|
-
});
|
|
2410
|
-
return c.text(lines.join("\n"));
|
|
2184
|
+
const resolved = resolveTargetChannel(c, query.channel);
|
|
2185
|
+
if (resolved.kind === "error") return c.text(renderYaml(resolved.payload));
|
|
2186
|
+
const events = await funnel.diagnostics.recentEvents(resolved.name, limit);
|
|
2187
|
+
return c.text(renderYaml({ events }));
|
|
2411
2188
|
});
|
|
2412
|
-
const debugDroppedHandler = factory.createHandlers(zValidator$1("query",
|
|
2413
|
-
channel: z.string().optional(),
|
|
2414
|
-
limit: z.string().optional(),
|
|
2415
|
-
json: z.enum([
|
|
2416
|
-
"true",
|
|
2417
|
-
"false",
|
|
2418
|
-
""
|
|
2419
|
-
]).optional()
|
|
2420
|
-
}), debugDroppedHelp), async (c) => {
|
|
2189
|
+
const debugDroppedHandler = factory.createHandlers(zValidator$1("query", channelLimitQuery, debugDroppedHelp), async (c) => {
|
|
2421
2190
|
const query = c.req.valid("query");
|
|
2422
|
-
const
|
|
2423
|
-
const isJson = query.json === "true" || query.json === "";
|
|
2191
|
+
const funnel = c.env.funnel;
|
|
2424
2192
|
const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
|
|
2425
|
-
const
|
|
2426
|
-
if (
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
}
|
|
2430
|
-
const resolvedDropped = resolveChannelId(channels, query.channel);
|
|
2431
|
-
if (!resolvedDropped.found) {
|
|
2432
|
-
if (resolvedDropped.reason === "not-found") {
|
|
2433
|
-
if (isJson) return c.json({
|
|
2434
|
-
error: `channel not found: ${resolvedDropped.name}`,
|
|
2435
|
-
availableChannels: channels.map((ch) => ch.name)
|
|
2436
|
-
});
|
|
2437
|
-
return c.text(`channel not found: ${resolvedDropped.name}`);
|
|
2438
|
-
}
|
|
2439
|
-
if (resolvedDropped.reason === "ambiguous") {
|
|
2440
|
-
if (isJson) return c.json({
|
|
2441
|
-
error: "multiple channels — specify one with --channel",
|
|
2442
|
-
channels: resolvedDropped.names
|
|
2443
|
-
});
|
|
2444
|
-
return c.text(`multiple channels — specify one with --channel:\n${resolvedDropped.names.map((n) => ` - ${n}`).join("\n")}`);
|
|
2445
|
-
}
|
|
2446
|
-
if (isJson) return c.json([]);
|
|
2447
|
-
return c.text("no channels configured");
|
|
2448
|
-
}
|
|
2449
|
-
const channel = resolvedDropped.channel;
|
|
2450
|
-
const reader = new ConnectorDiagnosticSqlReader(store);
|
|
2451
|
-
const rows = channel ? queryRows(reader, "SELECT p.seq, p.ts, p.type, p.outcome, p.payload, p.event_id FROM processed p WHERE p.channel_id = ? AND p.outcome LIKE 'skip:%' ORDER BY p.seq DESC LIMIT ?", [channel.id, limit]) : queryRows(reader, "SELECT seq, ts, type, outcome, payload, event_id FROM processed WHERE outcome LIKE 'skip:%' ORDER BY seq DESC LIMIT ?", [limit]);
|
|
2452
|
-
if (rows instanceof Error) return c.text(`error: ${rows.message}`);
|
|
2453
|
-
const events = rows.reverse().map(toDebugEvent);
|
|
2454
|
-
if (isJson) return c.json(events);
|
|
2455
|
-
if (events.length === 0) return c.text("no dropped events recorded");
|
|
2456
|
-
const lines = events.map((ev) => {
|
|
2457
|
-
return `${formatTs(ev.ts)} ${ev.type.padEnd(8)} ${ev.outcome.padEnd(20)}${ev.seq !== null ? ` seq=${ev.seq}` : ""}${ev.eventId ? ` event_id=${ev.eventId.slice(0, 8)}` : ""}${ev.preview ? ` "${ev.preview}"` : ""}`;
|
|
2458
|
-
});
|
|
2459
|
-
return c.text(lines.join("\n"));
|
|
2193
|
+
const resolved = resolveTargetChannel(c, query.channel);
|
|
2194
|
+
if (resolved.kind === "error") return c.text(renderYaml(resolved.payload));
|
|
2195
|
+
const events = await funnel.diagnostics.droppedEvents(resolved.name, limit);
|
|
2196
|
+
return c.text(renderYaml({ dropped: events }));
|
|
2460
2197
|
});
|
|
2461
|
-
const debugErrorsHandler = factory.createHandlers(zValidator$1("query",
|
|
2462
|
-
channel: z.string().optional(),
|
|
2463
|
-
limit: z.string().optional(),
|
|
2464
|
-
json: z.enum([
|
|
2465
|
-
"true",
|
|
2466
|
-
"false",
|
|
2467
|
-
""
|
|
2468
|
-
]).optional()
|
|
2469
|
-
}), debugErrorsHelp), async (c) => {
|
|
2198
|
+
const debugErrorsHandler = factory.createHandlers(zValidator$1("query", channelLimitQuery, debugErrorsHelp), async (c) => {
|
|
2470
2199
|
const query = c.req.valid("query");
|
|
2471
|
-
const
|
|
2472
|
-
const isJson = query.json === "true" || query.json === "";
|
|
2200
|
+
const funnel = c.env.funnel;
|
|
2473
2201
|
const limit = query.limit ? Math.max(1, Number(query.limit)) : 20;
|
|
2474
|
-
const
|
|
2475
|
-
if (
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
}
|
|
2479
|
-
const resolvedErrors = resolveChannelId(channels, query.channel);
|
|
2480
|
-
if (!resolvedErrors.found) {
|
|
2481
|
-
if (resolvedErrors.reason === "not-found") {
|
|
2482
|
-
if (isJson) return c.json({
|
|
2483
|
-
error: `channel not found: ${resolvedErrors.name}`,
|
|
2484
|
-
availableChannels: channels.map((ch) => ch.name)
|
|
2485
|
-
});
|
|
2486
|
-
return c.text(`channel not found: ${resolvedErrors.name}`);
|
|
2487
|
-
}
|
|
2488
|
-
if (resolvedErrors.reason === "ambiguous") {
|
|
2489
|
-
if (isJson) return c.json({
|
|
2490
|
-
error: "multiple channels — specify one with --channel",
|
|
2491
|
-
channels: resolvedErrors.names
|
|
2492
|
-
});
|
|
2493
|
-
return c.text(`multiple channels — specify one with --channel:\n${resolvedErrors.names.map((n) => ` - ${n}`).join("\n")}`);
|
|
2494
|
-
}
|
|
2495
|
-
if (isJson) return c.json([]);
|
|
2496
|
-
return c.text("no channels configured");
|
|
2497
|
-
}
|
|
2498
|
-
const channel = resolvedErrors.channel;
|
|
2499
|
-
const reader = new ConnectorDiagnosticSqlReader(store);
|
|
2500
|
-
const rows = channel ? queryRows(reader, "SELECT seq, ts, type, status, detail FROM connection WHERE channel_id = ? AND status IN ('auth-failed','error') ORDER BY seq DESC LIMIT ?", [channel.id, limit]) : queryRows(reader, "SELECT seq, ts, type, status, detail FROM connection WHERE status IN ('auth-failed','error') ORDER BY seq DESC LIMIT ?", [limit]);
|
|
2501
|
-
if (rows instanceof Error) return c.text(`error: ${rows.message}`);
|
|
2502
|
-
const errors = rows.reverse().map(toDebugConnectionError);
|
|
2503
|
-
if (isJson) return c.json(errors);
|
|
2504
|
-
if (errors.length === 0) return c.text("no connection errors recorded");
|
|
2505
|
-
const lines = errors.map((ev) => {
|
|
2506
|
-
return `${formatTs(ev.ts)} ${ev.type.padEnd(8)} ${ev.status.padEnd(16)}${ev.detail ? ` "${ev.detail}"` : ""}`;
|
|
2507
|
-
});
|
|
2508
|
-
return c.text(lines.join("\n"));
|
|
2202
|
+
const resolved = resolveTargetChannel(c, query.channel);
|
|
2203
|
+
if (resolved.kind === "error") return c.text(renderYaml(resolved.payload));
|
|
2204
|
+
const errors = await funnel.diagnostics.connectionErrors(resolved.name, limit);
|
|
2205
|
+
return c.text(renderYaml({ errors }));
|
|
2509
2206
|
});
|
|
2510
|
-
const
|
|
2511
|
-
const targetChannelName = targetChannel.name;
|
|
2512
|
-
const baseReport = {
|
|
2513
|
-
channel: targetChannelName,
|
|
2514
|
-
channelId: targetChannel.id,
|
|
2515
|
-
gateway: {
|
|
2516
|
-
running: gatewayStatus.running,
|
|
2517
|
-
pid: gatewayStatus.pid,
|
|
2518
|
-
port: gatewayStatus.running ? gatewayStatus.port : null,
|
|
2519
|
-
uptimeMs: gatewayBodyOrNull?.uptimeMs ?? null
|
|
2520
|
-
},
|
|
2521
|
-
listeners: [],
|
|
2522
|
-
claudeClients: 0,
|
|
2523
|
-
recentEvents: [],
|
|
2524
|
-
connectionErrors: []
|
|
2525
|
-
};
|
|
2526
|
-
if (gatewayBodyOrNull) {
|
|
2527
|
-
baseReport.listeners = gatewayBodyOrNull.listeners.filter((l) => l.channelName === targetChannelName).map((l) => ({
|
|
2528
|
-
name: l.name,
|
|
2529
|
-
type: l.type,
|
|
2530
|
-
alive: l.alive,
|
|
2531
|
-
events: l.events,
|
|
2532
|
-
errors: l.errors,
|
|
2533
|
-
lastEventAt: l.lastEventAt
|
|
2534
|
-
}));
|
|
2535
|
-
baseReport.claudeClients = gatewayBodyOrNull.clients.filter((cl) => cl.channelName === targetChannelName).length;
|
|
2536
|
-
}
|
|
2537
|
-
if (store) {
|
|
2538
|
-
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]);
|
|
2539
|
-
if (!(evRows instanceof Error)) baseReport.recentEvents = evRows.reverse().map(toDebugEvent);
|
|
2540
|
-
const hasDeadListeners = baseReport.listeners.some((l) => !l.alive);
|
|
2541
|
-
const hasListenerErrors = baseReport.listeners.some((l) => l.errors > 0);
|
|
2542
|
-
if (hasDeadListeners || hasListenerErrors) {
|
|
2543
|
-
const errRows = queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT ts, type, status, detail FROM connection WHERE channel_id = ? AND status IN ('auth-failed','error') ORDER BY seq DESC LIMIT 3", [targetChannel.id]);
|
|
2544
|
-
if (!(errRows instanceof Error)) baseReport.connectionErrors = errRows.reverse().map(toDebugConnectionError);
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
return {
|
|
2548
|
-
...baseReport,
|
|
2549
|
-
diagnosis: buildDiagnosis(baseReport)
|
|
2550
|
-
};
|
|
2551
|
-
};
|
|
2552
|
-
const debugHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
2207
|
+
const debugReplayHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
2553
2208
|
channel: z.string().optional(),
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2209
|
+
seq: z.string().optional()
|
|
2210
|
+
}), debugReplayHelp), async (c) => {
|
|
2211
|
+
const query = c.req.valid("query");
|
|
2212
|
+
const funnel = c.env.funnel;
|
|
2213
|
+
const resolved = resolveTargetChannel(c, query.channel);
|
|
2214
|
+
if (resolved.kind === "error") return c.text(renderYaml(resolved.payload));
|
|
2215
|
+
if (!resolved.name) return c.text(renderYaml({ error: "no channels configured" }));
|
|
2216
|
+
const seq = query.seq ? Number(query.seq) : void 0;
|
|
2217
|
+
const result = await funnel.diagnostics.replay(resolved.name, seq);
|
|
2218
|
+
return c.text(renderYaml(result));
|
|
2219
|
+
});
|
|
2220
|
+
const docsIndexHandler = factory.createHandlers(zValidator$1("query", z.object({}), `funnel docs / embedded documentation
|
|
2221
|
+
|
|
2222
|
+
usage / funnel docs [topic]
|
|
2223
|
+
|
|
2224
|
+
with no topic, lists topics as YAML. with a topic, prints the doc text.
|
|
2225
|
+
|
|
2226
|
+
output / valid YAML (topic listing) or plain text (topic body)
|
|
2227
|
+
|
|
2228
|
+
programmable / funnel.docs.list() / funnel.docs.get(topic)
|
|
2229
|
+
|
|
2230
|
+
examples:
|
|
2231
|
+
funnel docs
|
|
2232
|
+
funnel docs architecture
|
|
2233
|
+
funnel docs debugging`), async (c) => {
|
|
2234
|
+
const docs = c.env.funnel.docs;
|
|
2235
|
+
return c.text(renderYaml({ topics: docs.list() }));
|
|
2236
|
+
});
|
|
2237
|
+
const docsTopicHandler = factory.createHandlers(zValidator$1("param", z.object({ topic: z.string() })), async (c) => {
|
|
2238
|
+
const param = c.req.valid("param");
|
|
2239
|
+
const docs = c.env.funnel.docs;
|
|
2240
|
+
const text = docs.get(param.topic);
|
|
2241
|
+
if (text === null) return c.text(renderYaml({
|
|
2242
|
+
error: `unknown topic: ${param.topic}`,
|
|
2243
|
+
availableTopics: docs.topics()
|
|
2244
|
+
}));
|
|
2245
|
+
return c.text(text);
|
|
2246
|
+
});
|
|
2247
|
+
const doctorHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
2248
|
+
fix: z.enum([
|
|
2560
2249
|
"true",
|
|
2561
2250
|
"false",
|
|
2562
2251
|
""
|
|
2563
2252
|
]).optional(),
|
|
2564
|
-
|
|
2565
|
-
}), debugHelp), async (c) => {
|
|
2566
|
-
const query = c.req.valid("query");
|
|
2567
|
-
const funnel = c.env.funnel;
|
|
2568
|
-
const channels = funnel.channels.list();
|
|
2569
|
-
const gatewayStatus = funnel.gateway.getStatus();
|
|
2570
|
-
const isJson = query.json === "true" || query.json === "";
|
|
2571
|
-
const isAll = query.all === "true" || query.all === "";
|
|
2572
|
-
const eventLimit = query.limit ? Math.max(1, Number(query.limit)) : 5;
|
|
2573
|
-
if (channels.length === 0) {
|
|
2574
|
-
if (isJson) return c.json({
|
|
2575
|
-
error: "no channels configured",
|
|
2576
|
-
nextAction: "fnl channels add <name>"
|
|
2577
|
-
});
|
|
2578
|
-
return c.text("no channels configured — run: fnl channels add <name>");
|
|
2579
|
-
}
|
|
2580
|
-
const token = funnel.gatewayToken.read();
|
|
2581
|
-
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
2582
|
-
let gatewayBodyOrNull = null;
|
|
2583
|
-
if (gatewayStatus.running) {
|
|
2584
|
-
const res = await fetch(`http://127.0.0.1:${gatewayStatus.port}/status`, { headers }).catch(() => null);
|
|
2585
|
-
if (res && res.ok) {
|
|
2586
|
-
const body = await res.json();
|
|
2587
|
-
if (isGatewayStatusResponse(body)) gatewayBodyOrNull = body;
|
|
2588
|
-
}
|
|
2589
|
-
}
|
|
2590
|
-
const store = resolveStoreOrNull();
|
|
2591
|
-
if (isAll) {
|
|
2592
|
-
const reports = await Promise.all(channels.map((ch) => buildChannelReport(ch, gatewayStatus, gatewayBodyOrNull, store, eventLimit)));
|
|
2593
|
-
const errorChannels = reports.filter((r) => r.diagnosis.status === "error").map((r) => r.channel);
|
|
2594
|
-
const warnChannels = reports.filter((r) => r.diagnosis.status === "warn").map((r) => r.channel);
|
|
2595
|
-
const okChannels = reports.filter((r) => r.diagnosis.status === "ok").map((r) => r.channel);
|
|
2596
|
-
const uniqueActions = [...new Set(reports.flatMap((r) => r.diagnosis.nextActions))];
|
|
2597
|
-
return c.json({
|
|
2598
|
-
summary: {
|
|
2599
|
-
total: reports.length,
|
|
2600
|
-
ok: okChannels.length,
|
|
2601
|
-
warn: warnChannels.length,
|
|
2602
|
-
error: errorChannels.length,
|
|
2603
|
-
criticalChannels: errorChannels,
|
|
2604
|
-
warnChannels,
|
|
2605
|
-
suggestedActions: uniqueActions
|
|
2606
|
-
},
|
|
2607
|
-
channels: reports
|
|
2608
|
-
});
|
|
2609
|
-
}
|
|
2610
|
-
let targetChannel = null;
|
|
2611
|
-
if (query.channel) {
|
|
2612
|
-
targetChannel = channels.find((ch) => ch.name === query.channel) ?? null;
|
|
2613
|
-
if (!targetChannel) {
|
|
2614
|
-
if (isJson) return c.json({
|
|
2615
|
-
error: `channel not found: ${query.channel}`,
|
|
2616
|
-
availableChannels: channels.map((ch) => ch.name)
|
|
2617
|
-
});
|
|
2618
|
-
return c.text(`channel not found: ${query.channel}`);
|
|
2619
|
-
}
|
|
2620
|
-
} else if (channels.length === 1 && channels[0]) targetChannel = channels[0];
|
|
2621
|
-
else {
|
|
2622
|
-
const names = channels.map((ch) => ch.name);
|
|
2623
|
-
if (isJson) return c.json({
|
|
2624
|
-
error: "multiple channels — specify one with --channel or use --all",
|
|
2625
|
-
channels: names,
|
|
2626
|
-
hint: "use --all for all channels at once"
|
|
2627
|
-
});
|
|
2628
|
-
return c.text(`multiple channels — specify one with --channel or use --all:\n${names.map((n) => ` - ${n}`).join("\n")}`);
|
|
2629
|
-
}
|
|
2630
|
-
const report = await buildChannelReport(targetChannel, gatewayStatus, gatewayBodyOrNull, store, eventLimit);
|
|
2631
|
-
if (isJson) return c.json(report);
|
|
2632
|
-
return c.text(renderText(report));
|
|
2633
|
-
});
|
|
2634
|
-
const debugReplayHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
2635
|
-
channel: z.string().optional(),
|
|
2636
|
-
seq: z.string().optional(),
|
|
2637
|
-
json: z.enum([
|
|
2253
|
+
aggressive: z.enum([
|
|
2638
2254
|
"true",
|
|
2639
2255
|
"false",
|
|
2640
2256
|
""
|
|
2641
2257
|
]).optional()
|
|
2642
|
-
}), `funnel
|
|
2258
|
+
}), `funnel doctor / diagnose every channel; --fix applies safe self-healing
|
|
2643
2259
|
|
|
2644
|
-
usage
|
|
2260
|
+
usage / funnel doctor [--fix] [--aggressive]
|
|
2645
2261
|
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
--
|
|
2649
|
-
--
|
|
2262
|
+
modes:
|
|
2263
|
+
(default) / read-only diagnosis, safe to run anytime
|
|
2264
|
+
--fix / start the gateway if down, restart dead listeners (idempotent)
|
|
2265
|
+
--fix --aggressive / also restart the gateway when safe fixes do not return things to ok
|
|
2650
2266
|
|
|
2651
|
-
|
|
2652
|
-
so subscribers receive it again. Useful to verify that Claude handles an event
|
|
2653
|
-
correctly without waiting for a real external trigger.
|
|
2267
|
+
output / valid YAML, parseable by yq
|
|
2654
2268
|
|
|
2655
|
-
|
|
2269
|
+
programmable / await funnel.doctor.run() / await funnel.doctor.run("safe") / await funnel.doctor.run("aggressive")
|
|
2656
2270
|
|
|
2657
2271
|
examples:
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2272
|
+
funnel doctor
|
|
2273
|
+
funnel doctor --fix
|
|
2274
|
+
funnel doctor --fix --aggressive`), async (c) => {
|
|
2661
2275
|
const query = c.req.valid("query");
|
|
2662
|
-
const
|
|
2663
|
-
const
|
|
2664
|
-
const
|
|
2665
|
-
const
|
|
2666
|
-
|
|
2667
|
-
if (resolved.reason === "not-found") {
|
|
2668
|
-
if (isJson) return c.json({
|
|
2669
|
-
error: `channel not found: ${resolved.name}`,
|
|
2670
|
-
availableChannels: channels.map((ch) => ch.name)
|
|
2671
|
-
});
|
|
2672
|
-
return c.text(`channel not found: ${resolved.name}`);
|
|
2673
|
-
}
|
|
2674
|
-
if (resolved.reason === "ambiguous") {
|
|
2675
|
-
if (isJson) return c.json({
|
|
2676
|
-
error: "multiple channels — specify one with --channel",
|
|
2677
|
-
channels: resolved.names
|
|
2678
|
-
});
|
|
2679
|
-
return c.text(`multiple channels — specify one with --channel:\n${resolved.names.map((n) => ` - ${n}`).join("\n")}`);
|
|
2680
|
-
}
|
|
2681
|
-
if (isJson) return c.json({ error: "no channels configured" });
|
|
2682
|
-
return c.text("no channels configured");
|
|
2683
|
-
}
|
|
2684
|
-
const targetChannel = resolved.channel;
|
|
2685
|
-
const store = resolveStoreOrNull();
|
|
2686
|
-
if (!store) {
|
|
2687
|
-
if (isJson) return c.json({ error: "no diagnostic store yet (start the gateway first)" });
|
|
2688
|
-
return c.text("no diagnostic store yet (start the gateway first)");
|
|
2689
|
-
}
|
|
2690
|
-
const rows = query.seq ? queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT seq, event_id, type, payload, connector_id, channel_id FROM processed WHERE channel_id = ? AND seq = ? LIMIT 1", [targetChannel.id, Number(query.seq)]) : queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT seq, event_id, type, payload, connector_id, channel_id FROM processed WHERE channel_id = ? AND outcome LIKE 'emitted%' ORDER BY seq DESC LIMIT 1", [targetChannel.id]);
|
|
2691
|
-
if (rows instanceof Error) {
|
|
2692
|
-
if (isJson) return c.json({ error: rows.message });
|
|
2693
|
-
return c.text(`error: ${rows.message}`);
|
|
2694
|
-
}
|
|
2695
|
-
const firstRow = rows[0];
|
|
2696
|
-
if (!firstRow) {
|
|
2697
|
-
if (isJson) return c.json({ error: "no matching event found" });
|
|
2698
|
-
return c.text("no matching event found");
|
|
2699
|
-
}
|
|
2700
|
-
const seq = typeof firstRow.seq === "number" ? firstRow.seq : null;
|
|
2701
|
-
const eventId = typeof firstRow.event_id === "string" ? firstRow.event_id : null;
|
|
2702
|
-
const connectorId = typeof firstRow.connector_id === "string" ? firstRow.connector_id : null;
|
|
2703
|
-
let content = typeof firstRow.payload === "string" ? firstRow.payload : null;
|
|
2704
|
-
if ((!content || content.length === 0) && eventId) {
|
|
2705
|
-
const rawRows = queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT payload FROM raw WHERE event_id = ? LIMIT 1", [eventId]);
|
|
2706
|
-
const rawRow = rawRows instanceof Error ? null : rawRows[0];
|
|
2707
|
-
if (rawRow) content = typeof rawRow.payload === "string" ? rawRow.payload : null;
|
|
2708
|
-
}
|
|
2709
|
-
if (!content) {
|
|
2710
|
-
if (isJson) return c.json({ error: "event has no payload to replay" });
|
|
2711
|
-
return c.text("event has no payload to replay");
|
|
2712
|
-
}
|
|
2713
|
-
const connectorName = connectorOf(targetChannel, connectorId);
|
|
2714
|
-
const result = await funnel.publisher.publish(targetChannel.name, {
|
|
2715
|
-
content,
|
|
2716
|
-
connector: connectorName
|
|
2717
|
-
});
|
|
2718
|
-
if (result.state === "offline") {
|
|
2719
|
-
if (isJson) return c.json({
|
|
2720
|
-
error: "gateway daemon is not running",
|
|
2721
|
-
nextAction: "fnl gateway start"
|
|
2722
|
-
});
|
|
2723
|
-
return c.text("error: gateway daemon is not running — run: fnl gateway start");
|
|
2724
|
-
}
|
|
2725
|
-
if (result.state === "error") {
|
|
2726
|
-
if (isJson) return c.json({ error: result.reason });
|
|
2727
|
-
return c.text(`error: ${result.reason}`);
|
|
2728
|
-
}
|
|
2729
|
-
const preview = previewOf(content);
|
|
2730
|
-
if (isJson) return c.json({
|
|
2731
|
-
replayed: true,
|
|
2732
|
-
seq,
|
|
2733
|
-
offset: result.offset,
|
|
2734
|
-
preview
|
|
2735
|
-
});
|
|
2736
|
-
return c.text(`replayed seq=${seq ?? "?"} → offset=${result.offset}${preview ? ` "${preview}"` : ""}`);
|
|
2276
|
+
const wantsFix = query.fix === "true" || query.fix === "";
|
|
2277
|
+
const wantsAggressive = query.aggressive === "true" || query.aggressive === "";
|
|
2278
|
+
const mode = wantsFix ? wantsAggressive ? "aggressive" : "safe" : "off";
|
|
2279
|
+
const report = await c.env.funnel.doctor.run(mode);
|
|
2280
|
+
return c.text(renderYaml(report));
|
|
2737
2281
|
});
|
|
2738
2282
|
//#endregion
|
|
2739
2283
|
//#region lib/cli/routes/gateway.ts
|
|
2740
|
-
const groupHelp$1 = `funnel gateway
|
|
2284
|
+
const groupHelp$1 = `funnel gateway / manage the funnel daemon
|
|
2741
2285
|
|
|
2742
2286
|
The gateway daemon hosts the WebSocket /ws (used by Claude MCP) and the
|
|
2743
2287
|
listener supervisor that runs every connector. One daemon, one port (9743
|
|
2744
2288
|
for the CLI, 9742 for programmatic use), one PID file.
|
|
2745
2289
|
|
|
2746
|
-
usage
|
|
2290
|
+
usage / funnel gateway [subcommand]
|
|
2747
2291
|
|
|
2748
2292
|
subcommands:
|
|
2749
|
-
status
|
|
2750
|
-
start
|
|
2751
|
-
stop
|
|
2752
|
-
restart
|
|
2753
|
-
run
|
|
2754
|
-
logs [-n <N>]
|
|
2755
|
-
sql
|
|
2756
|
-
listeners
|
|
2293
|
+
status / show running status (default)
|
|
2294
|
+
start / start in background
|
|
2295
|
+
stop / stop
|
|
2296
|
+
restart / stop then start
|
|
2297
|
+
run / start in foreground (for developers)
|
|
2298
|
+
logs [-n <N>] / tail the daemon diagnostic log
|
|
2299
|
+
sql / query inbound connector traffic
|
|
2300
|
+
listeners / list running connector listeners
|
|
2757
2301
|
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
funnel gateway logs stream the daemon log
|
|
2762
|
-
funnel gateway sql --preset recent inspect last 20 inbound events
|
|
2302
|
+
output / valid YAML
|
|
2303
|
+
|
|
2304
|
+
see also / fnl doctor --fix (one command does diagnose + restart) / fnl debug --channel <name> (per-channel inspection)
|
|
2763
2305
|
|
|
2764
|
-
|
|
2306
|
+
programmable / funnel.gateway.start() / .stop() / .restart() / .getStatus() / funnel.doctor.run("safe")
|
|
2307
|
+
|
|
2308
|
+
examples:
|
|
2309
|
+
funnel gateway
|
|
2310
|
+
funnel gateway restart
|
|
2311
|
+
funnel gateway logs
|
|
2312
|
+
funnel gateway sql --preset recent`;
|
|
2765
2313
|
const renderGatewayStatus = async (c) => {
|
|
2766
2314
|
const status = c.env.funnel.gateway.getStatus();
|
|
2767
|
-
if (!status.running)
|
|
2315
|
+
if (!status.running) return c.text(renderYaml({ running: false }), 503);
|
|
2768
2316
|
const res = await fetch(`http://127.0.0.1:${status.port}/status`).catch(() => null);
|
|
2769
|
-
if (!res) return c.text(
|
|
2317
|
+
if (!res) return c.text(renderYaml({
|
|
2318
|
+
running: true,
|
|
2319
|
+
pid: status.pid,
|
|
2320
|
+
port: status.port,
|
|
2321
|
+
error: "health check failed"
|
|
2322
|
+
}));
|
|
2770
2323
|
const data = await res.json();
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
lines.push(` clients: ${data.clients.length}`);
|
|
2792
|
-
for (const cl of data.clients) {
|
|
2793
|
-
const connectors = cl.connectors.length > 0 ? ` → ${cl.connectors.join(", ")}` : "";
|
|
2794
|
-
lines.push(` · ${cl.channel}${connectors}`);
|
|
2795
|
-
}
|
|
2796
|
-
}
|
|
2797
|
-
return c.text(lines.join("\n"));
|
|
2324
|
+
return c.text(renderYaml({
|
|
2325
|
+
running: true,
|
|
2326
|
+
pid: data.pid,
|
|
2327
|
+
port: status.port,
|
|
2328
|
+
uptimeMs: data.uptimeMs,
|
|
2329
|
+
events: data.broadcaster.eventsBroadcast,
|
|
2330
|
+
listeners: data.listeners.map((l) => ({
|
|
2331
|
+
channel: l.channelName,
|
|
2332
|
+
name: l.name,
|
|
2333
|
+
type: l.type,
|
|
2334
|
+
alive: l.alive,
|
|
2335
|
+
events: l.events,
|
|
2336
|
+
errors: l.errors,
|
|
2337
|
+
lastEventAt: l.lastEventAt
|
|
2338
|
+
})),
|
|
2339
|
+
clients: data.clients.map((cl) => ({
|
|
2340
|
+
channel: cl.channel,
|
|
2341
|
+
connectors: cl.connectors
|
|
2342
|
+
}))
|
|
2343
|
+
}));
|
|
2798
2344
|
};
|
|
2799
2345
|
const gatewayGroupHandler = factory.createHandlers(zValidator$1("query", z.object({}), groupHelp$1), renderGatewayStatus);
|
|
2800
|
-
const gatewayListenersHandler = factory.createHandlers(zValidator$1("query", z.object({}), `funnel gateway listeners
|
|
2346
|
+
const gatewayListenersHandler = factory.createHandlers(zValidator$1("query", z.object({}), `funnel gateway listeners / show running connector listeners
|
|
2801
2347
|
|
|
2802
|
-
usage
|
|
2348
|
+
usage / funnel gateway listeners
|
|
2803
2349
|
|
|
2804
|
-
|
|
2350
|
+
output / valid YAML
|
|
2805
2351
|
|
|
2806
|
-
|
|
2807
|
-
funnel gateway listeners`), async (c) => {
|
|
2352
|
+
programmable / funnel.listeners.list() / funnel.recovery.restartAllDeadListeners()`), async (c) => {
|
|
2808
2353
|
const result = await c.env.funnel.listeners.list();
|
|
2809
2354
|
if (result.state === "offline") throw new HTTPException(503, { message: "funnel gateway: not running" });
|
|
2810
2355
|
if (result.state === "error") throw new HTTPException(503, { message: `funnel gateway: ${result.reason}` });
|
|
2811
|
-
|
|
2812
|
-
const lines = result.listeners.map((entry) => {
|
|
2813
|
-
return ` [${(entry.alive ? "alive" : "dead").padEnd(5)}] ${entry.type.padEnd(8)} ${entry.name}`;
|
|
2814
|
-
});
|
|
2815
|
-
return c.text(`funnel gateway: running listeners\n${lines.join("\n")}`);
|
|
2356
|
+
return c.text(renderYaml({ listeners: result.listeners }));
|
|
2816
2357
|
});
|
|
2817
2358
|
//#endregion
|
|
2818
2359
|
//#region lib/cli/routes/gateway.logs.ts
|
|
@@ -2840,7 +2381,10 @@ examples:
|
|
|
2840
2381
|
funnel gateway logs -n 100
|
|
2841
2382
|
funnel gateway logs --format json | jq 'select(.level == "error")'
|
|
2842
2383
|
|
|
2843
|
-
see also: fnl debug, fnl gateway sql
|
|
2384
|
+
see also: fnl debug, fnl gateway sql
|
|
2385
|
+
|
|
2386
|
+
programmable: this command tails a file directly. For structured introspection,
|
|
2387
|
+
prefer funnel.diagnostics.diagnoseAll() / .recentEvents() in code.`;
|
|
2844
2388
|
const logger = new NodeFunnelLogger();
|
|
2845
2389
|
const tryParseJson = (line) => {
|
|
2846
2390
|
try {
|
|
@@ -2924,6 +2468,13 @@ const PRESETS = {
|
|
|
2924
2468
|
summary: "SELECT outcome, COUNT(*) AS count FROM processed GROUP BY outcome ORDER BY count DESC",
|
|
2925
2469
|
"trace-dedup": "SELECT r.seq, r.ts, r.event_id, r.payload FROM raw r JOIN processed p USING(event_id) WHERE p.outcome='skip:dedup' ORDER BY r.seq DESC LIMIT 20"
|
|
2926
2470
|
};
|
|
2471
|
+
const PRESETS_BY_CHANNEL = {
|
|
2472
|
+
recent: "SELECT seq, ts, type, outcome FROM processed WHERE channel_id = ? ORDER BY seq DESC LIMIT 20",
|
|
2473
|
+
skipped: "SELECT seq, ts, type, outcome, payload FROM processed WHERE channel_id = ? AND outcome LIKE 'skip:%' ORDER BY seq DESC LIMIT 20",
|
|
2474
|
+
errors: "SELECT ts, status, detail FROM connection WHERE channel_id = ? AND status IN ('auth-failed','error') ORDER BY seq DESC LIMIT 20",
|
|
2475
|
+
summary: "SELECT outcome, COUNT(*) AS count FROM processed WHERE channel_id = ? GROUP BY outcome ORDER BY count DESC",
|
|
2476
|
+
"trace-dedup": "SELECT r.seq, r.ts, r.event_id, r.payload FROM raw r JOIN processed p USING(event_id) WHERE r.channel_id = ? AND p.outcome='skip:dedup' ORDER BY r.seq DESC LIMIT 20"
|
|
2477
|
+
};
|
|
2927
2478
|
const sqlHelp = `funnel gateway sql — query inbound connector traffic with SQL
|
|
2928
2479
|
|
|
2929
2480
|
usage: funnel gateway sql --preset <name> [--channel <name|id>] [--limit <N>]
|
|
@@ -2963,7 +2514,12 @@ examples:
|
|
|
2963
2514
|
|
|
2964
2515
|
tip: for a higher-level view without writing SQL, use: fnl debug --channel <name> --json
|
|
2965
2516
|
|
|
2966
|
-
see also: fnl debug, fnl gateway logs
|
|
2517
|
+
see also: fnl debug, fnl gateway logs
|
|
2518
|
+
|
|
2519
|
+
programmable: const reader = new ConnectorDiagnosticSqlReader({ rawPath, processedPath, connectionPath })
|
|
2520
|
+
reader.query("SELECT …")
|
|
2521
|
+
— but for most cases, funnel.diagnostics.recentEvents() / .droppedEvents()
|
|
2522
|
+
/ .connectionErrors() are higher level and don't need SQL.`;
|
|
2967
2523
|
const gatewaySqlHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
2968
2524
|
query: z.string().optional(),
|
|
2969
2525
|
preset: z.enum(Object.keys(PRESETS)).optional(),
|
|
@@ -2977,17 +2533,15 @@ const gatewaySqlHandler = factory.createHandlers(zValidator$1("query", z.object(
|
|
|
2977
2533
|
let resolvedChannelId = null;
|
|
2978
2534
|
if (query.channel) resolvedChannelId = funnel.channels.list().find((ch) => ch.id === query.channel || ch.name === query.channel)?.id ?? query.channel;
|
|
2979
2535
|
if (query.preset) {
|
|
2980
|
-
const base = PRESETS[query.preset] ?? null;
|
|
2536
|
+
const base = resolvedChannelId ? PRESETS_BY_CHANNEL[query.preset] ?? null : PRESETS[query.preset] ?? null;
|
|
2981
2537
|
if (!base) return c.text(sqlHelp);
|
|
2982
2538
|
let applied = base;
|
|
2983
2539
|
if (query.limit) {
|
|
2984
2540
|
const n = Math.max(1, Number(query.limit));
|
|
2985
2541
|
applied = applied.replace(/LIMIT \d+/, `LIMIT ${n}`);
|
|
2986
2542
|
}
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
params = [resolvedChannelId];
|
|
2990
|
-
} else sql = applied;
|
|
2543
|
+
sql = applied;
|
|
2544
|
+
params = resolvedChannelId ? [resolvedChannelId] : [];
|
|
2991
2545
|
} else if (query.query) sql = query.query;
|
|
2992
2546
|
if (!sql) return c.text(sqlHelp);
|
|
2993
2547
|
const tmpDir = funnelTmpDir();
|
|
@@ -3007,8 +2561,8 @@ const gatewaySqlHandler = factory.createHandlers(zValidator$1("query", z.object(
|
|
|
3007
2561
|
reader.close();
|
|
3008
2562
|
}
|
|
3009
2563
|
})();
|
|
3010
|
-
if (rows instanceof Error) return c.text(
|
|
3011
|
-
return c.text(
|
|
2564
|
+
if (rows instanceof Error) return c.text(renderYaml({ error: rows.message }));
|
|
2565
|
+
return c.text(renderYaml({ rows }));
|
|
3012
2566
|
});
|
|
3013
2567
|
const gatewayRestartHandler = factory.createHandlers(zValidator$1("query", z.object({ "no-caffeine": z.string().optional() }), `funnel gateway restart — restart the gateway
|
|
3014
2568
|
|
|
@@ -3019,7 +2573,10 @@ On macOS wraps with caffeinate -is by default. Use --no-caffeine to disable.
|
|
|
3019
2573
|
|
|
3020
2574
|
examples:
|
|
3021
2575
|
funnel gateway restart
|
|
3022
|
-
funnel gateway restart --no-caffeine
|
|
2576
|
+
funnel gateway restart --no-caffeine
|
|
2577
|
+
|
|
2578
|
+
programmable: funnel.gateway.restart({ caffeinate })
|
|
2579
|
+
funnel.recovery.restartGateway()`), async (c) => {
|
|
3023
2580
|
const query = c.req.valid("query");
|
|
3024
2581
|
const result = await c.env.funnel.gateway.restart({ caffeinate: query["no-caffeine"] !== "true" });
|
|
3025
2582
|
const lines = [];
|
|
@@ -3040,7 +2597,9 @@ For normal usage prefer funnel gateway start.
|
|
|
3040
2597
|
|
|
3041
2598
|
examples:
|
|
3042
2599
|
funnel gateway run
|
|
3043
|
-
funnel gateway run --no-caffeine
|
|
2600
|
+
funnel gateway run --no-caffeine
|
|
2601
|
+
|
|
2602
|
+
programmable: funnel.runGatewayForeground({ caffeinate })`), async (c) => {
|
|
3044
2603
|
const query = c.req.valid("query");
|
|
3045
2604
|
const exitCode = await c.env.funnel.runGatewayForeground({ caffeinate: query["no-caffeine"] !== "true" });
|
|
3046
2605
|
process.exit(exitCode);
|
|
@@ -3061,7 +2620,9 @@ log: ${join(funnelTmpDir(), "gateway.log")}
|
|
|
3061
2620
|
|
|
3062
2621
|
examples:
|
|
3063
2622
|
funnel gateway start
|
|
3064
|
-
funnel gateway start --no-caffeine
|
|
2623
|
+
funnel gateway start --no-caffeine
|
|
2624
|
+
|
|
2625
|
+
programmable: funnel.gateway.start({ caffeinate })`;
|
|
3065
2626
|
const gatewayStartHandler = factory.createHandlers(zValidator$1("query", z.object({ "no-caffeine": z.string().optional() }), startHelp), async (c) => {
|
|
3066
2627
|
const query = c.req.valid("query");
|
|
3067
2628
|
const funnel = c.env.funnel;
|
|
@@ -3072,41 +2633,13 @@ const gatewayStartHandler = factory.createHandlers(zValidator$1("query", z.objec
|
|
|
3072
2633
|
if (!await funnel.gateway.start({ caffeinate: query["no-caffeine"] !== "true" })) throw new HTTPException(500, { message: "funnel gateway: failed to start" });
|
|
3073
2634
|
return c.text("funnel gateway: started");
|
|
3074
2635
|
});
|
|
3075
|
-
const gatewayStatusHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
3076
|
-
"true",
|
|
3077
|
-
"false",
|
|
3078
|
-
""
|
|
3079
|
-
]).optional() }), `funnel gateway status — show gateway running status
|
|
2636
|
+
const gatewayStatusHandler = factory.createHandlers(zValidator$1("query", z.object({}), `funnel gateway status / show gateway running status
|
|
3080
2637
|
|
|
3081
|
-
usage
|
|
3082
|
-
|
|
3083
|
-
options:
|
|
3084
|
-
--json output as JSON
|
|
2638
|
+
usage / funnel gateway status
|
|
3085
2639
|
|
|
3086
|
-
|
|
3087
|
-
When not running, exits with 503.
|
|
2640
|
+
output / valid YAML
|
|
3088
2641
|
|
|
3089
|
-
|
|
3090
|
-
funnel gateway status
|
|
3091
|
-
funnel gateway status --json`), async (c) => {
|
|
3092
|
-
const query = c.req.valid("query");
|
|
3093
|
-
if (!(query.json === "true" || query.json === "")) return renderGatewayStatus(c);
|
|
3094
|
-
const status = c.env.funnel.gateway.getStatus();
|
|
3095
|
-
if (!status.running) throw new HTTPException(503, { message: "funnel gateway: not running" });
|
|
3096
|
-
const res = await fetch(`http://127.0.0.1:${status.port}/status`).catch(() => null);
|
|
3097
|
-
if (!res) return c.json({
|
|
3098
|
-
running: true,
|
|
3099
|
-
pid: status.pid,
|
|
3100
|
-
port: status.port,
|
|
3101
|
-
error: "health check failed"
|
|
3102
|
-
});
|
|
3103
|
-
const data = await res.json();
|
|
3104
|
-
return c.json({
|
|
3105
|
-
running: true,
|
|
3106
|
-
port: status.port,
|
|
3107
|
-
...data
|
|
3108
|
-
});
|
|
3109
|
-
});
|
|
2642
|
+
programmable / funnel.gateway.getStatus()`), async (c) => renderGatewayStatus(c));
|
|
3110
2643
|
const gatewayStopHandler = factory.createHandlers(zValidator$1("query", z.object({}), `funnel gateway stop — stop the gateway
|
|
3111
2644
|
|
|
3112
2645
|
usage: funnel gateway stop
|
|
@@ -3322,39 +2855,42 @@ const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object
|
|
|
3322
2855
|
});
|
|
3323
2856
|
return c.text(`updated profile "${param.profile}"`);
|
|
3324
2857
|
});
|
|
3325
|
-
const profilesGroupHandler = factory.createHandlers(zValidator$1("query", z.object({}), `funnel profiles
|
|
2858
|
+
const profilesGroupHandler = factory.createHandlers(zValidator$1("query", z.object({}), `funnel profiles / manage launch profiles
|
|
3326
2859
|
|
|
3327
|
-
usage
|
|
2860
|
+
usage / funnel profiles [subcommand]
|
|
3328
2861
|
|
|
3329
2862
|
subcommands:
|
|
3330
|
-
(none)
|
|
2863
|
+
(none) / list (first entry is the default)
|
|
3331
2864
|
add <name> --path <path> --channel <channel> [--agent ...] [--options ...] [--env ...] [--no-resume]
|
|
3332
2865
|
<name> set [--path ...] [--channel ...] [--agent ...] [--options ...] [--env ...] [--resume|--no-resume]
|
|
3333
|
-
<name> as-default
|
|
3334
|
-
rename <old> <new>
|
|
3335
|
-
remove <name>
|
|
3336
|
-
<name> run
|
|
3337
|
-
<name>
|
|
2866
|
+
<name> as-default / move profile to the front
|
|
2867
|
+
rename <old> <new> / rename
|
|
2868
|
+
remove <name> / remove
|
|
2869
|
+
<name> run / launch (sugar for fnl claude -p <name>)
|
|
2870
|
+
<name> / launch (alias for run)
|
|
3338
2871
|
|
|
3339
|
-
A profile carries the launch recipe —
|
|
3340
|
-
|
|
3341
|
-
|
|
2872
|
+
A profile carries the launch recipe — --agent / --options prepended to the
|
|
2873
|
+
claude argv, --env layered under the process, --resume toggling session
|
|
2874
|
+
reuse. The channel it points at only declares transport (connectors).
|
|
2875
|
+
|
|
2876
|
+
output / valid YAML
|
|
2877
|
+
|
|
2878
|
+
programmable / funnel.profiles.list() / .add() / .remove() / .rename() / .setDefault()
|
|
3342
2879
|
|
|
3343
2880
|
examples:
|
|
3344
2881
|
funnel profiles add cto --path /repo/myapp --channel prod-inbox --agent pm --options "--brief"
|
|
3345
2882
|
funnel profiles cto as-default
|
|
3346
2883
|
funnel profiles cto run`), (c) => {
|
|
3347
|
-
c.env.funnel;
|
|
3348
2884
|
const { profiles } = c.env;
|
|
3349
2885
|
const profileList = profiles.list();
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
2886
|
+
return c.text(renderYaml({ profiles: profileList.map((profile, index) => ({
|
|
2887
|
+
name: profile.name,
|
|
2888
|
+
default: index === 0,
|
|
2889
|
+
path: profile.path,
|
|
2890
|
+
channelId: profile.channelId,
|
|
2891
|
+
options: profile.options,
|
|
2892
|
+
resume: profile.resume
|
|
2893
|
+
})) }));
|
|
3358
2894
|
});
|
|
3359
2895
|
const schemaHandler = factory.createHandlers(zValidator$1("query", z.object({}), `funnel schema — print the JSON Schema for funnel.json
|
|
3360
2896
|
|
|
@@ -3370,60 +2906,51 @@ can validate and autocomplete the config:
|
|
|
3370
2906
|
{
|
|
3371
2907
|
"$schema": "./funnel.schema.json",
|
|
3372
2908
|
"channel": "ops"
|
|
3373
|
-
}
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
programmable: import { funnelJsonSchema } from "@interactive-inc/claude-funnel/local-config"
|
|
2912
|
+
funnelJsonSchema() // returns the same object as the CLI prints`), async (c) => {
|
|
3374
2913
|
const schema = funnelJsonSchema();
|
|
3375
2914
|
return c.text(`${JSON.stringify(schema, null, 2)}\n`);
|
|
3376
2915
|
});
|
|
3377
2916
|
//#endregion
|
|
3378
2917
|
//#region lib/cli/routes/status.ts
|
|
3379
|
-
const statusHelp = `funnel status
|
|
2918
|
+
const statusHelp = `funnel status / overall health snapshot
|
|
3380
2919
|
|
|
3381
|
-
usage
|
|
2920
|
+
usage / funnel status [--watch] [--interval <N>]
|
|
3382
2921
|
|
|
3383
2922
|
options:
|
|
3384
|
-
--watch
|
|
3385
|
-
--interval <N>
|
|
2923
|
+
--watch / continuously refresh (Ctrl+C to stop)
|
|
2924
|
+
--interval <N> / polling interval in seconds (default 3)
|
|
2925
|
+
|
|
2926
|
+
output / valid YAML
|
|
3386
2927
|
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
2928
|
+
For a richer diagnosis with rootCause + nextActions, prefer fnl doctor.
|
|
2929
|
+
|
|
2930
|
+
programmable / funnel.gateway.getStatus() / funnel.doctor.run()
|
|
3390
2931
|
|
|
3391
2932
|
examples:
|
|
3392
2933
|
funnel status
|
|
3393
2934
|
funnel status --watch
|
|
3394
|
-
funnel status --watch --interval 5
|
|
3395
|
-
|
|
3396
|
-
see also: fnl debug --channel <name> (per-channel diagnosis with next steps)`;
|
|
2935
|
+
funnel status --watch --interval 5`;
|
|
3397
2936
|
const isGatewayStatus = (value) => {
|
|
3398
2937
|
if (value === null || typeof value !== "object") return false;
|
|
3399
2938
|
if (!("clients" in value) || !Array.isArray(value.clients)) return false;
|
|
3400
2939
|
if (!("listeners" in value) || !Array.isArray(value.listeners)) return false;
|
|
3401
2940
|
return true;
|
|
3402
2941
|
};
|
|
3403
|
-
const
|
|
2942
|
+
const buildStatusReport = async (funnel, profiles) => {
|
|
3404
2943
|
const channels = funnel.channels.list();
|
|
3405
2944
|
const profileList = profiles.list();
|
|
3406
2945
|
const gatewayStatus = funnel.gateway.getStatus();
|
|
3407
|
-
const lines = [];
|
|
3408
|
-
lines.push("= funnel status =");
|
|
3409
|
-
lines.push("");
|
|
3410
2946
|
let gatewayData = null;
|
|
3411
|
-
if (
|
|
3412
|
-
else {
|
|
2947
|
+
if (gatewayStatus.running) {
|
|
3413
2948
|
const res = await fetch(`http://127.0.0.1:${gatewayStatus.port}/status`).catch(() => null);
|
|
3414
|
-
let uptimeStr = "";
|
|
3415
2949
|
if (res && res.ok) {
|
|
3416
2950
|
const body = await res.json();
|
|
3417
|
-
if (isGatewayStatus(body))
|
|
3418
|
-
gatewayData = body;
|
|
3419
|
-
const uptimeSec = Math.floor(body.uptimeMs / 1e3);
|
|
3420
|
-
const uptimeMin = Math.floor(uptimeSec / 60);
|
|
3421
|
-
uptimeStr = uptimeMin >= 60 ? ` · ${Math.floor(uptimeMin / 60)}h ${uptimeMin % 60}m` : uptimeSec >= 60 ? ` · ${uptimeMin}m ${uptimeSec % 60}s` : ` · ${uptimeSec}s`;
|
|
3422
|
-
}
|
|
2951
|
+
if (isGatewayStatus(body)) gatewayData = body;
|
|
3423
2952
|
}
|
|
3424
|
-
lines.push(`gateway: running (pid ${gatewayStatus.pid}, port ${gatewayStatus.port})${uptimeStr}`);
|
|
3425
2953
|
}
|
|
3426
|
-
lines.push("");
|
|
3427
2954
|
const clientsByChannel = /* @__PURE__ */ new Map();
|
|
3428
2955
|
const listenerAliveByChannel = /* @__PURE__ */ new Map();
|
|
3429
2956
|
if (gatewayData) {
|
|
@@ -3436,26 +2963,33 @@ const buildStatusLines = async (funnel, profiles) => {
|
|
|
3436
2963
|
listenerAliveByChannel.set(listener.channelName, current === void 0 ? listener.alive : current && listener.alive);
|
|
3437
2964
|
}
|
|
3438
2965
|
}
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
2966
|
+
return {
|
|
2967
|
+
gateway: gatewayStatus.running ? {
|
|
2968
|
+
running: true,
|
|
2969
|
+
pid: gatewayStatus.pid,
|
|
2970
|
+
port: gatewayStatus.port,
|
|
2971
|
+
uptimeMs: gatewayData?.uptimeMs ?? null
|
|
2972
|
+
} : { running: false },
|
|
2973
|
+
channels: channels.map((ch) => ({
|
|
2974
|
+
name: ch.name,
|
|
2975
|
+
connectors: ch.connectors.map((conn) => ({
|
|
2976
|
+
name: conn.name,
|
|
2977
|
+
type: conn.type
|
|
2978
|
+
})),
|
|
2979
|
+
listenerAlive: gatewayData === null ? null : listenerAliveByChannel.get(ch.name) ?? null,
|
|
2980
|
+
claudeClients: clientsByChannel.get(ch.name) ?? 0
|
|
2981
|
+
})),
|
|
2982
|
+
profiles: profileList.map((profile, index) => {
|
|
2983
|
+
const channel = funnel.channels.getById(profile.channelId);
|
|
2984
|
+
return {
|
|
2985
|
+
name: profile.name,
|
|
2986
|
+
default: index === 0,
|
|
2987
|
+
path: profile.path,
|
|
2988
|
+
channel: channel ? channel.name : null,
|
|
2989
|
+
channelId: channel ? void 0 : profile.channelId
|
|
2990
|
+
};
|
|
2991
|
+
})
|
|
2992
|
+
};
|
|
3459
2993
|
};
|
|
3460
2994
|
const statusHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
3461
2995
|
watch: z.enum([
|
|
@@ -3470,15 +3004,14 @@ const statusHandler = factory.createHandlers(zValidator$1("query", z.object({
|
|
|
3470
3004
|
const isWatch = query.watch === "true" || query.watch === "";
|
|
3471
3005
|
const intervalSec = Math.min(60, Math.max(1, query.interval ? Number(query.interval) : 3));
|
|
3472
3006
|
if (!isWatch) {
|
|
3473
|
-
const
|
|
3474
|
-
return c.text(
|
|
3007
|
+
const report = await buildStatusReport(funnel, c.env.profiles);
|
|
3008
|
+
return c.text(renderYaml(report));
|
|
3475
3009
|
}
|
|
3476
3010
|
const render = async () => {
|
|
3477
|
-
const
|
|
3478
|
-
const ts = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
3011
|
+
const report = await buildStatusReport(funnel, c.env.profiles);
|
|
3479
3012
|
process.stdout.write("\x1B[2J\x1B[H");
|
|
3480
|
-
process.stdout.write(
|
|
3481
|
-
process.stdout.write(`\n
|
|
3013
|
+
process.stdout.write(renderYaml(report));
|
|
3014
|
+
process.stdout.write(`\n# refreshing every ${intervalSec}s; Ctrl+C to stop\n`);
|
|
3482
3015
|
};
|
|
3483
3016
|
process.on("SIGINT", () => {
|
|
3484
3017
|
process.stdout.write("\n");
|
|
@@ -3497,7 +3030,10 @@ const updateHelp = `funnel update — update funnel to the latest version
|
|
|
3497
3030
|
|
|
3498
3031
|
usage: funnel update
|
|
3499
3032
|
|
|
3500
|
-
Runs "bun i -g @interactive-inc/claude-funnel"
|
|
3033
|
+
Runs "bun i -g @interactive-inc/claude-funnel".
|
|
3034
|
+
|
|
3035
|
+
This command has no programmable equivalent — package management belongs to
|
|
3036
|
+
the host (npm / bun / yarn install in the host's own way).`;
|
|
3501
3037
|
const PACKAGE = "@interactive-inc/claude-funnel";
|
|
3502
3038
|
const updateHandler = factory.createHandlers(zValidator$1("query", z.object({}), updateHelp), async (c) => {
|
|
3503
3039
|
const exitCode = await new NodeFunnelProcessRunner().attach([
|
|
@@ -3512,8 +3048,8 @@ const updateHandler = factory.createHandlers(zValidator$1("query", z.object({}),
|
|
|
3512
3048
|
//#endregion
|
|
3513
3049
|
//#region lib/cli/routes/index.ts
|
|
3514
3050
|
const routes = factory.createApp().onError((error, c) => {
|
|
3515
|
-
if (error instanceof HTTPException) return c.text(
|
|
3051
|
+
if (error instanceof HTTPException) return c.text(error.message, error.status);
|
|
3516
3052
|
return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
|
|
3517
|
-
}).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);
|
|
3053
|
+
}).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("/docs", ...docsIndexHandler).get("/docs/:topic", ...docsTopicHandler).get("/doctor", ...doctorHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
|
|
3518
3054
|
//#endregion
|
|
3519
|
-
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, channelWsProtocols, channelWsUrl, routes as cliRoutes, connectorConfigSchema, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, createSettings, discordConnectorSchema, factory, funnelEventSchema, gatewayLoopbackUrl, ghConnectorSchema, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, resolveFunnelDir, resolveFunnelPort, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, toRequest };
|
|
3055
|
+
export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_PORT, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDiagnostics, FunnelDocs, FunnelDoctor, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLogger, FunnelProcessRunner, FunnelRecovery, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, buildServiceRoutes, channelConfigSchema, channelDeliveryModeSchema, channelWsProtocols, channelWsUrl, routes as cliRoutes, connectorConfigSchema, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, createSettings, discordConnectorSchema, factory, funnelEventSchema, gatewayLoopbackUrl, ghConnectorSchema, previewOf, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryRows, queryToCliArgs, resolveFunnelDir, resolveFunnelPort, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, toDiagnosticConnectionError, toDiagnosticEvent, toRequest };
|