@interactive-inc/claude-funnel 0.18.0 → 0.20.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/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, t as discordConn
2
2
  import { n as FunnelLogger, r as FunnelConnectorListener, t as NodeFunnelLogger } from "./node-logger-DQz_BGOD.js";
3
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
- import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-B4hsf3AY.js";
5
+ import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-BM9xshol.js";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
8
8
  import { z } from "zod";
@@ -54,6 +54,12 @@ const channelConfigSchema = z.object({
54
54
  options: z.array(z.string()).default([]),
55
55
  /** Env vars layered under the launched claude process. process.env wins on collision. */
56
56
  env: z.record(z.string(), z.string()).default({}),
57
+ /**
58
+ * When true (the default), funnel injects `--session-id <uuid>` so that
59
+ * relaunching from the same cwd resumes the previous claude session.
60
+ * Set to false for channels that should always start a fresh session.
61
+ */
62
+ resume: z.boolean().default(true),
57
63
  connectors: z.array(connectorConfigSchema).default([])
58
64
  });
59
65
  const profileConfigSchema = z.object({
@@ -308,6 +314,7 @@ var FunnelChannels = class {
308
314
  delivery: input.delivery ?? "fanout",
309
315
  options: input.options ?? [],
310
316
  env: input.env ?? {},
317
+ resume: input.resume ?? true,
311
318
  connectors: []
312
319
  };
313
320
  settings.channels.push(channel);
@@ -320,6 +327,12 @@ var FunnelChannels = class {
320
327
  channel.delivery = delivery;
321
328
  this.store.write(settings);
322
329
  }
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
+ }
323
336
  setOptions(name, options) {
324
337
  const settings = this.store.read();
325
338
  const channel = this.requireChannel(settings, name);
@@ -551,6 +564,7 @@ var FunnelClaude = class {
551
564
  channels;
552
565
  mcp;
553
566
  gateway;
567
+ sessions;
554
568
  process;
555
569
  fs;
556
570
  logger;
@@ -559,6 +573,7 @@ var FunnelClaude = class {
559
573
  this.channels = deps.channels;
560
574
  this.mcp = deps.mcp;
561
575
  this.gateway = deps.gateway;
576
+ this.sessions = deps.sessions;
562
577
  this.process = deps.process ?? defaultProcess$2;
563
578
  this.fs = deps.fs ?? defaultFs$3;
564
579
  this.logger = deps.logger ?? defaultLogger$4;
@@ -570,7 +585,7 @@ var FunnelClaude = class {
570
585
  if (!channel) throw new Error(`channel "${options.channel}" not found`);
571
586
  if (options.profileName && this.isRunning(options.profileName)) throw new Error(`profile "${options.profileName}" is already running`);
572
587
  const cwd = options.cwd ?? globalThis.process.cwd();
573
- if (!this.mcp.findInstalledName(cwd)) {
588
+ if ((options.installMcp ?? true) && !this.mcp.findInstalledName(cwd)) {
574
589
  this.mcp.install(cwd);
575
590
  this.logger.info(`added funnel MCP to .mcp.json`, { cwd });
576
591
  }
@@ -582,7 +597,8 @@ var FunnelClaude = class {
582
597
  this.writePidFile(options.profileName);
583
598
  this.installCleanup(options.profileName);
584
599
  }
585
- const claudeArgs = this.buildArgs(channel.options, options.userArgs ?? [], cwd);
600
+ const sessionId = channel.resume ? this.resolveSessionId(channel.id, cwd, options.userArgs ?? []) : null;
601
+ const claudeArgs = this.buildArgs(channel.options, options.userArgs ?? [], cwd, sessionId);
586
602
  const env = this.buildEnv(channel.id, channel.env);
587
603
  this.logger.info(`claude launch`, {
588
604
  channel: options.channel,
@@ -643,12 +659,26 @@ var FunnelClaude = class {
643
659
  if (!state) return false;
644
660
  return !state.startsWith("Z");
645
661
  }
646
- buildArgs(channelOptions, userArgs, cwd) {
662
+ buildArgs(channelOptions, userArgs, cwd, sessionId) {
647
663
  const result = [...channelOptions, ...userArgs];
664
+ if (sessionId !== null) result.push("--session-id", sessionId);
648
665
  const mcpName = this.mcp.findInstalledName(cwd);
649
666
  if (mcpName && !result.includes("--dangerously-load-development-channels") && !result.includes("--channels")) result.push("--dangerously-load-development-channels", `server:${mcpName}`);
650
667
  return result;
651
668
  }
669
+ /**
670
+ * Decides whether funnel should inject `--session-id`. We back off when
671
+ * the user already passed a session-shaping flag, since combining them
672
+ * would either confuse claude or override the explicit user intent.
673
+ */
674
+ resolveSessionId(channelId, cwd, userArgs) {
675
+ for (const arg of userArgs) {
676
+ if (arg === "-c" || arg === "--continue") return null;
677
+ if (arg === "--resume" || arg.startsWith("--resume=")) return null;
678
+ if (arg === "--session-id" || arg.startsWith("--session-id=")) return null;
679
+ }
680
+ return this.sessions.getOrCreate(channelId, cwd);
681
+ }
652
682
  buildEnv(channelId, channelEnv) {
653
683
  const env = {};
654
684
  for (const [key, value] of Object.entries(channelEnv)) env[key] = value;
@@ -800,6 +830,13 @@ const channelSpecSchema = z.object({
800
830
  options: z.array(z.string()).optional(),
801
831
  /** Env vars layered under the launched claude process. process.env wins on collision. */
802
832
  env: z.record(z.string(), z.string()).optional(),
833
+ /**
834
+ * When true (the default), funnel injects `--session-id <uuid>` so that
835
+ * relaunching from the same cwd resumes the previous claude session
836
+ * without bleeding into other channels or workspaces. Set to false for
837
+ * channels that should always start a fresh session.
838
+ */
839
+ resume: z.boolean().optional(),
803
840
  connectors: z.array(connectorSpecSchema).optional()
804
841
  });
805
842
  const localConfigSchema = z.object({
@@ -918,6 +955,9 @@ const recordsEqual = (a, b) => {
918
955
  * absent field means "do not manage connectors from here" and leaves
919
956
  * everything in `~/.funnel` alone. Other channels in funnel.json (not
920
957
  * passed to this call) are untouched.
958
+ *
959
+ * Returns the per-connector change set so callers (e.g. the claude launcher)
960
+ * can drive listener hot-reload on the gateway after settings are written.
921
961
  */
922
962
  var FunnelLocalConfigSync = class {
923
963
  channels;
@@ -933,25 +973,39 @@ var FunnelLocalConfigSync = class {
933
973
  }
934
974
  async ensure(channel, cwd) {
935
975
  const existing = this.channels.get(channel.name);
976
+ const nextResume = channel.resume ?? true;
936
977
  if (!existing) this.channels.add({
937
978
  name: channel.name,
938
979
  options: channel.options ?? [],
939
- env: channel.env ?? {}
980
+ env: channel.env ?? {},
981
+ resume: nextResume
940
982
  });
941
983
  else {
942
984
  const nextOptions = channel.options ?? [];
943
985
  const nextEnv = channel.env ?? {};
944
986
  if (!arraysEqual(existing.options, nextOptions)) this.channels.setOptions(channel.name, nextOptions);
945
987
  if (!recordsEqual(existing.env, nextEnv)) this.channels.setEnv(channel.name, nextEnv);
988
+ if (existing.resume !== nextResume) this.channels.setResume(channel.name, nextResume);
946
989
  }
947
- if (channel.connectors === void 0) return;
990
+ if (channel.connectors === void 0) return {
991
+ touched: [],
992
+ removed: []
993
+ };
948
994
  const dotenv = this.dotenv.read(cwd);
949
- const touched = /* @__PURE__ */ new Set();
995
+ const touched = [];
996
+ const touchedIds = /* @__PURE__ */ new Set();
950
997
  for (const spec of channel.connectors) {
951
- const id = await this.ensureConnector(channel.name, spec, dotenv);
952
- touched.add(id);
998
+ const outcome = await this.ensureConnector(channel.name, spec, dotenv);
999
+ touched.push({
1000
+ name: outcome.name,
1001
+ changed: outcome.changed
1002
+ });
1003
+ touchedIds.add(outcome.id);
953
1004
  }
954
- this.removeExtras(channel.name, touched);
1005
+ return {
1006
+ touched,
1007
+ removed: this.removeExtras(channel.name, touchedIds)
1008
+ };
955
1009
  }
956
1010
  async ensureConnector(channelName, spec, dotenv) {
957
1011
  if (spec.type === "slack") return await this.ensureSlack(channelName, spec, dotenv);
@@ -976,11 +1030,22 @@ var FunnelLocalConfigSync = class {
976
1030
  existing: byName?.appToken
977
1031
  });
978
1032
  if (byName) {
979
- if (byName.botToken !== botToken || byName.appToken !== appToken) this.channels.updateSlackConnector(channelName, spec.name, {
980
- botToken,
981
- appToken
982
- });
983
- return byName.id;
1033
+ if (byName.botToken !== botToken || byName.appToken !== appToken) {
1034
+ this.channels.updateSlackConnector(channelName, spec.name, {
1035
+ botToken,
1036
+ appToken
1037
+ });
1038
+ return {
1039
+ id: byName.id,
1040
+ name: spec.name,
1041
+ changed: true
1042
+ };
1043
+ }
1044
+ return {
1045
+ id: byName.id,
1046
+ name: spec.name,
1047
+ changed: false
1048
+ };
984
1049
  }
985
1050
  const byToken = this.findSlackByToken(channelName, [botToken, appToken]);
986
1051
  if (byToken) {
@@ -989,14 +1054,22 @@ var FunnelLocalConfigSync = class {
989
1054
  botToken,
990
1055
  appToken
991
1056
  });
992
- return byToken.id;
1057
+ return {
1058
+ id: byToken.id,
1059
+ name: spec.name,
1060
+ changed: true
1061
+ };
993
1062
  }
994
- return this.channels.addConnector(channelName, {
995
- type: "slack",
1063
+ return {
1064
+ id: this.channels.addConnector(channelName, {
1065
+ type: "slack",
1066
+ name: spec.name,
1067
+ botToken,
1068
+ appToken
1069
+ }).id,
996
1070
  name: spec.name,
997
- botToken,
998
- appToken
999
- }).id;
1071
+ changed: true
1072
+ };
1000
1073
  }
1001
1074
  async ensureDiscord(channelName, spec, dotenv) {
1002
1075
  const byName = this.findExistingDiscord(channelName, spec.name);
@@ -1008,42 +1081,84 @@ var FunnelLocalConfigSync = class {
1008
1081
  existing: byName?.botToken
1009
1082
  });
1010
1083
  if (byName) {
1011
- if (byName.botToken !== botToken) this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
1012
- return byName.id;
1084
+ if (byName.botToken !== botToken) {
1085
+ this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
1086
+ return {
1087
+ id: byName.id,
1088
+ name: spec.name,
1089
+ changed: true
1090
+ };
1091
+ }
1092
+ return {
1093
+ id: byName.id,
1094
+ name: spec.name,
1095
+ changed: false
1096
+ };
1013
1097
  }
1014
1098
  const byToken = this.findDiscordByToken(channelName, botToken);
1015
1099
  if (byToken) {
1016
1100
  this.channels.renameConnector(channelName, byToken.name, spec.name);
1017
1101
  if (byToken.botToken !== botToken) this.channels.updateDiscordConnector(channelName, spec.name, { botToken });
1018
- return byToken.id;
1102
+ return {
1103
+ id: byToken.id,
1104
+ name: spec.name,
1105
+ changed: true
1106
+ };
1019
1107
  }
1020
- return this.channels.addConnector(channelName, {
1021
- type: "discord",
1108
+ return {
1109
+ id: this.channels.addConnector(channelName, {
1110
+ type: "discord",
1111
+ name: spec.name,
1112
+ botToken
1113
+ }).id,
1022
1114
  name: spec.name,
1023
- botToken
1024
- }).id;
1115
+ changed: true
1116
+ };
1025
1117
  }
1026
1118
  ensureGh(channelName, spec) {
1027
1119
  const existing = this.channels.getConnector(channelName, spec.name);
1028
1120
  if (existing && existing.type !== "gh") throw new Error(`connector "${spec.name}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "gh"`);
1029
1121
  if (existing && existing.type === "gh") {
1030
- if (spec.pollInterval !== void 0 && existing.pollInterval !== spec.pollInterval) this.channels.updateGhConnector(channelName, spec.name, { pollInterval: spec.pollInterval });
1031
- return existing.id;
1122
+ if (spec.pollInterval !== void 0 && existing.pollInterval !== spec.pollInterval) {
1123
+ this.channels.updateGhConnector(channelName, spec.name, { pollInterval: spec.pollInterval });
1124
+ return {
1125
+ id: existing.id,
1126
+ name: spec.name,
1127
+ changed: true
1128
+ };
1129
+ }
1130
+ return {
1131
+ id: existing.id,
1132
+ name: spec.name,
1133
+ changed: false
1134
+ };
1032
1135
  }
1033
- return this.channels.addConnector(channelName, {
1034
- type: "gh",
1136
+ return {
1137
+ id: this.channels.addConnector(channelName, {
1138
+ type: "gh",
1139
+ name: spec.name,
1140
+ ...spec.pollInterval !== void 0 ? { pollInterval: spec.pollInterval } : {}
1141
+ }).id,
1035
1142
  name: spec.name,
1036
- ...spec.pollInterval !== void 0 ? { pollInterval: spec.pollInterval } : {}
1037
- }).id;
1143
+ changed: true
1144
+ };
1038
1145
  }
1039
1146
  ensureSchedule(channelName, spec) {
1040
1147
  const existing = this.channels.getConnector(channelName, spec.name);
1041
1148
  if (existing && existing.type !== "schedule") throw new Error(`connector "${spec.name}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "schedule"`);
1042
- if (existing && existing.type === "schedule") return existing.id;
1043
- return this.channels.addConnector(channelName, {
1044
- type: "schedule",
1045
- name: spec.name
1046
- }).id;
1149
+ if (existing && existing.type === "schedule") return {
1150
+ id: existing.id,
1151
+ name: spec.name,
1152
+ changed: false
1153
+ };
1154
+ return {
1155
+ id: this.channels.addConnector(channelName, {
1156
+ type: "schedule",
1157
+ name: spec.name
1158
+ }).id,
1159
+ name: spec.name,
1160
+ changed: true
1161
+ };
1047
1162
  }
1048
1163
  findExistingSlack(channelName, connectorName) {
1049
1164
  const existing = this.channels.getConnector(channelName, connectorName);
@@ -1077,9 +1192,10 @@ var FunnelLocalConfigSync = class {
1077
1192
  }
1078
1193
  removeExtras(channelName, touched) {
1079
1194
  const channel = this.channels.get(channelName);
1080
- if (!channel) return;
1195
+ if (!channel) return [];
1081
1196
  const stale = channel.connectors.filter((c) => !touched.has(c.id));
1082
1197
  for (const connector of stale) this.channels.removeConnector(channelName, connector.name);
1198
+ return stale.map((c) => c.name);
1083
1199
  }
1084
1200
  async resolveField(input) {
1085
1201
  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`);
@@ -1353,6 +1469,81 @@ var FunnelProfiles = class {
1353
1469
  }
1354
1470
  };
1355
1471
  //#endregion
1472
+ //#region lib/engine/sessions/sessions.ts
1473
+ const sessionsMapSchema = z.record(z.string(), z.string());
1474
+ /**
1475
+ * Per-channel persistent Claude Code session IDs, keyed by the cwd the
1476
+ * channel was launched from. The whole point is to give each (channel, cwd)
1477
+ * its own stable conversation: relaunching from the same path picks up the
1478
+ * previous claude session via `--session-id <uuid>`, while a different cwd
1479
+ * (or a different channel) gets an independent one — so sessions never
1480
+ * silently bleed across workspaces the way claude's `-c` does.
1481
+ *
1482
+ * Storage lives under `<dir>/channels/<channel-id>/sessions.json` (channel
1483
+ * id, not name, so renames don't lose history). The file is a flat
1484
+ * `{ cwd: uuid }` map; the channel directory itself is created lazily.
1485
+ */
1486
+ var FunnelSessions = class {
1487
+ fs;
1488
+ idGenerator;
1489
+ dir;
1490
+ constructor(deps) {
1491
+ this.fs = deps.fs;
1492
+ this.idGenerator = deps.idGenerator;
1493
+ this.dir = deps.dir;
1494
+ Object.freeze(this);
1495
+ }
1496
+ /** Returns the existing session id for (channelId, cwd) or generates and persists a new one. */
1497
+ getOrCreate(channelId, cwd) {
1498
+ const map = this.readMap(channelId);
1499
+ const existing = map[cwd];
1500
+ if (existing) return existing;
1501
+ const sessionId = this.idGenerator.generate();
1502
+ map[cwd] = sessionId;
1503
+ this.writeMap(channelId, map);
1504
+ return sessionId;
1505
+ }
1506
+ /** Returns the existing session id for (channelId, cwd) or null. */
1507
+ get(channelId, cwd) {
1508
+ return this.readMap(channelId)[cwd] ?? null;
1509
+ }
1510
+ /** Drops the recorded session id for (channelId, cwd). No-op if absent. */
1511
+ clear(channelId, cwd) {
1512
+ const map = this.readMap(channelId);
1513
+ if (!(cwd in map)) return;
1514
+ delete map[cwd];
1515
+ this.writeMap(channelId, map);
1516
+ }
1517
+ /** Drops the whole session map for the channel (e.g. when the channel is deleted). */
1518
+ clearAll(channelId) {
1519
+ const path = this.pathFor(channelId);
1520
+ if (this.fs.existsSync(path)) this.fs.unlink(path);
1521
+ }
1522
+ readMap(channelId) {
1523
+ const path = this.pathFor(channelId);
1524
+ if (!this.fs.existsSync(path)) return {};
1525
+ const raw = this.fs.readFileSync(path);
1526
+ try {
1527
+ const parsed = sessionsMapSchema.safeParse(JSON.parse(raw));
1528
+ return parsed.success ? parsed.data : {};
1529
+ } catch {
1530
+ return {};
1531
+ }
1532
+ }
1533
+ writeMap(channelId, map) {
1534
+ const path = this.pathFor(channelId);
1535
+ const channelDir = this.channelDir(channelId);
1536
+ if (!this.fs.existsSync(channelDir)) this.fs.mkdirSync(channelDir, { recursive: true });
1537
+ this.fs.writeFileSync(path, `${JSON.stringify(map, null, 2)}\n`);
1538
+ }
1539
+ channelDir(channelId) {
1540
+ return join(this.dir, "channels", channelId);
1541
+ }
1542
+ pathFor(channelId) {
1543
+ return join(this.channelDir(channelId), "sessions.json");
1544
+ }
1545
+ };
1546
+ //#endregion
1356
1547
  //#region lib/engine/token-prompter/node-token-prompter.ts
1357
1548
  const STAR = "*";
1358
1549
  const CR = "\r";
@@ -3285,6 +3476,15 @@ var Funnel = class Funnel {
3285
3476
  if (!this.memos.profiles) this.memos.profiles = new FunnelProfiles({ store: this.store });
3286
3477
  return this.memos.profiles;
3287
3478
  }
3479
+ /** Per-(channel, cwd) claude session-id store. Backs `--session-id` injection on launch. */
3480
+ get sessions() {
3481
+ if (!this.memos.sessions) this.memos.sessions = new FunnelSessions({
3482
+ fs: this.fs,
3483
+ idGenerator: this.idGenerator,
3484
+ dir: this.paths.dir
3485
+ });
3486
+ return this.memos.sessions;
3487
+ }
3288
3488
  /** Reads `funnel.json` from a cwd. `fnl claude` consults it before falling back to the default profile. */
3289
3489
  get localConfig() {
3290
3490
  if (!this.memos.localConfig) this.memos.localConfig = new FunnelLocalConfig({ fs: this.fs });
@@ -3320,6 +3520,7 @@ var Funnel = class Funnel {
3320
3520
  channels: this.channels,
3321
3521
  mcp: this.mcp,
3322
3522
  gateway: this.gateway,
3523
+ sessions: this.sessions,
3323
3524
  fs: this.fs,
3324
3525
  process: this.process,
3325
3526
  logger: this.logger,
@@ -4165,7 +4366,10 @@ const claudeHandler = factory.createHandlers(zValidator$1("query", z.object({
4165
4366
  if (local) {
4166
4367
  const picked = query.channel !== void 0 ? local.channels.find((c) => c.name === query.channel) : local.channels[0];
4167
4368
  if (!picked) throw new HTTPException(404, { message: query.channel ? `channel "${query.channel}" is not declared in funnel.json` : `funnel.json declares no channels` });
4168
- await funnel.localConfigSync.ensure(picked, cwd);
4369
+ const synced = await funnel.localConfigSync.ensure(picked, cwd);
4370
+ for (const outcome of synced.touched) if (outcome.changed) await funnel.listeners.restart(picked.name, outcome.name);
4371
+ else await funnel.listeners.start(picked.name, outcome.name);
4372
+ for (const name of synced.removed) await funnel.listeners.stop(picked.name, name);
4169
4373
  const exitCode = await funnel.claude.launch({
4170
4374
  channel: picked.name,
4171
4375
  cwd,
@@ -6851,4 +7055,4 @@ async function launchTui(funnel) {
6851
7055
  });
6852
7056
  }
6853
7057
  //#endregion
6854
- 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, 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 };
7058
+ 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 };
@@ -9,6 +9,24 @@ const toRecord = (value) => {
9
9
  for (const [key, val] of Object.entries(value)) result[key] = val;
10
10
  return result;
11
11
  };
12
+ /**
13
+ * Recognises errors that @slack/web-api throws for Slack-side API failures
14
+ * (e.g. `cant_delete_message`, `channel_not_found`, rate limits). Every such
15
+ * error carries `code: "slack_webapi_*"` and a `data` field holding the raw
16
+ * Slack response with `ok: false`. We unwrap to that response so the caller
17
+ * receives a structured failure instead of having the gateway translate it
18
+ * into an opaque HTTP 500.
19
+ */
20
+ const slackErrorResponse = (error) => {
21
+ if (!error || typeof error !== "object") return null;
22
+ if (!("code" in error)) return null;
23
+ const code = error.code;
24
+ if (typeof code !== "string" || !code.startsWith("slack_webapi_")) return null;
25
+ if (!("data" in error)) return null;
26
+ const data = error.data;
27
+ if (!data || typeof data !== "object") return null;
28
+ return data;
29
+ };
12
30
  var FunnelSlackAdapter = class extends FunnelConnectorAdapter {
13
31
  client;
14
32
  constructor(deps) {
@@ -18,7 +36,13 @@ var FunnelSlackAdapter = class extends FunnelConnectorAdapter {
18
36
  }
19
37
  async call(input) {
20
38
  const body = input.body !== null && typeof input.body === "object" ? toRecord(input.body) : {};
21
- return await this.client.apiCall(input.path, body);
39
+ try {
40
+ return await this.client.apiCall(input.path, body);
41
+ } catch (error) {
42
+ const slackResponse = slackErrorResponse(error);
43
+ if (slackResponse) return slackResponse;
44
+ throw error;
45
+ }
22
46
  }
23
47
  };
24
48
  //#endregion
@@ -29,6 +29,9 @@
29
29
  "type": "string"
30
30
  }
31
31
  },
32
+ "resume": {
33
+ "type": "boolean"
34
+ },
32
35
  "connectors": {
33
36
  "type": "array",
34
37
  "items": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interactive-inc/claude-funnel",
3
- "version": "0.18.0",
3
+ "version": "0.20.1",
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",