@interactive-inc/claude-funnel 0.41.0 → 0.50.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 +34 -9
- package/dist/bin.js +255 -256
- 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/daemon.js +151 -152
- 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-ChomoTZ5.d.ts +3404 -0
- package/dist/index.d.ts +11 -4214
- package/dist/index.js +195 -3869
- 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 +16 -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,439 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { stderr, stdin } from "node:process";
|
|
4
|
+
//#region lib/engine/local-config/local-config-schema.ts
|
|
5
|
+
/**
|
|
6
|
+
* Per-repo launch config (`funnel.json`).
|
|
7
|
+
*
|
|
8
|
+
* `fnl claude` reads this when no global --profile preset is used. It picks one
|
|
9
|
+
* of the declared channels (`--channel <name>` selects by name; otherwise the
|
|
10
|
+
* first entry wins) and materializes its transport (connectors / delivery) into
|
|
11
|
+
* the repo's scoped settings (`~/.funnel/projects/<id>/settings.json`) on launch.
|
|
12
|
+
* Connectors carry no tokens here — a token absent from settings is prompted for
|
|
13
|
+
* at launch (TTY) and saved there, never in the repo.
|
|
14
|
+
*
|
|
15
|
+
* The launch recipe (`options` / `env` / `resume`) lives on `profiles[]`, not on
|
|
16
|
+
* the channel: a channel only describes where events come from. `fnl claude`
|
|
17
|
+
* applies the first profile bound to the chosen channel; the recipe is passed
|
|
18
|
+
* straight to the launcher and is not persisted into the global profile list.
|
|
19
|
+
* These profiles are selected by their `channel` binding, not by name.
|
|
20
|
+
*/
|
|
21
|
+
const slackConnectorSpecSchema = z.object({
|
|
22
|
+
type: z.literal("slack"),
|
|
23
|
+
name: z.string(),
|
|
24
|
+
/** Shrink raw Slack events before fanout. Defaults to true. */
|
|
25
|
+
minify: z.boolean().optional()
|
|
26
|
+
});
|
|
27
|
+
const discordConnectorSpecSchema = z.object({
|
|
28
|
+
type: z.literal("discord"),
|
|
29
|
+
name: z.string()
|
|
30
|
+
});
|
|
31
|
+
const ghConnectorSpecSchema = z.object({
|
|
32
|
+
type: z.literal("gh"),
|
|
33
|
+
name: z.string(),
|
|
34
|
+
pollInterval: z.number().int().positive().optional()
|
|
35
|
+
});
|
|
36
|
+
const scheduleConnectorSpecSchema = z.object({
|
|
37
|
+
type: z.literal("schedule"),
|
|
38
|
+
name: z.string()
|
|
39
|
+
});
|
|
40
|
+
const connectorSpecSchema = z.discriminatedUnion("type", [
|
|
41
|
+
slackConnectorSpecSchema,
|
|
42
|
+
discordConnectorSpecSchema,
|
|
43
|
+
ghConnectorSpecSchema,
|
|
44
|
+
scheduleConnectorSpecSchema
|
|
45
|
+
]);
|
|
46
|
+
const channelSpecSchema = z.object({
|
|
47
|
+
name: z.string(),
|
|
48
|
+
connectors: z.array(connectorSpecSchema).optional()
|
|
49
|
+
});
|
|
50
|
+
const profileSpecSchema = z.object({
|
|
51
|
+
/** Handle for `fnl claude --profile <name>`. A profile is only launchable by this name. */
|
|
52
|
+
name: z.string(),
|
|
53
|
+
/** Name of the channel (declared in `channels[]`) this profile binds. The profile depends on the channel, never the reverse. */
|
|
54
|
+
channel: z.string(),
|
|
55
|
+
/** Args prepended to the claude argv on every launch through this profile. */
|
|
56
|
+
options: z.array(z.string()).optional(),
|
|
57
|
+
/** Env vars layered under the launched claude process. process.env wins on collision. */
|
|
58
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
59
|
+
/**
|
|
60
|
+
* When true (the default), funnel injects `--session-id <uuid>` so that
|
|
61
|
+
* relaunching from the same cwd resumes the previous claude session
|
|
62
|
+
* without bleeding into other channels or workspaces. Set to false for
|
|
63
|
+
* profiles that should always start a fresh session.
|
|
64
|
+
*/
|
|
65
|
+
resume: z.boolean().optional()
|
|
66
|
+
});
|
|
67
|
+
const localConfigSchema = z.object({
|
|
68
|
+
$schema: z.string().optional(),
|
|
69
|
+
/**
|
|
70
|
+
* Stable per-repo identifier. funnel writes this on first launch when absent;
|
|
71
|
+
* all funnel state for this repo lives under `~/.funnel/projects/<id>/`, so the
|
|
72
|
+
* repo itself never holds settings or tokens. Committed alongside funnel.json.
|
|
73
|
+
*/
|
|
74
|
+
id: z.string().optional(),
|
|
75
|
+
/** Declared channels (transport only). First entry is the default; --channel <name> selects by name. */
|
|
76
|
+
channels: z.array(channelSpecSchema).min(1),
|
|
77
|
+
/** Launch presets bound to a channel. First entry bound to the chosen channel is the default. */
|
|
78
|
+
profiles: z.array(profileSpecSchema).optional()
|
|
79
|
+
});
|
|
80
|
+
const LOCAL_CONFIG_FILENAME = "funnel.json";
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region lib/engine/local-config/local-config.ts
|
|
83
|
+
/**
|
|
84
|
+
* Reads `funnel.json` from a directory. Returns `null` when the file is
|
|
85
|
+
* absent so callers can fall through to other resolution paths (default
|
|
86
|
+
* profile, help). Throws on present-but-invalid files so misconfiguration
|
|
87
|
+
* surfaces loudly instead of silently launching the wrong channel.
|
|
88
|
+
*/
|
|
89
|
+
var FunnelLocalConfig = class {
|
|
90
|
+
fs;
|
|
91
|
+
constructor(deps) {
|
|
92
|
+
this.fs = deps.fs;
|
|
93
|
+
Object.freeze(this);
|
|
94
|
+
}
|
|
95
|
+
read(cwd) {
|
|
96
|
+
const path = join(cwd, LOCAL_CONFIG_FILENAME);
|
|
97
|
+
if (!this.fs.existsSync(path)) return null;
|
|
98
|
+
const raw = this.fs.readFileSync(path);
|
|
99
|
+
const parsed = (() => {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(raw);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
104
|
+
throw new Error(`${LOCAL_CONFIG_FILENAME} is not valid JSON: ${message}`);
|
|
105
|
+
}
|
|
106
|
+
})();
|
|
107
|
+
const result = localConfigSchema.safeParse(parsed);
|
|
108
|
+
if (!result.success) throw new Error(`${LOCAL_CONFIG_FILENAME} is invalid: ${result.error.message}`);
|
|
109
|
+
this.assertProfilesValid(result.data);
|
|
110
|
+
return result.data;
|
|
111
|
+
}
|
|
112
|
+
assertProfilesValid(config) {
|
|
113
|
+
const profiles = config.profiles ?? [];
|
|
114
|
+
if (profiles.length === 0) return;
|
|
115
|
+
const channelNames = new Set(config.channels.map((channel) => channel.name));
|
|
116
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
117
|
+
for (const profile of profiles) {
|
|
118
|
+
if (!channelNames.has(profile.channel)) throw new Error(`${LOCAL_CONFIG_FILENAME} is invalid: profile "${profile.name}" binds channel "${profile.channel}", which is not declared in channels[]`);
|
|
119
|
+
if (seenNames.has(profile.name)) throw new Error(`${LOCAL_CONFIG_FILENAME} is invalid: more than one profile is named "${profile.name}" — names must be unique`);
|
|
120
|
+
seenNames.add(profile.name);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
//#region lib/engine/token-prompter/token-prompter.ts
|
|
126
|
+
/**
|
|
127
|
+
* Asks the user for a secret value on stdin. Used as a last resort when a
|
|
128
|
+
* funnel.json token field is absent and not present in `~/.funnel`. The Node
|
|
129
|
+
* implementation refuses to prompt when stdin is not a TTY so non-interactive
|
|
130
|
+
* launches (CI, agent spawning agent, daemons) fail fast instead of hanging.
|
|
131
|
+
*/
|
|
132
|
+
var FunnelTokenPrompter = class {};
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region lib/engine/local-config/local-config-sync.ts
|
|
135
|
+
/**
|
|
136
|
+
* Reconciles a single funnel.json channel spec with `~/.funnel/settings.json`.
|
|
137
|
+
* The spec is the source of truth for the channel it declares:
|
|
138
|
+
*
|
|
139
|
+
* - missing channel → created
|
|
140
|
+
* - declared connector matched by name → tokens reconciled
|
|
141
|
+
* - declared connector matched by token in the same channel under a
|
|
142
|
+
* different name → renamed in place (then tokens reconciled)
|
|
143
|
+
* - declared connector with no match → added
|
|
144
|
+
* - any connector left in the channel that the spec did not touch → removed
|
|
145
|
+
*
|
|
146
|
+
* Removal only fires when the channel spec has a `connectors` field. An
|
|
147
|
+
* absent field means "do not manage connectors from here" and leaves
|
|
148
|
+
* everything in `~/.funnel` alone. Other channels in funnel.json (not
|
|
149
|
+
* passed to this call) are untouched.
|
|
150
|
+
*
|
|
151
|
+
* Returns the per-connector change set so callers (e.g. the claude launcher)
|
|
152
|
+
* can drive listener hot-reload on the gateway after settings are written.
|
|
153
|
+
*/
|
|
154
|
+
var FunnelLocalConfigSync = class {
|
|
155
|
+
channels;
|
|
156
|
+
prompter;
|
|
157
|
+
constructor(deps) {
|
|
158
|
+
this.channels = deps.channels;
|
|
159
|
+
this.prompter = deps.prompter;
|
|
160
|
+
Object.freeze(this);
|
|
161
|
+
}
|
|
162
|
+
async ensure(channel) {
|
|
163
|
+
if (!this.channels.get(channel.name)) this.channels.add({ name: channel.name });
|
|
164
|
+
if (channel.connectors === void 0) return {
|
|
165
|
+
touched: [],
|
|
166
|
+
removed: []
|
|
167
|
+
};
|
|
168
|
+
const touched = [];
|
|
169
|
+
const touchedIds = /* @__PURE__ */ new Set();
|
|
170
|
+
for (const spec of channel.connectors) {
|
|
171
|
+
const outcome = await this.ensureConnector(channel.name, spec);
|
|
172
|
+
touched.push({
|
|
173
|
+
name: outcome.name,
|
|
174
|
+
changed: outcome.changed
|
|
175
|
+
});
|
|
176
|
+
touchedIds.add(outcome.id);
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
touched,
|
|
180
|
+
removed: this.removeExtras(channel.name, touchedIds)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
async ensureConnector(channelName, spec) {
|
|
184
|
+
if (spec.type === "slack") return await this.ensureSlack(channelName, spec);
|
|
185
|
+
if (spec.type === "discord") return await this.ensureDiscord(channelName, spec);
|
|
186
|
+
if (spec.type === "gh") return this.ensureGh(channelName, spec);
|
|
187
|
+
return this.ensureSchedule(channelName, spec);
|
|
188
|
+
}
|
|
189
|
+
async ensureSlack(channelName, spec) {
|
|
190
|
+
const byName = this.findExistingSlack(channelName, spec.name);
|
|
191
|
+
const bot = await this.resolveSlot({
|
|
192
|
+
label: `${spec.name}.botToken`,
|
|
193
|
+
existingLiteral: byName?.botToken,
|
|
194
|
+
existingEnv: byName?.botTokenEnv
|
|
195
|
+
});
|
|
196
|
+
const app = await this.resolveSlot({
|
|
197
|
+
label: `${spec.name}.appToken`,
|
|
198
|
+
existingLiteral: byName?.appToken,
|
|
199
|
+
existingEnv: byName?.appTokenEnv
|
|
200
|
+
});
|
|
201
|
+
const update = {
|
|
202
|
+
botToken: bot.token,
|
|
203
|
+
botTokenEnv: bot.tokenEnv,
|
|
204
|
+
appToken: app.token,
|
|
205
|
+
appTokenEnv: app.tokenEnv
|
|
206
|
+
};
|
|
207
|
+
if (byName) {
|
|
208
|
+
if (!(byName.botToken === bot.token && byName.botTokenEnv === bot.tokenEnv && byName.appToken === app.token && byName.appTokenEnv === app.tokenEnv)) {
|
|
209
|
+
this.channels.updateSlackConnector(channelName, spec.name, update);
|
|
210
|
+
return {
|
|
211
|
+
id: byName.id,
|
|
212
|
+
name: spec.name,
|
|
213
|
+
changed: true
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
id: byName.id,
|
|
218
|
+
name: spec.name,
|
|
219
|
+
changed: false
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
id: this.channels.addConnector(channelName, {
|
|
224
|
+
type: "slack",
|
|
225
|
+
name: spec.name,
|
|
226
|
+
...update,
|
|
227
|
+
...spec.minify !== void 0 ? { minify: spec.minify } : {}
|
|
228
|
+
}).id,
|
|
229
|
+
name: spec.name,
|
|
230
|
+
changed: true
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
async ensureDiscord(channelName, spec) {
|
|
234
|
+
const byName = this.findExistingDiscord(channelName, spec.name);
|
|
235
|
+
const bot = await this.resolveSlot({
|
|
236
|
+
label: `${spec.name}.botToken`,
|
|
237
|
+
existingLiteral: byName?.botToken,
|
|
238
|
+
existingEnv: byName?.botTokenEnv
|
|
239
|
+
});
|
|
240
|
+
const update = {
|
|
241
|
+
botToken: bot.token,
|
|
242
|
+
botTokenEnv: bot.tokenEnv
|
|
243
|
+
};
|
|
244
|
+
if (byName) {
|
|
245
|
+
if (byName.botToken !== bot.token || byName.botTokenEnv !== bot.tokenEnv) {
|
|
246
|
+
this.channels.updateDiscordConnector(channelName, spec.name, update);
|
|
247
|
+
return {
|
|
248
|
+
id: byName.id,
|
|
249
|
+
name: spec.name,
|
|
250
|
+
changed: true
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
id: byName.id,
|
|
255
|
+
name: spec.name,
|
|
256
|
+
changed: false
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
id: this.channels.addConnector(channelName, {
|
|
261
|
+
type: "discord",
|
|
262
|
+
name: spec.name,
|
|
263
|
+
...update
|
|
264
|
+
}).id,
|
|
265
|
+
name: spec.name,
|
|
266
|
+
changed: true
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
ensureGh(channelName, spec) {
|
|
270
|
+
const existing = this.channels.getConnector(channelName, spec.name);
|
|
271
|
+
if (existing && existing.type !== "gh") throw new Error(`connector "${spec.name}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "gh"`);
|
|
272
|
+
if (existing && existing.type === "gh") {
|
|
273
|
+
if (spec.pollInterval !== void 0 && existing.pollInterval !== spec.pollInterval) {
|
|
274
|
+
this.channels.updateGhConnector(channelName, spec.name, { pollInterval: spec.pollInterval });
|
|
275
|
+
return {
|
|
276
|
+
id: existing.id,
|
|
277
|
+
name: spec.name,
|
|
278
|
+
changed: true
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
id: existing.id,
|
|
283
|
+
name: spec.name,
|
|
284
|
+
changed: false
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
id: this.channels.addConnector(channelName, {
|
|
289
|
+
type: "gh",
|
|
290
|
+
name: spec.name,
|
|
291
|
+
...spec.pollInterval !== void 0 ? { pollInterval: spec.pollInterval } : {}
|
|
292
|
+
}).id,
|
|
293
|
+
name: spec.name,
|
|
294
|
+
changed: true
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
ensureSchedule(channelName, spec) {
|
|
298
|
+
const existing = this.channels.getConnector(channelName, spec.name);
|
|
299
|
+
if (existing && existing.type !== "schedule") throw new Error(`connector "${spec.name}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "schedule"`);
|
|
300
|
+
if (existing && existing.type === "schedule") return {
|
|
301
|
+
id: existing.id,
|
|
302
|
+
name: spec.name,
|
|
303
|
+
changed: false
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
id: this.channels.addConnector(channelName, {
|
|
307
|
+
type: "schedule",
|
|
308
|
+
name: spec.name
|
|
309
|
+
}).id,
|
|
310
|
+
name: spec.name,
|
|
311
|
+
changed: true
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
findExistingSlack(channelName, connectorName) {
|
|
315
|
+
const existing = this.channels.getConnector(channelName, connectorName);
|
|
316
|
+
if (!existing) return null;
|
|
317
|
+
if (existing.type !== "slack") throw new Error(`connector "${connectorName}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "slack"`);
|
|
318
|
+
return existing;
|
|
319
|
+
}
|
|
320
|
+
findExistingDiscord(channelName, connectorName) {
|
|
321
|
+
const existing = this.channels.getConnector(channelName, connectorName);
|
|
322
|
+
if (!existing) return null;
|
|
323
|
+
if (existing.type !== "discord") throw new Error(`connector "${connectorName}" exists in channel "${channelName}" with type "${existing.type}", funnel.json declares "discord"`);
|
|
324
|
+
return existing;
|
|
325
|
+
}
|
|
326
|
+
removeExtras(channelName, touched) {
|
|
327
|
+
const channel = this.channels.get(channelName);
|
|
328
|
+
if (!channel) return [];
|
|
329
|
+
const stale = channel.connectors.filter((c) => !touched.has(c.id));
|
|
330
|
+
for (const connector of stale) this.channels.removeConnector(channelName, connector.name);
|
|
331
|
+
return stale.map((c) => c.name);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Decides how a single token slot is stored in settings.json. funnel.json
|
|
335
|
+
* never carries tokens, so the only sources are a value already in
|
|
336
|
+
* settings.json (carried over verbatim, whichever form it was — literal or an
|
|
337
|
+
* `env`-var reference set via the CLI) or, on first sync, a TTY prompt for a
|
|
338
|
+
* literal (throws when stdin is not a TTY). Either way the secret lands in the
|
|
339
|
+
* repo-scoped settings, never in the repo itself.
|
|
340
|
+
*/
|
|
341
|
+
async resolveSlot(input) {
|
|
342
|
+
if (input.existingEnv !== void 0) return {
|
|
343
|
+
token: void 0,
|
|
344
|
+
tokenEnv: input.existingEnv
|
|
345
|
+
};
|
|
346
|
+
if (input.existingLiteral !== void 0) return {
|
|
347
|
+
token: input.existingLiteral,
|
|
348
|
+
tokenEnv: void 0
|
|
349
|
+
};
|
|
350
|
+
return {
|
|
351
|
+
token: await this.prompter.promptSecret(input.label),
|
|
352
|
+
tokenEnv: void 0
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region lib/engine/token-prompter/node-token-prompter.ts
|
|
358
|
+
const STAR = "*";
|
|
359
|
+
const CR = "\r";
|
|
360
|
+
const LF = "\n";
|
|
361
|
+
const BACKSPACE = String.fromCharCode(8);
|
|
362
|
+
const DEL = String.fromCharCode(127);
|
|
363
|
+
const CTRL_C = String.fromCharCode(3);
|
|
364
|
+
const CTRL_D = String.fromCharCode(4);
|
|
365
|
+
/**
|
|
366
|
+
* Reads a secret from stdin in raw mode. Echoes a `*` per byte so the user
|
|
367
|
+
* can see progress without exposing the token. Refuses to prompt when stdin
|
|
368
|
+
* is not a TTY — callers should surface the resulting error with a hint
|
|
369
|
+
* pointing at the corresponding env var or CLI command.
|
|
370
|
+
*/
|
|
371
|
+
var NodeFunnelTokenPrompter = class extends FunnelTokenPrompter {
|
|
372
|
+
async promptSecret(label) {
|
|
373
|
+
if (!stdin.isTTY) throw new Error(`cannot prompt for "${label}": stdin is not a TTY. Set the matching env var or run \`fnl channels <ch> connectors add ...\` first.`);
|
|
374
|
+
stderr.write(`${label}: `);
|
|
375
|
+
const wasRaw = stdin.isRaw;
|
|
376
|
+
stdin.setRawMode(true);
|
|
377
|
+
stdin.resume();
|
|
378
|
+
try {
|
|
379
|
+
return await this.readSecret();
|
|
380
|
+
} finally {
|
|
381
|
+
stdin.setRawMode(wasRaw);
|
|
382
|
+
stdin.pause();
|
|
383
|
+
stderr.write(LF);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
readSecret() {
|
|
387
|
+
return new Promise((resolve, reject) => {
|
|
388
|
+
let buffer = "";
|
|
389
|
+
const onData = (chunk) => {
|
|
390
|
+
for (const byte of chunk) {
|
|
391
|
+
const char = String.fromCharCode(byte);
|
|
392
|
+
if (char === LF || char === CR) {
|
|
393
|
+
stdin.off("data", onData);
|
|
394
|
+
resolve(buffer);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (char === CTRL_C) {
|
|
398
|
+
stdin.off("data", onData);
|
|
399
|
+
reject(/* @__PURE__ */ new Error("prompt cancelled"));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (char === CTRL_D) {
|
|
403
|
+
stdin.off("data", onData);
|
|
404
|
+
if (buffer.length === 0) reject(/* @__PURE__ */ new Error("prompt cancelled"));
|
|
405
|
+
else resolve(buffer);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (char === BACKSPACE || char === DEL) {
|
|
409
|
+
if (buffer.length > 0) {
|
|
410
|
+
buffer = buffer.slice(0, -1);
|
|
411
|
+
stderr.write("\b \b");
|
|
412
|
+
}
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
buffer += char;
|
|
416
|
+
stderr.write(STAR);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
stdin.on("data", onData);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region lib/engine/local-config/local-config-json-schema.ts
|
|
425
|
+
/**
|
|
426
|
+
* Generates the JSON Schema (draft 2020-12) for `funnel.json`. Useful for
|
|
427
|
+
* `$schema` references in committed `funnel.json` files so editors can give
|
|
428
|
+
* autocomplete and validation for channels[] (transport) and profiles[]
|
|
429
|
+
* (launch recipe) without anyone hand-maintaining a separate schema.
|
|
430
|
+
*/
|
|
431
|
+
const funnelJsonSchema = () => {
|
|
432
|
+
return {
|
|
433
|
+
...z.toJSONSchema(localConfigSchema, { target: "draft-2020-12" }),
|
|
434
|
+
title: "Funnel per-repo launch config",
|
|
435
|
+
description: "Used by `fnl claude` to declare channels (transport: connectors to materialize into ~/.funnel/settings.json on launch) and profiles (launch recipe: options / env / resume) bound to those channels."
|
|
436
|
+
};
|
|
437
|
+
};
|
|
438
|
+
//#endregion
|
|
439
|
+
export { FunnelLocalConfig as a, connectorSpecSchema as c, FunnelTokenPrompter as i, localConfigSchema as l, NodeFunnelTokenPrompter as n, LOCAL_CONFIG_FILENAME as o, FunnelLocalConfigSync as r, channelSpecSchema as s, funnelJsonSchema as t, profileSpecSchema as u };
|