@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/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
- /** Args prepended to the claude argv on every launch bound to this channel. */
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 channels that should always start a fresh session.
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 = channel.resume ? this.resolveSession(channel.id, cwd, options.userArgs ?? []) : null;
602
- const claudeArgs = this.buildArgs(channel.options, options.userArgs ?? [], cwd, session);
603
- const env = this.buildEnv(channel.id, channel.env);
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(channelOptions, userArgs, cwd, session) {
654
- const result = [...channelOptions, ...userArgs];
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, channelEnv) {
662
+ buildEnv(channelId, recipeEnv) {
684
663
  const env = {};
685
- for (const [key, value] of Object.entries(channelEnv)) env[key] = value;
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 given and picks one of the
785
- * declared channels (`--channel <name>` selects by name; otherwise the first
786
- * entry wins). The chosen channel is materialized into
787
- * `~/.funnel/settings.json` on launch — token fields in connectors resolve
788
- * via literal / `env.<field>` / TTY prompt.
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
- * Top-level `options` and `env` are defaults shared by every channel: each
791
- * channel's own `options` is appended after the shared ones (CLI semantics
792
- * keep the later flag winning), and `env` is a shallow merge with the
793
- * channel's keys overriding the shared ones.
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
- /** Args prepended to the claude argv on every launch bound to this channel. */
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
- * channels that should always start a fresh session.
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
- const existing = this.channels.get(channel.name);
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, a sub-agent name, and the channel id its Claude instance will
1432
- * subscribe to. Implements ProfileChannelChecker so FunnelChannels can refuse
1433
- * to remove a channel that is still referenced.
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(config) {
1419
+ add(input) {
1457
1420
  const settings = this.store.read();
1458
- if (settings.profiles.some((p) => p.name === config.name)) throw new Error(`profile "${config.name}" already exists`);
1459
- if (!settings.channels.some((c) => c.id === config.channelId)) throw new Error(`channel id "${config.channelId}" not found`);
1460
- settings.profiles.push(config);
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 channel / subAgent / env / connectors[]
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` when no --profile / --channel is given. Declares the channel to subscribe to, optional sub-agent and brief flag, environment variables to layer under process.env, and optional connectors to materialize into ~/.funnel/settings.json on launch."
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 working directory passed to claude as cwd
4681
- --channel channel name (resolved to channel id internally)
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
- Per-launch flags like --agent or --brief now live on the channel itself
4684
- (set with \`fnl channels <name> set options ...\`), so profiles are only
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
- Per-launch flags like --agent or --brief now live on the channel itself
4791
- (set with \`fnl channels <name> set options ...\`), so profiles are only
4792
- \`{ name, path, channelId }\`.
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
- return `${profile.name}${tag} [path=${profile.path}, channel=${profile.channelId}]`;
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 };
@@ -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` when no --profile / --channel is given. Declares the channel to subscribe to, optional sub-agent and brief flag, environment variables to layer under process.env, and optional connectors to materialize into ~/.funnel/settings.json on launch."
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interactive-inc/claude-funnel",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Hub CLI that routes external events (Slack / GitHub / Discord) to Claude Code agents through subscription channels over MCP.",
5
5
  "keywords": [
6
6
  "bun",