@interactive-inc/claude-funnel 0.66.0 → 0.68.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.
Files changed (97) hide show
  1. package/README.md +31 -6
  2. package/dist/bin.js +292 -383
  3. package/dist/channel.d.ts +76 -52
  4. package/dist/channel.js +82 -47
  5. package/dist/{channels-DR90RmUP.d.ts → channels-B0cifCfw.d.ts} +6 -6
  6. package/dist/claude.d.ts +11 -8
  7. package/dist/claude.js +31 -15
  8. package/dist/connector-descriptor-D4XtEsH8.d.ts +215 -0
  9. package/dist/{connector-diagnostics-recorder-COtNEmUp.js → connector-diagnostics-recorder-BmOijU1p.js} +1 -0
  10. package/dist/connectors/discord.d.ts +11 -8
  11. package/dist/connectors/discord.js +2 -2
  12. package/dist/connectors/gh.d.ts +7 -6
  13. package/dist/connectors/gh.js +2 -2
  14. package/dist/connectors/schedule.d.ts +21 -17
  15. package/dist/connectors/schedule.js +3 -3
  16. package/dist/connectors/slack.d.ts +15 -10
  17. package/dist/connectors/slack.js +3 -3
  18. package/dist/diagnostic-event-CxMM5Bl2.js +78 -0
  19. package/dist/diagnostic-log-BsAg5qZK.d.ts +186 -0
  20. package/dist/diagnostics.d.ts +2 -2
  21. package/dist/diagnostics.js +3 -2
  22. package/dist/{discord-connector-DXARcO6w.js → discord-connector-CAcWifJn.js} +6 -6
  23. package/dist/{discord-connector-schema-D1dyxhXk.d.ts → discord-connector-schema-BaBZYHxK.d.ts} +0 -1
  24. package/dist/docs.d.ts +1 -1
  25. package/dist/docs.js +1 -1
  26. package/dist/doctor.d.ts +1 -1
  27. package/dist/doctor.js +1 -1
  28. package/dist/event-log.d.ts +2 -0
  29. package/dist/event-log.js +3 -0
  30. package/dist/{file-process-guard-CiBwXjzm.d.ts → file-process-guard-D-QatrwL.d.ts} +22 -18
  31. package/dist/{file-system-DHgbuTFb.d.ts → file-system-BNtl997T.d.ts} +2 -1
  32. package/dist/{flume-source-listener-FzR5lGpL.d.ts → flume-source-listener-Ba8_1Mg-.d.ts} +9 -6
  33. package/dist/{funnel-diagnostics-Cvk6Sk4x.js → funnel-diagnostics-CAG7hJP0.js} +85 -141
  34. package/dist/{funnel-diagnostics-kbd27d7I.d.ts → funnel-diagnostics-CDrZznTB.d.ts} +18 -18
  35. package/dist/{funnel-docs-C-ge0MuB.js → funnel-docs-CI4hMkhq.js} +12 -3
  36. package/dist/{funnel-doctor-CVTNUtDn.d.ts → funnel-doctor-CY4S5cnz.d.ts} +20 -9
  37. package/dist/{funnel-doctor-XrI2GBH8.js → funnel-doctor-Csw5qjcH.js} +1 -0
  38. package/dist/{funnel-error-0t1MK1R6.js → funnel-error-DoSBCogD.js} +13 -0
  39. package/dist/{funnel-recovery-D8MkiTpk.d.ts → funnel-recovery-BwcSNNSN.d.ts} +1 -2
  40. package/dist/{funnel-recovery-DKnEutUS.js → funnel-recovery-DYbSYGWC.js} +1 -0
  41. package/dist/gateway/daemon.js +60 -139
  42. package/dist/gateway.d.ts +2 -3
  43. package/dist/gateway.js +2 -2
  44. package/dist/{gh-connector-BUGCOEWS.js → gh-connector-DMwI6zJA.js} +5 -5
  45. package/dist/{gh-connector-schema-BCHEqpUg.d.ts → gh-connector-schema-BrNsCl1T.d.ts} +0 -1
  46. package/dist/{index-BU1TtuFy.d.ts → index-fHrKn4jm.d.ts} +210 -40
  47. package/dist/index.d.ts +125 -115
  48. package/dist/index.js +242 -57
  49. package/dist/{local-config-json-schema-DexV8vX3.js → local-config-json-schema-_-YVHX4c.js} +4 -4
  50. package/dist/local-config.d.ts +2 -3
  51. package/dist/local-config.js +1 -1
  52. package/dist/logger.d.ts +17 -265
  53. package/dist/logger.js +6 -141
  54. package/dist/memory-event-log-BY8XjFzi.js +147 -0
  55. package/dist/{memory-token-prompter-D0kIdhar.d.ts → memory-token-prompter-B20tl_aU.d.ts} +2 -3
  56. package/dist/{node-file-system-BOXIHW_Q.js → node-file-system-xJx7dymE.js} +11 -3
  57. package/dist/{profiles-M24JrcOh.d.ts → profiles-CePRnv3F.d.ts} +3 -4
  58. package/dist/{profiles-ZHLONml4.js → profiles-KpN7_gBw.js} +19 -5
  59. package/dist/profiles.d.ts +1 -1
  60. package/dist/profiles.js +1 -1
  61. package/dist/recovery.d.ts +1 -1
  62. package/dist/recovery.js +1 -1
  63. package/dist/{schedule-connector-9k3gOIgl.js → schedule-connector-BXUOYgwZ.js} +63 -99
  64. package/dist/schedule-connector-schema-CNV8PgWX.js +137 -0
  65. package/dist/schedule-connector-schema-Tq5kJul-.d.ts +127 -0
  66. package/dist/{settings-reader-BnKIbmsD.d.ts → settings-reader-CyTpbAre.d.ts} +1 -2
  67. package/dist/{settings-schema-IShSMm5G.d.ts → settings-schema-B-CKLuST.d.ts} +0 -1
  68. package/dist/{settings-store-ccv291GV.js → settings-store-ZFWBhOnp.js} +12 -3
  69. package/dist/{slack-connector-CxpWagbT.js → slack-connector-CC3V2VXQ.js} +10 -9
  70. package/dist/{slack-event-processor-xFDG3US0.js → slack-event-processor-Cs_YuI3w.js} +2 -2
  71. package/dist/{slack-event-processor-BDTr62cN.d.ts → slack-event-processor-DmAlq4cV.d.ts} +0 -1
  72. package/dist/{slot-fields-D-pvMgTK.js → slot-fields-eCWizJqS.js} +4 -4
  73. package/dist/{sqlite-diagnostic-log-CwhXE9Qh.js → sqlite-diagnostic-log-Qs4cf9I_.js} +92 -100
  74. package/dist/sqlite-event-log-BYgCFMxC.d.ts +284 -0
  75. package/dist/{funnel-log-sqlite-sink-DLYkY0pZ.js → sqlite-event-log-eZuXKszJ.js} +53 -12
  76. package/dist/{yaml-render-DVEtYuF1.js → yaml-render-DxxRQT-V.js} +3 -3
  77. package/package.json +25 -10
  78. package/dist/broadcaster-DVNcw00K.d.ts +0 -138
  79. package/dist/connector-descriptor-CmyHlEiT.d.ts +0 -380
  80. package/dist/diagnostic-sql-reader-oXZnWFf_.js +0 -83
  81. package/dist/file-system-Wvzc2ePY.js +0 -9
  82. package/dist/logger-B6iyNbxM.js +0 -9
  83. package/dist/schedule-connector-schema-D7ZEKvit.d.ts +0 -49
  84. package/dist/schedule-connector-schema-DKEPZnVv.js +0 -31
  85. package/dist/settings-reader-9FcX3qS1.js +0 -11
  86. /package/dist/{connector-adapter-Dvs8N7ew.js → connector-adapter-BL2ehMej.js} +0 -0
  87. /package/dist/{connector-listener-mPGZYa8e.js → connector-listener-BcQMzY6Q.js} +0 -0
  88. /package/dist/{discord-connector-schema-B4YpWpR3.js → discord-connector-schema-saAGuPmJ.js} +0 -0
  89. /package/dist/{error-message-of-ColuYmAk.js → error-message-of-BG4ezZ_s.js} +0 -0
  90. /package/dist/{funnel-docs-Cd66sKyV.d.ts → funnel-docs-BJWawfWI.d.ts} +0 -0
  91. /package/dist/{gh-connector-schema-CAqIhzGr.js → gh-connector-schema-DmZTrzUu.js} +0 -0
  92. /package/dist/{logger-Bc_Ooypb.d.ts → logger-mMZvblTx.d.ts} +0 -0
  93. /package/dist/{loopback-fetch-CVNuN3YZ.js → loopback-fetch-HP-bo9Kv.js} +0 -0
  94. /package/dist/{node-http-client-u00atiKx.js → node-http-client-SX3xFh4h.js} +0 -0
  95. /package/dist/{node-process-runner-DxTvycoK.js → node-process-runner-D4rzPl8Q.js} +0 -0
  96. /package/dist/{resolve-connector-token-DxDG9mhf.js → resolve-connector-token-BwiJCu82.js} +0 -0
  97. /package/dist/{slack-connector-schema-Dem8to4P.js → slack-connector-schema-EGVuuqSX.js} +0 -0
@@ -1,58 +1,8 @@
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";
1
+ import { n as gatewayLoopbackUrl, t as loopbackFetch } from "./loopback-fetch-HP-bo9Kv.js";
2
+ import { t as errorMessageOf } from "./error-message-of-BG4ezZ_s.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;
@@ -164,6 +114,7 @@ const diagnoseSlackEventSubscriptionGap = (report) => {
164
114
  * mutation; pair with FunnelRecovery for self-healing actions.
165
115
  */
166
116
  var FunnelDiagnostics = class {
117
+ props;
167
118
  constructor(props) {
168
119
  this.props = props;
169
120
  Object.freeze(this);
@@ -173,14 +124,12 @@ var FunnelDiagnostics = class {
173
124
  const target = channelName ? channels.find((ch) => ch.name === channelName) ?? null : channels[0] ?? null;
174
125
  if (!target) return null;
175
126
  const gatewayProbe = await this.fetchGatewayStatus();
176
- const store = this.resolveStore();
177
- return this.buildChannelDiagnosis(target, gatewayProbe, store, 5);
127
+ return this.buildChannelDiagnosis(target, gatewayProbe, 5);
178
128
  }
179
129
  async diagnoseAll() {
180
130
  const channels = this.props.channels.list();
181
131
  const gatewayProbe = await this.fetchGatewayStatus();
182
- const store = this.resolveStore();
183
- const reports = await Promise.all(channels.map((ch) => this.buildChannelDiagnosis(ch, gatewayProbe, store, 5)));
132
+ const reports = await Promise.all(channels.map((ch) => this.buildChannelDiagnosis(ch, gatewayProbe, 5)));
184
133
  const errorChannels = reports.filter((r) => r.diagnosis.status === "error").map((r) => r.channel);
185
134
  const warnChannels = reports.filter((r) => r.diagnosis.status === "warn").map((r) => r.channel);
186
135
  const okChannels = reports.filter((r) => r.diagnosis.status === "ok").map((r) => r.channel);
@@ -200,20 +149,22 @@ var FunnelDiagnostics = class {
200
149
  }
201
150
  async recentEvents(channelName, options = {}) {
202
151
  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]);
206
- if (rows instanceof Error) return [];
207
- return rows.reverse().map(toDiagnosticEvent);
152
+ const scope = this.resolveScope(channelName, options.connector);
153
+ if (scope === null) return [];
154
+ return this.queryLog((log) => log.queryProcessed({
155
+ ...scope,
156
+ limit
157
+ })).map(diagnosticEventOfProcessed);
208
158
  }
209
159
  async droppedEvents(channelName, options = {}) {
210
160
  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]);
215
- if (rows instanceof Error) return [];
216
- return rows.reverse().map(toDiagnosticEvent);
161
+ const scope = this.resolveScope(channelName, options.connector);
162
+ if (scope === null) return [];
163
+ return this.queryLog((log) => log.queryProcessed({
164
+ ...scope,
165
+ outcomePrefix: "skip:",
166
+ limit
167
+ })).map(diagnosticEventOfProcessed);
217
168
  }
218
169
  /**
219
170
  * Raw inbound rows the connector recorded before any processing. The most
@@ -223,20 +174,22 @@ var FunnelDiagnostics = class {
223
174
  */
224
175
  async rawEvents(channelName, options = {}) {
225
176
  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);
177
+ const scope = this.resolveScope(channelName, options.connector);
178
+ if (scope === null) return [];
179
+ return this.queryLog((log) => log.queryRaw({
180
+ ...scope,
181
+ limit
182
+ })).map(diagnosticEventOfRaw);
231
183
  }
232
184
  async connectionErrors(channelName, options = {}) {
233
185
  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]);
238
- if (rows instanceof Error) return [];
239
- return rows.reverse().map(toDiagnosticConnectionError);
186
+ const scope = this.resolveScope(channelName, options.connector);
187
+ if (scope === null) return [];
188
+ return this.queryLog((log) => log.queryConnection({
189
+ ...scope,
190
+ statuses: ["auth-failed", "error"],
191
+ limit
192
+ })).map(diagnosticConnectionEventOf);
240
193
  }
241
194
  /**
242
195
  * Full connection lifecycle for one channel/connector — started, connected,
@@ -247,11 +200,12 @@ var FunnelDiagnostics = class {
247
200
  */
248
201
  async connectionTimeline(channelName, options = {}) {
249
202
  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);
203
+ const scope = this.resolveScope(channelName, options.connector);
204
+ if (scope === null) return [];
205
+ return this.queryLog((log) => log.queryConnection({
206
+ ...scope,
207
+ limit
208
+ })).map(diagnosticConnectionEventOf);
255
209
  }
256
210
  /**
257
211
  * Tail of `~/.funnel/.../funnel.log`. Use when a flume internal log (e.g.
@@ -292,28 +246,30 @@ var FunnelDiagnostics = class {
292
246
  async replay(channelName, seq) {
293
247
  const channel = this.props.channels.list().find((ch) => ch.name === channelName);
294
248
  if (!channel) return { state: "not-found" };
295
- const store = this.resolveStore();
296
- if (!store) return {
249
+ if (!this.props.diagnosticLog) return {
297
250
  state: "error",
298
- reason: "no diagnostic store yet"
251
+ reason: "no diagnostic log configured"
299
252
  };
300
- const reader = new ConnectorDiagnosticSqlReader(store);
301
- 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]);
253
+ const rows = this.queryLogResult((log) => log.queryProcessed({
254
+ channelId: channel.id,
255
+ ...seq !== void 0 ? { seq } : { outcomePrefix: "emitted" },
256
+ limit: 1
257
+ }));
302
258
  if (rows instanceof Error) return {
303
259
  state: "error",
304
260
  reason: rows.message
305
261
  };
306
262
  const firstRow = rows[0];
307
263
  if (!firstRow) return { state: "not-found" };
308
- const replaySeq = typeof firstRow.seq === "number" ? firstRow.seq : null;
309
- const eventId = typeof firstRow.event_id === "string" ? firstRow.event_id : null;
310
- const connectorId = typeof firstRow.connector_id === "string" ? firstRow.connector_id : null;
311
- let content = typeof firstRow.payload === "string" ? firstRow.payload : null;
312
- if ((!content || content.length === 0) && eventId) {
313
- const rawRows = queryRows(new ConnectorDiagnosticSqlReader(store), "SELECT payload FROM raw WHERE event_id = ? LIMIT 1", [eventId]);
314
- const rawRow = rawRows instanceof Error ? null : rawRows[0];
315
- if (rawRow) content = typeof rawRow.payload === "string" ? rawRow.payload : null;
316
- }
264
+ const replaySeq = firstRow.seq;
265
+ const eventId = firstRow.eventId;
266
+ const connectorId = firstRow.connectorId;
267
+ const processedContent = firstRow.payload.length > 0 ? firstRow.payload : null;
268
+ const rawContent = processedContent === null ? this.queryLog((log) => log.queryRaw({
269
+ eventId,
270
+ limit: 1
271
+ }))[0]?.payload ?? null : null;
272
+ const content = processedContent ?? rawContent;
317
273
  if (!content) return {
318
274
  state: "error",
319
275
  reason: "event has no payload to replay"
@@ -335,52 +291,35 @@ var FunnelDiagnostics = class {
335
291
  preview: content.slice(0, 60)
336
292
  };
337
293
  }
338
- resolveStore() {
339
- const tmpDir = this.props.tmpDir;
340
- const rawPath = join(tmpDir, "connector-raw.db");
341
- const processedPath = join(tmpDir, "connector-processed.db");
342
- const connectionPath = join(tmpDir, "connector-connection.db");
343
- if (!existsSync(rawPath) || !existsSync(processedPath) || !existsSync(connectionPath)) return null;
344
- return {
345
- rawPath,
346
- processedPath,
347
- connectionPath
348
- };
349
- }
350
- resolveChannelId(channelName) {
351
- if (!channelName) return null;
352
- return this.props.channels.list().find((ch) => ch.name === channelName)?.id ?? null;
353
- }
354
294
  /**
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.
295
+ * Resolves human-facing channel/connector names into the stable ids stored
296
+ * by the diagnostic log. Connector names are only unique inside a channel.
360
297
  */
361
298
  resolveScope(channelName, connectorName) {
362
- const store = this.resolveStore();
363
- if (!store) return null;
364
- if (!channelName) return {
365
- store,
366
- whereClause: "",
367
- params: []
368
- };
299
+ if (!channelName) return {};
369
300
  const channel = this.props.channels.list().find((ch) => ch.name === channelName) ?? null;
370
301
  if (!channel) return null;
371
- if (!connectorName) return {
372
- store,
373
- whereClause: "WHERE channel_id = ?",
374
- params: [channel.id]
375
- };
302
+ if (!connectorName) return { channelId: channel.id };
376
303
  const connectorId = channel.connectors?.find((c) => c.name === connectorName)?.id ?? null;
377
304
  if (!connectorId) return null;
378
305
  return {
379
- store,
380
- whereClause: "WHERE channel_id = ? AND connector_id = ?",
381
- params: [channel.id, connectorId]
306
+ channelId: channel.id,
307
+ connectorId
382
308
  };
383
309
  }
310
+ queryLog(query) {
311
+ const result = this.queryLogResult(query);
312
+ return result instanceof Error ? [] : result;
313
+ }
314
+ queryLogResult(query) {
315
+ const log = this.props.diagnosticLog;
316
+ if (!log) return /* @__PURE__ */ new Error("no diagnostic log configured");
317
+ try {
318
+ return query(log);
319
+ } catch (error) {
320
+ return new Error(errorMessageOf(error));
321
+ }
322
+ }
384
323
  async fetchGatewayStatus() {
385
324
  const gatewayStatus = this.props.gateway.getStatus();
386
325
  if (!gatewayStatus.running) return {
@@ -420,7 +359,7 @@ var FunnelDiagnostics = class {
420
359
  error: null
421
360
  };
422
361
  }
423
- async buildChannelDiagnosis(target, gatewayProbe, store, eventLimit) {
362
+ async buildChannelDiagnosis(target, gatewayProbe, eventLimit) {
424
363
  const gatewayStatus = this.props.gateway.getStatus();
425
364
  const targetName = target.name;
426
365
  const gatewayBody = gatewayProbe.body;
@@ -451,11 +390,16 @@ var FunnelDiagnostics = class {
451
390
  }));
452
391
  baseReport.claudeClients = gatewayBody.clients.filter((cl) => cl.channelName === targetName).length;
453
392
  }
454
- if (store) {
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]);
456
- if (!(evRows instanceof Error)) baseReport.recentEvents = evRows.reverse().map(toDiagnosticEvent);
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);
393
+ if (this.props.diagnosticLog) {
394
+ baseReport.recentEvents = this.queryLog((log) => log.queryProcessed({
395
+ channelId: target.id,
396
+ limit: eventLimit
397
+ })).map(diagnosticEventOfProcessed);
398
+ baseReport.connectionErrors = this.queryLog((log) => log.queryConnection({
399
+ channelId: target.id,
400
+ statuses: ["auth-failed", "error"],
401
+ limit: 3
402
+ })).map(diagnosticConnectionEventOf);
459
403
  }
460
404
  return {
461
405
  ...baseReport,
@@ -464,4 +408,4 @@ var FunnelDiagnostics = class {
464
408
  }
465
409
  };
466
410
  //#endregion
467
- export { toDiagnosticEvent as a, toDiagnosticConnectionError as i, previewOf as n, queryRows as r, FunnelDiagnostics as t };
411
+ export { FunnelDiagnostics as t };
@@ -1,9 +1,12 @@
1
- import { t as ChannelConfig } from "./settings-schema-IShSMm5G.js";
2
-
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";
3
3
  //#region lib/engine/diagnostic-log/diagnostic-sql-reader.d.ts
4
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. */
5
+ /** SQLite file holding the raw (pre-filter) table. */
6
+ rawPath: string;
7
+ /** SQLite file holding the processed (verdict) table. */
8
+ processedPath: string;
9
+ /** SQLite file holding the connection (lifecycle) table. */
7
10
  connectionPath: string;
8
11
  };
9
12
  type Row = Record<string, unknown>;
@@ -54,6 +57,9 @@ type DiagnosticConnectionError = {
54
57
  declare const previewOf: (payload: unknown) => string | null;
55
58
  declare const toDiagnosticEvent: (row: Record<string, unknown>) => DiagnosticEvent;
56
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;
57
63
  declare const queryRows: (reader: ConnectorDiagnosticSqlReader, sql: string, params: (string | number | null)[]) => Record<string, unknown>[] | Error;
58
64
  //#endregion
59
65
  //#region lib/services/diagnostics/funnel-diagnostics.d.ts
@@ -93,6 +99,7 @@ type Props = {
93
99
  gatewayToken: DiagnosticsTokenReader;
94
100
  channels: DiagnosticsChannelSource;
95
101
  publisher: DiagnosticsPublisher;
102
+ diagnosticLog?: ConnectorDiagnosticLog;
96
103
  tmpDir: string;
97
104
  };
98
105
  type DiagnosisStatus = "ok" | "warn" | "error";
@@ -111,7 +118,8 @@ type ChannelDiagnosis = {
111
118
  * but the probe failed (auth refused, fetch error, non-OK response).
112
119
  */
113
120
  statusError: string | null;
114
- }; /** Connectors declared in settings for this channel. */
121
+ };
122
+ /** Connectors declared in settings for this channel. */
115
123
  configuredConnectors: number;
116
124
  listeners: Array<{
117
125
  name: string;
@@ -156,11 +164,6 @@ type ReplayResult = {
156
164
  } | {
157
165
  state: "not-found";
158
166
  };
159
- type StorePaths = {
160
- rawPath: string;
161
- processedPath: string;
162
- connectionPath: string;
163
- };
164
167
  /**
165
168
  * Programmable diagnostics surface — used by both the CLI (fnl debug …) and
166
169
  * the MCP tools (fnl_debug, fnl_recent_events, …). Pure read-side, no
@@ -221,18 +224,15 @@ declare class FunnelDiagnostics {
221
224
  truncated: boolean;
222
225
  }>;
223
226
  replay(channelName: string, seq?: number): Promise<ReplayResult>;
224
- resolveStore(): StorePaths | null;
225
- private resolveChannelId;
226
227
  /**
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.
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.
232
230
  */
233
231
  private resolveScope;
232
+ private queryLog;
233
+ private queryLogResult;
234
234
  private fetchGatewayStatus;
235
235
  private buildChannelDiagnosis;
236
236
  }
237
237
  //#endregion
238
- 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
 
@@ -378,6 +378,10 @@ what funnel never writes to funnel.json:
378
378
 
379
379
  ~/.funnel/projects/<id>/settings.json per-repo, set via CLI or TTY prompt
380
380
 
381
+ When a profile declares resume: true, its generated Claude session id lives
382
+ in ~/.funnel/projects/<id>/claude/local-sessions.json. The id is runtime
383
+ state and is never written to the committed funnel.json.
384
+
381
385
  how it is read:
382
386
 
383
387
  All CLI commands check for funnel.json in cwd. If found, FUNNEL_DIR is
@@ -704,6 +708,11 @@ related: fnl docs architecture, fnl docs debugging, fnl docs mcp`,
704
708
  fnl channels daily connectors cron schedules add morning \\
705
709
  --cron="0 9 * * *" --prompt="summarize yesterday's PRs"
706
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
+
707
716
  — diagnose "events stopped arriving" —
708
717
 
709
718
  fnl debug --all --json
@@ -1,18 +1,29 @@
1
- import { c as FunnelDiagnostics, n as DiagnoseAllReport, t as ChannelDiagnosis } from "./funnel-diagnostics-kbd27d7I.js";
2
- import { n as RecoveryAction, t as FunnelRecovery } from "./funnel-recovery-D8MkiTpk.js";
3
-
1
+ import { c as FunnelDiagnostics, n as DiagnoseAllReport, t as ChannelDiagnosis } from "./funnel-diagnostics-CDrZznTB.js";
2
+ import { n as RecoveryAction, t as FunnelRecovery } from "./funnel-recovery-BwcSNNSN.js";
4
3
  //#region lib/services/doctor/funnel-doctor.d.ts
5
4
  type Props = {
6
5
  diagnostics: FunnelDiagnostics;
7
6
  recovery: FunnelRecovery;
8
7
  };
9
- type DoctorFixMode = /** No mutations. Run diagnoseAll and report what would be fixed. */"off" /** Apply only idempotent, low-risk fixes — start the gateway, restart dead listeners. */ | "safe" /** Add high-impact fixes — full gateway restart when partial fixes are insufficient. */ | "aggressive";
8
+ type DoctorFixMode =
9
+ /** No mutations. Run diagnoseAll and report what would be fixed. */
10
+ "off" |
11
+ /** Apply only idempotent, low-risk fixes — start the gateway, restart dead listeners. */
12
+ "safe" |
13
+ /** Add high-impact fixes — full gateway restart when partial fixes are insufficient. */
14
+ "aggressive";
10
15
  type DoctorReport = {
11
- /** "ok" if every channel is healthy after the run; "warn" if anything is still off; "error" if a fix step itself failed. */status: "ok" | "warn" | "error"; /** Human-readable summary suitable for stdout. */
12
- message: string; /** Aggregated diagnosis after any fix pass. */
13
- after: DiagnoseAllReport; /** Diagnosis before fixing, only included when fix mode is not "off". */
14
- before: DiagnoseAllReport | null; /** Each recovery action that ran during the fix pass. */
15
- appliedActions: RecoveryAction[]; /** Channels still unhealthy after the fix pass (or all unhealthy channels when fix is off). */
16
+ /** "ok" if every channel is healthy after the run; "warn" if anything is still off; "error" if a fix step itself failed. */
17
+ status: "ok" | "warn" | "error";
18
+ /** Human-readable summary suitable for stdout. */
19
+ message: string;
20
+ /** Aggregated diagnosis after any fix pass. */
21
+ after: DiagnoseAllReport;
22
+ /** Diagnosis before fixing, only included when fix mode is not "off". */
23
+ before: DiagnoseAllReport | null;
24
+ /** Each recovery action that ran during the fix pass. */
25
+ appliedActions: RecoveryAction[];
26
+ /** Channels still unhealthy after the fix pass (or all unhealthy channels when fix is off). */
16
27
  remainingIssues: Array<{
17
28
  channel: string;
18
29
  diagnosis: ChannelDiagnosis["diagnosis"];
@@ -6,6 +6,7 @@
6
6
  * logic (which fixes to attempt, in what order, when to escalate) lives here.
7
7
  */
8
8
  var FunnelDoctor = class {
9
+ props;
9
10
  constructor(props) {
10
11
  this.props = props;
11
12
  Object.freeze(this);
@@ -17,6 +17,7 @@ var FunnelError = class extends Error {
17
17
  }
18
18
  };
19
19
  var FunnelChannelNotFoundError = class extends FunnelError {
20
+ channel;
20
21
  code = "channel-not-found";
21
22
  constructor(channel, options) {
22
23
  super(`channel not found: ${channel}`, options);
@@ -24,6 +25,7 @@ var FunnelChannelNotFoundError = class extends FunnelError {
24
25
  }
25
26
  };
26
27
  var FunnelChannelAlreadyExistsError = class extends FunnelError {
28
+ channel;
27
29
  code = "channel-already-exists";
28
30
  constructor(channel, options) {
29
31
  super(`channel already exists: ${channel}`, options);
@@ -31,6 +33,8 @@ var FunnelChannelAlreadyExistsError = class extends FunnelError {
31
33
  }
32
34
  };
33
35
  var FunnelConnectorNotFoundError = class extends FunnelError {
36
+ channel;
37
+ connector;
34
38
  code = "connector-not-found";
35
39
  constructor(channel, connector, options) {
36
40
  super(`connector not found in ${channel}: ${connector}`, options);
@@ -39,6 +43,9 @@ var FunnelConnectorNotFoundError = class extends FunnelError {
39
43
  }
40
44
  };
41
45
  var FunnelConnectorTypeMismatchError = class extends FunnelError {
46
+ connector;
47
+ expected;
48
+ actual;
42
49
  code = "connector-type-mismatch";
43
50
  constructor(connector, expected, actual, options) {
44
51
  super(`connector ${connector} type mismatch: expected ${expected}, got ${actual}`, options);
@@ -48,6 +55,8 @@ var FunnelConnectorTypeMismatchError = class extends FunnelError {
48
55
  }
49
56
  };
50
57
  var FunnelAuthFailedError = class extends FunnelError {
58
+ connector;
59
+ detail;
51
60
  code = "auth-failed";
52
61
  constructor(connector, detail, options) {
53
62
  super(`${connector}: auth failed — ${detail}`, options);
@@ -56,6 +65,9 @@ var FunnelAuthFailedError = class extends FunnelError {
56
65
  }
57
66
  };
58
67
  var FunnelGatewayBindError = class extends FunnelError {
68
+ host;
69
+ port;
70
+ detail;
59
71
  code = "gateway-bind";
60
72
  constructor(host, port, detail, options) {
61
73
  super(`gateway failed to bind ${host}:${port} — ${detail}`, options);
@@ -65,6 +77,7 @@ var FunnelGatewayBindError = class extends FunnelError {
65
77
  }
66
78
  };
67
79
  var FunnelTokenCollisionError = class extends FunnelError {
80
+ connector;
68
81
  code = "token-collision";
69
82
  constructor(connector, options) {
70
83
  super(`${connector}: both literal token and tokenEnv reference are set — pick one`, options);
@@ -1,5 +1,4 @@
1
- import { t as ChannelConfig } from "./settings-schema-IShSMm5G.js";
2
-
1
+ import { t as ChannelConfig } from "./settings-schema-B-CKLuST.js";
3
2
  //#region lib/services/recovery/funnel-recovery.d.ts
4
3
  /** Narrow gateway control — start / stop / restart and a probe. */
5
4
  type RecoveryGatewayControl = {
@@ -9,6 +9,7 @@
9
9
  * throwing.
10
10
  */
11
11
  var FunnelRecovery = class {
12
+ props;
12
13
  constructor(props) {
13
14
  this.props = props;
14
15
  Object.freeze(this);