@interactive-inc/claude-funnel 0.22.1 → 0.23.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.d.ts CHANGED
@@ -498,6 +498,18 @@ declare class FunnelClaude {
498
498
  private buildEnv;
499
499
  }
500
500
  //#endregion
501
+ //#region lib/engine/error/on-funnel-error.d.ts
502
+ /**
503
+ * Host integration hook called when Funnel catches an exception that would
504
+ * otherwise be silently swallowed (subscriber throw, listener start failure,
505
+ * MCP forward failure, etc.). Pass `Sentry.captureException` from the host to
506
+ * pipe these into your error reporter. Defaults to a no-op when omitted.
507
+ *
508
+ * `context` carries the component name and any extra metadata the caller had
509
+ * at the catch site (channel / connector / subscriber id when available).
510
+ */
511
+ type OnFunnelError = (error: Error, context?: Record<string, unknown>) => void;
512
+ //#endregion
501
513
  //#region lib/engine/local-config/dotenv-reader.d.ts
502
514
  type Deps$11 = {
503
515
  fs: FunnelFileSystem;
@@ -793,7 +805,8 @@ type ReplaySource = {
793
805
  loadSince(since: number): ReplayableEvent[];
794
806
  };
795
807
  type Deps$6 = {
796
- logger?: FunnelLogger;
808
+ logger?: FunnelLogger; /** Host hook for surfacing subscriber-throw exceptions. Defaults to no-op. */
809
+ onError?: OnFunnelError;
797
810
  maxBufferedBytes?: number;
798
811
  now?: () => number; /** Number of recent events kept in the in-memory replay buffer. */
799
812
  replayBufferSize?: number; /** Hard byte cap on replay buffer payloads. Older events are evicted FIFO until under this cap. */
@@ -831,6 +844,7 @@ declare class FunnelBroadcaster {
831
844
  private readonly clients;
832
845
  private readonly subscribers;
833
846
  private readonly logger;
847
+ private readonly onError;
834
848
  private readonly maxBufferedBytes;
835
849
  private readonly now;
836
850
  private readonly replayBufferSize;
@@ -892,7 +906,8 @@ type SupervisorNotify = (channelName: string, connectorName: string, content: st
892
906
  type Deps$5 = {
893
907
  channels: ConnectorRegistry;
894
908
  notify: SupervisorNotify;
895
- logger?: FunnelLogger;
909
+ logger?: FunnelLogger; /** Host hook for surfacing listener lifecycle exceptions. Defaults to no-op. */
910
+ onError?: OnFunnelError;
896
911
  healthCheckIntervalMs?: number;
897
912
  maxBackoffMs?: number;
898
913
  sleep?: (ms: number) => Promise<void>;
@@ -925,6 +940,7 @@ declare class FunnelListenerSupervisor {
925
940
  private readonly channels;
926
941
  private readonly notify;
927
942
  private readonly logger;
943
+ private readonly onError;
928
944
  private readonly running;
929
945
  private readonly failureCounts;
930
946
  private readonly stats;
@@ -1125,7 +1141,8 @@ type Deps$3 = {
1125
1141
  dbPath?: string;
1126
1142
  process?: FunnelProcessRunner;
1127
1143
  clock?: FunnelClock;
1128
- logger?: FunnelLogger;
1144
+ logger?: FunnelLogger; /** Host hook for surfacing internal exceptions (broadcaster / supervisor). Defaults to no-op. */
1145
+ onError?: OnFunnelError;
1129
1146
  selfPid?: number; /** Funnel home dir, used to scope kill-competing to daemons rooted at the same dir. Defaults to FUNNEL_DIR. */
1130
1147
  dir?: string;
1131
1148
  killCompetingSlack?: boolean; /** Bearer token required for `/listeners*`, `/status`, and `/ws`. Empty string disables auth (tests only). */
@@ -1163,6 +1180,7 @@ declare class FunnelGatewayServer {
1163
1180
  private readonly dbPath;
1164
1181
  private readonly process?;
1165
1182
  private readonly logger;
1183
+ private readonly onError;
1166
1184
  private readonly selfPid;
1167
1185
  private readonly dir;
1168
1186
  private readonly killCompetingSlack;
@@ -1327,6 +1345,12 @@ type Props$5 = {
1327
1345
  * each successful fire, useful for dropping one-shot entries.
1328
1346
  */
1329
1347
  scheduleListenerOptions?: ScheduleListenerOptions;
1348
+ /**
1349
+ * Called when Funnel catches an exception that would otherwise be silently
1350
+ * swallowed (subscriber throw, listener start/stop failure, etc.). Pass
1351
+ * `Sentry.captureException` from the host to surface these. Defaults to no-op.
1352
+ */
1353
+ onError?: OnFunnelError;
1330
1354
  };
1331
1355
  /**
1332
1356
  * Facade exposing every funnel facet as a getter.
@@ -1371,6 +1395,11 @@ declare class Funnel {
1371
1395
  get logger(): FunnelLogger;
1372
1396
  /** Clock boundary. Defaults to NodeFunnelClock. */
1373
1397
  get clock(): FunnelClock;
1398
+ /**
1399
+ * Error hook. Forwards Funnel-internal exceptions that would otherwise be
1400
+ * swallowed. Defaults to a no-op when no host hook was passed.
1401
+ */
1402
+ get onError(): OnFunnelError;
1374
1403
  /** ID generator boundary. Defaults to NodeFunnelIdGenerator. */
1375
1404
  get idGenerator(): FunnelIdGenerator;
1376
1405
  /** Settings reader. If not injected, a FunnelSettingsStore rooted at `dir` is created. */
@@ -4281,4 +4310,4 @@ ${string}`;
4281
4310
  //#region lib/tui/tui.d.ts
4282
4311
  declare function launchTui(funnel: Funnel): Promise<void>;
4283
4312
  //#endregion
4284
- export { AliveStub, AttachOptions, BroadcastEvent, BroadcastSubscriber, ChannelConfig, ChannelConnectorView, ChannelDeliveryMode, ChannelServerOptions, ChannelSpec, ConnectorConfig, ConnectorSpec, ConnectorSyncOutcome, ConnectorType, DEFAULT_GATEWAY_TOKEN_PATH, DetachOptions, DiscordConnectorConfig, Env, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileStat, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEvent, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSessions, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, type GatewayEmitInput, type GatewayRouteDeps, type Env$1 as GatewayServerEnv, GhConnectorConfig, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, LaunchOptions, ListListenersResult, ListenerEntry, ListenerOpResult, LocalConfig, LocalConfigSyncResult, LogEntry, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MemoryProcessCall, MemoryProcessHandler, MemoryProcessResponse, MemoryProcessSyncHandler, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, NotifyFn, ProcessListStub, ProcessSnapshot, ProfileConfig, PublishRequest, PublishResponse, PublishResult, ReplayableEvent, RunOptions, RunResult, SETTINGS_PATH, SETTINGS_VERSION, ScheduleCatchupPolicy, ScheduleConnectorConfig, ScheduleEntry, ScheduleListenerOptions, Settings, SlackConnectorConfig, SlackListenerOptions, SlackProcessed, SlackProcessedEmit, SlackProcessedSkip, SlackRawEvent, 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 };
4313
+ export { AliveStub, AttachOptions, BroadcastEvent, BroadcastSubscriber, ChannelConfig, ChannelConnectorView, ChannelDeliveryMode, ChannelServerOptions, ChannelSpec, ConnectorConfig, ConnectorSpec, ConnectorSyncOutcome, ConnectorType, DEFAULT_GATEWAY_TOKEN_PATH, DetachOptions, DiscordConnectorConfig, Env, FUNNEL_DIR, FUNNEL_MCP_COMMAND, FUNNEL_MCP_NAME, FileStat, Funnel, FunnelBroadcaster, FunnelChannelPublisher, FunnelChannels, FunnelClaude, FunnelClock, FunnelConnectorFactory, FunnelConnectorListener, FunnelDotenvReader, FunnelEvent, FunnelEventStore, FunnelFileSystem, FunnelGateway, FunnelGatewayServer, FunnelGatewayToken, FunnelIdGenerator, FunnelListenerSupervisor, FunnelListenersClient, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLogger, FunnelMcp, FunnelProcessRunner, FunnelProfiles, FunnelSessions, FunnelSettingsReader, FunnelSettingsStore, FunnelSlackEventProcessor, FunnelTokenPrompter, type GatewayEmitInput, type GatewayRouteDeps, type Env$1 as GatewayServerEnv, GhConnectorConfig, LOCAL_CONFIG_FILENAME, LOCAL_ENV_FILENAME, LaunchOptions, ListListenersResult, ListenerEntry, ListenerOpResult, LocalConfig, LocalConfigSyncResult, LogEntry, MemoryFunnelClock, MemoryFunnelFileSystem, MemoryFunnelIdGenerator, MemoryFunnelLogger, MemoryFunnelProcessRunner, MemoryFunnelTokenPrompter, MemoryProcessCall, MemoryProcessHandler, MemoryProcessResponse, MemoryProcessSyncHandler, MockFunnelSettingsReader, NodeFunnelClock, NodeFunnelFileSystem, NodeFunnelIdGenerator, NodeFunnelLogger, NodeFunnelProcessRunner, NodeFunnelTokenPrompter, NoopFunnelLogger, NotifyFn, OnFunnelError, ProcessListStub, ProcessSnapshot, ProfileConfig, PublishRequest, PublishResponse, PublishResult, ReplayableEvent, RunOptions, RunResult, SETTINGS_PATH, SETTINGS_VERSION, ScheduleCatchupPolicy, ScheduleConnectorConfig, ScheduleEntry, ScheduleListenerOptions, Settings, SlackConnectorConfig, SlackListenerOptions, SlackProcessed, SlackProcessedEmit, SlackProcessedSkip, SlackRawEvent, 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 };
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, t as discordConn
2
2
  import { i as FunnelConnectorListener, n as funnelTmpDir, r as FunnelLogger, t as NodeFunnelLogger } from "./node-logger-B97ZiGwj.js";
3
3
  import { a as FunnelProcessRunner, i as NodeFunnelProcessRunner, n as FunnelGhListener, r as FunnelGhAdapter, t as ghConnectorSchema } from "./gh-connector-schema-CAC24s0r.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-BZpH6ZmR.js";
5
- import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-B3jr-RTH.js";
5
+ import { i as FunnelSlackAdapter, n as FunnelSlackListener, r as FunnelSlackEventProcessor, t as slackConnectorSchema } from "./slack-connector-schema-Bi2DyeZw.js";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
8
8
  import { homedir } from "node:os";
@@ -1982,6 +1982,7 @@ const DEFAULT_MAX_BUFFERED_BYTES = 1024 * 1024;
1982
1982
  const DEFAULT_REPLAY_BUFFER_SIZE = 200;
1983
1983
  const DEFAULT_REPLAY_BUFFER_MAX_BYTES = 4 * 1024 * 1024;
1984
1984
  const defaultLogger$3 = new NoopFunnelLogger();
1985
+ const defaultOnError$2 = () => {};
1985
1986
  /**
1986
1987
  * In-process pub/sub for connector events.
1987
1988
  *
@@ -2004,6 +2005,7 @@ var FunnelBroadcaster = class {
2004
2005
  clients = /* @__PURE__ */ new Map();
2005
2006
  subscribers = /* @__PURE__ */ new Set();
2006
2007
  logger;
2008
+ onError;
2007
2009
  maxBufferedBytes;
2008
2010
  now;
2009
2011
  replayBufferSize;
@@ -2018,6 +2020,7 @@ var FunnelBroadcaster = class {
2018
2020
  latestOffset = 0;
2019
2021
  constructor(deps = {}) {
2020
2022
  this.logger = deps.logger ?? defaultLogger$3;
2023
+ this.onError = deps.onError ?? defaultOnError$2;
2021
2024
  this.maxBufferedBytes = deps.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES;
2022
2025
  this.now = deps.now ?? (() => Date.now());
2023
2026
  this.replayBufferSize = Math.max(0, deps.replayBufferSize ?? DEFAULT_REPLAY_BUFFER_SIZE);
@@ -2156,7 +2159,14 @@ var FunnelBroadcaster = class {
2156
2159
  for (const handler of this.subscribers) try {
2157
2160
  handler(event);
2158
2161
  } catch (error) {
2159
- this.logger.error("broadcast subscriber threw", { error: error instanceof Error ? error.message : String(error) });
2162
+ const err = error instanceof Error ? error : new Error(String(error));
2163
+ this.logger.error("broadcast subscriber threw", { error: err.message });
2164
+ this.onError(err, {
2165
+ component: "broadcaster.subscriber",
2166
+ offset: event.offset,
2167
+ connector: event.meta?.connector ?? null,
2168
+ channel: event.meta?.channel ?? null
2169
+ });
2160
2170
  }
2161
2171
  return event;
2162
2172
  }
@@ -2527,6 +2537,7 @@ function truncate$1(content) {
2527
2537
  //#endregion
2528
2538
  //#region lib/gateway/listener-supervisor.ts
2529
2539
  const defaultLogger$2 = new NodeFunnelLogger();
2540
+ const defaultOnError$1 = () => {};
2530
2541
  const DEFAULT_HEALTH_INTERVAL_MS = 3e4;
2531
2542
  const DEFAULT_MAX_BACKOFF_MS = 6e4;
2532
2543
  const defaultSleep = (ms) => new Promise((r) => {
@@ -2548,6 +2559,7 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
2548
2559
  channels;
2549
2560
  notify;
2550
2561
  logger;
2562
+ onError;
2551
2563
  running = /* @__PURE__ */ new Map();
2552
2564
  failureCounts = /* @__PURE__ */ new Map();
2553
2565
  stats = /* @__PURE__ */ new Map();
@@ -2561,6 +2573,7 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
2561
2573
  this.channels = deps.channels;
2562
2574
  this.notify = deps.notify;
2563
2575
  this.logger = deps.logger ?? defaultLogger$2;
2576
+ this.onError = deps.onError ?? defaultOnError$1;
2564
2577
  this.healthCheckIntervalMs = deps.healthCheckIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS;
2565
2578
  this.maxBackoffMs = deps.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
2566
2579
  this.sleep = deps.sleep ?? defaultSleep;
@@ -2623,14 +2636,21 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
2623
2636
  });
2624
2637
  return { ok: true };
2625
2638
  } catch (error) {
2639
+ const err = error instanceof Error ? error : new Error(String(error));
2626
2640
  this.logger.error(`${created.config.type} listener failed to start`, {
2627
2641
  channel: channelName,
2628
2642
  connector: connectorName,
2629
- error: error instanceof Error ? error.message : String(error)
2643
+ error: err.message
2644
+ });
2645
+ this.onError(err, {
2646
+ component: "listener-supervisor.start",
2647
+ channel: channelName,
2648
+ connector: connectorName,
2649
+ type: created.config.type
2630
2650
  });
2631
2651
  return {
2632
2652
  ok: false,
2633
- reason: error instanceof Error ? error.message : String(error)
2653
+ reason: err.message
2634
2654
  };
2635
2655
  }
2636
2656
  }
@@ -2651,14 +2671,21 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
2651
2671
  });
2652
2672
  return { ok: true };
2653
2673
  } catch (error) {
2674
+ const err = error instanceof Error ? error : new Error(String(error));
2654
2675
  this.logger.error(`${entry.config.type} listener failed to stop`, {
2655
2676
  channel: channelName,
2656
2677
  connector: connectorName,
2657
- error: error instanceof Error ? error.message : String(error)
2678
+ error: err.message
2679
+ });
2680
+ this.onError(err, {
2681
+ component: "listener-supervisor.stop",
2682
+ channel: channelName,
2683
+ connector: connectorName,
2684
+ type: entry.config.type
2658
2685
  });
2659
2686
  return {
2660
2687
  ok: false,
2661
- reason: error instanceof Error ? error.message : String(error)
2688
+ reason: err.message
2662
2689
  };
2663
2690
  }
2664
2691
  }
@@ -2939,6 +2966,7 @@ const gatewayRoutes = factory$1.createApp().get("/health", ...healthHandler).get
2939
2966
  const DEFAULT_PORT = 9742;
2940
2967
  const defaultDbPath = () => join(funnelTmpDir(), "events.db");
2941
2968
  const defaultLogger = new NodeFunnelLogger();
2969
+ const defaultOnError = () => {};
2942
2970
  /**
2943
2971
  * In-process gateway: runs `Bun.serve` (HTTP + WebSocket /ws), boots connector
2944
2972
  * listeners through `FunnelListenerSupervisor`, fans events out via
@@ -2956,6 +2984,7 @@ var FunnelGatewayServer = class {
2956
2984
  dbPath;
2957
2985
  process;
2958
2986
  logger;
2987
+ onError;
2959
2988
  selfPid;
2960
2989
  dir;
2961
2990
  killCompetingSlack;
@@ -2974,6 +3003,7 @@ var FunnelGatewayServer = class {
2974
3003
  this.dbPath = deps.dbPath ?? defaultDbPath();
2975
3004
  this.process = deps.process;
2976
3005
  this.logger = deps.logger ?? defaultLogger;
3006
+ this.onError = deps.onError ?? defaultOnError;
2977
3007
  this.selfPid = deps.selfPid ?? globalThis.process.pid;
2978
3008
  this.dir = deps.dir ?? FUNNEL_DIR;
2979
3009
  this.killCompetingSlack = deps.killCompetingSlack ?? true;
@@ -2989,6 +3019,7 @@ var FunnelGatewayServer = class {
2989
3019
  });
2990
3020
  this.broadcaster = new FunnelBroadcaster({
2991
3021
  logger: this.logger,
3022
+ onError: this.onError,
2992
3023
  now: this.nowMs,
2993
3024
  persistentReplay: this.eventStore
2994
3025
  });
@@ -2996,6 +3027,7 @@ var FunnelGatewayServer = class {
2996
3027
  this.supervisor = new FunnelListenerSupervisor({
2997
3028
  channels: this.channels,
2998
3029
  logger: this.logger,
3030
+ onError: this.onError,
2999
3031
  notify: async (channelName, connectorName, content, meta) => {
3000
3032
  this.emit({
3001
3033
  channel: channelName,
@@ -3377,6 +3409,7 @@ var FunnelListenersClient = class {
3377
3409
  //#region lib/funnel.ts
3378
3410
  const SANDBOX_DIR = "/sandbox/.funnel";
3379
3411
  const SANDBOX_TMP_DIR = "/sandbox/tmp";
3412
+ const noopOnError = () => {};
3380
3413
  /**
3381
3414
  * Facade exposing every funnel facet as a getter.
3382
3415
  *
@@ -3448,6 +3481,13 @@ var Funnel = class Funnel {
3448
3481
  if (!this.memos.clock) this.memos.clock = this.props.clock ?? new NodeFunnelClock();
3449
3482
  return this.memos.clock;
3450
3483
  }
3484
+ /**
3485
+ * Error hook. Forwards Funnel-internal exceptions that would otherwise be
3486
+ * swallowed. Defaults to a no-op when no host hook was passed.
3487
+ */
3488
+ get onError() {
3489
+ return this.props.onError ?? noopOnError;
3490
+ }
3451
3491
  /** ID generator boundary. Defaults to NodeFunnelIdGenerator. */
3452
3492
  get idGenerator() {
3453
3493
  if (!this.memos.idGenerator) this.memos.idGenerator = this.props.idGenerator ?? new NodeFunnelIdGenerator();
@@ -3608,6 +3648,7 @@ var Funnel = class Funnel {
3608
3648
  process: this.process,
3609
3649
  clock: this.clock,
3610
3650
  logger: this.logger,
3651
+ onError: this.onError,
3611
3652
  dir: this.paths.dir,
3612
3653
  killCompetingSlack: options.killCompetingSlack,
3613
3654
  token: options.token ?? this.gatewayToken.ensure(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interactive-inc/claude-funnel",
3
- "version": "0.22.1",
3
+ "version": "0.23.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",
@@ -140,6 +140,7 @@ var FunnelSlackListener = class extends FunnelConnectorListener {
140
140
  if (event === null) return;
141
141
  const result = processor.process(event);
142
142
  if (result.skip) return;
143
+ await notify(result.content, result.meta);
143
144
  if (result.shouldReact) try {
144
145
  await app.client.reactions.add({
145
146
  token: this.config.botToken,
@@ -148,7 +149,6 @@ var FunnelSlackListener = class extends FunnelConnectorListener {
148
149
  name: "eyes"
149
150
  });
150
151
  } catch {}
151
- await notify(result.content, result.meta);
152
152
  });
153
153
  app.error(async (error) => {
154
154
  this.logger.error("Slack error", { error: error instanceof Error ? error.message : String(error) });