@interactive-inc/claude-funnel 0.60.1 → 0.64.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 +2 -2
- package/dist/bin.js +428 -761
- package/dist/{channels-2g_BU1N0.d.ts → channels-CRGb6B5_.d.ts} +17 -16
- package/dist/claude.d.ts +5 -7
- package/dist/claude.js +143 -36
- package/dist/{connector-descriptor-6SXJoszo.d.ts → connector-descriptor-BFIhyTfa.d.ts} +49 -10
- package/dist/connector-diagnostics-recorder-COtNEmUp.js +42 -0
- package/dist/connectors/discord.d.ts +31 -37
- package/dist/connectors/discord.js +3 -3
- package/dist/connectors/gh.d.ts +37 -33
- package/dist/connectors/gh.js +3 -3
- package/dist/connectors/schedule.d.ts +9 -57
- package/dist/connectors/schedule.js +3 -3
- package/dist/connectors/slack.d.ts +106 -132
- package/dist/connectors/slack.js +4 -3
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +1 -1
- package/dist/discord-connector-DIFkYBbi.js +250 -0
- package/dist/discord-connector-schema-D-bOVAKt.d.ts +22 -0
- package/dist/docs.js +1 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +1 -1
- package/dist/{file-process-guard-C_PLxfUX.d.ts → file-process-guard-tVcgckH6.d.ts} +6 -6
- package/dist/{file-system-o51IsM0W.d.ts → file-system-VhwwXZbm.d.ts} +8 -0
- package/dist/flume-source-listener-BNyAII7N.d.ts +133 -0
- package/dist/{funnel-diagnostics-CSiJmPlZ.js → funnel-diagnostics-Cvk6Sk4x.js} +193 -43
- package/dist/{funnel-diagnostics-DpXOsCty.d.ts → funnel-diagnostics-b9ar0Ing.d.ts} +67 -5
- package/dist/{funnel-docs-BxXZ9Ksx.js → funnel-docs-C-ge0MuB.js} +42 -6
- package/dist/{funnel-doctor-CZf_0Luq.d.ts → funnel-doctor-CnRQi4kM.d.ts} +2 -2
- package/dist/{funnel-doctor-DiJCjHsg.js → funnel-doctor-XrI2GBH8.js} +1 -1
- package/dist/funnel-error-0t1MK1R6.js +75 -0
- package/dist/{funnel-recovery-DnLrdWO9.d.ts → funnel-recovery-CMhY8Jfk.d.ts} +1 -1
- package/dist/gateway/daemon.js +167 -527
- package/dist/gateway.d.ts +3 -3
- package/dist/gateway.js +3 -3
- package/dist/gh-connector-BUGCOEWS.js +187 -0
- package/dist/{gh-connector-schema-Rzwc1c1N.js → gh-connector-schema-CAqIhzGr.js} +7 -0
- package/dist/gh-connector-schema-DWQaB6gX.d.ts +16 -0
- package/dist/{index-CgY8NdMz.d.ts → index-Ds6sHhA-.d.ts} +37 -19
- package/dist/index.d.ts +182 -22
- package/dist/index.js +363 -173
- package/dist/{local-config-json-schema-JyLqOQNX.js → local-config-json-schema-DexV8vX3.js} +24 -4
- package/dist/local-config.d.ts +39 -2
- package/dist/local-config.js +53 -2
- package/dist/logger.js +1 -1
- package/dist/loopback-fetch-CVNuN3YZ.js +40 -0
- package/dist/{local-config-sync-Dh1Croqe.d.ts → memory-token-prompter-BoV8Hf-n.d.ts} +30 -3
- package/dist/node-file-system-BOXIHW_Q.js +174 -0
- package/dist/{profiles-DSzTeKQw.js → profiles-ZHLONml4.js} +49 -49
- package/dist/{profiles-Cy5wXQ0L.d.ts → profiles-cVZQkM69.d.ts} +3 -3
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +1 -1
- package/dist/recovery.d.ts +1 -1
- package/dist/recovery.js +1 -1
- package/dist/resolve-connector-token-DxDG9mhf.js +22 -0
- package/dist/{schedule-connector-L4uzg5M8.js → schedule-connector-9k3gOIgl.js} +54 -55
- package/dist/schedule-connector-schema-Z0RXLgPI.d.ts +49 -0
- package/dist/settings-reader-BNxjsxCB.d.ts +27 -0
- package/dist/{settings-store-CUKSeTXC.js → settings-store-C2QdOH-t.js} +23 -4
- package/dist/slack-connector-CxpWagbT.js +388 -0
- package/dist/slack-event-processor-BhCf5Wiy.d.ts +95 -0
- package/dist/slack-event-processor-xFDG3US0.js +176 -0
- package/dist/slot-fields-D-pvMgTK.js +249 -0
- package/dist/{memory-diagnostic-log-CI60kNfB.js → sqlite-diagnostic-log-DOTPW-tG.js} +373 -249
- package/dist/{yaml-render-93pX7EF7.js → yaml-render--J1_3BSA.js} +25 -21
- package/package.json +2 -4
- package/dist/discord-connector-BL36yvbL.js +0 -250
- package/dist/gateway-base-url-Dy4Ykuoh.js +0 -14
- package/dist/gh-connector-DpiixfQZ.js +0 -226
- package/dist/http-client-oICicjuO.d.ts +0 -18
- package/dist/memory-token-prompter-B4sjyaAq.d.ts +0 -57
- package/dist/memory-token-prompter-CZde7e6y.js +0 -61
- package/dist/node-file-system-Blr8pAir.js +0 -48
- package/dist/settings-reader-BIFB_j2f.d.ts +0 -18
- package/dist/slack-connector-DQIFPdBF.js +0 -484
- package/dist/slot-fields-CMoRpwuy.js +0 -45
- /package/dist/{connector-adapter-DU9Rvyec.js → connector-adapter-Dvs8N7ew.js} +0 -0
- /package/dist/{connector-listener-DR3aKOuK.js → connector-listener-mPGZYa8e.js} +0 -0
- /package/dist/{diagnostic-sql-reader-C9zR-Csp.js → diagnostic-sql-reader-oXZnWFf_.js} +0 -0
- /package/dist/{discord-connector-schema-B_N6IXLz.js → discord-connector-schema-B4YpWpR3.js} +0 -0
- /package/dist/{error-message-of-Byi4y0Uf.js → error-message-of-ColuYmAk.js} +0 -0
- /package/dist/{funnel-log-sqlite-sink-kqJbx2H7.js → funnel-log-sqlite-sink-DLYkY0pZ.js} +0 -0
- /package/dist/{funnel-recovery-BFdPjL6Z.js → funnel-recovery-DKnEutUS.js} +0 -0
- /package/dist/{node-http-client-lowp60Oa.js → node-http-client-u00atiKx.js} +0 -0
- /package/dist/{schedule-connector-schema-CfyuMCMh.js → schedule-connector-schema-DKEPZnVv.js} +0 -0
- /package/dist/{settings-reader-CtQ-Ix8_.js → settings-reader-9FcX3qS1.js} +0 -0
- /package/dist/{settings-schema-D1xcOqRu.d.ts → settings-schema-BL_c2Udm.d.ts} +0 -0
- /package/dist/{slack-connector-schema-C1zEf4TG.js → slack-connector-schema-Dem8to4P.js} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as
|
|
1
|
+
import { n as gatewayLoopbackUrl, t as loopbackFetch } from "./loopback-fetch-CVNuN3YZ.js";
|
|
2
|
+
import { t as errorMessageOf } from "./error-message-of-ColuYmAk.js";
|
|
3
|
+
import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-oXZnWFf_.js";
|
|
3
4
|
import { join } from "node:path";
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
5
6
|
//#region lib/services/diagnostics/diagnostic-event.ts
|
|
6
7
|
const stringOrNull = (value) => typeof value === "string" && value.length > 0 ? value : null;
|
|
7
8
|
const numberOrNull = (value) => typeof value === "number" ? value : null;
|
|
@@ -63,21 +64,44 @@ const connectorOf = (channel, connectorId) => {
|
|
|
63
64
|
if (connectorId === null) return void 0;
|
|
64
65
|
return channel.connectors?.find((connector) => connector.id === connectorId)?.name;
|
|
65
66
|
};
|
|
67
|
+
const FLAPPING_ERROR_THRESHOLD = 3;
|
|
66
68
|
const buildDiagnosis = (report) => {
|
|
67
69
|
const rootCause = (report.connectionErrors[report.connectionErrors.length - 1] ?? null)?.detail ?? null;
|
|
70
|
+
const channel = report.channel;
|
|
68
71
|
if (!report.gateway.running) return {
|
|
69
72
|
status: "error",
|
|
70
73
|
message: "gateway is not running",
|
|
71
74
|
nextActions: ["fnl gateway start"],
|
|
72
75
|
rootCause: null
|
|
73
76
|
};
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
if (report.gateway.statusError !== null) return {
|
|
78
|
+
status: "error",
|
|
79
|
+
message: `gateway running but status probe failed: ${report.gateway.statusError}`,
|
|
80
|
+
nextActions: ["fnl gateway restart"],
|
|
81
|
+
rootCause: report.gateway.statusError
|
|
82
|
+
};
|
|
83
|
+
if (report.configuredConnectors > report.listeners.length) return {
|
|
84
|
+
status: "error",
|
|
85
|
+
message: `${report.configuredConnectors} connector(s) configured but ${report.listeners.length} registered with supervisor`,
|
|
86
|
+
nextActions: ["fnl gateway restart"],
|
|
87
|
+
rootCause: "supervisor missing listeners declared in settings.json"
|
|
88
|
+
};
|
|
89
|
+
if (report.configuredConnectors === 0) return {
|
|
76
90
|
status: "warn",
|
|
77
91
|
message: "no connectors configured on this channel",
|
|
78
92
|
nextActions: [`fnl channels ${channel} connectors add <name> --type=slack ...`],
|
|
79
93
|
rootCause: null
|
|
80
94
|
};
|
|
95
|
+
const authFailed = report.connectionErrors.filter((e) => e.status === "auth-failed");
|
|
96
|
+
if (authFailed.length > 0) {
|
|
97
|
+
const detail = authFailed[authFailed.length - 1]?.detail ?? null;
|
|
98
|
+
return {
|
|
99
|
+
status: "error",
|
|
100
|
+
message: "connector credentials rejected (auth-failed)",
|
|
101
|
+
nextActions: [`fnl channels ${channel} connectors set <connector> --bot-token=<new>`, "fnl gateway restart"],
|
|
102
|
+
rootCause: detail ?? "token rejected by upstream auth.test"
|
|
103
|
+
};
|
|
104
|
+
}
|
|
81
105
|
const allDead = report.listeners.every((l) => !l.alive);
|
|
82
106
|
const someDead = report.listeners.some((l) => !l.alive);
|
|
83
107
|
if (allDead) return {
|
|
@@ -92,6 +116,13 @@ const buildDiagnosis = (report) => {
|
|
|
92
116
|
nextActions: ["fnl doctor --fix"],
|
|
93
117
|
rootCause
|
|
94
118
|
};
|
|
119
|
+
const flapping = report.listeners.filter((l) => l.errors >= FLAPPING_ERROR_THRESHOLD);
|
|
120
|
+
if (flapping.length > 0) return {
|
|
121
|
+
status: "warn",
|
|
122
|
+
message: `listener(s) flapping (≥${FLAPPING_ERROR_THRESHOLD} errors): ${flapping.map((l) => l.name).join(", ")}`,
|
|
123
|
+
nextActions: ["fnl gateway logs"],
|
|
124
|
+
rootCause
|
|
125
|
+
};
|
|
95
126
|
if (report.claudeClients === 0) return {
|
|
96
127
|
status: "warn",
|
|
97
128
|
message: "no Claude connected to this channel",
|
|
@@ -141,15 +172,15 @@ var FunnelDiagnostics = class {
|
|
|
141
172
|
const channels = this.props.channels.list();
|
|
142
173
|
const target = channelName ? channels.find((ch) => ch.name === channelName) ?? null : channels[0] ?? null;
|
|
143
174
|
if (!target) return null;
|
|
144
|
-
const
|
|
175
|
+
const gatewayProbe = await this.fetchGatewayStatus();
|
|
145
176
|
const store = this.resolveStore();
|
|
146
|
-
return this.buildChannelDiagnosis(target,
|
|
177
|
+
return this.buildChannelDiagnosis(target, gatewayProbe, store, 5);
|
|
147
178
|
}
|
|
148
179
|
async diagnoseAll() {
|
|
149
180
|
const channels = this.props.channels.list();
|
|
150
|
-
const
|
|
181
|
+
const gatewayProbe = await this.fetchGatewayStatus();
|
|
151
182
|
const store = this.resolveStore();
|
|
152
|
-
const reports = await Promise.all(channels.map((ch) => this.buildChannelDiagnosis(ch,
|
|
183
|
+
const reports = await Promise.all(channels.map((ch) => this.buildChannelDiagnosis(ch, gatewayProbe, store, 5)));
|
|
153
184
|
const errorChannels = reports.filter((r) => r.diagnosis.status === "error").map((r) => r.channel);
|
|
154
185
|
const warnChannels = reports.filter((r) => r.diagnosis.status === "warn").map((r) => r.channel);
|
|
155
186
|
const okChannels = reports.filter((r) => r.diagnosis.status === "ok").map((r) => r.channel);
|
|
@@ -167,36 +198,97 @@ var FunnelDiagnostics = class {
|
|
|
167
198
|
channels: reports
|
|
168
199
|
};
|
|
169
200
|
}
|
|
170
|
-
async recentEvents(channelName,
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const reader = new ConnectorDiagnosticSqlReader(store);
|
|
176
|
-
const rows = channelId ? queryRows(reader, "SELECT seq, ts, type, outcome, payload FROM processed WHERE channel_id = ? ORDER BY seq DESC LIMIT ?", [channelId, limit]) : queryRows(reader, "SELECT seq, ts, type, outcome, payload FROM processed ORDER BY seq DESC LIMIT ?", [limit]);
|
|
201
|
+
async recentEvents(channelName, options = {}) {
|
|
202
|
+
const limit = options.limit ?? 20;
|
|
203
|
+
const ids = this.resolveScope(channelName, options.connector);
|
|
204
|
+
if (ids === null) return [];
|
|
205
|
+
const rows = queryRows(new ConnectorDiagnosticSqlReader(ids.store), `SELECT seq, ts, type, outcome, payload, event_id FROM processed ${ids.whereClause} ORDER BY seq DESC LIMIT ?`, [...ids.params, limit]);
|
|
177
206
|
if (rows instanceof Error) return [];
|
|
178
207
|
return rows.reverse().map(toDiagnosticEvent);
|
|
179
208
|
}
|
|
180
|
-
async droppedEvents(channelName,
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
const rows = channelId ? queryRows(reader, "SELECT seq, ts, type, outcome, payload, event_id FROM processed WHERE channel_id = ? AND outcome LIKE 'skip:%' ORDER BY seq DESC LIMIT ?", [channelId, limit]) : queryRows(reader, "SELECT seq, ts, type, outcome, payload, event_id FROM processed WHERE outcome LIKE 'skip:%' ORDER BY seq DESC LIMIT ?", [limit]);
|
|
209
|
+
async droppedEvents(channelName, options = {}) {
|
|
210
|
+
const limit = options.limit ?? 20;
|
|
211
|
+
const ids = this.resolveScope(channelName, options.connector);
|
|
212
|
+
if (ids === null) return [];
|
|
213
|
+
const where = ids.whereClause ? `${ids.whereClause} AND outcome LIKE 'skip:%'` : "WHERE outcome LIKE 'skip:%'";
|
|
214
|
+
const rows = queryRows(new ConnectorDiagnosticSqlReader(ids.store), `SELECT seq, ts, type, outcome, payload, event_id FROM processed ${where} ORDER BY seq DESC LIMIT ?`, [...ids.params, limit]);
|
|
187
215
|
if (rows instanceof Error) return [];
|
|
188
216
|
return rows.reverse().map(toDiagnosticEvent);
|
|
189
217
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
218
|
+
/**
|
|
219
|
+
* Raw inbound rows the connector recorded before any processing. The most
|
|
220
|
+
* useful read when "did the event even reach us?" is the question, since
|
|
221
|
+
* the processed table never gets a row for an event the listener dropped
|
|
222
|
+
* pre-processor.
|
|
223
|
+
*/
|
|
224
|
+
async rawEvents(channelName, options = {}) {
|
|
225
|
+
const limit = options.limit ?? 20;
|
|
226
|
+
const ids = this.resolveScope(channelName, options.connector);
|
|
227
|
+
if (ids === null) return [];
|
|
228
|
+
const rows = queryRows(new ConnectorDiagnosticSqlReader(ids.store), `SELECT seq, ts, type, '' AS outcome, payload, event_id FROM raw ${ids.whereClause} ORDER BY seq DESC LIMIT ?`, [...ids.params, limit]);
|
|
229
|
+
if (rows instanceof Error) return [];
|
|
230
|
+
return rows.reverse().map(toDiagnosticEvent);
|
|
231
|
+
}
|
|
232
|
+
async connectionErrors(channelName, options = {}) {
|
|
233
|
+
const limit = options.limit ?? 20;
|
|
234
|
+
const ids = this.resolveScope(channelName, options.connector);
|
|
235
|
+
if (ids === null) return [];
|
|
236
|
+
const where = ids.whereClause ? `${ids.whereClause} AND status IN ('auth-failed','error')` : "WHERE status IN ('auth-failed','error')";
|
|
237
|
+
const rows = queryRows(new ConnectorDiagnosticSqlReader(ids.store), `SELECT seq, ts, type, status, detail FROM connection ${where} ORDER BY seq DESC LIMIT ?`, [...ids.params, limit]);
|
|
197
238
|
if (rows instanceof Error) return [];
|
|
198
239
|
return rows.reverse().map(toDiagnosticConnectionError);
|
|
199
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Full connection lifecycle for one channel/connector — started, connected,
|
|
243
|
+
* disconnected, stopped, plus the auth-failed / error rows that
|
|
244
|
+
* `connectionErrors()` already surfaces. Use when you need to see the shape
|
|
245
|
+
* of a flap (connected → reconnecting → connected → disconnected) instead
|
|
246
|
+
* of just the failures.
|
|
247
|
+
*/
|
|
248
|
+
async connectionTimeline(channelName, options = {}) {
|
|
249
|
+
const limit = options.limit ?? 20;
|
|
250
|
+
const ids = this.resolveScope(channelName, options.connector);
|
|
251
|
+
if (ids === null) return [];
|
|
252
|
+
const rows = queryRows(new ConnectorDiagnosticSqlReader(ids.store), `SELECT seq, ts, type, status, detail FROM connection ${ids.whereClause} ORDER BY seq DESC LIMIT ?`, [...ids.params, limit]);
|
|
253
|
+
if (rows instanceof Error) return [];
|
|
254
|
+
return rows.reverse().map(toDiagnosticConnectionError);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Tail of `~/.funnel/.../funnel.log`. Use when a flume internal log (e.g.
|
|
258
|
+
* `slack/auth.test failed`) needs to be read from MCP — the gateway file
|
|
259
|
+
* sink is the only place that captures structured FunnelLogger output.
|
|
260
|
+
*
|
|
261
|
+
* `grep` is a case-insensitive substring filter applied after read so all
|
|
262
|
+
* matching levels and sources are scanned.
|
|
263
|
+
*/
|
|
264
|
+
async recentLogs(options = {}) {
|
|
265
|
+
const limit = options.limit ?? 200;
|
|
266
|
+
const path = join(this.props.tmpDir, "funnel.log");
|
|
267
|
+
if (!existsSync(path)) return {
|
|
268
|
+
lines: [],
|
|
269
|
+
path: null,
|
|
270
|
+
truncated: false
|
|
271
|
+
};
|
|
272
|
+
let content;
|
|
273
|
+
try {
|
|
274
|
+
content = readFileSync(path, "utf-8");
|
|
275
|
+
} catch (error) {
|
|
276
|
+
return {
|
|
277
|
+
lines: [`(read failed: ${errorMessageOf(error)})`],
|
|
278
|
+
path,
|
|
279
|
+
truncated: false
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
const all = content.split("\n").filter((line) => line.length > 0);
|
|
283
|
+
const needle = options.grep?.toLowerCase();
|
|
284
|
+
const filtered = needle ? all.filter((line) => line.toLowerCase().includes(needle)) : all;
|
|
285
|
+
const truncated = filtered.length > limit;
|
|
286
|
+
return {
|
|
287
|
+
lines: truncated ? filtered.slice(filtered.length - limit) : filtered,
|
|
288
|
+
path,
|
|
289
|
+
truncated
|
|
290
|
+
};
|
|
291
|
+
}
|
|
200
292
|
async replay(channelName, seq) {
|
|
201
293
|
const channel = this.props.channels.list().find((ch) => ch.name === channelName);
|
|
202
294
|
if (!channel) return { state: "not-found" };
|
|
@@ -259,19 +351,79 @@ var FunnelDiagnostics = class {
|
|
|
259
351
|
if (!channelName) return null;
|
|
260
352
|
return this.props.channels.list().find((ch) => ch.name === channelName)?.id ?? null;
|
|
261
353
|
}
|
|
354
|
+
/**
|
|
355
|
+
* Resolves a (channel, connector) filter into the SQL where-clause + the
|
|
356
|
+
* positional params, or returns `null` when the requested scope cannot be
|
|
357
|
+
* resolved (channel not found, connector not found in that channel, no
|
|
358
|
+
* store on disk yet). Centralises the channel/connector → id mapping so
|
|
359
|
+
* each read method does not redo the lookup.
|
|
360
|
+
*/
|
|
361
|
+
resolveScope(channelName, connectorName) {
|
|
362
|
+
const store = this.resolveStore();
|
|
363
|
+
if (!store) return null;
|
|
364
|
+
if (!channelName) return {
|
|
365
|
+
store,
|
|
366
|
+
whereClause: "",
|
|
367
|
+
params: []
|
|
368
|
+
};
|
|
369
|
+
const channel = this.props.channels.list().find((ch) => ch.name === channelName) ?? null;
|
|
370
|
+
if (!channel) return null;
|
|
371
|
+
if (!connectorName) return {
|
|
372
|
+
store,
|
|
373
|
+
whereClause: "WHERE channel_id = ?",
|
|
374
|
+
params: [channel.id]
|
|
375
|
+
};
|
|
376
|
+
const connectorId = channel.connectors?.find((c) => c.name === connectorName)?.id ?? null;
|
|
377
|
+
if (!connectorId) return null;
|
|
378
|
+
return {
|
|
379
|
+
store,
|
|
380
|
+
whereClause: "WHERE channel_id = ? AND connector_id = ?",
|
|
381
|
+
params: [channel.id, connectorId]
|
|
382
|
+
};
|
|
383
|
+
}
|
|
262
384
|
async fetchGatewayStatus() {
|
|
263
385
|
const gatewayStatus = this.props.gateway.getStatus();
|
|
264
|
-
if (!gatewayStatus.running) return
|
|
386
|
+
if (!gatewayStatus.running) return {
|
|
387
|
+
body: null,
|
|
388
|
+
error: null
|
|
389
|
+
};
|
|
265
390
|
const token = this.props.gatewayToken.read();
|
|
266
391
|
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
392
|
+
let res = null;
|
|
393
|
+
try {
|
|
394
|
+
res = await loopbackFetch(`${gatewayLoopbackUrl(gatewayStatus.port)}/status`, { headers });
|
|
395
|
+
} catch (error) {
|
|
396
|
+
return {
|
|
397
|
+
body: null,
|
|
398
|
+
error: `fetch failed: ${errorMessageOf(error)}`
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
if (!res.ok) return {
|
|
402
|
+
body: null,
|
|
403
|
+
error: `gateway /status returned ${res.status}`
|
|
404
|
+
};
|
|
405
|
+
let body;
|
|
406
|
+
try {
|
|
407
|
+
body = await res.json();
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return {
|
|
410
|
+
body: null,
|
|
411
|
+
error: `gateway /status body parse failed: ${errorMessageOf(error)}`
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (!isGatewayStatusResponse(body)) return {
|
|
415
|
+
body: null,
|
|
416
|
+
error: "gateway /status returned an unrecognized shape"
|
|
417
|
+
};
|
|
418
|
+
return {
|
|
419
|
+
body,
|
|
420
|
+
error: null
|
|
421
|
+
};
|
|
271
422
|
}
|
|
272
|
-
async buildChannelDiagnosis(target,
|
|
423
|
+
async buildChannelDiagnosis(target, gatewayProbe, store, eventLimit) {
|
|
273
424
|
const gatewayStatus = this.props.gateway.getStatus();
|
|
274
425
|
const targetName = target.name;
|
|
426
|
+
const gatewayBody = gatewayProbe.body;
|
|
275
427
|
const baseReport = {
|
|
276
428
|
channel: targetName,
|
|
277
429
|
channelId: target.id,
|
|
@@ -279,8 +431,10 @@ var FunnelDiagnostics = class {
|
|
|
279
431
|
running: gatewayStatus.running,
|
|
280
432
|
pid: gatewayStatus.pid,
|
|
281
433
|
port: gatewayStatus.running ? gatewayStatus.port : null,
|
|
282
|
-
uptimeMs: gatewayBody?.uptimeMs ?? null
|
|
434
|
+
uptimeMs: gatewayBody?.uptimeMs ?? null,
|
|
435
|
+
statusError: gatewayProbe.error
|
|
283
436
|
},
|
|
437
|
+
configuredConnectors: target.connectors?.length ?? 0,
|
|
284
438
|
listeners: [],
|
|
285
439
|
claudeClients: 0,
|
|
286
440
|
recentEvents: [],
|
|
@@ -300,12 +454,8 @@ var FunnelDiagnostics = class {
|
|
|
300
454
|
if (store) {
|
|
301
455
|
const evRows = queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT seq, ts, type, outcome, payload FROM processed WHERE channel_id = ? ORDER BY seq DESC LIMIT ?", [target.id, eventLimit]);
|
|
302
456
|
if (!(evRows instanceof Error)) baseReport.recentEvents = evRows.reverse().map(toDiagnosticEvent);
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
if (hasDeadListeners || hasListenerErrors) {
|
|
306
|
-
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", [target.id]);
|
|
307
|
-
if (!(errRows instanceof Error)) baseReport.connectionErrors = errRows.reverse().map(toDiagnosticConnectionError);
|
|
308
|
-
}
|
|
457
|
+
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", [target.id]);
|
|
458
|
+
if (!(errRows instanceof Error)) baseReport.connectionErrors = errRows.reverse().map(toDiagnosticConnectionError);
|
|
309
459
|
}
|
|
310
460
|
return {
|
|
311
461
|
...baseReport,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as ChannelConfig } from "./settings-schema-
|
|
1
|
+
import { t as ChannelConfig } from "./settings-schema-BL_c2Udm.js";
|
|
2
2
|
|
|
3
3
|
//#region lib/engine/diagnostic-log/diagnostic-sql-reader.d.ts
|
|
4
4
|
type Props$1 = {
|
|
@@ -104,7 +104,15 @@ type ChannelDiagnosis = {
|
|
|
104
104
|
pid: number | null;
|
|
105
105
|
port: number | null;
|
|
106
106
|
uptimeMs: number | null;
|
|
107
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Why the gateway /status probe failed to return a body. `null` when the
|
|
109
|
+
* gateway is not running (running=false makes the absence self-explanatory)
|
|
110
|
+
* or when the probe succeeded. A non-null value signals the daemon is up
|
|
111
|
+
* but the probe failed (auth refused, fetch error, non-OK response).
|
|
112
|
+
*/
|
|
113
|
+
statusError: string | null;
|
|
114
|
+
}; /** Connectors declared in settings for this channel. */
|
|
115
|
+
configuredConnectors: number;
|
|
108
116
|
listeners: Array<{
|
|
109
117
|
name: string;
|
|
110
118
|
type: string;
|
|
@@ -163,12 +171,66 @@ declare class FunnelDiagnostics {
|
|
|
163
171
|
constructor(props: Props);
|
|
164
172
|
diagnose(channelName?: string): Promise<ChannelDiagnosis | null>;
|
|
165
173
|
diagnoseAll(): Promise<DiagnoseAllReport>;
|
|
166
|
-
recentEvents(channelName: string | null,
|
|
167
|
-
|
|
168
|
-
|
|
174
|
+
recentEvents(channelName: string | null, options?: {
|
|
175
|
+
connector?: string;
|
|
176
|
+
limit?: number;
|
|
177
|
+
}): Promise<DiagnosticEvent[]>;
|
|
178
|
+
droppedEvents(channelName: string | null, options?: {
|
|
179
|
+
connector?: string;
|
|
180
|
+
limit?: number;
|
|
181
|
+
}): Promise<DiagnosticEvent[]>;
|
|
182
|
+
/**
|
|
183
|
+
* Raw inbound rows the connector recorded before any processing. The most
|
|
184
|
+
* useful read when "did the event even reach us?" is the question, since
|
|
185
|
+
* the processed table never gets a row for an event the listener dropped
|
|
186
|
+
* pre-processor.
|
|
187
|
+
*/
|
|
188
|
+
rawEvents(channelName: string | null, options?: {
|
|
189
|
+
connector?: string;
|
|
190
|
+
limit?: number;
|
|
191
|
+
}): Promise<DiagnosticEvent[]>;
|
|
192
|
+
connectionErrors(channelName: string | null, options?: {
|
|
193
|
+
connector?: string;
|
|
194
|
+
limit?: number;
|
|
195
|
+
}): Promise<DiagnosticConnectionError[]>;
|
|
196
|
+
/**
|
|
197
|
+
* Full connection lifecycle for one channel/connector — started, connected,
|
|
198
|
+
* disconnected, stopped, plus the auth-failed / error rows that
|
|
199
|
+
* `connectionErrors()` already surfaces. Use when you need to see the shape
|
|
200
|
+
* of a flap (connected → reconnecting → connected → disconnected) instead
|
|
201
|
+
* of just the failures.
|
|
202
|
+
*/
|
|
203
|
+
connectionTimeline(channelName: string | null, options?: {
|
|
204
|
+
connector?: string;
|
|
205
|
+
limit?: number;
|
|
206
|
+
}): Promise<DiagnosticConnectionError[]>;
|
|
207
|
+
/**
|
|
208
|
+
* Tail of `~/.funnel/.../funnel.log`. Use when a flume internal log (e.g.
|
|
209
|
+
* `slack/auth.test failed`) needs to be read from MCP — the gateway file
|
|
210
|
+
* sink is the only place that captures structured FunnelLogger output.
|
|
211
|
+
*
|
|
212
|
+
* `grep` is a case-insensitive substring filter applied after read so all
|
|
213
|
+
* matching levels and sources are scanned.
|
|
214
|
+
*/
|
|
215
|
+
recentLogs(options?: {
|
|
216
|
+
grep?: string;
|
|
217
|
+
limit?: number;
|
|
218
|
+
}): Promise<{
|
|
219
|
+
lines: string[];
|
|
220
|
+
path: string | null;
|
|
221
|
+
truncated: boolean;
|
|
222
|
+
}>;
|
|
169
223
|
replay(channelName: string, seq?: number): Promise<ReplayResult>;
|
|
170
224
|
resolveStore(): StorePaths | null;
|
|
171
225
|
private resolveChannelId;
|
|
226
|
+
/**
|
|
227
|
+
* Resolves a (channel, connector) filter into the SQL where-clause + the
|
|
228
|
+
* positional params, or returns `null` when the requested scope cannot be
|
|
229
|
+
* resolved (channel not found, connector not found in that channel, no
|
|
230
|
+
* store on disk yet). Centralises the channel/connector → id mapping so
|
|
231
|
+
* each read method does not redo the lookup.
|
|
232
|
+
*/
|
|
233
|
+
private resolveScope;
|
|
172
234
|
private fetchGatewayStatus;
|
|
173
235
|
private buildChannelDiagnosis;
|
|
174
236
|
}
|
|
@@ -494,9 +494,10 @@ Hono app over these services.
|
|
|
494
494
|
import { Funnel } from "@interactive-inc/claude-funnel"
|
|
495
495
|
import { slackConnector } from "@interactive-inc/claude-funnel/connectors/slack"
|
|
496
496
|
|
|
497
|
-
// Connectors are fully DI: pass only the types you use. The core import
|
|
498
|
-
// bundles a connector
|
|
499
|
-
// does. With no connectors, the funnel handles
|
|
497
|
+
// Connectors are fully DI: pass only the types you use. The core import
|
|
498
|
+
// never bundles a connector's protocol code (Socket Mode / Gateway / poller)
|
|
499
|
+
// — importing the sub-entry does. With no connectors, the funnel handles
|
|
500
|
+
// zero connector types.
|
|
500
501
|
const funnel = new Funnel({ connectors: [slackConnector()] }) // uses ~/.funnel
|
|
501
502
|
const sandbox = Funnel.inMemory() // touches no disk / process / clock
|
|
502
503
|
|
|
@@ -543,9 +544,44 @@ For targeted imports (smaller bundle / clearer dependency footprint):
|
|
|
543
544
|
import { discordConnector } from "@interactive-inc/claude-funnel/connectors/discord"
|
|
544
545
|
import { scheduleConnector } from "@interactive-inc/claude-funnel/connectors/schedule"
|
|
545
546
|
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
|
|
547
|
+
// Schedule fires can be observed by passing onFired to the descriptor:
|
|
548
|
+
// scheduleConnector({ onFired: (entry, firedAt) => { ... } })
|
|
549
|
+
|
|
550
|
+
── flume 0.9 transport notes ───────────────────────────────────────────────
|
|
551
|
+
|
|
552
|
+
Slack / Discord / GitHub connectors wrap @interactive-inc/flume 0.9. Each
|
|
553
|
+
listener owns a single-source Flume FSM and reconnect is enabled by
|
|
554
|
+
default (infinite attempts, 1s base / 30s max exponential backoff +
|
|
555
|
+
jitter), so a wifi drop or upstream socket close auto-recovers without
|
|
556
|
+
the supervisor intervening.
|
|
557
|
+
|
|
558
|
+
Source ctor Flume options (cross-cutting)
|
|
559
|
+
----------- -----------------------------
|
|
560
|
+
FlumeSlackSource({appToken, sources / onEvent (firehose) /
|
|
561
|
+
botToken}) onError / signal / deps / reconnect
|
|
562
|
+
FlumeDiscordSource({token,
|
|
563
|
+
intents}) Flume 0.9 collapsed every
|
|
564
|
+
FlumeGitHubSource({token, observation into one firehose: the
|
|
565
|
+
pollInterval}) onEvent callback receives a union of
|
|
566
|
+
{ kind: "event" } | { kind: "log" }.
|
|
567
|
+
Funnel's base listener splits this
|
|
568
|
+
back into typed events, log forward,
|
|
569
|
+
and status mapping for subclasses.
|
|
570
|
+
|
|
571
|
+
new Funnel({ signal: controller.signal }) plumbs the AbortSignal down to
|
|
572
|
+
every Flume so a host SIGTERM handler can stop every listener cleanly:
|
|
573
|
+
|
|
574
|
+
const controller = new AbortController()
|
|
575
|
+
process.on("SIGTERM", () => controller.abort())
|
|
576
|
+
const funnel = new Funnel({
|
|
577
|
+
connectors: [slackConnector(), ghConnector()],
|
|
578
|
+
signal: controller.signal,
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
Custom connector types: extend FlumeSource from the flume package and
|
|
582
|
+
write your own ConnectorDescriptor — that's the only escape hatch for
|
|
583
|
+
host-specific protocol logic, since the bundled descriptors don't take
|
|
584
|
+
extension hooks.
|
|
549
585
|
|
|
550
586
|
── in-process gateway: receive events in your own process ──────────────────
|
|
551
587
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { c as FunnelDiagnostics, n as DiagnoseAllReport, t as ChannelDiagnosis } from "./funnel-diagnostics-
|
|
2
|
-
import { n as RecoveryAction, t as FunnelRecovery } from "./funnel-recovery-
|
|
1
|
+
import { c as FunnelDiagnostics, n as DiagnoseAllReport, t as ChannelDiagnosis } from "./funnel-diagnostics-b9ar0Ing.js";
|
|
2
|
+
import { n as RecoveryAction, t as FunnelRecovery } from "./funnel-recovery-CMhY8Jfk.js";
|
|
3
3
|
|
|
4
4
|
//#region lib/services/doctor/funnel-doctor.d.ts
|
|
5
5
|
type Props = {
|
|
@@ -34,7 +34,7 @@ var FunnelDoctor = class {
|
|
|
34
34
|
if (!result.ok && result.actions.length === 0) fixFailed = true;
|
|
35
35
|
}
|
|
36
36
|
if (mode === "aggressive") {
|
|
37
|
-
if (
|
|
37
|
+
if ((await this.props.diagnostics.diagnoseAll()).channels.some((ch) => ch.diagnosis.status === "error")) {
|
|
38
38
|
const result = await this.props.recovery.restartGateway();
|
|
39
39
|
applied.push(...result.actions);
|
|
40
40
|
if (!result.ok) fixFailed = true;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region lib/engine/error/funnel-error.ts
|
|
2
|
+
/**
|
|
3
|
+
* Base class every typed funnel error extends. Hosts can branch with
|
|
4
|
+
* `instanceof FunnelError` to distinguish library failures from arbitrary
|
|
5
|
+
* thrown values, then narrow to a specific subclass for action-grade
|
|
6
|
+
* matching. The `code` field is the discriminant for serialisation /
|
|
7
|
+
* cross-process boundaries where prototypes do not survive.
|
|
8
|
+
*/
|
|
9
|
+
var FunnelError = class extends Error {
|
|
10
|
+
constructor(message, options) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = new.target.name;
|
|
13
|
+
if (options?.cause !== void 0) Object.defineProperty(this, "cause", {
|
|
14
|
+
value: options.cause,
|
|
15
|
+
enumerable: false
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var FunnelChannelNotFoundError = class extends FunnelError {
|
|
20
|
+
code = "channel-not-found";
|
|
21
|
+
constructor(channel, options) {
|
|
22
|
+
super(`channel not found: ${channel}`, options);
|
|
23
|
+
this.channel = channel;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var FunnelChannelAlreadyExistsError = class extends FunnelError {
|
|
27
|
+
code = "channel-already-exists";
|
|
28
|
+
constructor(channel, options) {
|
|
29
|
+
super(`channel already exists: ${channel}`, options);
|
|
30
|
+
this.channel = channel;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var FunnelConnectorNotFoundError = class extends FunnelError {
|
|
34
|
+
code = "connector-not-found";
|
|
35
|
+
constructor(channel, connector, options) {
|
|
36
|
+
super(`connector not found in ${channel}: ${connector}`, options);
|
|
37
|
+
this.channel = channel;
|
|
38
|
+
this.connector = connector;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var FunnelConnectorTypeMismatchError = class extends FunnelError {
|
|
42
|
+
code = "connector-type-mismatch";
|
|
43
|
+
constructor(connector, expected, actual, options) {
|
|
44
|
+
super(`connector ${connector} type mismatch: expected ${expected}, got ${actual}`, options);
|
|
45
|
+
this.connector = connector;
|
|
46
|
+
this.expected = expected;
|
|
47
|
+
this.actual = actual;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
var FunnelAuthFailedError = class extends FunnelError {
|
|
51
|
+
code = "auth-failed";
|
|
52
|
+
constructor(connector, detail, options) {
|
|
53
|
+
super(`${connector}: auth failed — ${detail}`, options);
|
|
54
|
+
this.connector = connector;
|
|
55
|
+
this.detail = detail;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var FunnelGatewayBindError = class extends FunnelError {
|
|
59
|
+
code = "gateway-bind";
|
|
60
|
+
constructor(host, port, detail, options) {
|
|
61
|
+
super(`gateway failed to bind ${host}:${port} — ${detail}`, options);
|
|
62
|
+
this.host = host;
|
|
63
|
+
this.port = port;
|
|
64
|
+
this.detail = detail;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var FunnelTokenCollisionError = class extends FunnelError {
|
|
68
|
+
code = "token-collision";
|
|
69
|
+
constructor(connector, options) {
|
|
70
|
+
super(`${connector}: both literal token and tokenEnv reference are set — pick one`, options);
|
|
71
|
+
this.connector = connector;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
75
|
+
export { FunnelConnectorTypeMismatchError as a, FunnelTokenCollisionError as c, FunnelConnectorNotFoundError as i, FunnelChannelAlreadyExistsError as n, FunnelError as o, FunnelChannelNotFoundError as r, FunnelGatewayBindError as s, FunnelAuthFailedError as t };
|