@interactive-inc/claude-funnel 0.17.0 → 0.19.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
@@ -1,6 +1,6 @@
1
1
  import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, t as discordConnectorSchema } from "./discord-connector-schema-ygf5Df-2.js";
2
2
  import { n as FunnelLogger, r as FunnelConnectorListener, t as NodeFunnelLogger } from "./node-logger-DQz_BGOD.js";
3
- import { a as FunnelProcessRunner, i as NodeFunnelProcessRunner, n as FunnelGhListener, r as FunnelGhAdapter, t as ghConnectorSchema } from "./gh-connector-schema-2ml29MBC.js";
3
+ import { a as FunnelProcessRunner, i as NodeFunnelProcessRunner, n as FunnelGhListener, r as FunnelGhAdapter, t as ghConnectorSchema } from "./gh-connector-schema-CD5HIkrd.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-FxP7LPlx.js";
5
5
  import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-B4hsf3AY.js";
6
6
  import { dirname, join, resolve } from "node:path";
@@ -570,7 +570,7 @@ var FunnelClaude = class {
570
570
  if (!channel) throw new Error(`channel "${options.channel}" not found`);
571
571
  if (options.profileName && this.isRunning(options.profileName)) throw new Error(`profile "${options.profileName}" is already running`);
572
572
  const cwd = options.cwd ?? globalThis.process.cwd();
573
- if (!this.mcp.findInstalledName(cwd)) {
573
+ if ((options.installMcp ?? true) && !this.mcp.findInstalledName(cwd)) {
574
574
  this.mcp.install(cwd);
575
575
  this.logger.info(`added funnel MCP to .mcp.json`, { cwd });
576
576
  }
@@ -592,7 +592,8 @@ var FunnelClaude = class {
592
592
  try {
593
593
  return await this.process.attach(["claude", ...claudeArgs], {
594
594
  cwd,
595
- env
595
+ env,
596
+ onSpawned: options.onSpawned
596
597
  });
597
598
  } finally {
598
599
  if (options.profileName) this.removePidFile(options.profileName);
@@ -917,6 +918,9 @@ const recordsEqual = (a, b) => {
917
918
  * absent field means "do not manage connectors from here" and leaves
918
919
  * everything in `~/.funnel` alone. Other channels in funnel.json (not
919
920
  * passed to this call) are untouched.
921
+ *
922
+ * Returns the per-connector change set so callers (e.g. the claude launcher)
923
+ * can drive listener hot-reload on the gateway after settings are written.
920
924
  */
921
925
  var FunnelLocalConfigSync = class {
922
926
  channels;
@@ -943,14 +947,25 @@ var FunnelLocalConfigSync = class {
943
947
  if (!arraysEqual(existing.options, nextOptions)) this.channels.setOptions(channel.name, nextOptions);
944
948
  if (!recordsEqual(existing.env, nextEnv)) this.channels.setEnv(channel.name, nextEnv);
945
949
  }
946
- if (channel.connectors === void 0) return;
950
+ if (channel.connectors === void 0) return {
951
+ touched: [],
952
+ removed: []
953
+ };
947
954
  const dotenv = this.dotenv.read(cwd);
948
- const touched = /* @__PURE__ */ new Set();
955
+ const touched = [];
956
+ const touchedIds = /* @__PURE__ */ new Set();
949
957
  for (const spec of channel.connectors) {
950
- const id = await this.ensureConnector(channel.name, spec, dotenv);
951
- touched.add(id);
958
+ const outcome = await this.ensureConnector(channel.name, spec, dotenv);
959
+ touched.push({
960
+ name: outcome.name,
961
+ changed: outcome.changed
962
+ });
963
+ touchedIds.add(outcome.id);
952
964
  }
953
- this.removeExtras(channel.name, touched);
965
+ return {
966
+ touched,
967
+ removed: this.removeExtras(channel.name, touchedIds)
968
+ };
954
969
  }
955
970
  async ensureConnector(channelName, spec, dotenv) {
956
971
  if (spec.type === "slack") return await this.ensureSlack(channelName, spec, dotenv);
@@ -975,11 +990,22 @@ var FunnelLocalConfigSync = class {
975
990
  existing: byName?.appToken
976
991
  });
977
992
  if (byName) {
978
- if (byName.botToken !== botToken || byName.appToken !== appToken) this.channels.updateSlackConnector(channelName, spec.name, {
979
- botToken,
980
- appToken
981
- });
982
- return byName.id;
993
+ if (byName.botToken !== botToken || byName.appToken !== appToken) {
994
+ this.channels.updateSlackConnector(channelName, spec.name, {
995
+ botToken,
996
+ appToken
997
+ });
998
+ return {
999
+ id: byName.id,
1000
+ name: spec.name,
1001
+ changed: true
1002
+ };
1003
+ }
1004
+ return {
1005
+ id: byName.id,
1006
+ name: spec.name,
1007
+ changed: false
1008
+ };
983
1009
  }
984
1010
  const byToken = this.findSlackByToken(channelName, [botToken, appToken]);
985
1011
  if (byToken) {
@@ -988,14 +1014,22 @@ var FunnelLocalConfigSync = class {
988
1014
  botToken,
989
1015
  appToken
990
1016
  });
991
- return byToken.id;
1017
+ return {
1018
+ id: byToken.id,
1019
+ name: spec.name,
1020
+ changed: true
1021
+ };
992
1022
  }
993
- return this.channels.addConnector(channelName, {
994
- type: "slack",
1023
+ return {
1024
+ id: this.channels.addConnector(channelName, {
1025
+ type: "slack",
1026
+ name: spec.name,
1027
+ botToken,
1028
+ appToken
1029
+ }).id,
995
1030
  name: spec.name,
996
- botToken,
997
- appToken
998
- }).id;
1031
+ changed: true
1032
+ };
999
1033
  }
1000
1034
  async ensureDiscord(channelName, spec, dotenv) {
1001
1035
  const byName = this.findExistingDiscord(channelName, spec.name);
@@ -1007,42 +1041,84 @@ var FunnelLocalConfigSync = class {
1007
1041
  existing: byName?.botToken
1008
1042
  });
1009
1043
  if (byName) {
1010
- if (byName.botToken !== botToken) this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
1011
- return byName.id;
1044
+ if (byName.botToken !== botToken) {
1045
+ this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
1046
+ return {
1047
+ id: byName.id,
1048
+ name: spec.name,
1049
+ changed: true
1050
+ };
1051
+ }
1052
+ return {
1053
+ id: byName.id,
1054
+ name: spec.name,
1055
+ changed: false
1056
+ };
1012
1057
  }
1013
1058
  const byToken = this.findDiscordByToken(channelName, botToken);
1014
1059
  if (byToken) {
1015
1060
  this.channels.renameConnector(channelName, byToken.name, spec.name);
1016
1061
  if (byToken.botToken !== botToken) this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
1017
- return byToken.id;
1062
+ return {
1063
+ id: byToken.id,
1064
+ name: spec.name,
1065
+ changed: true
1066
+ };
1018
1067
  }
1019
- return this.channels.addConnector(channelName, {
1020
- type: "discord",
1068
+ return {
1069
+ id: this.channels.addConnector(channelName, {
1070
+ type: "discord",
1071
+ name: spec.name,
1072
+ botToken
1073
+ }).id,
1021
1074
  name: spec.name,
1022
- botToken
1023
- }).id;
1075
+ changed: true
1076
+ };
1024
1077
  }
1025
1078
  ensureGh(channelName, spec) {
1026
1079
  const existing = this.channels.getConnector(channelName, spec.name);
1027
1080
  if (existing && existing.type !== "gh") throw new Error(`connector "${spec.name}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "gh"`);
1028
1081
  if (existing && existing.type === "gh") {
1029
- if (spec.pollInterval !== void 0 && existing.pollInterval !== spec.pollInterval) this.channels.updateGhConnector(channelName, spec.name, { pollInterval: spec.pollInterval });
1030
- return existing.id;
1082
+ if (spec.pollInterval !== void 0 && existing.pollInterval !== spec.pollInterval) {
1083
+ this.channels.updateGhConnector(channelName, spec.name, { pollInterval: spec.pollInterval });
1084
+ return {
1085
+ id: existing.id,
1086
+ name: spec.name,
1087
+ changed: true
1088
+ };
1089
+ }
1090
+ return {
1091
+ id: existing.id,
1092
+ name: spec.name,
1093
+ changed: false
1094
+ };
1031
1095
  }
1032
- return this.channels.addConnector(channelName, {
1033
- type: "gh",
1096
+ return {
1097
+ id: this.channels.addConnector(channelName, {
1098
+ type: "gh",
1099
+ name: spec.name,
1100
+ ...spec.pollInterval !== void 0 ? { pollInterval: spec.pollInterval } : {}
1101
+ }).id,
1034
1102
  name: spec.name,
1035
- ...spec.pollInterval !== void 0 ? { pollInterval: spec.pollInterval } : {}
1036
- }).id;
1103
+ changed: true
1104
+ };
1037
1105
  }
1038
1106
  ensureSchedule(channelName, spec) {
1039
1107
  const existing = this.channels.getConnector(channelName, spec.name);
1040
1108
  if (existing && existing.type !== "schedule") throw new Error(`connector "${spec.name}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "schedule"`);
1041
- if (existing && existing.type === "schedule") return existing.id;
1042
- return this.channels.addConnector(channelName, {
1043
- type: "schedule",
1044
- name: spec.name
1045
- }).id;
1109
+ if (existing && existing.type === "schedule") return {
1110
+ id: existing.id,
1111
+ name: spec.name,
1112
+ changed: false
1113
+ };
1114
+ return {
1115
+ id: this.channels.addConnector(channelName, {
1116
+ type: "schedule",
1117
+ name: spec.name
1118
+ }).id,
1119
+ name: spec.name,
1120
+ changed: true
1121
+ };
1046
1122
  }
1047
1123
  findExistingSlack(channelName, connectorName) {
1048
1124
  const existing = this.channels.getConnector(channelName, connectorName);
@@ -1076,9 +1152,10 @@ var FunnelLocalConfigSync = class {
1076
1152
  }
1077
1153
  removeExtras(channelName, touched) {
1078
1154
  const channel = this.channels.get(channelName);
1079
- if (!channel) return;
1155
+ if (!channel) return [];
1080
1156
  const stale = channel.connectors.filter((c) => !touched.has(c.id));
1081
1157
  for (const connector of stale) this.channels.removeConnector(channelName, connector.name);
1158
+ return stale.map((c) => c.name);
1082
1159
  }
1083
1160
  async resolveField(input) {
1084
1161
  if (input.literal !== void 0 && input.envVar !== void 0) throw new Error(`${input.label} is set both as a literal and as env.${input.label.split(".").pop()}; pick one`);
@@ -1254,6 +1331,7 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
1254
1331
  command,
1255
1332
  options
1256
1333
  });
1334
+ if (options.onSpawned) options.onSpawned(1);
1257
1335
  return (await this.handler(command)).exitCode ?? 0;
1258
1336
  }
1259
1337
  detach(command, options = {}) {
@@ -4163,7 +4241,10 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
4163
4241
  if (local) {
4164
4242
  const picked = query.channel !== void 0 ? local.channels.find((c) => c.name === query.channel) : local.channels[0];
4165
4243
  if (!picked) throw new HTTPException(404, { message: query.channel ? `channel "${query.channel}" is not declared in funnel.json` : `funnel.json declares no channels` });
4166
- await funnel.localConfigSync.ensure(picked, cwd);
4244
+ const synced = await funnel.localConfigSync.ensure(picked, cwd);
4245
+ for (const outcome of synced.touched) if (outcome.changed) await funnel.listeners.restart(picked.name, outcome.name);
4246
+ else await funnel.listeners.start(picked.name, outcome.name);
4247
+ for (const name of synced.removed) await funnel.listeners.stop(picked.name, name);
4167
4248
  const exitCode = await funnel.claude.launch({
4168
4249
  channel: picked.name,
4169
4250
  cwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interactive-inc/claude-funnel",
3
- "version": "0.17.0",
3
+ "version": "0.19.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",