@interactive-inc/claude-funnel 0.66.0 → 0.67.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 +8 -4
  2. package/dist/bin.js +275 -372
  3. package/dist/channel.d.ts +76 -52
  4. package/dist/channel.js +82 -47
  5. package/dist/{channels-DR90RmUP.d.ts → channels-BBA3GTAa.d.ts} +5 -6
  6. package/dist/claude.d.ts +11 -8
  7. package/dist/claude.js +31 -15
  8. package/dist/{connector-descriptor-CmyHlEiT.d.ts → connector-descriptor-F75ZI8Fh.d.ts} +26 -15
  9. package/dist/{connector-diagnostics-recorder-COtNEmUp.js → connector-diagnostics-recorder-BmOijU1p.js} +1 -0
  10. package/dist/connectors/discord.d.ts +10 -8
  11. package/dist/connectors/discord.js +2 -2
  12. package/dist/connectors/gh.d.ts +6 -6
  13. package/dist/connectors/gh.js +2 -2
  14. package/dist/connectors/schedule.d.ts +10 -8
  15. package/dist/connectors/schedule.js +2 -2
  16. package/dist/connectors/slack.d.ts +14 -10
  17. package/dist/connectors/slack.js +3 -3
  18. package/dist/diagnostics.d.ts +1 -1
  19. package/dist/diagnostics.js +1 -1
  20. package/dist/{discord-connector-DXARcO6w.js → discord-connector-CdjwPoty.js} +6 -6
  21. package/dist/{discord-connector-schema-D1dyxhXk.d.ts → discord-connector-schema-BaBZYHxK.d.ts} +0 -1
  22. package/dist/docs.d.ts +1 -1
  23. package/dist/docs.js +1 -1
  24. package/dist/doctor.d.ts +1 -1
  25. package/dist/doctor.js +1 -1
  26. package/dist/{file-process-guard-CiBwXjzm.d.ts → file-process-guard-DAYe9Ndt.d.ts} +22 -18
  27. package/dist/{file-system-DHgbuTFb.d.ts → file-system-BNtl997T.d.ts} +2 -1
  28. package/dist/{flume-source-listener-FzR5lGpL.d.ts → flume-source-listener-CLtsdcT2.d.ts} +8 -6
  29. package/dist/{funnel-diagnostics-Cvk6Sk4x.js → funnel-diagnostics-B-PCuv_E.js} +4 -3
  30. package/dist/{funnel-diagnostics-kbd27d7I.d.ts → funnel-diagnostics-CnKus3Jg.d.ts} +8 -5
  31. package/dist/{funnel-docs-C-ge0MuB.js → funnel-docs-BFcqITFA.js} +4 -0
  32. package/dist/{funnel-doctor-XrI2GBH8.js → funnel-doctor-Csw5qjcH.js} +1 -0
  33. package/dist/{funnel-doctor-CVTNUtDn.d.ts → funnel-doctor-CviUTINw.d.ts} +20 -9
  34. package/dist/{funnel-error-0t1MK1R6.js → funnel-error-DoSBCogD.js} +13 -0
  35. package/dist/{funnel-log-sqlite-sink-DLYkY0pZ.js → funnel-log-sqlite-sink-CXbs3qQp.js} +1 -1
  36. package/dist/{funnel-recovery-D8MkiTpk.d.ts → funnel-recovery-BwcSNNSN.d.ts} +1 -2
  37. package/dist/{funnel-recovery-DKnEutUS.js → funnel-recovery-DYbSYGWC.js} +1 -0
  38. package/dist/gateway/daemon.js +66 -125
  39. package/dist/gateway.d.ts +2 -3
  40. package/dist/gateway.js +2 -2
  41. package/dist/{gh-connector-BUGCOEWS.js → gh-connector-D-iyjl-d.js} +5 -5
  42. package/dist/{gh-connector-schema-BCHEqpUg.d.ts → gh-connector-schema-BrNsCl1T.d.ts} +0 -1
  43. package/dist/{index-BU1TtuFy.d.ts → index-B4RvEaW-.d.ts} +205 -40
  44. package/dist/index.d.ts +115 -109
  45. package/dist/index.js +123 -32
  46. package/dist/{local-config-json-schema-DexV8vX3.js → local-config-json-schema-_-YVHX4c.js} +4 -4
  47. package/dist/local-config.d.ts +2 -3
  48. package/dist/local-config.js +1 -1
  49. package/dist/logger.d.ts +29 -13
  50. package/dist/logger.js +1 -1
  51. package/dist/{memory-token-prompter-D0kIdhar.d.ts → memory-token-prompter-D3R_kgM_.d.ts} +2 -3
  52. package/dist/{node-file-system-BOXIHW_Q.js → node-file-system-xJx7dymE.js} +11 -3
  53. package/dist/{profiles-M24JrcOh.d.ts → profiles-CePRnv3F.d.ts} +3 -4
  54. package/dist/{profiles-ZHLONml4.js → profiles-KpN7_gBw.js} +19 -5
  55. package/dist/profiles.d.ts +1 -1
  56. package/dist/profiles.js +1 -1
  57. package/dist/recovery.d.ts +1 -1
  58. package/dist/recovery.js +1 -1
  59. package/dist/{schedule-connector-schema-D7ZEKvit.d.ts → schedule-connector-schema-DBko_Bf7.d.ts} +0 -1
  60. package/dist/{schedule-connector-9k3gOIgl.js → schedule-connector-teeDMHUe.js} +5 -5
  61. package/dist/{settings-reader-BnKIbmsD.d.ts → settings-reader-CyTpbAre.d.ts} +1 -2
  62. package/dist/{settings-schema-IShSMm5G.d.ts → settings-schema-B-CKLuST.d.ts} +0 -1
  63. package/dist/{settings-store-ccv291GV.js → settings-store-ZFWBhOnp.js} +12 -3
  64. package/dist/{slack-connector-CxpWagbT.js → slack-connector-BE0KAjCs.js} +10 -9
  65. package/dist/{slack-event-processor-xFDG3US0.js → slack-event-processor-Cs_YuI3w.js} +2 -2
  66. package/dist/{slack-event-processor-BDTr62cN.d.ts → slack-event-processor-DmAlq4cV.d.ts} +0 -1
  67. package/dist/{slot-fields-D-pvMgTK.js → slot-fields-5ERhytIA.js} +3 -3
  68. package/dist/{sqlite-diagnostic-log-CwhXE9Qh.js → sqlite-diagnostic-log-BDxfXGZy.js} +42 -19
  69. package/dist/{yaml-render-DVEtYuF1.js → yaml-render-DxxRQT-V.js} +3 -3
  70. package/package.json +10 -10
  71. package/dist/broadcaster-DVNcw00K.d.ts +0 -138
  72. package/dist/file-system-Wvzc2ePY.js +0 -9
  73. package/dist/logger-B6iyNbxM.js +0 -9
  74. package/dist/settings-reader-9FcX3qS1.js +0 -11
  75. /package/dist/{connector-adapter-Dvs8N7ew.js → connector-adapter-BL2ehMej.js} +0 -0
  76. /package/dist/{connector-listener-mPGZYa8e.js → connector-listener-BcQMzY6Q.js} +0 -0
  77. /package/dist/{diagnostic-sql-reader-oXZnWFf_.js → diagnostic-sql-reader-CoBk_EFF.js} +0 -0
  78. /package/dist/{discord-connector-schema-B4YpWpR3.js → discord-connector-schema-saAGuPmJ.js} +0 -0
  79. /package/dist/{error-message-of-ColuYmAk.js → error-message-of-BG4ezZ_s.js} +0 -0
  80. /package/dist/{funnel-docs-Cd66sKyV.d.ts → funnel-docs-BJWawfWI.d.ts} +0 -0
  81. /package/dist/{gh-connector-schema-CAqIhzGr.js → gh-connector-schema-DmZTrzUu.js} +0 -0
  82. /package/dist/{logger-Bc_Ooypb.d.ts → logger-mMZvblTx.d.ts} +0 -0
  83. /package/dist/{loopback-fetch-CVNuN3YZ.js → loopback-fetch-HP-bo9Kv.js} +0 -0
  84. /package/dist/{node-http-client-u00atiKx.js → node-http-client-SX3xFh4h.js} +0 -0
  85. /package/dist/{node-process-runner-DxTvycoK.js → node-process-runner-D4rzPl8Q.js} +0 -0
  86. /package/dist/{resolve-connector-token-DxDG9mhf.js → resolve-connector-token-BwiJCu82.js} +0 -0
  87. /package/dist/{schedule-connector-schema-DKEPZnVv.js → schedule-connector-schema-C2AChpwX.js} +0 -0
  88. /package/dist/{slack-connector-schema-Dem8to4P.js → slack-connector-schema-EGVuuqSX.js} +0 -0
package/dist/channel.d.ts CHANGED
@@ -1,12 +1,10 @@
1
- import { n as FunnelClock, t as FunnelLogger } from "./logger-Bc_Ooypb.js";
2
- import { n as FunnelFileSystem } from "./file-system-DHgbuTFb.js";
3
- import { r as FunnelBroadcaster } from "./broadcaster-DVNcw00K.js";
1
+ import { n as FunnelClock, t as FunnelLogger } from "./logger-mMZvblTx.js";
2
+ import { n as FunnelFileSystem } from "./file-system-BNtl997T.js";
4
3
  import { FlumeCatchupPolicy, FlumeErrorHandler, FlumeEvent, FlumeRuntimeDeps, FlumeSource, FlumeStatePersister } from "@interactive-inc/flume";
5
-
6
4
  //#region lib/engine/channel/channel.d.ts
7
5
  /**
8
- * 1 tick / 1 event を `broadcaster.broadcast(content, meta)` に流すための整形済みペイロード。
9
- * `null` を返すと「drop(broadcast しない) を意味する
6
+ * Payload shaped for `broadcaster.broadcast(content, meta)`. A transform
7
+ * returning `null` means "drop" (do not broadcast)
10
8
  */
11
9
  type ChannelBroadcastPayload = {
12
10
  readonly content: string;
@@ -14,21 +12,29 @@ type ChannelBroadcastPayload = {
14
12
  };
15
13
  type ChannelTransform = (event: FlumeEvent) => ChannelBroadcastPayload | null;
16
14
  /**
17
- * Channel `build(ctx)` で返す実体。flume sources の集合と任意の transform 1 まとまりとして
18
- * supervisor に渡す
15
+ * Narrow broadcast seam the supervisor depends on. The gateway
16
+ * `FunnelBroadcaster` satisfies it structurally, so the engine layer never
17
+ * imports from the gateway layer
18
+ */
19
+ type ChannelBroadcastSink = {
20
+ broadcast(content: string, meta?: Record<string, string>): void;
21
+ };
22
+ /**
23
+ * What `Channel.build(ctx)` returns: the flume sources to run plus an optional
24
+ * transform, handed to the supervisor as one unit
19
25
  */
20
26
  type ChannelRuntime = {
21
27
  readonly sources: ReadonlyArray<FlumeSource>;
22
28
  /**
23
- * source FlumeEvent broadcast payload に変換する関数。
24
- * 省略時は `{ content: JSON.stringify(event.data), meta: event.meta }` でフォールバック
29
+ * Converts each source's FlumeEvent into a broadcast payload. Defaults to
30
+ * `{ content: JSON.stringify(event.data), meta: event.meta }` when omitted
25
31
  */
26
32
  readonly transform?: ChannelTransform;
27
33
  };
28
34
  /**
29
- * Channel.build に渡される環境。FunnelClock / FunnelLogger / FunnelFileSystem を経由して
30
- * IO 境界を抽象化する。statePersister funnel-fs に紐づいた `FlumeStatePersister<S>`
31
- * channel id-scoped なファイル名で生成するヘルパー
35
+ * Environment passed to `Channel.build`. IO boundaries stay abstract via
36
+ * FunnelClock / FunnelLogger / FunnelFileSystem. `statePersister` builds a
37
+ * funnel-fs-backed `FlumeStatePersister<S>` scoped to the channel id
32
38
  */
33
39
  type ChannelBuildContext = {
34
40
  readonly channelId: string;
@@ -38,66 +44,77 @@ type ChannelBuildContext = {
38
44
  readonly clock: FunnelClock;
39
45
  readonly fs: FunnelFileSystem;
40
46
  /**
41
- * `<funnelDir>/channels/<channelId>/<filename>.json` Read/Write する純粋 DI 用 helper。
42
- * flume が要求する `FlumeStatePersister<S>` 形に合わせる
47
+ * Reads/writes `<funnelDir>/channels/<channelId>/<filename>.json` in the
48
+ * `FlumeStatePersister<S>` shape flume expects
43
49
  */
44
50
  readonly statePersister: <S>(filename: string) => FlumeStatePersister<S>;
45
51
  };
46
52
  /**
47
- * 1 channel = 1 inbound 受信単位。`build` confluence に挿す sources を返す。
48
- * `build` host abort / token rotation 等で何度でも再呼び出しされうる「純粋なファクトリ」
53
+ * One channel = one inbound intake unit. `build` returns the sources to plug
54
+ * into the confluence and may be re-invoked any number of times (host abort,
55
+ * token rotation, ...) — treat it as a pure factory
49
56
  */
50
57
  type Channel = {
51
58
  readonly id: string;
52
59
  readonly name?: string;
53
60
  readonly build: (ctx: ChannelBuildContext) => ChannelRuntime | Promise<ChannelRuntime>;
54
61
  };
55
- /** `Channel` frozen で返す identity helper。consumer の型推論を整える */
62
+ /** Frozen identity helper that keeps consumer type inference tidy */
56
63
  declare function defineChannel(channel: Channel): Channel;
57
64
  //#endregion
58
65
  //#region lib/engine/channel/channel-supervisor.d.ts
59
- type Props$1 = {
60
- readonly broadcaster: FunnelBroadcaster;
66
+ type Props$2 = {
67
+ readonly broadcaster: ChannelBroadcastSink;
61
68
  readonly logger: FunnelLogger;
62
69
  readonly clock: FunnelClock;
63
70
  readonly fs: FunnelFileSystem;
64
71
  /**
65
- * Channel ごとの state / 永続化ファイルの起点。
66
- * `<root>/channels/<channelId>/` 配下に書き込む
72
+ * Root for per-channel state files. Writes go under
73
+ * `<root>/channels/<channelId>/`
67
74
  */
68
75
  readonly dir: string;
69
76
  readonly deps?: FlumeRuntimeDeps;
70
- readonly onError?: FlumeErrorHandler; /** Host abort. fired すると全 channel を close する */
77
+ readonly onError?: FlumeErrorHandler;
78
+ /** Host abort. Once fired the supervisor closes every channel and goes inert */
71
79
  readonly signal?: AbortSignal;
72
80
  };
73
81
  /**
74
- * Channel-manifest `FlumeConfluence` 1 つにマッピングする supervisor。
82
+ * Maps the Channel manifest onto a single `FlumeConfluence`.
75
83
  *
76
- * - `register(channel)` で受け入れた channel は `start()` 時に `build(ctx)` され
77
- * sources confluence `add(channelId, sources)` で挿される
78
- * - confluence onItem (groupId 付き) 1 本受けて、対応する channel の `transform`
79
- * を通してから `broadcaster.broadcast(content, meta)` に流す
80
- * - `unregister(id)` で個別停止、`stop()` で全停止
84
+ * - channels accepted via `register(channel)` are `build(ctx)`-ed on `start()`
85
+ * and their sources inserted with `add(channelId, sources)`
86
+ * - the confluence onItem stream (tagged with groupId) is routed through the
87
+ * owning channel's `transform`, then into `broadcaster.broadcast(content, meta)`
88
+ * - `unregister(id)` stops one channel, `stop()` stops them all
81
89
  *
82
- * 既存の `FunnelListenerRegistry` (ConnectorDescriptor 系) とは独立。
83
- * 並走させて段階的に移行する設計
90
+ * Independent from the ConnectorDescriptor system (`FunnelListenerRegistry`);
91
+ * the two run side-by-side so callers can migrate incrementally
84
92
  */
85
93
  declare class FunnelChannelSupervisor {
86
94
  private readonly props;
87
95
  private readonly confluence;
88
96
  private readonly registered;
89
97
  private readonly pending;
98
+ private readonly opening;
90
99
  private started;
91
- constructor(props: Props$1);
92
- /** start 前 / 後どちらでも受け付ける。start 後は即座に build + add する */
100
+ private aborted;
101
+ constructor(props: Props$2);
102
+ /**
103
+ * Accepted before or after start (post-start registers open immediately,
104
+ * tracked in `opening` until settled). No-op once the host signal aborted
105
+ */
93
106
  register(channel: Channel): void;
107
+ /** Waits for an in-flight post-start open of the same id before removing */
94
108
  unregister(id: string): Promise<void>;
95
109
  start(): Promise<void>;
96
110
  stop(): Promise<void>;
97
111
  ids(): ReadonlyArray<string>;
98
112
  has(id: string): boolean;
113
+ /** Never rejects: build/add failures are logged and the channel is skipped */
99
114
  private openChannel;
115
+ private buildRuntime;
100
116
  private handleItem;
117
+ /** Never throws: a throwing user transform is logged and the event dropped */
101
118
  private transformToPayload;
102
119
  }
103
120
  //#endregion
@@ -105,22 +122,24 @@ declare class FunnelChannelSupervisor {
105
122
  type Options = {
106
123
  readonly id: string;
107
124
  readonly name?: string;
108
- readonly cron: string; /** 既定: `{ mode: "lastOnly" }` (起動時に直近 1 件だけ取り戻す) */
125
+ readonly cron: string;
126
+ /** Default: `{ mode: "lastOnly" }` (recover only the most recent missed tick on startup) */
109
127
  readonly catchupPolicy?: FlumeCatchupPolicy;
110
128
  /**
111
- * tick broadcast payload に変換する関数。省略時は
112
- * `{ content: "tick", meta: { cron, firedAt } }` 相当
129
+ * Converts each tick into a broadcast payload. Defaults to the equivalent of
130
+ * `{ content: "tick", meta: { cron, firedAt } }`
113
131
  */
114
132
  readonly transform?: ChannelTransform;
115
133
  /**
116
- * `statePersister` を有効にするかどうか (既定 true)。`false` にすると catchup も無効
134
+ * Whether to enable `statePersister` (default true). `false` also disables
135
+ * catchup
117
136
  */
118
137
  readonly persist?: boolean;
119
138
  };
120
139
  /**
121
- * 1 cron entry time channel を作る factory。
140
+ * Factory for a single-cron-entry time channel.
122
141
  *
123
- * 使用例 (inta jiho):
142
+ * Example (inta jiho):
124
143
  * ```ts
125
144
  * const jiho = timeChannel({
126
145
  * id: "inta-jiho",
@@ -132,30 +151,35 @@ type Options = {
132
151
  * })
133
152
  * ```
134
153
  *
135
- * `persist: true` (既定) の場合、`<channelDir>/time.json` に `lastFiredAt` を残し、
136
- * 起動時に `catchupPolicy` (既定 lastOnly) に従って過去 tick を取り戻す
154
+ * With `persist: true` (default), `lastFiredAt` is kept in `<channelDir>/time.json`
155
+ * and missed ticks are recovered on startup according to `catchupPolicy`
156
+ * (default lastOnly)
137
157
  */
138
158
  declare function timeChannel(options: Options): Channel;
139
159
  //#endregion
140
160
  //#region lib/engine/channel/file-state-persister.d.ts
141
- type Props = {
161
+ type Props$1 = {
142
162
  readonly fs: FunnelFileSystem;
143
163
  readonly path: string;
144
164
  };
145
165
  /**
146
- * `FunnelFileSystem` 上に JSON 1 / 1 状態を保存する `FlumeStatePersister`。
147
- * 保存先ディレクトリは load/save の前に再帰作成する。
148
- * load は不在 / parse 失敗時に `null` を返す (flume 側で素直に「初回扱い」になる)
166
+ * `FlumeStatePersister` storing one JSON document per file on top of
167
+ * `FunnelFileSystem`. The target directory is created recursively before save.
168
+ * load returns `null` when the file is absent, empty, or unparsable (flume
169
+ * then treats the source as first-run)
149
170
  */
150
- declare function createFileStatePersister<S>(props: Props): FlumeStatePersister<S>;
151
- type DirProps = {
152
- readonly fs: FunnelFileSystem; /** `<funnelDir>/channels/<channelId>` の絶対パス */
171
+ declare function createFileStatePersister<S>(props: Props$1): FlumeStatePersister<S>;
172
+ //#endregion
173
+ //#region lib/engine/channel/channel-state-persister-factory.d.ts
174
+ type Props = {
175
+ readonly fs: FunnelFileSystem;
176
+ /** Absolute path of `<funnelDir>/channels/<channelId>` */
153
177
  readonly channelDir: string;
154
178
  };
155
179
  /**
156
- * channel 単位の state persister ファクトリ。`statePersister<S>("time")` を呼ぶと
157
- * `<channelDir>/time.json` 用の persister を返す
180
+ * Per-channel state persister factory: `statePersister<S>("time")` returns a
181
+ * persister bound to `<channelDir>/time.json`
158
182
  */
159
- declare function createChannelStatePersisterFactory(props: DirProps): <S>(filename: string) => FlumeStatePersister<S>;
183
+ declare function createChannelStatePersisterFactory(props: Props): <S>(filename: string) => FlumeStatePersister<S>;
160
184
  //#endregion
161
- export { type Channel, type ChannelBroadcastPayload, type ChannelBuildContext, type ChannelRuntime, type ChannelTransform, FunnelChannelSupervisor, createChannelStatePersisterFactory, createFileStatePersister, defineChannel, timeChannel };
185
+ export { type Channel, type ChannelBroadcastPayload, type ChannelBroadcastSink, type ChannelBuildContext, type ChannelRuntime, type ChannelTransform, FunnelChannelSupervisor, createChannelStatePersisterFactory, createFileStatePersister, defineChannel, timeChannel };
package/dist/channel.js CHANGED
@@ -1,16 +1,18 @@
1
1
  import { dirname, join } from "node:path";
2
- import { FlumeConfluence, FlumeTimeSource } from "@interactive-inc/flume";
2
+ import { FlumeConfluence } from "@interactive-inc/flume";
3
+ import { FlumeTimeSource } from "@interactive-inc/flume/time";
3
4
  //#region lib/engine/channel/channel.ts
4
- /** `Channel` frozen で返す identity helper。consumer の型推論を整える */
5
+ /** Frozen identity helper that keeps consumer type inference tidy */
5
6
  function defineChannel(channel) {
6
7
  return Object.freeze({ ...channel });
7
8
  }
8
9
  //#endregion
9
10
  //#region lib/engine/channel/file-state-persister.ts
10
11
  /**
11
- * `FunnelFileSystem` 上に JSON 1 / 1 状態を保存する `FlumeStatePersister`。
12
- * 保存先ディレクトリは load/save の前に再帰作成する。
13
- * load は不在 / parse 失敗時に `null` を返す (flume 側で素直に「初回扱い」になる)
12
+ * `FlumeStatePersister` storing one JSON document per file on top of
13
+ * `FunnelFileSystem`. The target directory is created recursively before save.
14
+ * load returns `null` when the file is absent, empty, or unparsable (flume
15
+ * then treats the source as first-run)
14
16
  */
15
17
  function createFileStatePersister(props) {
16
18
  return {
@@ -31,9 +33,11 @@ function createFileStatePersister(props) {
31
33
  }
32
34
  };
33
35
  }
36
+ //#endregion
37
+ //#region lib/engine/channel/channel-state-persister-factory.ts
34
38
  /**
35
- * channel 単位の state persister ファクトリ。`statePersister<S>("time")` を呼ぶと
36
- * `<channelDir>/time.json` 用の persister を返す
39
+ * Per-channel state persister factory: `statePersister<S>("time")` returns a
40
+ * persister bound to `<channelDir>/time.json`
37
41
  */
38
42
  function createChannelStatePersisterFactory(props) {
39
43
  return (filename) => createFileStatePersister({
@@ -44,22 +48,25 @@ function createChannelStatePersisterFactory(props) {
44
48
  //#endregion
45
49
  //#region lib/engine/channel/channel-supervisor.ts
46
50
  /**
47
- * Channel-manifest `FlumeConfluence` 1 つにマッピングする supervisor。
51
+ * Maps the Channel manifest onto a single `FlumeConfluence`.
48
52
  *
49
- * - `register(channel)` で受け入れた channel は `start()` 時に `build(ctx)` され
50
- * sources confluence `add(channelId, sources)` で挿される
51
- * - confluence onItem (groupId 付き) 1 本受けて、対応する channel の `transform`
52
- * を通してから `broadcaster.broadcast(content, meta)` に流す
53
- * - `unregister(id)` で個別停止、`stop()` で全停止
53
+ * - channels accepted via `register(channel)` are `build(ctx)`-ed on `start()`
54
+ * and their sources inserted with `add(channelId, sources)`
55
+ * - the confluence onItem stream (tagged with groupId) is routed through the
56
+ * owning channel's `transform`, then into `broadcaster.broadcast(content, meta)`
57
+ * - `unregister(id)` stops one channel, `stop()` stops them all
54
58
  *
55
- * 既存の `FunnelListenerRegistry` (ConnectorDescriptor 系) とは独立。
56
- * 並走させて段階的に移行する設計
59
+ * Independent from the ConnectorDescriptor system (`FunnelListenerRegistry`);
60
+ * the two run side-by-side so callers can migrate incrementally
57
61
  */
58
62
  var FunnelChannelSupervisor = class {
63
+ props;
59
64
  confluence;
60
65
  registered = /* @__PURE__ */ new Map();
61
66
  pending = /* @__PURE__ */ new Map();
67
+ opening = /* @__PURE__ */ new Map();
62
68
  started = false;
69
+ aborted = false;
63
70
  constructor(props) {
64
71
  this.props = props;
65
72
  this.confluence = new FlumeConfluence({
@@ -67,25 +74,36 @@ var FunnelChannelSupervisor = class {
67
74
  onError: props.onError,
68
75
  deps: props.deps
69
76
  });
70
- if (props.signal) {
77
+ if (props.signal) if (props.signal.aborted) this.aborted = true;
78
+ else {
71
79
  const onAbort = () => {
80
+ this.aborted = true;
72
81
  this.stop().catch(() => {});
73
82
  };
74
- if (props.signal.aborted) this.started = true;
75
- else props.signal.addEventListener("abort", onAbort, { once: true });
83
+ props.signal.addEventListener("abort", onAbort, { once: true });
76
84
  }
77
85
  }
78
- /** start 前 / 後どちらでも受け付ける。start 後は即座に build + add する */
86
+ /**
87
+ * Accepted before or after start (post-start registers open immediately,
88
+ * tracked in `opening` until settled). No-op once the host signal aborted
89
+ */
79
90
  register(channel) {
80
- if (this.registered.has(channel.id) || this.pending.has(channel.id)) throw new Error(`FunnelChannelSupervisor: channel id already registered: ${channel.id}`);
91
+ if (this.aborted) return;
92
+ if (this.has(channel.id)) throw new Error(`FunnelChannelSupervisor: channel id already registered: ${channel.id}`);
81
93
  if (!this.started) {
82
94
  this.pending.set(channel.id, channel);
83
95
  return;
84
96
  }
85
- this.openChannel(channel);
97
+ const open = this.openChannel(channel).finally(() => {
98
+ this.opening.delete(channel.id);
99
+ });
100
+ this.opening.set(channel.id, open);
86
101
  }
102
+ /** Waits for an in-flight post-start open of the same id before removing */
87
103
  async unregister(id) {
88
104
  this.pending.delete(id);
105
+ const open = this.opening.get(id);
106
+ if (open) await open;
89
107
  const entry = this.registered.get(id);
90
108
  if (!entry) return;
91
109
  this.registered.delete(id);
@@ -93,7 +111,7 @@ var FunnelChannelSupervisor = class {
93
111
  await this.confluence.remove(id);
94
112
  }
95
113
  async start() {
96
- if (this.started) return;
114
+ if (this.started || this.aborted) return;
97
115
  this.started = true;
98
116
  const channels = [...this.pending.values()];
99
117
  this.pending.clear();
@@ -101,32 +119,27 @@ var FunnelChannelSupervisor = class {
101
119
  }
102
120
  async stop() {
103
121
  this.started = false;
122
+ this.pending.clear();
123
+ await Promise.all(this.opening.values());
104
124
  for (const entry of this.registered.values()) entry.abortController.abort();
105
125
  this.registered.clear();
106
- this.pending.clear();
107
126
  await this.confluence.closeAll();
108
127
  }
109
128
  ids() {
110
129
  return [...this.registered.keys()];
111
130
  }
112
131
  has(id) {
113
- return this.registered.has(id) || this.pending.has(id);
132
+ return this.registered.has(id) || this.pending.has(id) || this.opening.has(id);
114
133
  }
134
+ /** Never rejects: build/add failures are logged and the channel is skipped */
115
135
  async openChannel(channel) {
116
- const channelDir = join(this.props.dir, "channels", channel.id);
117
136
  const abortController = new AbortController();
118
- const runtime = await channel.build({
119
- channelId: channel.id,
120
- channelName: channel.name ?? channel.id,
121
- signal: abortController.signal,
122
- logger: this.props.logger,
123
- clock: this.props.clock,
124
- fs: this.props.fs,
125
- statePersister: createChannelStatePersisterFactory({
126
- fs: this.props.fs,
127
- channelDir
128
- })
129
- });
137
+ const runtime = await this.buildRuntime(channel, abortController.signal);
138
+ if (runtime instanceof Error) {
139
+ this.props.logger.error(`channel "${channel.id}" failed to build`, { error: runtime.message });
140
+ abortController.abort();
141
+ return;
142
+ }
130
143
  const result = await this.confluence.add(channel.id, runtime.sources);
131
144
  if (result instanceof Error) {
132
145
  this.props.logger.error(`channel "${channel.id}" failed to start`, { error: result.message });
@@ -139,6 +152,25 @@ var FunnelChannelSupervisor = class {
139
152
  abortController
140
153
  });
141
154
  }
155
+ async buildRuntime(channel, signal) {
156
+ const channelDir = join(this.props.dir, "channels", channel.id);
157
+ try {
158
+ return await channel.build({
159
+ channelId: channel.id,
160
+ channelName: channel.name ?? channel.id,
161
+ signal,
162
+ logger: this.props.logger,
163
+ clock: this.props.clock,
164
+ fs: this.props.fs,
165
+ statePersister: createChannelStatePersisterFactory({
166
+ fs: this.props.fs,
167
+ channelDir
168
+ })
169
+ });
170
+ } catch (error) {
171
+ return error instanceof Error ? error : new Error(String(error));
172
+ }
173
+ }
142
174
  handleItem(item) {
143
175
  if (item.kind !== "event") return;
144
176
  const entry = this.registered.get(item.groupId);
@@ -147,26 +179,28 @@ var FunnelChannelSupervisor = class {
147
179
  if (payload === null) return;
148
180
  this.props.broadcaster.broadcast(payload.content, payload.meta);
149
181
  }
182
+ /** Never throws: a throwing user transform is logged and the event dropped */
150
183
  transformToPayload(entry, event) {
151
184
  const transform = entry.runtime.transform;
152
185
  if (transform === void 0) return {
153
186
  content: JSON.stringify(event.data),
154
187
  meta: event.meta
155
188
  };
156
- const out = transform(event);
157
- if (out === null) return null;
158
- return {
159
- content: out.content,
160
- meta: out.meta
161
- };
189
+ try {
190
+ return transform(event);
191
+ } catch (error) {
192
+ const message = error instanceof Error ? error.message : String(error);
193
+ this.props.logger.error(`channel "${entry.channel.id}" transform threw`, { error: message });
194
+ return null;
195
+ }
162
196
  }
163
197
  };
164
198
  //#endregion
165
199
  //#region lib/engine/channel/time-channel.ts
166
200
  /**
167
- * 1 cron entry time channel を作る factory。
201
+ * Factory for a single-cron-entry time channel.
168
202
  *
169
- * 使用例 (inta jiho):
203
+ * Example (inta jiho):
170
204
  * ```ts
171
205
  * const jiho = timeChannel({
172
206
  * id: "inta-jiho",
@@ -178,8 +212,9 @@ var FunnelChannelSupervisor = class {
178
212
  * })
179
213
  * ```
180
214
  *
181
- * `persist: true` (既定) の場合、`<channelDir>/time.json` に `lastFiredAt` を残し、
182
- * 起動時に `catchupPolicy` (既定 lastOnly) に従って過去 tick を取り戻す
215
+ * With `persist: true` (default), `lastFiredAt` is kept in `<channelDir>/time.json`
216
+ * and missed ticks are recovered on startup according to `catchupPolicy`
217
+ * (default lastOnly)
183
218
  */
184
219
  function timeChannel(options) {
185
220
  const persist = options.persist ?? true;
@@ -1,9 +1,8 @@
1
- import { n as FunnelClock, t as FunnelLogger } from "./logger-Bc_Ooypb.js";
2
- import { n as FunnelFileSystem } from "./file-system-DHgbuTFb.js";
3
- import { n as ChannelDeliveryMode, t as ChannelConfig } from "./settings-schema-IShSMm5G.js";
4
- import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-BnKIbmsD.js";
5
- import { A as FunnelConnectorListener, F as BaseConnectorConfig, M as CallInput, N as FunnelConnectorAdapter, c as FunnelHttpClient, g as ConnectorDiagnosticLog, n as ConnectorBuildContext, o as ConnectorOperationContext, r as ConnectorDescriptor, s as ConnectorUpdateContext, z as FunnelProcessRunner } from "./connector-descriptor-CmyHlEiT.js";
6
-
1
+ import { n as FunnelClock, t as FunnelLogger } from "./logger-mMZvblTx.js";
2
+ import { n as FunnelFileSystem } from "./file-system-BNtl997T.js";
3
+ import { n as ChannelDeliveryMode, t as ChannelConfig } from "./settings-schema-B-CKLuST.js";
4
+ import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-CyTpbAre.js";
5
+ import { A as FunnelConnectorListener, F as BaseConnectorConfig, M as CallInput, N as FunnelConnectorAdapter, c as FunnelHttpClient, g as ConnectorDiagnosticLog, n as ConnectorBuildContext, o as ConnectorOperationContext, r as ConnectorDescriptor, s as ConnectorUpdateContext, z as FunnelProcessRunner } from "./connector-descriptor-F75ZI8Fh.js";
7
6
  //#region lib/engine/connectors/connector-registry.d.ts
8
7
  type Deps$1 = {
9
8
  /** Connector types this funnel handles. Each is a self-describing descriptor;
package/dist/claude.d.ts CHANGED
@@ -1,8 +1,7 @@
1
- import { n as FunnelFileSystem } from "./file-system-DHgbuTFb.js";
2
- import { a as ProcessGuard, c as ChannelResolver, i as SessionStore, n as FunnelClaude, o as McpInstaller, r as LaunchOptions, s as GatewayController, t as FunnelFileProcessGuard } from "./file-process-guard-CiBwXjzm.js";
3
- import { r as ConnectorDescriptor } from "./connector-descriptor-CmyHlEiT.js";
4
- import { t as FunnelProfiles } from "./profiles-M24JrcOh.js";
5
-
1
+ import { n as FunnelFileSystem } from "./file-system-BNtl997T.js";
2
+ import { a as ProcessGuard, c as ChannelResolver, i as SessionStore, n as FunnelClaude, o as McpInstaller, r as LaunchOptions, s as GatewayController, t as FunnelFileProcessGuard } from "./file-process-guard-DAYe9Ndt.js";
3
+ import { r as ConnectorDescriptor } from "./connector-descriptor-F75ZI8Fh.js";
4
+ import { t as FunnelProfiles } from "./profiles-CePRnv3F.js";
6
5
  //#region lib/engine/mcp/mcp.d.ts
7
6
  declare const FUNNEL_MCP_COMMAND = "bun";
8
7
  declare const FUNNEL_MCP_ARGS: string[];
@@ -35,9 +34,13 @@ declare class FunnelMcp {
35
34
  //#endregion
36
35
  //#region lib/engine/mcp/channel-server.d.ts
37
36
  type ChannelServerOptions = {
38
- /** Funnel home directory (settings.json + gateway.token). Defaults to ~/.funnel. */dir?: string; /** Gateway base URL. Defaults to `$FUNNEL_GATEWAY_URL` or `http://127.0.0.1:<port>`. */
39
- gatewayUrl?: string; /** Channel id to subscribe to. Defaults to `$FUNNEL_CHANNEL_ID`. */
40
- channelId?: string; /** Auth token. Defaults to `$FUNNEL_GATEWAY_TOKEN` then `<dir>/gateway.token`. */
37
+ /** Funnel home directory (settings.json + gateway.token). Defaults to ~/.funnel. */
38
+ dir?: string;
39
+ /** Gateway base URL. Defaults to `$FUNNEL_GATEWAY_URL` or `http://127.0.0.1:<port>`. */
40
+ gatewayUrl?: string;
41
+ /** Channel id to subscribe to. Defaults to `$FUNNEL_CHANNEL_ID`. */
42
+ channelId?: string;
43
+ /** Auth token. Defaults to `$FUNNEL_GATEWAY_TOKEN` then `<dir>/gateway.token`. */
41
44
  token?: string;
42
45
  /** Connector descriptors whose tools this MCP server exposes. Defaults to the
43
46
  * four built-in connectors; the tool-exposed subset is derived from them. */
package/dist/claude.js CHANGED
@@ -1,13 +1,13 @@
1
- import { n as gatewayLoopbackUrl, t as loopbackFetch } from "./loopback-fetch-CVNuN3YZ.js";
2
- import { f as settingsSchema, s as resolveFunnelPort } from "./settings-store-ccv291GV.js";
3
- import { a as FunnelMcp, i as FUNNEL_MCP_NAME, n as FUNNEL_MCP_ARGS, o as FunnelFileProcessGuard, r as FUNNEL_MCP_COMMAND, s as FunnelClaude, t as renderYaml } from "./yaml-render-DVEtYuF1.js";
4
- import { t as errorMessageOf } from "./error-message-of-ColuYmAk.js";
5
- import { t as FunnelDocs } from "./funnel-docs-C-ge0MuB.js";
6
- import { t as FunnelProfiles } from "./profiles-ZHLONml4.js";
7
- import { t as discordConnector } from "./discord-connector-DXARcO6w.js";
8
- import { t as ghConnector } from "./gh-connector-BUGCOEWS.js";
9
- import { t as scheduleConnector } from "./schedule-connector-9k3gOIgl.js";
10
- import { t as slackConnector } from "./slack-connector-CxpWagbT.js";
1
+ import { n as gatewayLoopbackUrl, t as loopbackFetch } from "./loopback-fetch-HP-bo9Kv.js";
2
+ import { f as settingsSchema, s as resolveFunnelPort } from "./settings-store-ZFWBhOnp.js";
3
+ import { a as FunnelMcp, i as FUNNEL_MCP_NAME, n as FUNNEL_MCP_ARGS, o as FunnelFileProcessGuard, r as FUNNEL_MCP_COMMAND, s as FunnelClaude, t as renderYaml } from "./yaml-render-DxxRQT-V.js";
4
+ import { t as FunnelProfiles } from "./profiles-KpN7_gBw.js";
5
+ import { t as errorMessageOf } from "./error-message-of-BG4ezZ_s.js";
6
+ import { t as FunnelDocs } from "./funnel-docs-BFcqITFA.js";
7
+ import { t as discordConnector } from "./discord-connector-CdjwPoty.js";
8
+ import { t as ghConnector } from "./gh-connector-D-iyjl-d.js";
9
+ import { t as scheduleConnector } from "./schedule-connector-teeDMHUe.js";
10
+ import { t as slackConnector } from "./slack-connector-BE0KAjCs.js";
11
11
  import { join } from "node:path";
12
12
  import { existsSync, readFileSync } from "node:fs";
13
13
  import { homedir } from "node:os";
@@ -506,11 +506,14 @@ const MAX_RECONNECT_DELAY = 1e4;
506
506
  * Reconnects with exponential backoff and replays missed events via `?since=<offset>`.
507
507
  */
508
508
  var FunnelChannelSubscriber = class {
509
+ props;
509
510
  state = {
510
511
  reconnectDelay: RECONNECT_DELAY,
511
512
  lastOffset: 0,
512
513
  isStarted: false,
513
- hasPendingReconnect: false
514
+ hasPendingReconnect: false,
515
+ hasDeliveryFailure: false,
516
+ messageQueue: Promise.resolve()
514
517
  };
515
518
  constructor(props) {
516
519
  this.props = props;
@@ -529,14 +532,17 @@ var FunnelChannelSubscriber = class {
529
532
  this.state.reconnectDelay = RECONNECT_DELAY;
530
533
  process.stderr.write(`funnel: connected (${wsUrl})\n`);
531
534
  });
532
- ws.addEventListener("message", (event) => this.handleMessage(event));
535
+ ws.addEventListener("message", (event) => this.enqueueMessage(event, ws));
533
536
  ws.addEventListener("close", () => {
534
537
  if (this.state.hasPendingReconnect) return;
535
538
  this.state.hasPendingReconnect = true;
536
539
  process.stderr.write(`funnel: disconnected, reconnecting in ${this.state.reconnectDelay}ms\n`);
537
540
  setTimeout(() => {
538
- this.state.hasPendingReconnect = false;
539
- this.connect();
541
+ this.state.messageQueue.then(() => {
542
+ this.state.hasPendingReconnect = false;
543
+ this.state.hasDeliveryFailure = false;
544
+ this.connect();
545
+ });
540
546
  }, this.state.reconnectDelay);
541
547
  this.state.reconnectDelay = Math.min(this.state.reconnectDelay * 2, MAX_RECONNECT_DELAY);
542
548
  });
@@ -545,12 +551,19 @@ var FunnelChannelSubscriber = class {
545
551
  process.stderr.write(`funnel: ws error: ${reason}\n`);
546
552
  });
547
553
  }
554
+ enqueueMessage(event, socket) {
555
+ this.state.messageQueue = this.state.messageQueue.then(async () => {
556
+ if (this.state.hasDeliveryFailure) return;
557
+ if (await this.handleMessage(event)) return;
558
+ this.state.hasDeliveryFailure = true;
559
+ socket?.close();
560
+ });
561
+ }
548
562
  async handleMessage(event) {
549
563
  try {
550
564
  const payload = JSON.parse(String(event.data));
551
565
  const eventType = payload.meta?.event_type ?? "unknown";
552
566
  const offset = typeof payload.offset === "number" ? payload.offset : null;
553
- if (offset !== null && offset > this.state.lastOffset) this.state.lastOffset = offset;
554
567
  process.stderr.write(`funnel: received event (${eventType})\n`);
555
568
  await this.props.server.notification({
556
569
  method: "notifications/claude/channel",
@@ -559,8 +572,11 @@ var FunnelChannelSubscriber = class {
559
572
  meta: payload.meta
560
573
  }
561
574
  });
575
+ if (offset !== null && offset > this.state.lastOffset) this.state.lastOffset = offset;
576
+ return true;
562
577
  } catch (error) {
563
578
  process.stderr.write(`funnel: error handling ws message (offset=${this.state.lastOffset}): ${errorMessageOf(error)}\n`);
579
+ return false;
564
580
  }
565
581
  }
566
582
  };