@interactive-inc/claude-funnel 0.24.0 → 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 -358
- package/dist/gateway/daemon.js +159 -159
- package/dist/index.d.ts +80 -26
- package/dist/index.js +153 -108
- package/funnel.schema.json +38 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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);
|
|
@@ -598,9 +577,9 @@ var FunnelClaude = class {
|
|
|
598
577
|
this.writePidFile(options.profileName);
|
|
599
578
|
this.installCleanup(options.profileName);
|
|
600
579
|
}
|
|
601
|
-
const session =
|
|
602
|
-
const claudeArgs = this.buildArgs(
|
|
603
|
-
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 ?? {});
|
|
604
583
|
this.logger.info(`claude launch`, {
|
|
605
584
|
channel: options.channel,
|
|
606
585
|
channelId: channel.id,
|
|
@@ -650,8 +629,8 @@ var FunnelClaude = class {
|
|
|
650
629
|
isProcessAlive(pid) {
|
|
651
630
|
return this.process.isAlive(pid);
|
|
652
631
|
}
|
|
653
|
-
buildArgs(
|
|
654
|
-
const result = [...
|
|
632
|
+
buildArgs(recipeOptions, userArgs, cwd, session) {
|
|
633
|
+
const result = [...recipeOptions, ...userArgs];
|
|
655
634
|
if (session !== null) if (session.mode === "resume") result.push("--resume", session.id);
|
|
656
635
|
else result.push("--session-id", session.id);
|
|
657
636
|
const mcpName = this.mcp.findInstalledName(cwd);
|
|
@@ -680,9 +659,9 @@ var FunnelClaude = class {
|
|
|
680
659
|
mode: "new"
|
|
681
660
|
};
|
|
682
661
|
}
|
|
683
|
-
buildEnv(channelId,
|
|
662
|
+
buildEnv(channelId, recipeEnv) {
|
|
684
663
|
const env = {};
|
|
685
|
-
for (const [key, value] of Object.entries(
|
|
664
|
+
for (const [key, value] of Object.entries(recipeEnv)) env[key] = value;
|
|
686
665
|
for (const [key, value] of Object.entries(globalThis.process.env)) if (typeof value === "string") env[key] = value;
|
|
687
666
|
env.FUNNEL_CHANNEL_ID = channelId;
|
|
688
667
|
return env;
|
|
@@ -781,16 +760,17 @@ var MemoryFunnelIdGenerator = class extends FunnelIdGenerator {
|
|
|
781
760
|
/**
|
|
782
761
|
* Per-repo launch config (`funnel.json`).
|
|
783
762
|
*
|
|
784
|
-
* `fnl claude` reads this when no --profile is
|
|
785
|
-
* declared channels (`--channel <name>` selects by name; otherwise the
|
|
786
|
-
* entry wins)
|
|
787
|
-
* `~/.funnel/settings.json` on launch — token fields in connectors resolve
|
|
788
|
-
*
|
|
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.
|
|
789
768
|
*
|
|
790
|
-
*
|
|
791
|
-
* channel
|
|
792
|
-
*
|
|
793
|
-
*
|
|
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.
|
|
794
774
|
*/
|
|
795
775
|
const slackEnvSchema = z.object({
|
|
796
776
|
botToken: z.string().optional(),
|
|
@@ -829,7 +809,13 @@ const connectorSpecSchema = z.discriminatedUnion("type", [
|
|
|
829
809
|
]);
|
|
830
810
|
const channelSpecSchema = z.object({
|
|
831
811
|
name: z.string(),
|
|
832
|
-
|
|
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. */
|
|
833
819
|
options: z.array(z.string()).optional(),
|
|
834
820
|
/** Env vars layered under the launched claude process. process.env wins on collision. */
|
|
835
821
|
env: z.record(z.string(), z.string()).optional(),
|
|
@@ -837,15 +823,16 @@ const channelSpecSchema = z.object({
|
|
|
837
823
|
* When true (the default), funnel injects `--session-id <uuid>` so that
|
|
838
824
|
* relaunching from the same cwd resumes the previous claude session
|
|
839
825
|
* without bleeding into other channels or workspaces. Set to false for
|
|
840
|
-
*
|
|
826
|
+
* profiles that should always start a fresh session.
|
|
841
827
|
*/
|
|
842
|
-
resume: z.boolean().optional()
|
|
843
|
-
connectors: z.array(connectorSpecSchema).optional()
|
|
828
|
+
resume: z.boolean().optional()
|
|
844
829
|
});
|
|
845
830
|
const localConfigSchema = z.object({
|
|
846
831
|
$schema: z.string().optional(),
|
|
847
|
-
/** Declared channels. First entry is the default; --channel <name> selects by name. */
|
|
848
|
-
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()
|
|
849
836
|
});
|
|
850
837
|
const LOCAL_CONFIG_FILENAME = "funnel.json";
|
|
851
838
|
const LOCAL_ENV_FILENAME = ".env.local";
|
|
@@ -932,17 +919,6 @@ var FunnelLocalConfig = class {
|
|
|
932
919
|
var FunnelTokenPrompter = class {};
|
|
933
920
|
//#endregion
|
|
934
921
|
//#region lib/engine/local-config/local-config-sync.ts
|
|
935
|
-
const arraysEqual = (a, b) => {
|
|
936
|
-
if (a.length !== b.length) return false;
|
|
937
|
-
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
938
|
-
return true;
|
|
939
|
-
};
|
|
940
|
-
const recordsEqual = (a, b) => {
|
|
941
|
-
const keys = Object.keys(a);
|
|
942
|
-
if (keys.length !== Object.keys(b).length) return false;
|
|
943
|
-
for (const key of keys) if (a[key] !== b[key]) return false;
|
|
944
|
-
return true;
|
|
945
|
-
};
|
|
946
922
|
/**
|
|
947
923
|
* Reconciles a single funnel.json channel spec with `~/.funnel/settings.json`.
|
|
948
924
|
* The spec is the source of truth for the channel it declares:
|
|
@@ -975,21 +951,7 @@ var FunnelLocalConfigSync = class {
|
|
|
975
951
|
Object.freeze(this);
|
|
976
952
|
}
|
|
977
953
|
async ensure(channel, cwd) {
|
|
978
|
-
|
|
979
|
-
const nextResume = channel.resume ?? true;
|
|
980
|
-
if (!existing) this.channels.add({
|
|
981
|
-
name: channel.name,
|
|
982
|
-
options: channel.options ?? [],
|
|
983
|
-
env: channel.env ?? {},
|
|
984
|
-
resume: nextResume
|
|
985
|
-
});
|
|
986
|
-
else {
|
|
987
|
-
const nextOptions = channel.options ?? [];
|
|
988
|
-
const nextEnv = channel.env ?? {};
|
|
989
|
-
if (!arraysEqual(existing.options, nextOptions)) this.channels.setOptions(channel.name, nextOptions);
|
|
990
|
-
if (!recordsEqual(existing.env, nextEnv)) this.channels.setEnv(channel.name, nextEnv);
|
|
991
|
-
if (existing.resume !== nextResume) this.channels.setResume(channel.name, nextResume);
|
|
992
|
-
}
|
|
954
|
+
if (!this.channels.get(channel.name)) this.channels.add({ name: channel.name });
|
|
993
955
|
if (channel.connectors === void 0) return {
|
|
994
956
|
touched: [],
|
|
995
957
|
removed: []
|
|
@@ -1428,9 +1390,10 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
|
|
|
1428
1390
|
//#region lib/engine/profiles/profiles.ts
|
|
1429
1391
|
/**
|
|
1430
1392
|
* Named launch presets for `fnl claude`. Each profile bundles a working
|
|
1431
|
-
* directory,
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
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.
|
|
1434
1397
|
*
|
|
1435
1398
|
* The first entry in the persisted array is treated as the default profile;
|
|
1436
1399
|
* `asDefault` reorders the array to put a named profile first.
|
|
@@ -1453,11 +1416,18 @@ var FunnelProfiles = class {
|
|
|
1453
1416
|
getDefault() {
|
|
1454
1417
|
return this.list()[0] ?? null;
|
|
1455
1418
|
}
|
|
1456
|
-
add(
|
|
1419
|
+
add(input) {
|
|
1457
1420
|
const settings = this.store.read();
|
|
1458
|
-
if (settings.profiles.some((p) => p.name ===
|
|
1459
|
-
if (!settings.channels.some((c) => c.id ===
|
|
1460
|
-
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
|
+
});
|
|
1461
1431
|
this.store.write(settings);
|
|
1462
1432
|
}
|
|
1463
1433
|
remove(name) {
|
|
@@ -1497,6 +1467,9 @@ var FunnelProfiles = class {
|
|
|
1497
1467
|
profile.channelId = fields.channelId;
|
|
1498
1468
|
}
|
|
1499
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;
|
|
1500
1473
|
this.store.write(settings);
|
|
1501
1474
|
}
|
|
1502
1475
|
};
|
|
@@ -3847,14 +3820,14 @@ const startChannelServer = async (options = {}) => {
|
|
|
3847
3820
|
/**
|
|
3848
3821
|
* Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
|
|
3849
3822
|
* `$schema` references in committed `funnel.json` files so editors can give
|
|
3850
|
-
* autocomplete and validation for
|
|
3851
|
-
* 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.
|
|
3852
3825
|
*/
|
|
3853
3826
|
const funnelJsonSchema = () => {
|
|
3854
3827
|
return {
|
|
3855
3828
|
...z.toJSONSchema(localConfigSchema, { target: "draft-2020-12" }),
|
|
3856
3829
|
title: "Funnel per-repo launch config",
|
|
3857
|
-
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."
|
|
3858
3831
|
};
|
|
3859
3832
|
};
|
|
3860
3833
|
//#endregion
|
|
@@ -4671,31 +4644,78 @@ examples:
|
|
|
4671
4644
|
return c.text("funnel gateway: stopped");
|
|
4672
4645
|
});
|
|
4673
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
|
|
4674
4682
|
//#region lib/cli/routes/profiles.add.$profile.ts
|
|
4675
4683
|
const addHelp = `funnel profiles add — add a profile
|
|
4676
4684
|
|
|
4677
|
-
usage: funnel profiles add <name> --path <path> --channel <channel-name>
|
|
4685
|
+
usage: funnel profiles add <name> --path <path> --channel <channel-name> [recipe]
|
|
4678
4686
|
|
|
4679
4687
|
options:
|
|
4680
|
-
--path
|
|
4681
|
-
--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)
|
|
4682
4694
|
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
\`{ name, path, channelId }\`.`;
|
|
4695
|
+
The launch recipe (--agent / --options / --env / --resume) lives on the
|
|
4696
|
+
profile; the channel only declares transport (connectors / delivery).`;
|
|
4686
4697
|
const profilesAddHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({
|
|
4687
4698
|
path: z.string(),
|
|
4688
|
-
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()
|
|
4689
4705
|
}), addHelp), (c) => {
|
|
4690
4706
|
const param = c.req.valid("param");
|
|
4691
4707
|
const query = c.req.valid("query");
|
|
4692
4708
|
const funnel = c.var.funnel;
|
|
4693
4709
|
const channel = funnel.channels.get(query.channel);
|
|
4694
4710
|
if (!channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
|
|
4711
|
+
const recipe = parseProfileRecipe(query);
|
|
4695
4712
|
funnel.profiles.add({
|
|
4696
4713
|
name: param.profile,
|
|
4697
4714
|
path: query.path,
|
|
4698
|
-
channelId: channel.id
|
|
4715
|
+
channelId: channel.id,
|
|
4716
|
+
options: recipe.options,
|
|
4717
|
+
env: recipe.env,
|
|
4718
|
+
resume: recipe.resume
|
|
4699
4719
|
});
|
|
4700
4720
|
return c.text(`added profile "${param.profile}"`);
|
|
4701
4721
|
});
|
|
@@ -4739,7 +4759,10 @@ const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.obj
|
|
|
4739
4759
|
channel: profile.channelId,
|
|
4740
4760
|
cwd: profile.path,
|
|
4741
4761
|
userArgs: queryToCliArgs(c.req.url, RESERVED_KEYS),
|
|
4742
|
-
profileName: profile.name
|
|
4762
|
+
profileName: profile.name,
|
|
4763
|
+
options: profile.options,
|
|
4764
|
+
env: profile.env,
|
|
4765
|
+
resume: profile.resume
|
|
4743
4766
|
});
|
|
4744
4767
|
process.exit(exitCode);
|
|
4745
4768
|
});
|
|
@@ -4757,19 +4780,39 @@ const profilesRemoveHandler = factory.createHandlers(zValidator$1("param", z.obj
|
|
|
4757
4780
|
//#region lib/cli/routes/profiles.set.$profile.ts
|
|
4758
4781
|
const setHelp = `funnel profiles <name> set — update a profile
|
|
4759
4782
|
|
|
4760
|
-
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.`;
|
|
4761
4795
|
const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({
|
|
4762
4796
|
path: z.string().optional(),
|
|
4763
|
-
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()
|
|
4764
4803
|
}), setHelp), (c) => {
|
|
4765
4804
|
const param = c.req.valid("param");
|
|
4766
4805
|
const query = c.req.valid("query");
|
|
4767
4806
|
const funnel = c.var.funnel;
|
|
4768
4807
|
const channel = query.channel !== void 0 ? funnel.channels.get(query.channel) : null;
|
|
4769
4808
|
if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
|
|
4809
|
+
const recipe = parseProfileRecipe(query);
|
|
4770
4810
|
funnel.profiles.update(param.profile, {
|
|
4771
4811
|
path: query.path,
|
|
4772
|
-
channelId: channel?.id
|
|
4812
|
+
channelId: channel?.id,
|
|
4813
|
+
options: recipe.options,
|
|
4814
|
+
env: recipe.env,
|
|
4815
|
+
resume: recipe.resume
|
|
4773
4816
|
});
|
|
4774
4817
|
return c.text(`updated profile "${param.profile}"`);
|
|
4775
4818
|
});
|
|
@@ -4779,27 +4822,29 @@ usage: funnel profiles [subcommand]
|
|
|
4779
4822
|
|
|
4780
4823
|
subcommands:
|
|
4781
4824
|
(none) list (first entry is the default)
|
|
4782
|
-
add <name> --path <path> --channel <channel>
|
|
4783
|
-
<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]
|
|
4784
4827
|
<name> as-default move profile to the front (becomes default)
|
|
4785
4828
|
rename <old> <new> rename
|
|
4786
4829
|
remove <name> remove
|
|
4787
4830
|
<name> run launch (sugar for fnl claude -p <name>)
|
|
4788
4831
|
<name> launch (alias for run)
|
|
4789
4832
|
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
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).
|
|
4793
4836
|
|
|
4794
4837
|
examples:
|
|
4795
|
-
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"
|
|
4796
4839
|
funnel profiles cto as-default
|
|
4797
4840
|
funnel profiles cto run`), (c) => {
|
|
4798
4841
|
const profiles = c.var.funnel.profiles.list();
|
|
4799
4842
|
if (profiles.length === 0) return c.text("no profiles");
|
|
4800
4843
|
const lines = profiles.map((profile, index) => {
|
|
4801
4844
|
const tag = index === 0 ? " (default)" : "";
|
|
4802
|
-
|
|
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}]`;
|
|
4803
4848
|
});
|
|
4804
4849
|
return c.text(lines.join("\n"));
|
|
4805
4850
|
});
|
|
@@ -7116,4 +7161,4 @@ async function launchTui(funnel) {
|
|
|
7116
7161
|
});
|
|
7117
7162
|
}
|
|
7118
7163
|
//#endregion
|
|
7119
|
-
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 };
|
package/funnel.schema.json
CHANGED
|
@@ -14,24 +14,6 @@
|
|
|
14
14
|
"name": {
|
|
15
15
|
"type": "string"
|
|
16
16
|
},
|
|
17
|
-
"options": {
|
|
18
|
-
"type": "array",
|
|
19
|
-
"items": {
|
|
20
|
-
"type": "string"
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
"env": {
|
|
24
|
-
"type": "object",
|
|
25
|
-
"propertyNames": {
|
|
26
|
-
"type": "string"
|
|
27
|
-
},
|
|
28
|
-
"additionalProperties": {
|
|
29
|
-
"type": "string"
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"resume": {
|
|
33
|
-
"type": "boolean"
|
|
34
|
-
},
|
|
35
17
|
"connectors": {
|
|
36
18
|
"type": "array",
|
|
37
19
|
"items": {
|
|
@@ -151,6 +133,43 @@
|
|
|
151
133
|
],
|
|
152
134
|
"additionalProperties": false
|
|
153
135
|
}
|
|
136
|
+
},
|
|
137
|
+
"profiles": {
|
|
138
|
+
"type": "array",
|
|
139
|
+
"items": {
|
|
140
|
+
"type": "object",
|
|
141
|
+
"properties": {
|
|
142
|
+
"name": {
|
|
143
|
+
"type": "string"
|
|
144
|
+
},
|
|
145
|
+
"channel": {
|
|
146
|
+
"type": "string"
|
|
147
|
+
},
|
|
148
|
+
"options": {
|
|
149
|
+
"type": "array",
|
|
150
|
+
"items": {
|
|
151
|
+
"type": "string"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"env": {
|
|
155
|
+
"type": "object",
|
|
156
|
+
"propertyNames": {
|
|
157
|
+
"type": "string"
|
|
158
|
+
},
|
|
159
|
+
"additionalProperties": {
|
|
160
|
+
"type": "string"
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
"resume": {
|
|
164
|
+
"type": "boolean"
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
"required": [
|
|
168
|
+
"name",
|
|
169
|
+
"channel"
|
|
170
|
+
],
|
|
171
|
+
"additionalProperties": false
|
|
172
|
+
}
|
|
154
173
|
}
|
|
155
174
|
},
|
|
156
175
|
"required": [
|
|
@@ -158,6 +177,6 @@
|
|
|
158
177
|
],
|
|
159
178
|
"additionalProperties": false,
|
|
160
179
|
"title": "Funnel per-repo launch config",
|
|
161
|
-
"description": "Used by `fnl claude`
|
|
180
|
+
"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."
|
|
162
181
|
}
|
|
163
182
|
|
package/package.json
CHANGED