@interactive-inc/claude-funnel 0.60.1 → 0.63.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.
Files changed (88) hide show
  1. package/README.md +2 -2
  2. package/dist/bin.js +428 -761
  3. package/dist/{channels-2g_BU1N0.d.ts → channels-B8RQPrVq.d.ts} +17 -16
  4. package/dist/claude.d.ts +5 -7
  5. package/dist/claude.js +143 -36
  6. package/dist/{connector-descriptor-6SXJoszo.d.ts → connector-descriptor-ClEEbuW3.d.ts} +50 -11
  7. package/dist/connector-diagnostics-recorder-COtNEmUp.js +42 -0
  8. package/dist/connectors/discord.d.ts +31 -37
  9. package/dist/connectors/discord.js +3 -3
  10. package/dist/connectors/gh.d.ts +37 -33
  11. package/dist/connectors/gh.js +3 -3
  12. package/dist/connectors/schedule.d.ts +9 -57
  13. package/dist/connectors/schedule.js +3 -3
  14. package/dist/connectors/slack.d.ts +71 -131
  15. package/dist/connectors/slack.js +4 -3
  16. package/dist/diagnostics.d.ts +1 -1
  17. package/dist/diagnostics.js +1 -1
  18. package/dist/discord-connector-DIFkYBbi.js +250 -0
  19. package/dist/discord-connector-schema-D-bOVAKt.d.ts +22 -0
  20. package/dist/docs.js +1 -1
  21. package/dist/doctor.d.ts +1 -1
  22. package/dist/doctor.js +1 -1
  23. package/dist/{file-process-guard-C_PLxfUX.d.ts → file-process-guard-DGHxALfI.d.ts} +6 -6
  24. package/dist/{file-system-o51IsM0W.d.ts → file-system-VhwwXZbm.d.ts} +8 -0
  25. package/dist/flume-source-listener-Dim5szHG.d.ts +133 -0
  26. package/dist/{funnel-diagnostics-CSiJmPlZ.js → funnel-diagnostics-Cvk6Sk4x.js} +193 -43
  27. package/dist/{funnel-diagnostics-DpXOsCty.d.ts → funnel-diagnostics-b9ar0Ing.d.ts} +67 -5
  28. package/dist/{funnel-docs-BxXZ9Ksx.js → funnel-docs-C-ge0MuB.js} +42 -6
  29. package/dist/{funnel-doctor-CZf_0Luq.d.ts → funnel-doctor-CnRQi4kM.d.ts} +2 -2
  30. package/dist/{funnel-doctor-DiJCjHsg.js → funnel-doctor-XrI2GBH8.js} +1 -1
  31. package/dist/funnel-error-0t1MK1R6.js +75 -0
  32. package/dist/{funnel-recovery-DnLrdWO9.d.ts → funnel-recovery-CMhY8Jfk.d.ts} +1 -1
  33. package/dist/gateway/daemon.js +167 -527
  34. package/dist/gateway.d.ts +3 -3
  35. package/dist/gateway.js +3 -3
  36. package/dist/gh-connector-BUGCOEWS.js +187 -0
  37. package/dist/{gh-connector-schema-Rzwc1c1N.js → gh-connector-schema-CAqIhzGr.js} +7 -0
  38. package/dist/gh-connector-schema-DWQaB6gX.d.ts +16 -0
  39. package/dist/{index-CgY8NdMz.d.ts → index-DxRikYmu.d.ts} +37 -19
  40. package/dist/index.d.ts +182 -22
  41. package/dist/index.js +363 -173
  42. package/dist/{local-config-json-schema-JyLqOQNX.js → local-config-json-schema-DexV8vX3.js} +24 -4
  43. package/dist/local-config.d.ts +39 -2
  44. package/dist/local-config.js +53 -2
  45. package/dist/logger.js +1 -1
  46. package/dist/loopback-fetch-CVNuN3YZ.js +40 -0
  47. package/dist/{local-config-sync-Dh1Croqe.d.ts → memory-token-prompter-DP_YV9xX.d.ts} +30 -3
  48. package/dist/node-file-system-BOXIHW_Q.js +174 -0
  49. package/dist/{profiles-DSzTeKQw.js → profiles-ZHLONml4.js} +49 -49
  50. package/dist/{profiles-Cy5wXQ0L.d.ts → profiles-cVZQkM69.d.ts} +3 -3
  51. package/dist/profiles.d.ts +1 -1
  52. package/dist/profiles.js +1 -1
  53. package/dist/recovery.d.ts +1 -1
  54. package/dist/recovery.js +1 -1
  55. package/dist/resolve-connector-token-DxDG9mhf.js +22 -0
  56. package/dist/{schedule-connector-L4uzg5M8.js → schedule-connector-9k3gOIgl.js} +54 -55
  57. package/dist/schedule-connector-schema-Z0RXLgPI.d.ts +49 -0
  58. package/dist/settings-reader-BNxjsxCB.d.ts +27 -0
  59. package/dist/{settings-store-CUKSeTXC.js → settings-store-C2QdOH-t.js} +23 -4
  60. package/dist/slack-connector-BU86fIge.js +359 -0
  61. package/dist/slack-event-processor-BhCf5Wiy.d.ts +95 -0
  62. package/dist/slack-event-processor-xFDG3US0.js +176 -0
  63. package/dist/slot-fields-D-pvMgTK.js +249 -0
  64. package/dist/{memory-diagnostic-log-CI60kNfB.js → sqlite-diagnostic-log-DOTPW-tG.js} +373 -249
  65. package/dist/{yaml-render-93pX7EF7.js → yaml-render--J1_3BSA.js} +25 -21
  66. package/package.json +2 -4
  67. package/dist/discord-connector-BL36yvbL.js +0 -250
  68. package/dist/gateway-base-url-Dy4Ykuoh.js +0 -14
  69. package/dist/gh-connector-DpiixfQZ.js +0 -226
  70. package/dist/http-client-oICicjuO.d.ts +0 -18
  71. package/dist/memory-token-prompter-B4sjyaAq.d.ts +0 -57
  72. package/dist/memory-token-prompter-CZde7e6y.js +0 -61
  73. package/dist/node-file-system-Blr8pAir.js +0 -48
  74. package/dist/settings-reader-BIFB_j2f.d.ts +0 -18
  75. package/dist/slack-connector-DQIFPdBF.js +0 -484
  76. package/dist/slot-fields-CMoRpwuy.js +0 -45
  77. /package/dist/{connector-adapter-DU9Rvyec.js → connector-adapter-Dvs8N7ew.js} +0 -0
  78. /package/dist/{connector-listener-DR3aKOuK.js → connector-listener-mPGZYa8e.js} +0 -0
  79. /package/dist/{diagnostic-sql-reader-C9zR-Csp.js → diagnostic-sql-reader-oXZnWFf_.js} +0 -0
  80. /package/dist/{discord-connector-schema-B_N6IXLz.js → discord-connector-schema-B4YpWpR3.js} +0 -0
  81. /package/dist/{error-message-of-Byi4y0Uf.js → error-message-of-ColuYmAk.js} +0 -0
  82. /package/dist/{funnel-log-sqlite-sink-kqJbx2H7.js → funnel-log-sqlite-sink-DLYkY0pZ.js} +0 -0
  83. /package/dist/{funnel-recovery-BFdPjL6Z.js → funnel-recovery-DKnEutUS.js} +0 -0
  84. /package/dist/{node-http-client-lowp60Oa.js → node-http-client-u00atiKx.js} +0 -0
  85. /package/dist/{schedule-connector-schema-CfyuMCMh.js → schedule-connector-schema-DKEPZnVv.js} +0 -0
  86. /package/dist/{settings-reader-CtQ-Ix8_.js → settings-reader-9FcX3qS1.js} +0 -0
  87. /package/dist/{settings-schema-D1xcOqRu.d.ts → settings-schema-BL_c2Udm.d.ts} +0 -0
  88. /package/dist/{slack-connector-schema-C1zEf4TG.js → slack-connector-schema-Dem8to4P.js} +0 -0
@@ -1,6 +1,6 @@
1
- import { t as discordConnectorSchema } from "./discord-connector-schema-B_N6IXLz.js";
2
- import { t as ghConnectorSchema } from "./gh-connector-schema-Rzwc1c1N.js";
3
- import { t as slackConnectorSchema } from "./slack-connector-schema-C1zEf4TG.js";
1
+ import { t as discordConnectorSchema } from "./discord-connector-schema-B4YpWpR3.js";
2
+ import { t as ghConnectorSchema } from "./gh-connector-schema-CAqIhzGr.js";
3
+ import { t as slackConnectorSchema } from "./slack-connector-schema-Dem8to4P.js";
4
4
  import { join } from "node:path";
5
5
  import { z } from "zod";
6
6
  import { stderr, stdin } from "node:process";
@@ -451,6 +451,26 @@ var NodeFunnelTokenPrompter = class extends FunnelTokenPrompter {
451
451
  }
452
452
  };
453
453
  //#endregion
454
+ //#region lib/engine/token-prompter/memory-token-prompter.ts
455
+ /**
456
+ * Pre-seeded answers keyed by prompt label. Tests configure the map up front;
457
+ * unmapped labels throw so the test surfaces unexpected prompts loudly.
458
+ */
459
+ var MemoryFunnelTokenPrompter = class extends FunnelTokenPrompter {
460
+ answers;
461
+ asked = [];
462
+ constructor(props = {}) {
463
+ super();
464
+ this.answers = new Map(Object.entries(props.answers ?? {}));
465
+ }
466
+ async promptSecret(label) {
467
+ this.asked.push(label);
468
+ const answer = this.answers.get(label);
469
+ if (answer === void 0) throw new Error(`no answer seeded for prompt "${label}"`);
470
+ return answer;
471
+ }
472
+ };
473
+ //#endregion
454
474
  //#region lib/services/local-config/local-config-json-schema.ts
455
475
  /**
456
476
  * Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
@@ -466,4 +486,4 @@ const funnelJsonSchema = () => {
466
486
  };
467
487
  };
468
488
  //#endregion
469
- export { FunnelLocalConfig as a, connectorSpecSchema as c, FunnelTokenPrompter as i, localConfigSchema as l, NodeFunnelTokenPrompter as n, LOCAL_CONFIG_FILENAME as o, FunnelLocalConfigSync as r, channelSpecSchema as s, funnelJsonSchema as t, profileSpecSchema as u };
489
+ export { FunnelTokenPrompter as a, channelSpecSchema as c, profileSpecSchema as d, FunnelLocalConfigSync as i, connectorSpecSchema as l, MemoryFunnelTokenPrompter as n, FunnelLocalConfig as o, NodeFunnelTokenPrompter as r, LOCAL_CONFIG_FILENAME as s, funnelJsonSchema as t, localConfigSchema as u };
@@ -1,3 +1,40 @@
1
- import { a as FunnelLocalConfig, c as LOCAL_CONFIG_FILENAME, d as channelSpecSchema, f as connectorSpecSchema, i as FunnelTokenPrompter, l as LocalConfig, m as profileSpecSchema, n as FunnelLocalConfigSync, o as ChannelSpec, p as localConfigSchema, r as LocalConfigSyncResult, s as ConnectorSpec, t as ConnectorSyncOutcome, u as ProfileSpec } from "./local-config-sync-Dh1Croqe.js";
2
- import { i as funnelJsonSchema, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigWriter, t as MemoryFunnelTokenPrompter } from "./memory-token-prompter-B4sjyaAq.js";
1
+ import { n as FunnelFileSystem } from "./file-system-VhwwXZbm.js";
2
+ import { a as LocalConfigSyncResult, c as ChannelSpec, d as LocalConfig, f as ProfileSpec, g as profileSpecSchema, h as localConfigSchema, i as FunnelLocalConfigSync, l as ConnectorSpec, m as connectorSpecSchema, n as NodeFunnelTokenPrompter, o as FunnelTokenPrompter, p as channelSpecSchema, r as ConnectorSyncOutcome, s as FunnelLocalConfig, t as MemoryFunnelTokenPrompter, u as LOCAL_CONFIG_FILENAME } from "./memory-token-prompter-DP_YV9xX.js";
3
+
4
+ //#region lib/services/local-config/local-config-json-schema.d.ts
5
+ /**
6
+ * Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
7
+ * `$schema` references in committed `funnel.json` files so editors can give
8
+ * autocomplete and validation for channels[] (transport) and profiles[]
9
+ * (launch recipe) without anyone hand-maintaining a separate schema.
10
+ */
11
+ declare const funnelJsonSchema: () => Record<string, unknown>;
12
+ //#endregion
13
+ //#region lib/services/local-config/local-config-writer.d.ts
14
+ type Deps = {
15
+ fs: FunnelFileSystem;
16
+ };
17
+ /**
18
+ * The one path that mutates the repo-committed funnel.json, and it only ever
19
+ * inserts `id`. On first launch a repo has no `id`; funnel generates one and
20
+ * writes it back here so future launches resolve the same `~/.funnel/projects/<id>/`.
21
+ * Idempotent — a no-op once `id` is present. Kept separate from the read-only
22
+ * FunnelLocalConfig so reads stay side-effect free.
23
+ */
24
+ declare class FunnelLocalConfigWriter {
25
+ private readonly fs;
26
+ constructor(deps: Deps);
27
+ /**
28
+ * Returns the id that ends up persisted in funnel.json. If the file already
29
+ * has an id, the candidate is ignored and the persisted one wins. Otherwise
30
+ * the candidate is written and returned. Returns null when there is no
31
+ * funnel.json (the caller stays on the global ~/.funnel).
32
+ *
33
+ * The read+merge+write runs under an exclusive lock so two concurrent
34
+ * 'fnl claude' launches on the same repo cannot each persist a different
35
+ * generated id and split state across two ~/.funnel/projects/<id>/ dirs.
36
+ */
37
+ ensureId(cwd: string, candidate: string): string | null;
38
+ }
39
+ //#endregion
3
40
  export { ChannelSpec, ConnectorSpec, ConnectorSyncOutcome, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, LocalConfig, LocalConfigSyncResult, MemoryFunnelTokenPrompter, NodeFunnelTokenPrompter, ProfileSpec, channelSpecSchema, connectorSpecSchema, funnelJsonSchema, localConfigSchema, profileSpecSchema };
@@ -1,3 +1,54 @@
1
- import { a as FunnelLocalConfig, c as connectorSpecSchema, i as FunnelTokenPrompter, l as localConfigSchema, n as NodeFunnelTokenPrompter, o as LOCAL_CONFIG_FILENAME, r as FunnelLocalConfigSync, s as channelSpecSchema, t as funnelJsonSchema, u as profileSpecSchema } from "./local-config-json-schema-JyLqOQNX.js";
2
- import { n as FunnelLocalConfigWriter, t as MemoryFunnelTokenPrompter } from "./memory-token-prompter-CZde7e6y.js";
1
+ import { a as FunnelTokenPrompter, c as channelSpecSchema, d as profileSpecSchema, i as FunnelLocalConfigSync, l as connectorSpecSchema, n as MemoryFunnelTokenPrompter, o as FunnelLocalConfig, r as NodeFunnelTokenPrompter, s as LOCAL_CONFIG_FILENAME, t as funnelJsonSchema, u as localConfigSchema } from "./local-config-json-schema-DexV8vX3.js";
2
+ import { join } from "node:path";
3
+ //#region lib/services/local-config/local-config-writer.ts
4
+ const isRecord = (value) => {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ };
7
+ const withIdFirst = (config, id) => {
8
+ const ordered = {};
9
+ if (config.$schema !== void 0) ordered.$schema = config.$schema;
10
+ ordered.id = id;
11
+ for (const key of Object.keys(config)) {
12
+ if (key === "$schema" || key === "id") continue;
13
+ ordered[key] = config[key];
14
+ }
15
+ return ordered;
16
+ };
17
+ /**
18
+ * The one path that mutates the repo-committed funnel.json, and it only ever
19
+ * inserts `id`. On first launch a repo has no `id`; funnel generates one and
20
+ * writes it back here so future launches resolve the same `~/.funnel/projects/<id>/`.
21
+ * Idempotent — a no-op once `id` is present. Kept separate from the read-only
22
+ * FunnelLocalConfig so reads stay side-effect free.
23
+ */
24
+ var FunnelLocalConfigWriter = class {
25
+ fs;
26
+ constructor(deps) {
27
+ this.fs = deps.fs;
28
+ Object.freeze(this);
29
+ }
30
+ /**
31
+ * Returns the id that ends up persisted in funnel.json. If the file already
32
+ * has an id, the candidate is ignored and the persisted one wins. Otherwise
33
+ * the candidate is written and returned. Returns null when there is no
34
+ * funnel.json (the caller stays on the global ~/.funnel).
35
+ *
36
+ * The read+merge+write runs under an exclusive lock so two concurrent
37
+ * 'fnl claude' launches on the same repo cannot each persist a different
38
+ * generated id and split state across two ~/.funnel/projects/<id>/ dirs.
39
+ */
40
+ ensureId(cwd, candidate) {
41
+ const path = join(cwd, LOCAL_CONFIG_FILENAME);
42
+ if (!this.fs.existsSync(path)) return null;
43
+ return this.fs.withFileLock(`${path}.lock`, () => {
44
+ const parsed = JSON.parse(this.fs.readFileSync(path));
45
+ if (!isRecord(parsed)) return null;
46
+ if (typeof parsed.id === "string" && parsed.id !== "") return parsed.id;
47
+ const ordered = withIdFirst(parsed, candidate);
48
+ this.fs.writeFileSync(path, `${JSON.stringify(ordered, null, 2)}\n`);
49
+ return candidate;
50
+ });
51
+ }
52
+ };
53
+ //#endregion
3
54
  export { FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, MemoryFunnelTokenPrompter, NodeFunnelTokenPrompter, channelSpecSchema, connectorSpecSchema, funnelJsonSchema, localConfigSchema, profileSpecSchema };
package/dist/logger.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as FunnelLogSqliteSink } from "./funnel-log-sqlite-sink-kqJbx2H7.js";
1
+ import { t as FunnelLogSqliteSink } from "./funnel-log-sqlite-sink-DLYkY0pZ.js";
2
2
  import { dirname } from "node:path";
3
3
  import { appendFileSync, existsSync, mkdirSync, renameSync, statSync, unlinkSync } from "node:fs";
4
4
  //#region lib/logger/funnel-log.ts
@@ -0,0 +1,40 @@
1
+ //#region lib/engine/http/gateway-base-url.ts
2
+ /**
3
+ * The HTTP base URL of a gateway daemon on the loopback interface. The daemon
4
+ * always binds 127.0.0.1 for its management API (only the WS `/ws` endpoint is
5
+ * ever exposed off-box), so every in-process HTTP client — publisher, listeners
6
+ * client, MCP channel server — talks to it here. Centralizing the construction
7
+ * keeps the host/port shape in one place instead of re-spelling
8
+ * `http://127.0.0.1:${port}` at each call site.
9
+ */
10
+ function gatewayLoopbackUrl(port) {
11
+ return `http://127.0.0.1:${port}`;
12
+ }
13
+ //#endregion
14
+ //#region lib/engine/http/loopback-fetch.ts
15
+ /**
16
+ * Default ceiling on every loopback request to the gateway daemon. Five
17
+ * seconds is well above the daemon's normal /status latency (microseconds)
18
+ * but short enough that a wedged daemon does not hang the CLI / MCP / SDK
19
+ * caller for any meaningful time.
20
+ */
21
+ const DEFAULT_LOOPBACK_TIMEOUT_MS = 5e3;
22
+ /**
23
+ * Wraps `fetch` with an automatic abort signal so a wedged gateway daemon
24
+ * cannot hang the caller forever. Composes with a host-supplied
25
+ * `init.signal`: if either the timeout or the host signal aborts, the
26
+ * request is cancelled and `fetch` rejects with an AbortError.
27
+ *
28
+ * Returns the raw `Response` so callers can branch on `res.ok` / parse body
29
+ * however they like — this is the lowest-level wrapper, not a JSON helper.
30
+ */
31
+ const loopbackFetch = async (url, init = {}, timeoutMs = DEFAULT_LOOPBACK_TIMEOUT_MS) => {
32
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
33
+ const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
34
+ return fetch(url, {
35
+ ...init,
36
+ signal
37
+ });
38
+ };
39
+ //#endregion
40
+ export { gatewayLoopbackUrl as n, loopbackFetch as t };
@@ -1,5 +1,5 @@
1
- import { n as FunnelFileSystem } from "./file-system-o51IsM0W.js";
2
- import { r as FunnelChannels } from "./channels-2g_BU1N0.js";
1
+ import { n as FunnelFileSystem } from "./file-system-VhwwXZbm.js";
2
+ import { r as FunnelChannels } from "./channels-B8RQPrVq.js";
3
3
  import { z } from "zod";
4
4
 
5
5
  //#region lib/services/local-config/local-config-schema.d.ts
@@ -166,4 +166,31 @@ declare class FunnelLocalConfigSync {
166
166
  private resolveSlot;
167
167
  }
168
168
  //#endregion
169
- export { FunnelLocalConfig as a, LOCAL_CONFIG_FILENAME as c, channelSpecSchema as d, connectorSpecSchema as f, FunnelTokenPrompter as i, LocalConfig as l, profileSpecSchema as m, FunnelLocalConfigSync as n, ChannelSpec as o, localConfigSchema as p, LocalConfigSyncResult as r, ConnectorSpec as s, ConnectorSyncOutcome as t, ProfileSpec as u };
169
+ //#region lib/engine/token-prompter/node-token-prompter.d.ts
170
+ /**
171
+ * Reads a secret from stdin in raw mode. Echoes a `*` per byte so the user
172
+ * can see progress without exposing the token. Refuses to prompt when stdin
173
+ * is not a TTY — callers should surface the resulting error with a hint
174
+ * pointing at the corresponding env var or CLI command.
175
+ */
176
+ declare class NodeFunnelTokenPrompter extends FunnelTokenPrompter {
177
+ promptSecret(label: string): Promise<string>;
178
+ private readSecret;
179
+ }
180
+ //#endregion
181
+ //#region lib/engine/token-prompter/memory-token-prompter.d.ts
182
+ type Props = {
183
+ answers?: Record<string, string>;
184
+ };
185
+ /**
186
+ * Pre-seeded answers keyed by prompt label. Tests configure the map up front;
187
+ * unmapped labels throw so the test surfaces unexpected prompts loudly.
188
+ */
189
+ declare class MemoryFunnelTokenPrompter extends FunnelTokenPrompter {
190
+ private readonly answers;
191
+ readonly asked: string[];
192
+ constructor(props?: Props);
193
+ promptSecret(label: string): Promise<string>;
194
+ }
195
+ //#endregion
196
+ export { LocalConfigSyncResult as a, ChannelSpec as c, LocalConfig as d, ProfileSpec as f, profileSpecSchema as g, localConfigSchema as h, FunnelLocalConfigSync as i, ConnectorSpec as l, connectorSpecSchema as m, NodeFunnelTokenPrompter as n, FunnelTokenPrompter as o, channelSpecSchema as p, ConnectorSyncOutcome as r, FunnelLocalConfig as s, MemoryFunnelTokenPrompter as t, LOCAL_CONFIG_FILENAME as u };
@@ -0,0 +1,174 @@
1
+ import { t as FunnelFileSystem } from "./file-system-Wvzc2ePY.js";
2
+ import { basename, dirname } from "node:path";
3
+ import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
+ //#region lib/engine/fs/node-file-system.ts
5
+ const SECRET_MODE = 384;
6
+ /**
7
+ * Random suffix for the temp file used by the atomic write path. Cannot use
8
+ * crypto.randomUUID directly because Bun spec restricts where it works; a
9
+ * pid+counter pair is enough for in-process uniqueness, which is all we need
10
+ * (the temp file lives at most a few ms before rename).
11
+ */
12
+ let tempCounter = 0;
13
+ const nextTempSuffix = () => {
14
+ tempCounter = tempCounter + 1 | 0;
15
+ return `${process.pid}-${tempCounter}-${Math.floor(Math.random() * 1e9)}`;
16
+ };
17
+ var NodeFunnelFileSystem = class extends FunnelFileSystem {
18
+ constructor() {
19
+ super();
20
+ Object.freeze(this);
21
+ }
22
+ existsSync(path) {
23
+ return existsSync(path);
24
+ }
25
+ readFileSync(path) {
26
+ return readFileSync(path, "utf-8");
27
+ }
28
+ writeFileSync(path, data) {
29
+ atomicWrite(path, data, null);
30
+ }
31
+ writeSecretFileSync(path, data) {
32
+ atomicWrite(path, data, SECRET_MODE);
33
+ }
34
+ appendFileSync(path, data) {
35
+ appendFileSync(path, data);
36
+ }
37
+ unlink(path) {
38
+ try {
39
+ unlinkSync(path);
40
+ } catch (error) {
41
+ if (isErrnoCode(error, "ENOENT")) return;
42
+ throw error;
43
+ }
44
+ }
45
+ mkdirSync(path, options) {
46
+ mkdirSync(path, { recursive: options?.recursive ?? false });
47
+ }
48
+ readdirSync(path) {
49
+ return readdirSync(path);
50
+ }
51
+ statSync(path) {
52
+ const stat = statSync(path);
53
+ return {
54
+ mtimeMs: stat.mtimeMs,
55
+ mode: stat.mode & 511
56
+ };
57
+ }
58
+ withFileLock(lockPath, fn) {
59
+ const fd = acquireLock(lockPath);
60
+ try {
61
+ return fn();
62
+ } finally {
63
+ try {
64
+ closeSync(fd);
65
+ } catch {}
66
+ try {
67
+ unlinkSync(lockPath);
68
+ } catch {}
69
+ }
70
+ }
71
+ };
72
+ const LOCK_RETRY_BASE_MS = 10;
73
+ const LOCK_RETRY_MAX_MS = 100;
74
+ const LOCK_TIMEOUT_MS = 5e3;
75
+ const LOCK_STALE_AFTER_MS = 3e4;
76
+ /**
77
+ * Acquire an exclusive lock by atomically creating `lockPath` (`O_EXCL`).
78
+ * Retries with bounded backoff up to LOCK_TIMEOUT_MS. If the existing lock
79
+ * file is older than LOCK_STALE_AFTER_MS or owned by a dead pid, break it
80
+ * and try again. The pid is written to the lock file so the staleness check
81
+ * can be precise (mtime alone is fooled by clock jumps).
82
+ */
83
+ const acquireLock = (lockPath) => {
84
+ const deadline = performance.now() + LOCK_TIMEOUT_MS;
85
+ let attempt = 0;
86
+ while (true) try {
87
+ const fd = openSync(lockPath, "wx", 384);
88
+ writeFileSync(fd, String(process.pid));
89
+ return fd;
90
+ } catch (error) {
91
+ if (!isErrnoCode(error, "EEXIST")) throw error;
92
+ if (performance.now() >= deadline) throw new Error(`failed to acquire file lock ${lockPath} within ${LOCK_TIMEOUT_MS}ms`);
93
+ breakIfStale(lockPath);
94
+ sleepSyncMs(Math.min(LOCK_RETRY_MAX_MS, LOCK_RETRY_BASE_MS * 2 ** Math.min(attempt, 4)));
95
+ attempt = attempt + 1;
96
+ }
97
+ };
98
+ const breakIfStale = (lockPath) => {
99
+ let pid;
100
+ let mtimeMs;
101
+ try {
102
+ mtimeMs = statSync(lockPath).mtimeMs;
103
+ pid = Number(readFileSync(lockPath, "utf-8").trim());
104
+ } catch {
105
+ return;
106
+ }
107
+ if (Date.now() - mtimeMs > LOCK_STALE_AFTER_MS) {
108
+ try {
109
+ unlinkSync(lockPath);
110
+ } catch {}
111
+ return;
112
+ }
113
+ if (pid > 0 && !isPidAlive(pid)) try {
114
+ unlinkSync(lockPath);
115
+ } catch {}
116
+ };
117
+ const isPidAlive = (pid) => {
118
+ try {
119
+ process.kill(pid, 0);
120
+ return true;
121
+ } catch (error) {
122
+ if (isErrnoCode(error, "EPERM")) return true;
123
+ return false;
124
+ }
125
+ };
126
+ /**
127
+ * Sleep synchronously for `ms` by spinning on Atomics.wait against a private
128
+ * SharedArrayBuffer. Required because the lock acquisition path is itself
129
+ * synchronous (every settings-mutating call site is sync) and cannot await.
130
+ * The spin is bounded by LOCK_RETRY_MAX_MS so total wall time stays low.
131
+ */
132
+ const sleepSyncMs = (ms) => {
133
+ const sab = new SharedArrayBuffer(4);
134
+ const view = new Int32Array(sab);
135
+ Atomics.wait(view, 0, 0, ms);
136
+ };
137
+ /**
138
+ * Narrow `unknown` to a Node errno-typed error and check whether its `code`
139
+ * matches the expected value. Avoids `as NodeJS.ErrnoException` casts at
140
+ * each call site while still letting callers distinguish ENOENT / EACCES /
141
+ * etc. without falling back to message-string matching.
142
+ */
143
+ const isErrnoCode = (error, code) => {
144
+ if (!(error instanceof Error)) return false;
145
+ if (!("code" in error)) return false;
146
+ return error.code === code;
147
+ };
148
+ /**
149
+ * Atomic write via temp + rename. `rename(2)` is atomic on POSIX when source
150
+ * and target share a filesystem, which is guaranteed because the temp file
151
+ * lives in the same directory as the target. A failed write unlinks the
152
+ * temp file so we do not leak `.foo.json.<pid>.tmp` leftovers.
153
+ *
154
+ * `mode` controls the perm bits on both temp and final file. Pass `null` for
155
+ * the OS default (umask-derived), or `0o600` for secret-bearing files.
156
+ */
157
+ const atomicWrite = (path, data, mode) => {
158
+ const tempPath = `${dirname(path)}/.${basename(path)}.${nextTempSuffix()}.tmp`;
159
+ const writeOptions = mode === null ? void 0 : { mode };
160
+ try {
161
+ writeFileSync(tempPath, data, writeOptions);
162
+ if (mode !== null) try {
163
+ chmodSync(tempPath, mode);
164
+ } catch {}
165
+ renameSync(tempPath, path);
166
+ } catch (error) {
167
+ try {
168
+ unlinkSync(tempPath);
169
+ } catch {}
170
+ throw error;
171
+ }
172
+ };
173
+ //#endregion
174
+ export { NodeFunnelFileSystem as t };
@@ -1,4 +1,4 @@
1
- import { t as NodeFunnelFileSystem } from "./node-file-system-Blr8pAir.js";
1
+ import { t as NodeFunnelFileSystem } from "./node-file-system-BOXIHW_Q.js";
2
2
  import { join } from "node:path";
3
3
  import { homedir } from "node:os";
4
4
  //#region lib/engine/profiles/profiles.ts
@@ -43,44 +43,44 @@ var FunnelProfiles = class {
43
43
  return this.list()[0] ?? null;
44
44
  }
45
45
  add(input) {
46
- const settings = this.store.read();
47
- if (settings.profiles.some((p) => p.name === input.name)) throw new Error(`profile "${input.name}" already exists`);
48
- if (!settings.channels.some((c) => c.id === input.channelId)) throw new Error(`channel id "${input.channelId}" not found`);
49
- settings.profiles.push({
50
- id: this.idGenerator.generate(),
51
- name: input.name,
52
- path: input.path,
53
- channelId: input.channelId,
54
- options: input.options ?? [],
55
- env: input.env ?? {},
56
- resume: input.resume ?? true
46
+ this.store.update((settings) => {
47
+ if (settings.profiles.some((p) => p.name === input.name)) throw new Error(`profile "${input.name}" already exists`);
48
+ if (!settings.channels.some((c) => c.id === input.channelId)) throw new Error(`channel id "${input.channelId}" not found`);
49
+ settings.profiles.push({
50
+ id: this.idGenerator.generate(),
51
+ name: input.name,
52
+ path: input.path,
53
+ channelId: input.channelId,
54
+ options: input.options ?? [],
55
+ env: input.env ?? {},
56
+ resume: input.resume ?? true
57
+ });
57
58
  });
58
- this.store.write(settings);
59
59
  }
60
60
  remove(name) {
61
- const settings = this.store.read();
62
- const index = settings.profiles.findIndex((p) => p.name === name);
63
- if (index < 0) throw new Error(`profile "${name}" not found`);
64
- settings.profiles.splice(index, 1);
65
- this.store.write(settings);
61
+ this.store.update((settings) => {
62
+ const index = settings.profiles.findIndex((p) => p.name === name);
63
+ if (index < 0) throw new Error(`profile "${name}" not found`);
64
+ settings.profiles.splice(index, 1);
65
+ });
66
66
  }
67
67
  rename(oldName, newName) {
68
- const settings = this.store.read();
69
- const profile = settings.profiles.find((p) => p.name === oldName);
70
- if (!profile) throw new Error(`profile "${oldName}" not found`);
71
- if (settings.profiles.some((p) => p.name === newName)) throw new Error(`profile "${newName}" already exists`);
72
- profile.name = newName;
73
- this.store.write(settings);
68
+ this.store.update((settings) => {
69
+ const profile = settings.profiles.find((p) => p.name === oldName);
70
+ if (!profile) throw new Error(`profile "${oldName}" not found`);
71
+ if (settings.profiles.some((p) => p.name === newName)) throw new Error(`profile "${newName}" already exists`);
72
+ profile.name = newName;
73
+ });
74
74
  }
75
75
  asDefault(name) {
76
- const settings = this.store.read();
77
- const index = settings.profiles.findIndex((p) => p.name === name);
78
- if (index < 0) throw new Error(`profile "${name}" not found`);
79
- if (index === 0) return;
80
- const [profile] = settings.profiles.splice(index, 1);
81
- if (!profile) return;
82
- settings.profiles.unshift(profile);
83
- this.store.write(settings);
76
+ this.store.update((settings) => {
77
+ const index = settings.profiles.findIndex((p) => p.name === name);
78
+ if (index < 0) throw new Error(`profile "${name}" not found`);
79
+ if (index === 0) return;
80
+ const [profile] = settings.profiles.splice(index, 1);
81
+ if (!profile) return;
82
+ settings.profiles.unshift(profile);
83
+ });
84
84
  }
85
85
  hasChannelRef(channelId) {
86
86
  return this.store.read().profiles.some((p) => p.channelId === channelId);
@@ -91,11 +91,11 @@ var FunnelProfiles = class {
91
91
  }
92
92
  /** Records the claude session id this profile launched, overwriting any prior one. */
93
93
  setSessionId(id, sessionId) {
94
- const settings = this.store.read();
95
- const profile = settings.profiles.find((p) => p.id === id);
96
- if (!profile) throw new Error(`profile id "${id}" not found`);
97
- profile.sessionId = sessionId;
98
- this.store.write(settings);
94
+ this.store.update((settings) => {
95
+ const profile = settings.profiles.find((p) => p.id === id);
96
+ if (!profile) throw new Error(`profile id "${id}" not found`);
97
+ profile.sessionId = sessionId;
98
+ });
99
99
  }
100
100
  /**
101
101
  * Mirrors claude's session storage path
@@ -111,18 +111,18 @@ var FunnelProfiles = class {
111
111
  return this.fs.readFileSync(path).trim().length > 0;
112
112
  }
113
113
  update(name, fields) {
114
- const settings = this.store.read();
115
- const profile = settings.profiles.find((p) => p.name === name);
116
- if (!profile) throw new Error(`profile "${name}" not found`);
117
- if (fields.channelId !== void 0) {
118
- if (!settings.channels.some((c) => c.id === fields.channelId)) throw new Error(`channel id "${fields.channelId}" not found`);
119
- profile.channelId = fields.channelId;
120
- }
121
- if (fields.path !== void 0) profile.path = fields.path;
122
- if (fields.options !== void 0) profile.options = fields.options;
123
- if (fields.env !== void 0) profile.env = fields.env;
124
- if (fields.resume !== void 0) profile.resume = fields.resume;
125
- this.store.write(settings);
114
+ this.store.update((settings) => {
115
+ const profile = settings.profiles.find((p) => p.name === name);
116
+ if (!profile) throw new Error(`profile "${name}" not found`);
117
+ if (fields.channelId !== void 0) {
118
+ if (!settings.channels.some((c) => c.id === fields.channelId)) throw new Error(`channel id "${fields.channelId}" not found`);
119
+ profile.channelId = fields.channelId;
120
+ }
121
+ if (fields.path !== void 0) profile.path = fields.path;
122
+ if (fields.options !== void 0) profile.options = fields.options;
123
+ if (fields.env !== void 0) profile.env = fields.env;
124
+ if (fields.resume !== void 0) profile.resume = fields.resume;
125
+ });
126
126
  }
127
127
  };
128
128
  //#endregion
@@ -1,6 +1,6 @@
1
- import { r as ProfileConfig } from "./settings-schema-D1xcOqRu.js";
2
- import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-BIFB_j2f.js";
3
- import { n as FunnelFileSystem } from "./file-system-o51IsM0W.js";
1
+ import { r as ProfileConfig } from "./settings-schema-BL_c2Udm.js";
2
+ import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-BNxjsxCB.js";
3
+ import { n as FunnelFileSystem } from "./file-system-VhwwXZbm.js";
4
4
 
5
5
  //#region lib/engine/profiles/profiles.d.ts
6
6
  type Deps = {
@@ -1,2 +1,2 @@
1
- import { t as FunnelProfiles } from "./profiles-Cy5wXQ0L.js";
1
+ import { t as FunnelProfiles } from "./profiles-cVZQkM69.js";
2
2
  export { FunnelProfiles };
package/dist/profiles.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as FunnelProfiles } from "./profiles-DSzTeKQw.js";
1
+ import { t as FunnelProfiles } from "./profiles-ZHLONml4.js";
2
2
  export { FunnelProfiles };
@@ -1,2 +1,2 @@
1
- import { a as RecoveryListenerControl, i as RecoveryGatewayControl, n as RecoveryAction, o as RecoveryResult, r as RecoveryChannelSource, t as FunnelRecovery } from "./funnel-recovery-DnLrdWO9.js";
1
+ import { a as RecoveryListenerControl, i as RecoveryGatewayControl, n as RecoveryAction, o as RecoveryResult, r as RecoveryChannelSource, t as FunnelRecovery } from "./funnel-recovery-CMhY8Jfk.js";
2
2
  export { FunnelRecovery, RecoveryAction, RecoveryChannelSource, RecoveryGatewayControl, RecoveryListenerControl, RecoveryResult };
package/dist/recovery.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as FunnelRecovery } from "./funnel-recovery-BFdPjL6Z.js";
1
+ import { t as FunnelRecovery } from "./funnel-recovery-DKnEutUS.js";
2
2
  export { FunnelRecovery };
@@ -0,0 +1,22 @@
1
+ //#region lib/engine/connectors/resolve-connector-token.ts
2
+ /**
3
+ * Resolves a connector token from either a literal value or the name of an env
4
+ * var. A connector config carries one or the other per slot (see
5
+ * slack-connector-schema): literals are inlined into settings.json, references
6
+ * keep the secret in `process.env` and out of settings.json.
7
+ *
8
+ * Errors loudly when neither yields a value — a misconfigured connector should
9
+ * fail at listener start, not connect with an empty token and silently never
10
+ * receive events.
11
+ */
12
+ const resolveConnectorToken = (props) => {
13
+ if (props.literal !== void 0 && props.literal !== "") return props.literal;
14
+ if (props.envVar !== void 0 && props.envVar !== "") {
15
+ const fromEnv = props.env[props.envVar];
16
+ if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
17
+ throw new Error(`${props.label} references env var "${props.envVar}" but it is not set in the environment`);
18
+ }
19
+ throw new Error(`${props.label} has neither a literal token nor an env var reference`);
20
+ };
21
+ //#endregion
22
+ export { resolveConnectorToken as t };