@interactive-inc/claude-funnel 0.67.0 → 0.68.1

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.
Files changed (54) hide show
  1. package/README.md +24 -3
  2. package/dist/bin.js +276 -270
  3. package/dist/{channels-BBA3GTAa.d.ts → channels-B0cifCfw.d.ts} +2 -1
  4. package/dist/claude.d.ts +2 -2
  5. package/dist/claude.js +5 -5
  6. package/dist/connector-descriptor-D4XtEsH8.d.ts +215 -0
  7. package/dist/connectors/discord.d.ts +3 -2
  8. package/dist/connectors/discord.js +1 -1
  9. package/dist/connectors/gh.d.ts +3 -2
  10. package/dist/connectors/gh.js +1 -1
  11. package/dist/connectors/schedule.d.ts +13 -11
  12. package/dist/connectors/schedule.js +3 -3
  13. package/dist/connectors/slack.d.ts +3 -2
  14. package/dist/connectors/slack.js +1 -1
  15. package/dist/diagnostic-event-CxMM5Bl2.js +78 -0
  16. package/dist/diagnostic-log-BsAg5qZK.d.ts +186 -0
  17. package/dist/diagnostics.d.ts +2 -2
  18. package/dist/diagnostics.js +3 -2
  19. package/dist/{discord-connector-CdjwPoty.js → discord-connector-CAcWifJn.js} +1 -1
  20. package/dist/docs.js +1 -1
  21. package/dist/doctor.d.ts +1 -1
  22. package/dist/event-log.d.ts +2 -0
  23. package/dist/event-log.js +3 -0
  24. package/dist/{file-process-guard-DAYe9Ndt.d.ts → file-process-guard-D-QatrwL.d.ts} +1 -1
  25. package/dist/{flume-source-listener-CLtsdcT2.d.ts → flume-source-listener-Ba8_1Mg-.d.ts} +2 -1
  26. package/dist/{funnel-diagnostics-B-PCuv_E.js → funnel-diagnostics-7rV3HxY3.js} +100 -143
  27. package/dist/{funnel-diagnostics-CnKus3Jg.d.ts → funnel-diagnostics-CDrZznTB.d.ts} +10 -13
  28. package/dist/{funnel-docs-BFcqITFA.js → funnel-docs-CI4hMkhq.js} +8 -3
  29. package/dist/{funnel-doctor-CviUTINw.d.ts → funnel-doctor-CY4S5cnz.d.ts} +1 -1
  30. package/dist/gateway/daemon.js +63 -83
  31. package/dist/gateway.d.ts +2 -2
  32. package/dist/gateway.js +1 -1
  33. package/dist/{gh-connector-D-iyjl-d.js → gh-connector-DMwI6zJA.js} +1 -1
  34. package/dist/{index-B4RvEaW-.d.ts → index-fHrKn4jm.d.ts} +7 -2
  35. package/dist/index.d.ts +18 -14
  36. package/dist/index.js +124 -30
  37. package/dist/local-config.d.ts +1 -1
  38. package/dist/logger.d.ts +6 -270
  39. package/dist/logger.js +6 -141
  40. package/dist/memory-event-log-BY8XjFzi.js +147 -0
  41. package/dist/{memory-token-prompter-D3R_kgM_.d.ts → memory-token-prompter-B20tl_aU.d.ts} +1 -1
  42. package/dist/{schedule-connector-teeDMHUe.js → schedule-connector-BXUOYgwZ.js} +59 -95
  43. package/dist/schedule-connector-schema-CNV8PgWX.js +137 -0
  44. package/dist/schedule-connector-schema-Tq5kJul-.d.ts +127 -0
  45. package/dist/{slack-connector-BE0KAjCs.js → slack-connector-BTtUiXAU.js} +2 -2
  46. package/dist/{slot-fields-5ERhytIA.js → slot-fields-eCWizJqS.js} +1 -1
  47. package/dist/{sqlite-diagnostic-log-BDxfXGZy.js → sqlite-diagnostic-log-Qs4cf9I_.js} +53 -84
  48. package/dist/sqlite-event-log-BYgCFMxC.d.ts +284 -0
  49. package/dist/{funnel-log-sqlite-sink-CXbs3qQp.js → sqlite-event-log-eZuXKszJ.js} +52 -11
  50. package/package.json +16 -1
  51. package/dist/connector-descriptor-F75ZI8Fh.d.ts +0 -391
  52. package/dist/diagnostic-sql-reader-CoBk_EFF.js +0 -83
  53. package/dist/schedule-connector-schema-C2AChpwX.js +0 -31
  54. package/dist/schedule-connector-schema-DBko_Bf7.d.ts +0 -48
@@ -3,7 +3,7 @@ import { t as errorMessageOf } from "./error-message-of-BG4ezZ_s.js";
3
3
  import { t as discordConnectorSchema } from "./discord-connector-schema-saAGuPmJ.js";
4
4
  import { t as FunnelConnectorAdapter } from "./connector-adapter-BL2ehMej.js";
5
5
  import { t as resolveConnectorToken } from "./resolve-connector-token-BwiJCu82.js";
6
- import { i as resolveFlumeDeps, n as FunnelFlumeSourceListener, r as flumeLogHandler, t as slotFields } from "./slot-fields-5ERhytIA.js";
6
+ import { i as resolveFlumeDeps, n as FunnelFlumeSourceListener, r as flumeLogHandler, t as slotFields } from "./slot-fields-eCWizJqS.js";
7
7
  import { FlumeDiscordGatewayIntents, FlumeDiscordSource } from "@interactive-inc/flume/discord";
8
8
  //#region lib/engine/connectors/discord-adapter.ts
9
9
  const DISCORD_API_BASE = "https://discord.com/api/v10";
package/dist/docs.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as FunnelDocs } from "./funnel-docs-BFcqITFA.js";
1
+ import { t as FunnelDocs } from "./funnel-docs-CI4hMkhq.js";
2
2
  export { FunnelDocs };
package/dist/doctor.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { n as DoctorReport, r as FunnelDoctor, t as DoctorFixMode } from "./funnel-doctor-CviUTINw.js";
1
+ import { n as DoctorReport, r as FunnelDoctor, t as DoctorFixMode } from "./funnel-doctor-CY4S5cnz.js";
2
2
  export { DoctorFixMode, DoctorReport, FunnelDoctor };
@@ -0,0 +1,2 @@
1
+ import { a as MemoryEventLogProps, c as EventLogValidator, d as EventLogEntry, i as MemoryEventLog, l as EventLogRelay, n as SqliteEventLogProps, o as EventLog, r as SqliteEventLogQuery, s as EventLogProps, t as SqliteEventLog, u as EventLogStore } from "./sqlite-event-log-BYgCFMxC.js";
2
+ export { EventLog, EventLogEntry, EventLogProps, EventLogRelay, EventLogStore, EventLogValidator, MemoryEventLog, MemoryEventLogProps, SqliteEventLog, SqliteEventLogProps, SqliteEventLogQuery };
@@ -0,0 +1,3 @@
1
+ import { t as SqliteEventLog } from "./sqlite-event-log-eZuXKszJ.js";
2
+ import { n as EventLog, t as MemoryEventLog } from "./memory-event-log-BY8XjFzi.js";
3
+ export { EventLog, MemoryEventLog, SqliteEventLog };
@@ -2,7 +2,7 @@ import { t as FunnelLogger } from "./logger-mMZvblTx.js";
2
2
  import { n as FunnelFileSystem } from "./file-system-BNtl997T.js";
3
3
  import { t as ChannelConfig } from "./settings-schema-B-CKLuST.js";
4
4
  import { n as FunnelIdGenerator } from "./settings-reader-CyTpbAre.js";
5
- import { z as FunnelProcessRunner } from "./connector-descriptor-F75ZI8Fh.js";
5
+ import { b as FunnelProcessRunner } from "./connector-descriptor-D4XtEsH8.js";
6
6
  //#region lib/engine/claude/channel-resolver.d.ts
7
7
  type ChannelResolver = {
8
8
  get(name: string): ChannelConfig | null;
@@ -1,5 +1,6 @@
1
1
  import { t as FunnelLogger } from "./logger-mMZvblTx.js";
2
- import { A as FunnelConnectorListener, g as ConnectorDiagnosticLog, h as ConnectorConnectionStatus } from "./connector-descriptor-F75ZI8Fh.js";
2
+ import { d as FunnelConnectorListener } from "./connector-descriptor-D4XtEsH8.js";
3
+ import { a as ConnectorConnectionStatus, o as ConnectorDiagnosticLog } from "./diagnostic-log-BsAg5qZK.js";
3
4
  import { FlumeEventHandler, FlumeLogHandler, FlumeReconnectOptions, FlumeRunning, FlumeRuntimeDeps, FlumeSource, FlumeStatus } from "@interactive-inc/flume";
4
5
  //#region lib/engine/connectors/connector-diagnostics-recorder.d.ts
5
6
  type Props$1 = {
@@ -1,58 +1,8 @@
1
1
  import { n as gatewayLoopbackUrl, t as loopbackFetch } from "./loopback-fetch-HP-bo9Kv.js";
2
2
  import { t as errorMessageOf } from "./error-message-of-BG4ezZ_s.js";
3
- import { t as ConnectorDiagnosticSqlReader } from "./diagnostic-sql-reader-CoBk_EFF.js";
3
+ import { n as diagnosticEventOfProcessed, r as diagnosticEventOfRaw, t as diagnosticConnectionEventOf } from "./diagnostic-event-CxMM5Bl2.js";
4
4
  import { join } from "node:path";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
- //#region lib/services/diagnostics/diagnostic-event.ts
7
- const stringOrNull = (value) => typeof value === "string" && value.length > 0 ? value : null;
8
- const numberOrNull = (value) => typeof value === "number" ? value : null;
9
- const stringOr = (value, fallback) => typeof value === "string" ? value : fallback;
10
- const isStringKeyedObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
11
- const parsePayloadObject = (payload) => {
12
- if (payload === null) return null;
13
- try {
14
- const parsed = JSON.parse(payload);
15
- if (isStringKeyedObject(parsed)) return parsed;
16
- } catch {
17
- return null;
18
- }
19
- return null;
20
- };
21
- const truncate = (text, max) => text.length <= max ? text : `${text.slice(0, max)}…`;
22
- const previewOf = (payload) => {
23
- if (typeof payload !== "string" || payload.length === 0) return null;
24
- const parsed = parsePayloadObject(payload);
25
- if (parsed !== null && "text" in parsed) return truncate(String(parsed.text), 60);
26
- return truncate(payload, 60);
27
- };
28
- const toDiagnosticEvent = (row) => {
29
- const payload = stringOrNull(row.payload);
30
- return {
31
- seq: numberOrNull(row.seq),
32
- ts: numberOrNull(row.ts),
33
- type: stringOr(row.type, "?"),
34
- outcome: stringOr(row.outcome, "?"),
35
- eventId: stringOrNull(row.event_id),
36
- payload,
37
- payloadParsed: parsePayloadObject(payload),
38
- preview: previewOf(row.payload)
39
- };
40
- };
41
- const toDiagnosticConnectionError = (row) => ({
42
- seq: numberOrNull(row.seq),
43
- ts: numberOrNull(row.ts),
44
- type: stringOr(row.type, "?"),
45
- status: stringOr(row.status, "?"),
46
- detail: stringOrNull(row.detail)
47
- });
48
- const queryRows = (reader, sql, params) => {
49
- try {
50
- return reader.query(sql, params);
51
- } finally {
52
- reader.close();
53
- }
54
- };
55
- //#endregion
56
6
  //#region lib/services/diagnostics/funnel-diagnostics.ts
57
7
  const isGatewayStatusResponse = (value) => {
58
8
  if (value === null || typeof value !== "object") return false;
@@ -65,8 +15,20 @@ const connectorOf = (channel, connectorId) => {
65
15
  return channel.connectors?.find((connector) => connector.id === connectorId)?.name;
66
16
  };
67
17
  const FLAPPING_ERROR_THRESHOLD = 3;
68
- const buildDiagnosis = (report) => {
69
- const rootCause = (report.connectionErrors[report.connectionErrors.length - 1] ?? null)?.detail ?? null;
18
+ const CONNECTION_DIAGNOSIS_WINDOW = 100;
19
+ const connectionKeyOf = (event) => `${event.type}\u0000${event.connectorId ?? ""}`;
20
+ const unresolvedConnectionFailures = (timeline) => {
21
+ const lastConnectedIndex = /* @__PURE__ */ new Map();
22
+ timeline.forEach((event, index) => {
23
+ if (event.status === "connected") lastConnectedIndex.set(connectionKeyOf(event), index);
24
+ });
25
+ return timeline.filter((event, index) => {
26
+ if (event.status !== "auth-failed" && event.status !== "error") return false;
27
+ return (lastConnectedIndex.get(connectionKeyOf(event)) ?? -1) < index;
28
+ });
29
+ };
30
+ const buildDiagnosis = (report, unresolvedErrors = report.connectionErrors) => {
31
+ const rootCause = (unresolvedErrors[unresolvedErrors.length - 1] ?? null)?.detail ?? null;
70
32
  const channel = report.channel;
71
33
  if (!report.gateway.running) return {
72
34
  status: "error",
@@ -92,7 +54,7 @@ const buildDiagnosis = (report) => {
92
54
  nextActions: [`fnl channels ${channel} connectors add <name> --type=slack ...`],
93
55
  rootCause: null
94
56
  };
95
- const authFailed = report.connectionErrors.filter((e) => e.status === "auth-failed");
57
+ const authFailed = unresolvedErrors.filter((e) => e.status === "auth-failed");
96
58
  if (authFailed.length > 0) {
97
59
  const detail = authFailed[authFailed.length - 1]?.detail ?? null;
98
60
  return {
@@ -174,14 +136,12 @@ var FunnelDiagnostics = class {
174
136
  const target = channelName ? channels.find((ch) => ch.name === channelName) ?? null : channels[0] ?? null;
175
137
  if (!target) return null;
176
138
  const gatewayProbe = await this.fetchGatewayStatus();
177
- const store = this.resolveStore();
178
- return this.buildChannelDiagnosis(target, gatewayProbe, store, 5);
139
+ return this.buildChannelDiagnosis(target, gatewayProbe, 5);
179
140
  }
180
141
  async diagnoseAll() {
181
142
  const channels = this.props.channels.list();
182
143
  const gatewayProbe = await this.fetchGatewayStatus();
183
- const store = this.resolveStore();
184
- const reports = await Promise.all(channels.map((ch) => this.buildChannelDiagnosis(ch, gatewayProbe, store, 5)));
144
+ const reports = await Promise.all(channels.map((ch) => this.buildChannelDiagnosis(ch, gatewayProbe, 5)));
185
145
  const errorChannels = reports.filter((r) => r.diagnosis.status === "error").map((r) => r.channel);
186
146
  const warnChannels = reports.filter((r) => r.diagnosis.status === "warn").map((r) => r.channel);
187
147
  const okChannels = reports.filter((r) => r.diagnosis.status === "ok").map((r) => r.channel);
@@ -201,20 +161,22 @@ var FunnelDiagnostics = class {
201
161
  }
202
162
  async recentEvents(channelName, options = {}) {
203
163
  const limit = options.limit ?? 20;
204
- const ids = this.resolveScope(channelName, options.connector);
205
- if (ids === null) return [];
206
- 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]);
207
- if (rows instanceof Error) return [];
208
- return rows.reverse().map(toDiagnosticEvent);
164
+ const scope = this.resolveScope(channelName, options.connector);
165
+ if (scope === null) return [];
166
+ return this.queryLog((log) => log.queryProcessed({
167
+ ...scope,
168
+ limit
169
+ })).map(diagnosticEventOfProcessed);
209
170
  }
210
171
  async droppedEvents(channelName, options = {}) {
211
172
  const limit = options.limit ?? 20;
212
- const ids = this.resolveScope(channelName, options.connector);
213
- if (ids === null) return [];
214
- const where = ids.whereClause ? `${ids.whereClause} AND outcome LIKE 'skip:%'` : "WHERE outcome LIKE 'skip:%'";
215
- 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]);
216
- if (rows instanceof Error) return [];
217
- return rows.reverse().map(toDiagnosticEvent);
173
+ const scope = this.resolveScope(channelName, options.connector);
174
+ if (scope === null) return [];
175
+ return this.queryLog((log) => log.queryProcessed({
176
+ ...scope,
177
+ outcomePrefix: "skip:",
178
+ limit
179
+ })).map(diagnosticEventOfProcessed);
218
180
  }
219
181
  /**
220
182
  * Raw inbound rows the connector recorded before any processing. The most
@@ -224,20 +186,22 @@ var FunnelDiagnostics = class {
224
186
  */
225
187
  async rawEvents(channelName, options = {}) {
226
188
  const limit = options.limit ?? 20;
227
- const ids = this.resolveScope(channelName, options.connector);
228
- if (ids === null) return [];
229
- 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]);
230
- if (rows instanceof Error) return [];
231
- return rows.reverse().map(toDiagnosticEvent);
189
+ const scope = this.resolveScope(channelName, options.connector);
190
+ if (scope === null) return [];
191
+ return this.queryLog((log) => log.queryRaw({
192
+ ...scope,
193
+ limit
194
+ })).map(diagnosticEventOfRaw);
232
195
  }
233
196
  async connectionErrors(channelName, options = {}) {
234
197
  const limit = options.limit ?? 20;
235
- const ids = this.resolveScope(channelName, options.connector);
236
- if (ids === null) return [];
237
- const where = ids.whereClause ? `${ids.whereClause} AND status IN ('auth-failed','error')` : "WHERE status IN ('auth-failed','error')";
238
- const rows = queryRows(new ConnectorDiagnosticSqlReader(ids.store), `SELECT seq, ts, type, status, detail FROM connection ${where} ORDER BY seq DESC LIMIT ?`, [...ids.params, limit]);
239
- if (rows instanceof Error) return [];
240
- return rows.reverse().map(toDiagnosticConnectionError);
198
+ const scope = this.resolveScope(channelName, options.connector);
199
+ if (scope === null) return [];
200
+ return this.queryLog((log) => log.queryConnection({
201
+ ...scope,
202
+ statuses: ["auth-failed", "error"],
203
+ limit
204
+ })).map(diagnosticConnectionEventOf);
241
205
  }
242
206
  /**
243
207
  * Full connection lifecycle for one channel/connector — started, connected,
@@ -248,11 +212,12 @@ var FunnelDiagnostics = class {
248
212
  */
249
213
  async connectionTimeline(channelName, options = {}) {
250
214
  const limit = options.limit ?? 20;
251
- const ids = this.resolveScope(channelName, options.connector);
252
- if (ids === null) return [];
253
- 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]);
254
- if (rows instanceof Error) return [];
255
- return rows.reverse().map(toDiagnosticConnectionError);
215
+ const scope = this.resolveScope(channelName, options.connector);
216
+ if (scope === null) return [];
217
+ return this.queryLog((log) => log.queryConnection({
218
+ ...scope,
219
+ limit
220
+ })).map(diagnosticConnectionEventOf);
256
221
  }
257
222
  /**
258
223
  * Tail of `~/.funnel/.../funnel.log`. Use when a flume internal log (e.g.
@@ -293,28 +258,30 @@ var FunnelDiagnostics = class {
293
258
  async replay(channelName, seq) {
294
259
  const channel = this.props.channels.list().find((ch) => ch.name === channelName);
295
260
  if (!channel) return { state: "not-found" };
296
- const store = this.resolveStore();
297
- if (!store) return {
261
+ if (!this.props.diagnosticLog) return {
298
262
  state: "error",
299
- reason: "no diagnostic store yet"
263
+ reason: "no diagnostic log configured"
300
264
  };
301
- const reader = new ConnectorDiagnosticSqlReader(store);
302
- 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]);
265
+ const rows = this.queryLogResult((log) => log.queryProcessed({
266
+ channelId: channel.id,
267
+ ...seq !== void 0 ? { seq } : { outcomePrefix: "emitted" },
268
+ limit: 1
269
+ }));
303
270
  if (rows instanceof Error) return {
304
271
  state: "error",
305
272
  reason: rows.message
306
273
  };
307
274
  const firstRow = rows[0];
308
275
  if (!firstRow) return { state: "not-found" };
309
- const replaySeq = typeof firstRow.seq === "number" ? firstRow.seq : null;
310
- const eventId = typeof firstRow.event_id === "string" ? firstRow.event_id : null;
311
- const connectorId = typeof firstRow.connector_id === "string" ? firstRow.connector_id : null;
312
- let content = typeof firstRow.payload === "string" ? firstRow.payload : null;
313
- if ((!content || content.length === 0) && eventId) {
314
- const rawRows = queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT payload FROM raw WHERE event_id = ? LIMIT 1", [eventId]);
315
- const rawRow = rawRows instanceof Error ? null : rawRows[0];
316
- if (rawRow) content = typeof rawRow.payload === "string" ? rawRow.payload : null;
317
- }
276
+ const replaySeq = firstRow.seq;
277
+ const eventId = firstRow.eventId;
278
+ const connectorId = firstRow.connectorId;
279
+ const processedContent = firstRow.payload.length > 0 ? firstRow.payload : null;
280
+ const rawContent = processedContent === null ? this.queryLog((log) => log.queryRaw({
281
+ eventId,
282
+ limit: 1
283
+ }))[0]?.payload ?? null : null;
284
+ const content = processedContent ?? rawContent;
318
285
  if (!content) return {
319
286
  state: "error",
320
287
  reason: "event has no payload to replay"
@@ -336,52 +303,35 @@ var FunnelDiagnostics = class {
336
303
  preview: content.slice(0, 60)
337
304
  };
338
305
  }
339
- resolveStore() {
340
- const tmpDir = this.props.tmpDir;
341
- const rawPath = join(tmpDir, "connector-raw.db");
342
- const processedPath = join(tmpDir, "connector-processed.db");
343
- const connectionPath = join(tmpDir, "connector-connection.db");
344
- if (!existsSync(rawPath) || !existsSync(processedPath) || !existsSync(connectionPath)) return null;
345
- return {
346
- rawPath,
347
- processedPath,
348
- connectionPath
349
- };
350
- }
351
- resolveChannelId(channelName) {
352
- if (!channelName) return null;
353
- return this.props.channels.list().find((ch) => ch.name === channelName)?.id ?? null;
354
- }
355
306
  /**
356
- * Resolves a (channel, connector) filter into the SQL where-clause + the
357
- * positional params, or returns `null` when the requested scope cannot be
358
- * resolved (channel not found, connector not found in that channel, no
359
- * store on disk yet). Centralises the channel/connector → id mapping so
360
- * each read method does not redo the lookup.
307
+ * Resolves human-facing channel/connector names into the stable ids stored
308
+ * by the diagnostic log. Connector names are only unique inside a channel.
361
309
  */
362
310
  resolveScope(channelName, connectorName) {
363
- const store = this.resolveStore();
364
- if (!store) return null;
365
- if (!channelName) return {
366
- store,
367
- whereClause: "",
368
- params: []
369
- };
311
+ if (!channelName) return {};
370
312
  const channel = this.props.channels.list().find((ch) => ch.name === channelName) ?? null;
371
313
  if (!channel) return null;
372
- if (!connectorName) return {
373
- store,
374
- whereClause: "WHERE channel_id = ?",
375
- params: [channel.id]
376
- };
314
+ if (!connectorName) return { channelId: channel.id };
377
315
  const connectorId = channel.connectors?.find((c) => c.name === connectorName)?.id ?? null;
378
316
  if (!connectorId) return null;
379
317
  return {
380
- store,
381
- whereClause: "WHERE channel_id = ? AND connector_id = ?",
382
- params: [channel.id, connectorId]
318
+ channelId: channel.id,
319
+ connectorId
383
320
  };
384
321
  }
322
+ queryLog(query) {
323
+ const result = this.queryLogResult(query);
324
+ return result instanceof Error ? [] : result;
325
+ }
326
+ queryLogResult(query) {
327
+ const log = this.props.diagnosticLog;
328
+ if (!log) return /* @__PURE__ */ new Error("no diagnostic log configured");
329
+ try {
330
+ return query(log);
331
+ } catch (error) {
332
+ return new Error(errorMessageOf(error));
333
+ }
334
+ }
385
335
  async fetchGatewayStatus() {
386
336
  const gatewayStatus = this.props.gateway.getStatus();
387
337
  if (!gatewayStatus.running) return {
@@ -421,7 +371,7 @@ var FunnelDiagnostics = class {
421
371
  error: null
422
372
  };
423
373
  }
424
- async buildChannelDiagnosis(target, gatewayProbe, store, eventLimit) {
374
+ async buildChannelDiagnosis(target, gatewayProbe, eventLimit) {
425
375
  const gatewayStatus = this.props.gateway.getStatus();
426
376
  const targetName = target.name;
427
377
  const gatewayBody = gatewayProbe.body;
@@ -452,17 +402,24 @@ var FunnelDiagnostics = class {
452
402
  }));
453
403
  baseReport.claudeClients = gatewayBody.clients.filter((cl) => cl.channelName === targetName).length;
454
404
  }
455
- if (store) {
456
- 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]);
457
- if (!(evRows instanceof Error)) baseReport.recentEvents = evRows.reverse().map(toDiagnosticEvent);
458
- 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]);
459
- if (!(errRows instanceof Error)) baseReport.connectionErrors = errRows.reverse().map(toDiagnosticConnectionError);
405
+ let unresolvedErrors = [];
406
+ if (this.props.diagnosticLog) {
407
+ baseReport.recentEvents = this.queryLog((log) => log.queryProcessed({
408
+ channelId: target.id,
409
+ limit: eventLimit
410
+ })).map(diagnosticEventOfProcessed);
411
+ const connectionRows = this.queryLog((log) => log.queryConnection({
412
+ channelId: target.id,
413
+ limit: CONNECTION_DIAGNOSIS_WINDOW
414
+ }));
415
+ baseReport.connectionErrors = connectionRows.filter((row) => row.status === "auth-failed" || row.status === "error").slice(-3).map(diagnosticConnectionEventOf);
416
+ unresolvedErrors = unresolvedConnectionFailures(connectionRows).map(diagnosticConnectionEventOf);
460
417
  }
461
418
  return {
462
419
  ...baseReport,
463
- diagnosis: buildDiagnosis(baseReport)
420
+ diagnosis: buildDiagnosis(baseReport, unresolvedErrors)
464
421
  };
465
422
  }
466
423
  };
467
424
  //#endregion
468
- export { toDiagnosticEvent as a, toDiagnosticConnectionError as i, previewOf as n, queryRows as r, FunnelDiagnostics as t };
425
+ export { FunnelDiagnostics as t };
@@ -1,4 +1,5 @@
1
1
  import { t as ChannelConfig } from "./settings-schema-B-CKLuST.js";
2
+ import { g as StoredRawEvent, h as StoredProcessedEvent, m as StoredConnectionEvent, o as ConnectorDiagnosticLog } from "./diagnostic-log-BsAg5qZK.js";
2
3
  //#region lib/engine/diagnostic-log/diagnostic-sql-reader.d.ts
3
4
  type Props$1 = {
4
5
  /** SQLite file holding the raw (pre-filter) table. */
@@ -56,6 +57,9 @@ type DiagnosticConnectionError = {
56
57
  declare const previewOf: (payload: unknown) => string | null;
57
58
  declare const toDiagnosticEvent: (row: Record<string, unknown>) => DiagnosticEvent;
58
59
  declare const toDiagnosticConnectionError: (row: Record<string, unknown>) => DiagnosticConnectionError;
60
+ declare const diagnosticEventOfProcessed: (event: StoredProcessedEvent) => DiagnosticEvent;
61
+ declare const diagnosticEventOfRaw: (event: StoredRawEvent) => DiagnosticEvent;
62
+ declare const diagnosticConnectionEventOf: (event: StoredConnectionEvent) => DiagnosticConnectionError;
59
63
  declare const queryRows: (reader: ConnectorDiagnosticSqlReader, sql: string, params: (string | number | null)[]) => Record<string, unknown>[] | Error;
60
64
  //#endregion
61
65
  //#region lib/services/diagnostics/funnel-diagnostics.d.ts
@@ -95,6 +99,7 @@ type Props = {
95
99
  gatewayToken: DiagnosticsTokenReader;
96
100
  channels: DiagnosticsChannelSource;
97
101
  publisher: DiagnosticsPublisher;
102
+ diagnosticLog?: ConnectorDiagnosticLog;
98
103
  tmpDir: string;
99
104
  };
100
105
  type DiagnosisStatus = "ok" | "warn" | "error";
@@ -159,11 +164,6 @@ type ReplayResult = {
159
164
  } | {
160
165
  state: "not-found";
161
166
  };
162
- type StorePaths = {
163
- rawPath: string;
164
- processedPath: string;
165
- connectionPath: string;
166
- };
167
167
  /**
168
168
  * Programmable diagnostics surface — used by both the CLI (fnl debug …) and
169
169
  * the MCP tools (fnl_debug, fnl_recent_events, …). Pure read-side, no
@@ -224,18 +224,15 @@ declare class FunnelDiagnostics {
224
224
  truncated: boolean;
225
225
  }>;
226
226
  replay(channelName: string, seq?: number): Promise<ReplayResult>;
227
- resolveStore(): StorePaths | null;
228
- private resolveChannelId;
229
227
  /**
230
- * Resolves a (channel, connector) filter into the SQL where-clause + the
231
- * positional params, or returns `null` when the requested scope cannot be
232
- * resolved (channel not found, connector not found in that channel, no
233
- * store on disk yet). Centralises the channel/connector → id mapping so
234
- * each read method does not redo the lookup.
228
+ * Resolves human-facing channel/connector names into the stable ids stored
229
+ * by the diagnostic log. Connector names are only unique inside a channel.
235
230
  */
236
231
  private resolveScope;
232
+ private queryLog;
233
+ private queryLogResult;
237
234
  private fetchGatewayStatus;
238
235
  private buildChannelDiagnosis;
239
236
  }
240
237
  //#endregion
241
- 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 };
238
+ export { toDiagnosticConnectionError as _, DiagnosticsGatewayProbe as a, FunnelDiagnostics as c, DiagnosticEvent as d, diagnosticConnectionEventOf as f, queryRows as g, previewOf as h, DiagnosticsChannelSource as i, ReplayResult as l, diagnosticEventOfRaw as m, DiagnoseAllReport as n, DiagnosticsPublisher as o, diagnosticEventOfProcessed as p, DiagnosisStatus as r, DiagnosticsTokenReader as s, ChannelDiagnosis as t, DiagnosticConnectionError as u, toDiagnosticEvent as v, ConnectorDiagnosticSqlReader as y };
@@ -124,7 +124,7 @@ types:
124
124
  slack Socket Mode listener + REST adapter
125
125
  gh GitHub polling listener + REST adapter
126
126
  discord Discord listener + REST adapter
127
- schedule cron-style tick listener (no adapter — schedule does not send out)
127
+ schedule cron + one-shot tick listener (no adapter — schedule does not send out)
128
128
 
129
129
  per-type parts:
130
130
 
@@ -145,8 +145,8 @@ operations (all nested under a channel):
145
145
  fnl channels <ch> connectors <name> request … proxy an HTTP call to the
146
146
  adapter (e.g. send a Slack
147
147
  message via Claude path)
148
- fnl channels <ch> connectors <name> schedules … manage cron entries on a
149
- schedule connector
148
+ fnl channels <ch> connectors <name> schedules … manage cron / one-shot
149
+ schedule entries
150
150
 
151
151
  tokens:
152
152
 
@@ -708,6 +708,11 @@ related: fnl docs architecture, fnl docs debugging, fnl docs mcp`,
708
708
  fnl channels daily connectors cron schedules add morning \\
709
709
  --cron="0 9 * * *" --prompt="summarize yesterday's PRs"
710
710
 
711
+ — schedule a one-shot prompt —
712
+
713
+ fnl channels daily connectors cron schedules add release \\
714
+ --run-at="2026-08-01T09:00:00+09:00" --prompt="run the release check"
715
+
711
716
  — diagnose "events stopped arriving" —
712
717
 
713
718
  fnl debug --all --json
@@ -1,4 +1,4 @@
1
- import { c as FunnelDiagnostics, n as DiagnoseAllReport, t as ChannelDiagnosis } from "./funnel-diagnostics-CnKus3Jg.js";
1
+ import { c as FunnelDiagnostics, n as DiagnoseAllReport, t as ChannelDiagnosis } from "./funnel-diagnostics-CDrZznTB.js";
2
2
  import { n as RecoveryAction, t as FunnelRecovery } from "./funnel-recovery-BwcSNNSN.js";
3
3
  //#region lib/services/doctor/funnel-doctor.d.ts
4
4
  type Props = {