@interactive-inc/claude-funnel 0.24.0 → 0.25.1
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 +185 -185
- package/dist/index.d.ts +95 -26
- package/dist/index.js +174 -110
- 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 ?? [], options.env ?? {}) : 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);
|
|
@@ -663,15 +642,22 @@ var FunnelClaude = class {
|
|
|
663
642
|
* a freshly minted one. Backs off when the user already passed a
|
|
664
643
|
* session-shaping flag, since combining them would either confuse claude
|
|
665
644
|
* or override the explicit user intent.
|
|
645
|
+
*
|
|
646
|
+
* A persisted id is only resumed when its session jsonl still exists on
|
|
647
|
+
* disk. claude errors out on `--resume <id>` for a missing conversation, and
|
|
648
|
+
* a persisted id can outlive its jsonl (claude pruned it, or the very first
|
|
649
|
+
* launch was aborted after `create` wrote the id but before the jsonl
|
|
650
|
+
* appeared). When the file is gone we mint a fresh session instead, which
|
|
651
|
+
* overwrites the dangling entry — so the store self-heals.
|
|
666
652
|
*/
|
|
667
|
-
resolveSession(channelId, cwd, userArgs) {
|
|
653
|
+
resolveSession(channelId, cwd, userArgs, recipeEnv) {
|
|
668
654
|
for (const arg of userArgs) {
|
|
669
655
|
if (arg === "-c" || arg === "--continue") return null;
|
|
670
656
|
if (arg === "--resume" || arg.startsWith("--resume=")) return null;
|
|
671
657
|
if (arg === "--session-id" || arg.startsWith("--session-id=")) return null;
|
|
672
658
|
}
|
|
673
659
|
const existing = this.sessions.get(channelId, cwd);
|
|
674
|
-
if (existing !== null) return {
|
|
660
|
+
if (existing !== null && this.sessionFileExists(cwd, existing, recipeEnv)) return {
|
|
675
661
|
id: existing,
|
|
676
662
|
mode: "resume"
|
|
677
663
|
};
|
|
@@ -680,9 +666,21 @@ var FunnelClaude = class {
|
|
|
680
666
|
mode: "new"
|
|
681
667
|
};
|
|
682
668
|
}
|
|
683
|
-
|
|
669
|
+
/**
|
|
670
|
+
* Mirrors claude's session storage path
|
|
671
|
+
* (`<config-dir>/projects/<cwd-with-slashes-as-dashes>/<id>.jsonl`) to check
|
|
672
|
+
* whether a recorded session still exists. Reads the same `CLAUDE_CONFIG_DIR`
|
|
673
|
+
* the child will run under so the check matches reality; a wrong guess can
|
|
674
|
+
* only ever produce a false negative (start fresh), never a bad resume.
|
|
675
|
+
*/
|
|
676
|
+
sessionFileExists(cwd, sessionId, recipeEnv) {
|
|
677
|
+
const configDir = recipeEnv.CLAUDE_CONFIG_DIR ?? globalThis.process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
|
|
678
|
+
const projectSlug = cwd.replace(/\//g, "-");
|
|
679
|
+
return this.fs.existsSync(join(configDir, "projects", projectSlug, `${sessionId}.jsonl`));
|
|
680
|
+
}
|
|
681
|
+
buildEnv(channelId, recipeEnv) {
|
|
684
682
|
const env = {};
|
|
685
|
-
for (const [key, value] of Object.entries(
|
|
683
|
+
for (const [key, value] of Object.entries(recipeEnv)) env[key] = value;
|
|
686
684
|
for (const [key, value] of Object.entries(globalThis.process.env)) if (typeof value === "string") env[key] = value;
|
|
687
685
|
env.FUNNEL_CHANNEL_ID = channelId;
|
|
688
686
|
return env;
|
|
@@ -781,16 +779,17 @@ var MemoryFunnelIdGenerator = class extends FunnelIdGenerator {
|
|
|
781
779
|
/**
|
|
782
780
|
* Per-repo launch config (`funnel.json`).
|
|
783
781
|
*
|
|
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
|
-
*
|
|
782
|
+
* `fnl claude` reads this when no global --profile preset is used. It picks one
|
|
783
|
+
* of the declared channels (`--channel <name>` selects by name; otherwise the
|
|
784
|
+
* first entry wins) and materializes its transport (connectors / delivery) into
|
|
785
|
+
* `~/.funnel/settings.json` on launch — token fields in connectors resolve via
|
|
786
|
+
* literal / `env.<field>` / TTY prompt.
|
|
789
787
|
*
|
|
790
|
-
*
|
|
791
|
-
* channel
|
|
792
|
-
*
|
|
793
|
-
*
|
|
788
|
+
* The launch recipe (`options` / `env` / `resume`) lives on `profiles[]`, not on
|
|
789
|
+
* the channel: a channel only describes where events come from. `fnl claude`
|
|
790
|
+
* applies the first profile bound to the chosen channel (or `--profile <name>`
|
|
791
|
+
* to pick another); the recipe is passed straight to the launcher and is not
|
|
792
|
+
* persisted into the global profile list.
|
|
794
793
|
*/
|
|
795
794
|
const slackEnvSchema = z.object({
|
|
796
795
|
botToken: z.string().optional(),
|
|
@@ -829,7 +828,13 @@ const connectorSpecSchema = z.discriminatedUnion("type", [
|
|
|
829
828
|
]);
|
|
830
829
|
const channelSpecSchema = z.object({
|
|
831
830
|
name: z.string(),
|
|
832
|
-
|
|
831
|
+
connectors: z.array(connectorSpecSchema).optional()
|
|
832
|
+
});
|
|
833
|
+
const profileSpecSchema = z.object({
|
|
834
|
+
name: z.string(),
|
|
835
|
+
/** Name of the channel (declared in `channels[]`) this profile subscribes to. */
|
|
836
|
+
channel: z.string(),
|
|
837
|
+
/** Args prepended to the claude argv on every launch through this profile. */
|
|
833
838
|
options: z.array(z.string()).optional(),
|
|
834
839
|
/** Env vars layered under the launched claude process. process.env wins on collision. */
|
|
835
840
|
env: z.record(z.string(), z.string()).optional(),
|
|
@@ -837,15 +842,16 @@ const channelSpecSchema = z.object({
|
|
|
837
842
|
* When true (the default), funnel injects `--session-id <uuid>` so that
|
|
838
843
|
* relaunching from the same cwd resumes the previous claude session
|
|
839
844
|
* without bleeding into other channels or workspaces. Set to false for
|
|
840
|
-
*
|
|
845
|
+
* profiles that should always start a fresh session.
|
|
841
846
|
*/
|
|
842
|
-
resume: z.boolean().optional()
|
|
843
|
-
connectors: z.array(connectorSpecSchema).optional()
|
|
847
|
+
resume: z.boolean().optional()
|
|
844
848
|
});
|
|
845
849
|
const localConfigSchema = z.object({
|
|
846
850
|
$schema: z.string().optional(),
|
|
847
|
-
/** Declared channels. First entry is the default; --channel <name> selects by name. */
|
|
848
|
-
channels: z.array(channelSpecSchema).min(1)
|
|
851
|
+
/** Declared channels (transport only). First entry is the default; --channel <name> selects by name. */
|
|
852
|
+
channels: z.array(channelSpecSchema).min(1),
|
|
853
|
+
/** Launch presets bound to a channel. First entry bound to the chosen channel is the default. */
|
|
854
|
+
profiles: z.array(profileSpecSchema).optional()
|
|
849
855
|
});
|
|
850
856
|
const LOCAL_CONFIG_FILENAME = "funnel.json";
|
|
851
857
|
const LOCAL_ENV_FILENAME = ".env.local";
|
|
@@ -932,17 +938,6 @@ var FunnelLocalConfig = class {
|
|
|
932
938
|
var FunnelTokenPrompter = class {};
|
|
933
939
|
//#endregion
|
|
934
940
|
//#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
941
|
/**
|
|
947
942
|
* Reconciles a single funnel.json channel spec with `~/.funnel/settings.json`.
|
|
948
943
|
* The spec is the source of truth for the channel it declares:
|
|
@@ -975,21 +970,7 @@ var FunnelLocalConfigSync = class {
|
|
|
975
970
|
Object.freeze(this);
|
|
976
971
|
}
|
|
977
972
|
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
|
-
}
|
|
973
|
+
if (!this.channels.get(channel.name)) this.channels.add({ name: channel.name });
|
|
993
974
|
if (channel.connectors === void 0) return {
|
|
994
975
|
touched: [],
|
|
995
976
|
removed: []
|
|
@@ -1428,9 +1409,10 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
|
|
|
1428
1409
|
//#region lib/engine/profiles/profiles.ts
|
|
1429
1410
|
/**
|
|
1430
1411
|
* Named launch presets for `fnl claude`. Each profile bundles a working
|
|
1431
|
-
* directory,
|
|
1432
|
-
*
|
|
1433
|
-
*
|
|
1412
|
+
* directory, the channel id its Claude instance subscribes to, and the launch
|
|
1413
|
+
* recipe (`options` prepended to the claude argv, `env` layered under the
|
|
1414
|
+
* process, `resume` toggling session reuse). Implements ProfileChannelChecker
|
|
1415
|
+
* so FunnelChannels can refuse to remove a channel that is still referenced.
|
|
1434
1416
|
*
|
|
1435
1417
|
* The first entry in the persisted array is treated as the default profile;
|
|
1436
1418
|
* `asDefault` reorders the array to put a named profile first.
|
|
@@ -1453,11 +1435,18 @@ var FunnelProfiles = class {
|
|
|
1453
1435
|
getDefault() {
|
|
1454
1436
|
return this.list()[0] ?? null;
|
|
1455
1437
|
}
|
|
1456
|
-
add(
|
|
1438
|
+
add(input) {
|
|
1457
1439
|
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(
|
|
1440
|
+
if (settings.profiles.some((p) => p.name === input.name)) throw new Error(`profile "${input.name}" already exists`);
|
|
1441
|
+
if (!settings.channels.some((c) => c.id === input.channelId)) throw new Error(`channel id "${input.channelId}" not found`);
|
|
1442
|
+
settings.profiles.push({
|
|
1443
|
+
name: input.name,
|
|
1444
|
+
path: input.path,
|
|
1445
|
+
channelId: input.channelId,
|
|
1446
|
+
options: input.options ?? [],
|
|
1447
|
+
env: input.env ?? {},
|
|
1448
|
+
resume: input.resume ?? true
|
|
1449
|
+
});
|
|
1461
1450
|
this.store.write(settings);
|
|
1462
1451
|
}
|
|
1463
1452
|
remove(name) {
|
|
@@ -1497,6 +1486,9 @@ var FunnelProfiles = class {
|
|
|
1497
1486
|
profile.channelId = fields.channelId;
|
|
1498
1487
|
}
|
|
1499
1488
|
if (fields.path !== void 0) profile.path = fields.path;
|
|
1489
|
+
if (fields.options !== void 0) profile.options = fields.options;
|
|
1490
|
+
if (fields.env !== void 0) profile.env = fields.env;
|
|
1491
|
+
if (fields.resume !== void 0) profile.resume = fields.resume;
|
|
1500
1492
|
this.store.write(settings);
|
|
1501
1493
|
}
|
|
1502
1494
|
};
|
|
@@ -3847,14 +3839,14 @@ const startChannelServer = async (options = {}) => {
|
|
|
3847
3839
|
/**
|
|
3848
3840
|
* Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
|
|
3849
3841
|
* `$schema` references in committed `funnel.json` files so editors can give
|
|
3850
|
-
* autocomplete and validation for
|
|
3851
|
-
* without anyone hand-maintaining a separate schema.
|
|
3842
|
+
* autocomplete and validation for channels[] (transport) and profiles[]
|
|
3843
|
+
* (launch recipe) without anyone hand-maintaining a separate schema.
|
|
3852
3844
|
*/
|
|
3853
3845
|
const funnelJsonSchema = () => {
|
|
3854
3846
|
return {
|
|
3855
3847
|
...z.toJSONSchema(localConfigSchema, { target: "draft-2020-12" }),
|
|
3856
3848
|
title: "Funnel per-repo launch config",
|
|
3857
|
-
description: "Used by `fnl claude`
|
|
3849
|
+
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
3850
|
};
|
|
3859
3851
|
};
|
|
3860
3852
|
//#endregion
|
|
@@ -4671,31 +4663,78 @@ examples:
|
|
|
4671
4663
|
return c.text("funnel gateway: stopped");
|
|
4672
4664
|
});
|
|
4673
4665
|
//#endregion
|
|
4666
|
+
//#region lib/cli/routes/parse-profile-recipe.ts
|
|
4667
|
+
/**
|
|
4668
|
+
* Turns the single-string CLI flags (`--agent`, `--options "<argv>"`,
|
|
4669
|
+
* `--env "k=v,k=v"`, `--resume` / `--no-resume`) into the profile recipe.
|
|
4670
|
+
* A field stays `undefined` when its flag is absent so `profiles.update`
|
|
4671
|
+
* leaves it untouched. `--options` is whitespace-split, so values that
|
|
4672
|
+
* themselves contain spaces are not expressible here — set those via
|
|
4673
|
+
* funnel.json instead.
|
|
4674
|
+
*/
|
|
4675
|
+
const parseProfileRecipe = (query) => {
|
|
4676
|
+
const recipe = {};
|
|
4677
|
+
if (query.agent !== void 0 || query.options !== void 0) {
|
|
4678
|
+
const options = [];
|
|
4679
|
+
if (query.agent !== void 0) options.push("--agent", query.agent);
|
|
4680
|
+
if (query.options !== void 0) {
|
|
4681
|
+
for (const token of query.options.split(/\s+/)) if (token.length > 0) options.push(token);
|
|
4682
|
+
}
|
|
4683
|
+
recipe.options = options;
|
|
4684
|
+
}
|
|
4685
|
+
if (query.env !== void 0) {
|
|
4686
|
+
const env = {};
|
|
4687
|
+
for (const pair of query.env.split(",")) {
|
|
4688
|
+
const trimmed = pair.trim();
|
|
4689
|
+
if (trimmed.length === 0) continue;
|
|
4690
|
+
const eq = trimmed.indexOf("=");
|
|
4691
|
+
if (eq < 0) continue;
|
|
4692
|
+
env[trimmed.slice(0, eq)] = trimmed.slice(eq + 1);
|
|
4693
|
+
}
|
|
4694
|
+
recipe.env = env;
|
|
4695
|
+
}
|
|
4696
|
+
if (query["no-resume"] !== void 0) recipe.resume = false;
|
|
4697
|
+
else if (query.resume !== void 0) recipe.resume = query.resume !== "false";
|
|
4698
|
+
return recipe;
|
|
4699
|
+
};
|
|
4700
|
+
//#endregion
|
|
4674
4701
|
//#region lib/cli/routes/profiles.add.$profile.ts
|
|
4675
4702
|
const addHelp = `funnel profiles add — add a profile
|
|
4676
4703
|
|
|
4677
|
-
usage: funnel profiles add <name> --path <path> --channel <channel-name>
|
|
4704
|
+
usage: funnel profiles add <name> --path <path> --channel <channel-name> [recipe]
|
|
4678
4705
|
|
|
4679
4706
|
options:
|
|
4680
|
-
--path
|
|
4681
|
-
--channel
|
|
4707
|
+
--path working directory passed to claude as cwd
|
|
4708
|
+
--channel channel name (resolved to channel id internally)
|
|
4709
|
+
--agent sub-agent name, prepended to the launch argv as --agent <name>
|
|
4710
|
+
--options extra launch argv as one whitespace-split string (e.g. "--brief")
|
|
4711
|
+
--env env vars layered under the process, as "KEY=VAL,KEY2=VAL2"
|
|
4712
|
+
--no-resume start a fresh claude session every launch (default resumes)
|
|
4682
4713
|
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
\`{ name, path, channelId }\`.`;
|
|
4714
|
+
The launch recipe (--agent / --options / --env / --resume) lives on the
|
|
4715
|
+
profile; the channel only declares transport (connectors / delivery).`;
|
|
4686
4716
|
const profilesAddHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({
|
|
4687
4717
|
path: z.string(),
|
|
4688
|
-
channel: z.string()
|
|
4718
|
+
channel: z.string(),
|
|
4719
|
+
agent: z.string().optional(),
|
|
4720
|
+
options: z.string().optional(),
|
|
4721
|
+
env: z.string().optional(),
|
|
4722
|
+
resume: z.string().optional(),
|
|
4723
|
+
"no-resume": z.string().optional()
|
|
4689
4724
|
}), addHelp), (c) => {
|
|
4690
4725
|
const param = c.req.valid("param");
|
|
4691
4726
|
const query = c.req.valid("query");
|
|
4692
4727
|
const funnel = c.var.funnel;
|
|
4693
4728
|
const channel = funnel.channels.get(query.channel);
|
|
4694
4729
|
if (!channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
|
|
4730
|
+
const recipe = parseProfileRecipe(query);
|
|
4695
4731
|
funnel.profiles.add({
|
|
4696
4732
|
name: param.profile,
|
|
4697
4733
|
path: query.path,
|
|
4698
|
-
channelId: channel.id
|
|
4734
|
+
channelId: channel.id,
|
|
4735
|
+
options: recipe.options,
|
|
4736
|
+
env: recipe.env,
|
|
4737
|
+
resume: recipe.resume
|
|
4699
4738
|
});
|
|
4700
4739
|
return c.text(`added profile "${param.profile}"`);
|
|
4701
4740
|
});
|
|
@@ -4739,7 +4778,10 @@ const profilesLaunchHandler = factory.createHandlers(zValidator$1("param", z.obj
|
|
|
4739
4778
|
channel: profile.channelId,
|
|
4740
4779
|
cwd: profile.path,
|
|
4741
4780
|
userArgs: queryToCliArgs(c.req.url, RESERVED_KEYS),
|
|
4742
|
-
profileName: profile.name
|
|
4781
|
+
profileName: profile.name,
|
|
4782
|
+
options: profile.options,
|
|
4783
|
+
env: profile.env,
|
|
4784
|
+
resume: profile.resume
|
|
4743
4785
|
});
|
|
4744
4786
|
process.exit(exitCode);
|
|
4745
4787
|
});
|
|
@@ -4757,19 +4799,39 @@ const profilesRemoveHandler = factory.createHandlers(zValidator$1("param", z.obj
|
|
|
4757
4799
|
//#region lib/cli/routes/profiles.set.$profile.ts
|
|
4758
4800
|
const setHelp = `funnel profiles <name> set — update a profile
|
|
4759
4801
|
|
|
4760
|
-
usage: funnel profiles <name> set [--path <path>] [--channel <channel-name>]
|
|
4802
|
+
usage: funnel profiles <name> set [--path <path>] [--channel <channel-name>] [recipe]
|
|
4803
|
+
|
|
4804
|
+
options:
|
|
4805
|
+
--path working directory passed to claude as cwd
|
|
4806
|
+
--channel channel name (resolved to channel id internally)
|
|
4807
|
+
--agent sub-agent name, prepended to the launch argv as --agent <name>
|
|
4808
|
+
--options extra launch argv as one whitespace-split string (e.g. "--brief")
|
|
4809
|
+
--env env vars layered under the process, as "KEY=VAL,KEY2=VAL2"
|
|
4810
|
+
--resume / --no-resume toggle claude session reuse
|
|
4811
|
+
|
|
4812
|
+
Only the flags you pass are changed; --agent and --options together replace
|
|
4813
|
+
the profile's whole options list.`;
|
|
4761
4814
|
const profilesSetHandler = factory.createHandlers(zValidator$1("param", z.object({ profile: z.string() })), zValidator$1("query", z.object({
|
|
4762
4815
|
path: z.string().optional(),
|
|
4763
|
-
channel: z.string().optional()
|
|
4816
|
+
channel: z.string().optional(),
|
|
4817
|
+
agent: z.string().optional(),
|
|
4818
|
+
options: z.string().optional(),
|
|
4819
|
+
env: z.string().optional(),
|
|
4820
|
+
resume: z.string().optional(),
|
|
4821
|
+
"no-resume": z.string().optional()
|
|
4764
4822
|
}), setHelp), (c) => {
|
|
4765
4823
|
const param = c.req.valid("param");
|
|
4766
4824
|
const query = c.req.valid("query");
|
|
4767
4825
|
const funnel = c.var.funnel;
|
|
4768
4826
|
const channel = query.channel !== void 0 ? funnel.channels.get(query.channel) : null;
|
|
4769
4827
|
if (query.channel !== void 0 && !channel) throw new HTTPException(400, { message: `channel "${query.channel}" not found` });
|
|
4828
|
+
const recipe = parseProfileRecipe(query);
|
|
4770
4829
|
funnel.profiles.update(param.profile, {
|
|
4771
4830
|
path: query.path,
|
|
4772
|
-
channelId: channel?.id
|
|
4831
|
+
channelId: channel?.id,
|
|
4832
|
+
options: recipe.options,
|
|
4833
|
+
env: recipe.env,
|
|
4834
|
+
resume: recipe.resume
|
|
4773
4835
|
});
|
|
4774
4836
|
return c.text(`updated profile "${param.profile}"`);
|
|
4775
4837
|
});
|
|
@@ -4779,27 +4841,29 @@ usage: funnel profiles [subcommand]
|
|
|
4779
4841
|
|
|
4780
4842
|
subcommands:
|
|
4781
4843
|
(none) list (first entry is the default)
|
|
4782
|
-
add <name> --path <path> --channel <channel>
|
|
4783
|
-
<name> set [--path ...] [--channel ...]
|
|
4844
|
+
add <name> --path <path> --channel <channel> [--agent ...] [--options ...] [--env ...] [--no-resume]
|
|
4845
|
+
<name> set [--path ...] [--channel ...] [--agent ...] [--options ...] [--env ...] [--resume|--no-resume]
|
|
4784
4846
|
<name> as-default move profile to the front (becomes default)
|
|
4785
4847
|
rename <old> <new> rename
|
|
4786
4848
|
remove <name> remove
|
|
4787
4849
|
<name> run launch (sugar for fnl claude -p <name>)
|
|
4788
4850
|
<name> launch (alias for run)
|
|
4789
4851
|
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4852
|
+
A profile carries the launch recipe — \`--agent\` / \`--options\` prepended to
|
|
4853
|
+
the claude argv, \`--env\` layered under the process, \`--resume\` toggling
|
|
4854
|
+
session reuse. The channel it points at only declares transport (connectors).
|
|
4793
4855
|
|
|
4794
4856
|
examples:
|
|
4795
|
-
funnel profiles add cto --path /repo/myapp --channel prod-inbox
|
|
4857
|
+
funnel profiles add cto --path /repo/myapp --channel prod-inbox --agent pm --options "--brief"
|
|
4796
4858
|
funnel profiles cto as-default
|
|
4797
4859
|
funnel profiles cto run`), (c) => {
|
|
4798
4860
|
const profiles = c.var.funnel.profiles.list();
|
|
4799
4861
|
if (profiles.length === 0) return c.text("no profiles");
|
|
4800
4862
|
const lines = profiles.map((profile, index) => {
|
|
4801
4863
|
const tag = index === 0 ? " (default)" : "";
|
|
4802
|
-
|
|
4864
|
+
const recipe = profile.options.length > 0 ? `, options=${profile.options.join(" ")}` : "";
|
|
4865
|
+
const session = profile.resume ? "" : ", resume=false";
|
|
4866
|
+
return `${profile.name}${tag} [path=${profile.path}, channel=${profile.channelId}${recipe}${session}]`;
|
|
4803
4867
|
});
|
|
4804
4868
|
return c.text(lines.join("\n"));
|
|
4805
4869
|
});
|
|
@@ -7116,4 +7180,4 @@ async function launchTui(funnel) {
|
|
|
7116
7180
|
});
|
|
7117
7181
|
}
|
|
7118
7182
|
//#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 };
|
|
7183
|
+
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