@interactive-inc/claude-funnel 0.23.1 → 0.25.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 +41 -26
- package/dist/bin.js +372 -355
- package/dist/connectors/slack.d.ts +1 -1
- package/dist/connectors/slack.js +1 -1
- package/dist/gateway/daemon.js +189 -186
- package/dist/index.d.ts +88 -27
- package/dist/index.js +159 -110
- package/dist/{slack-connector-schema-Bi2DyeZw.js → slack-connector-schema-B0NyhxqQ.js} +91 -2
- package/dist/{slack-listener-tQH7cXU7.d.ts → slack-listener-DbNCPMqY.d.ts} +3 -0
- package/funnel.schema.json +41 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, t as discordConn
|
|
|
2
2
|
import { i as FunnelConnectorListener, n as funnelTmpDir, r as FunnelLogger, t as NodeFunnelLogger } from "./node-logger-B97ZiGwj.js";
|
|
3
3
|
import { a as FunnelProcessRunner, i as NodeFunnelProcessRunner, n as FunnelGhListener, r as FunnelGhAdapter, t as ghConnectorSchema } from "./gh-connector-schema-CAC24s0r.js";
|
|
4
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-BZpH6ZmR.js";
|
|
5
|
-
import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-
|
|
5
|
+
import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-B0NyhxqQ.js";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
@@ -50,22 +50,22 @@ const channelConfigSchema = z.object({
|
|
|
50
50
|
id: z.string(),
|
|
51
51
|
name: z.string(),
|
|
52
52
|
delivery: channelDeliveryModeSchema.default("fanout"),
|
|
53
|
-
|
|
53
|
+
connectors: z.array(connectorConfigSchema).default([])
|
|
54
|
+
});
|
|
55
|
+
const profileConfigSchema = z.object({
|
|
56
|
+
name: z.string(),
|
|
57
|
+
path: z.string(),
|
|
58
|
+
channelId: z.string(),
|
|
59
|
+
/** Args prepended to the claude argv on every launch through this profile. */
|
|
54
60
|
options: z.array(z.string()).default([]),
|
|
55
61
|
/** Env vars layered under the launched claude process. process.env wins on collision. */
|
|
56
62
|
env: z.record(z.string(), z.string()).default({}),
|
|
57
63
|
/**
|
|
58
64
|
* When true (the default), funnel injects `--session-id <uuid>` so that
|
|
59
65
|
* relaunching from the same cwd resumes the previous claude session.
|
|
60
|
-
* Set to false for
|
|
66
|
+
* Set to false for profiles that should always start a fresh session.
|
|
61
67
|
*/
|
|
62
|
-
resume: z.boolean().default(true)
|
|
63
|
-
connectors: z.array(connectorConfigSchema).default([])
|
|
64
|
-
});
|
|
65
|
-
const profileConfigSchema = z.object({
|
|
66
|
-
name: z.string(),
|
|
67
|
-
path: z.string(),
|
|
68
|
-
channelId: z.string()
|
|
68
|
+
resume: z.boolean().default(true)
|
|
69
69
|
});
|
|
70
70
|
const SETTINGS_VERSION = 1;
|
|
71
71
|
const settingsSchema = z.object({
|
|
@@ -312,9 +312,6 @@ var FunnelChannels = class {
|
|
|
312
312
|
id: this.idGenerator.generate(),
|
|
313
313
|
name: input.name,
|
|
314
314
|
delivery: input.delivery ?? "fanout",
|
|
315
|
-
options: input.options ?? [],
|
|
316
|
-
env: input.env ?? {},
|
|
317
|
-
resume: input.resume ?? true,
|
|
318
315
|
connectors: []
|
|
319
316
|
};
|
|
320
317
|
settings.channels.push(channel);
|
|
@@ -327,24 +324,6 @@ var FunnelChannels = class {
|
|
|
327
324
|
channel.delivery = delivery;
|
|
328
325
|
this.store.write(settings);
|
|
329
326
|
}
|
|
330
|
-
setResume(name, resume) {
|
|
331
|
-
const settings = this.store.read();
|
|
332
|
-
const channel = this.requireChannel(settings, name);
|
|
333
|
-
channel.resume = resume;
|
|
334
|
-
this.store.write(settings);
|
|
335
|
-
}
|
|
336
|
-
setOptions(name, options) {
|
|
337
|
-
const settings = this.store.read();
|
|
338
|
-
const channel = this.requireChannel(settings, name);
|
|
339
|
-
channel.options = options;
|
|
340
|
-
this.store.write(settings);
|
|
341
|
-
}
|
|
342
|
-
setEnv(name, env) {
|
|
343
|
-
const settings = this.store.read();
|
|
344
|
-
const channel = this.requireChannel(settings, name);
|
|
345
|
-
channel.env = env;
|
|
346
|
-
this.store.write(settings);
|
|
347
|
-
}
|
|
348
327
|
remove(name) {
|
|
349
328
|
const settings = this.store.read();
|
|
350
329
|
const index = settings.channels.findIndex((c) => c.name === name);
|
|
@@ -401,6 +380,7 @@ var FunnelChannels = class {
|
|
|
401
380
|
name: input.name,
|
|
402
381
|
botToken: input.botToken,
|
|
403
382
|
appToken: input.appToken,
|
|
383
|
+
minify: input.minify ?? true,
|
|
404
384
|
createdAt,
|
|
405
385
|
updatedAt
|
|
406
386
|
};
|
|
@@ -597,9 +577,9 @@ var FunnelClaude = class {
|
|
|
597
577
|
this.writePidFile(options.profileName);
|
|
598
578
|
this.installCleanup(options.profileName);
|
|
599
579
|
}
|
|
600
|
-
const session =
|
|
601
|
-
const claudeArgs = this.buildArgs(
|
|
602
|
-
const env = this.buildEnv(channel.id,
|
|
580
|
+
const session = options.resume ?? true ? this.resolveSession(channel.id, cwd, options.userArgs ?? []) : null;
|
|
581
|
+
const claudeArgs = this.buildArgs(options.options ?? [], options.userArgs ?? [], cwd, session);
|
|
582
|
+
const env = this.buildEnv(channel.id, options.env ?? {});
|
|
603
583
|
this.logger.info(`claude launch`, {
|
|
604
584
|
channel: options.channel,
|
|
605
585
|
channelId: channel.id,
|
|
@@ -649,8 +629,8 @@ var FunnelClaude = class {
|
|
|
649
629
|
isProcessAlive(pid) {
|
|
650
630
|
return this.process.isAlive(pid);
|
|
651
631
|
}
|
|
652
|
-
buildArgs(
|
|
653
|
-
const result = [...
|
|
632
|
+
buildArgs(recipeOptions, userArgs, cwd, session) {
|
|
633
|
+
const result = [...recipeOptions, ...userArgs];
|
|
654
634
|
if (session !== null) if (session.mode === "resume") result.push("--resume", session.id);
|
|
655
635
|
else result.push("--session-id", session.id);
|
|
656
636
|
const mcpName = this.mcp.findInstalledName(cwd);
|
|
@@ -679,9 +659,9 @@ var FunnelClaude = class {
|
|
|
679
659
|
mode: "new"
|
|
680
660
|
};
|
|
681
661
|
}
|
|
682
|
-
buildEnv(channelId,
|
|
662
|
+
buildEnv(channelId, recipeEnv) {
|
|
683
663
|
const env = {};
|
|
684
|
-
for (const [key, value] of Object.entries(
|
|
664
|
+
for (const [key, value] of Object.entries(recipeEnv)) env[key] = value;
|
|
685
665
|
for (const [key, value] of Object.entries(globalThis.process.env)) if (typeof value === "string") env[key] = value;
|
|
686
666
|
env.FUNNEL_CHANNEL_ID = channelId;
|
|
687
667
|
return env;
|
|
@@ -780,16 +760,17 @@ var MemoryFunnelIdGenerator = class extends FunnelIdGenerator {
|
|
|
780
760
|
/**
|
|
781
761
|
* Per-repo launch config (`funnel.json`).
|
|
782
762
|
*
|
|
783
|
-
* `fnl claude` reads this when no --profile is
|
|
784
|
-
* declared channels (`--channel <name>` selects by name; otherwise the
|
|
785
|
-
* entry wins)
|
|
786
|
-
* `~/.funnel/settings.json` on launch — token fields in connectors resolve
|
|
787
|
-
*
|
|
763
|
+
* `fnl claude` reads this when no global --profile preset is used. It picks one
|
|
764
|
+
* of the declared channels (`--channel <name>` selects by name; otherwise the
|
|
765
|
+
* first entry wins) and materializes its transport (connectors / delivery) into
|
|
766
|
+
* `~/.funnel/settings.json` on launch — token fields in connectors resolve via
|
|
767
|
+
* literal / `env.<field>` / TTY prompt.
|
|
788
768
|
*
|
|
789
|
-
*
|
|
790
|
-
* channel
|
|
791
|
-
*
|
|
792
|
-
*
|
|
769
|
+
* The launch recipe (`options` / `env` / `resume`) lives on `profiles[]`, not on
|
|
770
|
+
* the channel: a channel only describes where events come from. `fnl claude`
|
|
771
|
+
* applies the first profile bound to the chosen channel (or `--profile <name>`
|
|
772
|
+
* to pick another); the recipe is passed straight to the launcher and is not
|
|
773
|
+
* persisted into the global profile list.
|
|
793
774
|
*/
|
|
794
775
|
const slackEnvSchema = z.object({
|
|
795
776
|
botToken: z.string().optional(),
|
|
@@ -800,6 +781,8 @@ const slackConnectorSpecSchema = z.object({
|
|
|
800
781
|
name: z.string(),
|
|
801
782
|
botToken: z.string().optional(),
|
|
802
783
|
appToken: z.string().optional(),
|
|
784
|
+
/** Shrink raw Slack events before fanout. Defaults to true. */
|
|
785
|
+
minify: z.boolean().optional(),
|
|
803
786
|
env: slackEnvSchema
|
|
804
787
|
});
|
|
805
788
|
const discordEnvSchema = z.object({ botToken: z.string().optional() }).optional();
|
|
@@ -826,7 +809,13 @@ const connectorSpecSchema = z.discriminatedUnion("type", [
|
|
|
826
809
|
]);
|
|
827
810
|
const channelSpecSchema = z.object({
|
|
828
811
|
name: z.string(),
|
|
829
|
-
|
|
812
|
+
connectors: z.array(connectorSpecSchema).optional()
|
|
813
|
+
});
|
|
814
|
+
const profileSpecSchema = z.object({
|
|
815
|
+
name: z.string(),
|
|
816
|
+
/** Name of the channel (declared in `channels[]`) this profile subscribes to. */
|
|
817
|
+
channel: z.string(),
|
|
818
|
+
/** Args prepended to the claude argv on every launch through this profile. */
|
|
830
819
|
options: z.array(z.string()).optional(),
|
|
831
820
|
/** Env vars layered under the launched claude process. process.env wins on collision. */
|
|
832
821
|
env: z.record(z.string(), z.string()).optional(),
|
|
@@ -834,15 +823,16 @@ const channelSpecSchema = z.object({
|
|
|
834
823
|
* When true (the default), funnel injects `--session-id <uuid>` so that
|
|
835
824
|
* relaunching from the same cwd resumes the previous claude session
|
|
836
825
|
* without bleeding into other channels or workspaces. Set to false for
|
|
837
|
-
*
|
|
826
|
+
* profiles that should always start a fresh session.
|
|
838
827
|
*/
|
|
839
|
-
resume: z.boolean().optional()
|
|
840
|
-
connectors: z.array(connectorSpecSchema).optional()
|
|
828
|
+
resume: z.boolean().optional()
|
|
841
829
|
});
|
|
842
830
|
const localConfigSchema = z.object({
|
|
843
831
|
$schema: z.string().optional(),
|
|
844
|
-
/** Declared channels. First entry is the default; --channel <name> selects by name. */
|
|
845
|
-
channels: z.array(channelSpecSchema).min(1)
|
|
832
|
+
/** Declared channels (transport only). First entry is the default; --channel <name> selects by name. */
|
|
833
|
+
channels: z.array(channelSpecSchema).min(1),
|
|
834
|
+
/** Launch presets bound to a channel. First entry bound to the chosen channel is the default. */
|
|
835
|
+
profiles: z.array(profileSpecSchema).optional()
|
|
846
836
|
});
|
|
847
837
|
const LOCAL_CONFIG_FILENAME = "funnel.json";
|
|
848
838
|
const LOCAL_ENV_FILENAME = ".env.local";
|
|
@@ -929,17 +919,6 @@ var FunnelLocalConfig = class {
|
|
|
929
919
|
var FunnelTokenPrompter = class {};
|
|
930
920
|
//#endregion
|
|
931
921
|
//#region lib/engine/local-config/local-config-sync.ts
|
|
932
|
-
const arraysEqual = (a, b) => {
|
|
933
|
-
if (a.length !== b.length) return false;
|
|
934
|
-
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
935
|
-
return true;
|
|
936
|
-
};
|
|
937
|
-
const recordsEqual = (a, b) => {
|
|
938
|
-
const keys = Object.keys(a);
|
|
939
|
-
if (keys.length !== Object.keys(b).length) return false;
|
|
940
|
-
for (const key of keys) if (a[key] !== b[key]) return false;
|
|
941
|
-
return true;
|
|
942
|
-
};
|
|
943
922
|
/**
|
|
944
923
|
* Reconciles a single funnel.json channel spec with `~/.funnel/settings.json`.
|
|
945
924
|
* The spec is the source of truth for the channel it declares:
|
|
@@ -972,21 +951,7 @@ var FunnelLocalConfigSync = class {
|
|
|
972
951
|
Object.freeze(this);
|
|
973
952
|
}
|
|
974
953
|
async ensure(channel, cwd) {
|
|
975
|
-
|
|
976
|
-
const nextResume = channel.resume ?? true;
|
|
977
|
-
if (!existing) this.channels.add({
|
|
978
|
-
name: channel.name,
|
|
979
|
-
options: channel.options ?? [],
|
|
980
|
-
env: channel.env ?? {},
|
|
981
|
-
resume: nextResume
|
|
982
|
-
});
|
|
983
|
-
else {
|
|
984
|
-
const nextOptions = channel.options ?? [];
|
|
985
|
-
const nextEnv = channel.env ?? {};
|
|
986
|
-
if (!arraysEqual(existing.options, nextOptions)) this.channels.setOptions(channel.name, nextOptions);
|
|
987
|
-
if (!recordsEqual(existing.env, nextEnv)) this.channels.setEnv(channel.name, nextEnv);
|
|
988
|
-
if (existing.resume !== nextResume) this.channels.setResume(channel.name, nextResume);
|
|
989
|
-
}
|
|
954
|
+
if (!this.channels.get(channel.name)) this.channels.add({ name: channel.name });
|
|
990
955
|
if (channel.connectors === void 0) return {
|
|
991
956
|
touched: [],
|
|
992
957
|
removed: []
|
|
@@ -1065,7 +1030,8 @@ var FunnelLocalConfigSync = class {
|
|
|
1065
1030
|
type: "slack",
|
|
1066
1031
|
name: spec.name,
|
|
1067
1032
|
botToken,
|
|
1068
|
-
appToken
|
|
1033
|
+
appToken,
|
|
1034
|
+
...spec.minify !== void 0 ? { minify: spec.minify } : {}
|
|
1069
1035
|
}).id,
|
|
1070
1036
|
name: spec.name,
|
|
1071
1037
|
changed: true
|
|
@@ -1424,9 +1390,10 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
|
|
|
1424
1390
|
//#region lib/engine/profiles/profiles.ts
|
|
1425
1391
|
/**
|
|
1426
1392
|
* Named launch presets for `fnl claude`. Each profile bundles a working
|
|
1427
|
-
* directory,
|
|
1428
|
-
*
|
|
1429
|
-
*
|
|
1393
|
+
* directory, the channel id its Claude instance subscribes to, and the launch
|
|
1394
|
+
* recipe (`options` prepended to the claude argv, `env` layered under the
|
|
1395
|
+
* process, `resume` toggling session reuse). Implements ProfileChannelChecker
|
|
1396
|
+
* so FunnelChannels can refuse to remove a channel that is still referenced.
|
|
1430
1397
|
*
|
|
1431
1398
|
* The first entry in the persisted array is treated as the default profile;
|
|
1432
1399
|
* `asDefault` reorders the array to put a named profile first.
|
|
@@ -1449,11 +1416,18 @@ var FunnelProfiles = class {
|
|
|
1449
1416
|
getDefault() {
|
|
1450
1417
|
return this.list()[0] ?? null;
|
|
1451
1418
|
}
|
|
1452
|
-
add(
|
|
1419
|
+
add(input) {
|
|
1453
1420
|
const settings = this.store.read();
|
|
1454
|
-
if (settings.profiles.some((p) => p.name ===
|
|
1455
|
-
if (!settings.channels.some((c) => c.id ===
|
|
1456
|
-
settings.profiles.push(
|
|
1421
|
+
if (settings.profiles.some((p) => p.name === input.name)) throw new Error(`profile "${input.name}" already exists`);
|
|
1422
|
+
if (!settings.channels.some((c) => c.id === input.channelId)) throw new Error(`channel id "${input.channelId}" not found`);
|
|
1423
|
+
settings.profiles.push({
|
|
1424
|
+
name: input.name,
|
|
1425
|
+
path: input.path,
|
|
1426
|
+
channelId: input.channelId,
|
|
1427
|
+
options: input.options ?? [],
|
|
1428
|
+
env: input.env ?? {},
|
|
1429
|
+
resume: input.resume ?? true
|
|
1430
|
+
});
|
|
1457
1431
|
this.store.write(settings);
|
|
1458
1432
|
}
|
|
1459
1433
|
remove(name) {
|
|
@@ -1493,6 +1467,9 @@ var FunnelProfiles = class {
|
|
|
1493
1467
|
profile.channelId = fields.channelId;
|
|
1494
1468
|
}
|
|
1495
1469
|
if (fields.path !== void 0) profile.path = fields.path;
|
|
1470
|
+
if (fields.options !== void 0) profile.options = fields.options;
|
|
1471
|
+
if (fields.env !== void 0) profile.env = fields.env;
|
|
1472
|
+
if (fields.resume !== void 0) profile.resume = fields.resume;
|
|
1496
1473
|
this.store.write(settings);
|
|
1497
1474
|
}
|
|
1498
1475
|
};
|
|
@@ -3843,14 +3820,14 @@ const startChannelServer = async (options = {}) => {
|
|
|
3843
3820
|
/**
|
|
3844
3821
|
* Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
|
|
3845
3822
|
* `$schema` references in committed `funnel.json` files so editors can give
|
|
3846
|
-
* autocomplete and validation for
|
|
3847
|
-
* without anyone hand-maintaining a separate schema.
|
|
3823
|
+
* autocomplete and validation for channels[] (transport) and profiles[]
|
|
3824
|
+
* (launch recipe) without anyone hand-maintaining a separate schema.
|
|
3848
3825
|
*/
|
|
3849
3826
|
const funnelJsonSchema = () => {
|
|
3850
3827
|
return {
|
|
3851
3828
|
...z.toJSONSchema(localConfigSchema, { target: "draft-2020-12" }),
|
|
3852
3829
|
title: "Funnel per-repo launch config",
|
|
3853
|
-
description: "Used by `fnl claude`
|
|
3830
|
+
description: "Used by `fnl claude` to declare channels (transport: connectors to materialize into ~/.funnel/settings.json on launch) and profiles (launch recipe: options / env / resume) bound to those channels."
|
|
3854
3831
|
};
|
|
3855
3832
|
};
|
|
3856
3833
|
//#endregion
|
|
@@ -4667,31 +4644,78 @@ examples:
|
|
|
4667
4644
|
return c.text("funnel gateway: stopped");
|
|
4668
4645
|
});
|
|
4669
4646
|
//#endregion
|
|
4647
|
+
//#region lib/cli/routes/parse-profile-recipe.ts
|
|
4648
|
+
/**
|
|
4649
|
+
* Turns the single-string CLI flags (`--agent`, `--options "<argv>"`,
|
|
4650
|
+
* `--env "k=v,k=v"`, `--resume` / `--no-resume`) into the profile recipe.
|
|
4651
|
+
* A field stays `undefined` when its flag is absent so `profiles.update`
|
|
4652
|
+
* leaves it untouched. `--options` is whitespace-split, so values that
|
|
4653
|
+
* themselves contain spaces are not expressible here — set those via
|
|
4654
|
+
* funnel.json instead.
|
|
4655
|
+
*/
|
|
4656
|
+
const parseProfileRecipe = (query) => {
|
|
4657
|
+
const recipe = {};
|
|
4658
|
+
if (query.agent !== void 0 || query.options !== void 0) {
|
|
4659
|
+
const options = [];
|
|
4660
|
+
if (query.agent !== void 0) options.push("--agent", query.agent);
|
|
4661
|
+
if (query.options !== void 0) {
|
|
4662
|
+
for (const token of query.options.split(/\s+/)) if (token.length > 0) options.push(token);
|
|
4663
|
+
}
|
|
4664
|
+
recipe.options = options;
|
|
4665
|
+
}
|
|
4666
|
+
if (query.env !== void 0) {
|
|
4667
|
+
const env = {};
|
|
4668
|
+
for (const pair of query.env.split(",")) {
|
|
4669
|
+
const trimmed = pair.trim();
|
|
4670
|
+
if (trimmed.length === 0) continue;
|
|
4671
|
+
const eq = trimmed.indexOf("=");
|
|
4672
|
+
if (eq < 0) continue;
|
|
4673
|
+
env[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
|
|
4674
|
+
}
|
|
4675
|
+
recipe.env = env;
|
|
4676
|
+
}
|
|
4677
|
+
if (query["no-resume"] !== void 0) recipe.resume = false;
|
|
4678
|
+
else if (query.resume !== void 0) recipe.resume = query.resume !== "false";
|
|
4679
|
+
return recipe;
|
|
4680
|
+
};
|
|
4681
|
+
//#endregion
|
|
4670
4682
|
//#region lib/cli/routes/profiles.add.$profile.ts
|
|
4671
4683
|
const addHelp = `funnel profiles add — add a profile
|
|
4672
4684
|
|
|
4673
|
-
usage: funnel profiles add <name> --path <path> --channel <channel-name>
|
|
4685
|
+
usage: funnel profiles add <name> --path <path> --channel <channel-name> [recipe]
|
|
4674
4686
|
|
|
4675
4687
|
options:
|
|
4676
|
-
--path
|
|
4677
|
-
--channel
|
|
4688
|
+
--path working directory passed to claude as cwd
|
|
4689
|
+
--channel channel name (resolved to channel id internally)
|
|
4690
|
+
--agent sub-agent name, prepended to the launch argv as --agent <name>
|
|
4691
|
+
--options extra launch argv as one whitespace-split string (e.g. "--brief")
|
|
4692
|
+
--env env vars layered under the process, as "KEY=VAL,KEY2=VAL2"
|
|
4693
|
+
--no-resume start a fresh claude session every launch (default resumes)
|
|
4678
4694
|
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
\`{ name, path, channelId }\`.`;
|
|
4695
|
+
The launch recipe (--agent / --options / --env / --resume) lives on the
|
|
4696
|
+
profile; the channel only declares transport (connectors / delivery).`;
|
|
4682
4697
|
const profilesAddHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({
|
|
4683
4698
|
path: z.string(),
|
|
4684
|
-
channel: z.string()
|
|
4699
|
+
channel: z.string(),
|
|
4700
|
+
agent: z.string().optional(),
|
|
4701
|
+
options: z.string().optional(),
|
|
4702
|
+
env: z.string().optional(),
|
|
4703
|
+
resume: z.string().optional(),
|
|
4704
|
+
"no-resume": z.string().optional()
|
|
4685
4705
|
}), addHelp), (c) => {
|
|
4686
4706
|
const param = c.req.valid("param");
|
|
4687
4707
|
const query = c.req.valid("query");
|
|
4688
4708
|
const funnel = c.var.funnel;
|
|
4689
4709
|
const channel = funnel.channels.get(query.channel);
|
|
4690
4710
|
if (!channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
|
|
4711
|
+
const recipe = parseProfileRecipe(query);
|
|
4691
4712
|
funnel.profiles.add({
|
|
4692
4713
|
name: param.profile,
|
|
4693
4714
|
path: query.path,
|
|
4694
|
-
channelId: channel.id
|
|
4715
|
+
channelId: channel.id,
|
|
4716
|
+
options: recipe.options,
|
|
4717
|
+
env: recipe.env,
|
|
4718
|
+
resume: recipe.resume
|
|
4695
4719
|
});
|
|
4696
4720
|
return c.text(`added profile "${param.profile}"`);
|
|
4697
4721
|
});
|
|
@@ -4735,7 +4759,10 @@ const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.obj
|
|
|
4735
4759
|
channel: profile.channelId,
|
|
4736
4760
|
cwd: profile.path,
|
|
4737
4761
|
userArgs: queryToCliArgs(c.req.url, RESERVED_KEYS),
|
|
4738
|
-
profileName: profile.name
|
|
4762
|
+
profileName: profile.name,
|
|
4763
|
+
options: profile.options,
|
|
4764
|
+
env: profile.env,
|
|
4765
|
+
resume: profile.resume
|
|
4739
4766
|
});
|
|
4740
4767
|
process.exit(exitCode);
|
|
4741
4768
|
});
|
|
@@ -4753,19 +4780,39 @@ const profilesRemoveHandler = factory.createHandlers(zValidator$1("param", z.obj
|
|
|
4753
4780
|
//#region lib/cli/routes/profiles.set.$profile.ts
|
|
4754
4781
|
const setHelp = `funnel profiles <name> set — update a profile
|
|
4755
4782
|
|
|
4756
|
-
usage: funnel profiles <name> set [--path <path>] [--channel <channel-name>]
|
|
4783
|
+
usage: funnel profiles <name> set [--path <path>] [--channel <channel-name>] [recipe]
|
|
4784
|
+
|
|
4785
|
+
options:
|
|
4786
|
+
--path working directory passed to claude as cwd
|
|
4787
|
+
--channel channel name (resolved to channel id internally)
|
|
4788
|
+
--agent sub-agent name, prepended to the launch argv as --agent <name>
|
|
4789
|
+
--options extra launch argv as one whitespace-split string (e.g. "--brief")
|
|
4790
|
+
--env env vars layered under the process, as "KEY=VAL,KEY2=VAL2"
|
|
4791
|
+
--resume / --no-resume toggle claude session reuse
|
|
4792
|
+
|
|
4793
|
+
Only the flags you pass are changed; --agent and --options together replace
|
|
4794
|
+
the profile's whole options list.`;
|
|
4757
4795
|
const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({
|
|
4758
4796
|
path: z.string().optional(),
|
|
4759
|
-
channel: z.string().optional()
|
|
4797
|
+
channel: z.string().optional(),
|
|
4798
|
+
agent: z.string().optional(),
|
|
4799
|
+
options: z.string().optional(),
|
|
4800
|
+
env: z.string().optional(),
|
|
4801
|
+
resume: z.string().optional(),
|
|
4802
|
+
"no-resume": z.string().optional()
|
|
4760
4803
|
}), setHelp), (c) => {
|
|
4761
4804
|
const param = c.req.valid("param");
|
|
4762
4805
|
const query = c.req.valid("query");
|
|
4763
4806
|
const funnel = c.var.funnel;
|
|
4764
4807
|
const channel = query.channel !== void 0 ? funnel.channels.get(query.channel) : null;
|
|
4765
4808
|
if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
|
|
4809
|
+
const recipe = parseProfileRecipe(query);
|
|
4766
4810
|
funnel.profiles.update(param.profile, {
|
|
4767
4811
|
path: query.path,
|
|
4768
|
-
channelId: channel?.id
|
|
4812
|
+
channelId: channel?.id,
|
|
4813
|
+
options: recipe.options,
|
|
4814
|
+
env: recipe.env,
|
|
4815
|
+
resume: recipe.resume
|
|
4769
4816
|
});
|
|
4770
4817
|
return c.text(`updated profile "${param.profile}"`);
|
|
4771
4818
|
});
|
|
@@ -4775,27 +4822,29 @@ usage: funnel profiles [subcommand]
|
|
|
4775
4822
|
|
|
4776
4823
|
subcommands:
|
|
4777
4824
|
(none) list (first entry is the default)
|
|
4778
|
-
add <name> --path <path> --channel <channel>
|
|
4779
|
-
<name> set [--path ...] [--channel ...]
|
|
4825
|
+
add <name> --path <path> --channel <channel> [--agent ...] [--options ...] [--env ...] [--no-resume]
|
|
4826
|
+
<name> set [--path ...] [--channel ...] [--agent ...] [--options ...] [--env ...] [--resume|--no-resume]
|
|
4780
4827
|
<name> as-default move profile to the front (becomes default)
|
|
4781
4828
|
rename <old> <new> rename
|
|
4782
4829
|
remove <name> remove
|
|
4783
4830
|
<name> run launch (sugar for fnl claude -p <name>)
|
|
4784
4831
|
<name> launch (alias for run)
|
|
4785
4832
|
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4833
|
+
A profile carries the launch recipe — \`--agent\` / \`--options\` prepended to
|
|
4834
|
+
the claude argv, \`--env\` layered under the process, \`--resume\` toggling
|
|
4835
|
+
session reuse. The channel it points at only declares transport (connectors).
|
|
4789
4836
|
|
|
4790
4837
|
examples:
|
|
4791
|
-
funnel profiles add cto --path /repo/myapp --channel prod-inbox
|
|
4838
|
+
funnel profiles add cto --path /repo/myapp --channel prod-inbox --agent pm --options "--brief"
|
|
4792
4839
|
funnel profiles cto as-default
|
|
4793
4840
|
funnel profiles cto run`), (c) => {
|
|
4794
4841
|
const profiles = c.var.funnel.profiles.list();
|
|
4795
4842
|
if (profiles.length === 0) return c.text("no profiles");
|
|
4796
4843
|
const lines = profiles.map((profile, index) => {
|
|
4797
4844
|
const tag = index === 0 ? " (default)" : "";
|
|
4798
|
-
|
|
4845
|
+
const recipe = profile.options.length > 0 ? `, options=${profile.options.join(" ")}` : "";
|
|
4846
|
+
const session = profile.resume ? "" : ", resume=false";
|
|
4847
|
+
return `${profile.name}${tag} [path=${profile.path}, channel=${profile.channelId}${recipe}${session}]`;
|
|
4799
4848
|
});
|
|
4800
4849
|
return c.text(lines.join("\n"));
|
|
4801
4850
|
});
|
|
@@ -7112,4 +7161,4 @@ async function launchTui(funnel) {
|
|
|
7112
7161
|
});
|
|
7113
7162
|
}
|
|
7114
7163
|
//#endregion
|
|
7115
|
-
export { DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSessions, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, channelConfigSchema, channelDeliveryModeSchema, channelSpecSchema, app as cliApp, connectorConfigSchema, connectorSpecSchema, createCliApp, createSettings, discordConnectorSchema, factory, funnelEventSchema, funnelJsonSchema, ghConnectorSchema, launchTui, localConfigSchema, profileConfigSchema, publishRequestSchema, publishResponseSchema, queryToCliArgs, scheduleCatchupPolicySchema, scheduleConnectorSchema, scheduleEntrySchema, settingsSchema, slackConnectorSchema, startChannelServer, toRequest };
|
|
7164
|
+
export { DEFAULT_GATEWAY_TOKEN_PATH, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSessions, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, SETTINGS_PATH, SETTINGS_VERSION, 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 };
|
|
@@ -46,6 +46,90 @@ var FunnelSlackAdapter = class extends FunnelConnectorAdapter {
|
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
48
|
//#endregion
|
|
49
|
+
//#region lib/connectors/minify-slack-event.ts
|
|
50
|
+
const TOP_LEVEL_KEYS = [
|
|
51
|
+
"type",
|
|
52
|
+
"subtype",
|
|
53
|
+
"user",
|
|
54
|
+
"bot_id",
|
|
55
|
+
"text",
|
|
56
|
+
"ts",
|
|
57
|
+
"thread_ts",
|
|
58
|
+
"channel",
|
|
59
|
+
"channel_type",
|
|
60
|
+
"files",
|
|
61
|
+
"attachments"
|
|
62
|
+
];
|
|
63
|
+
const FILE_KEYS = [
|
|
64
|
+
"id",
|
|
65
|
+
"name",
|
|
66
|
+
"mimetype",
|
|
67
|
+
"filetype",
|
|
68
|
+
"size",
|
|
69
|
+
"url_private",
|
|
70
|
+
"permalink"
|
|
71
|
+
];
|
|
72
|
+
const ATTACHMENT_KEYS = [
|
|
73
|
+
"title",
|
|
74
|
+
"text",
|
|
75
|
+
"fallback"
|
|
76
|
+
];
|
|
77
|
+
const isRecord = (value) => {
|
|
78
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
79
|
+
};
|
|
80
|
+
const pickDefined = (source, keys) => {
|
|
81
|
+
const picked = {};
|
|
82
|
+
for (const key of keys) if (source[key] !== void 0) picked[key] = source[key];
|
|
83
|
+
return picked;
|
|
84
|
+
};
|
|
85
|
+
const hasThumbOrPreviewKey = (file) => {
|
|
86
|
+
return Object.keys(file).some((key) => key.startsWith("thumb") || key.startsWith("preview"));
|
|
87
|
+
};
|
|
88
|
+
const minifyFile = (file) => {
|
|
89
|
+
if (!isRecord(file)) return file;
|
|
90
|
+
const minified = pickDefined(file, FILE_KEYS);
|
|
91
|
+
if (hasThumbOrPreviewKey(file)) minified._funnel_omitted = ["thumb_*"];
|
|
92
|
+
return minified;
|
|
93
|
+
};
|
|
94
|
+
const flattenRichText = (node) => {
|
|
95
|
+
if (!isRecord(node)) return "";
|
|
96
|
+
const text = node.text;
|
|
97
|
+
if (typeof text === "string") return text;
|
|
98
|
+
const elements = node.elements;
|
|
99
|
+
if (!Array.isArray(elements)) return "";
|
|
100
|
+
return elements.map(flattenRichText).join("");
|
|
101
|
+
};
|
|
102
|
+
const flattenTableRow = (row) => {
|
|
103
|
+
if (!Array.isArray(row)) return "";
|
|
104
|
+
return row.map(flattenRichText).join(" ");
|
|
105
|
+
};
|
|
106
|
+
const flattenBlock = (block) => {
|
|
107
|
+
if (!isRecord(block)) return "";
|
|
108
|
+
if (block.type === "table" && Array.isArray(block.rows)) return block.rows.map(flattenTableRow).join("\n");
|
|
109
|
+
return flattenRichText(block);
|
|
110
|
+
};
|
|
111
|
+
const flattenBlocks = (blocks) => {
|
|
112
|
+
return blocks.map(flattenBlock).filter((line) => line.length > 0).join("\n");
|
|
113
|
+
};
|
|
114
|
+
const minifyAttachment = (attachment) => {
|
|
115
|
+
if (!isRecord(attachment)) return attachment;
|
|
116
|
+
const minified = pickDefined(attachment, ATTACHMENT_KEYS);
|
|
117
|
+
const blocks = attachment.blocks;
|
|
118
|
+
if (Array.isArray(blocks)) {
|
|
119
|
+
const flattened = flattenBlocks(blocks);
|
|
120
|
+
const existingText = typeof minified.text === "string" ? minified.text : "";
|
|
121
|
+
minified.text = existingText ? `${existingText}\n${flattened}` : flattened;
|
|
122
|
+
minified._funnel_omitted = ["blocks"];
|
|
123
|
+
}
|
|
124
|
+
return minified;
|
|
125
|
+
};
|
|
126
|
+
const minifySlackEvent = (event) => {
|
|
127
|
+
const minified = pickDefined(event, TOP_LEVEL_KEYS);
|
|
128
|
+
if (Array.isArray(minified.files)) minified.files = minified.files.map(minifyFile);
|
|
129
|
+
if (Array.isArray(minified.attachments)) minified.attachments = minified.attachments.map(minifyAttachment);
|
|
130
|
+
return minified;
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
49
133
|
//#region lib/connectors/slack-event-processor.ts
|
|
50
134
|
const ALLOWED_EVENTS = new Set(["message", "app_mention"]);
|
|
51
135
|
const ALLOWED_SUBTYPES = new Set([
|
|
@@ -62,11 +146,13 @@ const getString = (event, key) => {
|
|
|
62
146
|
var FunnelSlackEventProcessor = class {
|
|
63
147
|
ownBotUserId;
|
|
64
148
|
ownBotId;
|
|
149
|
+
minify;
|
|
65
150
|
now;
|
|
66
151
|
dedup = /* @__PURE__ */ new Map();
|
|
67
152
|
constructor(props) {
|
|
68
153
|
this.ownBotUserId = props.ownBotUserId;
|
|
69
154
|
this.ownBotId = props.ownBotId;
|
|
155
|
+
this.minify = props.minify ?? true;
|
|
70
156
|
this.now = props.now ?? (() => Date.now());
|
|
71
157
|
}
|
|
72
158
|
process(event) {
|
|
@@ -86,9 +172,10 @@ var FunnelSlackEventProcessor = class {
|
|
|
86
172
|
if (botId === this.ownBotId) return { skip: true };
|
|
87
173
|
const mentioned = (getString(event, "text") ?? "").includes(`<@${this.ownBotUserId}>`);
|
|
88
174
|
const threadTs = getString(event, "thread_ts") ?? getString(event, "ts") ?? "";
|
|
175
|
+
const emitted = this.minify ? minifySlackEvent(event) : event;
|
|
89
176
|
return {
|
|
90
177
|
skip: false,
|
|
91
|
-
content: JSON.stringify(
|
|
178
|
+
content: JSON.stringify(emitted),
|
|
92
179
|
meta: {
|
|
93
180
|
event_type: "slack",
|
|
94
181
|
channel_id: channelId,
|
|
@@ -129,7 +216,8 @@ var FunnelSlackListener = class extends FunnelConnectorListener {
|
|
|
129
216
|
const authResult = await app.client.auth.test({ token: this.config.botToken });
|
|
130
217
|
const processor = new FunnelSlackEventProcessor({
|
|
131
218
|
ownBotUserId: authResult.user_id ?? "",
|
|
132
|
-
ownBotId: authResult.bot_id ?? ""
|
|
219
|
+
ownBotId: authResult.bot_id ?? "",
|
|
220
|
+
minify: this.config.minify
|
|
133
221
|
});
|
|
134
222
|
const preprocess = this.preprocessEvent;
|
|
135
223
|
app.use(async (args) => {
|
|
@@ -179,6 +267,7 @@ const slackConnectorSchema = z.object({
|
|
|
179
267
|
type: z.literal("slack"),
|
|
180
268
|
botToken: z.string().startsWith("xoxb-"),
|
|
181
269
|
appToken: z.string().startsWith("xapp-"),
|
|
270
|
+
minify: z.boolean().default(true),
|
|
182
271
|
createdAt: z.string().datetime().optional(),
|
|
183
272
|
updatedAt: z.string().datetime().optional()
|
|
184
273
|
});
|
|
@@ -9,6 +9,7 @@ declare const slackConnectorSchema: z.ZodObject<{
|
|
|
9
9
|
type: z.ZodLiteral<"slack">;
|
|
10
10
|
botToken: z.ZodString;
|
|
11
11
|
appToken: z.ZodString;
|
|
12
|
+
minify: z.ZodDefault<z.ZodBoolean>;
|
|
12
13
|
createdAt: z.ZodOptional<z.ZodString>;
|
|
13
14
|
updatedAt: z.ZodOptional<z.ZodString>;
|
|
14
15
|
}, z.core.$strip>;
|
|
@@ -31,11 +32,13 @@ type SlackProcessed = SlackProcessedSkip | SlackProcessedEmit;
|
|
|
31
32
|
type Props = {
|
|
32
33
|
ownBotUserId: string;
|
|
33
34
|
ownBotId: string;
|
|
35
|
+
minify?: boolean;
|
|
34
36
|
now?: () => number;
|
|
35
37
|
};
|
|
36
38
|
declare class FunnelSlackEventProcessor {
|
|
37
39
|
private readonly ownBotUserId;
|
|
38
40
|
private readonly ownBotId;
|
|
41
|
+
private readonly minify;
|
|
39
42
|
private readonly now;
|
|
40
43
|
private readonly dedup;
|
|
41
44
|
constructor(props: Props);
|