@interactive-inc/claude-funnel 0.22.0 → 0.23.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/bin.js +73 -73
- package/dist/gateway/daemon.js +65 -65
- package/dist/index.d.ts +49 -14
- package/dist/index.js +80 -26
- package/package.json +4 -1
package/dist/index.d.ts
CHANGED
|
@@ -405,10 +405,15 @@ type Deps$13 = {
|
|
|
405
405
|
/**
|
|
406
406
|
* Per-channel persistent Claude Code session IDs, keyed by the cwd the
|
|
407
407
|
* channel was launched from. The whole point is to give each (channel, cwd)
|
|
408
|
-
* its own stable conversation: relaunching from the same path
|
|
409
|
-
* previous claude session via `--
|
|
410
|
-
*
|
|
411
|
-
*
|
|
408
|
+
* its own stable conversation: relaunching from the same path resumes the
|
|
409
|
+
* previous claude session via `--resume <uuid>`, while a different cwd (or
|
|
410
|
+
* a different channel) gets an independent one — so sessions never silently
|
|
411
|
+
* bleed across workspaces the way claude's `-c` does.
|
|
412
|
+
*
|
|
413
|
+
* `get` and `create` are intentionally separate: claude's `--session-id`
|
|
414
|
+
* only accepts a fresh UUID (it errors if the session jsonl already
|
|
415
|
+
* exists), so callers must check `get` first and fall back to `create`
|
|
416
|
+
* only when there is nothing to resume.
|
|
412
417
|
*
|
|
413
418
|
* Storage lives under `<dir>/channels/<channel-id>/sessions.json` (channel
|
|
414
419
|
* id, not name, so renames don't lose history). The file is a flat
|
|
@@ -419,10 +424,10 @@ declare class FunnelSessions {
|
|
|
419
424
|
private readonly idGenerator;
|
|
420
425
|
private readonly dir;
|
|
421
426
|
constructor(deps: Deps$13);
|
|
422
|
-
/** Returns the existing session id for (channelId, cwd) or generates and persists a new one. */
|
|
423
|
-
getOrCreate(channelId: string, cwd: string): string;
|
|
424
427
|
/** Returns the existing session id for (channelId, cwd) or null. */
|
|
425
428
|
get(channelId: string, cwd: string): string | null;
|
|
429
|
+
/** Generates a new session id for (channelId, cwd) and persists it, overwriting any prior entry. */
|
|
430
|
+
create(channelId: string, cwd: string): string;
|
|
426
431
|
/** Drops the recorded session id for (channelId, cwd). No-op if absent. */
|
|
427
432
|
clear(channelId: string, cwd: string): void;
|
|
428
433
|
/** Drops the whole session map for the channel (e.g. when the channel is deleted). */
|
|
@@ -484,14 +489,27 @@ declare class FunnelClaude {
|
|
|
484
489
|
private isProcessAlive;
|
|
485
490
|
private buildArgs;
|
|
486
491
|
/**
|
|
487
|
-
* Decides whether funnel should
|
|
488
|
-
* the user already passed a
|
|
489
|
-
*
|
|
492
|
+
* Decides whether funnel should resume an existing claude session or start
|
|
493
|
+
* a freshly minted one. Backs off when the user already passed a
|
|
494
|
+
* session-shaping flag, since combining them would either confuse claude
|
|
495
|
+
* or override the explicit user intent.
|
|
490
496
|
*/
|
|
491
|
-
private
|
|
497
|
+
private resolveSession;
|
|
492
498
|
private buildEnv;
|
|
493
499
|
}
|
|
494
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
|
|
495
513
|
//#region lib/engine/local-config/dotenv-reader.d.ts
|
|
496
514
|
type Deps$11 = {
|
|
497
515
|
fs: FunnelFileSystem;
|
|
@@ -787,7 +805,8 @@ type ReplaySource = {
|
|
|
787
805
|
loadSince(since: number): ReplayableEvent[];
|
|
788
806
|
};
|
|
789
807
|
type Deps$6 = {
|
|
790
|
-
logger?: FunnelLogger;
|
|
808
|
+
logger?: FunnelLogger; /** Host hook for surfacing subscriber-throw exceptions. Defaults to no-op. */
|
|
809
|
+
onError?: OnFunnelError;
|
|
791
810
|
maxBufferedBytes?: number;
|
|
792
811
|
now?: () => number; /** Number of recent events kept in the in-memory replay buffer. */
|
|
793
812
|
replayBufferSize?: number; /** Hard byte cap on replay buffer payloads. Older events are evicted FIFO until under this cap. */
|
|
@@ -825,6 +844,7 @@ declare class FunnelBroadcaster {
|
|
|
825
844
|
private readonly clients;
|
|
826
845
|
private readonly subscribers;
|
|
827
846
|
private readonly logger;
|
|
847
|
+
private readonly onError;
|
|
828
848
|
private readonly maxBufferedBytes;
|
|
829
849
|
private readonly now;
|
|
830
850
|
private readonly replayBufferSize;
|
|
@@ -886,7 +906,8 @@ type SupervisorNotify = (channelName: string, connectorName: string, content: st
|
|
|
886
906
|
type Deps$5 = {
|
|
887
907
|
channels: ConnectorRegistry;
|
|
888
908
|
notify: SupervisorNotify;
|
|
889
|
-
logger?: FunnelLogger;
|
|
909
|
+
logger?: FunnelLogger; /** Host hook for surfacing listener lifecycle exceptions. Defaults to no-op. */
|
|
910
|
+
onError?: OnFunnelError;
|
|
890
911
|
healthCheckIntervalMs?: number;
|
|
891
912
|
maxBackoffMs?: number;
|
|
892
913
|
sleep?: (ms: number) => Promise<void>;
|
|
@@ -919,6 +940,7 @@ declare class FunnelListenerSupervisor {
|
|
|
919
940
|
private readonly channels;
|
|
920
941
|
private readonly notify;
|
|
921
942
|
private readonly logger;
|
|
943
|
+
private readonly onError;
|
|
922
944
|
private readonly running;
|
|
923
945
|
private readonly failureCounts;
|
|
924
946
|
private readonly stats;
|
|
@@ -1119,7 +1141,8 @@ type Deps$3 = {
|
|
|
1119
1141
|
dbPath?: string;
|
|
1120
1142
|
process?: FunnelProcessRunner;
|
|
1121
1143
|
clock?: FunnelClock;
|
|
1122
|
-
logger?: FunnelLogger;
|
|
1144
|
+
logger?: FunnelLogger; /** Host hook for surfacing internal exceptions (broadcaster / supervisor). Defaults to no-op. */
|
|
1145
|
+
onError?: OnFunnelError;
|
|
1123
1146
|
selfPid?: number; /** Funnel home dir, used to scope kill-competing to daemons rooted at the same dir. Defaults to FUNNEL_DIR. */
|
|
1124
1147
|
dir?: string;
|
|
1125
1148
|
killCompetingSlack?: boolean; /** Bearer token required for `/listeners*`, `/status`, and `/ws`. Empty string disables auth (tests only). */
|
|
@@ -1157,6 +1180,7 @@ declare class FunnelGatewayServer {
|
|
|
1157
1180
|
private readonly dbPath;
|
|
1158
1181
|
private readonly process?;
|
|
1159
1182
|
private readonly logger;
|
|
1183
|
+
private readonly onError;
|
|
1160
1184
|
private readonly selfPid;
|
|
1161
1185
|
private readonly dir;
|
|
1162
1186
|
private readonly killCompetingSlack;
|
|
@@ -1321,6 +1345,12 @@ type Props$5 = {
|
|
|
1321
1345
|
* each successful fire, useful for dropping one-shot entries.
|
|
1322
1346
|
*/
|
|
1323
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;
|
|
1324
1354
|
};
|
|
1325
1355
|
/**
|
|
1326
1356
|
* Facade exposing every funnel facet as a getter.
|
|
@@ -1365,6 +1395,11 @@ declare class Funnel {
|
|
|
1365
1395
|
get logger(): FunnelLogger;
|
|
1366
1396
|
/** Clock boundary. Defaults to NodeFunnelClock. */
|
|
1367
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;
|
|
1368
1403
|
/** ID generator boundary. Defaults to NodeFunnelIdGenerator. */
|
|
1369
1404
|
get idGenerator(): FunnelIdGenerator;
|
|
1370
1405
|
/** Settings reader. If not injected, a FunnelSettingsStore rooted at `dir` is created. */
|
|
@@ -4275,4 +4310,4 @@ ${string}`;
|
|
|
4275
4310
|
//#region lib/tui/tui.d.ts
|
|
4276
4311
|
declare function launchTui(funnel: Funnel): Promise<void>;
|
|
4277
4312
|
//#endregion
|
|
4278
|
-
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
|
@@ -597,8 +597,8 @@ var FunnelClaude = class {
|
|
|
597
597
|
this.writePidFile(options.profileName);
|
|
598
598
|
this.installCleanup(options.profileName);
|
|
599
599
|
}
|
|
600
|
-
const
|
|
601
|
-
const claudeArgs = this.buildArgs(channel.options, options.userArgs ?? [], cwd,
|
|
600
|
+
const session = channel.resume ? this.resolveSession(channel.id, cwd, options.userArgs ?? []) : null;
|
|
601
|
+
const claudeArgs = this.buildArgs(channel.options, options.userArgs ?? [], cwd, session);
|
|
602
602
|
const env = this.buildEnv(channel.id, channel.env);
|
|
603
603
|
this.logger.info(`claude launch`, {
|
|
604
604
|
channel: options.channel,
|
|
@@ -649,25 +649,35 @@ var FunnelClaude = class {
|
|
|
649
649
|
isProcessAlive(pid) {
|
|
650
650
|
return this.process.isAlive(pid);
|
|
651
651
|
}
|
|
652
|
-
buildArgs(channelOptions, userArgs, cwd,
|
|
652
|
+
buildArgs(channelOptions, userArgs, cwd, session) {
|
|
653
653
|
const result = [...channelOptions, ...userArgs];
|
|
654
|
-
if (
|
|
654
|
+
if (session !== null) if (session.mode === "resume") result.push("--resume", session.id);
|
|
655
|
+
else result.push("--session-id", session.id);
|
|
655
656
|
const mcpName = this.mcp.findInstalledName(cwd);
|
|
656
657
|
if (mcpName && !result.includes("--dangerously-load-development-channels") && !result.includes("--channels")) result.push("--dangerously-load-development-channels", `server:${mcpName}`);
|
|
657
658
|
return result;
|
|
658
659
|
}
|
|
659
660
|
/**
|
|
660
|
-
* Decides whether funnel should
|
|
661
|
-
* the user already passed a
|
|
662
|
-
*
|
|
661
|
+
* Decides whether funnel should resume an existing claude session or start
|
|
662
|
+
* a freshly minted one. Backs off when the user already passed a
|
|
663
|
+
* session-shaping flag, since combining them would either confuse claude
|
|
664
|
+
* or override the explicit user intent.
|
|
663
665
|
*/
|
|
664
|
-
|
|
666
|
+
resolveSession(channelId, cwd, userArgs) {
|
|
665
667
|
for (const arg of userArgs) {
|
|
666
668
|
if (arg === "-c" || arg === "--continue") return null;
|
|
667
669
|
if (arg === "--resume" || arg.startsWith("--resume=")) return null;
|
|
668
670
|
if (arg === "--session-id" || arg.startsWith("--session-id=")) return null;
|
|
669
671
|
}
|
|
670
|
-
|
|
672
|
+
const existing = this.sessions.get(channelId, cwd);
|
|
673
|
+
if (existing !== null) return {
|
|
674
|
+
id: existing,
|
|
675
|
+
mode: "resume"
|
|
676
|
+
};
|
|
677
|
+
return {
|
|
678
|
+
id: this.sessions.create(channelId, cwd),
|
|
679
|
+
mode: "new"
|
|
680
|
+
};
|
|
671
681
|
}
|
|
672
682
|
buildEnv(channelId, channelEnv) {
|
|
673
683
|
const env = {};
|
|
@@ -1492,10 +1502,15 @@ const sessionsMapSchema = z.record(z.string(), z.string());
|
|
|
1492
1502
|
/**
|
|
1493
1503
|
* Per-channel persistent Claude Code session IDs, keyed by the cwd the
|
|
1494
1504
|
* channel was launched from. The whole point is to give each (channel, cwd)
|
|
1495
|
-
* its own stable conversation: relaunching from the same path
|
|
1496
|
-
* previous claude session via `--
|
|
1497
|
-
*
|
|
1498
|
-
*
|
|
1505
|
+
* its own stable conversation: relaunching from the same path resumes the
|
|
1506
|
+
* previous claude session via `--resume <uuid>`, while a different cwd (or
|
|
1507
|
+
* a different channel) gets an independent one — so sessions never silently
|
|
1508
|
+
* bleed across workspaces the way claude's `-c` does.
|
|
1509
|
+
*
|
|
1510
|
+
* `get` and `create` are intentionally separate: claude's `--session-id`
|
|
1511
|
+
* only accepts a fresh UUID (it errors if the session jsonl already
|
|
1512
|
+
* exists), so callers must check `get` first and fall back to `create`
|
|
1513
|
+
* only when there is nothing to resume.
|
|
1499
1514
|
*
|
|
1500
1515
|
* Storage lives under `<dir>/channels/<channel-id>/sessions.json` (channel
|
|
1501
1516
|
* id, not name, so renames don't lose history). The file is a flat
|
|
@@ -1511,20 +1526,18 @@ var FunnelSessions = class {
|
|
|
1511
1526
|
this.dir = deps.dir;
|
|
1512
1527
|
Object.freeze(this);
|
|
1513
1528
|
}
|
|
1514
|
-
/** Returns the existing session id for (channelId, cwd) or
|
|
1515
|
-
|
|
1529
|
+
/** Returns the existing session id for (channelId, cwd) or null. */
|
|
1530
|
+
get(channelId, cwd) {
|
|
1531
|
+
return this.readMap(channelId)[cwd] ?? null;
|
|
1532
|
+
}
|
|
1533
|
+
/** Generates a new session id for (channelId, cwd) and persists it, overwriting any prior entry. */
|
|
1534
|
+
create(channelId, cwd) {
|
|
1516
1535
|
const map = this.readMap(channelId);
|
|
1517
|
-
const existing = map[cwd];
|
|
1518
|
-
if (existing) return existing;
|
|
1519
1536
|
const sessionId = this.idGenerator.generate();
|
|
1520
1537
|
map[cwd] = sessionId;
|
|
1521
1538
|
this.writeMap(channelId, map);
|
|
1522
1539
|
return sessionId;
|
|
1523
1540
|
}
|
|
1524
|
-
/** Returns the existing session id for (channelId, cwd) or null. */
|
|
1525
|
-
get(channelId, cwd) {
|
|
1526
|
-
return this.readMap(channelId)[cwd] ?? null;
|
|
1527
|
-
}
|
|
1528
1541
|
/** Drops the recorded session id for (channelId, cwd). No-op if absent. */
|
|
1529
1542
|
clear(channelId, cwd) {
|
|
1530
1543
|
const map = this.readMap(channelId);
|
|
@@ -1969,6 +1982,7 @@ const DEFAULT_MAX_BUFFERED_BYTES = 1024 * 1024;
|
|
|
1969
1982
|
const DEFAULT_REPLAY_BUFFER_SIZE = 200;
|
|
1970
1983
|
const DEFAULT_REPLAY_BUFFER_MAX_BYTES = 4 * 1024 * 1024;
|
|
1971
1984
|
const defaultLogger$3 = new NoopFunnelLogger();
|
|
1985
|
+
const defaultOnError$2 = () => {};
|
|
1972
1986
|
/**
|
|
1973
1987
|
* In-process pub/sub for connector events.
|
|
1974
1988
|
*
|
|
@@ -1991,6 +2005,7 @@ var FunnelBroadcaster = class {
|
|
|
1991
2005
|
clients = /* @__PURE__ */ new Map();
|
|
1992
2006
|
subscribers = /* @__PURE__ */ new Set();
|
|
1993
2007
|
logger;
|
|
2008
|
+
onError;
|
|
1994
2009
|
maxBufferedBytes;
|
|
1995
2010
|
now;
|
|
1996
2011
|
replayBufferSize;
|
|
@@ -2005,6 +2020,7 @@ var FunnelBroadcaster = class {
|
|
|
2005
2020
|
latestOffset = 0;
|
|
2006
2021
|
constructor(deps = {}) {
|
|
2007
2022
|
this.logger = deps.logger ?? defaultLogger$3;
|
|
2023
|
+
this.onError = deps.onError ?? defaultOnError$2;
|
|
2008
2024
|
this.maxBufferedBytes = deps.maxBufferedBytes ?? DEFAULT_MAX_BUFFERED_BYTES;
|
|
2009
2025
|
this.now = deps.now ?? (() => Date.now());
|
|
2010
2026
|
this.replayBufferSize = Math.max(0, deps.replayBufferSize ?? DEFAULT_REPLAY_BUFFER_SIZE);
|
|
@@ -2143,7 +2159,14 @@ var FunnelBroadcaster = class {
|
|
|
2143
2159
|
for (const handler of this.subscribers) try {
|
|
2144
2160
|
handler(event);
|
|
2145
2161
|
} catch (error) {
|
|
2146
|
-
|
|
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
|
+
});
|
|
2147
2170
|
}
|
|
2148
2171
|
return event;
|
|
2149
2172
|
}
|
|
@@ -2514,6 +2537,7 @@ function truncate$1(content) {
|
|
|
2514
2537
|
//#endregion
|
|
2515
2538
|
//#region lib/gateway/listener-supervisor.ts
|
|
2516
2539
|
const defaultLogger$2 = new NodeFunnelLogger();
|
|
2540
|
+
const defaultOnError$1 = () => {};
|
|
2517
2541
|
const DEFAULT_HEALTH_INTERVAL_MS = 3e4;
|
|
2518
2542
|
const DEFAULT_MAX_BACKOFF_MS = 6e4;
|
|
2519
2543
|
const defaultSleep = (ms) => new Promise((r) => {
|
|
@@ -2535,6 +2559,7 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
2535
2559
|
channels;
|
|
2536
2560
|
notify;
|
|
2537
2561
|
logger;
|
|
2562
|
+
onError;
|
|
2538
2563
|
running = /* @__PURE__ */ new Map();
|
|
2539
2564
|
failureCounts = /* @__PURE__ */ new Map();
|
|
2540
2565
|
stats = /* @__PURE__ */ new Map();
|
|
@@ -2548,6 +2573,7 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
2548
2573
|
this.channels = deps.channels;
|
|
2549
2574
|
this.notify = deps.notify;
|
|
2550
2575
|
this.logger = deps.logger ?? defaultLogger$2;
|
|
2576
|
+
this.onError = deps.onError ?? defaultOnError$1;
|
|
2551
2577
|
this.healthCheckIntervalMs = deps.healthCheckIntervalMs ?? DEFAULT_HEALTH_INTERVAL_MS;
|
|
2552
2578
|
this.maxBackoffMs = deps.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
|
|
2553
2579
|
this.sleep = deps.sleep ?? defaultSleep;
|
|
@@ -2610,14 +2636,21 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
2610
2636
|
});
|
|
2611
2637
|
return { ok: true };
|
|
2612
2638
|
} catch (error) {
|
|
2639
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2613
2640
|
this.logger.error(`${created.config.type} listener failed to start`, {
|
|
2614
2641
|
channel: channelName,
|
|
2615
2642
|
connector: connectorName,
|
|
2616
|
-
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
|
|
2617
2650
|
});
|
|
2618
2651
|
return {
|
|
2619
2652
|
ok: false,
|
|
2620
|
-
reason:
|
|
2653
|
+
reason: err.message
|
|
2621
2654
|
};
|
|
2622
2655
|
}
|
|
2623
2656
|
}
|
|
@@ -2638,14 +2671,21 @@ var FunnelListenerSupervisor = class FunnelListenerSupervisor {
|
|
|
2638
2671
|
});
|
|
2639
2672
|
return { ok: true };
|
|
2640
2673
|
} catch (error) {
|
|
2674
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2641
2675
|
this.logger.error(`${entry.config.type} listener failed to stop`, {
|
|
2642
2676
|
channel: channelName,
|
|
2643
2677
|
connector: connectorName,
|
|
2644
|
-
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
|
|
2645
2685
|
});
|
|
2646
2686
|
return {
|
|
2647
2687
|
ok: false,
|
|
2648
|
-
reason:
|
|
2688
|
+
reason: err.message
|
|
2649
2689
|
};
|
|
2650
2690
|
}
|
|
2651
2691
|
}
|
|
@@ -2926,6 +2966,7 @@ const gatewayRoutes = factory$1.createApp().get("/health", ...healthHandler).get
|
|
|
2926
2966
|
const DEFAULT_PORT = 9742;
|
|
2927
2967
|
const defaultDbPath = () => join(funnelTmpDir(), "events.db");
|
|
2928
2968
|
const defaultLogger = new NodeFunnelLogger();
|
|
2969
|
+
const defaultOnError = () => {};
|
|
2929
2970
|
/**
|
|
2930
2971
|
* In-process gateway: runs `Bun.serve` (HTTP + WebSocket /ws), boots connector
|
|
2931
2972
|
* listeners through `FunnelListenerSupervisor`, fans events out via
|
|
@@ -2943,6 +2984,7 @@ var FunnelGatewayServer = class {
|
|
|
2943
2984
|
dbPath;
|
|
2944
2985
|
process;
|
|
2945
2986
|
logger;
|
|
2987
|
+
onError;
|
|
2946
2988
|
selfPid;
|
|
2947
2989
|
dir;
|
|
2948
2990
|
killCompetingSlack;
|
|
@@ -2961,6 +3003,7 @@ var FunnelGatewayServer = class {
|
|
|
2961
3003
|
this.dbPath = deps.dbPath ?? defaultDbPath();
|
|
2962
3004
|
this.process = deps.process;
|
|
2963
3005
|
this.logger = deps.logger ?? defaultLogger;
|
|
3006
|
+
this.onError = deps.onError ?? defaultOnError;
|
|
2964
3007
|
this.selfPid = deps.selfPid ?? globalThis.process.pid;
|
|
2965
3008
|
this.dir = deps.dir ?? FUNNEL_DIR;
|
|
2966
3009
|
this.killCompetingSlack = deps.killCompetingSlack ?? true;
|
|
@@ -2976,6 +3019,7 @@ var FunnelGatewayServer = class {
|
|
|
2976
3019
|
});
|
|
2977
3020
|
this.broadcaster = new FunnelBroadcaster({
|
|
2978
3021
|
logger: this.logger,
|
|
3022
|
+
onError: this.onError,
|
|
2979
3023
|
now: this.nowMs,
|
|
2980
3024
|
persistentReplay: this.eventStore
|
|
2981
3025
|
});
|
|
@@ -2983,6 +3027,7 @@ var FunnelGatewayServer = class {
|
|
|
2983
3027
|
this.supervisor = new FunnelListenerSupervisor({
|
|
2984
3028
|
channels: this.channels,
|
|
2985
3029
|
logger: this.logger,
|
|
3030
|
+
onError: this.onError,
|
|
2986
3031
|
notify: async (channelName, connectorName, content, meta) => {
|
|
2987
3032
|
this.emit({
|
|
2988
3033
|
channel: channelName,
|
|
@@ -3364,6 +3409,7 @@ var FunnelListenersClient = class {
|
|
|
3364
3409
|
//#region lib/funnel.ts
|
|
3365
3410
|
const SANDBOX_DIR = "/sandbox/.funnel";
|
|
3366
3411
|
const SANDBOX_TMP_DIR = "/sandbox/tmp";
|
|
3412
|
+
const noopOnError = () => {};
|
|
3367
3413
|
/**
|
|
3368
3414
|
* Facade exposing every funnel facet as a getter.
|
|
3369
3415
|
*
|
|
@@ -3435,6 +3481,13 @@ var Funnel = class Funnel {
|
|
|
3435
3481
|
if (!this.memos.clock) this.memos.clock = this.props.clock ?? new NodeFunnelClock();
|
|
3436
3482
|
return this.memos.clock;
|
|
3437
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
|
+
}
|
|
3438
3491
|
/** ID generator boundary. Defaults to NodeFunnelIdGenerator. */
|
|
3439
3492
|
get idGenerator() {
|
|
3440
3493
|
if (!this.memos.idGenerator) this.memos.idGenerator = this.props.idGenerator ?? new NodeFunnelIdGenerator();
|
|
@@ -3595,6 +3648,7 @@ var Funnel = class Funnel {
|
|
|
3595
3648
|
process: this.process,
|
|
3596
3649
|
clock: this.clock,
|
|
3597
3650
|
logger: this.logger,
|
|
3651
|
+
onError: this.onError,
|
|
3598
3652
|
dir: this.paths.dir,
|
|
3599
3653
|
killCompetingSlack: options.killCompetingSlack,
|
|
3600
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.
|
|
3
|
+
"version": "0.23.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",
|
|
@@ -90,5 +90,8 @@
|
|
|
90
90
|
},
|
|
91
91
|
"engines": {
|
|
92
92
|
"bun": ">=1.3.0"
|
|
93
|
+
},
|
|
94
|
+
"scripts": {
|
|
95
|
+
"prepack": "make build"
|
|
93
96
|
}
|
|
94
97
|
}
|