@interactive-inc/claude-funnel 0.19.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.d.ts CHANGED
@@ -63,7 +63,7 @@ type SlackListenerOptions = {
63
63
  type ScheduleListenerOptions = {
64
64
  onFired?: ScheduleOnFired;
65
65
  };
66
- type Deps$15 = {
66
+ type Deps$16 = {
67
67
  fs?: FunnelFileSystem;
68
68
  process?: FunnelProcessRunner;
69
69
  logger?: FunnelLogger;
@@ -90,7 +90,7 @@ declare class FunnelConnectorFactory {
90
90
  private readonly dir;
91
91
  private readonly slackListenerOptions;
92
92
  private readonly scheduleListenerOptions;
93
- constructor(deps?: Deps$15);
93
+ constructor(deps?: Deps$16);
94
94
  createListener(channelId: string, config: ConnectorConfig): FunnelConnectorListener;
95
95
  createAdapter(config: ConnectorConfig): FunnelConnectorAdapter | null;
96
96
  connectorDir(channelId: string, connectorId: string): string;
@@ -152,6 +152,7 @@ declare const channelConfigSchema: z.ZodObject<{
152
152
  }>>;
153
153
  options: z.ZodDefault<z.ZodArray<z.ZodString>>;
154
154
  env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
155
+ resume: z.ZodDefault<z.ZodBoolean>;
155
156
  connectors: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
156
157
  id: z.ZodString;
157
158
  name: z.ZodString;
@@ -212,6 +213,7 @@ declare const settingsSchema: z.ZodObject<{
212
213
  }>>;
213
214
  options: z.ZodDefault<z.ZodArray<z.ZodString>>;
214
215
  env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
216
+ resume: z.ZodDefault<z.ZodBoolean>;
215
217
  connectors: z.ZodDefault<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
216
218
  id: z.ZodString;
217
219
  name: z.ZodString;
@@ -268,7 +270,7 @@ declare abstract class FunnelSettingsReader {
268
270
  }
269
271
  //#endregion
270
272
  //#region lib/engine/channels/channels.d.ts
271
- type Deps$14 = {
273
+ type Deps$15 = {
272
274
  store: FunnelSettingsReader;
273
275
  factory: FunnelConnectorFactory;
274
276
  profileChecker: ProfileChannelChecker;
@@ -311,7 +313,7 @@ declare class FunnelChannels {
311
313
  private readonly profileChecker;
312
314
  private readonly clock;
313
315
  private readonly idGenerator;
314
- constructor(deps: Deps$14);
316
+ constructor(deps: Deps$15);
315
317
  list(): ChannelConfig[];
316
318
  get(name: string): ChannelConfig | null;
317
319
  getById(id: string): ChannelConfig | null;
@@ -320,8 +322,10 @@ declare class FunnelChannels {
320
322
  delivery?: ChannelDeliveryMode;
321
323
  options?: string[];
322
324
  env?: Record<string, string>;
325
+ resume?: boolean;
323
326
  }): ChannelConfig;
324
327
  setDelivery(name: string, delivery: ChannelDeliveryMode): void;
328
+ setResume(name: string, resume: boolean): void;
325
329
  setOptions(name: string, options: string[]): void;
326
330
  setEnv(name: string, env: Record<string, string>): void;
327
331
  remove(name: string): void;
@@ -373,7 +377,7 @@ type GatewayController = {
373
377
  //#region lib/engine/mcp/mcp.d.ts
374
378
  declare const FUNNEL_MCP_COMMAND = "funnel";
375
379
  declare const FUNNEL_MCP_NAME = "funnel";
376
- type Deps$13 = {
380
+ type Deps$14 = {
377
381
  fs?: FunnelFileSystem;
378
382
  };
379
383
  /**
@@ -383,7 +387,7 @@ type Deps$13 = {
383
387
  */
384
388
  declare class FunnelMcp {
385
389
  private readonly fs;
386
- constructor(deps?: Deps$13);
390
+ constructor(deps?: Deps$14);
387
391
  install(repoPath: string): void;
388
392
  uninstall(repoPath: string): void;
389
393
  findInstalledName(cwd: string): string | null;
@@ -392,6 +396,43 @@ declare class FunnelMcp {
392
396
  private writeConfig;
393
397
  }
394
398
  //#endregion
399
+ //#region lib/engine/sessions/sessions.d.ts
400
+ type Deps$13 = {
401
+ fs: FunnelFileSystem;
402
+ idGenerator: FunnelIdGenerator;
403
+ dir: string;
404
+ };
405
+ /**
406
+ * Per-channel persistent Claude Code session IDs, keyed by the cwd the
407
+ * channel was launched from. The whole point is to give each (channel, cwd)
408
+ * its own stable conversation: relaunching from the same path picks up the
409
+ * previous claude session via `--session-id <uuid>`, while a different cwd
410
+ * (or a different channel) gets an independent one — so sessions never
411
+ * silently bleed across workspaces the way claude's `-c` does.
412
+ *
413
+ * Storage lives under `<dir>/channels/<channel-id>/sessions.json` (channel
414
+ * id, not name, so renames don't lose history). The file is a flat
415
+ * `{ cwd: uuid }` map; the channel directory itself is created lazily.
416
+ */
417
+ declare class FunnelSessions {
418
+ private readonly fs;
419
+ private readonly idGenerator;
420
+ private readonly dir;
421
+ 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
+ /** Returns the existing session id for (channelId, cwd) or null. */
425
+ get(channelId: string, cwd: string): string | null;
426
+ /** Drops the recorded session id for (channelId, cwd). No-op if absent. */
427
+ clear(channelId: string, cwd: string): void;
428
+ /** Drops the whole session map for the channel (e.g. when the channel is deleted). */
429
+ clearAll(channelId: string): void;
430
+ private readMap;
431
+ private writeMap;
432
+ private channelDir;
433
+ private pathFor;
434
+ }
435
+ //#endregion
395
436
  //#region lib/engine/claude/claude.d.ts
396
437
  type LaunchOptions = {
397
438
  channel: string;
@@ -411,6 +452,7 @@ type Deps$12 = {
411
452
  channels: FunnelChannels;
412
453
  mcp: FunnelMcp;
413
454
  gateway: GatewayController;
455
+ sessions: FunnelSessions;
414
456
  process?: FunnelProcessRunner;
415
457
  fs?: FunnelFileSystem;
416
458
  logger?: FunnelLogger;
@@ -426,6 +468,7 @@ declare class FunnelClaude {
426
468
  private readonly channels;
427
469
  private readonly mcp;
428
470
  private readonly gateway;
471
+ private readonly sessions;
429
472
  private readonly process;
430
473
  private readonly fs;
431
474
  private readonly logger;
@@ -440,6 +483,12 @@ declare class FunnelClaude {
440
483
  private installCleanup;
441
484
  private isProcessAlive;
442
485
  private buildArgs;
486
+ /**
487
+ * Decides whether funnel should inject `--session-id`. We back off when
488
+ * the user already passed a session-shaping flag, since combining them
489
+ * would either confuse claude or override the explicit user intent.
490
+ */
491
+ private resolveSessionId;
443
492
  private buildEnv;
444
493
  }
445
494
  //#endregion
@@ -489,6 +538,7 @@ declare const channelSpecSchema: z.ZodObject<{
489
538
  name: z.ZodString;
490
539
  options: z.ZodOptional<z.ZodArray<z.ZodString>>;
491
540
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
541
+ resume: z.ZodOptional<z.ZodBoolean>;
492
542
  connectors: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
493
543
  type: z.ZodLiteral<"slack">;
494
544
  name: z.ZodString;
@@ -521,6 +571,7 @@ declare const localConfigSchema: z.ZodObject<{
521
571
  name: z.ZodString;
522
572
  options: z.ZodOptional<z.ZodArray<z.ZodString>>;
523
573
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
574
+ resume: z.ZodOptional<z.ZodBoolean>;
524
575
  connectors: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
525
576
  type: z.ZodLiteral<"slack">;
526
577
  name: z.ZodString;
@@ -1326,6 +1377,8 @@ declare class Funnel {
1326
1377
  get channels(): FunnelChannels;
1327
1378
  /** Launch profiles (named presets for `fnl claude`: path + sub-agent + channel id). */
1328
1379
  get profiles(): FunnelProfiles;
1380
+ /** Per-(channel, cwd) claude session-id store. Backs `--session-id` injection on launch. */
1381
+ get sessions(): FunnelSessions;
1329
1382
  /** Reads `funnel.json` from a cwd. `fnl claude` consults it before falling back to the default profile. */
1330
1383
  get localConfig(): FunnelLocalConfig;
1331
1384
  /** Parses `.env.local` from a cwd (used by sync to back $VAR references). */
@@ -4210,4 +4263,4 @@ ${string}`;
4210
4263
  //#region lib/tui/tui.d.ts
4211
4264
  declare function launchTui(funnel: Funnel): Promise<void>;
4212
4265
  //#endregion
4213
- export { 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, 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, 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 };
4266
+ export { 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, 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 { 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;
@@ -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({
@@ -936,16 +973,19 @@ var FunnelLocalConfigSync = class {
936
973
  }
937
974
  async ensure(channel, cwd) {
938
975
  const existing = this.channels.get(channel.name);
976
+ const nextResume = channel.resume ?? true;
939
977
  if (!existing) this.channels.add({
940
978
  name: channel.name,
941
979
  options: channel.options ?? [],
942
- env: channel.env ?? {}
980
+ env: channel.env ?? {},
981
+ resume: nextResume
943
982
  });
944
983
  else {
945
984
  const nextOptions = channel.options ?? [];
946
985
  const nextEnv = channel.env ?? {};
947
986
  if (!arraysEqual(existing.options, nextOptions)) this.channels.setOptions(channel.name, nextOptions);
948
987
  if (!recordsEqual(existing.env, nextEnv)) this.channels.setEnv(channel.name, nextEnv);
988
+ if (existing.resume !== nextResume) this.channels.setResume(channel.name, nextResume);
949
989
  }
950
990
  if (channel.connectors === void 0) return {
951
991
  touched: [],
@@ -1429,6 +1469,81 @@ var FunnelProfiles = class {
1429
1469
  }
1430
1470
  };
1431
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
1432
1547
  //#region lib/engine/token-prompter/node-token-prompter.ts
1433
1548
  const STAR = "*";
1434
1549
  const CR = "\r";
@@ -3361,6 +3476,15 @@ var Funnel = class Funnel {
3361
3476
  if (!this.memos.profiles) this.memos.profiles = new FunnelProfiles({ store: this.store });
3362
3477
  return this.memos.profiles;
3363
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
+ }
3364
3488
  /** Reads `funnel.json` from a cwd. `fnl claude` consults it before falling back to the default profile. */
3365
3489
  get localConfig() {
3366
3490
  if (!this.memos.localConfig) this.memos.localConfig = new FunnelLocalConfig({ fs: this.fs });
@@ -3396,6 +3520,7 @@ var Funnel = class Funnel {
3396
3520
  channels: this.channels,
3397
3521
  mcp: this.mcp,
3398
3522
  gateway: this.gateway,
3523
+ sessions: this.sessions,
3399
3524
  fs: this.fs,
3400
3525
  process: this.process,
3401
3526
  logger: this.logger,
@@ -6930,4 +7055,4 @@ async function launchTui(funnel) {
6930
7055
  });
6931
7056
  }
6932
7057
  //#endregion
6933
- 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.19.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",