@interactive-inc/claude-funnel 0.52.0 → 0.55.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 +25 -3
- package/dist/bin.js +1276 -520
- package/dist/claude.d.ts +22 -5
- package/dist/claude.js +456 -169
- package/dist/connector-adapter-1PxjN-Uk.d.ts +25 -0
- 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-BgrVHe9I.d.ts → file-process-guard-DI1742H5.d.ts} +31 -15
- 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 +810 -211
- package/dist/{settings-store-D2XSXTyt.js → gateway-base-url-6foMXfFf.js} +19 -6
- package/dist/gateway.d.ts +3 -3
- package/dist/gateway.js +3 -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-NFs2jzCa.d.ts → index-CrngHrne.d.ts} +187 -619
- package/dist/index.d.ts +16 -11
- package/dist/index.js +512 -976
- package/dist/{local-config-json-schema-8IHjS4Q7.js → local-config-json-schema-DE1zkMcb.js} +35 -9
- package/dist/{local-config-sync-BdsrDZOu.d.ts → local-config-sync-B8b04LrZ.d.ts} +45 -25
- package/dist/local-config.d.ts +2 -2
- package/dist/local-config.js +2 -2
- package/dist/{memory-connector-diagnostic-log-CrW1ltLM.js → memory-diagnostic-log-BZ1VD80X.js} +61 -99
- package/dist/{memory-token-prompter-B5FFCsGP.d.ts → memory-token-prompter-Lo3YRDzq.d.ts} +4 -4
- package/dist/{memory-token-prompter-CLerGsgM.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-Dr-nIBwN.js → yaml-render-OhUN-qkS.js} +52 -34
- package/package.json +21 -1
- package/dist/connector-adapter-DKgsVuMH.d.ts +0 -11
- /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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-CzYgZpq2.js";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
//#region lib/services/diagnostics/diagnostic-event.ts
|
|
5
|
+
const stringOrNull = (value) => typeof value === "string" && value.length > 0 ? value : null;
|
|
6
|
+
const numberOrNull = (value) => typeof value === "number" ? value : null;
|
|
7
|
+
const stringOr = (value, fallback) => typeof value === "string" ? value : fallback;
|
|
8
|
+
const isStringKeyedObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
const parsePayloadObject = (payload) => {
|
|
10
|
+
if (payload === null) return null;
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(payload);
|
|
13
|
+
if (isStringKeyedObject(parsed)) return parsed;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
const truncate = (text, max) => text.length <= max ? text : `${text.slice(0, max)}…`;
|
|
20
|
+
const previewOf = (payload) => {
|
|
21
|
+
if (typeof payload !== "string" || payload.length === 0) return null;
|
|
22
|
+
const parsed = parsePayloadObject(payload);
|
|
23
|
+
if (parsed !== null && "text" in parsed) return truncate(String(parsed.text), 60);
|
|
24
|
+
return truncate(payload, 60);
|
|
25
|
+
};
|
|
26
|
+
const toDiagnosticEvent = (row) => {
|
|
27
|
+
const payload = stringOrNull(row.payload);
|
|
28
|
+
return {
|
|
29
|
+
seq: numberOrNull(row.seq),
|
|
30
|
+
ts: numberOrNull(row.ts),
|
|
31
|
+
type: stringOr(row.type, "?"),
|
|
32
|
+
outcome: stringOr(row.outcome, "?"),
|
|
33
|
+
eventId: stringOrNull(row.event_id),
|
|
34
|
+
payload,
|
|
35
|
+
payloadParsed: parsePayloadObject(payload),
|
|
36
|
+
preview: previewOf(row.payload)
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
const toDiagnosticConnectionError = (row) => ({
|
|
40
|
+
seq: numberOrNull(row.seq),
|
|
41
|
+
ts: numberOrNull(row.ts),
|
|
42
|
+
type: stringOr(row.type, "?"),
|
|
43
|
+
status: stringOr(row.status, "?"),
|
|
44
|
+
detail: stringOrNull(row.detail)
|
|
45
|
+
});
|
|
46
|
+
const queryRows = (reader, sql, params) => {
|
|
47
|
+
try {
|
|
48
|
+
return reader.query(sql, params);
|
|
49
|
+
} finally {
|
|
50
|
+
reader.close();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region lib/services/diagnostics/funnel-diagnostics.ts
|
|
55
|
+
const isGatewayStatusResponse = (value) => {
|
|
56
|
+
if (value === null || typeof value !== "object") return false;
|
|
57
|
+
if (!("clients" in value) || !Array.isArray(value.clients)) return false;
|
|
58
|
+
if (!("listeners" in value) || !Array.isArray(value.listeners)) return false;
|
|
59
|
+
return true;
|
|
60
|
+
};
|
|
61
|
+
const connectorOf = (channel, connectorId) => {
|
|
62
|
+
if (connectorId === null) return void 0;
|
|
63
|
+
return channel.connectors?.find((connector) => connector.id === connectorId)?.name;
|
|
64
|
+
};
|
|
65
|
+
const buildDiagnosis = (report) => {
|
|
66
|
+
const rootCause = (report.connectionErrors[report.connectionErrors.length - 1] ?? null)?.detail ?? null;
|
|
67
|
+
if (!report.gateway.running) return {
|
|
68
|
+
status: "error",
|
|
69
|
+
message: "gateway is not running",
|
|
70
|
+
nextActions: ["fnl gateway start"],
|
|
71
|
+
rootCause: null
|
|
72
|
+
};
|
|
73
|
+
const channel = report.channel;
|
|
74
|
+
if (!(report.listeners.length > 0)) return {
|
|
75
|
+
status: "warn",
|
|
76
|
+
message: "no connectors configured on this channel",
|
|
77
|
+
nextActions: [`fnl channels ${channel} connectors add <name> --type=slack ...`],
|
|
78
|
+
rootCause: null
|
|
79
|
+
};
|
|
80
|
+
const allDead = report.listeners.every((l) => !l.alive);
|
|
81
|
+
const someDead = report.listeners.some((l) => !l.alive);
|
|
82
|
+
if (allDead) return {
|
|
83
|
+
status: "error",
|
|
84
|
+
message: "all listeners are dead",
|
|
85
|
+
nextActions: ["fnl doctor --fix", "fnl doctor --fix --aggressive"],
|
|
86
|
+
rootCause
|
|
87
|
+
};
|
|
88
|
+
if (someDead) return {
|
|
89
|
+
status: "warn",
|
|
90
|
+
message: "some listeners are dead",
|
|
91
|
+
nextActions: ["fnl doctor --fix"],
|
|
92
|
+
rootCause
|
|
93
|
+
};
|
|
94
|
+
if (report.claudeClients === 0) return {
|
|
95
|
+
status: "warn",
|
|
96
|
+
message: "no Claude connected to this channel",
|
|
97
|
+
nextActions: [`fnl claude --channel ${channel}`],
|
|
98
|
+
rootCause: null
|
|
99
|
+
};
|
|
100
|
+
if (report.listeners.some((l) => l.errors > 0)) return {
|
|
101
|
+
status: "warn",
|
|
102
|
+
message: "listeners have errors",
|
|
103
|
+
nextActions: ["fnl gateway logs"],
|
|
104
|
+
rootCause
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
status: "ok",
|
|
108
|
+
message: "everything looks healthy",
|
|
109
|
+
nextActions: [],
|
|
110
|
+
rootCause: null
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Programmable diagnostics surface — used by both the CLI (fnl debug …) and
|
|
115
|
+
* the MCP tools (fnl_debug, fnl_recent_events, …). Pure read-side, no
|
|
116
|
+
* mutation; pair with FunnelRecovery for self-healing actions.
|
|
117
|
+
*/
|
|
118
|
+
var FunnelDiagnostics = class {
|
|
119
|
+
constructor(props) {
|
|
120
|
+
this.props = props;
|
|
121
|
+
Object.freeze(this);
|
|
122
|
+
}
|
|
123
|
+
async diagnose(channelName) {
|
|
124
|
+
const channels = this.props.channels.list();
|
|
125
|
+
const target = channelName ? channels.find((ch) => ch.name === channelName) ?? null : channels[0] ?? null;
|
|
126
|
+
if (!target) return null;
|
|
127
|
+
const gatewayBody = await this.fetchGatewayStatus();
|
|
128
|
+
const store = this.resolveStore();
|
|
129
|
+
return this.buildChannelDiagnosis(target, gatewayBody, store, 5);
|
|
130
|
+
}
|
|
131
|
+
async diagnoseAll() {
|
|
132
|
+
const channels = this.props.channels.list();
|
|
133
|
+
const gatewayBody = await this.fetchGatewayStatus();
|
|
134
|
+
const store = this.resolveStore();
|
|
135
|
+
const reports = await Promise.all(channels.map((ch) => this.buildChannelDiagnosis(ch, gatewayBody, store, 5)));
|
|
136
|
+
const errorChannels = reports.filter((r) => r.diagnosis.status === "error").map((r) => r.channel);
|
|
137
|
+
const warnChannels = reports.filter((r) => r.diagnosis.status === "warn").map((r) => r.channel);
|
|
138
|
+
const okChannels = reports.filter((r) => r.diagnosis.status === "ok").map((r) => r.channel);
|
|
139
|
+
const uniqueActions = [...new Set(reports.flatMap((r) => r.diagnosis.nextActions))];
|
|
140
|
+
return {
|
|
141
|
+
summary: {
|
|
142
|
+
total: reports.length,
|
|
143
|
+
ok: okChannels.length,
|
|
144
|
+
warn: warnChannels.length,
|
|
145
|
+
error: errorChannels.length,
|
|
146
|
+
criticalChannels: errorChannels,
|
|
147
|
+
warnChannels,
|
|
148
|
+
suggestedActions: uniqueActions
|
|
149
|
+
},
|
|
150
|
+
channels: reports
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
async recentEvents(channelName, limit = 20) {
|
|
154
|
+
const store = this.resolveStore();
|
|
155
|
+
if (!store) return [];
|
|
156
|
+
const channelId = this.resolveChannelId(channelName);
|
|
157
|
+
if (channelName && !channelId) return [];
|
|
158
|
+
const reader = new ConnectorDiagnosticSqlReader(store);
|
|
159
|
+
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]);
|
|
160
|
+
if (rows instanceof Error) return [];
|
|
161
|
+
return rows.reverse().map(toDiagnosticEvent);
|
|
162
|
+
}
|
|
163
|
+
async droppedEvents(channelName, limit = 20) {
|
|
164
|
+
const store = this.resolveStore();
|
|
165
|
+
if (!store) return [];
|
|
166
|
+
const channelId = this.resolveChannelId(channelName);
|
|
167
|
+
if (channelName && !channelId) return [];
|
|
168
|
+
const reader = new ConnectorDiagnosticSqlReader(store);
|
|
169
|
+
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]);
|
|
170
|
+
if (rows instanceof Error) return [];
|
|
171
|
+
return rows.reverse().map(toDiagnosticEvent);
|
|
172
|
+
}
|
|
173
|
+
async connectionErrors(channelName, limit = 20) {
|
|
174
|
+
const store = this.resolveStore();
|
|
175
|
+
if (!store) return [];
|
|
176
|
+
const channelId = this.resolveChannelId(channelName);
|
|
177
|
+
if (channelName && !channelId) return [];
|
|
178
|
+
const reader = new ConnectorDiagnosticSqlReader(store);
|
|
179
|
+
const rows = channelId ? queryRows(reader, "SELECT seq, ts, type, status, detail FROM connection WHERE channel_id = ? AND status IN ('auth-failed','error') ORDER BY seq DESC LIMIT ?", [channelId, limit]) : queryRows(reader, "SELECT seq, ts, type, status, detail FROM connection WHERE status IN ('auth-failed','error') ORDER BY seq DESC LIMIT ?", [limit]);
|
|
180
|
+
if (rows instanceof Error) return [];
|
|
181
|
+
return rows.reverse().map(toDiagnosticConnectionError);
|
|
182
|
+
}
|
|
183
|
+
async replay(channelName, seq) {
|
|
184
|
+
const channel = this.props.channels.list().find((ch) => ch.name === channelName);
|
|
185
|
+
if (!channel) return { state: "not-found" };
|
|
186
|
+
const store = this.resolveStore();
|
|
187
|
+
if (!store) return {
|
|
188
|
+
state: "error",
|
|
189
|
+
reason: "no diagnostic store yet"
|
|
190
|
+
};
|
|
191
|
+
const reader = new ConnectorDiagnosticSqlReader(store);
|
|
192
|
+
const rows = seq !== void 0 ? queryRows(reader, "SELECT seq, event_id, type, payload, connector_id, channel_id FROM processed WHERE channel_id = ? AND seq = ? LIMIT 1", [channel.id, seq]) : queryRows(reader, "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", [channel.id]);
|
|
193
|
+
if (rows instanceof Error) return {
|
|
194
|
+
state: "error",
|
|
195
|
+
reason: rows.message
|
|
196
|
+
};
|
|
197
|
+
const firstRow = rows[0];
|
|
198
|
+
if (!firstRow) return { state: "not-found" };
|
|
199
|
+
const replaySeq = typeof firstRow.seq === "number" ? firstRow.seq : null;
|
|
200
|
+
const eventId = typeof firstRow.event_id === "string" ? firstRow.event_id : null;
|
|
201
|
+
const connectorId = typeof firstRow.connector_id === "string" ? firstRow.connector_id : null;
|
|
202
|
+
let content = typeof firstRow.payload === "string" ? firstRow.payload : null;
|
|
203
|
+
if ((!content || content.length === 0) && eventId) {
|
|
204
|
+
const rawRows = queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT payload FROM raw WHERE event_id = ? LIMIT 1", [eventId]);
|
|
205
|
+
const rawRow = rawRows instanceof Error ? null : rawRows[0];
|
|
206
|
+
if (rawRow) content = typeof rawRow.payload === "string" ? rawRow.payload : null;
|
|
207
|
+
}
|
|
208
|
+
if (!content) return {
|
|
209
|
+
state: "error",
|
|
210
|
+
reason: "event has no payload to replay"
|
|
211
|
+
};
|
|
212
|
+
const connectorName = connectorOf(channel, connectorId);
|
|
213
|
+
const result = await this.props.publisher.publish(channel.name, {
|
|
214
|
+
content,
|
|
215
|
+
connector: connectorName
|
|
216
|
+
});
|
|
217
|
+
if (result.state === "offline") return { state: "offline" };
|
|
218
|
+
if (result.state === "error") return {
|
|
219
|
+
state: "error",
|
|
220
|
+
reason: result.reason
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
state: "ok",
|
|
224
|
+
seq: replaySeq,
|
|
225
|
+
offset: result.offset,
|
|
226
|
+
preview: content.slice(0, 60)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
resolveStore() {
|
|
230
|
+
const tmpDir = this.props.tmpDir;
|
|
231
|
+
const rawPath = join(tmpDir, "connector-raw.db");
|
|
232
|
+
const processedPath = join(tmpDir, "connector-processed.db");
|
|
233
|
+
const connectionPath = join(tmpDir, "connector-connection.db");
|
|
234
|
+
if (!existsSync(rawPath) || !existsSync(processedPath) || !existsSync(connectionPath)) return null;
|
|
235
|
+
return {
|
|
236
|
+
rawPath,
|
|
237
|
+
processedPath,
|
|
238
|
+
connectionPath
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
resolveChannelId(channelName) {
|
|
242
|
+
if (!channelName) return null;
|
|
243
|
+
return this.props.channels.list().find((ch) => ch.name === channelName)?.id ?? null;
|
|
244
|
+
}
|
|
245
|
+
async fetchGatewayStatus() {
|
|
246
|
+
const gatewayStatus = this.props.gateway.getStatus();
|
|
247
|
+
if (!gatewayStatus.running) return null;
|
|
248
|
+
const token = this.props.gatewayToken.read();
|
|
249
|
+
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
|
250
|
+
const res = await fetch(`http://127.0.0.1:${gatewayStatus.port}/status`, { headers }).catch(() => null);
|
|
251
|
+
if (!res || !res.ok) return null;
|
|
252
|
+
const body = await res.json();
|
|
253
|
+
return isGatewayStatusResponse(body) ? body : null;
|
|
254
|
+
}
|
|
255
|
+
async buildChannelDiagnosis(target, gatewayBody, store, eventLimit) {
|
|
256
|
+
const gatewayStatus = this.props.gateway.getStatus();
|
|
257
|
+
const targetName = target.name;
|
|
258
|
+
const baseReport = {
|
|
259
|
+
channel: targetName,
|
|
260
|
+
channelId: target.id,
|
|
261
|
+
gateway: {
|
|
262
|
+
running: gatewayStatus.running,
|
|
263
|
+
pid: gatewayStatus.pid,
|
|
264
|
+
port: gatewayStatus.running ? gatewayStatus.port : null,
|
|
265
|
+
uptimeMs: gatewayBody?.uptimeMs ?? null
|
|
266
|
+
},
|
|
267
|
+
listeners: [],
|
|
268
|
+
claudeClients: 0,
|
|
269
|
+
recentEvents: [],
|
|
270
|
+
connectionErrors: []
|
|
271
|
+
};
|
|
272
|
+
if (gatewayBody) {
|
|
273
|
+
baseReport.listeners = gatewayBody.listeners.filter((l) => l.channelName === targetName).map((l) => ({
|
|
274
|
+
name: l.name,
|
|
275
|
+
type: l.type,
|
|
276
|
+
alive: l.alive,
|
|
277
|
+
events: l.events,
|
|
278
|
+
errors: l.errors,
|
|
279
|
+
lastEventAt: l.lastEventAt
|
|
280
|
+
}));
|
|
281
|
+
baseReport.claudeClients = gatewayBody.clients.filter((cl) => cl.channelName === targetName).length;
|
|
282
|
+
}
|
|
283
|
+
if (store) {
|
|
284
|
+
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]);
|
|
285
|
+
if (!(evRows instanceof Error)) baseReport.recentEvents = evRows.reverse().map(toDiagnosticEvent);
|
|
286
|
+
const hasDeadListeners = baseReport.listeners.some((l) => !l.alive);
|
|
287
|
+
const hasListenerErrors = baseReport.listeners.some((l) => l.errors > 0);
|
|
288
|
+
if (hasDeadListeners || hasListenerErrors) {
|
|
289
|
+
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]);
|
|
290
|
+
if (!(errRows instanceof Error)) baseReport.connectionErrors = errRows.reverse().map(toDiagnosticConnectionError);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
...baseReport,
|
|
295
|
+
diagnosis: buildDiagnosis(baseReport)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
//#endregion
|
|
300
|
+
export { toDiagnosticEvent as a, toDiagnosticConnectionError as i, previewOf as n, queryRows as r, FunnelDiagnostics as t };
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { t as ChannelConfig } from "./settings-schema-zhnMIa8I.js";
|
|
2
|
+
|
|
3
|
+
//#region lib/gateway/diagnostic-log/diagnostic-sql-reader.d.ts
|
|
4
|
+
type Props$1 = {
|
|
5
|
+
/** SQLite file holding the raw (pre-filter) table. */rawPath: string; /** SQLite file holding the processed (verdict) table. */
|
|
6
|
+
processedPath: string; /** SQLite file holding the connection (lifecycle) table. */
|
|
7
|
+
connectionPath: string;
|
|
8
|
+
};
|
|
9
|
+
type Row = Record<string, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Read-only SQL surface over the three diagnostic tables, for Claude to query
|
|
12
|
+
* the log with arbitrary `SELECT`s. It opens all files read-only and exposes
|
|
13
|
+
* three views — `raw`, `processed`, `connection` — that hide the storage
|
|
14
|
+
* details (the physical table is `leuco_log` and each row's columns live
|
|
15
|
+
* inside a JSON `event` blob): the views surface the columns as plain fields,
|
|
16
|
+
* with `payload` already pulled out of the nested JSON.
|
|
17
|
+
*
|
|
18
|
+
* The tables are separate files. `raw` and `processed` share an `event_id`,
|
|
19
|
+
* so a `JOIN` answers "the event arrived, but what verdict did it get?";
|
|
20
|
+
* `connection` answers the other half — "did the listener ever connect at
|
|
21
|
+
* all?". Writes are impossible: the connection is read-only and `query`
|
|
22
|
+
* rejects anything but a single `SELECT`.
|
|
23
|
+
*/
|
|
24
|
+
declare class ConnectorDiagnosticSqlReader {
|
|
25
|
+
private readonly db;
|
|
26
|
+
constructor(props: Props$1);
|
|
27
|
+
/**
|
|
28
|
+
* Run one read-only `SELECT` and return the rows. Returns an `Error` (rather
|
|
29
|
+
* than throwing) for a non-SELECT statement or a SQL error, so the caller
|
|
30
|
+
* can surface the message without a stack trace.
|
|
31
|
+
*/
|
|
32
|
+
query(sql: string, params?: (string | number | null)[]): Row[] | Error;
|
|
33
|
+
close(): void;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region lib/services/diagnostics/diagnostic-event.d.ts
|
|
37
|
+
type DiagnosticEvent = {
|
|
38
|
+
seq: number | null;
|
|
39
|
+
ts: number | null;
|
|
40
|
+
type: string;
|
|
41
|
+
outcome: string;
|
|
42
|
+
eventId: string | null;
|
|
43
|
+
payload: string | null;
|
|
44
|
+
payloadParsed: Record<string, unknown> | null;
|
|
45
|
+
preview: string | null;
|
|
46
|
+
};
|
|
47
|
+
type DiagnosticConnectionError = {
|
|
48
|
+
seq: number | null;
|
|
49
|
+
ts: number | null;
|
|
50
|
+
type: string;
|
|
51
|
+
status: string;
|
|
52
|
+
detail: string | null;
|
|
53
|
+
};
|
|
54
|
+
declare const previewOf: (payload: unknown) => string | null;
|
|
55
|
+
declare const toDiagnosticEvent: (row: Record<string, unknown>) => DiagnosticEvent;
|
|
56
|
+
declare const toDiagnosticConnectionError: (row: Record<string, unknown>) => DiagnosticConnectionError;
|
|
57
|
+
declare const queryRows: (reader: ConnectorDiagnosticSqlReader, sql: string, params: (string | number | null)[]) => Record<string, unknown>[] | Error;
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region lib/services/diagnostics/funnel-diagnostics.d.ts
|
|
60
|
+
/** Narrow channel registry — only `list()` is needed. */
|
|
61
|
+
type DiagnosticsChannelSource = {
|
|
62
|
+
list(): ChannelConfig[];
|
|
63
|
+
};
|
|
64
|
+
/** Narrow gateway probe — only the daemon status is needed. */
|
|
65
|
+
type DiagnosticsGatewayProbe = {
|
|
66
|
+
getStatus(): {
|
|
67
|
+
running: boolean;
|
|
68
|
+
pid: number | null;
|
|
69
|
+
port: number;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
/** Narrow token reader — diagnostics only needs to read the token, never to mint or rotate it. */
|
|
73
|
+
type DiagnosticsTokenReader = {
|
|
74
|
+
read(): string | null;
|
|
75
|
+
};
|
|
76
|
+
/** Narrow publisher used only for replay. */
|
|
77
|
+
type DiagnosticsPublisher = {
|
|
78
|
+
publish(channelName: string, request: {
|
|
79
|
+
content: string;
|
|
80
|
+
connector?: string;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
state: "ok";
|
|
83
|
+
offset: number;
|
|
84
|
+
} | {
|
|
85
|
+
state: "offline";
|
|
86
|
+
} | {
|
|
87
|
+
state: "error";
|
|
88
|
+
reason: string;
|
|
89
|
+
}>;
|
|
90
|
+
};
|
|
91
|
+
type Props = {
|
|
92
|
+
gateway: DiagnosticsGatewayProbe;
|
|
93
|
+
gatewayToken: DiagnosticsTokenReader;
|
|
94
|
+
channels: DiagnosticsChannelSource;
|
|
95
|
+
publisher: DiagnosticsPublisher;
|
|
96
|
+
tmpDir: string;
|
|
97
|
+
};
|
|
98
|
+
type DiagnosisStatus = "ok" | "warn" | "error";
|
|
99
|
+
type ChannelDiagnosis = {
|
|
100
|
+
channel: string;
|
|
101
|
+
channelId: string;
|
|
102
|
+
gateway: {
|
|
103
|
+
running: boolean;
|
|
104
|
+
pid: number | null;
|
|
105
|
+
port: number | null;
|
|
106
|
+
uptimeMs: number | null;
|
|
107
|
+
};
|
|
108
|
+
listeners: Array<{
|
|
109
|
+
name: string;
|
|
110
|
+
type: string;
|
|
111
|
+
alive: boolean;
|
|
112
|
+
events: number;
|
|
113
|
+
errors: number;
|
|
114
|
+
lastEventAt: string | null;
|
|
115
|
+
}>;
|
|
116
|
+
claudeClients: number;
|
|
117
|
+
recentEvents: DiagnosticEvent[];
|
|
118
|
+
connectionErrors: DiagnosticConnectionError[];
|
|
119
|
+
diagnosis: {
|
|
120
|
+
status: DiagnosisStatus;
|
|
121
|
+
message: string;
|
|
122
|
+
nextActions: string[];
|
|
123
|
+
rootCause: string | null;
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
type DiagnoseAllReport = {
|
|
127
|
+
summary: {
|
|
128
|
+
total: number;
|
|
129
|
+
ok: number;
|
|
130
|
+
warn: number;
|
|
131
|
+
error: number;
|
|
132
|
+
criticalChannels: string[];
|
|
133
|
+
warnChannels: string[];
|
|
134
|
+
suggestedActions: string[];
|
|
135
|
+
};
|
|
136
|
+
channels: ChannelDiagnosis[];
|
|
137
|
+
};
|
|
138
|
+
type ReplayResult = {
|
|
139
|
+
state: "ok";
|
|
140
|
+
seq: number | null;
|
|
141
|
+
offset: number;
|
|
142
|
+
preview: string | null;
|
|
143
|
+
} | {
|
|
144
|
+
state: "offline";
|
|
145
|
+
} | {
|
|
146
|
+
state: "error";
|
|
147
|
+
reason: string;
|
|
148
|
+
} | {
|
|
149
|
+
state: "not-found";
|
|
150
|
+
};
|
|
151
|
+
type StorePaths = {
|
|
152
|
+
rawPath: string;
|
|
153
|
+
processedPath: string;
|
|
154
|
+
connectionPath: string;
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* Programmable diagnostics surface — used by both the CLI (fnl debug …) and
|
|
158
|
+
* the MCP tools (fnl_debug, fnl_recent_events, …). Pure read-side, no
|
|
159
|
+
* mutation; pair with FunnelRecovery for self-healing actions.
|
|
160
|
+
*/
|
|
161
|
+
declare class FunnelDiagnostics {
|
|
162
|
+
private readonly props;
|
|
163
|
+
constructor(props: Props);
|
|
164
|
+
diagnose(channelName?: string): Promise<ChannelDiagnosis | null>;
|
|
165
|
+
diagnoseAll(): Promise<DiagnoseAllReport>;
|
|
166
|
+
recentEvents(channelName: string | null, limit?: number): Promise<DiagnosticEvent[]>;
|
|
167
|
+
droppedEvents(channelName: string | null, limit?: number): Promise<DiagnosticEvent[]>;
|
|
168
|
+
connectionErrors(channelName: string | null, limit?: number): Promise<DiagnosticConnectionError[]>;
|
|
169
|
+
replay(channelName: string, seq?: number): Promise<ReplayResult>;
|
|
170
|
+
resolveStore(): StorePaths | null;
|
|
171
|
+
private resolveChannelId;
|
|
172
|
+
private fetchGatewayStatus;
|
|
173
|
+
private buildChannelDiagnosis;
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
176
|
+
export { DiagnosticsGatewayProbe as a, FunnelDiagnostics as c, DiagnosticEvent as d, previewOf as f, ConnectorDiagnosticSqlReader as g, toDiagnosticEvent as h, DiagnosticsChannelSource as i, ReplayResult as l, toDiagnosticConnectionError as m, DiagnoseAllReport as n, DiagnosticsPublisher as o, queryRows as p, DiagnosisStatus as r, DiagnosticsTokenReader as s, ChannelDiagnosis as t, DiagnosticConnectionError as u };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region lib/services/docs/funnel-docs.d.ts
|
|
2
|
+
type DocsTopicListing = {
|
|
3
|
+
name: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Programmable docs surface — used by both the CLI (fnl docs <topic>) and the
|
|
8
|
+
* MCP / SDK consumers. Docs are embedded into the build so a Claude session
|
|
9
|
+
* can self-discover funnel's vocabulary without external network access.
|
|
10
|
+
*/
|
|
11
|
+
declare class FunnelDocs {
|
|
12
|
+
constructor();
|
|
13
|
+
list(): DocsTopicListing[];
|
|
14
|
+
get(topic: string): string | null;
|
|
15
|
+
topics(): string[];
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { FunnelDocs as n, DocsTopicListing as t };
|