@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.
Files changed (53) hide show
  1. package/dist/bin.js +1 -1
  2. package/dist/claude-CB1WkV77.d.ts +115 -0
  3. package/dist/claude.d.ts +59 -0
  4. package/dist/claude.js +322 -0
  5. package/dist/{connector-diagnostic-log-OPpPi9V9.d.ts → connector-diagnostic-log-yTOojKUR.d.ts} +14 -14
  6. package/dist/{logger-Czli2OKh.js → connector-listener-DU54DN-f.js} +1 -9
  7. package/dist/connectors/discord.d.ts +3 -3
  8. package/dist/connectors/discord.js +2 -1
  9. package/dist/connectors/gh.d.ts +4 -3
  10. package/dist/connectors/gh.js +2 -1
  11. package/dist/connectors/schedule.d.ts +1 -1
  12. package/dist/connectors/schedule.js +2 -1
  13. package/dist/connectors/slack.d.ts +2 -2
  14. package/dist/connectors/slack.js +2 -1
  15. package/dist/discord-connector-schema-CBDyGdOI.js +21 -0
  16. package/dist/{discord-connector-schema-BeThExJp.js → discord-listener-_jSE3HsQ.js} +2 -22
  17. package/dist/file-system-BeOKXjlV.d.ts +26 -0
  18. package/dist/file-system-PWKKU7lA.js +9 -0
  19. package/dist/gateway.d.ts +3 -0
  20. package/dist/gateway.js +2 -0
  21. package/dist/gh-connector-schema-eoTtHbY6.d.ts +14 -0
  22. package/dist/{gh-connector-schema-eYE4g77K.js → gh-connector-schema-o3Q1-ojL.js} +1 -176
  23. package/dist/gh-listener-DH-fClQm.js +178 -0
  24. package/dist/index-BM0-f6KL.d.ts +3404 -0
  25. package/dist/index.d.ts +11 -4083
  26. package/dist/index.js +247 -3459
  27. package/dist/local-config-json-schema-8IHjS4Q7.js +439 -0
  28. package/dist/local-config-sync-BdsrDZOu.d.ts +381 -0
  29. package/dist/local-config.d.ts +3 -0
  30. package/dist/local-config.js +3 -0
  31. package/dist/logger-BP6SisKt.js +9 -0
  32. package/dist/mcp-Dr-nIBwN.js +253 -0
  33. package/dist/memory-connector-diagnostic-log-CrW1ltLM.js +2245 -0
  34. package/dist/memory-token-prompter-B5FFCsGP.d.ts +57 -0
  35. package/dist/memory-token-prompter-CLerGsgM.js +61 -0
  36. package/dist/node-file-system-BcrmWN9I.js +48 -0
  37. package/dist/{gh-connector-schema-CQmEWzdV.d.ts → process-runner-DfniuWVU.d.ts} +1 -14
  38. package/dist/profiles-f0mNmEyP.d.ts +64 -0
  39. package/dist/profiles-wMRnjSid.js +129 -0
  40. package/dist/profiles.d.ts +2 -0
  41. package/dist/profiles.js +2 -0
  42. package/dist/schedule-connector-schema-iCI61gzU.js +31 -0
  43. package/dist/{schedule-listener-3M6WkH1Y.d.ts → schedule-listener-CUyUFFR1.d.ts} +22 -46
  44. package/dist/{schedule-connector-schema-CM-sRkac.js → schedule-listener-ePAjians.js} +3 -86
  45. package/dist/settings-reader-BSU6JyvM.d.ts +167 -0
  46. package/dist/settings-reader-DPqrpV7s.js +11 -0
  47. package/dist/settings-store-D2XSXTyt.js +186 -0
  48. package/dist/slack-connector-schema-BCNWluHM.js +32 -0
  49. package/dist/{slack-listener-9UdAn_ui.d.ts → slack-listener-Bv5xI9gC.d.ts} +31 -31
  50. package/dist/{slack-connector-schema-DDbSGPZn.js → slack-listener-ClQuHhEF.js} +2 -32
  51. package/package.json +6 -1
  52. /package/dist/{connector-adapter-VA6undzc.d.ts → connector-adapter-DKgsVuMH.d.ts} +0 -0
  53. /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 };