@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.
- package/README.md +31 -6
- package/dist/bin.js +292 -383
- package/dist/channel.d.ts +76 -52
- package/dist/channel.js +82 -47
- package/dist/{channels-DR90RmUP.d.ts → channels-B0cifCfw.d.ts} +6 -6
- package/dist/claude.d.ts +11 -8
- package/dist/claude.js +31 -15
- package/dist/connector-descriptor-D4XtEsH8.d.ts +215 -0
- package/dist/{connector-diagnostics-recorder-COtNEmUp.js → connector-diagnostics-recorder-BmOijU1p.js} +1 -0
- package/dist/connectors/discord.d.ts +11 -8
- package/dist/connectors/discord.js +2 -2
- package/dist/connectors/gh.d.ts +7 -6
- package/dist/connectors/gh.js +2 -2
- package/dist/connectors/schedule.d.ts +21 -17
- package/dist/connectors/schedule.js +3 -3
- package/dist/connectors/slack.d.ts +15 -10
- package/dist/connectors/slack.js +3 -3
- package/dist/diagnostic-event-CxMM5Bl2.js +78 -0
- package/dist/diagnostic-log-BsAg5qZK.d.ts +186 -0
- package/dist/diagnostics.d.ts +2 -2
- package/dist/diagnostics.js +3 -2
- package/dist/{discord-connector-DXARcO6w.js → discord-connector-CAcWifJn.js} +6 -6
- package/dist/{discord-connector-schema-D1dyxhXk.d.ts → discord-connector-schema-BaBZYHxK.d.ts} +0 -1
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +1 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +1 -1
- package/dist/event-log.d.ts +2 -0
- package/dist/event-log.js +3 -0
- package/dist/{file-process-guard-CiBwXjzm.d.ts → file-process-guard-D-QatrwL.d.ts} +22 -18
- package/dist/{file-system-DHgbuTFb.d.ts → file-system-BNtl997T.d.ts} +2 -1
- package/dist/{flume-source-listener-FzR5lGpL.d.ts → flume-source-listener-Ba8_1Mg-.d.ts} +9 -6
- package/dist/{funnel-diagnostics-Cvk6Sk4x.js → funnel-diagnostics-CAG7hJP0.js} +85 -141
- package/dist/{funnel-diagnostics-kbd27d7I.d.ts → funnel-diagnostics-CDrZznTB.d.ts} +18 -18
- package/dist/{funnel-docs-C-ge0MuB.js → funnel-docs-CI4hMkhq.js} +12 -3
- package/dist/{funnel-doctor-CVTNUtDn.d.ts → funnel-doctor-CY4S5cnz.d.ts} +20 -9
- package/dist/{funnel-doctor-XrI2GBH8.js → funnel-doctor-Csw5qjcH.js} +1 -0
- package/dist/{funnel-error-0t1MK1R6.js → funnel-error-DoSBCogD.js} +13 -0
- package/dist/{funnel-recovery-D8MkiTpk.d.ts → funnel-recovery-BwcSNNSN.d.ts} +1 -2
- package/dist/{funnel-recovery-DKnEutUS.js → funnel-recovery-DYbSYGWC.js} +1 -0
- package/dist/gateway/daemon.js +60 -139
- package/dist/gateway.d.ts +2 -3
- package/dist/gateway.js +2 -2
- package/dist/{gh-connector-BUGCOEWS.js → gh-connector-DMwI6zJA.js} +5 -5
- package/dist/{gh-connector-schema-BCHEqpUg.d.ts → gh-connector-schema-BrNsCl1T.d.ts} +0 -1
- package/dist/{index-BU1TtuFy.d.ts → index-fHrKn4jm.d.ts} +210 -40
- package/dist/index.d.ts +125 -115
- package/dist/index.js +242 -57
- package/dist/{local-config-json-schema-DexV8vX3.js → local-config-json-schema-_-YVHX4c.js} +4 -4
- package/dist/local-config.d.ts +2 -3
- package/dist/local-config.js +1 -1
- package/dist/logger.d.ts +17 -265
- package/dist/logger.js +6 -141
- package/dist/memory-event-log-BY8XjFzi.js +147 -0
- package/dist/{memory-token-prompter-D0kIdhar.d.ts → memory-token-prompter-B20tl_aU.d.ts} +2 -3
- package/dist/{node-file-system-BOXIHW_Q.js → node-file-system-xJx7dymE.js} +11 -3
- package/dist/{profiles-M24JrcOh.d.ts → profiles-CePRnv3F.d.ts} +3 -4
- package/dist/{profiles-ZHLONml4.js → profiles-KpN7_gBw.js} +19 -5
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +1 -1
- package/dist/recovery.d.ts +1 -1
- package/dist/recovery.js +1 -1
- package/dist/{schedule-connector-9k3gOIgl.js → schedule-connector-BXUOYgwZ.js} +63 -99
- package/dist/schedule-connector-schema-CNV8PgWX.js +137 -0
- package/dist/schedule-connector-schema-Tq5kJul-.d.ts +127 -0
- package/dist/{settings-reader-BnKIbmsD.d.ts → settings-reader-CyTpbAre.d.ts} +1 -2
- package/dist/{settings-schema-IShSMm5G.d.ts → settings-schema-B-CKLuST.d.ts} +0 -1
- package/dist/{settings-store-ccv291GV.js → settings-store-ZFWBhOnp.js} +12 -3
- package/dist/{slack-connector-CxpWagbT.js → slack-connector-CC3V2VXQ.js} +10 -9
- package/dist/{slack-event-processor-xFDG3US0.js → slack-event-processor-Cs_YuI3w.js} +2 -2
- package/dist/{slack-event-processor-BDTr62cN.d.ts → slack-event-processor-DmAlq4cV.d.ts} +0 -1
- package/dist/{slot-fields-D-pvMgTK.js → slot-fields-eCWizJqS.js} +4 -4
- package/dist/{sqlite-diagnostic-log-CwhXE9Qh.js → sqlite-diagnostic-log-Qs4cf9I_.js} +92 -100
- package/dist/sqlite-event-log-BYgCFMxC.d.ts +284 -0
- package/dist/{funnel-log-sqlite-sink-DLYkY0pZ.js → sqlite-event-log-eZuXKszJ.js} +53 -12
- package/dist/{yaml-render-DVEtYuF1.js → yaml-render-DxxRQT-V.js} +3 -3
- package/package.json +25 -10
- package/dist/broadcaster-DVNcw00K.d.ts +0 -138
- package/dist/connector-descriptor-CmyHlEiT.d.ts +0 -380
- package/dist/diagnostic-sql-reader-oXZnWFf_.js +0 -83
- package/dist/file-system-Wvzc2ePY.js +0 -9
- package/dist/logger-B6iyNbxM.js +0 -9
- package/dist/schedule-connector-schema-D7ZEKvit.d.ts +0 -49
- package/dist/schedule-connector-schema-DKEPZnVv.js +0 -31
- package/dist/settings-reader-9FcX3qS1.js +0 -11
- /package/dist/{connector-adapter-Dvs8N7ew.js → connector-adapter-BL2ehMej.js} +0 -0
- /package/dist/{connector-listener-mPGZYa8e.js → connector-listener-BcQMzY6Q.js} +0 -0
- /package/dist/{discord-connector-schema-B4YpWpR3.js → discord-connector-schema-saAGuPmJ.js} +0 -0
- /package/dist/{error-message-of-ColuYmAk.js → error-message-of-BG4ezZ_s.js} +0 -0
- /package/dist/{funnel-docs-Cd66sKyV.d.ts → funnel-docs-BJWawfWI.d.ts} +0 -0
- /package/dist/{gh-connector-schema-CAqIhzGr.js → gh-connector-schema-DmZTrzUu.js} +0 -0
- /package/dist/{logger-Bc_Ooypb.d.ts → logger-mMZvblTx.d.ts} +0 -0
- /package/dist/{loopback-fetch-CVNuN3YZ.js → loopback-fetch-HP-bo9Kv.js} +0 -0
- /package/dist/{node-http-client-u00atiKx.js → node-http-client-SX3xFh4h.js} +0 -0
- /package/dist/{node-process-runner-DxTvycoK.js → node-process-runner-D4rzPl8Q.js} +0 -0
- /package/dist/{resolve-connector-token-DxDG9mhf.js → resolve-connector-token-BwiJCu82.js} +0 -0
- /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/
|
|
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
|
|
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 `
|
|
40
|
+
* atomically via `lastInsertRowid`. Two `EventLog` instances pointed
|
|
41
41
|
* at the same database file therefore see one monotonically increasing
|
|
42
|
-
* seq stream without
|
|
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
|
|
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 = [
|
|
167
|
-
const params = [
|
|
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
|
|
209
|
-
|
|
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 {
|
|
342
|
+
export { SqliteEventLog as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { t as NodeFunnelFileSystem } from "./node-file-system-
|
|
2
|
-
import { t as NodeFunnelProcessRunner } from "./node-process-runner-
|
|
3
|
-
import {
|
|
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.
|
|
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": "
|
|
27
|
-
"funnel": "
|
|
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.
|
|
126
|
-
"@interactive-inc/flume": "^0.10.
|
|
127
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
128
|
-
"hono": "^4.12.
|
|
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.
|
|
135
|
-
"vite-plus": "^0.
|
|
136
|
-
"vitest": "^4.1.
|
|
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 };
|