@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.
- package/README.md +8 -4
- package/dist/bin.js +275 -372
- package/dist/channel.d.ts +185 -0
- package/dist/channel.js +238 -0
- package/dist/{channels-CRGb6B5_.d.ts → channels-BBA3GTAa.d.ts} +5 -5
- package/dist/claude.d.ts +11 -8
- package/dist/claude.js +31 -15
- package/dist/{connector-descriptor-BFIhyTfa.d.ts → connector-descriptor-F75ZI8Fh.d.ts} +27 -39
- package/dist/{connector-diagnostics-recorder-COtNEmUp.js → connector-diagnostics-recorder-BmOijU1p.js} +1 -0
- package/dist/connectors/discord.d.ts +10 -7
- package/dist/connectors/discord.js +2 -2
- package/dist/connectors/gh.d.ts +6 -5
- package/dist/connectors/gh.js +2 -2
- package/dist/connectors/schedule.d.ts +10 -7
- package/dist/connectors/schedule.js +2 -2
- package/dist/connectors/slack.d.ts +14 -9
- package/dist/connectors/slack.js +3 -3
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +1 -1
- package/dist/{discord-connector-DXARcO6w.js → discord-connector-CdjwPoty.js} +6 -6
- package/dist/{discord-connector-schema-D-bOVAKt.d.ts → discord-connector-schema-BaBZYHxK.d.ts} +0 -1
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +1 -1
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +1 -1
- package/dist/{file-process-guard-tVcgckH6.d.ts → file-process-guard-DAYe9Ndt.d.ts} +22 -17
- package/dist/{file-system-VhwwXZbm.d.ts → file-system-BNtl997T.d.ts} +2 -1
- package/dist/{flume-source-listener-BNyAII7N.d.ts → flume-source-listener-CLtsdcT2.d.ts} +8 -5
- package/dist/{funnel-diagnostics-Cvk6Sk4x.js → funnel-diagnostics-B-PCuv_E.js} +4 -3
- package/dist/{funnel-diagnostics-b9ar0Ing.d.ts → funnel-diagnostics-CnKus3Jg.d.ts} +8 -5
- package/dist/{funnel-docs-C-ge0MuB.js → funnel-docs-BFcqITFA.js} +4 -0
- package/dist/{funnel-doctor-XrI2GBH8.js → funnel-doctor-Csw5qjcH.js} +1 -0
- package/dist/{funnel-doctor-CnRQi4kM.d.ts → funnel-doctor-CviUTINw.d.ts} +20 -9
- package/dist/{funnel-error-0t1MK1R6.js → funnel-error-DoSBCogD.js} +13 -0
- package/dist/{funnel-log-sqlite-sink-DLYkY0pZ.js → funnel-log-sqlite-sink-CXbs3qQp.js} +1 -1
- package/dist/{funnel-recovery-CMhY8Jfk.d.ts → funnel-recovery-BwcSNNSN.d.ts} +1 -2
- package/dist/{funnel-recovery-DKnEutUS.js → funnel-recovery-DYbSYGWC.js} +1 -0
- package/dist/gateway/daemon.js +66 -125
- package/dist/gateway.d.ts +2 -2
- package/dist/gateway.js +2 -2
- package/dist/{gh-connector-BUGCOEWS.js → gh-connector-D-iyjl-d.js} +5 -5
- package/dist/{gh-connector-schema-DWQaB6gX.d.ts → gh-connector-schema-BrNsCl1T.d.ts} +0 -1
- package/dist/{index-Ds6sHhA-.d.ts → index-B4RvEaW-.d.ts} +78 -46
- package/dist/index.d.ts +115 -107
- package/dist/index.js +123 -32
- package/dist/{local-config-json-schema-DexV8vX3.js → local-config-json-schema-_-YVHX4c.js} +4 -4
- package/dist/local-config.d.ts +2 -3
- package/dist/local-config.js +1 -1
- package/dist/logger-mMZvblTx.d.ts +25 -0
- package/dist/logger.d.ts +29 -13
- package/dist/logger.js +1 -1
- package/dist/{memory-token-prompter-BoV8Hf-n.d.ts → memory-token-prompter-D3R_kgM_.d.ts} +2 -3
- package/dist/{node-file-system-BOXIHW_Q.js → node-file-system-xJx7dymE.js} +11 -3
- package/dist/{profiles-cVZQkM69.d.ts → profiles-CePRnv3F.d.ts} +3 -4
- package/dist/{profiles-ZHLONml4.js → profiles-KpN7_gBw.js} +19 -5
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +1 -1
- package/dist/recovery.d.ts +1 -1
- package/dist/recovery.js +1 -1
- package/dist/{schedule-connector-schema-Z0RXLgPI.d.ts → schedule-connector-schema-DBko_Bf7.d.ts} +0 -1
- package/dist/{schedule-connector-9k3gOIgl.js → schedule-connector-teeDMHUe.js} +5 -5
- package/dist/{settings-reader-BNxjsxCB.d.ts → settings-reader-CyTpbAre.d.ts} +1 -2
- package/dist/{settings-schema-BL_c2Udm.d.ts → settings-schema-B-CKLuST.d.ts} +0 -1
- package/dist/{settings-store-ccv291GV.js → settings-store-ZFWBhOnp.js} +12 -3
- package/dist/{slack-connector-CxpWagbT.js → slack-connector-BE0KAjCs.js} +10 -9
- package/dist/{slack-event-processor-xFDG3US0.js → slack-event-processor-Cs_YuI3w.js} +2 -2
- package/dist/{slack-event-processor-BhCf5Wiy.d.ts → slack-event-processor-DmAlq4cV.d.ts} +0 -1
- package/dist/{slot-fields-D-pvMgTK.js → slot-fields-5ERhytIA.js} +3 -3
- package/dist/{sqlite-diagnostic-log-CwhXE9Qh.js → sqlite-diagnostic-log-BDxfXGZy.js} +42 -19
- package/dist/{yaml-render-DVEtYuF1.js → yaml-render-DxxRQT-V.js} +3 -3
- package/package.json +15 -10
- package/dist/file-system-Wvzc2ePY.js +0 -9
- package/dist/logger-B6iyNbxM.js +0 -9
- package/dist/settings-reader-9FcX3qS1.js +0 -11
- /package/dist/{connector-adapter-Dvs8N7ew.js → connector-adapter-BL2ehMej.js} +0 -0
- /package/dist/{connector-listener-mPGZYa8e.js → connector-listener-BcQMzY6Q.js} +0 -0
- /package/dist/{diagnostic-sql-reader-oXZnWFf_.js → diagnostic-sql-reader-CoBk_EFF.js} +0 -0
- /package/dist/{discord-connector-schema-B4YpWpR3.js → discord-connector-schema-saAGuPmJ.js} +0 -0
- /package/dist/{error-message-of-ColuYmAk.js → error-message-of-BG4ezZ_s.js} +0 -0
- /package/dist/{funnel-docs-CNklHvbt.d.ts → funnel-docs-BJWawfWI.d.ts} +0 -0
- /package/dist/{gh-connector-schema-CAqIhzGr.js → gh-connector-schema-DmZTrzUu.js} +0 -0
- /package/dist/{loopback-fetch-CVNuN3YZ.js → loopback-fetch-HP-bo9Kv.js} +0 -0
- /package/dist/{node-http-client-u00atiKx.js → node-http-client-SX3xFh4h.js} +0 -0
- /package/dist/{node-process-runner-DxTvycoK.js → node-process-runner-D4rzPl8Q.js} +0 -0
- /package/dist/{resolve-connector-token-DxDG9mhf.js → resolve-connector-token-BwiJCu82.js} +0 -0
- /package/dist/{schedule-connector-schema-DKEPZnVv.js → schedule-connector-schema-C2AChpwX.js} +0 -0
- /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 };
|
package/dist/channel.js
ADDED
|
@@ -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
|
|
2
|
-
import { n as
|
|
3
|
-
import {
|
|
4
|
-
import { n as
|
|
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 {
|
|
2
|
-
import { r as
|
|
3
|
-
import {
|
|
4
|
-
import { t as FunnelProfiles } from "./profiles-
|
|
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. */
|
|
39
|
-
|
|
40
|
-
|
|
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-
|
|
2
|
-
import { f as settingsSchema, s as resolveFunnelPort } from "./settings-store-
|
|
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-
|
|
4
|
-
import { t as
|
|
5
|
-
import { t as
|
|
6
|
-
import { t as
|
|
7
|
-
import { t as discordConnector } from "./discord-connector-
|
|
8
|
-
import { t as ghConnector } from "./gh-connector-
|
|
9
|
-
import { t as scheduleConnector } from "./schedule-connector-
|
|
10
|
-
import { t as slackConnector } from "./slack-connector-
|
|
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.
|
|
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.
|
|
539
|
-
|
|
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
|
};
|