@interactive-inc/claude-funnel 0.26.1 → 0.27.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/dist/bin.js +786 -717
- package/dist/connector-diagnostic-log-Clb2sCcz.d.ts +206 -0
- package/dist/connectors/discord.d.ts +16 -6
- package/dist/connectors/discord.js +1 -1
- package/dist/connectors/gh.d.ts +12 -5
- package/dist/connectors/gh.js +1 -1
- package/dist/connectors/schedule.d.ts +1 -1
- package/dist/connectors/schedule.js +1 -1
- package/dist/connectors/slack.d.ts +5 -4
- package/dist/connectors/slack.js +1 -1
- package/dist/{discord-connector-schema-Dww2I4zH.d.ts → discord-connector-schema-Df_McRJI.d.ts} +7 -1
- package/dist/{discord-connector-schema-CpuI6rmE.js → discord-connector-schema-RzDvrNE5.js} +81 -8
- package/dist/gateway/daemon.js +225 -225
- package/dist/{gh-connector-schema-CQRIvPpz.js → gh-connector-schema-eYE4g77K.js} +51 -3
- package/dist/index.d.ts +213 -38
- package/dist/index.js +727 -103
- package/dist/resolve-connector-token-Ch6XWMJM.js +22 -0
- package/dist/{schedule-connector-schema-CuCjP7z4.js → schedule-connector-schema-CM-sRkac.js} +53 -3
- package/dist/{schedule-listener-CBYF2bGZ.d.ts → schedule-listener-C2-KqHQc.d.ts} +10 -3
- package/dist/{slack-connector-schema-BWL7dWlY.js → slack-connector-schema-CHbRJHGp.js} +140 -19
- package/dist/slack-listener-BMknoyVr.d.ts +112 -0
- package/package.json +1 -1
- package/dist/logger-B3aXsVcX.d.ts +0 -33
- package/dist/slack-listener-DbNCPMqY.d.ts +0 -77
- /package/dist/{connector-adapter-CXB-q_XC.d.ts → connector-adapter-VA6undzc.d.ts} +0 -0
- /package/dist/{gh-connector-schema-Cmi57jvL.d.ts → gh-connector-schema-CQmEWzdV.d.ts} +0 -0
- /package/dist/{logger-D1A3_JXV.js → logger-Czli2OKh.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, t as discordConnectorSchema } from "./discord-connector-schema-
|
|
2
|
-
import { n as FunnelConnectorListener, t as FunnelLogger } from "./logger-
|
|
3
|
-
import { a as FunnelProcessRunner, i as NodeFunnelProcessRunner, n as FunnelGhListener, r as FunnelGhAdapter, t as ghConnectorSchema } from "./gh-connector-schema-
|
|
4
|
-
import { a as ScheduleStateStore, i as FunnelScheduleListener, n as scheduleConnectorSchema, o as NodeFunnelFileSystem, r as scheduleEntrySchema, s as FunnelFileSystem, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-
|
|
5
|
-
import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-
|
|
1
|
+
import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, t as discordConnectorSchema } from "./discord-connector-schema-RzDvrNE5.js";
|
|
2
|
+
import { n as FunnelConnectorListener, t as FunnelLogger } from "./logger-Czli2OKh.js";
|
|
3
|
+
import { a as FunnelProcessRunner, i as NodeFunnelProcessRunner, n as FunnelGhListener, r as FunnelGhAdapter, t as ghConnectorSchema } from "./gh-connector-schema-eYE4g77K.js";
|
|
4
|
+
import { a as ScheduleStateStore, i as FunnelScheduleListener, n as scheduleConnectorSchema, o as NodeFunnelFileSystem, r as scheduleEntrySchema, s as FunnelFileSystem, t as scheduleCatchupPolicySchema } from "./schedule-connector-schema-CM-sRkac.js";
|
|
5
|
+
import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-CHbRJHGp.js";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { homedir, tmpdir } from "node:os";
|
|
10
10
|
import { stderr, stdin } from "node:process";
|
|
@@ -104,6 +104,18 @@ const settingsSchema = z.object({
|
|
|
104
104
|
});
|
|
105
105
|
//#endregion
|
|
106
106
|
//#region lib/engine/settings/settings-store.ts
|
|
107
|
+
/**
|
|
108
|
+
* Resolves the funnel home dir. Defaults to `~/.funnel`, overridable via
|
|
109
|
+
* `FUNNEL_DIR` so a funnel.json-scoped launch can point everything (settings,
|
|
110
|
+
* gateway pid/token, claude pids) at a repo-local `<repo>/.funnel` and never
|
|
111
|
+
* touch the global home. Read at call time, not module load, so a daemon
|
|
112
|
+
* spawned with the env set resolves the override.
|
|
113
|
+
*/
|
|
114
|
+
function resolveFunnelDir() {
|
|
115
|
+
const override = process.env.FUNNEL_DIR;
|
|
116
|
+
if (override && override.length > 0) return override;
|
|
117
|
+
return join(homedir(), ".funnel");
|
|
118
|
+
}
|
|
107
119
|
const FUNNEL_DIR = join(homedir(), ".funnel");
|
|
108
120
|
const SETTINGS_PATH = join(FUNNEL_DIR, "settings.json");
|
|
109
121
|
const defaultFs$5 = new NodeFunnelFileSystem();
|
|
@@ -198,6 +210,7 @@ var FunnelConnectorFactory = class {
|
|
|
198
210
|
fs;
|
|
199
211
|
process;
|
|
200
212
|
logger;
|
|
213
|
+
diagnosticLog;
|
|
201
214
|
dir;
|
|
202
215
|
slackListenerOptions;
|
|
203
216
|
scheduleListenerOptions;
|
|
@@ -205,6 +218,7 @@ var FunnelConnectorFactory = class {
|
|
|
205
218
|
this.fs = deps.fs ?? defaultFs$4;
|
|
206
219
|
this.process = deps.process ?? defaultProcess$3;
|
|
207
220
|
this.logger = deps.logger;
|
|
221
|
+
this.diagnosticLog = deps.diagnosticLog;
|
|
208
222
|
this.dir = deps.dir ?? FUNNEL_DIR;
|
|
209
223
|
this.slackListenerOptions = deps.slackListenerOptions ?? {};
|
|
210
224
|
this.scheduleListenerOptions = deps.scheduleListenerOptions ?? {};
|
|
@@ -213,18 +227,24 @@ var FunnelConnectorFactory = class {
|
|
|
213
227
|
createListener(channelId, config) {
|
|
214
228
|
if (config.type === "slack") return new FunnelSlackListener({
|
|
215
229
|
config,
|
|
230
|
+
channelId,
|
|
216
231
|
logger: this.logger,
|
|
232
|
+
diagnosticLog: this.diagnosticLog,
|
|
217
233
|
onAppCreated: this.slackListenerOptions.onAppCreated,
|
|
218
234
|
preprocessEvent: this.slackListenerOptions.preprocessEvent
|
|
219
235
|
});
|
|
220
236
|
if (config.type === "gh") return new FunnelGhListener({
|
|
221
237
|
config,
|
|
238
|
+
channelId,
|
|
222
239
|
process: this.process,
|
|
223
|
-
logger: this.logger
|
|
240
|
+
logger: this.logger,
|
|
241
|
+
diagnosticLog: this.diagnosticLog
|
|
224
242
|
});
|
|
225
243
|
if (config.type === "discord") return new FunnelDiscordListener({
|
|
226
244
|
config,
|
|
227
|
-
|
|
245
|
+
channelId,
|
|
246
|
+
logger: this.logger,
|
|
247
|
+
diagnosticLog: this.diagnosticLog
|
|
228
248
|
});
|
|
229
249
|
return new FunnelScheduleListener({
|
|
230
250
|
config,
|
|
@@ -232,7 +252,9 @@ var FunnelConnectorFactory = class {
|
|
|
232
252
|
path: join(this.connectorDir(channelId, config.id), "state.json"),
|
|
233
253
|
fs: this.fs
|
|
234
254
|
}),
|
|
255
|
+
channelId,
|
|
235
256
|
logger: this.logger,
|
|
257
|
+
diagnosticLog: this.diagnosticLog,
|
|
236
258
|
onFired: this.scheduleListenerOptions.onFired
|
|
237
259
|
});
|
|
238
260
|
}
|
|
@@ -252,15 +274,17 @@ var FunnelConnectorFactory = class {
|
|
|
252
274
|
//#endregion
|
|
253
275
|
//#region lib/engine/channels/connector-tokens.ts
|
|
254
276
|
/**
|
|
255
|
-
* Return every secret token contained in a connector config. Used by
|
|
256
|
-
* collision detection at add/update time so the same Slack bot or
|
|
257
|
-
* bot cannot be registered under two connectors.
|
|
258
|
-
*
|
|
277
|
+
* Return every literal secret token contained in a connector config. Used by
|
|
278
|
+
* token collision detection at add/update time so the same Slack bot or
|
|
279
|
+
* Discord bot cannot be registered under two connectors. Connectors that hold
|
|
280
|
+
* an env *reference* instead of a literal contribute nothing here — two
|
|
281
|
+
* connectors naming the same env var is not a secret collision, and the secret
|
|
282
|
+
* is not in settings.json to compare anyway.
|
|
259
283
|
*/
|
|
260
284
|
function connectorTokens(connector) {
|
|
261
285
|
switch (connector.type) {
|
|
262
|
-
case "slack": return [connector.botToken, connector.appToken];
|
|
263
|
-
case "discord": return [connector.botToken];
|
|
286
|
+
case "slack": return [connector.botToken, connector.appToken].filter((token) => token !== void 0);
|
|
287
|
+
case "discord": return [connector.botToken].filter((token) => token !== void 0);
|
|
264
288
|
case "gh":
|
|
265
289
|
case "schedule": return [];
|
|
266
290
|
}
|
|
@@ -306,6 +330,26 @@ var NodeFunnelClock = class extends FunnelClock {
|
|
|
306
330
|
};
|
|
307
331
|
//#endregion
|
|
308
332
|
//#region lib/engine/channels/channels.ts
|
|
333
|
+
/**
|
|
334
|
+
* Resolves one token slot (e.g. botToken/botTokenEnv) for an update. The
|
|
335
|
+
* literal and the env-ref form are mutually exclusive: if `fields` supplies
|
|
336
|
+
* either, that form wins and the other key is omitted entirely; if it supplies
|
|
337
|
+
* neither, the connector's current slot is carried over unchanged. Returns a
|
|
338
|
+
* partial object spread into the rebuilt connector, so an omitted key is truly
|
|
339
|
+
* absent rather than set to undefined.
|
|
340
|
+
*/
|
|
341
|
+
const slotFields = (literalKey, envKey, fields, current) => {
|
|
342
|
+
const literal = fields[literalKey];
|
|
343
|
+
if (literal !== void 0) return { [literalKey]: literal };
|
|
344
|
+
const envVar = fields[envKey];
|
|
345
|
+
if (envVar !== void 0) return { [envKey]: envVar };
|
|
346
|
+
const result = {};
|
|
347
|
+
const currentLiteral = current[literalKey];
|
|
348
|
+
const currentEnv = current[envKey];
|
|
349
|
+
if (typeof currentLiteral === "string") result[literalKey] = currentLiteral;
|
|
350
|
+
if (typeof currentEnv === "string") result[envKey] = currentEnv;
|
|
351
|
+
return result;
|
|
352
|
+
};
|
|
309
353
|
const defaultClock$1 = new NodeFunnelClock();
|
|
310
354
|
const defaultIdGenerator$1 = new NodeFunnelIdGenerator();
|
|
311
355
|
/**
|
|
@@ -412,8 +456,10 @@ var FunnelChannels = class {
|
|
|
412
456
|
id,
|
|
413
457
|
type: "slack",
|
|
414
458
|
name: input.name,
|
|
415
|
-
botToken: input.botToken,
|
|
416
|
-
appToken: input.appToken,
|
|
459
|
+
...input.botToken !== void 0 ? { botToken: input.botToken } : {},
|
|
460
|
+
...input.appToken !== void 0 ? { appToken: input.appToken } : {},
|
|
461
|
+
...input.botTokenEnv !== void 0 ? { botTokenEnv: input.botTokenEnv } : {},
|
|
462
|
+
...input.appTokenEnv !== void 0 ? { appTokenEnv: input.appTokenEnv } : {},
|
|
417
463
|
minify: input.minify ?? true,
|
|
418
464
|
createdAt,
|
|
419
465
|
updatedAt
|
|
@@ -430,7 +476,8 @@ var FunnelChannels = class {
|
|
|
430
476
|
id,
|
|
431
477
|
type: "discord",
|
|
432
478
|
name: input.name,
|
|
433
|
-
botToken: input.botToken,
|
|
479
|
+
...input.botToken !== void 0 ? { botToken: input.botToken } : {},
|
|
480
|
+
...input.botTokenEnv !== void 0 ? { botTokenEnv: input.botTokenEnv } : {},
|
|
434
481
|
createdAt,
|
|
435
482
|
updatedAt
|
|
436
483
|
};
|
|
@@ -464,15 +511,20 @@ var FunnelChannels = class {
|
|
|
464
511
|
}
|
|
465
512
|
updateSlackConnector(channelName, connectorName, fields) {
|
|
466
513
|
const settings = this.store.read();
|
|
467
|
-
const
|
|
514
|
+
const channel = this.requireChannel(settings, channelName);
|
|
515
|
+
const connector = requireConnectorOfType(channel, connectorName, "slack");
|
|
468
516
|
const updated = {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
517
|
+
id: connector.id,
|
|
518
|
+
name: connector.name,
|
|
519
|
+
type: "slack",
|
|
520
|
+
minify: connector.minify,
|
|
521
|
+
createdAt: connector.createdAt,
|
|
522
|
+
updatedAt: this.clock.iso(),
|
|
523
|
+
...slotFields("botToken", "botTokenEnv", fields, connector),
|
|
524
|
+
...slotFields("appToken", "appTokenEnv", fields, connector)
|
|
473
525
|
};
|
|
474
526
|
this.assertNoTokenCollision(settings, updated);
|
|
475
|
-
|
|
527
|
+
this.replaceConnector(channel, connector.name, updated);
|
|
476
528
|
this.store.write(settings);
|
|
477
529
|
}
|
|
478
530
|
updateGhConnector(channelName, connectorName, fields) {
|
|
@@ -484,14 +536,18 @@ var FunnelChannels = class {
|
|
|
484
536
|
}
|
|
485
537
|
updateDiscordConnector(channelName, connectorName, fields) {
|
|
486
538
|
const settings = this.store.read();
|
|
487
|
-
const
|
|
539
|
+
const channel = this.requireChannel(settings, channelName);
|
|
540
|
+
const connector = requireConnectorOfType(channel, connectorName, "discord");
|
|
488
541
|
const updated = {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
542
|
+
id: connector.id,
|
|
543
|
+
name: connector.name,
|
|
544
|
+
type: "discord",
|
|
545
|
+
createdAt: connector.createdAt,
|
|
546
|
+
updatedAt: this.clock.iso(),
|
|
547
|
+
...slotFields("botToken", "botTokenEnv", fields, connector)
|
|
492
548
|
};
|
|
493
549
|
this.assertNoTokenCollision(settings, updated);
|
|
494
|
-
|
|
550
|
+
this.replaceConnector(channel, connector.name, updated);
|
|
495
551
|
this.store.write(settings);
|
|
496
552
|
}
|
|
497
553
|
listScheduleEntries(channelName, connectorName) {
|
|
@@ -554,6 +610,11 @@ var FunnelChannels = class {
|
|
|
554
610
|
if (!channel) throw new Error(`channel "${name}" not found`);
|
|
555
611
|
return channel;
|
|
556
612
|
}
|
|
613
|
+
replaceConnector(channel, connectorName, next) {
|
|
614
|
+
const index = channel.connectors.findIndex((c) => c.name === connectorName);
|
|
615
|
+
if (index < 0) throw new Error(`connector "${connectorName}" not found in channel "${channel.name}"`);
|
|
616
|
+
channel.connectors[index] = next;
|
|
617
|
+
}
|
|
557
618
|
assertNoTokenCollision(settings, candidate) {
|
|
558
619
|
const tokens = connectorTokens(candidate);
|
|
559
620
|
if (tokens.length === 0) return;
|
|
@@ -1054,26 +1115,31 @@ var FunnelLocalConfigSync = class {
|
|
|
1054
1115
|
}
|
|
1055
1116
|
async ensureSlack(channelName, spec, dotenv) {
|
|
1056
1117
|
const byName = this.findExistingSlack(channelName, spec.name);
|
|
1057
|
-
const
|
|
1118
|
+
const bot = await this.resolveSlot({
|
|
1058
1119
|
literal: spec.botToken,
|
|
1059
1120
|
envVar: spec.env?.botToken,
|
|
1060
1121
|
dotenv,
|
|
1061
1122
|
label: `${spec.name}.botToken`,
|
|
1062
|
-
|
|
1123
|
+
existingLiteral: byName?.botToken,
|
|
1124
|
+
existingEnv: byName?.botTokenEnv
|
|
1063
1125
|
});
|
|
1064
|
-
const
|
|
1126
|
+
const app = await this.resolveSlot({
|
|
1065
1127
|
literal: spec.appToken,
|
|
1066
1128
|
envVar: spec.env?.appToken,
|
|
1067
1129
|
dotenv,
|
|
1068
1130
|
label: `${spec.name}.appToken`,
|
|
1069
|
-
|
|
1131
|
+
existingLiteral: byName?.appToken,
|
|
1132
|
+
existingEnv: byName?.appTokenEnv
|
|
1070
1133
|
});
|
|
1134
|
+
const update = {
|
|
1135
|
+
botToken: bot.token,
|
|
1136
|
+
botTokenEnv: bot.tokenEnv,
|
|
1137
|
+
appToken: app.token,
|
|
1138
|
+
appTokenEnv: app.tokenEnv
|
|
1139
|
+
};
|
|
1071
1140
|
if (byName) {
|
|
1072
|
-
if (byName.botToken
|
|
1073
|
-
this.channels.updateSlackConnector(channelName, spec.name,
|
|
1074
|
-
botToken,
|
|
1075
|
-
appToken
|
|
1076
|
-
});
|
|
1141
|
+
if (!(byName.botToken === bot.token && byName.botTokenEnv === bot.tokenEnv && byName.appToken === app.token && byName.appTokenEnv === app.tokenEnv)) {
|
|
1142
|
+
this.channels.updateSlackConnector(channelName, spec.name, update);
|
|
1077
1143
|
return {
|
|
1078
1144
|
id: byName.id,
|
|
1079
1145
|
name: spec.name,
|
|
@@ -1086,25 +1152,11 @@ var FunnelLocalConfigSync = class {
|
|
|
1086
1152
|
changed: false
|
|
1087
1153
|
};
|
|
1088
1154
|
}
|
|
1089
|
-
const byToken = this.findSlackByToken(channelName, [botToken, appToken]);
|
|
1090
|
-
if (byToken) {
|
|
1091
|
-
this.channels.renameConnector(channelName, byToken.name, spec.name);
|
|
1092
|
-
if (byToken.botToken !== botToken || byToken.appToken !== appToken) this.channels.updateSlackConnector(channelName, spec.name, {
|
|
1093
|
-
botToken,
|
|
1094
|
-
appToken
|
|
1095
|
-
});
|
|
1096
|
-
return {
|
|
1097
|
-
id: byToken.id,
|
|
1098
|
-
name: spec.name,
|
|
1099
|
-
changed: true
|
|
1100
|
-
};
|
|
1101
|
-
}
|
|
1102
1155
|
return {
|
|
1103
1156
|
id: this.channels.addConnector(channelName, {
|
|
1104
1157
|
type: "slack",
|
|
1105
1158
|
name: spec.name,
|
|
1106
|
-
|
|
1107
|
-
appToken,
|
|
1159
|
+
...update,
|
|
1108
1160
|
...spec.minify !== void 0 ? { minify: spec.minify } : {}
|
|
1109
1161
|
}).id,
|
|
1110
1162
|
name: spec.name,
|
|
@@ -1113,16 +1165,21 @@ var FunnelLocalConfigSync = class {
|
|
|
1113
1165
|
}
|
|
1114
1166
|
async ensureDiscord(channelName, spec, dotenv) {
|
|
1115
1167
|
const byName = this.findExistingDiscord(channelName, spec.name);
|
|
1116
|
-
const
|
|
1168
|
+
const bot = await this.resolveSlot({
|
|
1117
1169
|
literal: spec.botToken,
|
|
1118
1170
|
envVar: spec.env?.botToken,
|
|
1119
1171
|
dotenv,
|
|
1120
1172
|
label: `${spec.name}.botToken`,
|
|
1121
|
-
|
|
1173
|
+
existingLiteral: byName?.botToken,
|
|
1174
|
+
existingEnv: byName?.botTokenEnv
|
|
1122
1175
|
});
|
|
1176
|
+
const update = {
|
|
1177
|
+
botToken: bot.token,
|
|
1178
|
+
botTokenEnv: bot.tokenEnv
|
|
1179
|
+
};
|
|
1123
1180
|
if (byName) {
|
|
1124
|
-
if (byName.botToken !==
|
|
1125
|
-
this.channels.updateDiscordConnector(channelName, spec.name,
|
|
1181
|
+
if (byName.botToken !== bot.token || byName.botTokenEnv !== bot.tokenEnv) {
|
|
1182
|
+
this.channels.updateDiscordConnector(channelName, spec.name, update);
|
|
1126
1183
|
return {
|
|
1127
1184
|
id: byName.id,
|
|
1128
1185
|
name: spec.name,
|
|
@@ -1135,21 +1192,11 @@ var FunnelLocalConfigSync = class {
|
|
|
1135
1192
|
changed: false
|
|
1136
1193
|
};
|
|
1137
1194
|
}
|
|
1138
|
-
const byToken = this.findDiscordByToken(channelName, botToken);
|
|
1139
|
-
if (byToken) {
|
|
1140
|
-
this.channels.renameConnector(channelName, byToken.name, spec.name);
|
|
1141
|
-
if (byToken.botToken !== botToken) this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
|
|
1142
|
-
return {
|
|
1143
|
-
id: byToken.id,
|
|
1144
|
-
name: spec.name,
|
|
1145
|
-
changed: true
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
1195
|
return {
|
|
1149
1196
|
id: this.channels.addConnector(channelName, {
|
|
1150
1197
|
type: "discord",
|
|
1151
1198
|
name: spec.name,
|
|
1152
|
-
|
|
1199
|
+
...update
|
|
1153
1200
|
}).id,
|
|
1154
1201
|
name: spec.name,
|
|
1155
1202
|
changed: true
|
|
@@ -1212,24 +1259,6 @@ var FunnelLocalConfigSync = class {
|
|
|
1212
1259
|
if (existing.type !== "discord") throw new Error(`connector "${connectorName}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "discord"`);
|
|
1213
1260
|
return existing;
|
|
1214
1261
|
}
|
|
1215
|
-
findSlackByToken(channelName, tokens) {
|
|
1216
|
-
const channel = this.channels.get(channelName);
|
|
1217
|
-
if (!channel) return null;
|
|
1218
|
-
for (const connector of channel.connectors) {
|
|
1219
|
-
if (connector.type !== "slack") continue;
|
|
1220
|
-
if (tokens.includes(connector.botToken) || tokens.includes(connector.appToken)) return connector;
|
|
1221
|
-
}
|
|
1222
|
-
return null;
|
|
1223
|
-
}
|
|
1224
|
-
findDiscordByToken(channelName, token) {
|
|
1225
|
-
const channel = this.channels.get(channelName);
|
|
1226
|
-
if (!channel) return null;
|
|
1227
|
-
for (const connector of channel.connectors) {
|
|
1228
|
-
if (connector.type !== "discord") continue;
|
|
1229
|
-
if (connector.botToken === token) return connector;
|
|
1230
|
-
}
|
|
1231
|
-
return null;
|
|
1232
|
-
}
|
|
1233
1262
|
removeExtras(channelName, touched) {
|
|
1234
1263
|
const channel = this.channels.get(channelName);
|
|
1235
1264
|
if (!channel) return [];
|
|
@@ -1237,18 +1266,43 @@ var FunnelLocalConfigSync = class {
|
|
|
1237
1266
|
for (const connector of stale) this.channels.removeConnector(channelName, connector.name);
|
|
1238
1267
|
return stale.map((c) => c.name);
|
|
1239
1268
|
}
|
|
1240
|
-
|
|
1269
|
+
/**
|
|
1270
|
+
* Decides how a single token slot is stored in settings.json:
|
|
1271
|
+
*
|
|
1272
|
+
* - `env.<field>` reference → `{ tokenEnv: "<VAR>" }`; the secret is NOT
|
|
1273
|
+
* resolved into settings, it stays in the environment / `.env.local` and
|
|
1274
|
+
* the listener resolves it at start. We still assert the var is set so a
|
|
1275
|
+
* typo fails loudly here instead of as a dead listener later.
|
|
1276
|
+
* - literal → `{ token: "<secret>" }`.
|
|
1277
|
+
* - neither, but a prior value exists → carry it over verbatim (whichever
|
|
1278
|
+
* form it already was), so a tokenless re-sync is a no-op.
|
|
1279
|
+
* - nothing at all → prompt for a literal (TTY only; throws otherwise).
|
|
1280
|
+
*/
|
|
1281
|
+
async resolveSlot(input) {
|
|
1241
1282
|
if (input.literal !== void 0 && input.envVar !== void 0) throw new Error(`${input.label} is set both as a literal and as env.${input.label.split(".").pop()}; pick one`);
|
|
1242
|
-
if (input.literal !== void 0 && input.literal !== "") return input.literal;
|
|
1243
1283
|
if (input.envVar !== void 0 && input.envVar !== "") {
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1284
|
+
if (!this.env[input.envVar] && !input.dotenv[input.envVar]) throw new Error(`${input.label} references env var "${input.envVar}" but it is not set in process env or .env.local`);
|
|
1285
|
+
return {
|
|
1286
|
+
token: void 0,
|
|
1287
|
+
tokenEnv: input.envVar
|
|
1288
|
+
};
|
|
1249
1289
|
}
|
|
1250
|
-
if (input.
|
|
1251
|
-
|
|
1290
|
+
if (input.literal !== void 0 && input.literal !== "") return {
|
|
1291
|
+
token: input.literal,
|
|
1292
|
+
tokenEnv: void 0
|
|
1293
|
+
};
|
|
1294
|
+
if (input.existingEnv !== void 0) return {
|
|
1295
|
+
token: void 0,
|
|
1296
|
+
tokenEnv: input.existingEnv
|
|
1297
|
+
};
|
|
1298
|
+
if (input.existingLiteral !== void 0) return {
|
|
1299
|
+
token: input.existingLiteral,
|
|
1300
|
+
tokenEnv: void 0
|
|
1301
|
+
};
|
|
1302
|
+
return {
|
|
1303
|
+
token: await this.prompter.promptSecret(input.label),
|
|
1304
|
+
tokenEnv: void 0
|
|
1305
|
+
};
|
|
1252
1306
|
}
|
|
1253
1307
|
};
|
|
1254
1308
|
//#endregion
|
|
@@ -1845,6 +1899,7 @@ var FunnelGateway = class {
|
|
|
1845
1899
|
const gatewayScript = resolveDaemonScript();
|
|
1846
1900
|
const command = this.buildStartCommand(gatewayScript, options);
|
|
1847
1901
|
this.process.detach(command, {
|
|
1902
|
+
env: { FUNNEL_DIR: this.dir },
|
|
1848
1903
|
stdoutFile: this.gatewayLog,
|
|
1849
1904
|
stderrFile: this.gatewayLog
|
|
1850
1905
|
});
|
|
@@ -2358,8 +2413,11 @@ var LeucoLoggerSqliteSink = class {
|
|
|
2358
2413
|
if (props.where) this.appendWhereConditions(props.where, conditions, params);
|
|
2359
2414
|
const limit = props.limit ?? 1e3;
|
|
2360
2415
|
params.push(limit);
|
|
2361
|
-
const
|
|
2362
|
-
|
|
2416
|
+
const dir = props.order === "desc" ? "DESC" : "ASC";
|
|
2417
|
+
const sql = `SELECT seq, ts, type, event FROM leuco_log WHERE ${conditions.join(" AND ")} ORDER BY seq ${dir} LIMIT ?`;
|
|
2418
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
2419
|
+
if (dir === "DESC") rows.reverse();
|
|
2420
|
+
return rows.map(toRecord);
|
|
2363
2421
|
}
|
|
2364
2422
|
/**
|
|
2365
2423
|
* Current schema version. Useful for diagnostics and for tests that want
|
|
@@ -3480,7 +3538,7 @@ var Funnel = class Funnel {
|
|
|
3480
3538
|
}
|
|
3481
3539
|
/** Resolved on-disk paths the facade will read/write when methods are called. Pure compute, not memoized. */
|
|
3482
3540
|
get paths() {
|
|
3483
|
-
const dir = this.props.dir ??
|
|
3541
|
+
const dir = this.props.dir ?? resolveFunnelDir();
|
|
3484
3542
|
return {
|
|
3485
3543
|
dir,
|
|
3486
3544
|
tmpDir: this.props.tmpDir ?? funnelTmpDir(),
|
|
@@ -3533,6 +3591,7 @@ var Funnel = class Funnel {
|
|
|
3533
3591
|
fs: this.fs,
|
|
3534
3592
|
process: this.process,
|
|
3535
3593
|
logger: this.logger,
|
|
3594
|
+
diagnosticLog: this.props.diagnosticLog,
|
|
3536
3595
|
dir: this.paths.dir,
|
|
3537
3596
|
slackListenerOptions: this.props.slackListenerOptions,
|
|
3538
3597
|
scheduleListenerOptions: this.props.scheduleListenerOptions
|
|
@@ -3976,6 +4035,501 @@ var MemoryFunnelEventLog = class extends FunnelEventLog {
|
|
|
3976
4035
|
close() {}
|
|
3977
4036
|
};
|
|
3978
4037
|
//#endregion
|
|
4038
|
+
//#region lib/gateway/connector-diagnostic-log.ts
|
|
4039
|
+
/**
|
|
4040
|
+
* Points in the listener's connection lifecycle. The single source of truth
|
|
4041
|
+
* for the value set: the `status` column schema, the `ConnectorConnectionStatus`
|
|
4042
|
+
* union, and the runtime Set used to narrow on read-back all derive from this
|
|
4043
|
+
* array, so adding a status is a one-line change that cannot drift out of sync.
|
|
4044
|
+
*
|
|
4045
|
+
* started start() was called
|
|
4046
|
+
* connected the socket opened and events can flow
|
|
4047
|
+
* disconnected the socket was closed by a stop() call (a clean teardown)
|
|
4048
|
+
* auth-failed the token was rejected before the socket opened
|
|
4049
|
+
* stopped the listener was fully torn down (always follows a stop(),
|
|
4050
|
+
* paired with the disconnected/error that preceded it)
|
|
4051
|
+
* error start/stop threw, or Bolt surfaced an error frame — this is
|
|
4052
|
+
* also where an unsolicited socket drop shows up when Bolt
|
|
4053
|
+
* reports it (an `error` with no following `stopped` means the
|
|
4054
|
+
* supervisor recycled the listener, not a clean stop)
|
|
4055
|
+
*
|
|
4056
|
+
* A connection row is independent of any single inbound event, so it carries
|
|
4057
|
+
* no `eventId`. This is how "no notification arrived because the listener
|
|
4058
|
+
* never connected (or dropped, or failed auth)" becomes visible: the
|
|
4059
|
+
* raw/processed tables only hold events that *did* arrive.
|
|
4060
|
+
*/
|
|
4061
|
+
const CONNECTOR_CONNECTION_STATUSES = [
|
|
4062
|
+
"started",
|
|
4063
|
+
"connected",
|
|
4064
|
+
"disconnected",
|
|
4065
|
+
"auth-failed",
|
|
4066
|
+
"stopped",
|
|
4067
|
+
"error"
|
|
4068
|
+
];
|
|
4069
|
+
/**
|
|
4070
|
+
* Rows stored in the diagnostic tables. Connector-agnostic on purpose: `type`
|
|
4071
|
+
* carries the listener kind ("slack" | "discord" | "gh" | "schedule") so new
|
|
4072
|
+
* connectors land in the same tables without a schema change. `event_id` is
|
|
4073
|
+
* the correlation key the listener mints once per inbound event and stamps
|
|
4074
|
+
* onto both the raw and processed rows, so the two are joinable even though
|
|
4075
|
+
* they live in separate tables with independent `seq` counters.
|
|
4076
|
+
*
|
|
4077
|
+
* These schemas mirror the stored shape (snake_case columns) the way
|
|
4078
|
+
* `FunnelEvent` does for the replay log; they exist for `z.infer` and to
|
|
4079
|
+
* document the column set, not as a parse boundary.
|
|
4080
|
+
*/
|
|
4081
|
+
const connectorRawEventSchema = z.object({
|
|
4082
|
+
event_id: z.string(),
|
|
4083
|
+
type: z.string(),
|
|
4084
|
+
connector_id: z.string().nullable(),
|
|
4085
|
+
channel_id: z.string().nullable(),
|
|
4086
|
+
payload: z.string()
|
|
4087
|
+
});
|
|
4088
|
+
const connectorProcessedEventSchema = z.object({
|
|
4089
|
+
event_id: z.string(),
|
|
4090
|
+
type: z.string(),
|
|
4091
|
+
connector_id: z.string().nullable(),
|
|
4092
|
+
channel_id: z.string().nullable(),
|
|
4093
|
+
outcome: z.string(),
|
|
4094
|
+
payload: z.string()
|
|
4095
|
+
});
|
|
4096
|
+
const connectorConnectionEventSchema = z.object({
|
|
4097
|
+
type: z.string(),
|
|
4098
|
+
connector_id: z.string().nullable(),
|
|
4099
|
+
channel_id: z.string().nullable(),
|
|
4100
|
+
status: z.enum(CONNECTOR_CONNECTION_STATUSES),
|
|
4101
|
+
detail: z.string()
|
|
4102
|
+
});
|
|
4103
|
+
/**
|
|
4104
|
+
* Three-table diagnostic log of everything a connector listener does, so
|
|
4105
|
+
* "why was there no notification?" is answerable whichever way it failed:
|
|
4106
|
+
* - `raw` — every inbound event, before any filtering, with the listener's
|
|
4107
|
+
* untouched payload (the Slack Bolt event, the GH webhook, …)
|
|
4108
|
+
* - `processed` — the verdict for that event: `outcome` (emitted, or the
|
|
4109
|
+
* reason it was dropped) and, when emitted, the body that was delivered.
|
|
4110
|
+
* Shares an `eventId` with its raw row, so the two join into one story.
|
|
4111
|
+
* - `connection` — the listener's lifecycle (started, connected, dropped,
|
|
4112
|
+
* auth-failed, stopped, errored). This is the half the event tables can't
|
|
4113
|
+
* show: an event that never arrived leaves no raw row, but a listener that
|
|
4114
|
+
* never connected leaves a `connection` trail that says so.
|
|
4115
|
+
*
|
|
4116
|
+
* The three are physically separate (independent retention and payload-size
|
|
4117
|
+
* policy) so a query never crosses them by accident and a huge raw payload
|
|
4118
|
+
* never bloats the verdict or lifecycle trails. None flow to WS clients or the
|
|
4119
|
+
* MCP channel — this is a separate store from `FunnelEventLog` (replay) and
|
|
4120
|
+
* exists solely for debugging.
|
|
4121
|
+
*
|
|
4122
|
+
* Implementations:
|
|
4123
|
+
* - `SqliteConnectorDiagnosticLog` — the default; survives daemon restarts,
|
|
4124
|
+
* bounded by per-table row/age caps.
|
|
4125
|
+
* - `MemoryConnectorDiagnosticLog` — an in-process double for tests.
|
|
4126
|
+
*/
|
|
4127
|
+
var ConnectorDiagnosticLog = class {};
|
|
4128
|
+
//#endregion
|
|
4129
|
+
//#region lib/gateway/sqlite-connector-diagnostic-log.ts
|
|
4130
|
+
/**
|
|
4131
|
+
* Cap on a raw payload kept verbatim. The point of the raw table is to see
|
|
4132
|
+
* what Slack/Discord actually sent, and a typical event is a few KB — so 256
|
|
4133
|
+
* KiB keeps essentially everything intact while bounding the rare giant
|
|
4134
|
+
* payload (a huge Block Kit message, a file dump) that would otherwise let a
|
|
4135
|
+
* single row bloat the debug database without limit.
|
|
4136
|
+
*/
|
|
4137
|
+
const RAW_PAYLOAD_CAP = 256 * 1024;
|
|
4138
|
+
/**
|
|
4139
|
+
* Default `ConnectorDiagnosticLog`: three independent `LeucoLoggerSqliteSink`s, one
|
|
4140
|
+
* per table (raw / processed / connection), in separate files. Each sink
|
|
4141
|
+
* indexes the columns its queries filter on — `event_id` / `connector_id` /
|
|
4142
|
+
* `channel_id` for raw, plus `outcome` for processed and `status` for
|
|
4143
|
+
* connection — so those lookups are indexed scans (`type` is a fixed column
|
|
4144
|
+
* the sink extracts separately, not an index, so filtering by it is a scan).
|
|
4145
|
+
*
|
|
4146
|
+
* The raw table offloads any payload over `RAW_PAYLOAD_CAP`: rather than
|
|
4147
|
+
* truncating mid-string (which yields unparseable JSON), it replaces the
|
|
4148
|
+
* body with a small JSON object that keeps the diagnostic essentials and
|
|
4149
|
+
* records the dropped size under `_funnel_oversized`. Every stored payload
|
|
4150
|
+
* therefore stays valid JSON.
|
|
4151
|
+
*/
|
|
4152
|
+
var SqliteConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
4153
|
+
raw;
|
|
4154
|
+
processed;
|
|
4155
|
+
connection;
|
|
4156
|
+
now;
|
|
4157
|
+
logger;
|
|
4158
|
+
constructor(props) {
|
|
4159
|
+
super();
|
|
4160
|
+
this.now = props.now ?? (() => Date.now());
|
|
4161
|
+
this.logger = props.logger;
|
|
4162
|
+
const ageCap = props.maxAgeMs !== void 0 ? { maxAgeMs: props.maxAgeMs } : {};
|
|
4163
|
+
const verdictCap = {
|
|
4164
|
+
now: this.now,
|
|
4165
|
+
...ageCap,
|
|
4166
|
+
...props.maxRows !== void 0 ? { maxRows: props.maxRows } : {}
|
|
4167
|
+
};
|
|
4168
|
+
const rawMax = props.rawMaxRows ?? props.maxRows;
|
|
4169
|
+
const rawCap = {
|
|
4170
|
+
now: this.now,
|
|
4171
|
+
...ageCap,
|
|
4172
|
+
...rawMax !== void 0 ? { maxRows: rawMax } : {}
|
|
4173
|
+
};
|
|
4174
|
+
this.raw = new LeucoLoggerSqliteSink({
|
|
4175
|
+
path: props.rawPath,
|
|
4176
|
+
indexes: [
|
|
4177
|
+
"event_id",
|
|
4178
|
+
"connector_id",
|
|
4179
|
+
"channel_id"
|
|
4180
|
+
],
|
|
4181
|
+
extractIndexes: (event) => ({
|
|
4182
|
+
event_id: event.event_id,
|
|
4183
|
+
connector_id: event.connector_id,
|
|
4184
|
+
channel_id: event.channel_id
|
|
4185
|
+
}),
|
|
4186
|
+
...rawCap
|
|
4187
|
+
});
|
|
4188
|
+
this.processed = new LeucoLoggerSqliteSink({
|
|
4189
|
+
path: props.processedPath,
|
|
4190
|
+
indexes: [
|
|
4191
|
+
"event_id",
|
|
4192
|
+
"connector_id",
|
|
4193
|
+
"channel_id",
|
|
4194
|
+
"outcome"
|
|
4195
|
+
],
|
|
4196
|
+
extractIndexes: (event) => ({
|
|
4197
|
+
event_id: event.event_id,
|
|
4198
|
+
connector_id: event.connector_id,
|
|
4199
|
+
channel_id: event.channel_id,
|
|
4200
|
+
outcome: event.outcome
|
|
4201
|
+
}),
|
|
4202
|
+
...verdictCap
|
|
4203
|
+
});
|
|
4204
|
+
this.connection = new LeucoLoggerSqliteSink({
|
|
4205
|
+
path: props.connectionPath,
|
|
4206
|
+
indexes: [
|
|
4207
|
+
"connector_id",
|
|
4208
|
+
"channel_id",
|
|
4209
|
+
"status"
|
|
4210
|
+
],
|
|
4211
|
+
extractIndexes: (event) => ({
|
|
4212
|
+
connector_id: event.connector_id,
|
|
4213
|
+
channel_id: event.channel_id,
|
|
4214
|
+
status: event.status
|
|
4215
|
+
}),
|
|
4216
|
+
...verdictCap
|
|
4217
|
+
});
|
|
4218
|
+
restrictPermissions(props.rawPath);
|
|
4219
|
+
restrictPermissions(props.processedPath);
|
|
4220
|
+
restrictPermissions(props.connectionPath);
|
|
4221
|
+
Object.freeze(this);
|
|
4222
|
+
}
|
|
4223
|
+
recordRaw(record) {
|
|
4224
|
+
const event = {
|
|
4225
|
+
event_id: record.eventId,
|
|
4226
|
+
type: record.type,
|
|
4227
|
+
connector_id: record.connectorId,
|
|
4228
|
+
channel_id: record.channelId,
|
|
4229
|
+
payload: capPayload(record.payload, record.type)
|
|
4230
|
+
};
|
|
4231
|
+
this.report("raw", this.raw.insert({
|
|
4232
|
+
ts: this.now(),
|
|
4233
|
+
event
|
|
4234
|
+
}));
|
|
4235
|
+
}
|
|
4236
|
+
recordProcessed(record) {
|
|
4237
|
+
const event = {
|
|
4238
|
+
event_id: record.eventId,
|
|
4239
|
+
type: record.type,
|
|
4240
|
+
connector_id: record.connectorId,
|
|
4241
|
+
channel_id: record.channelId,
|
|
4242
|
+
outcome: record.outcome,
|
|
4243
|
+
payload: record.payload
|
|
4244
|
+
};
|
|
4245
|
+
this.report("processed", this.processed.insert({
|
|
4246
|
+
ts: this.now(),
|
|
4247
|
+
event
|
|
4248
|
+
}));
|
|
4249
|
+
}
|
|
4250
|
+
recordConnection(record) {
|
|
4251
|
+
const event = {
|
|
4252
|
+
type: record.type,
|
|
4253
|
+
connector_id: record.connectorId,
|
|
4254
|
+
channel_id: record.channelId,
|
|
4255
|
+
status: record.status,
|
|
4256
|
+
detail: record.detail
|
|
4257
|
+
};
|
|
4258
|
+
this.report("connection", this.connection.insert({
|
|
4259
|
+
ts: this.now(),
|
|
4260
|
+
event
|
|
4261
|
+
}));
|
|
4262
|
+
}
|
|
4263
|
+
report(table, result) {
|
|
4264
|
+
if (result instanceof Error) this.logger?.error("diagnostic log insert failed", {
|
|
4265
|
+
table,
|
|
4266
|
+
error: result.message
|
|
4267
|
+
});
|
|
4268
|
+
}
|
|
4269
|
+
queryRaw(query) {
|
|
4270
|
+
return this.raw.getRecords({
|
|
4271
|
+
...query.type !== void 0 ? { type: query.type } : {},
|
|
4272
|
+
...query.limit !== void 0 ? { limit: query.limit } : {},
|
|
4273
|
+
where: buildWhere(query),
|
|
4274
|
+
order: "desc"
|
|
4275
|
+
}).map((record) => ({
|
|
4276
|
+
seq: record.seq,
|
|
4277
|
+
ts: record.ts,
|
|
4278
|
+
eventId: record.event.event_id,
|
|
4279
|
+
type: record.event.type,
|
|
4280
|
+
connectorId: record.event.connector_id,
|
|
4281
|
+
channelId: record.event.channel_id,
|
|
4282
|
+
payload: record.event.payload
|
|
4283
|
+
}));
|
|
4284
|
+
}
|
|
4285
|
+
queryProcessed(query) {
|
|
4286
|
+
const where = buildWhere(query);
|
|
4287
|
+
if (query.outcome !== void 0) where.outcome = query.outcome;
|
|
4288
|
+
return this.processed.getRecords({
|
|
4289
|
+
...query.type !== void 0 ? { type: query.type } : {},
|
|
4290
|
+
...query.limit !== void 0 ? { limit: query.limit } : {},
|
|
4291
|
+
where,
|
|
4292
|
+
order: "desc"
|
|
4293
|
+
}).map((record) => ({
|
|
4294
|
+
seq: record.seq,
|
|
4295
|
+
ts: record.ts,
|
|
4296
|
+
eventId: record.event.event_id,
|
|
4297
|
+
type: record.event.type,
|
|
4298
|
+
connectorId: record.event.connector_id,
|
|
4299
|
+
channelId: record.event.channel_id,
|
|
4300
|
+
outcome: record.event.outcome,
|
|
4301
|
+
payload: record.event.payload
|
|
4302
|
+
}));
|
|
4303
|
+
}
|
|
4304
|
+
queryConnection(query) {
|
|
4305
|
+
const where = buildWhere(query);
|
|
4306
|
+
if (query.status !== void 0) where.status = query.status;
|
|
4307
|
+
return this.connection.getRecords({
|
|
4308
|
+
...query.type !== void 0 ? { type: query.type } : {},
|
|
4309
|
+
...query.limit !== void 0 ? { limit: query.limit } : {},
|
|
4310
|
+
where,
|
|
4311
|
+
order: "desc"
|
|
4312
|
+
}).map((record) => ({
|
|
4313
|
+
seq: record.seq,
|
|
4314
|
+
ts: record.ts,
|
|
4315
|
+
type: record.event.type,
|
|
4316
|
+
connectorId: record.event.connector_id,
|
|
4317
|
+
channelId: record.event.channel_id,
|
|
4318
|
+
status: statusOf(record.event.status),
|
|
4319
|
+
detail: record.event.detail
|
|
4320
|
+
}));
|
|
4321
|
+
}
|
|
4322
|
+
close() {
|
|
4323
|
+
this.raw.close();
|
|
4324
|
+
this.processed.close();
|
|
4325
|
+
this.connection.close();
|
|
4326
|
+
}
|
|
4327
|
+
};
|
|
4328
|
+
const restrictPermissions = (path) => {
|
|
4329
|
+
if (path === ":memory:") return;
|
|
4330
|
+
for (const suffix of [
|
|
4331
|
+
"",
|
|
4332
|
+
"-wal",
|
|
4333
|
+
"-shm"
|
|
4334
|
+
]) try {
|
|
4335
|
+
chmodSync(`${path}${suffix}`, 384);
|
|
4336
|
+
} catch {}
|
|
4337
|
+
};
|
|
4338
|
+
const buildWhere = (query) => {
|
|
4339
|
+
const where = {};
|
|
4340
|
+
if (query.connectorId !== void 0) where.connector_id = query.connectorId;
|
|
4341
|
+
if (query.channelId !== void 0) where.channel_id = query.channelId;
|
|
4342
|
+
return where;
|
|
4343
|
+
};
|
|
4344
|
+
const statusField = connectorConnectionEventSchema.shape.status;
|
|
4345
|
+
const statusOf = (value) => {
|
|
4346
|
+
const parsed = statusField.safeParse(value);
|
|
4347
|
+
return parsed.success ? parsed.data : "error";
|
|
4348
|
+
};
|
|
4349
|
+
const capPayload = (payload, type) => {
|
|
4350
|
+
const size = Buffer.byteLength(payload, "utf8");
|
|
4351
|
+
if (size <= RAW_PAYLOAD_CAP) return payload;
|
|
4352
|
+
return JSON.stringify({
|
|
4353
|
+
...headFields(payload),
|
|
4354
|
+
_funnel_oversized: size,
|
|
4355
|
+
_funnel_type: type
|
|
4356
|
+
});
|
|
4357
|
+
};
|
|
4358
|
+
const HEAD_KEYS = [
|
|
4359
|
+
"type",
|
|
4360
|
+
"subtype",
|
|
4361
|
+
"ts",
|
|
4362
|
+
"channel",
|
|
4363
|
+
"channel_type",
|
|
4364
|
+
"user",
|
|
4365
|
+
"bot_id"
|
|
4366
|
+
];
|
|
4367
|
+
const headFields = (payload) => {
|
|
4368
|
+
try {
|
|
4369
|
+
const parsed = JSON.parse(payload);
|
|
4370
|
+
if (typeof parsed !== "object" || parsed === null) return {};
|
|
4371
|
+
const source = parsed;
|
|
4372
|
+
const head = {};
|
|
4373
|
+
for (const key of HEAD_KEYS) if (source[key] !== void 0) head[key] = source[key];
|
|
4374
|
+
return head;
|
|
4375
|
+
} catch {
|
|
4376
|
+
return {};
|
|
4377
|
+
}
|
|
4378
|
+
};
|
|
4379
|
+
//#endregion
|
|
4380
|
+
//#region lib/gateway/memory-connector-diagnostic-log.ts
|
|
4381
|
+
/**
|
|
4382
|
+
* In-process `ConnectorDiagnosticLog` backed by one array per table. Used by tests
|
|
4383
|
+
* and embedders that do not need durability. Like the SQLite log it keeps
|
|
4384
|
+
* `seq` per-table (each array's 1-based position) and returns the most recent
|
|
4385
|
+
* `limit` rows oldest-first; unlike it, it never prunes and never offloads
|
|
4386
|
+
* oversized payloads — it keeps whatever the caller hands it, which is fine
|
|
4387
|
+
* for the bounded volumes a test produces. Payload-validity is therefore a
|
|
4388
|
+
* SQLite-only guarantee; do not write a test that leans on this double
|
|
4389
|
+
* rejecting a malformed payload.
|
|
4390
|
+
*/
|
|
4391
|
+
var MemoryConnectorDiagnosticLog = class extends ConnectorDiagnosticLog {
|
|
4392
|
+
raws = [];
|
|
4393
|
+
processeds = [];
|
|
4394
|
+
connections = [];
|
|
4395
|
+
constructor(now = () => Date.now()) {
|
|
4396
|
+
super();
|
|
4397
|
+
this.now = now;
|
|
4398
|
+
Object.freeze(this);
|
|
4399
|
+
}
|
|
4400
|
+
recordRaw(record) {
|
|
4401
|
+
this.raws.push({
|
|
4402
|
+
...record,
|
|
4403
|
+
seq: this.raws.length + 1,
|
|
4404
|
+
ts: this.now()
|
|
4405
|
+
});
|
|
4406
|
+
}
|
|
4407
|
+
recordProcessed(record) {
|
|
4408
|
+
this.processeds.push({
|
|
4409
|
+
...record,
|
|
4410
|
+
seq: this.processeds.length + 1,
|
|
4411
|
+
ts: this.now()
|
|
4412
|
+
});
|
|
4413
|
+
}
|
|
4414
|
+
recordConnection(record) {
|
|
4415
|
+
this.connections.push({
|
|
4416
|
+
...record,
|
|
4417
|
+
seq: this.connections.length + 1,
|
|
4418
|
+
ts: this.now()
|
|
4419
|
+
});
|
|
4420
|
+
}
|
|
4421
|
+
queryRaw(query) {
|
|
4422
|
+
return takeRecent(this.raws.filter((event) => matches$1(event, query)), query.limit);
|
|
4423
|
+
}
|
|
4424
|
+
queryProcessed(query) {
|
|
4425
|
+
return takeRecent(this.processeds.filter((event) => {
|
|
4426
|
+
if (!matches$1(event, query)) return false;
|
|
4427
|
+
if (query.outcome !== void 0 && event.outcome !== query.outcome) return false;
|
|
4428
|
+
return true;
|
|
4429
|
+
}), query.limit);
|
|
4430
|
+
}
|
|
4431
|
+
queryConnection(query) {
|
|
4432
|
+
return takeRecent(this.connections.filter((event) => {
|
|
4433
|
+
if (!matches$1(event, query)) return false;
|
|
4434
|
+
if (query.status !== void 0 && event.status !== query.status) return false;
|
|
4435
|
+
return true;
|
|
4436
|
+
}), query.limit);
|
|
4437
|
+
}
|
|
4438
|
+
close() {}
|
|
4439
|
+
};
|
|
4440
|
+
const matches$1 = (event, query) => {
|
|
4441
|
+
if (query.type !== void 0 && event.type !== query.type) return false;
|
|
4442
|
+
if (query.connectorId !== void 0 && event.connectorId !== query.connectorId) return false;
|
|
4443
|
+
if (query.channelId !== void 0 && event.channelId !== query.channelId) return false;
|
|
4444
|
+
return true;
|
|
4445
|
+
};
|
|
4446
|
+
const takeRecent = (events, limit) => {
|
|
4447
|
+
if (limit === void 0) return events;
|
|
4448
|
+
if (limit <= 0) return [];
|
|
4449
|
+
return events.slice(-limit);
|
|
4450
|
+
};
|
|
4451
|
+
//#endregion
|
|
4452
|
+
//#region lib/gateway/connector-diagnostic-sql-reader.ts
|
|
4453
|
+
/**
|
|
4454
|
+
* Read-only SQL surface over the three diagnostic tables, for Claude to query
|
|
4455
|
+
* the log with arbitrary `SELECT`s. It opens all files read-only and exposes
|
|
4456
|
+
* three views — `raw`, `processed`, `connection` — that hide the storage
|
|
4457
|
+
* details (the physical table is `leuco_log` and each row's columns live
|
|
4458
|
+
* inside a JSON `event` blob): the views surface the columns as plain fields,
|
|
4459
|
+
* with `payload` already pulled out of the nested JSON.
|
|
4460
|
+
*
|
|
4461
|
+
* The tables are separate files. `raw` and `processed` share an `event_id`,
|
|
4462
|
+
* so a `JOIN` answers "the event arrived, but what verdict did it get?";
|
|
4463
|
+
* `connection` answers the other half — "did the listener ever connect at
|
|
4464
|
+
* all?". Writes are impossible: the connection is read-only and `query`
|
|
4465
|
+
* rejects anything but a single `SELECT`.
|
|
4466
|
+
*/
|
|
4467
|
+
var ConnectorDiagnosticSqlReader = class {
|
|
4468
|
+
db;
|
|
4469
|
+
constructor(props) {
|
|
4470
|
+
const db = new Database(props.rawPath, { readonly: true });
|
|
4471
|
+
try {
|
|
4472
|
+
db.run("PRAGMA busy_timeout = 500");
|
|
4473
|
+
db.prepare("ATTACH DATABASE ? AS processeddb").run(props.processedPath);
|
|
4474
|
+
db.prepare("ATTACH DATABASE ? AS connectiondb").run(props.connectionPath);
|
|
4475
|
+
db.run(rawViewSql);
|
|
4476
|
+
db.run(processedViewSql);
|
|
4477
|
+
db.run(connectionViewSql);
|
|
4478
|
+
} catch (error) {
|
|
4479
|
+
db.close();
|
|
4480
|
+
throw error;
|
|
4481
|
+
}
|
|
4482
|
+
this.db = db;
|
|
4483
|
+
Object.freeze(this);
|
|
4484
|
+
}
|
|
4485
|
+
/**
|
|
4486
|
+
* Run one read-only `SELECT` and return the rows. Returns an `Error` (rather
|
|
4487
|
+
* than throwing) for a non-SELECT statement or a SQL error, so the caller
|
|
4488
|
+
* can surface the message without a stack trace.
|
|
4489
|
+
*/
|
|
4490
|
+
query(sql) {
|
|
4491
|
+
const trimmed = sql.trim().replace(/;$/, "").trim();
|
|
4492
|
+
if (!/^select\b/i.test(trimmed)) return /* @__PURE__ */ new Error("only a single SELECT statement is allowed");
|
|
4493
|
+
if (trimmed.includes(";")) return /* @__PURE__ */ new Error("only a single statement is allowed (remove the ';')");
|
|
4494
|
+
try {
|
|
4495
|
+
return this.db.prepare(trimmed).all();
|
|
4496
|
+
} catch (error) {
|
|
4497
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
4498
|
+
}
|
|
4499
|
+
}
|
|
4500
|
+
close() {
|
|
4501
|
+
this.db.close();
|
|
4502
|
+
}
|
|
4503
|
+
};
|
|
4504
|
+
const rawViewSql = `CREATE TEMP VIEW raw AS SELECT
|
|
4505
|
+
seq,
|
|
4506
|
+
ts,
|
|
4507
|
+
json_extract(event, '$.event_id') AS event_id,
|
|
4508
|
+
json_extract(event, '$.type') AS type,
|
|
4509
|
+
json_extract(event, '$.connector_id') AS connector_id,
|
|
4510
|
+
json_extract(event, '$.channel_id') AS channel_id,
|
|
4511
|
+
json_extract(event, '$.payload') AS payload
|
|
4512
|
+
FROM main.leuco_log`;
|
|
4513
|
+
const processedViewSql = `CREATE TEMP VIEW processed AS SELECT
|
|
4514
|
+
seq,
|
|
4515
|
+
ts,
|
|
4516
|
+
json_extract(event, '$.event_id') AS event_id,
|
|
4517
|
+
json_extract(event, '$.type') AS type,
|
|
4518
|
+
json_extract(event, '$.connector_id') AS connector_id,
|
|
4519
|
+
json_extract(event, '$.channel_id') AS channel_id,
|
|
4520
|
+
json_extract(event, '$.outcome') AS outcome,
|
|
4521
|
+
json_extract(event, '$.payload') AS payload
|
|
4522
|
+
FROM processeddb.leuco_log`;
|
|
4523
|
+
const connectionViewSql = `CREATE TEMP VIEW connection AS SELECT
|
|
4524
|
+
seq,
|
|
4525
|
+
ts,
|
|
4526
|
+
json_extract(event, '$.type') AS type,
|
|
4527
|
+
json_extract(event, '$.connector_id') AS connector_id,
|
|
4528
|
+
json_extract(event, '$.channel_id') AS channel_id,
|
|
4529
|
+
json_extract(event, '$.status') AS status,
|
|
4530
|
+
json_extract(event, '$.detail') AS detail
|
|
4531
|
+
FROM connectiondb.leuco_log`;
|
|
4532
|
+
//#endregion
|
|
3979
4533
|
//#region lib/cli/factory.ts
|
|
3980
4534
|
const factory = createFactory();
|
|
3981
4535
|
//#endregion
|
|
@@ -4565,7 +5119,8 @@ subcommands:
|
|
|
4565
5119
|
stop stop
|
|
4566
5120
|
restart stop then start
|
|
4567
5121
|
run start in foreground (for developers)
|
|
4568
|
-
logs [-n <N>]
|
|
5122
|
+
logs [-n <N>] tail the daemon diagnostic log (lifecycle, listener boot)
|
|
5123
|
+
sql --query "<SQL>" query inbound connector traffic (raw + processed verdict)
|
|
4569
5124
|
listeners list running connector listeners (alive / dead)
|
|
4570
5125
|
|
|
4571
5126
|
examples:
|
|
@@ -4682,6 +5237,70 @@ const gatewayLogsHandler = factory.createHandlers(zValidator$1("query", z.object
|
|
|
4682
5237
|
await tail.exited;
|
|
4683
5238
|
process.exit(0);
|
|
4684
5239
|
});
|
|
5240
|
+
//#endregion
|
|
5241
|
+
//#region lib/cli/routes/gateway.sql.ts
|
|
5242
|
+
const sqlHelp = `funnel gateway sql — query inbound connector traffic with SQL
|
|
5243
|
+
|
|
5244
|
+
usage: funnel gateway sql --query "<SELECT ...>"
|
|
5245
|
+
|
|
5246
|
+
Runs one read-only SELECT against the daemon's diagnostic store of inbound
|
|
5247
|
+
connector events and prints the rows as JSON. Use it to answer "Slack
|
|
5248
|
+
delivered an event, so why was there no notification?".
|
|
5249
|
+
|
|
5250
|
+
Three views:
|
|
5251
|
+
raw every inbound event, untouched, before any filtering
|
|
5252
|
+
processed the verdict for that event after the per-type processor ran
|
|
5253
|
+
connection the listener lifecycle (so you can tell events never arrived)
|
|
5254
|
+
|
|
5255
|
+
Shared columns (all three views):
|
|
5256
|
+
seq row id within the view (not comparable across views)
|
|
5257
|
+
ts epoch milliseconds
|
|
5258
|
+
type connector kind: slack | discord | gh | schedule
|
|
5259
|
+
connector_id funnel connector id
|
|
5260
|
+
channel_id funnel channel id
|
|
5261
|
+
raw and processed also have:
|
|
5262
|
+
event_id correlation id shared by an event's raw and processed rows
|
|
5263
|
+
payload raw: the original event JSON (text); processed: the delivered body, or "" when skipped
|
|
5264
|
+
processed also has:
|
|
5265
|
+
outcome 'emitted' | 'emitted:delivery-failed' | 'skip:<reason>'
|
|
5266
|
+
(skip reasons: skip:type, skip:subtype, skip:dedup,
|
|
5267
|
+
skip:self-user, skip:self-bot, skip:preprocess)
|
|
5268
|
+
connection also has:
|
|
5269
|
+
status 'started' | 'connected' | 'disconnected' | 'auth-failed' | 'stopped' | 'error'
|
|
5270
|
+
detail an error message or reason, or "" when none
|
|
5271
|
+
|
|
5272
|
+
To trace one event end to end, join raw and processed on event_id. When the
|
|
5273
|
+
event tables are empty, query connection — a listener that never connected, or
|
|
5274
|
+
failed auth, explains why nothing arrived.
|
|
5275
|
+
|
|
5276
|
+
examples:
|
|
5277
|
+
funnel gateway sql --query "SELECT event_id, ts, type FROM raw ORDER BY seq DESC LIMIT 20"
|
|
5278
|
+
funnel gateway sql --query "SELECT outcome, COUNT(*) n FROM processed GROUP BY outcome"
|
|
5279
|
+
funnel gateway sql --query "SELECT r.payload FROM raw r JOIN processed p USING(event_id) WHERE p.outcome='skip:dedup'"
|
|
5280
|
+
funnel gateway sql --query "SELECT ts, status, detail FROM connection WHERE status IN ('auth-failed','error') ORDER BY seq DESC"`;
|
|
5281
|
+
const gatewaySqlHandler = factory.createHandlers(zValidator$1("query", z.object({ query: z.string().optional() }), sqlHelp), async (c) => {
|
|
5282
|
+
const sql = c.req.valid("query").query;
|
|
5283
|
+
if (!sql) return c.text(sqlHelp);
|
|
5284
|
+
const tmpDir = funnelTmpDir();
|
|
5285
|
+
const rawPath = join(tmpDir, "connector-raw.db");
|
|
5286
|
+
const processedPath = join(tmpDir, "connector-processed.db");
|
|
5287
|
+
const connectionPath = join(tmpDir, "connector-connection.db");
|
|
5288
|
+
if (!existsSync(rawPath) || !existsSync(processedPath) || !existsSync(connectionPath)) return c.text("no diagnostic store yet (the gateway has not initialized it)");
|
|
5289
|
+
const reader = new ConnectorDiagnosticSqlReader({
|
|
5290
|
+
rawPath,
|
|
5291
|
+
processedPath,
|
|
5292
|
+
connectionPath
|
|
5293
|
+
});
|
|
5294
|
+
const rows = (() => {
|
|
5295
|
+
try {
|
|
5296
|
+
return reader.query(sql);
|
|
5297
|
+
} finally {
|
|
5298
|
+
reader.close();
|
|
5299
|
+
}
|
|
5300
|
+
})();
|
|
5301
|
+
if (rows instanceof Error) return c.text(`error: ${rows.message}`);
|
|
5302
|
+
return c.text(JSON.stringify(rows, null, 2));
|
|
5303
|
+
});
|
|
4685
5304
|
const gatewayRestartHandler = factory.createHandlers(zValidator$1("query", z.object({ "no-caffeine": z.string().optional() }), `funnel gateway restart — restart the gateway
|
|
4686
5305
|
|
|
4687
5306
|
usage: funnel gateway restart [--no-caffeine]
|
|
@@ -5090,7 +5709,7 @@ const createCliApp = (funnel) => {
|
|
|
5090
5709
|
if (error instanceof HTTPException) return c.text(`error: ${error.message}`, error.status);
|
|
5091
5710
|
return c.text(`error: ${error instanceof Error ? error.message : String(error)}`, 400);
|
|
5092
5711
|
});
|
|
5093
|
-
return base.get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...helpRoute(addHelp$3)).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...helpRoute(removeHelp$1)).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...helpRoute(renameHelp$1)).post("/channels/:channel/rename", ...helpRoute(renameHelp$1)).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...helpRoute(publishHelp)).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...helpRoute(addHelp$2)).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...helpRoute(removeHelp$3)).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...helpRoute(setHelp$1)).post("/channels/:channel/connectors/set/:connector", ...channelsConnectorsSetHandler).post("/channels/:channel/connectors/rename/:connector/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/:connector/rename/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/rename", ...helpRoute(renameHelp$2)).post("/channels/:channel/connectors/:connector/rename", ...helpRoute(renameHelp$2)).post("/channels/:channel/connectors/:connector/request", ...channelsConnectorsRequestHandler).get("/channels/:channel/connectors/:connector", ...channelsConnectorsShowHandler).get("/channels/:channel/connectors/:connector/schedules", ...channelsConnectorsSchedulesGroupHandler).post("/channels/:channel/connectors/:connector/schedules/add", ...helpRoute(addHelp$1)).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...helpRoute(removeHelp$2)).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...helpRoute(addHelp)).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...helpRoute(setHelp)).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...helpRoute(removeHelp)).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...helpRoute(renameHelp)).post("/profiles/:profile/rename", ...helpRoute(renameHelp)).post("/profiles/:profile/as-default", ...profilesAsDefaultHandler).get("/profiles/:profile/run", ...profilesLaunchHandler).get("/profiles/:profile", ...profilesLaunchHandler).get("/gateway", ...gatewayGroupHandler).get("/gateway/status", ...gatewayStatusHandler).get("/gateway/start", ...gatewayStartHandler).get("/gateway/stop", ...gatewayStopHandler).get("/gateway/restart", ...gatewayRestartHandler).get("/gateway/run", ...gatewayRunHandler).get("/gateway/logs", ...gatewayLogsHandler).get("/gateway/listeners", ...gatewayListenersHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
|
|
5712
|
+
return base.get("/claude", ...claudeHandler).get("/channels", ...channelsGroupHandler).post("/channels/add", ...helpRoute(addHelp$3)).post("/channels/add/:channel", ...channelsAddHandler).post("/channels/remove", ...helpRoute(removeHelp$1)).post("/channels/remove/:channel", ...channelsRemoveHandler).post("/channels/rename/:channel/:newName", ...channelsRenameHandler).post("/channels/:channel/rename/:newName", ...channelsRenameHandler).post("/channels/rename", ...helpRoute(renameHelp$1)).post("/channels/:channel/rename", ...helpRoute(renameHelp$1)).post("/channels/:channel/set/delivery/:mode", ...channelsSetDeliveryHandler).post("/channels/publish", ...helpRoute(publishHelp)).post("/channels/:channel/publish", ...channelsPublishHandler).get("/channels/:channel", ...channelsShowHandler).get("/channels/:channel/connectors", ...channelsConnectorsGroupHandler).post("/channels/:channel/connectors/add", ...helpRoute(addHelp$2)).post("/channels/:channel/connectors/add/:connector", ...channelsConnectorsAddHandler).post("/channels/:channel/connectors/remove", ...helpRoute(removeHelp$3)).post("/channels/:channel/connectors/remove/:connector", ...channelsConnectorsRemoveHandler).post("/channels/:channel/connectors/set", ...helpRoute(setHelp$1)).post("/channels/:channel/connectors/set/:connector", ...channelsConnectorsSetHandler).post("/channels/:channel/connectors/rename/:connector/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/:connector/rename/:newName", ...channelsConnectorsRenameHandler).post("/channels/:channel/connectors/rename", ...helpRoute(renameHelp$2)).post("/channels/:channel/connectors/:connector/rename", ...helpRoute(renameHelp$2)).post("/channels/:channel/connectors/:connector/request", ...channelsConnectorsRequestHandler).get("/channels/:channel/connectors/:connector", ...channelsConnectorsShowHandler).get("/channels/:channel/connectors/:connector/schedules", ...channelsConnectorsSchedulesGroupHandler).post("/channels/:channel/connectors/:connector/schedules/add", ...helpRoute(addHelp$1)).post("/channels/:channel/connectors/:connector/schedules/add/:id", ...channelsConnectorsSchedulesAddHandler).post("/channels/:channel/connectors/:connector/schedules/remove", ...helpRoute(removeHelp$2)).post("/channels/:channel/connectors/:connector/schedules/remove/:id", ...channelsConnectorsSchedulesRemoveHandler).get("/profiles", ...profilesGroupHandler).post("/profiles/add", ...helpRoute(addHelp)).post("/profiles/add/:profile", ...profilesAddHandler).post("/profiles/set", ...helpRoute(setHelp)).post("/profiles/set/:profile", ...profilesSetHandler).post("/profiles/remove", ...helpRoute(removeHelp)).post("/profiles/remove/:profile", ...profilesRemoveHandler).post("/profiles/rename/:profile/:newName", ...profilesRenameHandler).post("/profiles/:profile/rename/:newName", ...profilesRenameHandler).post("/profiles/rename", ...helpRoute(renameHelp)).post("/profiles/:profile/rename", ...helpRoute(renameHelp)).post("/profiles/:profile/as-default", ...profilesAsDefaultHandler).get("/profiles/:profile/run", ...profilesLaunchHandler).get("/profiles/:profile", ...profilesLaunchHandler).get("/gateway", ...gatewayGroupHandler).get("/gateway/status", ...gatewayStatusHandler).get("/gateway/start", ...gatewayStartHandler).get("/gateway/stop", ...gatewayStopHandler).get("/gateway/restart", ...gatewayRestartHandler).get("/gateway/run", ...gatewayRunHandler).get("/gateway/logs", ...gatewayLogsHandler).get("/gateway/sql", ...gatewaySqlHandler).get("/gateway/listeners", ...gatewayListenersHandler).get("/schema", ...schemaHandler).get("/status", ...statusHandler).get("/update", ...updateHandler);
|
|
5094
5713
|
};
|
|
5095
5714
|
/** CLI Hono app wired to a default `new Funnel()`. For embedding with a custom Funnel use `createCliApp`. */
|
|
5096
5715
|
const app = createCliApp(new Funnel());
|
|
@@ -6493,6 +7112,11 @@ function ChannelsView(props) {
|
|
|
6493
7112
|
}
|
|
6494
7113
|
//#endregion
|
|
6495
7114
|
//#region lib/tui/views/connectors-view.tsx
|
|
7115
|
+
const tokenDisplay = (literal, envVar) => {
|
|
7116
|
+
if (literal !== void 0 && literal !== "") return literal;
|
|
7117
|
+
if (envVar !== void 0 && envVar !== "") return `env:${envVar}`;
|
|
7118
|
+
return "—";
|
|
7119
|
+
};
|
|
6496
7120
|
const formatTimestamp = (iso) => {
|
|
6497
7121
|
if (!iso) return "—";
|
|
6498
7122
|
const d = new Date(iso);
|
|
@@ -6577,10 +7201,10 @@ function ConnectorsView(props) {
|
|
|
6577
7201
|
}),
|
|
6578
7202
|
connector.type === "slack" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ReadonlyField, {
|
|
6579
7203
|
label: "bot-token",
|
|
6580
|
-
value: connector.botToken
|
|
7204
|
+
value: tokenDisplay(connector.botToken, connector.botTokenEnv)
|
|
6581
7205
|
}), /* @__PURE__ */ jsx(ReadonlyField, {
|
|
6582
7206
|
label: "app-token",
|
|
6583
|
-
value: connector.appToken
|
|
7207
|
+
value: tokenDisplay(connector.appToken, connector.appTokenEnv)
|
|
6584
7208
|
})] }) : null,
|
|
6585
7209
|
connector.type === "gh" ? /* @__PURE__ */ jsx(ReadonlyField, {
|
|
6586
7210
|
label: "poll",
|
|
@@ -6588,7 +7212,7 @@ function ConnectorsView(props) {
|
|
|
6588
7212
|
}) : null,
|
|
6589
7213
|
connector.type === "discord" ? /* @__PURE__ */ jsx(ReadonlyField, {
|
|
6590
7214
|
label: "bot-token",
|
|
6591
|
-
value: connector.botToken
|
|
7215
|
+
value: tokenDisplay(connector.botToken, connector.botTokenEnv)
|
|
6592
7216
|
}) : null,
|
|
6593
7217
|
connector.type === "schedule" ? /* @__PURE__ */ jsx(ReadonlyField, {
|
|
6594
7218
|
label: "entries",
|
|
@@ -7295,4 +7919,4 @@ async function launchTui(funnel) {
|
|
|
7295
7919
|
});
|
|
7296
7920
|
}
|
|
7297
7921
|
//#endregion
|
|
7298
|
-
export { DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteFunnelEventLog, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, profileSpecSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
|
|
7922
|
+
export { CONNECTOR_CONNECTION_STATUSES, ConnectorDiagnosticLog, ConnectorDiagnosticSqlReader, DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEventLog, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, MemoryConnectorDiagnosticLog, MemoryFunnelClock, MemoryFunnelEventLog, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, SqliteConnectorDiagnosticLog, SqliteFunnelEventLog, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorConnectionEventSchema, connectorProcessedEventSchema, connectorRawEventSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, profileSpecSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, resolveFunnelDir, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
|