@interactive-inc/claude-funnel 0.49.0 → 0.51.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/dist/bin.js +1 -1
- package/dist/claude-CB1WkV77.d.ts +115 -0
- package/dist/claude.d.ts +59 -0
- package/dist/claude.js +322 -0
- package/dist/{connector-diagnostic-log-OPpPi9V9.d.ts → connector-diagnostic-log-yTOojKUR.d.ts} +14 -14
- package/dist/{logger-Czli2OKh.js → connector-listener-DU54DN-f.js} +1 -9
- package/dist/connectors/discord.d.ts +3 -3
- package/dist/connectors/discord.js +2 -1
- package/dist/connectors/gh.d.ts +4 -3
- package/dist/connectors/gh.js +2 -1
- package/dist/connectors/schedule.d.ts +1 -1
- package/dist/connectors/schedule.js +2 -1
- package/dist/connectors/slack.d.ts +2 -2
- package/dist/connectors/slack.js +2 -1
- package/dist/discord-connector-schema-CBDyGdOI.js +21 -0
- package/dist/{discord-connector-schema-BeThExJp.js → discord-listener-_jSE3HsQ.js} +2 -22
- package/dist/file-system-BeOKXjlV.d.ts +26 -0
- package/dist/file-system-PWKKU7lA.js +9 -0
- package/dist/gateway.d.ts +3 -0
- package/dist/gateway.js +2 -0
- package/dist/gh-connector-schema-eoTtHbY6.d.ts +14 -0
- package/dist/{gh-connector-schema-eYE4g77K.js → gh-connector-schema-o3Q1-ojL.js} +1 -176
- package/dist/gh-listener-DH-fClQm.js +178 -0
- package/dist/index-BM0-f6KL.d.ts +3404 -0
- package/dist/index.d.ts +11 -4083
- package/dist/index.js +247 -3459
- package/dist/local-config-json-schema-8IHjS4Q7.js +439 -0
- package/dist/local-config-sync-BdsrDZOu.d.ts +381 -0
- package/dist/local-config.d.ts +3 -0
- package/dist/local-config.js +3 -0
- package/dist/logger-BP6SisKt.js +9 -0
- package/dist/mcp-Dr-nIBwN.js +253 -0
- package/dist/memory-connector-diagnostic-log-CrW1ltLM.js +2245 -0
- package/dist/memory-token-prompter-B5FFCsGP.d.ts +57 -0
- package/dist/memory-token-prompter-CLerGsgM.js +61 -0
- package/dist/node-file-system-BcrmWN9I.js +48 -0
- package/dist/{gh-connector-schema-CQmEWzdV.d.ts → process-runner-DfniuWVU.d.ts} +1 -14
- package/dist/profiles-f0mNmEyP.d.ts +64 -0
- package/dist/profiles-wMRnjSid.js +129 -0
- package/dist/profiles.d.ts +2 -0
- package/dist/profiles.js +2 -0
- package/dist/schedule-connector-schema-iCI61gzU.js +31 -0
- package/dist/{schedule-listener-3M6WkH1Y.d.ts → schedule-listener-CUyUFFR1.d.ts} +22 -46
- package/dist/{schedule-connector-schema-CM-sRkac.js → schedule-listener-ePAjians.js} +3 -86
- package/dist/settings-reader-BSU6JyvM.d.ts +167 -0
- package/dist/settings-reader-DPqrpV7s.js +11 -0
- package/dist/settings-store-D2XSXTyt.js +186 -0
- package/dist/slack-connector-schema-BCNWluHM.js +32 -0
- package/dist/{slack-listener-9UdAn_ui.d.ts → slack-listener-Bv5xI9gC.d.ts} +31 -31
- package/dist/{slack-connector-schema-DDbSGPZn.js → slack-listener-ClQuHhEF.js} +2 -32
- package/package.json +6 -1
- /package/dist/{connector-adapter-VA6undzc.d.ts → connector-adapter-DKgsVuMH.d.ts} +0 -0
- /package/dist/{discord-connector-schema-DF4pL3Sc.d.ts → discord-connector-schema-R0Uu-3ns.d.ts} +0 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { i as ChannelDeliveryMode, n as FunnelIdGenerator, r as ChannelConfig, t as FunnelSettingsReader } from "./settings-reader-BSU6JyvM.js";
|
|
2
|
+
import { S as FunnelLogger, b as FunnelConnectorListener, o as ConnectorDiagnosticLog } from "./connector-diagnostic-log-yTOojKUR.js";
|
|
3
|
+
import { r as FunnelProcessRunner } from "./process-runner-DfniuWVU.js";
|
|
4
|
+
import { n as FunnelFileSystem } from "./file-system-BeOKXjlV.js";
|
|
5
|
+
import { n as FunnelConnectorAdapter, t as CallInput } from "./connector-adapter-DKgsVuMH.js";
|
|
6
|
+
import { a as ScheduleEntry, n as ScheduleOnFired } from "./schedule-listener-CUyUFFR1.js";
|
|
7
|
+
import { n as SlackOnAppCreated, r as SlackPreprocessEvent } from "./slack-listener-Bv5xI9gC.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
//#region lib/engine/local-config/local-config-schema.d.ts
|
|
11
|
+
declare const connectorSpecSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
12
|
+
type: z.ZodLiteral<"slack">;
|
|
13
|
+
name: z.ZodString;
|
|
14
|
+
minify: z.ZodOptional<z.ZodBoolean>;
|
|
15
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
16
|
+
type: z.ZodLiteral<"discord">;
|
|
17
|
+
name: z.ZodString;
|
|
18
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
19
|
+
type: z.ZodLiteral<"gh">;
|
|
20
|
+
name: z.ZodString;
|
|
21
|
+
pollInterval: z.ZodOptional<z.ZodNumber>;
|
|
22
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
23
|
+
type: z.ZodLiteral<"schedule">;
|
|
24
|
+
name: z.ZodString;
|
|
25
|
+
}, z.core.$strip>], "type">;
|
|
26
|
+
type ConnectorSpec = z.infer<typeof connectorSpecSchema>;
|
|
27
|
+
declare const channelSpecSchema: z.ZodObject<{
|
|
28
|
+
name: z.ZodString;
|
|
29
|
+
connectors: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
30
|
+
type: z.ZodLiteral<"slack">;
|
|
31
|
+
name: z.ZodString;
|
|
32
|
+
minify: z.ZodOptional<z.ZodBoolean>;
|
|
33
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
34
|
+
type: z.ZodLiteral<"discord">;
|
|
35
|
+
name: z.ZodString;
|
|
36
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
37
|
+
type: z.ZodLiteral<"gh">;
|
|
38
|
+
name: z.ZodString;
|
|
39
|
+
pollInterval: z.ZodOptional<z.ZodNumber>;
|
|
40
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
41
|
+
type: z.ZodLiteral<"schedule">;
|
|
42
|
+
name: z.ZodString;
|
|
43
|
+
}, z.core.$strip>], "type">>>;
|
|
44
|
+
}, z.core.$strip>;
|
|
45
|
+
type ChannelSpec = z.infer<typeof channelSpecSchema>;
|
|
46
|
+
declare const profileSpecSchema: z.ZodObject<{
|
|
47
|
+
name: z.ZodString;
|
|
48
|
+
channel: z.ZodString;
|
|
49
|
+
options: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
50
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
51
|
+
resume: z.ZodOptional<z.ZodBoolean>;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
type ProfileSpec = z.infer<typeof profileSpecSchema>;
|
|
54
|
+
declare const localConfigSchema: z.ZodObject<{
|
|
55
|
+
$schema: z.ZodOptional<z.ZodString>;
|
|
56
|
+
id: z.ZodOptional<z.ZodString>;
|
|
57
|
+
channels: z.ZodArray<z.ZodObject<{
|
|
58
|
+
name: z.ZodString;
|
|
59
|
+
connectors: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
60
|
+
type: z.ZodLiteral<"slack">;
|
|
61
|
+
name: z.ZodString;
|
|
62
|
+
minify: z.ZodOptional<z.ZodBoolean>;
|
|
63
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
64
|
+
type: z.ZodLiteral<"discord">;
|
|
65
|
+
name: z.ZodString;
|
|
66
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
67
|
+
type: z.ZodLiteral<"gh">;
|
|
68
|
+
name: z.ZodString;
|
|
69
|
+
pollInterval: z.ZodOptional<z.ZodNumber>;
|
|
70
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
71
|
+
type: z.ZodLiteral<"schedule">;
|
|
72
|
+
name: z.ZodString;
|
|
73
|
+
}, z.core.$strip>], "type">>>;
|
|
74
|
+
}, z.core.$strip>>;
|
|
75
|
+
profiles: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
76
|
+
name: z.ZodString;
|
|
77
|
+
channel: z.ZodString;
|
|
78
|
+
options: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
79
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
80
|
+
resume: z.ZodOptional<z.ZodBoolean>;
|
|
81
|
+
}, z.core.$strip>>>;
|
|
82
|
+
}, z.core.$strip>;
|
|
83
|
+
type LocalConfig = z.infer<typeof localConfigSchema>;
|
|
84
|
+
declare const LOCAL_CONFIG_FILENAME = "funnel.json";
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region lib/engine/local-config/local-config.d.ts
|
|
87
|
+
type Deps$3 = {
|
|
88
|
+
fs: FunnelFileSystem;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Reads `funnel.json` from a directory. Returns `null` when the file is
|
|
92
|
+
* absent so callers can fall through to other resolution paths (default
|
|
93
|
+
* profile, help). Throws on present-but-invalid files so misconfiguration
|
|
94
|
+
* surfaces loudly instead of silently launching the wrong channel.
|
|
95
|
+
*/
|
|
96
|
+
declare class FunnelLocalConfig {
|
|
97
|
+
private readonly fs;
|
|
98
|
+
constructor(deps: Deps$3);
|
|
99
|
+
read(cwd: string): LocalConfig | null;
|
|
100
|
+
private assertProfilesValid;
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region lib/connectors/connector-config-schema.d.ts
|
|
104
|
+
declare const connectorConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
105
|
+
id: z.ZodString;
|
|
106
|
+
name: z.ZodString;
|
|
107
|
+
type: z.ZodLiteral<"slack">;
|
|
108
|
+
botToken: z.ZodOptional<z.ZodString>;
|
|
109
|
+
appToken: z.ZodOptional<z.ZodString>;
|
|
110
|
+
botTokenEnv: z.ZodOptional<z.ZodString>;
|
|
111
|
+
appTokenEnv: z.ZodOptional<z.ZodString>;
|
|
112
|
+
minify: z.ZodDefault<z.ZodBoolean>;
|
|
113
|
+
createdAt: z.ZodOptional<z.ZodString>;
|
|
114
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
115
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
116
|
+
id: z.ZodString;
|
|
117
|
+
name: z.ZodString;
|
|
118
|
+
type: z.ZodLiteral<"gh">;
|
|
119
|
+
pollInterval: z.ZodOptional<z.ZodNumber>;
|
|
120
|
+
createdAt: z.ZodOptional<z.ZodString>;
|
|
121
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
122
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
123
|
+
id: z.ZodString;
|
|
124
|
+
name: z.ZodString;
|
|
125
|
+
type: z.ZodLiteral<"discord">;
|
|
126
|
+
botToken: z.ZodOptional<z.ZodString>;
|
|
127
|
+
botTokenEnv: z.ZodOptional<z.ZodString>;
|
|
128
|
+
createdAt: z.ZodOptional<z.ZodString>;
|
|
129
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
130
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
131
|
+
id: z.ZodString;
|
|
132
|
+
name: z.ZodString;
|
|
133
|
+
type: z.ZodLiteral<"schedule">;
|
|
134
|
+
entries: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
135
|
+
id: z.ZodString;
|
|
136
|
+
cron: z.ZodString;
|
|
137
|
+
prompt: z.ZodString;
|
|
138
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
139
|
+
catchupPolicy: z.ZodDefault<z.ZodEnum<{
|
|
140
|
+
latest: "latest";
|
|
141
|
+
all: "all";
|
|
142
|
+
skip: "skip";
|
|
143
|
+
}>>;
|
|
144
|
+
}, z.core.$strip>>>;
|
|
145
|
+
createdAt: z.ZodOptional<z.ZodString>;
|
|
146
|
+
updatedAt: z.ZodOptional<z.ZodString>;
|
|
147
|
+
}, z.core.$strip>], "type">;
|
|
148
|
+
type ConnectorConfig = z.infer<typeof connectorConfigSchema>;
|
|
149
|
+
type ConnectorType = ConnectorConfig["type"];
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region lib/connectors/connector-factory.d.ts
|
|
152
|
+
type SlackListenerOptions = {
|
|
153
|
+
onAppCreated?: SlackOnAppCreated;
|
|
154
|
+
preprocessEvent?: SlackPreprocessEvent;
|
|
155
|
+
};
|
|
156
|
+
type ScheduleListenerOptions = {
|
|
157
|
+
onFired?: ScheduleOnFired;
|
|
158
|
+
};
|
|
159
|
+
type Deps$2 = {
|
|
160
|
+
fs?: FunnelFileSystem;
|
|
161
|
+
process?: FunnelProcessRunner;
|
|
162
|
+
logger?: FunnelLogger;
|
|
163
|
+
dir?: string; /** Diagnostic log of inbound connector traffic. Threaded into listeners that record raw/processed events. No-op when absent. */
|
|
164
|
+
diagnosticLog?: ConnectorDiagnosticLog; /** Per-listener hooks for the slack connector type. Threaded into every Slack listener built by this factory. */
|
|
165
|
+
slackListenerOptions?: SlackListenerOptions; /** Per-listener hooks for the schedule connector type. Threaded into every Schedule listener built by this factory. */
|
|
166
|
+
scheduleListenerOptions?: ScheduleListenerOptions;
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* Pure factory for per-type listeners and adapters. The factory has no CRUD
|
|
170
|
+
* responsibility — connector configs live inside settings.json under their
|
|
171
|
+
* channel, and FunnelChannels passes them in by value.
|
|
172
|
+
*
|
|
173
|
+
* `dir` is the funnel home (defaults to ~/.funnel); per-connector state files
|
|
174
|
+
* land at `<dir>/channels/<channel-id>/connectors/<connector-id>/state.json`.
|
|
175
|
+
*
|
|
176
|
+
* Host integrations can supply per-type listener hooks via
|
|
177
|
+
* `slackListenerOptions` / `scheduleListenerOptions` — e.g. to attach a
|
|
178
|
+
* Bolt `app.action` handler or to drop one-shot schedule entries on fire.
|
|
179
|
+
*/
|
|
180
|
+
declare class FunnelConnectorFactory {
|
|
181
|
+
private readonly fs;
|
|
182
|
+
private readonly process;
|
|
183
|
+
private readonly logger;
|
|
184
|
+
private readonly diagnosticLog;
|
|
185
|
+
private readonly dir;
|
|
186
|
+
private readonly slackListenerOptions;
|
|
187
|
+
private readonly scheduleListenerOptions;
|
|
188
|
+
constructor(deps?: Deps$2);
|
|
189
|
+
createListener(channelId: string, config: ConnectorConfig): FunnelConnectorListener;
|
|
190
|
+
createAdapter(config: ConnectorConfig): FunnelConnectorAdapter | null;
|
|
191
|
+
connectorDir(channelId: string, connectorId: string): string;
|
|
192
|
+
channelDir(channelId: string): string;
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region lib/engine/profiles/profile-channel-checker.d.ts
|
|
196
|
+
/**
|
|
197
|
+
* Read-side dependency that lets FunnelChannels ask whether a profile
|
|
198
|
+
* references a given channel id, without depending on FunnelProfiles directly.
|
|
199
|
+
*/
|
|
200
|
+
type ProfileChannelChecker = {
|
|
201
|
+
hasChannelRef(channelId: string): boolean;
|
|
202
|
+
};
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region lib/engine/time/clock.d.ts
|
|
205
|
+
/**
|
|
206
|
+
* Time boundary. Default NodeFunnelClock returns `new Date()`; MemoryFunnelClock
|
|
207
|
+
* is settable and `advance(ms)`-able for deterministic schedule / timeout tests.
|
|
208
|
+
*/
|
|
209
|
+
declare abstract class FunnelClock {
|
|
210
|
+
abstract now(): Date;
|
|
211
|
+
millis(): number;
|
|
212
|
+
iso(): string;
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region lib/engine/channels/channels.d.ts
|
|
216
|
+
type Deps$1 = {
|
|
217
|
+
store: FunnelSettingsReader;
|
|
218
|
+
factory: FunnelConnectorFactory;
|
|
219
|
+
profileChecker?: ProfileChannelChecker;
|
|
220
|
+
clock?: FunnelClock;
|
|
221
|
+
idGenerator?: FunnelIdGenerator;
|
|
222
|
+
};
|
|
223
|
+
type ChannelConnectorView = ConnectorConfig & {
|
|
224
|
+
channelId: string;
|
|
225
|
+
channelName: string;
|
|
226
|
+
};
|
|
227
|
+
type AddConnectorInput = {
|
|
228
|
+
type: "slack";
|
|
229
|
+
name: string;
|
|
230
|
+
botToken?: string;
|
|
231
|
+
appToken?: string;
|
|
232
|
+
botTokenEnv?: string;
|
|
233
|
+
appTokenEnv?: string;
|
|
234
|
+
minify?: boolean;
|
|
235
|
+
} | {
|
|
236
|
+
type: "gh";
|
|
237
|
+
name: string;
|
|
238
|
+
pollInterval?: number;
|
|
239
|
+
} | {
|
|
240
|
+
type: "discord";
|
|
241
|
+
name: string;
|
|
242
|
+
botToken?: string;
|
|
243
|
+
botTokenEnv?: string;
|
|
244
|
+
} | {
|
|
245
|
+
type: "schedule";
|
|
246
|
+
name: string;
|
|
247
|
+
entries?: ScheduleEntry[];
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Channels own their connectors. Each channel has a stable id (UUID); the
|
|
251
|
+
* `name` is the human-facing label used by the CLI. Connectors live nested
|
|
252
|
+
* inside `channel.connectors[]`, so add/remove/rename are channel-scoped — no
|
|
253
|
+
* global connector namespace exists. Token uniqueness is enforced across all
|
|
254
|
+
* channels at add/update time so the same Slack/Discord credentials cannot
|
|
255
|
+
* be registered twice.
|
|
256
|
+
*/
|
|
257
|
+
declare class FunnelChannels {
|
|
258
|
+
private readonly store;
|
|
259
|
+
private readonly factory;
|
|
260
|
+
private readonly profileChecker;
|
|
261
|
+
private readonly clock;
|
|
262
|
+
private readonly idGenerator;
|
|
263
|
+
constructor(deps: Deps$1);
|
|
264
|
+
list(): ChannelConfig[];
|
|
265
|
+
get(name: string): ChannelConfig | null;
|
|
266
|
+
getById(id: string): ChannelConfig | null;
|
|
267
|
+
add(input: {
|
|
268
|
+
name: string;
|
|
269
|
+
delivery?: ChannelDeliveryMode;
|
|
270
|
+
}): ChannelConfig;
|
|
271
|
+
setDelivery(name: string, delivery: ChannelDeliveryMode): void;
|
|
272
|
+
remove(name: string): void;
|
|
273
|
+
rename(oldName: string, newName: string): void;
|
|
274
|
+
listConnectors(channelName: string): ConnectorConfig[];
|
|
275
|
+
getConnector(channelName: string, connectorName: string): ConnectorConfig | null;
|
|
276
|
+
listAllConnectors(): ChannelConnectorView[];
|
|
277
|
+
addConnector(channelName: string, input: AddConnectorInput): ConnectorConfig;
|
|
278
|
+
private fromInput;
|
|
279
|
+
removeConnector(channelName: string, connectorName: string): void;
|
|
280
|
+
renameConnector(channelName: string, oldName: string, newName: string): void;
|
|
281
|
+
updateSlackConnector(channelName: string, connectorName: string, fields: {
|
|
282
|
+
botToken?: string;
|
|
283
|
+
appToken?: string;
|
|
284
|
+
botTokenEnv?: string;
|
|
285
|
+
appTokenEnv?: string;
|
|
286
|
+
}): void;
|
|
287
|
+
updateGhConnector(channelName: string, connectorName: string, fields: {
|
|
288
|
+
pollInterval?: number;
|
|
289
|
+
}): void;
|
|
290
|
+
updateDiscordConnector(channelName: string, connectorName: string, fields: {
|
|
291
|
+
botToken?: string;
|
|
292
|
+
botTokenEnv?: string;
|
|
293
|
+
}): void;
|
|
294
|
+
listScheduleEntries(channelName: string, connectorName: string): ScheduleEntry[];
|
|
295
|
+
addScheduleEntry(channelName: string, connectorName: string, entry: Pick<ScheduleEntry, "cron" | "prompt"> & Partial<Pick<ScheduleEntry, "id" | "enabled" | "catchupPolicy">>): ScheduleEntry;
|
|
296
|
+
removeScheduleEntry(channelName: string, connectorName: string, id: string): void;
|
|
297
|
+
call(channelName: string, connectorName: string, input: CallInput): Promise<unknown>;
|
|
298
|
+
createListener(channelName: string, connectorName: string): {
|
|
299
|
+
config: ConnectorConfig;
|
|
300
|
+
channelId: string;
|
|
301
|
+
listener: FunnelConnectorListener;
|
|
302
|
+
} | null;
|
|
303
|
+
createAllListeners(): {
|
|
304
|
+
config: ConnectorConfig;
|
|
305
|
+
channelId: string;
|
|
306
|
+
channelName: string;
|
|
307
|
+
listener: FunnelConnectorListener;
|
|
308
|
+
}[];
|
|
309
|
+
private requireChannel;
|
|
310
|
+
private replaceConnector;
|
|
311
|
+
private assertNoTokenCollision;
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region lib/engine/token-prompter/token-prompter.d.ts
|
|
315
|
+
/**
|
|
316
|
+
* Asks the user for a secret value on stdin. Used as a last resort when a
|
|
317
|
+
* funnel.json token field is absent and not present in `~/.funnel`. The Node
|
|
318
|
+
* implementation refuses to prompt when stdin is not a TTY so non-interactive
|
|
319
|
+
* launches (CI, agent spawning agent, daemons) fail fast instead of hanging.
|
|
320
|
+
*/
|
|
321
|
+
declare abstract class FunnelTokenPrompter {
|
|
322
|
+
abstract promptSecret(label: string): Promise<string>;
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region lib/engine/local-config/local-config-sync.d.ts
|
|
326
|
+
type Deps = {
|
|
327
|
+
channels: FunnelChannels;
|
|
328
|
+
prompter: FunnelTokenPrompter;
|
|
329
|
+
};
|
|
330
|
+
type ConnectorSyncOutcome = {
|
|
331
|
+
name: string;
|
|
332
|
+
changed: boolean;
|
|
333
|
+
};
|
|
334
|
+
type LocalConfigSyncResult = {
|
|
335
|
+
touched: ConnectorSyncOutcome[];
|
|
336
|
+
removed: string[];
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* Reconciles a single funnel.json channel spec with `~/.funnel/settings.json`.
|
|
340
|
+
* The spec is the source of truth for the channel it declares:
|
|
341
|
+
*
|
|
342
|
+
* - missing channel → created
|
|
343
|
+
* - declared connector matched by name → tokens reconciled
|
|
344
|
+
* - declared connector matched by token in the same channel under a
|
|
345
|
+
* different name → renamed in place (then tokens reconciled)
|
|
346
|
+
* - declared connector with no match → added
|
|
347
|
+
* - any connector left in the channel that the spec did not touch → removed
|
|
348
|
+
*
|
|
349
|
+
* Removal only fires when the channel spec has a `connectors` field. An
|
|
350
|
+
* absent field means "do not manage connectors from here" and leaves
|
|
351
|
+
* everything in `~/.funnel` alone. Other channels in funnel.json (not
|
|
352
|
+
* passed to this call) are untouched.
|
|
353
|
+
*
|
|
354
|
+
* Returns the per-connector change set so callers (e.g. the claude launcher)
|
|
355
|
+
* can drive listener hot-reload on the gateway after settings are written.
|
|
356
|
+
*/
|
|
357
|
+
declare class FunnelLocalConfigSync {
|
|
358
|
+
private readonly channels;
|
|
359
|
+
private readonly prompter;
|
|
360
|
+
constructor(deps: Deps);
|
|
361
|
+
ensure(channel: ChannelSpec): Promise<LocalConfigSyncResult>;
|
|
362
|
+
private ensureConnector;
|
|
363
|
+
private ensureSlack;
|
|
364
|
+
private ensureDiscord;
|
|
365
|
+
private ensureGh;
|
|
366
|
+
private ensureSchedule;
|
|
367
|
+
private findExistingSlack;
|
|
368
|
+
private findExistingDiscord;
|
|
369
|
+
private removeExtras;
|
|
370
|
+
/**
|
|
371
|
+
* Decides how a single token slot is stored in settings.json. funnel.json
|
|
372
|
+
* never carries tokens, so the only sources are a value already in
|
|
373
|
+
* settings.json (carried over verbatim, whichever form it was — literal or an
|
|
374
|
+
* `env`-var reference set via the CLI) or, on first sync, a TTY prompt for a
|
|
375
|
+
* literal (throws when stdin is not a TTY). Either way the secret lands in the
|
|
376
|
+
* repo-scoped settings, never in the repo itself.
|
|
377
|
+
*/
|
|
378
|
+
private resolveSlot;
|
|
379
|
+
}
|
|
380
|
+
//#endregion
|
|
381
|
+
export { profileSpecSchema as C, localConfigSchema as S, LOCAL_CONFIG_FILENAME as _, ChannelConnectorView as a, channelSpecSchema as b, FunnelConnectorFactory as c, ConnectorConfig as d, ConnectorType as f, ConnectorSpec as g, ChannelSpec as h, FunnelTokenPrompter as i, ScheduleListenerOptions as l, FunnelLocalConfig as m, FunnelLocalConfigSync as n, FunnelChannels as o, connectorConfigSchema as p, LocalConfigSyncResult as r, FunnelClock as s, ConnectorSyncOutcome as t, SlackListenerOptions as u, LocalConfig as v, connectorSpecSchema as x, ProfileSpec as y };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { C as profileSpecSchema, S as localConfigSchema, _ as LOCAL_CONFIG_FILENAME, b as channelSpecSchema, g as ConnectorSpec, h as ChannelSpec, i as FunnelTokenPrompter, m as FunnelLocalConfig, n as FunnelLocalConfigSync, r as LocalConfigSyncResult, t as ConnectorSyncOutcome, v as LocalConfig, x as connectorSpecSchema, y as ProfileSpec } from "./local-config-sync-BdsrDZOu.js";
|
|
2
|
+
import { i as funnelJsonSchema, n as NodeFunnelTokenPrompter, r as FunnelLocalConfigWriter, t as MemoryFunnelTokenPrompter } from "./memory-token-prompter-B5FFCsGP.js";
|
|
3
|
+
export { ChannelSpec, ConnectorSpec, ConnectorSyncOutcome, FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, LocalConfig, LocalConfigSyncResult, MemoryFunnelTokenPrompter, NodeFunnelTokenPrompter, ProfileSpec, channelSpecSchema, connectorSpecSchema, funnelJsonSchema, localConfigSchema, profileSpecSchema };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as FunnelLocalConfig, c as connectorSpecSchema, i as FunnelTokenPrompter, l as localConfigSchema, n as NodeFunnelTokenPrompter, o as LOCAL_CONFIG_FILENAME, r as FunnelLocalConfigSync, s as channelSpecSchema, t as funnelJsonSchema, u as profileSpecSchema } from "./local-config-json-schema-8IHjS4Q7.js";
|
|
2
|
+
import { n as FunnelLocalConfigWriter, t as MemoryFunnelTokenPrompter } from "./memory-token-prompter-CLerGsgM.js";
|
|
3
|
+
export { FunnelLocalConfig, FunnelLocalConfigSync, FunnelLocalConfigWriter, FunnelTokenPrompter, LOCAL_CONFIG_FILENAME, MemoryFunnelTokenPrompter, NodeFunnelTokenPrompter, channelSpecSchema, connectorSpecSchema, funnelJsonSchema, localConfigSchema, profileSpecSchema };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region lib/engine/logger/logger.ts
|
|
2
|
+
/**
|
|
3
|
+
* Structured logger with three levels and an optional log-file path.
|
|
4
|
+
* Defaults to NodeFunnelLogger (appends to `<os.tmpdir()>/funnel/funnel.log`);
|
|
5
|
+
* MemoryFunnelLogger captures entries in memory and NoopFunnelLogger silences output.
|
|
6
|
+
*/
|
|
7
|
+
var FunnelLogger = class {};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { FunnelLogger as t };
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { n as NodeFunnelProcessRunner } from "./gh-connector-schema-o3Q1-ojL.js";
|
|
2
|
+
import { t as NodeFunnelFileSystem } from "./node-file-system-BcrmWN9I.js";
|
|
3
|
+
import { n as FUNNEL_DIR, o as resolveFunnelPort, p as NodeFunnelIdGenerator } from "./settings-store-D2XSXTyt.js";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region lib/engine/claude/claude.ts
|
|
7
|
+
const defaultProcess$1 = new NodeFunnelProcessRunner();
|
|
8
|
+
const defaultIdGenerator = new NodeFunnelIdGenerator();
|
|
9
|
+
/**
|
|
10
|
+
* Launches Claude Code with funnel pre-wired: ensures the gateway is running,
|
|
11
|
+
* installs the funnel MCP into the target repo's `.mcp.json` if missing,
|
|
12
|
+
* injects `FUNNEL_CHANNEL_ID` into the child env, and delegates singleton
|
|
13
|
+
* enforcement to a ProcessGuard.
|
|
14
|
+
*/
|
|
15
|
+
var FunnelClaude = class {
|
|
16
|
+
channels;
|
|
17
|
+
mcp;
|
|
18
|
+
gateway;
|
|
19
|
+
sessions;
|
|
20
|
+
guard;
|
|
21
|
+
process;
|
|
22
|
+
idGenerator;
|
|
23
|
+
logger;
|
|
24
|
+
constructor(deps) {
|
|
25
|
+
this.channels = deps.channels;
|
|
26
|
+
this.mcp = deps.mcp;
|
|
27
|
+
this.gateway = deps.gateway;
|
|
28
|
+
this.sessions = deps.sessions;
|
|
29
|
+
this.guard = deps.guard;
|
|
30
|
+
this.process = deps.process ?? defaultProcess$1;
|
|
31
|
+
this.idGenerator = deps.idGenerator ?? defaultIdGenerator;
|
|
32
|
+
this.logger = deps.logger;
|
|
33
|
+
Object.freeze(this);
|
|
34
|
+
}
|
|
35
|
+
async launch(options) {
|
|
36
|
+
const channel = this.channels.get(options.channel) ?? this.channels.getById(options.channel);
|
|
37
|
+
if (!channel) throw new Error(`channel "${options.channel}" not found`);
|
|
38
|
+
if (options.profileId && this.guard.isRunning(options.profileId)) throw new Error(`profile "${options.profileId}" is already running`);
|
|
39
|
+
const cwd = options.cwd ?? globalThis.process.cwd();
|
|
40
|
+
if ((options.installMcp ?? true) && !this.mcp.findInstalledName(cwd)) {
|
|
41
|
+
this.mcp.install(cwd);
|
|
42
|
+
this.logger?.info(`added funnel MCP to .mcp.json`, { cwd });
|
|
43
|
+
}
|
|
44
|
+
if (!this.gateway.isRunning()) {
|
|
45
|
+
this.logger?.info(`starting gateway automatically`);
|
|
46
|
+
await this.gateway.start();
|
|
47
|
+
}
|
|
48
|
+
if (options.profileId) this.guard.acquire(options.profileId);
|
|
49
|
+
const session = (options.resume ?? false) && options.profileId ? this.resolveSession(options.profileId, cwd, options.userArgs ?? [], options.env ?? {}) : null;
|
|
50
|
+
const claudeArgs = this.buildArgs(options.options ?? [], options.userArgs ?? [], cwd, session);
|
|
51
|
+
const env = this.buildEnv(channel.id, options.env ?? {});
|
|
52
|
+
this.logger?.info(`claude launch`, {
|
|
53
|
+
channel: options.channel,
|
|
54
|
+
channelId: channel.id,
|
|
55
|
+
cwd
|
|
56
|
+
});
|
|
57
|
+
try {
|
|
58
|
+
return await this.process.attach(["claude", ...claudeArgs], {
|
|
59
|
+
cwd,
|
|
60
|
+
env,
|
|
61
|
+
onSpawned: options.onSpawned
|
|
62
|
+
});
|
|
63
|
+
} finally {
|
|
64
|
+
if (options.profileId) this.guard.release(options.profileId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
buildArgs(recipeOptions, userArgs, cwd, session) {
|
|
68
|
+
const result = [...recipeOptions, ...userArgs];
|
|
69
|
+
if (session !== null) if (session.mode === "resume") result.push("--resume", session.id);
|
|
70
|
+
else result.push("--session-id", session.id);
|
|
71
|
+
const mcpName = this.mcp.findInstalledName(cwd);
|
|
72
|
+
if (mcpName && !result.includes("--dangerously-load-development-channels") && !result.includes("--channels")) result.push("--dangerously-load-development-channels", `server:${mcpName}`);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Decides whether funnel should resume an existing claude session or start
|
|
77
|
+
* a freshly minted one. Backs off when the user already passed a
|
|
78
|
+
* session-shaping flag, since combining them would either confuse claude
|
|
79
|
+
* or override the explicit user intent.
|
|
80
|
+
*
|
|
81
|
+
* The session is owned by the profile (by id), not by cwd: two profiles
|
|
82
|
+
* pointing at the same repo each keep their own conversation, and a launch
|
|
83
|
+
* with no profile never resumes — so an unrelated session in the same repo
|
|
84
|
+
* can't bleed in. The channel never enters into it; sessions belong to the
|
|
85
|
+
* launch layer (profiles), keeping the transport layer ignorant of them.
|
|
86
|
+
*
|
|
87
|
+
* A persisted id is only resumed when its session jsonl still exists on
|
|
88
|
+
* disk. claude errors out on `--resume <id>` for a missing conversation, and
|
|
89
|
+
* a persisted id can outlive its jsonl (claude pruned it, or the very first
|
|
90
|
+
* launch was aborted after the id was written but before the jsonl
|
|
91
|
+
* appeared). When the file is gone we mint a fresh session instead, which
|
|
92
|
+
* overwrites the dangling entry — so the store self-heals.
|
|
93
|
+
*/
|
|
94
|
+
resolveSession(profileId, cwd, userArgs, recipeEnv) {
|
|
95
|
+
for (const arg of userArgs) {
|
|
96
|
+
if (arg === "-c" || arg === "--continue") return null;
|
|
97
|
+
if (arg === "--resume" || arg.startsWith("--resume=")) return null;
|
|
98
|
+
if (arg === "--session-id" || arg.startsWith("--session-id=")) return null;
|
|
99
|
+
}
|
|
100
|
+
const existing = this.sessions.getSessionId(profileId);
|
|
101
|
+
if (existing !== null && this.sessions.sessionFileExists(cwd, existing, recipeEnv)) return {
|
|
102
|
+
id: existing,
|
|
103
|
+
mode: "resume"
|
|
104
|
+
};
|
|
105
|
+
const fresh = this.idGenerator.generate();
|
|
106
|
+
this.sessions.setSessionId(profileId, fresh);
|
|
107
|
+
return {
|
|
108
|
+
id: fresh,
|
|
109
|
+
mode: "new"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
buildEnv(channelId, recipeEnv) {
|
|
113
|
+
const env = {};
|
|
114
|
+
for (const [key, value] of Object.entries(recipeEnv)) env[key] = value;
|
|
115
|
+
for (const [key, value] of Object.entries(globalThis.process.env)) if (typeof value === "string") env[key] = value;
|
|
116
|
+
env.FUNNEL_CHANNEL_ID = channelId;
|
|
117
|
+
env.FUNNEL_PORT = String(resolveFunnelPort());
|
|
118
|
+
return env;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region lib/engine/claude/file-process-guard.ts
|
|
123
|
+
const defaultFs$1 = new NodeFunnelFileSystem();
|
|
124
|
+
const defaultProcess = new NodeFunnelProcessRunner();
|
|
125
|
+
var FileProcessGuard = class {
|
|
126
|
+
fs;
|
|
127
|
+
process;
|
|
128
|
+
pidDir;
|
|
129
|
+
constructor(deps = {}) {
|
|
130
|
+
this.fs = deps.fs ?? defaultFs$1;
|
|
131
|
+
this.process = deps.process ?? defaultProcess;
|
|
132
|
+
this.pidDir = join(deps.dir ?? FUNNEL_DIR, "claude");
|
|
133
|
+
Object.freeze(this);
|
|
134
|
+
}
|
|
135
|
+
isRunning(profileId) {
|
|
136
|
+
const pid = this.readPid(profileId);
|
|
137
|
+
if (!pid) return false;
|
|
138
|
+
return this.process.isAlive(pid);
|
|
139
|
+
}
|
|
140
|
+
acquire(profileId) {
|
|
141
|
+
this.fs.mkdirSync(this.pidDir, { recursive: true });
|
|
142
|
+
this.fs.writeFileSync(this.pidPath(profileId), String(globalThis.process.pid));
|
|
143
|
+
globalThis.process.once("exit", () => this.release(profileId));
|
|
144
|
+
}
|
|
145
|
+
release(profileId) {
|
|
146
|
+
const path = this.pidPath(profileId);
|
|
147
|
+
if (this.fs.existsSync(path)) this.fs.unlink(path);
|
|
148
|
+
}
|
|
149
|
+
pidPath(profileId) {
|
|
150
|
+
return join(this.pidDir, `${profileId}.pid`);
|
|
151
|
+
}
|
|
152
|
+
readPid(profileId) {
|
|
153
|
+
const path = this.pidPath(profileId);
|
|
154
|
+
if (!this.fs.existsSync(path)) return null;
|
|
155
|
+
try {
|
|
156
|
+
const content = this.fs.readFileSync(path).trim();
|
|
157
|
+
const pid = Number(content);
|
|
158
|
+
if (!pid || pid <= 0) return null;
|
|
159
|
+
return pid;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region lib/engine/mcp/mcp.ts
|
|
167
|
+
const FUNNEL_MCP_COMMAND = "bun";
|
|
168
|
+
const FUNNEL_MCP_ARGS = ["funnel", "mcp"];
|
|
169
|
+
const FUNNEL_MCP_NAME = "funnel";
|
|
170
|
+
const mcpEntrySchema = z.object({
|
|
171
|
+
command: z.string().optional(),
|
|
172
|
+
args: z.array(z.string()).optional()
|
|
173
|
+
});
|
|
174
|
+
const mcpConfigSchema = z.object({ mcpServers: z.record(z.string(), mcpEntrySchema).optional() });
|
|
175
|
+
const defaultFs = new NodeFunnelFileSystem();
|
|
176
|
+
/**
|
|
177
|
+
* Installs/uninstalls the funnel MCP entry into a target repository's
|
|
178
|
+
* `.mcp.json`. Detects an existing entry by command match so renaming is
|
|
179
|
+
* preserved across re-installs.
|
|
180
|
+
*/
|
|
181
|
+
var FunnelMcp = class {
|
|
182
|
+
fs;
|
|
183
|
+
constructor(deps = {}) {
|
|
184
|
+
this.fs = deps.fs ?? defaultFs;
|
|
185
|
+
Object.freeze(this);
|
|
186
|
+
}
|
|
187
|
+
install(repoPath) {
|
|
188
|
+
if (!this.fs.existsSync(repoPath)) throw new Error(`repository does not exist: ${repoPath}`);
|
|
189
|
+
const config = this.readConfig(repoPath);
|
|
190
|
+
const servers = config.mcpServers ?? {};
|
|
191
|
+
const targetName = this.findServerName(servers) ?? "funnel";
|
|
192
|
+
servers[targetName] = {
|
|
193
|
+
command: "bun",
|
|
194
|
+
args: FUNNEL_MCP_ARGS
|
|
195
|
+
};
|
|
196
|
+
this.writeConfig(repoPath, {
|
|
197
|
+
...config,
|
|
198
|
+
mcpServers: servers
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
uninstall(repoPath) {
|
|
202
|
+
if (!this.fs.existsSync(repoPath)) return;
|
|
203
|
+
const config = this.readConfig(repoPath);
|
|
204
|
+
const servers = config.mcpServers ?? {};
|
|
205
|
+
const name = this.findServerName(servers);
|
|
206
|
+
if (!name) return;
|
|
207
|
+
const next = { ...servers };
|
|
208
|
+
delete next[name];
|
|
209
|
+
this.writeConfig(repoPath, {
|
|
210
|
+
...config,
|
|
211
|
+
mcpServers: next
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
findInstalledName(cwd) {
|
|
215
|
+
const config = this.readConfig(cwd);
|
|
216
|
+
return this.findServerName(config.mcpServers ?? {});
|
|
217
|
+
}
|
|
218
|
+
findServerName(servers) {
|
|
219
|
+
for (const entry of Object.entries(servers)) {
|
|
220
|
+
const name = entry[0];
|
|
221
|
+
const value = entry[1];
|
|
222
|
+
if (this.isFunnelEntry(value)) return name;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
isFunnelEntry(value) {
|
|
227
|
+
if (!value) return false;
|
|
228
|
+
if (value.command === "bun" && value.args?.[0] === "funnel") return true;
|
|
229
|
+
if (value.command === "funnel") return true;
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
readConfig(repoPath) {
|
|
233
|
+
const mcpPath = join(repoPath, ".mcp.json");
|
|
234
|
+
if (!this.fs.existsSync(mcpPath)) return {};
|
|
235
|
+
const content = this.fs.readFileSync(mcpPath).trim();
|
|
236
|
+
if (!content) return {};
|
|
237
|
+
let parsed;
|
|
238
|
+
try {
|
|
239
|
+
parsed = JSON.parse(content);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
throw new Error(`invalid .mcp.json (${mcpPath}): ${error instanceof Error ? error.message : String(error)}`);
|
|
242
|
+
}
|
|
243
|
+
const result = mcpConfigSchema.safeParse(parsed);
|
|
244
|
+
if (!result.success) throw new Error(`invalid .mcp.json (${mcpPath}): ${result.error.message}`);
|
|
245
|
+
return result.data;
|
|
246
|
+
}
|
|
247
|
+
writeConfig(repoPath, config) {
|
|
248
|
+
const mcpPath = join(repoPath, ".mcp.json");
|
|
249
|
+
this.fs.writeFileSync(mcpPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
//#endregion
|
|
253
|
+
export { FileProcessGuard as a, FunnelMcp as i, FUNNEL_MCP_COMMAND as n, FunnelClaude as o, FUNNEL_MCP_NAME as r, FUNNEL_MCP_ARGS as t };
|