@interactive-inc/claude-funnel 0.60.1 → 0.63.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 +2 -2
- package/dist/bin.js +428 -761
- package/dist/{channels-2g_BU1N0.d.ts → channels-B8RQPrVq.d.ts} +17 -16
- package/dist/claude.d.ts +5 -7
- package/dist/claude.js +143 -36
- package/dist/{connector-descriptor-6SXJoszo.d.ts → connector-descriptor-ClEEbuW3.d.ts} +50 -11
- package/dist/connector-diagnostics-recorder-COtNEmUp.js +42 -0
- package/dist/connectors/discord.d.ts +31 -37
- package/dist/connectors/discord.js +3 -3
- package/dist/connectors/gh.d.ts +37 -33
- package/dist/connectors/gh.js +3 -3
- package/dist/connectors/schedule.d.ts +9 -57
- package/dist/connectors/schedule.js +3 -3
- package/dist/connectors/slack.d.ts +71 -131
- package/dist/connectors/slack.js +4 -3
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +1 -1
- package/dist/discord-connector-DIFkYBbi.js +250 -0
- package/dist/discord-connector-schema-D-bOVAKt.d.ts +22 -0
- package/dist/docs.js +1 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +1 -1
- package/dist/{file-process-guard-C_PLxfUX.d.ts → file-process-guard-DGHxALfI.d.ts} +6 -6
- package/dist/{file-system-o51IsM0W.d.ts → file-system-VhwwXZbm.d.ts} +8 -0
- package/dist/flume-source-listener-Dim5szHG.d.ts +133 -0
- package/dist/{funnel-diagnostics-CSiJmPlZ.js → funnel-diagnostics-Cvk6Sk4x.js} +193 -43
- package/dist/{funnel-diagnostics-DpXOsCty.d.ts → funnel-diagnostics-b9ar0Ing.d.ts} +67 -5
- package/dist/{funnel-docs-BxXZ9Ksx.js → funnel-docs-C-ge0MuB.js} +42 -6
- package/dist/{funnel-doctor-CZf_0Luq.d.ts → funnel-doctor-CnRQi4kM.d.ts} +2 -2
- package/dist/{funnel-doctor-DiJCjHsg.js → funnel-doctor-XrI2GBH8.js} +1 -1
- package/dist/funnel-error-0t1MK1R6.js +75 -0
- package/dist/{funnel-recovery-DnLrdWO9.d.ts → funnel-recovery-CMhY8Jfk.d.ts} +1 -1
- package/dist/gateway/daemon.js +167 -527
- package/dist/gateway.d.ts +3 -3
- package/dist/gateway.js +3 -3
- package/dist/gh-connector-BUGCOEWS.js +187 -0
- package/dist/{gh-connector-schema-Rzwc1c1N.js → gh-connector-schema-CAqIhzGr.js} +7 -0
- package/dist/gh-connector-schema-DWQaB6gX.d.ts +16 -0
- package/dist/{index-CgY8NdMz.d.ts → index-DxRikYmu.d.ts} +37 -19
- package/dist/index.d.ts +182 -22
- package/dist/index.js +363 -173
- package/dist/{local-config-json-schema-JyLqOQNX.js → local-config-json-schema-DexV8vX3.js} +24 -4
- package/dist/local-config.d.ts +39 -2
- package/dist/local-config.js +53 -2
- package/dist/logger.js +1 -1
- package/dist/loopback-fetch-CVNuN3YZ.js +40 -0
- package/dist/{local-config-sync-Dh1Croqe.d.ts → memory-token-prompter-DP_YV9xX.d.ts} +30 -3
- package/dist/node-file-system-BOXIHW_Q.js +174 -0
- package/dist/{profiles-DSzTeKQw.js → profiles-ZHLONml4.js} +49 -49
- package/dist/{profiles-Cy5wXQ0L.d.ts → profiles-cVZQkM69.d.ts} +3 -3
- 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/resolve-connector-token-DxDG9mhf.js +22 -0
- package/dist/{schedule-connector-L4uzg5M8.js → schedule-connector-9k3gOIgl.js} +54 -55
- package/dist/schedule-connector-schema-Z0RXLgPI.d.ts +49 -0
- package/dist/settings-reader-BNxjsxCB.d.ts +27 -0
- package/dist/{settings-store-CUKSeTXC.js → settings-store-C2QdOH-t.js} +23 -4
- package/dist/slack-connector-BU86fIge.js +359 -0
- package/dist/slack-event-processor-BhCf5Wiy.d.ts +95 -0
- package/dist/slack-event-processor-xFDG3US0.js +176 -0
- package/dist/slot-fields-D-pvMgTK.js +249 -0
- package/dist/{memory-diagnostic-log-CI60kNfB.js → sqlite-diagnostic-log-DOTPW-tG.js} +373 -249
- package/dist/{yaml-render-93pX7EF7.js → yaml-render--J1_3BSA.js} +25 -21
- package/package.json +2 -4
- package/dist/discord-connector-BL36yvbL.js +0 -250
- package/dist/gateway-base-url-Dy4Ykuoh.js +0 -14
- package/dist/gh-connector-DpiixfQZ.js +0 -226
- package/dist/http-client-oICicjuO.d.ts +0 -18
- package/dist/memory-token-prompter-B4sjyaAq.d.ts +0 -57
- package/dist/memory-token-prompter-CZde7e6y.js +0 -61
- package/dist/node-file-system-Blr8pAir.js +0 -48
- package/dist/settings-reader-BIFB_j2f.d.ts +0 -18
- package/dist/slack-connector-DQIFPdBF.js +0 -484
- package/dist/slot-fields-CMoRpwuy.js +0 -45
- /package/dist/{connector-adapter-DU9Rvyec.js → connector-adapter-Dvs8N7ew.js} +0 -0
- /package/dist/{connector-listener-DR3aKOuK.js → connector-listener-mPGZYa8e.js} +0 -0
- /package/dist/{diagnostic-sql-reader-C9zR-Csp.js → diagnostic-sql-reader-oXZnWFf_.js} +0 -0
- /package/dist/{discord-connector-schema-B_N6IXLz.js → discord-connector-schema-B4YpWpR3.js} +0 -0
- /package/dist/{error-message-of-Byi4y0Uf.js → error-message-of-ColuYmAk.js} +0 -0
- /package/dist/{funnel-log-sqlite-sink-kqJbx2H7.js → funnel-log-sqlite-sink-DLYkY0pZ.js} +0 -0
- /package/dist/{funnel-recovery-BFdPjL6Z.js → funnel-recovery-DKnEutUS.js} +0 -0
- /package/dist/{node-http-client-lowp60Oa.js → node-http-client-u00atiKx.js} +0 -0
- /package/dist/{schedule-connector-schema-CfyuMCMh.js → schedule-connector-schema-DKEPZnVv.js} +0 -0
- /package/dist/{settings-reader-CtQ-Ix8_.js → settings-reader-9FcX3qS1.js} +0 -0
- /package/dist/{settings-schema-D1xcOqRu.d.ts → settings-schema-BL_c2Udm.d.ts} +0 -0
- /package/dist/{slack-connector-schema-C1zEf4TG.js → slack-connector-schema-Dem8to4P.js} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { t as NodeFunnelFileSystem } from "./node-file-system-
|
|
2
|
-
import { t as
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { t as NodeFunnelFileSystem } from "./node-file-system-BOXIHW_Q.js";
|
|
2
|
+
import { t as errorMessageOf } from "./error-message-of-ColuYmAk.js";
|
|
3
|
+
import { t as FunnelConnectorListener } from "./connector-listener-mPGZYa8e.js";
|
|
4
|
+
import { n as scheduleConnectorSchema, r as scheduleEntrySchema } from "./schedule-connector-schema-DKEPZnVv.js";
|
|
5
|
+
import { t as FunnelConnectorDiagnosticsRecorder } from "./connector-diagnostics-recorder-COtNEmUp.js";
|
|
5
6
|
import { dirname, join } from "node:path";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
//#region lib/engine/connectors/match-cron.ts
|
|
@@ -87,7 +88,7 @@ const defaultFs = new NodeFunnelFileSystem();
|
|
|
87
88
|
* connectorDir) so this store does not know about the funnel directory layout
|
|
88
89
|
* (`channels/<id>/connectors/<id>/state.json` lives outside this class).
|
|
89
90
|
*/
|
|
90
|
-
var
|
|
91
|
+
var FunnelScheduleStateStore = class {
|
|
91
92
|
path;
|
|
92
93
|
fs;
|
|
93
94
|
constructor(deps) {
|
|
@@ -116,50 +117,75 @@ const MAX_CATCHUP_MINUTES = 1440;
|
|
|
116
117
|
var FunnelScheduleListener = class extends FunnelConnectorListener {
|
|
117
118
|
config;
|
|
118
119
|
lastFiredStore;
|
|
119
|
-
channelId;
|
|
120
120
|
logger;
|
|
121
|
-
|
|
121
|
+
diagnostics;
|
|
122
122
|
now;
|
|
123
123
|
onFired;
|
|
124
124
|
timer = null;
|
|
125
125
|
stopped = false;
|
|
126
|
+
tickScheduled = false;
|
|
126
127
|
constructor(deps) {
|
|
127
128
|
super();
|
|
128
129
|
this.config = deps.config;
|
|
129
130
|
this.lastFiredStore = deps.lastFiredStore;
|
|
130
|
-
this.channelId = deps.channelId ?? null;
|
|
131
131
|
this.logger = deps.logger;
|
|
132
|
-
this.
|
|
132
|
+
this.diagnostics = new FunnelConnectorDiagnosticsRecorder({
|
|
133
|
+
type: "schedule",
|
|
134
|
+
connectorId: deps.config.id,
|
|
135
|
+
channelId: deps.channelId ?? null,
|
|
136
|
+
log: deps.diagnosticLog
|
|
137
|
+
});
|
|
133
138
|
this.now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
134
139
|
this.onFired = deps.onFired ?? null;
|
|
135
140
|
}
|
|
136
141
|
async start(notify) {
|
|
137
142
|
this.stopped = false;
|
|
138
|
-
this.
|
|
143
|
+
this.tickScheduled = true;
|
|
144
|
+
this.diagnostics.recordConnection("started", "");
|
|
139
145
|
const scheduleNext = () => {
|
|
140
146
|
if (this.stopped) return;
|
|
141
147
|
const date = this.now();
|
|
142
148
|
const msUntilNextMinute = 6e4 - (date.getSeconds() * 1e3 + date.getMilliseconds());
|
|
143
149
|
this.timer = setTimeout(async () => {
|
|
144
150
|
if (this.stopped) return;
|
|
145
|
-
|
|
151
|
+
try {
|
|
152
|
+
await this.tick(notify);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this.recordTickError(error);
|
|
155
|
+
}
|
|
146
156
|
scheduleNext();
|
|
147
157
|
}, msUntilNextMinute);
|
|
148
158
|
this.timer.unref();
|
|
159
|
+
this.tickScheduled = true;
|
|
149
160
|
};
|
|
150
|
-
|
|
161
|
+
try {
|
|
162
|
+
await this.tick(notify);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
this.recordTickError(error);
|
|
165
|
+
}
|
|
166
|
+
this.diagnostics.recordConnection("connected", "");
|
|
151
167
|
scheduleNext();
|
|
152
168
|
}
|
|
153
169
|
async stop() {
|
|
154
170
|
this.stopped = true;
|
|
171
|
+
this.tickScheduled = false;
|
|
155
172
|
if (this.timer) {
|
|
156
173
|
clearTimeout(this.timer);
|
|
157
174
|
this.timer = null;
|
|
158
175
|
}
|
|
159
|
-
this.recordConnection("
|
|
176
|
+
this.diagnostics.recordConnection("disconnected", "");
|
|
177
|
+
this.diagnostics.recordConnection("stopped", "");
|
|
178
|
+
}
|
|
179
|
+
recordTickError(error) {
|
|
180
|
+
const message = errorMessageOf(error);
|
|
181
|
+
this.diagnostics.recordConnection("error", `tick: ${message}`);
|
|
182
|
+
this.logger?.error("schedule tick failed", {
|
|
183
|
+
connector: this.config.name,
|
|
184
|
+
error: message
|
|
185
|
+
});
|
|
160
186
|
}
|
|
161
187
|
isAlive() {
|
|
162
|
-
return !this.stopped && this.
|
|
188
|
+
return !this.stopped && this.tickScheduled;
|
|
163
189
|
}
|
|
164
190
|
async tick(notify) {
|
|
165
191
|
const now = this.truncateToMinute(this.now());
|
|
@@ -209,14 +235,20 @@ var FunnelScheduleListener = class extends FunnelConnectorListener {
|
|
|
209
235
|
};
|
|
210
236
|
if (catchup) meta.catchup = "true";
|
|
211
237
|
const eventId = `${entry.id}@${firedAt.toISOString()}`;
|
|
212
|
-
this.recordRaw(eventId,
|
|
238
|
+
this.diagnostics.recordRaw(eventId, JSON.stringify({
|
|
239
|
+
schedule_id: entry.id,
|
|
240
|
+
cron: entry.cron,
|
|
241
|
+
prompt: entry.prompt,
|
|
242
|
+
fired_at: firedAt.toISOString(),
|
|
243
|
+
catchup
|
|
244
|
+
}));
|
|
213
245
|
try {
|
|
214
246
|
await notify(entry.prompt, meta);
|
|
215
247
|
} catch (error) {
|
|
216
|
-
this.recordProcessed(eventId,
|
|
248
|
+
this.diagnostics.recordProcessed(eventId, "emitted:delivery-failed", entry.prompt);
|
|
217
249
|
throw error;
|
|
218
250
|
}
|
|
219
|
-
this.recordProcessed(eventId,
|
|
251
|
+
this.diagnostics.recordProcessed(eventId, "emitted", entry.prompt);
|
|
220
252
|
if (this.onFired) try {
|
|
221
253
|
await this.onFired(entry, firedAt);
|
|
222
254
|
} catch (error) {
|
|
@@ -263,7 +295,7 @@ var FunnelScheduleListener = class extends FunnelConnectorListener {
|
|
|
263
295
|
}
|
|
264
296
|
logInvalidCron(entry, error) {
|
|
265
297
|
const message = errorMessageOf(error);
|
|
266
|
-
this.recordConnection("error", `invalid cron "${entry.cron}" (entry ${entry.id}): ${message}`);
|
|
298
|
+
this.diagnostics.recordConnection("error", `invalid cron "${entry.cron}" (entry ${entry.id}): ${message}`);
|
|
267
299
|
this.logger?.error("invalid cron expression in schedule", {
|
|
268
300
|
connector: this.config.name,
|
|
269
301
|
id: entry.id,
|
|
@@ -276,40 +308,6 @@ var FunnelScheduleListener = class extends FunnelConnectorListener {
|
|
|
276
308
|
copy.setSeconds(0, 0);
|
|
277
309
|
return copy;
|
|
278
310
|
}
|
|
279
|
-
recordRaw(eventId, entry, firedAt, catchup) {
|
|
280
|
-
this.diagnosticLog?.recordRaw({
|
|
281
|
-
eventId,
|
|
282
|
-
type: "schedule",
|
|
283
|
-
connectorId: this.config.id,
|
|
284
|
-
channelId: this.channelId,
|
|
285
|
-
payload: JSON.stringify({
|
|
286
|
-
schedule_id: entry.id,
|
|
287
|
-
cron: entry.cron,
|
|
288
|
-
prompt: entry.prompt,
|
|
289
|
-
fired_at: firedAt.toISOString(),
|
|
290
|
-
catchup
|
|
291
|
-
})
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
recordProcessed(eventId, entry, outcome) {
|
|
295
|
-
this.diagnosticLog?.recordProcessed({
|
|
296
|
-
eventId,
|
|
297
|
-
type: "schedule",
|
|
298
|
-
connectorId: this.config.id,
|
|
299
|
-
channelId: this.channelId,
|
|
300
|
-
outcome,
|
|
301
|
-
payload: entry.prompt
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
recordConnection(status, detail) {
|
|
305
|
-
this.diagnosticLog?.recordConnection({
|
|
306
|
-
type: "schedule",
|
|
307
|
-
connectorId: this.config.id,
|
|
308
|
-
channelId: this.channelId,
|
|
309
|
-
status,
|
|
310
|
-
detail
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
311
|
};
|
|
314
312
|
//#endregion
|
|
315
313
|
//#region lib/engine/connectors/schedule-connector.ts
|
|
@@ -335,14 +333,15 @@ const scheduleConnector = (options = {}) => ({
|
|
|
335
333
|
const parsed = scheduleConnectorSchema.parse(config);
|
|
336
334
|
return new FunnelScheduleListener({
|
|
337
335
|
config: parsed,
|
|
338
|
-
lastFiredStore: new
|
|
336
|
+
lastFiredStore: new FunnelScheduleStateStore({
|
|
339
337
|
path: join(deps.connectorDir(deps.channelId, parsed.id), "state.json"),
|
|
340
338
|
fs: deps.fs
|
|
341
339
|
}),
|
|
342
340
|
channelId: deps.channelId,
|
|
343
341
|
logger: deps.logger,
|
|
344
342
|
diagnosticLog: deps.diagnosticLog,
|
|
345
|
-
onFired: options.onFired
|
|
343
|
+
onFired: options.onFired,
|
|
344
|
+
now: () => deps.clock.now()
|
|
346
345
|
});
|
|
347
346
|
},
|
|
348
347
|
createAdapter: null,
|
|
@@ -409,4 +408,4 @@ const scheduleConnector = (options = {}) => ({
|
|
|
409
408
|
}
|
|
410
409
|
});
|
|
411
410
|
//#endregion
|
|
412
|
-
export { matchCron as i, FunnelScheduleListener as n,
|
|
411
|
+
export { matchCron as i, FunnelScheduleListener as n, FunnelScheduleStateStore as r, scheduleConnector as t };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region lib/engine/connectors/schedule-connector-schema.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Catch-up behavior when the daemon was down past one or more matching minutes.
|
|
6
|
+
*
|
|
7
|
+
* - `latest`: fire once with the most recent missed match (default; preserves prior behavior).
|
|
8
|
+
* - `all`: fire once per missed minute, oldest first (capped at 24 h).
|
|
9
|
+
* - `skip`: never fire missed matches; only fire when the current minute matches.
|
|
10
|
+
*/
|
|
11
|
+
declare const scheduleCatchupPolicySchema: z.ZodEnum<{
|
|
12
|
+
latest: "latest";
|
|
13
|
+
all: "all";
|
|
14
|
+
skip: "skip";
|
|
15
|
+
}>;
|
|
16
|
+
type ScheduleCatchupPolicy = z.infer<typeof scheduleCatchupPolicySchema>;
|
|
17
|
+
declare const scheduleEntrySchema: z.ZodObject<{
|
|
18
|
+
id: z.ZodString;
|
|
19
|
+
cron: z.ZodString;
|
|
20
|
+
prompt: z.ZodString;
|
|
21
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
22
|
+
catchupPolicy: z.ZodDefault<z.ZodEnum<{
|
|
23
|
+
latest: "latest";
|
|
24
|
+
all: "all";
|
|
25
|
+
skip: "skip";
|
|
26
|
+
}>>;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
type ScheduleEntry = z.infer<typeof scheduleEntrySchema>;
|
|
29
|
+
declare const scheduleConnectorSchema: z.ZodObject<{
|
|
30
|
+
id: z.ZodString;
|
|
31
|
+
name: z.ZodString;
|
|
32
|
+
type: z.ZodLiteral<"schedule">;
|
|
33
|
+
entries: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
34
|
+
id: z.ZodString;
|
|
35
|
+
cron: z.ZodString;
|
|
36
|
+
prompt: z.ZodString;
|
|
37
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
38
|
+
catchupPolicy: z.ZodDefault<z.ZodEnum<{
|
|
39
|
+
latest: "latest";
|
|
40
|
+
all: "all";
|
|
41
|
+
skip: "skip";
|
|
42
|
+
}>>;
|
|
43
|
+
}, z.core.$strip>>>;
|
|
44
|
+
createdAt: z.ZodOptional<z.ZodString>;
|
|
45
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
type ScheduleConnectorConfig = z.infer<typeof scheduleConnectorSchema>;
|
|
48
|
+
//#endregion
|
|
49
|
+
export { scheduleConnectorSchema as a, scheduleCatchupPolicySchema as i, ScheduleConnectorConfig as n, scheduleEntrySchema as o, ScheduleEntry as r, ScheduleCatchupPolicy as t };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { a as Settings } from "./settings-schema-BL_c2Udm.js";
|
|
2
|
+
|
|
3
|
+
//#region lib/engine/id/id-generator.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* ID generator boundary. Default NodeFunnelIdGenerator wraps `crypto.randomUUID()`;
|
|
6
|
+
* MemoryFunnelIdGenerator emits `<prefix>-1, <prefix>-2, ...` for deterministic tests.
|
|
7
|
+
*/
|
|
8
|
+
declare abstract class FunnelIdGenerator {
|
|
9
|
+
abstract generate(): string;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region lib/engine/settings/settings-reader.d.ts
|
|
13
|
+
declare abstract class FunnelSettingsReader {
|
|
14
|
+
abstract read(): Settings;
|
|
15
|
+
abstract write(settings: Settings): void;
|
|
16
|
+
/**
|
|
17
|
+
* Atomic read-modify-write. Implementations must serialize against
|
|
18
|
+
* concurrent processes touching the same file (the Node store does so via
|
|
19
|
+
* an exclusive lockfile; Memory stores are single-threaded). Engine
|
|
20
|
+
* classes must use `update` for any mutation that depends on prior state,
|
|
21
|
+
* otherwise a concurrent CLI invocation or `fnl claude` launch can lose
|
|
22
|
+
* the edit through a read-modify-write race.
|
|
23
|
+
*/
|
|
24
|
+
abstract update<T>(mutator: (settings: Settings) => T): T;
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { FunnelIdGenerator as n, FunnelSettingsReader as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { t as NodeFunnelFileSystem } from "./node-file-system-
|
|
2
|
-
import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-
|
|
1
|
+
import { t as NodeFunnelFileSystem } from "./node-file-system-BOXIHW_Q.js";
|
|
2
|
+
import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-9FcX3qS1.js";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { z } from "zod";
|
|
@@ -29,11 +29,11 @@ const baseConnectorConfigSchema = z.object({
|
|
|
29
29
|
//#region lib/engine/settings/settings-schema.ts
|
|
30
30
|
/**
|
|
31
31
|
* Connectors are stored loosely here: settings validates only the common base
|
|
32
|
-
* fields and preserves every type-specific key verbatim (`.
|
|
32
|
+
* fields and preserves every type-specific key verbatim (`.loose()`).
|
|
33
33
|
* Core does not enumerate connector types, so strict per-type validation happens
|
|
34
34
|
* at the registry/descriptor layer (CRUD time), not on every settings read.
|
|
35
35
|
*/
|
|
36
|
-
const storedConnectorSchema = baseConnectorConfigSchema.
|
|
36
|
+
const storedConnectorSchema = baseConnectorConfigSchema.loose();
|
|
37
37
|
/**
|
|
38
38
|
* Routing mode when multiple WS clients are subscribed to the same channel.
|
|
39
39
|
*
|
|
@@ -192,6 +192,25 @@ var FunnelSettingsStore = class extends FunnelSettingsReader {
|
|
|
192
192
|
};
|
|
193
193
|
this.fs.writeSecretFileSync(this.path, `${JSON.stringify(versioned, null, 2)}\n`);
|
|
194
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Run `mutator` against a freshly-read settings object inside an exclusive
|
|
197
|
+
* file lock, then persist the result. Use this instead of bare `read()` +
|
|
198
|
+
* `write()` for any logical edit (add channel, set token, rename profile),
|
|
199
|
+
* so two concurrent CLI invocations or `fnl claude` launches cannot lose
|
|
200
|
+
* each other's updates via a read-modify-write race. The mutator may
|
|
201
|
+
* mutate `settings` in place and/or return a value; the value is returned
|
|
202
|
+
* to the caller. A thrown error from the mutator skips the write but still
|
|
203
|
+
* releases the lock.
|
|
204
|
+
*/
|
|
205
|
+
update(mutator) {
|
|
206
|
+
this.fs.mkdirSync(dirname(this.path), { recursive: true });
|
|
207
|
+
return this.fs.withFileLock(`${this.path}.lock`, () => {
|
|
208
|
+
const settings = this.read();
|
|
209
|
+
const result = mutator(settings);
|
|
210
|
+
this.write(settings);
|
|
211
|
+
return result;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
195
214
|
};
|
|
196
215
|
//#endregion
|
|
197
216
|
export { resolveFunnelDir as a, channelConfigSchema as c, settingsSchema as d, baseConnectorConfigSchema as f, SETTINGS_PATH as i, channelDeliveryModeSchema as l, FUNNEL_DIR as n, resolveFunnelPort as o, NodeFunnelIdGenerator as p, FunnelSettingsStore as r, SETTINGS_VERSION as s, DEFAULT_GATEWAY_PORT as t, profileConfigSchema as u };
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { t as NodeFunnelHttpClient } from "./node-http-client-u00atiKx.js";
|
|
2
|
+
import { t as FunnelAuthFailedError } from "./funnel-error-0t1MK1R6.js";
|
|
3
|
+
import { t as errorMessageOf } from "./error-message-of-ColuYmAk.js";
|
|
4
|
+
import { t as slackConnectorSchema } from "./slack-connector-schema-Dem8to4P.js";
|
|
5
|
+
import { t as FunnelConnectorAdapter } from "./connector-adapter-Dvs8N7ew.js";
|
|
6
|
+
import { t as FunnelSlackEventProcessor } from "./slack-event-processor-xFDG3US0.js";
|
|
7
|
+
import { t as resolveConnectorToken } from "./resolve-connector-token-DxDG9mhf.js";
|
|
8
|
+
import { i as resolveFlumeDeps, n as FunnelFlumeSourceListener, r as flumeLogHandler, t as slotFields } from "./slot-fields-D-pvMgTK.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { FlumeSlackSource } from "@interactive-inc/flume/slack";
|
|
11
|
+
//#region lib/engine/connectors/slack-adapter.ts
|
|
12
|
+
const SLACK_API_BASE = "https://slack.com/api/";
|
|
13
|
+
const toRecord = (value) => {
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const [key, val] of Object.entries(value)) result[key] = val;
|
|
16
|
+
return result;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Slack Web API adapter over the injected `FunnelHttpClient`. `call()` posts
|
|
20
|
+
* to `https://slack.com/api/<method>` with `Authorization: Bearer <botToken>`
|
|
21
|
+
* and returns the parsed JSON body verbatim — Slack signals failures with
|
|
22
|
+
* `{ ok: false, error: "..." }` in a 200 response, so we surface that body
|
|
23
|
+
* unchanged and let the caller inspect `ok`.
|
|
24
|
+
*/
|
|
25
|
+
var FunnelSlackAdapter = class extends FunnelConnectorAdapter {
|
|
26
|
+
token;
|
|
27
|
+
http;
|
|
28
|
+
constructor(deps) {
|
|
29
|
+
super();
|
|
30
|
+
this.token = resolveConnectorToken({
|
|
31
|
+
literal: deps.config.botToken,
|
|
32
|
+
envVar: deps.config.botTokenEnv,
|
|
33
|
+
env: deps.env ?? process.env,
|
|
34
|
+
label: `${deps.config.name}.botToken`
|
|
35
|
+
});
|
|
36
|
+
this.http = deps.http ?? new NodeFunnelHttpClient();
|
|
37
|
+
Object.freeze(this);
|
|
38
|
+
}
|
|
39
|
+
async call(input) {
|
|
40
|
+
const url = `${SLACK_API_BASE}${input.path}`;
|
|
41
|
+
const body = input.body !== null && typeof input.body === "object" ? toRecord(input.body) : {};
|
|
42
|
+
const form = new URLSearchParams();
|
|
43
|
+
for (const [key, value] of Object.entries(body)) form.set(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
44
|
+
const text = await (await this.http.fetch({
|
|
45
|
+
method: "POST",
|
|
46
|
+
url,
|
|
47
|
+
headers: {
|
|
48
|
+
Authorization: `Bearer ${this.token}`,
|
|
49
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
50
|
+
},
|
|
51
|
+
body: form.toString()
|
|
52
|
+
})).text();
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(text);
|
|
55
|
+
} catch {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: `non-JSON response: ${text.slice(0, 200)}`
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async postMessage(props) {
|
|
63
|
+
return this.call({
|
|
64
|
+
method: "post",
|
|
65
|
+
path: "chat.postMessage",
|
|
66
|
+
body: {
|
|
67
|
+
channel: props.channel,
|
|
68
|
+
text: props.text,
|
|
69
|
+
...props.threadTs ? { thread_ts: props.threadTs } : {}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async addReaction(props) {
|
|
74
|
+
return this.call({
|
|
75
|
+
method: "post",
|
|
76
|
+
path: "reactions.add",
|
|
77
|
+
body: {
|
|
78
|
+
channel: props.channel,
|
|
79
|
+
timestamp: props.timestamp,
|
|
80
|
+
name: props.name
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async removeReaction(props) {
|
|
85
|
+
return this.call({
|
|
86
|
+
method: "post",
|
|
87
|
+
path: "reactions.remove",
|
|
88
|
+
body: {
|
|
89
|
+
channel: props.channel,
|
|
90
|
+
timestamp: props.timestamp,
|
|
91
|
+
name: props.name
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region lib/engine/connectors/slack-flume-listener.ts
|
|
98
|
+
const authTestResponseSchema = z.object({
|
|
99
|
+
ok: z.boolean(),
|
|
100
|
+
user_id: z.string().optional(),
|
|
101
|
+
bot_id: z.string().optional(),
|
|
102
|
+
error: z.string().optional()
|
|
103
|
+
});
|
|
104
|
+
const AUTH_TEST_URL = "https://slack.com/api/auth.test";
|
|
105
|
+
/**
|
|
106
|
+
* Slack listener backed by `@interactive-inc/flume`'s `FlumeSlackSource` (raw
|
|
107
|
+
* Socket Mode WebSocket + Zod). The processor layer
|
|
108
|
+
* (`FunnelSlackEventProcessor`) is the application layer — self-skip, mention
|
|
109
|
+
* detection, dedup, minify. Self-detection needs `auth.test` to learn the
|
|
110
|
+
* bot's own user/bot id, which the listener calls once at start using the
|
|
111
|
+
* bot token. Flume delivers the events API envelope and nothing else; Bolt's
|
|
112
|
+
* `app.action` / `app.command` / `preprocessEvent` hooks have no equivalent
|
|
113
|
+
* here and must be re-implemented against Slack's HTTP endpoints if needed.
|
|
114
|
+
*/
|
|
115
|
+
var FunnelFlumeSlackListener = class extends FunnelFlumeSourceListener {
|
|
116
|
+
config;
|
|
117
|
+
env;
|
|
118
|
+
flumeDeps;
|
|
119
|
+
http;
|
|
120
|
+
signal;
|
|
121
|
+
preprocessEvent;
|
|
122
|
+
processor = null;
|
|
123
|
+
botToken = "";
|
|
124
|
+
constructor(deps) {
|
|
125
|
+
super({
|
|
126
|
+
type: "slack",
|
|
127
|
+
connectorId: deps.config.id,
|
|
128
|
+
channelId: deps.channelId ?? null,
|
|
129
|
+
logger: deps.logger,
|
|
130
|
+
diagnosticLog: deps.diagnosticLog
|
|
131
|
+
});
|
|
132
|
+
this.config = deps.config;
|
|
133
|
+
this.env = deps.env ?? process.env;
|
|
134
|
+
this.flumeDeps = deps.flumeDeps ?? {};
|
|
135
|
+
this.http = deps.http ?? new NodeFunnelHttpClient();
|
|
136
|
+
this.signal = deps.signal;
|
|
137
|
+
this.preprocessEvent = deps.preprocessEvent;
|
|
138
|
+
}
|
|
139
|
+
async start(notify) {
|
|
140
|
+
this.diagnostics.recordConnection("started", "");
|
|
141
|
+
let appToken;
|
|
142
|
+
let botToken;
|
|
143
|
+
try {
|
|
144
|
+
appToken = resolveConnectorToken({
|
|
145
|
+
literal: this.config.appToken,
|
|
146
|
+
envVar: this.config.appTokenEnv,
|
|
147
|
+
env: this.env,
|
|
148
|
+
label: `${this.config.name}.appToken`
|
|
149
|
+
});
|
|
150
|
+
botToken = resolveConnectorToken({
|
|
151
|
+
literal: this.config.botToken,
|
|
152
|
+
envVar: this.config.botTokenEnv,
|
|
153
|
+
env: this.env,
|
|
154
|
+
label: `${this.config.name}.botToken`
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
this.diagnostics.recordConnection("auth-failed", errorMessageOf(error));
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
this.botToken = botToken;
|
|
161
|
+
const auth = await this.callAuthTest();
|
|
162
|
+
if (!auth.ok) {
|
|
163
|
+
const detail = auth.error ?? "auth.test returned ok=false";
|
|
164
|
+
this.diagnostics.recordConnection("auth-failed", detail);
|
|
165
|
+
throw new FunnelAuthFailedError(this.config.name, detail);
|
|
166
|
+
}
|
|
167
|
+
this.processor = new FunnelSlackEventProcessor({
|
|
168
|
+
ownBotUserId: auth.user_id ?? "",
|
|
169
|
+
ownBotId: auth.bot_id ?? "",
|
|
170
|
+
minify: this.config.minify
|
|
171
|
+
});
|
|
172
|
+
const source = new FlumeSlackSource({
|
|
173
|
+
appToken,
|
|
174
|
+
botToken: this.botToken
|
|
175
|
+
});
|
|
176
|
+
await this.runStart({
|
|
177
|
+
source,
|
|
178
|
+
onLog: flumeLogHandler(this.logger),
|
|
179
|
+
deps: resolveFlumeDeps(this.flumeDeps),
|
|
180
|
+
signal: this.signal,
|
|
181
|
+
onEvent: (event) => {
|
|
182
|
+
if (event.source !== "slack") return Promise.resolve();
|
|
183
|
+
return this.handleEvent(event, notify);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
onStop() {
|
|
188
|
+
this.processor = null;
|
|
189
|
+
}
|
|
190
|
+
async callAuthTest() {
|
|
191
|
+
let text;
|
|
192
|
+
try {
|
|
193
|
+
text = await (await this.http.fetch({
|
|
194
|
+
method: "POST",
|
|
195
|
+
url: AUTH_TEST_URL,
|
|
196
|
+
headers: {
|
|
197
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
198
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
199
|
+
}
|
|
200
|
+
})).text();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
this.diagnostics.recordConnection("auth-failed", errorMessageOf(error));
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
const parsed = authTestResponseSchema.safeParse(safeJsonParse(text));
|
|
206
|
+
if (!parsed.success) return {
|
|
207
|
+
ok: false,
|
|
208
|
+
error: `non-JSON auth.test response: ${text.slice(0, 200)}`
|
|
209
|
+
};
|
|
210
|
+
return parsed.data;
|
|
211
|
+
}
|
|
212
|
+
async handleEvent(event, notify) {
|
|
213
|
+
if (!this.processor) return;
|
|
214
|
+
const rawEvent = event.data.event;
|
|
215
|
+
if (!isSlackRawEvent(rawEvent)) {
|
|
216
|
+
const skipId = crypto.randomUUID();
|
|
217
|
+
this.diagnostics.recordRaw(skipId, JSON.stringify(event.data));
|
|
218
|
+
this.diagnostics.recordProcessed(skipId, "skip:non-object-event", "");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const eventId = crypto.randomUUID();
|
|
222
|
+
const rawJson = JSON.stringify(rawEvent);
|
|
223
|
+
this.diagnostics.recordRaw(eventId, rawJson);
|
|
224
|
+
let preprocessed = rawEvent;
|
|
225
|
+
if (this.preprocessEvent) {
|
|
226
|
+
const next = await this.preprocessEvent(rawEvent);
|
|
227
|
+
if (next === null) {
|
|
228
|
+
this.diagnostics.recordProcessed(eventId, "skip:preprocess", rawJson);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
preprocessed = next;
|
|
232
|
+
}
|
|
233
|
+
const result = this.processor.process(preprocessed);
|
|
234
|
+
if (result.skip) {
|
|
235
|
+
this.diagnostics.recordProcessed(eventId, result.reason, rawJson);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
await this.deliver(notify, eventId, rawJson, result.content, result.meta, result.shouldReact);
|
|
239
|
+
}
|
|
240
|
+
async deliver(notify, eventId, rawJson, content, meta, shouldReact) {
|
|
241
|
+
try {
|
|
242
|
+
await notify(content, meta);
|
|
243
|
+
} catch (error) {
|
|
244
|
+
this.diagnostics.recordProcessed(eventId, "emitted:delivery-failed", content || rawJson);
|
|
245
|
+
this.logger?.error("slack notify error", { error: errorMessageOf(error) });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
this.diagnostics.recordProcessed(eventId, "emitted", content);
|
|
249
|
+
if (shouldReact) this.postReaction(meta).catch((error) => {
|
|
250
|
+
this.diagnostics.recordProcessed(eventId, "emitted:reaction-failed", errorMessageOf(error));
|
|
251
|
+
this.logger?.warn("slack reaction failed", { error: errorMessageOf(error) });
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async postReaction(meta) {
|
|
255
|
+
const res = await this.http.fetch({
|
|
256
|
+
method: "POST",
|
|
257
|
+
url: "https://slack.com/api/reactions.add",
|
|
258
|
+
headers: {
|
|
259
|
+
Authorization: `Bearer ${this.botToken}`,
|
|
260
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
261
|
+
},
|
|
262
|
+
body: new URLSearchParams({
|
|
263
|
+
channel: meta.channel_id ?? "",
|
|
264
|
+
timestamp: meta.thread_ts ?? "",
|
|
265
|
+
name: "eyes"
|
|
266
|
+
}).toString()
|
|
267
|
+
});
|
|
268
|
+
const parsed = parseSlackResponse(await res.text());
|
|
269
|
+
if (!parsed.ok) throw new Error(`slack reactions.add: ${parsed.error ?? `status=${res.status}`}`);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const isSlackRawEvent = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
273
|
+
const safeJsonParse = (text) => {
|
|
274
|
+
try {
|
|
275
|
+
return JSON.parse(text);
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const slackResponseSchema = z.object({
|
|
281
|
+
ok: z.boolean(),
|
|
282
|
+
error: z.string().optional()
|
|
283
|
+
});
|
|
284
|
+
const parseSlackResponse = (text) => {
|
|
285
|
+
const parsed = slackResponseSchema.safeParse(safeJsonParse(text));
|
|
286
|
+
if (!parsed.success) return {
|
|
287
|
+
ok: false,
|
|
288
|
+
error: `non-JSON response: ${text.slice(0, 200)}`
|
|
289
|
+
};
|
|
290
|
+
return parsed.data;
|
|
291
|
+
};
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region lib/engine/connectors/slack-connector.ts
|
|
294
|
+
/**
|
|
295
|
+
* Slack connector descriptor. Pass `slackConnector()` to
|
|
296
|
+
* `new Funnel({ connectors: [...] })` to enable the type.
|
|
297
|
+
*
|
|
298
|
+
* The listener is backed by `@interactive-inc/flume`'s `FlumeSlackSource`
|
|
299
|
+
* (raw Socket Mode WebSocket). Only the events API envelope is delivered —
|
|
300
|
+
* there is no equivalent for the Bolt-style `app.action` / `app.command`
|
|
301
|
+
* dispatch. For HTTP-side interactivity (buttons, slash commands), run a
|
|
302
|
+
* separate Bolt app outside funnel; this descriptor only handles the
|
|
303
|
+
* incoming events firehose.
|
|
304
|
+
*/
|
|
305
|
+
const slackConnector = (options = {}) => ({
|
|
306
|
+
type: "slack",
|
|
307
|
+
toolExposed: true,
|
|
308
|
+
createListener(config, deps) {
|
|
309
|
+
return new FunnelFlumeSlackListener({
|
|
310
|
+
config: slackConnectorSchema.parse(config),
|
|
311
|
+
channelId: deps.channelId,
|
|
312
|
+
logger: deps.logger,
|
|
313
|
+
diagnosticLog: deps.diagnosticLog,
|
|
314
|
+
http: deps.http,
|
|
315
|
+
signal: deps.signal,
|
|
316
|
+
preprocessEvent: options.preprocessEvent
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
createAdapter(config, deps) {
|
|
320
|
+
return new FunnelSlackAdapter({
|
|
321
|
+
config: slackConnectorSchema.parse(config),
|
|
322
|
+
http: deps.http
|
|
323
|
+
});
|
|
324
|
+
},
|
|
325
|
+
secretTokens(config) {
|
|
326
|
+
const parsed = slackConnectorSchema.parse(config);
|
|
327
|
+
return [parsed.botToken, parsed.appToken].filter((token) => token !== void 0);
|
|
328
|
+
},
|
|
329
|
+
buildConfig(input, context) {
|
|
330
|
+
return slackConnectorSchema.parse({
|
|
331
|
+
id: context.id,
|
|
332
|
+
type: "slack",
|
|
333
|
+
name: input.name,
|
|
334
|
+
...typeof input.botToken === "string" ? { botToken: input.botToken } : {},
|
|
335
|
+
...typeof input.appToken === "string" ? { appToken: input.appToken } : {},
|
|
336
|
+
...typeof input.botTokenEnv === "string" ? { botTokenEnv: input.botTokenEnv } : {},
|
|
337
|
+
...typeof input.appTokenEnv === "string" ? { appTokenEnv: input.appTokenEnv } : {},
|
|
338
|
+
minify: typeof input.minify === "boolean" ? input.minify : true,
|
|
339
|
+
createdAt: context.now,
|
|
340
|
+
updatedAt: context.now
|
|
341
|
+
});
|
|
342
|
+
},
|
|
343
|
+
applyUpdate(config, fields, context) {
|
|
344
|
+
const current = slackConnectorSchema.parse(config);
|
|
345
|
+
return slackConnectorSchema.parse({
|
|
346
|
+
id: current.id,
|
|
347
|
+
name: current.name,
|
|
348
|
+
type: "slack",
|
|
349
|
+
minify: current.minify,
|
|
350
|
+
createdAt: current.createdAt,
|
|
351
|
+
updatedAt: context.now,
|
|
352
|
+
...slotFields("botToken", "botTokenEnv", fields, current),
|
|
353
|
+
...slotFields("appToken", "appTokenEnv", fields, current)
|
|
354
|
+
});
|
|
355
|
+
},
|
|
356
|
+
operations: {}
|
|
357
|
+
});
|
|
358
|
+
//#endregion
|
|
359
|
+
export { FunnelFlumeSlackListener as n, FunnelSlackAdapter as r, slackConnector as t };
|