@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
@@ -0,0 +1,284 @@
1
+ //#region lib/event-log/event-log-entry.d.ts
2
+ /**
3
+ * Wrapper that `EventLog.append` puts around every event before handing it
4
+ * to a sink. `seq` is monotonic across the lifetime of the underlying store —
5
+ * sinks persist it as the primary key so replay (and broadcaster seeding
6
+ * after restart) is an indexed range scan, not a full table walk. `ts` is
7
+ * epoch milliseconds. `event` is the caller-defined payload validated by the
8
+ * validation function passed to the log.
9
+ */
10
+ type EventLogEntry<E> = {
11
+ seq: number;
12
+ ts: number;
13
+ event: E;
14
+ };
15
+ //#endregion
16
+ //#region lib/event-log/event-log-store.d.ts
17
+ /**
18
+ * Relay sink. Receives records that already have a `seq` assigned by the
19
+ * primary and stores or forwards them — memory ring, stdout, network push,
20
+ * a second SQLite mirror, etc. Does not generate seq itself, so any number
21
+ * can be attached and they all observe the same monotonic stream.
22
+ *
23
+ * `write` returns `void` on success or an `Error` the `EventLog` surfaces via
24
+ * `onSinkError`. Throwing is also tolerated (`EventLog` catches it), but
25
+ * returning is preferred so the failure path is part of the type.
26
+ */
27
+ type EventLogRelay<E> = {
28
+ write(record: EventLogEntry<E>): void | Error;
29
+ close?(): void;
30
+ };
31
+ /**
32
+ * Primary sink. Owns the canonical seq sequence for the log. `insert` is
33
+ * the atomic boundary — it assigns a seq strictly greater than every
34
+ * previously assigned one, persists the record, and returns it. SQLite
35
+ * implementations get atomicity for free by delegating to `INTEGER PRIMARY
36
+ * KEY` so two processes sharing one database file see one monotonic
37
+ * stream without coordinating through `EventLog`.
38
+ *
39
+ * `getMaxSeq` is the highest seq currently in the sink — used for
40
+ * observability and for replay seeding by clients reading the store.
41
+ */
42
+ type EventLogStore<E> = {
43
+ insert(input: {
44
+ ts: number;
45
+ event: E;
46
+ }): EventLogEntry<E> | Error;
47
+ getMaxSeq(): number;
48
+ close?(): void;
49
+ };
50
+ //#endregion
51
+ //#region lib/event-log/event-log.d.ts
52
+ type Listener<E> = (record: EventLogEntry<E>) => void;
53
+ type SinkErrorHandler<E> = (error: Error, record: EventLogEntry<E>, sink: EventLogRelay<E>) => void;
54
+ type EventLogValidator<E> = (event: unknown) => {
55
+ success: true;
56
+ data: E;
57
+ } | {
58
+ success: false;
59
+ error: Error;
60
+ };
61
+ type EventLogProps<E> = {
62
+ /** Validates each event before emission. Use `schema.safeParse` from any validation library, or a plain function. */
63
+ validate: EventLogValidator<E>;
64
+ /** Owns seq assignment + durability. Use `SqliteEventLog` for multi-process safety. */
65
+ primary: EventLogStore<E>;
66
+ /** Optional fanout for already-sequenced records (memory ring, stdout, network mirror). */
67
+ relays?: ReadonlyArray<EventLogRelay<E>>;
68
+ /** Override for tests. Defaults to `Date.now`. */
69
+ now?: () => number;
70
+ /** Observer for relay failures. Default: silently swallow. */
71
+ onSinkError?: SinkErrorHandler<E>;
72
+ };
73
+ /**
74
+ * Validated event log. Three responsibilities and nothing else:
75
+ * validate the event, delegate seq + persistence to the primary sink, and
76
+ * fan the resulting record out to relays and live subscribers.
77
+ *
78
+ * Splitting "primary" from "relays" makes the seq invariant honest: there
79
+ * is exactly one source of truth (the primary's atomic insert). Two
80
+ * `EventLog` instances pointed at the same SQLite file therefore see
81
+ * one monotonic stream without coordinating with each other. Relays mirror
82
+ * already-sequenced records, so they can be added or removed without
83
+ * affecting correctness.
84
+ *
85
+ * Failure isolation:
86
+ * - Primary failure short-circuits append and is returned to the caller.
87
+ * - Relay failures never block the primary path — they surface via the
88
+ * optional `onSinkError` callback so the caller can observe without
89
+ * being interrupted.
90
+ * - A subscriber that throws is contained; the rest of the fanout
91
+ * completes normally.
92
+ */
93
+ declare class EventLog<E> {
94
+ private readonly validate;
95
+ private readonly primary;
96
+ private readonly relays;
97
+ private readonly now;
98
+ private readonly onSinkError;
99
+ private readonly listeners;
100
+ constructor(props: EventLogProps<E>);
101
+ append(event: E): EventLogEntry<E> | Error;
102
+ subscribe(listener: Listener<E>): () => void;
103
+ getMaxSeq(): number;
104
+ close(): void;
105
+ private callPrimary;
106
+ private fanOutToRelays;
107
+ private callRelay;
108
+ private fanOutToListeners;
109
+ private callClose;
110
+ }
111
+ //#endregion
112
+ //#region lib/event-log/memory-event-log.d.ts
113
+ type MemoryEventLogProps = {
114
+ /** Hard cap on retained records. The oldest is evicted on overflow. 0 disables retention. */
115
+ capacity?: number;
116
+ };
117
+ /**
118
+ * In-memory ring buffer that doubles as primary or relay. As primary it
119
+ * owns its own seq counter (single-process only — for multi-process
120
+ * safety, use `SqliteEventLog` as primary and place this as a
121
+ * relay). As relay it accepts whatever seq the primary assigned and
122
+ * advances its own counter to match, so `getMaxSeq` stays meaningful.
123
+ *
124
+ * Useful as a test double, as a short-window replay buffer paired with a
125
+ * persistent primary (covering reconnects without round-tripping disk),
126
+ * or as a backing store for live subscribers.
127
+ */
128
+ declare class MemoryEventLog<E> implements EventLogStore<E>, EventLogRelay<E> {
129
+ private readonly capacity;
130
+ private readonly buffer;
131
+ private seq;
132
+ constructor(props?: MemoryEventLogProps);
133
+ insert(input: {
134
+ ts: number;
135
+ event: E;
136
+ }): EventLogEntry<E>;
137
+ write(record: EventLogEntry<E>): void;
138
+ getMaxSeq(): number;
139
+ query(): ReadonlyArray<EventLogEntry<E>>;
140
+ clear(): void;
141
+ private append;
142
+ }
143
+ //#endregion
144
+ //#region lib/event-log/sqlite-event-log.d.ts
145
+ type IndexValues<I extends ReadonlyArray<string>> = Record<I[number], string | null>;
146
+ type IndexPrefixes<I extends ReadonlyArray<string>> = Partial<Record<I[number], string>>;
147
+ type IndexSets<I extends ReadonlyArray<string>> = Partial<Record<I[number], ReadonlyArray<string | null>>>;
148
+ /**
149
+ * Constructor props. The shape narrows on `I`: when no indexes are
150
+ * declared (the default), `extractIndexes` is forbidden; when indexes
151
+ * are declared, both `indexes` and `extractIndexes` are required and
152
+ * `extractIndexes` is type-checked against the index keys.
153
+ */
154
+ type SqliteEventLogProps<E, I extends ReadonlyArray<string>> = I extends readonly [] ? {
155
+ path: string;
156
+ maxRows?: number;
157
+ maxAgeMs?: number;
158
+ maxBytes?: number;
159
+ targetBytes?: number;
160
+ now?: () => number;
161
+ indexes?: I;
162
+ extractIndexes?: never;
163
+ } : {
164
+ path: string;
165
+ maxRows?: number;
166
+ maxAgeMs?: number;
167
+ maxBytes?: number;
168
+ targetBytes?: number;
169
+ now?: () => number;
170
+ indexes: I;
171
+ extractIndexes: (event: E) => IndexValues<I>;
172
+ };
173
+ type SqliteEventLogQuery<I extends ReadonlyArray<string>> = {
174
+ /** Return only records with seq strictly greater than this. */
175
+ sinceSeq?: number;
176
+ /** Match one sequence number exactly instead of using `sinceSeq`. */
177
+ seq?: number;
178
+ /** Filter by the top-level `event.type` discriminator. */
179
+ type?: string;
180
+ /** Filter by indexed columns. Keys are constrained to the declared `indexes`. */
181
+ where?: Partial<IndexValues<I>>;
182
+ /** Prefix-match indexed text columns. */
183
+ wherePrefix?: IndexPrefixes<I>;
184
+ /** Match any value in a set for indexed columns. */
185
+ whereIn?: IndexSets<I>;
186
+ /** Maximum rows returned. Default 1000. */
187
+ limit?: number;
188
+ /**
189
+ * Which end of the seq range to take when `limit` clips the result.
190
+ * "asc" (default) returns the oldest matching rows; "desc" returns the
191
+ * newest. Rows are always sorted ascending by seq before returning, so the
192
+ * caller sees a chronological slice either way — "desc" just picks the tail.
193
+ */
194
+ order?: "asc" | "desc";
195
+ };
196
+ /**
197
+ * SQLite-backed sink built on `bun:sqlite`. Implements both primary and
198
+ * relay roles so the same instance can own seq generation for one log and
199
+ * mirror records from another (e.g. cross-process replication, restore
200
+ * from a backup stream).
201
+ *
202
+ * Concurrency model: seq is `INTEGER PRIMARY KEY`, so SQLite assigns it
203
+ * atomically via `lastInsertRowid`. Two `EventLog` instances pointed
204
+ * at the same database file therefore see one monotonically increasing
205
+ * seq stream without coordinating through `EventLog` — the database itself is
206
+ * the synchronization point.
207
+ *
208
+ * Schema is version-managed via `PRAGMA user_version`. Migrations are
209
+ * append-only and run in a transaction on every construct so a partial
210
+ * upgrade rolls back cleanly. Caller-defined `indexes` are layered on top
211
+ * via `ALTER TABLE ADD COLUMN` + `CREATE INDEX IF NOT EXISTS`, so adding
212
+ * a new index to an existing database is a no-downtime operation.
213
+ *
214
+ * Type safety: the second generic parameter `I` is the literal tuple of
215
+ * index column names. `extractIndexes` and `query({ where })` are
216
+ * both type-checked against this tuple, so a typo at the call site is a
217
+ * compile-time error rather than a silent miss at runtime.
218
+ *
219
+ * Retention is bounded by `maxRows` and/or `maxAgeMs`. Both run on every
220
+ * insert as a single indexed DELETE that no-ops below the cap.
221
+ *
222
+ * Bulk inserts use `insertMany`, which wraps the batch in one transaction
223
+ * for ~10–100x throughput at the cost of one fsync per batch instead of
224
+ * one per row.
225
+ */
226
+ declare class SqliteEventLog<E, const I extends ReadonlyArray<string> = readonly []> implements EventLogStore<E>, EventLogRelay<E> {
227
+ private readonly db;
228
+ private readonly maxRows;
229
+ private readonly maxAgeMs;
230
+ private readonly maxBytes;
231
+ private readonly targetBytes;
232
+ private readonly now;
233
+ private readonly indexes;
234
+ private readonly extractIndexes;
235
+ private readonly insertStmt;
236
+ private readonly insertWithSeqStmt;
237
+ private readonly maxSeqStmt;
238
+ private readonly countStmt;
239
+ private readonly trimRowsStmt;
240
+ private readonly trimAgeStmt;
241
+ private readonly trimOldestStmt;
242
+ private insertsSinceByteCheck;
243
+ constructor(props: SqliteEventLogProps<E, I>);
244
+ insert(input: {
245
+ ts: number;
246
+ event: E;
247
+ }): EventLogEntry<E> | Error;
248
+ insertMany(inputs: ReadonlyArray<{
249
+ ts: number;
250
+ event: E;
251
+ }>): EventLogEntry<E>[] | Error;
252
+ write(record: EventLogEntry<E>): void | Error;
253
+ getMaxSeq(): number;
254
+ query(props?: SqliteEventLogQuery<I>): EventLogEntry<E>[];
255
+ /**
256
+ * Current schema version. Useful for diagnostics and for tests that want
257
+ * to verify migrations ran. Reads `PRAGMA user_version` once per call.
258
+ */
259
+ getSchemaVersion(): number;
260
+ close(): void;
261
+ private buildInsertParams;
262
+ private appendWhereConditions;
263
+ private appendPrefixConditions;
264
+ private appendSetConditions;
265
+ private trim;
266
+ private indexKeys;
267
+ /**
268
+ * Throttled byte-size enforcement. Only every BYTE_CHECK_INTERVAL inserts do
269
+ * we measure the file; on overflow we estimate how many of the oldest rows to
270
+ * drop to land near targetBytes (by the byte/row ratio), delete them in one
271
+ * statement, then VACUUM once to return the freed pages to the filesystem (a
272
+ * plain DELETE only frees pages inside the file). One DELETE + one VACUUM per
273
+ * overflow keeps the expensive rewrite rare — the file must refill the whole
274
+ * maxBytes→targetBytes delta before the next overflow can trigger.
275
+ */
276
+ private maybeTrimBytes;
277
+ private byteSize;
278
+ /** Drop every row and reclaim the file space. Used by `<log>.clear()`. */
279
+ clear(): void;
280
+ private syncIndexColumns;
281
+ private migrate;
282
+ }
283
+ //#endregion
284
+ export { MemoryEventLogProps as a, EventLogValidator as c, EventLogEntry as d, MemoryEventLog as i, EventLogRelay as l, SqliteEventLogProps as n, EventLog as o, SqliteEventLogQuery as r, EventLogProps as s, SqliteEventLog as t, EventLogStore as u };
@@ -1,12 +1,12 @@
1
1
  import { dirname } from "node:path";
2
2
  import { existsSync, mkdirSync } from "node:fs";
3
3
  import { Database } from "bun:sqlite";
4
- //#region lib/logger/funnel-log-sqlite-sink.ts
4
+ //#region lib/event-log/sqlite-event-log.ts
5
5
  /** Conservative whitelist for column names interpolated into SQL. */
6
6
  const COLUMN_NAME_RE = /^[a-z_][a-z0-9_]*$/;
7
7
  /** How many inserts between on-disk size checks (see insertsSinceByteCheck). */
8
8
  const BYTE_CHECK_INTERVAL = 500;
9
- const RESERVED_COLUMNS = new Set([
9
+ const RESERVED_COLUMNS = /* @__PURE__ */ new Set([
10
10
  "seq",
11
11
  "ts",
12
12
  "type",
@@ -32,14 +32,14 @@ const MIGRATIONS = [[
32
32
  ]];
33
33
  /**
34
34
  * SQLite-backed sink built on `bun:sqlite`. Implements both primary and
35
- * relay roles so the same instance can own seq generation for one bus and
35
+ * relay roles so the same instance can own seq generation for one log and
36
36
  * mirror records from another (e.g. cross-process replication, restore
37
37
  * from a backup stream).
38
38
  *
39
39
  * Concurrency model: seq is `INTEGER PRIMARY KEY`, so SQLite assigns it
40
- * atomically via `lastInsertRowid`. Two `FunnelLog` instances pointed
40
+ * atomically via `lastInsertRowid`. Two `EventLog` instances pointed
41
41
  * at the same database file therefore see one monotonically increasing
42
- * seq stream without any bus-level coordination — the database itself is
42
+ * seq stream without coordinating through `EventLog` — the database itself is
43
43
  * the synchronization point.
44
44
  *
45
45
  * Schema is version-managed via `PRAGMA user_version`. Migrations are
@@ -60,7 +60,7 @@ const MIGRATIONS = [[
60
60
  * for ~10–100x throughput at the cost of one fsync per batch instead of
61
61
  * one per row.
62
62
  */
63
- var FunnelLogSqliteSink = class {
63
+ var SqliteEventLog = class {
64
64
  db;
65
65
  maxRows;
66
66
  maxAgeMs;
@@ -163,13 +163,22 @@ var FunnelLogSqliteSink = class {
163
163
  return row ? row.max : 0;
164
164
  }
165
165
  query(props = {}) {
166
- const conditions = ["seq > ?"];
167
- const params = [props.sinceSeq ?? 0];
166
+ const conditions = [];
167
+ const params = [];
168
+ if (props.seq !== void 0) {
169
+ conditions.push("seq = ?");
170
+ params.push(props.seq);
171
+ } else {
172
+ conditions.push("seq > ?");
173
+ params.push(props.sinceSeq ?? 0);
174
+ }
168
175
  if (typeof props.type === "string") {
169
176
  conditions.push("type = ?");
170
177
  params.push(props.type);
171
178
  }
172
179
  if (props.where) this.appendWhereConditions(props.where, conditions, params);
180
+ if (props.wherePrefix) this.appendPrefixConditions(props.wherePrefix, conditions, params);
181
+ if (props.whereIn) this.appendSetConditions(props.whereIn, conditions, params);
173
182
  const limit = props.limit ?? 1e3;
174
183
  params.push(limit);
175
184
  const dir = props.order === "desc" ? "DESC" : "ASC";
@@ -205,9 +214,8 @@ var FunnelLogSqliteSink = class {
205
214
  ];
206
215
  }
207
216
  appendWhereConditions(where, conditions, params) {
208
- const widened = where;
209
- for (const col of this.indexes) {
210
- const value = widened[col];
217
+ for (const col of this.indexKeys()) {
218
+ const value = where[col];
211
219
  if (value === void 0) continue;
212
220
  if (value === null) conditions.push(`${col} IS NULL`);
213
221
  else {
@@ -216,6 +224,33 @@ var FunnelLogSqliteSink = class {
216
224
  }
217
225
  }
218
226
  }
227
+ appendPrefixConditions(prefixes, conditions, params) {
228
+ for (const col of this.indexKeys()) {
229
+ const value = prefixes[col];
230
+ if (value === void 0) continue;
231
+ conditions.push(`${col} LIKE ? ESCAPE '\\'`);
232
+ params.push(`${escapeLike(value)}%`);
233
+ }
234
+ }
235
+ appendSetConditions(sets, conditions, params) {
236
+ for (const col of this.indexKeys()) {
237
+ const values = sets[col];
238
+ if (values === void 0) continue;
239
+ if (values.length === 0) {
240
+ conditions.push("1 = 0");
241
+ continue;
242
+ }
243
+ const nonNull = values.filter((value) => value !== null);
244
+ const hasNull = nonNull.length !== values.length;
245
+ const alternatives = [];
246
+ if (nonNull.length > 0) {
247
+ alternatives.push(`${col} IN (${nonNull.map(() => "?").join(", ")})`);
248
+ params.push(...nonNull);
249
+ }
250
+ if (hasNull) alternatives.push(`${col} IS NULL`);
251
+ conditions.push(`(${alternatives.join(" OR ")})`);
252
+ }
253
+ }
219
254
  trim() {
220
255
  if (this.maxRows !== null) {
221
256
  const row = this.countStmt.get();
@@ -224,6 +259,9 @@ var FunnelLogSqliteSink = class {
224
259
  if (this.maxAgeMs !== null) this.trimAgeStmt.run(this.now() - this.maxAgeMs);
225
260
  this.maybeTrimBytes();
226
261
  }
262
+ indexKeys() {
263
+ return this.indexes;
264
+ }
227
265
  /**
228
266
  * Throttled byte-size enforcement. Only every BYTE_CHECK_INTERVAL inserts do
229
267
  * we measure the file; on overflow we estimate how many of the oldest rows to
@@ -278,6 +316,9 @@ var FunnelLogSqliteSink = class {
278
316
  }
279
317
  }
280
318
  };
319
+ function escapeLike(value) {
320
+ return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
321
+ }
281
322
  function validateIndexNames(names) {
282
323
  for (const name of names) {
283
324
  if (!COLUMN_NAME_RE.test(name)) throw new Error(`invalid index column name: ${name}`);
@@ -298,4 +339,4 @@ function toRecord(row) {
298
339
  };
299
340
  }
300
341
  //#endregion
301
- export { FunnelLogSqliteSink as t };
342
+ export { SqliteEventLog as t };
@@ -1,6 +1,6 @@
1
- import { t as NodeFunnelFileSystem } from "./node-file-system-BOXIHW_Q.js";
2
- import { t as NodeFunnelProcessRunner } from "./node-process-runner-DxTvycoK.js";
3
- import { m as NodeFunnelIdGenerator, n as FUNNEL_DIR, s as resolveFunnelPort } from "./settings-store-ccv291GV.js";
1
+ import { t as NodeFunnelFileSystem } from "./node-file-system-xJx7dymE.js";
2
+ import { t as NodeFunnelProcessRunner } from "./node-process-runner-D4rzPl8Q.js";
3
+ import { h as NodeFunnelIdGenerator, n as FUNNEL_DIR, s as resolveFunnelPort } from "./settings-store-ZFWBhOnp.js";
4
4
  import { join } from "node:path";
5
5
  import { stringify } from "yaml";
6
6
  //#region lib/engine/claude/claude.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interactive-inc/claude-funnel",
3
- "version": "0.66.0",
3
+ "version": "0.68.0",
4
4
  "description": "Hub CLI that routes external events (Slack / GitHub / Discord) to Claude Code agents through subscription channels over MCP.",
5
5
  "keywords": [
6
6
  "bun",
@@ -23,8 +23,8 @@
23
23
  "url": "git+https://github.com/interactive-inc/open-claude-funnel.git"
24
24
  },
25
25
  "bin": {
26
- "fnl": "./dist/bin.js",
27
- "funnel": "./dist/bin.js"
26
+ "fnl": "dist/bin.js",
27
+ "funnel": "dist/bin.js"
28
28
  },
29
29
  "files": [
30
30
  "dist/**/*",
@@ -102,6 +102,11 @@
102
102
  "import": "./dist/connectors/schedule.js",
103
103
  "default": "./dist/connectors/schedule.js"
104
104
  },
105
+ "./event-log": {
106
+ "types": "./dist/event-log.d.ts",
107
+ "import": "./dist/event-log.js",
108
+ "default": "./dist/event-log.js"
109
+ },
105
110
  "./logger": {
106
111
  "types": "./dist/logger.d.ts",
107
112
  "import": "./dist/logger.js",
@@ -122,18 +127,28 @@
122
127
  "prepack": "make build"
123
128
  },
124
129
  "dependencies": {
125
- "@hono/zod-validator": "0.8.0",
126
- "@interactive-inc/flume": "^0.10.0",
127
- "@modelcontextprotocol/sdk": "^1.29.0",
128
- "hono": "^4.12.19",
130
+ "@hono/zod-validator": "0.9.0",
131
+ "@interactive-inc/flume": "^0.10.1",
132
+ "@modelcontextprotocol/sdk": "^1.30.0",
133
+ "hono": "^4.12.32",
129
134
  "yaml": "^2.9.0",
130
135
  "zod": "^4.4.3"
131
136
  },
132
137
  "devDependencies": {
133
138
  "@types/bun": "^1.3.14",
134
- "@typescript/native-preview": "^7.0.0-dev.20260516.1",
135
- "vite-plus": "^0.1.21",
136
- "vitest": "^4.1.8"
139
+ "@typescript/native-preview": "^7.0.0-dev.20260707.2",
140
+ "vite-plus": "^0.2.6",
141
+ "vitest": "^4.1.10"
142
+ },
143
+ "overrides": {
144
+ "@hono/node-server": "2.0.12",
145
+ "body-parser": "2.3.0",
146
+ "fast-uri": "4.1.1",
147
+ "ip-address": "10.3.1",
148
+ "postcss": "8.5.23",
149
+ "qs": "6.15.3",
150
+ "vite": "8.1.5",
151
+ "ws": "8.21.1"
137
152
  },
138
153
  "engines": {
139
154
  "bun": ">=1.3.0"
@@ -1,138 +0,0 @@
1
- import { t as FunnelLogger } from "./logger-Bc_Ooypb.js";
2
- import { ServerWebSocket } from "bun";
3
-
4
- //#region lib/engine/error/on-funnel-error.d.ts
5
- /**
6
- * Host integration hook called when Funnel catches an exception that would
7
- * otherwise be silently swallowed (subscriber throw, listener start failure,
8
- * MCP forward failure, etc.). Pass `Sentry.captureException` from the host to
9
- * pipe these into your error reporter. Defaults to a no-op when omitted.
10
- *
11
- * `context` carries the component name and any extra metadata the caller had
12
- * at the catch site (channel / connector / subscriber id when available).
13
- */
14
- type OnFunnelError = (error: Error, context?: Record<string, unknown>) => void;
15
- //#endregion
16
- //#region lib/gateway/broadcaster.d.ts
17
- type ClientData = {
18
- /** Stable channel id (uuid) that the WS client subscribed to. */channel: string; /** Human-facing channel name resolved at upgrade time, kept for log readability. */
19
- channelName?: string | null; /** Connector names belonging to that channel. */
20
- connectors: string[]; /** Routing mode resolved from channel config at upgrade time. Defaults to fanout. */
21
- delivery?: "fanout" | "exclusive";
22
- /**
23
- * Opaque per-client id declared at upgrade time (`?id=<subscriberId>`). When an
24
- * event carries `meta.target`, only the client whose `subscriberId` equals it
25
- * receives the event among that channel's regular subscribers. Targeted delivery
26
- * is how a publisher addresses one specific instance (e.g. a single agent
27
- * session) without every subscriber having to receive and discard it.
28
- */
29
- subscriberId?: string;
30
- };
31
- type BroadcastEvent = {
32
- content: string;
33
- meta?: Record<string, string>;
34
- };
35
- type ReplayableEvent = BroadcastEvent & {
36
- offset: number;
37
- };
38
- type BroadcastSubscriber = (event: ReplayableEvent) => void;
39
- /**
40
- * Optional persistent replay source. Wired in by the gateway-server with a
41
- * `FunnelEventLog` (SQLite-backed by default) so reconnects across daemon
42
- * restarts can recover events older than the in-memory buffer via an indexed
43
- * `seq > since` range scan.
44
- */
45
- type ReplaySource = {
46
- loadSince(since: number): ReplayableEvent[];
47
- };
48
- type Deps = {
49
- logger?: FunnelLogger; /** Host hook for surfacing subscriber-throw exceptions. Defaults to no-op. */
50
- onError?: OnFunnelError;
51
- maxBufferedBytes?: number;
52
- now?: () => number; /** Number of recent events kept in the in-memory replay buffer. */
53
- replayBufferSize?: number; /** Hard byte cap on replay buffer payloads. Older events are evicted FIFO until under this cap. */
54
- replayBufferMaxBytes?: number; /** Persistent replay source consulted when the in-memory buffer cannot satisfy `since`. */
55
- persistentReplay?: ReplaySource;
56
- };
57
- type BroadcasterMetrics = {
58
- clients: number;
59
- subscribers: number;
60
- eventsBroadcast: number;
61
- droppedSlowClients: number;
62
- lastBroadcastAt: string | null; /** Latest emitted offset. Clients can `?since=<offset>` to ask for events strictly after this point. */
63
- latestOffset: number; /** Oldest offset still held in the replay buffer. Older values cannot be replayed and trigger a full resync. */
64
- oldestReplayableOffset: number | null;
65
- };
66
- /**
67
- * In-process pub/sub for connector events.
68
- *
69
- * Two outbound paths:
70
- * - WS clients connected via the gateway's `/ws` endpoint, scoped per channel
71
- * - In-process subscribers registered via `subscribe()` (programmable API)
72
- *
73
- * Backpressure: if a WS client's `bufferedAmount` exceeds `maxBufferedBytes`
74
- * (default 1 MiB), the client is closed with code 1009 and dropped from the
75
- * registry to keep one slow consumer from blocking the daemon.
76
- *
77
- * Replay: every emitted event gets a strictly increasing `offset`. The latest
78
- * `replayBufferSize` events are kept in memory; reconnecting WS clients can
79
- * pass `?since=<offset>` and the broadcaster resends matching events before
80
- * resuming the live stream. The in-memory ring covers short reconnects;
81
- * older history is served from the event log wired in as `persistentReplay`.
82
- */
83
- declare class FunnelBroadcaster {
84
- private readonly clients;
85
- private readonly subscribers;
86
- private readonly logger;
87
- private readonly onError;
88
- private readonly maxBufferedBytes;
89
- private readonly now;
90
- private readonly replayBufferSize;
91
- private readonly replayBufferMaxBytes;
92
- private readonly replayBuffer;
93
- private readonly persistentReplay;
94
- private readonly exclusiveCursor;
95
- private replayBufferBytes;
96
- private eventsBroadcast;
97
- private droppedSlowClients;
98
- private lastBroadcastAt;
99
- private latestOffset;
100
- constructor(deps?: Deps);
101
- getMetrics(): BroadcasterMetrics;
102
- /**
103
- * Returns events with offset > since, filtered by the connector subscription
104
- * rules of `data`. Used at WS upgrade time when the client passes `?since=<offset>`.
105
- *
106
- * Two-tier lookup:
107
- * 1. The in-memory ring buffer (covers short reconnects, last `replayBufferSize` events).
108
- * 2. If `since` predates the oldest in-memory entry and a persistent replay source
109
- * is wired in (SQLite by default), the gap is filled from it. This covers reconnects
110
- * across daemon restarts where the in-memory buffer was lost.
111
- *
112
- * Result is sorted ascending by offset and de-duplicated against the in-memory buffer.
113
- */
114
- replaySince(since: number, data: ClientData): ReplayableEvent[];
115
- private matchesClient;
116
- /**
117
- * Returns the list of WS clients that should receive `event`. For each per-channel group:
118
- * - fanout → every matching client receives
119
- * - exclusive → exactly one client receives, picked round-robin per channel
120
- *
121
- * `meta.target` narrows the recipient set via `matchesClient`: only the subscriber
122
- * whose `subscriberId` equals `target` receives a targeted event.
123
- */
124
- private pickRecipients;
125
- addClient(ws: ServerWebSocket<unknown>, data: ClientData): void;
126
- removeClient(ws: ServerWebSocket<unknown>): void;
127
- getClientCount(): number;
128
- listChannels(): {
129
- channel: string;
130
- connectors: string[];
131
- }[];
132
- subscribe(handler: BroadcastSubscriber): () => void;
133
- broadcast(content: string, meta?: Record<string, string>): ReplayableEvent;
134
- /** Forward-seed the offset counter (used at startup from the persisted event store). */
135
- seedLatestOffset(offset: number): void;
136
- }
137
- //#endregion
138
- export { OnFunnelError as a, ReplayableEvent as i, BroadcastSubscriber as n, FunnelBroadcaster as r, BroadcastEvent as t };