@interactive-inc/claude-funnel 0.21.1 → 0.22.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
@@ -1,12 +1,12 @@
1
- import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, t as discordConnectorSchema } from "./discord-connector-schema-ygf5Df-2.js";
2
- import { n as FunnelLogger, r as FunnelConnectorListener, t as NodeFunnelLogger } from "./node-logger-DQz_BGOD.js";
3
- import { a as FunnelProcessRunner, i as NodeFunnelProcessRunner, n as FunnelGhListener, r as FunnelGhAdapter, t as ghConnectorSchema } from "./gh-connector-schema-CD5HIkrd.js";
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-BM9xshol.js";
1
+ import { i as FunnelDiscordAdapter, n as FunnelDiscordListener, t as discordConnectorSchema } from "./discord-connector-schema-CR8RJ08_.js";
2
+ import { i as FunnelConnectorListener, n as funnelTmpDir, r as FunnelLogger, t as NodeFunnelLogger } from "./node-logger-B97ZiGwj.js";
3
+ import { a as FunnelProcessRunner, i as NodeFunnelProcessRunner, n as FunnelGhListener, r as FunnelGhAdapter, t as ghConnectorSchema } from "./gh-connector-schema-CAC24s0r.js";
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";
6
6
  import { dirname, join, resolve } from "node:path";
7
7
  import { existsSync, mkdirSync, readFileSync } from "node:fs";
8
- import { z } from "zod";
9
8
  import { homedir } from "node:os";
9
+ import { z } from "zod";
10
10
  import { stderr, stdin } from "node:process";
11
11
  import { fileURLToPath } from "node:url";
12
12
  import { timingSafeEqual } from "node:crypto";
@@ -597,8 +597,8 @@ var FunnelClaude = class {
597
597
  this.writePidFile(options.profileName);
598
598
  this.installCleanup(options.profileName);
599
599
  }
600
- const sessionId = channel.resume ? this.resolveSessionId(channel.id, cwd, options.userArgs ?? []) : null;
601
- const claudeArgs = this.buildArgs(channel.options, options.userArgs ?? [], cwd, sessionId);
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,
@@ -647,37 +647,37 @@ var FunnelClaude = class {
647
647
  globalThis.process.once("exit", () => this.removePidFile(profileName));
648
648
  }
649
649
  isProcessAlive(pid) {
650
- const result = this.process.runSync([
651
- "ps",
652
- "-p",
653
- String(pid),
654
- "-o",
655
- "state="
656
- ]);
657
- if (result.exitCode !== 0) return false;
658
- const state = result.stdout.trim();
659
- if (!state) return false;
660
- return !state.startsWith("Z");
650
+ return this.process.isAlive(pid);
661
651
  }
662
- buildArgs(channelOptions, userArgs, cwd, sessionId) {
652
+ buildArgs(channelOptions, userArgs, cwd, session) {
663
653
  const result = [...channelOptions, ...userArgs];
664
- if (sessionId !== null) result.push("--session-id", sessionId);
654
+ if (session !== null) if (session.mode === "resume") result.push("--resume", session.id);
655
+ else result.push("--session-id", session.id);
665
656
  const mcpName = this.mcp.findInstalledName(cwd);
666
657
  if (mcpName && !result.includes("--dangerously-load-development-channels") && !result.includes("--channels")) result.push("--dangerously-load-development-channels", `server:${mcpName}`);
667
658
  return result;
668
659
  }
669
660
  /**
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.
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.
673
665
  */
674
- resolveSessionId(channelId, cwd, userArgs) {
666
+ resolveSession(channelId, cwd, userArgs) {
675
667
  for (const arg of userArgs) {
676
668
  if (arg === "-c" || arg === "--continue") return null;
677
669
  if (arg === "--resume" || arg.startsWith("--resume=")) return null;
678
670
  if (arg === "--session-id" || arg.startsWith("--session-id=")) return null;
679
671
  }
680
- return this.sessions.getOrCreate(channelId, cwd);
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
+ };
681
681
  }
682
682
  buildEnv(channelId, channelEnv) {
683
683
  const env = {};
@@ -1332,6 +1332,8 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
1332
1332
  killed = [];
1333
1333
  handler = () => empty;
1334
1334
  syncHandler = () => empty;
1335
+ aliveStub = null;
1336
+ listStub = null;
1335
1337
  on(handler) {
1336
1338
  this.handler = handler;
1337
1339
  return this;
@@ -1340,6 +1342,14 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
1340
1342
  this.syncHandler = handler;
1341
1343
  return this;
1342
1344
  }
1345
+ onIsAlive(stub) {
1346
+ this.aliveStub = stub;
1347
+ return this;
1348
+ }
1349
+ onListProcessesContaining(stub) {
1350
+ this.listStub = stub;
1351
+ return this;
1352
+ }
1343
1353
  async run(command, options = {}) {
1344
1354
  this.calls.push({
1345
1355
  kind: "run",
@@ -1391,6 +1401,24 @@ var MemoryFunnelProcessRunner = class extends FunnelProcessRunner {
1391
1401
  signal
1392
1402
  });
1393
1403
  }
1404
+ isAlive(pid) {
1405
+ if (this.aliveStub) return this.aliveStub(pid);
1406
+ const result = this.syncHandler([
1407
+ "ps",
1408
+ "-p",
1409
+ String(pid),
1410
+ "-o",
1411
+ "state="
1412
+ ]);
1413
+ if ((result.exitCode ?? 0) !== 0) return false;
1414
+ const state = (result.stdout ?? "").trim();
1415
+ if (!state) return false;
1416
+ return !state.startsWith("Z");
1417
+ }
1418
+ listProcessesContaining(marker) {
1419
+ if (this.listStub) return this.listStub(marker);
1420
+ return [];
1421
+ }
1394
1422
  };
1395
1423
  //#endregion
1396
1424
  //#region lib/engine/profiles/profiles.ts
@@ -1474,10 +1502,15 @@ const sessionsMapSchema = z.record(z.string(), z.string());
1474
1502
  /**
1475
1503
  * Per-channel persistent Claude Code session IDs, keyed by the cwd the
1476
1504
  * 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.
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.
1481
1514
  *
1482
1515
  * Storage lives under `<dir>/channels/<channel-id>/sessions.json` (channel
1483
1516
  * id, not name, so renames don't lose history). The file is a flat
@@ -1493,20 +1526,18 @@ var FunnelSessions = class {
1493
1526
  this.dir = deps.dir;
1494
1527
  Object.freeze(this);
1495
1528
  }
1496
- /** Returns the existing session id for (channelId, cwd) or generates and persists a new one. */
1497
- getOrCreate(channelId, cwd) {
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) {
1498
1535
  const map = this.readMap(channelId);
1499
- const existing = map[cwd];
1500
- if (existing) return existing;
1501
1536
  const sessionId = this.idGenerator.generate();
1502
1537
  map[cwd] = sessionId;
1503
1538
  this.writeMap(channelId, map);
1504
1539
  return sessionId;
1505
1540
  }
1506
- /** Returns the existing session id for (channelId, cwd) or null. */
1507
- get(channelId, cwd) {
1508
- return this.readMap(channelId)[cwd] ?? null;
1509
- }
1510
1541
  /** Drops the recorded session id for (channelId, cwd). No-op if absent. */
1511
1542
  clear(channelId, cwd) {
1512
1543
  const map = this.readMap(channelId);
@@ -1749,7 +1780,6 @@ const resolveDaemonScript = () => {
1749
1780
  //#endregion
1750
1781
  //#region lib/gateway/gateway.ts
1751
1782
  const DEFAULT_PORT$1 = 9742;
1752
- const DEFAULT_TMP_DIR$1 = "/tmp/funnel";
1753
1783
  const STARTUP_TIMEOUT_MS = 5e3;
1754
1784
  const SIGTERM_TIMEOUT_MS = 2e3;
1755
1785
  const POLL_INTERVAL_MS$1 = 100;
@@ -1771,7 +1801,6 @@ var FunnelGateway = class {
1771
1801
  clock;
1772
1802
  dir;
1773
1803
  pidFile;
1774
- logDir;
1775
1804
  gatewayLog;
1776
1805
  tmpDir;
1777
1806
  port;
@@ -1781,9 +1810,8 @@ var FunnelGateway = class {
1781
1810
  this.fs = deps.fs ?? defaultFs$1;
1782
1811
  this.clock = deps.clock ?? defaultClock;
1783
1812
  this.dir = deps.dir ?? FUNNEL_DIR;
1784
- this.tmpDir = deps.tmpDir ?? DEFAULT_TMP_DIR$1;
1813
+ this.tmpDir = deps.tmpDir ?? funnelTmpDir();
1785
1814
  this.pidFile = join(this.dir, "gateway.pid");
1786
- this.logDir = join(this.tmpDir, "events");
1787
1815
  this.gatewayLog = join(this.tmpDir, "gateway.log");
1788
1816
  this.port = deps.port ?? DEFAULT_PORT$1;
1789
1817
  this.sleep = deps.sleep ?? defaultSleep$1;
@@ -1808,11 +1836,10 @@ var FunnelGateway = class {
1808
1836
  this.fs.mkdirSync(this.tmpDir, { recursive: true });
1809
1837
  const gatewayScript = resolveDaemonScript();
1810
1838
  const command = this.buildStartCommand(gatewayScript, options);
1811
- this.process.detach([
1812
- "bash",
1813
- "-c",
1814
- command
1815
- ]);
1839
+ this.process.detach(command, {
1840
+ stdoutFile: this.gatewayLog,
1841
+ stderrFile: this.gatewayLog
1842
+ });
1816
1843
  const deadline = this.clock.millis() + STARTUP_TIMEOUT_MS;
1817
1844
  while (this.clock.millis() < deadline) {
1818
1845
  if (this.isRunning()) return true;
@@ -1821,7 +1848,19 @@ var FunnelGateway = class {
1821
1848
  return this.isRunning();
1822
1849
  }
1823
1850
  buildStartCommand(gatewayScript, options = {}) {
1824
- return `nohup ${options.caffeinate !== false && globalThis.process.platform === "darwin" ? "caffeinate -i " : ""}bun ${gatewayScript} ${`funnel-gateway[${this.dir}]`} >> ${this.gatewayLog} 2>&1 &`;
1851
+ const tag = `funnel-gateway[${this.dir}]`;
1852
+ if (options.caffeinate !== false && globalThis.process.platform === "darwin") return [
1853
+ "caffeinate",
1854
+ "-is",
1855
+ "bun",
1856
+ gatewayScript,
1857
+ tag
1858
+ ];
1859
+ return [
1860
+ "bun",
1861
+ gatewayScript,
1862
+ tag
1863
+ ];
1825
1864
  }
1826
1865
  async stop() {
1827
1866
  const pid = this.readPid();
@@ -1873,9 +1912,6 @@ var FunnelGateway = class {
1873
1912
  started
1874
1913
  };
1875
1914
  }
1876
- getLogDir() {
1877
- return this.logDir;
1878
- }
1879
1915
  getGatewayLog() {
1880
1916
  return this.gatewayLog;
1881
1917
  }
@@ -1897,17 +1933,7 @@ var FunnelGateway = class {
1897
1933
  this.fs.unlink(this.pidFile);
1898
1934
  }
1899
1935
  isProcessAlive(pid) {
1900
- const result = this.process.runSync([
1901
- "ps",
1902
- "-p",
1903
- String(pid),
1904
- "-o",
1905
- "state="
1906
- ]);
1907
- if (result.exitCode !== 0) return false;
1908
- const state = result.stdout.trim();
1909
- if (!state) return false;
1910
- return !state.startsWith("Z");
1936
+ return this.process.isAlive(pid);
1911
1937
  }
1912
1938
  };
1913
1939
  //#endregion
@@ -2728,36 +2754,24 @@ const titleFor = (dir) => `funnel-gateway[${dir}]`;
2728
2754
  * which is the only situation that causes a real conflict (duplicate Slack
2729
2755
  * Socket Mode connections with the same tokens). Daemons rooted at a
2730
2756
  * different `~/.funnel/` are left alone — they hold different tokens and
2731
- * speak to different Slack apps. The daemon advertises its dir via
2732
- * `process.title = "funnel-gateway[<dir>]"`, which this routine matches.
2757
+ * speak to different Slack apps. The daemon advertises its dir via the
2758
+ * `funnel-gateway[<dir>]` marker appended to argv (also assigned to
2759
+ * `process.title` on POSIX). `FunnelProcessRunner.listProcessesContaining`
2760
+ * absorbs the POSIX/Windows enumeration difference behind the marker match.
2733
2761
  */
2734
2762
  const killCompetingSlackGateways = async (props) => {
2735
2763
  const runner = props.process ?? defaultProcess;
2736
2764
  const logger = props.logger ?? defaultLogger$1;
2737
- const result = await runner.run([
2738
- "ps",
2739
- "-e",
2740
- "-o",
2741
- "pid=,args="
2742
- ]);
2743
- if (result.exitCode !== 0) return [];
2744
2765
  const expectedTitle = titleFor(props.dir);
2766
+ const snapshots = runner.listProcessesContaining(expectedTitle);
2745
2767
  const killed = [];
2746
- for (const raw of result.stdout.split("\n")) {
2747
- const line = raw.trim();
2748
- if (!line) continue;
2749
- const match = /^(\d+)\s+(.+)$/.exec(line);
2750
- if (!match) continue;
2751
- const pid = Number(match[1]);
2752
- const args = match[2];
2753
- if (!Number.isInteger(pid) || pid <= 0) continue;
2754
- if (pid === props.selfPid) continue;
2755
- if (!args.includes(expectedTitle)) continue;
2756
- runner.kill(pid, "SIGTERM");
2757
- killed.push(pid);
2768
+ for (const snapshot of snapshots) {
2769
+ if (snapshot.pid === props.selfPid) continue;
2770
+ runner.kill(snapshot.pid, "SIGTERM");
2771
+ killed.push(snapshot.pid);
2758
2772
  logger.info("killed competing Slack gateway process", {
2759
- pid,
2760
- args: args.slice(0, 160)
2773
+ pid: snapshot.pid,
2774
+ args: snapshot.command.slice(0, 160)
2761
2775
  });
2762
2776
  }
2763
2777
  return killed;
@@ -2923,8 +2937,7 @@ const gatewayRoutes = factory$1.createApp().get("/health", ...healthHandler).get
2923
2937
  //#endregion
2924
2938
  //#region lib/gateway/gateway-server.ts
2925
2939
  const DEFAULT_PORT = 9742;
2926
- const DEFAULT_LOG_DIR = "/tmp/funnel/events";
2927
- const DB_FILENAME = "events.db";
2940
+ const defaultDbPath = () => join(funnelTmpDir(), "events.db");
2928
2941
  const defaultLogger = new NodeFunnelLogger();
2929
2942
  /**
2930
2943
  * In-process gateway: runs `Bun.serve` (HTTP + WebSocket /ws), boots connector
@@ -2940,7 +2953,7 @@ var FunnelGatewayServer = class {
2940
2953
  channels;
2941
2954
  settings;
2942
2955
  port;
2943
- logDir;
2956
+ dbPath;
2944
2957
  process;
2945
2958
  logger;
2946
2959
  selfPid;
@@ -2958,7 +2971,7 @@ var FunnelGatewayServer = class {
2958
2971
  this.channels = deps.channels;
2959
2972
  this.settings = deps.settings;
2960
2973
  this.port = deps.port ?? DEFAULT_PORT;
2961
- this.logDir = deps.logDir ?? DEFAULT_LOG_DIR;
2974
+ this.dbPath = deps.dbPath ?? defaultDbPath();
2962
2975
  this.process = deps.process;
2963
2976
  this.logger = deps.logger ?? defaultLogger;
2964
2977
  this.selfPid = deps.selfPid ?? globalThis.process.pid;
@@ -2968,9 +2981,10 @@ var FunnelGatewayServer = class {
2968
2981
  this.extraRoutes = deps.extraRoutes ?? null;
2969
2982
  const clock = deps.clock;
2970
2983
  this.nowMs = clock ? () => clock.millis() : () => Date.now();
2971
- if (!existsSync(this.logDir)) mkdirSync(this.logDir, { recursive: true });
2984
+ const dbDir = dirname(this.dbPath);
2985
+ if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
2972
2986
  this.eventStore = new FunnelEventStore({
2973
- path: join(this.logDir, DB_FILENAME),
2987
+ path: this.dbPath,
2974
2988
  now: this.nowMs
2975
2989
  });
2976
2990
  this.broadcaster = new FunnelBroadcaster({
@@ -3174,7 +3188,7 @@ var FunnelGatewayServer = class {
3174
3188
  channel: entry.channelName,
3175
3189
  connector: entry.name
3176
3190
  });
3177
- this.logger.info(`event store: ${join(this.logDir, DB_FILENAME)}`);
3191
+ this.logger.info(`event store: ${this.dbPath}`);
3178
3192
  this.logger.info("funnel gateway running");
3179
3193
  }
3180
3194
  /**
@@ -3361,7 +3375,6 @@ var FunnelListenersClient = class {
3361
3375
  };
3362
3376
  //#endregion
3363
3377
  //#region lib/funnel.ts
3364
- const DEFAULT_TMP_DIR = "/tmp/funnel";
3365
3378
  const SANDBOX_DIR = "/sandbox/.funnel";
3366
3379
  const SANDBOX_TMP_DIR = "/sandbox/tmp";
3367
3380
  /**
@@ -3411,7 +3424,7 @@ var Funnel = class Funnel {
3411
3424
  const dir = this.props.dir ?? FUNNEL_DIR;
3412
3425
  return {
3413
3426
  dir,
3414
- tmpDir: this.props.tmpDir ?? DEFAULT_TMP_DIR,
3427
+ tmpDir: this.props.tmpDir ?? funnelTmpDir(),
3415
3428
  settings: join(dir, "settings.json")
3416
3429
  };
3417
3430
  }
@@ -3591,7 +3604,7 @@ var Funnel = class Funnel {
3591
3604
  channels: this.channels,
3592
3605
  settings: this.store,
3593
3606
  port: options.port,
3594
- logDir: options.logDir,
3607
+ dbPath: options.dbPath,
3595
3608
  process: this.process,
3596
3609
  clock: this.clock,
3597
3610
  logger: this.logger,
@@ -4445,13 +4458,13 @@ usage: funnel gateway logs [-n <N>]
4445
4458
  options:
4446
4459
  -n <N> number of trailing lines to show (default: 20)
4447
4460
 
4448
- Tails /tmp/funnel/funnel.log (the daemon's diagnostic stream — gateway
4461
+ Tails ${join(funnelTmpDir(), "funnel.log")} (the daemon's diagnostic stream — gateway
4449
4462
  lifecycle, channel connect/disconnect, listener boot). Exit with SIGINT.
4450
4463
  Output is formatted as YAML.
4451
4464
 
4452
4465
  Domain events fanned out to WebSocket clients live in the SQLite event
4453
- store (<logDir>/events.db); they are not shown here. Subscribe via the
4454
- WS endpoint or query the store directly.
4466
+ store (${join(funnelTmpDir(), "events.db")}); they are not shown here. Subscribe via
4467
+ the WS endpoint or query the store directly.
4455
4468
 
4456
4469
  examples:
4457
4470
  funnel gateway logs
@@ -4525,7 +4538,7 @@ const gatewayRestartHandler = factory.createHandlers(zValidator$1("query", z.obj
4525
4538
  usage: funnel gateway restart [--no-caffeine]
4526
4539
 
4527
4540
  Stops the running process then starts it again in background.
4528
- On macOS wraps with caffeinate -i by default. Use --no-caffeine to disable.
4541
+ On macOS wraps with caffeinate -is by default. Use --no-caffeine to disable.
4529
4542
 
4530
4543
  examples:
4531
4544
  funnel gateway restart
@@ -4544,7 +4557,7 @@ const gatewayRunHandler = factory.createHandlers(zValidator$1("query", z.object(
4544
4557
  usage: funnel gateway run [--no-caffeine]
4545
4558
 
4546
4559
  For developers. The process is tied to the current terminal and exits on SIGINT / SIGTERM.
4547
- On macOS wraps with caffeinate -i by default. Use --no-caffeine to disable.
4560
+ On macOS wraps with caffeinate -is by default. Use --no-caffeine to disable.
4548
4561
 
4549
4562
  For normal usage prefer funnel gateway start.
4550
4563
 
@@ -4556,28 +4569,31 @@ examples:
4556
4569
  const gatewayScript = resolveDaemonScript();
4557
4570
  const command = query["no-caffeine"] !== "true" && process.platform === "darwin" ? [
4558
4571
  "caffeinate",
4559
- "-i",
4572
+ "-is",
4560
4573
  "bun",
4561
4574
  gatewayScript
4562
4575
  ] : ["bun", gatewayScript];
4563
4576
  const exitCode = await funnel.process.attach(command);
4564
4577
  process.exit(exitCode);
4565
4578
  });
4566
- const gatewayStartHandler = factory.createHandlers(zValidator$1("query", z.object({ "no-caffeine": z.string().optional() }), `funnel gateway start — start the gateway in background
4579
+ //#endregion
4580
+ //#region lib/cli/routes/gateway.start.ts
4581
+ const startHelp = `funnel gateway start — start the gateway in background
4567
4582
 
4568
4583
  usage: funnel gateway start [--no-caffeine]
4569
4584
 
4570
- Daemonized with nohup, so it keeps running after the terminal is closed.
4571
- On macOS wraps the process with caffeinate -i by default to prevent idle sleep.
4585
+ Spawned as a detached background process so it keeps running after the terminal is closed.
4586
+ On macOS wraps the process with caffeinate -is by default to prevent idle and system sleep.
4572
4587
  Use --no-caffeine to disable caffeinate.
4573
4588
 
4574
4589
  port: 9742 (override via FUNNEL_PORT)
4575
4590
  pid: ~/.funnel/gateway.pid
4576
- log: /tmp/funnel/gateway.log
4591
+ log: ${join(funnelTmpDir(), "gateway.log")}
4577
4592
 
4578
4593
  examples:
4579
4594
  funnel gateway start
4580
- funnel gateway start --no-caffeine`), async (c) => {
4595
+ funnel gateway start --no-caffeine`;
4596
+ const gatewayStartHandler = factory.createHandlers(zValidator$1("query", z.object({ "no-caffeine": z.string().optional() }), startHelp), async (c) => {
4581
4597
  const query = c.req.valid("query");
4582
4598
  const funnel = c.var.funnel;
4583
4599
  if (funnel.gateway.isRunning()) {
@@ -20,7 +20,7 @@ declare abstract class FunnelConnectorListener {
20
20
  //#region lib/engine/logger/logger.d.ts
21
21
  /**
22
22
  * Structured logger with three levels and an optional log-file path.
23
- * Defaults to NodeFunnelLogger (appends to /tmp/funnel/funnel.log);
23
+ * Defaults to NodeFunnelLogger (appends to `<os.tmpdir()>/funnel/funnel.log`);
24
24
  * MemoryFunnelLogger captures entries in memory and NoopFunnelLogger silences output.
25
25
  */
26
26
  declare abstract class FunnelLogger {
@@ -1,5 +1,6 @@
1
1
  import { dirname, join } from "node:path";
2
2
  import { appendFileSync, mkdirSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
3
4
  //#region lib/connectors/connector-listener.ts
4
5
  /**
5
6
  * Long-lived event source for one connector.
@@ -21,19 +22,31 @@ var FunnelConnectorListener = class {
21
22
  //#region lib/engine/logger/logger.ts
22
23
  /**
23
24
  * Structured logger with three levels and an optional log-file path.
24
- * Defaults to NodeFunnelLogger (appends to /tmp/funnel/funnel.log);
25
+ * Defaults to NodeFunnelLogger (appends to `<os.tmpdir()>/funnel/funnel.log`);
25
26
  * MemoryFunnelLogger captures entries in memory and NoopFunnelLogger silences output.
26
27
  */
27
28
  var FunnelLogger = class {};
28
29
  //#endregion
30
+ //#region lib/engine/settings/tmp-dir.ts
31
+ /**
32
+ * Resolves the funnel temp/log root for the current OS. Defaults to
33
+ * `<os.tmpdir()>/funnel` so Windows lands under `%TEMP%\funnel` and POSIX
34
+ * lands under `/tmp/funnel`. Callers may override via `FUNNEL_TMP_DIR`.
35
+ */
36
+ function funnelTmpDir() {
37
+ const override = process.env.FUNNEL_TMP_DIR;
38
+ if (override && override.length > 0) return override;
39
+ return join(tmpdir(), "funnel");
40
+ }
41
+ //#endregion
29
42
  //#region lib/engine/logger/node-logger.ts
30
- const DEFAULT_LOG_FILE = join("/tmp/funnel", "funnel.log");
43
+ const defaultLogFile = () => join(funnelTmpDir(), "funnel.log");
31
44
  var NodeFunnelLogger = class extends FunnelLogger {
32
45
  file;
33
46
  now;
34
47
  constructor(props = {}) {
35
48
  super();
36
- this.file = props.file ?? DEFAULT_LOG_FILE;
49
+ this.file = props.file ?? defaultLogFile();
37
50
  this.now = props.now ?? (() => /* @__PURE__ */ new Date());
38
51
  Object.freeze(this);
39
52
  }
@@ -58,4 +71,4 @@ var NodeFunnelLogger = class extends FunnelLogger {
58
71
  }
59
72
  };
60
73
  //#endregion
61
- export { FunnelLogger as n, FunnelConnectorListener as r, NodeFunnelLogger as t };
74
+ export { FunnelConnectorListener as i, funnelTmpDir as n, FunnelLogger as r, NodeFunnelLogger as t };
@@ -1,4 +1,4 @@
1
- import { r as FunnelConnectorListener, t as NodeFunnelLogger } from "./node-logger-DQz_BGOD.js";
1
+ import { i as FunnelConnectorListener, t as NodeFunnelLogger } from "./node-logger-B97ZiGwj.js";
2
2
  import { dirname } from "node:path";
3
3
  import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { z } from "zod";
@@ -1,4 +1,4 @@
1
- import { n as FunnelConnectorListener, r as NotifyFn, t as FunnelLogger } from "./logger-CTlXs7z4.js";
1
+ import { n as FunnelConnectorListener, r as NotifyFn, t as FunnelLogger } from "./logger-B3aXsVcX.js";
2
2
  import { z } from "zod";
3
3
 
4
4
  //#region lib/connectors/schedule-connector-schema.d.ts
@@ -1,5 +1,5 @@
1
1
  import { t as FunnelConnectorAdapter } from "./connector-adapter-D5Utumgz.js";
2
- import { r as FunnelConnectorListener, t as NodeFunnelLogger } from "./node-logger-DQz_BGOD.js";
2
+ import { i as FunnelConnectorListener, t as NodeFunnelLogger } from "./node-logger-B97ZiGwj.js";
3
3
  import { z } from "zod";
4
4
  import { WebClient } from "@slack/web-api";
5
5
  import { App, LogLevel } from "@slack/bolt";
@@ -1,4 +1,4 @@
1
- import { n as FunnelConnectorListener, r as NotifyFn, t as FunnelLogger } from "./logger-CTlXs7z4.js";
1
+ import { n as FunnelConnectorListener, r as NotifyFn, t as FunnelLogger } from "./logger-B3aXsVcX.js";
2
2
  import { z } from "zod";
3
3
  import { App } from "@slack/bolt";
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interactive-inc/claude-funnel",
3
- "version": "0.21.1",
3
+ "version": "0.22.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",
@@ -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
  }