@interactive-inc/claude-funnel 0.65.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 (87) hide show
  1. package/README.md +8 -4
  2. package/dist/bin.js +275 -372
  3. package/dist/channel.d.ts +185 -0
  4. package/dist/channel.js +238 -0
  5. package/dist/{channels-CRGb6B5_.d.ts → channels-BBA3GTAa.d.ts} +5 -5
  6. package/dist/claude.d.ts +11 -8
  7. package/dist/claude.js +31 -15
  8. package/dist/{connector-descriptor-BFIhyTfa.d.ts → connector-descriptor-F75ZI8Fh.d.ts} +27 -39
  9. package/dist/{connector-diagnostics-recorder-COtNEmUp.js → connector-diagnostics-recorder-BmOijU1p.js} +1 -0
  10. package/dist/connectors/discord.d.ts +10 -7
  11. package/dist/connectors/discord.js +2 -2
  12. package/dist/connectors/gh.d.ts +6 -5
  13. package/dist/connectors/gh.js +2 -2
  14. package/dist/connectors/schedule.d.ts +10 -7
  15. package/dist/connectors/schedule.js +2 -2
  16. package/dist/connectors/slack.d.ts +14 -9
  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-D-bOVAKt.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-tVcgckH6.d.ts → file-process-guard-DAYe9Ndt.d.ts} +22 -17
  27. package/dist/{file-system-VhwwXZbm.d.ts → file-system-BNtl997T.d.ts} +2 -1
  28. package/dist/{flume-source-listener-BNyAII7N.d.ts → flume-source-listener-CLtsdcT2.d.ts} +8 -5
  29. package/dist/{funnel-diagnostics-Cvk6Sk4x.js → funnel-diagnostics-B-PCuv_E.js} +4 -3
  30. package/dist/{funnel-diagnostics-b9ar0Ing.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-CnRQi4kM.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-CMhY8Jfk.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 -2
  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-DWQaB6gX.d.ts → gh-connector-schema-BrNsCl1T.d.ts} +0 -1
  43. package/dist/{index-Ds6sHhA-.d.ts → index-B4RvEaW-.d.ts} +78 -46
  44. package/dist/index.d.ts +115 -107
  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-mMZvblTx.d.ts +25 -0
  50. package/dist/logger.d.ts +29 -13
  51. package/dist/logger.js +1 -1
  52. package/dist/{memory-token-prompter-BoV8Hf-n.d.ts → memory-token-prompter-D3R_kgM_.d.ts} +2 -3
  53. package/dist/{node-file-system-BOXIHW_Q.js → node-file-system-xJx7dymE.js} +11 -3
  54. package/dist/{profiles-cVZQkM69.d.ts → profiles-CePRnv3F.d.ts} +3 -4
  55. package/dist/{profiles-ZHLONml4.js → profiles-KpN7_gBw.js} +19 -5
  56. package/dist/profiles.d.ts +1 -1
  57. package/dist/profiles.js +1 -1
  58. package/dist/recovery.d.ts +1 -1
  59. package/dist/recovery.js +1 -1
  60. package/dist/{schedule-connector-schema-Z0RXLgPI.d.ts → schedule-connector-schema-DBko_Bf7.d.ts} +0 -1
  61. package/dist/{schedule-connector-9k3gOIgl.js → schedule-connector-teeDMHUe.js} +5 -5
  62. package/dist/{settings-reader-BNxjsxCB.d.ts → settings-reader-CyTpbAre.d.ts} +1 -2
  63. package/dist/{settings-schema-BL_c2Udm.d.ts → settings-schema-B-CKLuST.d.ts} +0 -1
  64. package/dist/{settings-store-ccv291GV.js → settings-store-ZFWBhOnp.js} +12 -3
  65. package/dist/{slack-connector-CxpWagbT.js → slack-connector-BE0KAjCs.js} +10 -9
  66. package/dist/{slack-event-processor-xFDG3US0.js → slack-event-processor-Cs_YuI3w.js} +2 -2
  67. package/dist/{slack-event-processor-BhCf5Wiy.d.ts → slack-event-processor-DmAlq4cV.d.ts} +0 -1
  68. package/dist/{slot-fields-D-pvMgTK.js → slot-fields-5ERhytIA.js} +3 -3
  69. package/dist/{sqlite-diagnostic-log-CwhXE9Qh.js → sqlite-diagnostic-log-BDxfXGZy.js} +42 -19
  70. package/dist/{yaml-render-DVEtYuF1.js → yaml-render-DxxRQT-V.js} +3 -3
  71. package/package.json +15 -10
  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-CNklHvbt.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/{loopback-fetch-CVNuN3YZ.js → loopback-fetch-HP-bo9Kv.js} +0 -0
  83. /package/dist/{node-http-client-u00atiKx.js → node-http-client-SX3xFh4h.js} +0 -0
  84. /package/dist/{node-process-runner-DxTvycoK.js → node-process-runner-D4rzPl8Q.js} +0 -0
  85. /package/dist/{resolve-connector-token-DxDG9mhf.js → resolve-connector-token-BwiJCu82.js} +0 -0
  86. /package/dist/{schedule-connector-schema-DKEPZnVv.js → schedule-connector-schema-C2AChpwX.js} +0 -0
  87. /package/dist/{slack-connector-schema-Dem8to4P.js → slack-connector-schema-EGVuuqSX.js} +0 -0
@@ -0,0 +1,185 @@
1
+ import { n as FunnelClock, t as FunnelLogger } from "./logger-mMZvblTx.js";
2
+ import { n as FunnelFileSystem } from "./file-system-BNtl997T.js";
3
+ import { FlumeCatchupPolicy, FlumeErrorHandler, FlumeEvent, FlumeRuntimeDeps, FlumeSource, FlumeStatePersister } from "@interactive-inc/flume";
4
+ //#region lib/engine/channel/channel.d.ts
5
+ /**
6
+ * Payload shaped for `broadcaster.broadcast(content, meta)`. A transform
7
+ * returning `null` means "drop" (do not broadcast)
8
+ */
9
+ type ChannelBroadcastPayload = {
10
+ readonly content: string;
11
+ readonly meta?: Record<string, string>;
12
+ };
13
+ type ChannelTransform = (event: FlumeEvent) => ChannelBroadcastPayload | null;
14
+ /**
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
25
+ */
26
+ type ChannelRuntime = {
27
+ readonly sources: ReadonlyArray<FlumeSource>;
28
+ /**
29
+ * Converts each source's FlumeEvent into a broadcast payload. Defaults to
30
+ * `{ content: JSON.stringify(event.data), meta: event.meta }` when omitted
31
+ */
32
+ readonly transform?: ChannelTransform;
33
+ };
34
+ /**
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
38
+ */
39
+ type ChannelBuildContext = {
40
+ readonly channelId: string;
41
+ readonly channelName: string;
42
+ readonly signal: AbortSignal;
43
+ readonly logger: FunnelLogger;
44
+ readonly clock: FunnelClock;
45
+ readonly fs: FunnelFileSystem;
46
+ /**
47
+ * Reads/writes `<funnelDir>/channels/<channelId>/<filename>.json` in the
48
+ * `FlumeStatePersister<S>` shape flume expects
49
+ */
50
+ readonly statePersister: <S>(filename: string) => FlumeStatePersister<S>;
51
+ };
52
+ /**
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
56
+ */
57
+ type Channel = {
58
+ readonly id: string;
59
+ readonly name?: string;
60
+ readonly build: (ctx: ChannelBuildContext) => ChannelRuntime | Promise<ChannelRuntime>;
61
+ };
62
+ /** Frozen identity helper that keeps consumer type inference tidy */
63
+ declare function defineChannel(channel: Channel): Channel;
64
+ //#endregion
65
+ //#region lib/engine/channel/channel-supervisor.d.ts
66
+ type Props$2 = {
67
+ readonly broadcaster: ChannelBroadcastSink;
68
+ readonly logger: FunnelLogger;
69
+ readonly clock: FunnelClock;
70
+ readonly fs: FunnelFileSystem;
71
+ /**
72
+ * Root for per-channel state files. Writes go under
73
+ * `<root>/channels/<channelId>/`
74
+ */
75
+ readonly dir: string;
76
+ readonly deps?: FlumeRuntimeDeps;
77
+ readonly onError?: FlumeErrorHandler;
78
+ /** Host abort. Once fired the supervisor closes every channel and goes inert */
79
+ readonly signal?: AbortSignal;
80
+ };
81
+ /**
82
+ * Maps the Channel manifest onto a single `FlumeConfluence`.
83
+ *
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
89
+ *
90
+ * Independent from the ConnectorDescriptor system (`FunnelListenerRegistry`);
91
+ * the two run side-by-side so callers can migrate incrementally
92
+ */
93
+ declare class FunnelChannelSupervisor {
94
+ private readonly props;
95
+ private readonly confluence;
96
+ private readonly registered;
97
+ private readonly pending;
98
+ private readonly opening;
99
+ private started;
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
+ */
106
+ register(channel: Channel): void;
107
+ /** Waits for an in-flight post-start open of the same id before removing */
108
+ unregister(id: string): Promise<void>;
109
+ start(): Promise<void>;
110
+ stop(): Promise<void>;
111
+ ids(): ReadonlyArray<string>;
112
+ has(id: string): boolean;
113
+ /** Never rejects: build/add failures are logged and the channel is skipped */
114
+ private openChannel;
115
+ private buildRuntime;
116
+ private handleItem;
117
+ /** Never throws: a throwing user transform is logged and the event dropped */
118
+ private transformToPayload;
119
+ }
120
+ //#endregion
121
+ //#region lib/engine/channel/time-channel.d.ts
122
+ type Options = {
123
+ readonly id: string;
124
+ readonly name?: string;
125
+ readonly cron: string;
126
+ /** Default: `{ mode: "lastOnly" }` (recover only the most recent missed tick on startup) */
127
+ readonly catchupPolicy?: FlumeCatchupPolicy;
128
+ /**
129
+ * Converts each tick into a broadcast payload. Defaults to the equivalent of
130
+ * `{ content: "tick", meta: { cron, firedAt } }`
131
+ */
132
+ readonly transform?: ChannelTransform;
133
+ /**
134
+ * Whether to enable `statePersister` (default true). `false` also disables
135
+ * catchup
136
+ */
137
+ readonly persist?: boolean;
138
+ };
139
+ /**
140
+ * Factory for a single-cron-entry time channel.
141
+ *
142
+ * Example (inta jiho):
143
+ * ```ts
144
+ * const jiho = timeChannel({
145
+ * id: "inta-jiho",
146
+ * cron: "0 * * * *",
147
+ * transform: (event) => ({
148
+ * content: `⏰ ${new Date(event.data.firedAt as number).getHours()}時をお知らせします。`,
149
+ * meta: { event_type: "schedule", source: "inta-jiho", target_role: "primary" },
150
+ * }),
151
+ * })
152
+ * ```
153
+ *
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)
157
+ */
158
+ declare function timeChannel(options: Options): Channel;
159
+ //#endregion
160
+ //#region lib/engine/channel/file-state-persister.d.ts
161
+ type Props$1 = {
162
+ readonly fs: FunnelFileSystem;
163
+ readonly path: string;
164
+ };
165
+ /**
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)
170
+ */
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>` */
177
+ readonly channelDir: string;
178
+ };
179
+ /**
180
+ * Per-channel state persister factory: `statePersister<S>("time")` returns a
181
+ * persister bound to `<channelDir>/time.json`
182
+ */
183
+ declare function createChannelStatePersisterFactory(props: Props): <S>(filename: string) => FlumeStatePersister<S>;
184
+ //#endregion
185
+ export { type Channel, type ChannelBroadcastPayload, type ChannelBroadcastSink, type ChannelBuildContext, type ChannelRuntime, type ChannelTransform, FunnelChannelSupervisor, createChannelStatePersisterFactory, createFileStatePersister, defineChannel, timeChannel };
@@ -0,0 +1,238 @@
1
+ import { dirname, join } from "node:path";
2
+ import { FlumeConfluence } from "@interactive-inc/flume";
3
+ import { FlumeTimeSource } from "@interactive-inc/flume/time";
4
+ //#region lib/engine/channel/channel.ts
5
+ /** Frozen identity helper that keeps consumer type inference tidy */
6
+ function defineChannel(channel) {
7
+ return Object.freeze({ ...channel });
8
+ }
9
+ //#endregion
10
+ //#region lib/engine/channel/file-state-persister.ts
11
+ /**
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)
16
+ */
17
+ function createFileStatePersister(props) {
18
+ return {
19
+ async load() {
20
+ if (!props.fs.existsSync(props.path)) return null;
21
+ const raw = props.fs.readFileSync(props.path);
22
+ if (raw === "") return null;
23
+ try {
24
+ return JSON.parse(raw);
25
+ } catch {
26
+ return null;
27
+ }
28
+ },
29
+ async save(state) {
30
+ const dir = dirname(props.path);
31
+ if (!props.fs.existsSync(dir)) props.fs.mkdirSync(dir, { recursive: true });
32
+ props.fs.writeFileSync(props.path, JSON.stringify(state));
33
+ }
34
+ };
35
+ }
36
+ //#endregion
37
+ //#region lib/engine/channel/channel-state-persister-factory.ts
38
+ /**
39
+ * Per-channel state persister factory: `statePersister<S>("time")` returns a
40
+ * persister bound to `<channelDir>/time.json`
41
+ */
42
+ function createChannelStatePersisterFactory(props) {
43
+ return (filename) => createFileStatePersister({
44
+ fs: props.fs,
45
+ path: join(props.channelDir, `${filename}.json`)
46
+ });
47
+ }
48
+ //#endregion
49
+ //#region lib/engine/channel/channel-supervisor.ts
50
+ /**
51
+ * Maps the Channel manifest onto a single `FlumeConfluence`.
52
+ *
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
58
+ *
59
+ * Independent from the ConnectorDescriptor system (`FunnelListenerRegistry`);
60
+ * the two run side-by-side so callers can migrate incrementally
61
+ */
62
+ var FunnelChannelSupervisor = class {
63
+ props;
64
+ confluence;
65
+ registered = /* @__PURE__ */ new Map();
66
+ pending = /* @__PURE__ */ new Map();
67
+ opening = /* @__PURE__ */ new Map();
68
+ started = false;
69
+ aborted = false;
70
+ constructor(props) {
71
+ this.props = props;
72
+ this.confluence = new FlumeConfluence({
73
+ onEvent: (item) => this.handleItem(item),
74
+ onError: props.onError,
75
+ deps: props.deps
76
+ });
77
+ if (props.signal) if (props.signal.aborted) this.aborted = true;
78
+ else {
79
+ const onAbort = () => {
80
+ this.aborted = true;
81
+ this.stop().catch(() => {});
82
+ };
83
+ props.signal.addEventListener("abort", onAbort, { once: true });
84
+ }
85
+ }
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
+ */
90
+ register(channel) {
91
+ if (this.aborted) return;
92
+ if (this.has(channel.id)) throw new Error(`FunnelChannelSupervisor: channel id already registered: ${channel.id}`);
93
+ if (!this.started) {
94
+ this.pending.set(channel.id, channel);
95
+ return;
96
+ }
97
+ const open = this.openChannel(channel).finally(() => {
98
+ this.opening.delete(channel.id);
99
+ });
100
+ this.opening.set(channel.id, open);
101
+ }
102
+ /** Waits for an in-flight post-start open of the same id before removing */
103
+ async unregister(id) {
104
+ this.pending.delete(id);
105
+ const open = this.opening.get(id);
106
+ if (open) await open;
107
+ const entry = this.registered.get(id);
108
+ if (!entry) return;
109
+ this.registered.delete(id);
110
+ entry.abortController.abort();
111
+ await this.confluence.remove(id);
112
+ }
113
+ async start() {
114
+ if (this.started || this.aborted) return;
115
+ this.started = true;
116
+ const channels = [...this.pending.values()];
117
+ this.pending.clear();
118
+ for (const channel of channels) await this.openChannel(channel);
119
+ }
120
+ async stop() {
121
+ this.started = false;
122
+ this.pending.clear();
123
+ await Promise.all(this.opening.values());
124
+ for (const entry of this.registered.values()) entry.abortController.abort();
125
+ this.registered.clear();
126
+ await this.confluence.closeAll();
127
+ }
128
+ ids() {
129
+ return [...this.registered.keys()];
130
+ }
131
+ has(id) {
132
+ return this.registered.has(id) || this.pending.has(id) || this.opening.has(id);
133
+ }
134
+ /** Never rejects: build/add failures are logged and the channel is skipped */
135
+ async openChannel(channel) {
136
+ const abortController = new AbortController();
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
+ }
143
+ const result = await this.confluence.add(channel.id, runtime.sources);
144
+ if (result instanceof Error) {
145
+ this.props.logger.error(`channel "${channel.id}" failed to start`, { error: result.message });
146
+ abortController.abort();
147
+ return;
148
+ }
149
+ this.registered.set(channel.id, {
150
+ channel,
151
+ runtime,
152
+ abortController
153
+ });
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
+ }
174
+ handleItem(item) {
175
+ if (item.kind !== "event") return;
176
+ const entry = this.registered.get(item.groupId);
177
+ if (!entry) return;
178
+ const payload = this.transformToPayload(entry, item.event);
179
+ if (payload === null) return;
180
+ this.props.broadcaster.broadcast(payload.content, payload.meta);
181
+ }
182
+ /** Never throws: a throwing user transform is logged and the event dropped */
183
+ transformToPayload(entry, event) {
184
+ const transform = entry.runtime.transform;
185
+ if (transform === void 0) return {
186
+ content: JSON.stringify(event.data),
187
+ meta: event.meta
188
+ };
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
+ }
196
+ }
197
+ };
198
+ //#endregion
199
+ //#region lib/engine/channel/time-channel.ts
200
+ /**
201
+ * Factory for a single-cron-entry time channel.
202
+ *
203
+ * Example (inta jiho):
204
+ * ```ts
205
+ * const jiho = timeChannel({
206
+ * id: "inta-jiho",
207
+ * cron: "0 * * * *",
208
+ * transform: (event) => ({
209
+ * content: `⏰ ${new Date(event.data.firedAt as number).getHours()}時をお知らせします。`,
210
+ * meta: { event_type: "schedule", source: "inta-jiho", target_role: "primary" },
211
+ * }),
212
+ * })
213
+ * ```
214
+ *
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)
218
+ */
219
+ function timeChannel(options) {
220
+ const persist = options.persist ?? true;
221
+ const catchupPolicy = options.catchupPolicy ?? { mode: "lastOnly" };
222
+ return defineChannel({
223
+ id: options.id,
224
+ name: options.name ?? options.id,
225
+ build: (ctx) => {
226
+ return {
227
+ sources: [new FlumeTimeSource({
228
+ cron: options.cron,
229
+ statePersister: persist ? ctx.statePersister("time") : void 0,
230
+ catchupPolicy: persist ? catchupPolicy : { mode: "off" }
231
+ })],
232
+ transform: options.transform
233
+ };
234
+ }
235
+ });
236
+ }
237
+ //#endregion
238
+ export { FunnelChannelSupervisor, createChannelStatePersisterFactory, createFileStatePersister, defineChannel, timeChannel };
@@ -1,8 +1,8 @@
1
- import { n as ChannelDeliveryMode, t as ChannelConfig } from "./settings-schema-BL_c2Udm.js";
2
- import { n as FunnelIdGenerator, t as FunnelSettingsReader } from "./settings-reader-BNxjsxCB.js";
3
- import { B as FunnelProcessRunner, I as BaseConnectorConfig, N as CallInput, P as FunnelConnectorAdapter, W as FunnelLogger, _ as ConnectorDiagnosticLog, c as FunnelClock, j as FunnelConnectorListener, l as FunnelHttpClient, n as ConnectorBuildContext, o as ConnectorOperationContext, r as ConnectorDescriptor, s as ConnectorUpdateContext } from "./connector-descriptor-BFIhyTfa.js";
4
- import { n as FunnelFileSystem } from "./file-system-VhwwXZbm.js";
5
-
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";
6
6
  //#region lib/engine/connectors/connector-registry.d.ts
7
7
  type Deps$1 = {
8
8
  /** Connector types this funnel handles. Each is a self-describing descriptor;
package/dist/claude.d.ts CHANGED
@@ -1,8 +1,7 @@
1
- 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-tVcgckH6.js";
2
- import { r as ConnectorDescriptor } from "./connector-descriptor-BFIhyTfa.js";
3
- import { n as FunnelFileSystem } from "./file-system-VhwwXZbm.js";
4
- import { t as FunnelProfiles } from "./profiles-cVZQkM69.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
  };