@vornrun/connector-sdk 0.7.0-beta.8 → 0.7.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.
@@ -0,0 +1,3511 @@
1
+ // src/loopback.ts
2
+ var LOOPBACK_HOSTS = ["127.0.0.1", "localhost", "[::1]"];
3
+ function loopbackEndpoint(env, names, fail = (message) => new Error(message)) {
4
+ const url = env[names.urlVar]?.trim();
5
+ const token = env[names.tokenVar]?.trim();
6
+ if (!url || !token) throw fail(names.missing);
7
+ let parsed;
8
+ try {
9
+ parsed = new URL(url);
10
+ } catch {
11
+ throw fail(`${names.urlVar} is ${JSON.stringify(url)}, which is not a URL`);
12
+ }
13
+ if (parsed.protocol !== "http:" || !LOOPBACK_HOSTS.includes(parsed.hostname)) {
14
+ throw fail(
15
+ `${names.urlVar} is ${JSON.stringify(url)}; ${names.served}, over http on ${LOOPBACK_HOSTS.join(", ")}`
16
+ );
17
+ }
18
+ return { url: url.replace(/\/$/, ""), token };
19
+ }
20
+
21
+ // src/session.ts
22
+ var BROWSER_HOST_ENV = "VORN_BROWSER_HOST";
23
+ var BROWSER_TOKEN_ENV = "VORN_BROWSER_TOKEN";
24
+ var SESSION_CALL_META = "vorn/sessionCall";
25
+ var SESSION_CALL_HEADER = "x-vorn-session-call";
26
+ var SessionUnavailableError = class extends Error {
27
+ /** Asking again cannot bring the window back, so the SDK's retries let this through at once. */
28
+ retryable = false;
29
+ constructor(message) {
30
+ super(message);
31
+ this.name = "SessionUnavailableError";
32
+ }
33
+ };
34
+ var SessionRefusedError = class extends Error {
35
+ retryable = false;
36
+ constructor(message) {
37
+ super(message);
38
+ this.name = "SessionRefusedError";
39
+ }
40
+ };
41
+ var SESSION_TIMEOUT_MS = 45e3;
42
+ var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([204, 205, 304]);
43
+ function readReply(text) {
44
+ let parsed;
45
+ try {
46
+ parsed = JSON.parse(text);
47
+ } catch {
48
+ throw new Error("The signed-in window answered with a body that is not JSON");
49
+ }
50
+ const reply2 = parsed;
51
+ if (!parsed || typeof parsed !== "object" || typeof reply2.status !== "number") {
52
+ throw new Error("The signed-in window answered without a status");
53
+ }
54
+ return reply2;
55
+ }
56
+ var refusal = (text) => text.trim() || void 0;
57
+ function createSessionFetch(options = {}) {
58
+ const env = options.env ?? process.env;
59
+ const call = options.fetchImpl ?? fetch;
60
+ return (async (input, init) => {
61
+ const { url, token } = loopbackEndpoint(
62
+ env,
63
+ {
64
+ urlVar: BROWSER_HOST_ENV,
65
+ tokenVar: BROWSER_TOKEN_ENV,
66
+ missing: `This connector acts through a signed-in Vorn window; run it from Vorn, which sets ${BROWSER_HOST_ENV}`,
67
+ served: "the endpoint is served on this machine"
68
+ },
69
+ (message) => new SessionUnavailableError(message)
70
+ );
71
+ const request = new Request(input, init);
72
+ const body = request.body ? await request.text() : void 0;
73
+ const answer = await call(`${url}/fetch`, {
74
+ method: "POST",
75
+ headers: {
76
+ authorization: `Bearer ${token}`,
77
+ "content-type": "application/json",
78
+ ...options.call && { [SESSION_CALL_HEADER]: options.call }
79
+ },
80
+ body: JSON.stringify({
81
+ url: request.url,
82
+ method: request.method,
83
+ headers: Object.fromEntries(request.headers),
84
+ ...body !== void 0 && { body }
85
+ }),
86
+ signal: AbortSignal.any([request.signal, AbortSignal.timeout(SESSION_TIMEOUT_MS)])
87
+ });
88
+ const text = await answer.text();
89
+ if (answer.status === 503) {
90
+ throw new SessionUnavailableError(
91
+ refusal(text) ?? "Vorn could not reach the signed-in window"
92
+ );
93
+ }
94
+ if (!answer.ok) {
95
+ throw new SessionRefusedError(
96
+ refusal(text) ?? `The signed-in window refused the call with HTTP ${answer.status}`
97
+ );
98
+ }
99
+ const reply2 = readReply(text);
100
+ return new Response(NULL_BODY_STATUSES.has(reply2.status) ? null : reply2.body ?? "", {
101
+ status: reply2.status,
102
+ ...reply2.headers && { headers: reply2.headers }
103
+ });
104
+ });
105
+ }
106
+
107
+ // src/origins.ts
108
+ var ORIGIN_PATTERN = /^https:\/\/(\*\.)?[a-z0-9-]+(\.[a-z0-9-]+)+$/i;
109
+ function withinOrigins(origins, url) {
110
+ let parsed;
111
+ try {
112
+ parsed = new URL(url);
113
+ } catch {
114
+ return false;
115
+ }
116
+ if (parsed.protocol !== "https:" || parsed.port !== "") return false;
117
+ const target = parsed.hostname.toLowerCase();
118
+ return origins.some((origin) => {
119
+ if (!ORIGIN_PATTERN.test(origin)) return false;
120
+ const wildcard = origin.startsWith("https://*.");
121
+ const host = origin.slice(wildcard ? "https://*.".length : "https://".length).toLowerCase();
122
+ return wildcard ? target.endsWith(`.${host}`) : target === host;
123
+ });
124
+ }
125
+
126
+ // src/define.ts
127
+ var KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
128
+ var PATH_DATA_PATTERN = /^[MmZzLlHhVvCcSsQqTtAa0-9\s,.\-+eE]+$/;
129
+ var VIEW_BOX_PATTERN = /^-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+$/;
130
+ var DEDUPE_STRATEGIES = ["timestamp", "lastItem"];
131
+ var AUTH_RUNGS = ["none", "cli", "key", "browser", "oauth"];
132
+ var EXTENSION_PERMISSIONS = [
133
+ "git.read",
134
+ "terminal.read",
135
+ "terminal.selection",
136
+ "terminal.send",
137
+ "card.rename",
138
+ "agent.usage"
139
+ ];
140
+ var HOST_PERMISSIONS = {
141
+ diff: "git.read",
142
+ status: "git.read",
143
+ output: "terminal.read",
144
+ selection: "terminal.selection",
145
+ send: "terminal.send",
146
+ rename: "card.rename",
147
+ usage: "agent.usage"
148
+ };
149
+ var EXTENSION_AGENTS = [
150
+ "claude",
151
+ "copilot",
152
+ "codex",
153
+ "opencode",
154
+ "gemini",
155
+ "shell"
156
+ ];
157
+ var EXTENSION_PLATFORMS = ["darwin", "linux", "win32"];
158
+ var WEB_ENTRY_PATTERN = /^web\/[A-Za-z0-9._/-]+\.html$/;
159
+ var MIN_FOOTER_SECONDS = 5;
160
+ var MAX_PATTERN_LENGTH = 256;
161
+ var GROUP_OPEN = /^\((\?(:|=|!|<=|<!|<[A-Za-z_$][\w$]*>))?/;
162
+ function hasNestedQuantifier(pattern) {
163
+ const quantifierAt = (at) => {
164
+ const ch = pattern[at];
165
+ return ch !== void 0 && "*+?{".includes(ch);
166
+ };
167
+ const quantified = [];
168
+ let inClass = false;
169
+ for (let i = 0; i < pattern.length; i++) {
170
+ const ch = pattern[i];
171
+ if (ch === "\\") {
172
+ i++;
173
+ continue;
174
+ }
175
+ if (inClass) {
176
+ if (ch === "]") inClass = false;
177
+ continue;
178
+ }
179
+ if (ch === "[") {
180
+ inClass = true;
181
+ continue;
182
+ }
183
+ if (ch === "(") {
184
+ quantified.push(false);
185
+ i += (GROUP_OPEN.exec(pattern.slice(i))?.[0].length ?? 1) - 1;
186
+ continue;
187
+ }
188
+ if (ch === ")") {
189
+ const heldOne = quantified.pop() ?? false;
190
+ const repeated = quantifierAt(i + 1);
191
+ if (heldOne && repeated) return true;
192
+ if ((heldOne || repeated) && quantified.length > 0) {
193
+ quantified[quantified.length - 1] = true;
194
+ }
195
+ continue;
196
+ }
197
+ if (quantifierAt(i) && quantified.length > 0) quantified[quantified.length - 1] = true;
198
+ }
199
+ return false;
200
+ }
201
+ var ABSOLUTE_URL_PATTERN = /^https?:\/\//i;
202
+ var CONFIG_ROOTED_URL_PATTERN = /^\{\{\s*config\./;
203
+ function assertUnique(kind, keys) {
204
+ const seen = /* @__PURE__ */ new Set();
205
+ for (const key of keys) {
206
+ if (seen.has(key)) throw new Error(`Duplicate ${kind} "${key}"`);
207
+ seen.add(key);
208
+ }
209
+ }
210
+ function assertAuth(definition) {
211
+ const auth = definition.auth;
212
+ if (!auth) return;
213
+ const id = definition.id;
214
+ if (!AUTH_RUNGS.includes(auth.rung)) {
215
+ throw new Error(
216
+ `Connector ${id} declares unknown auth rung ${JSON.stringify(auth.rung)}; expected ${AUTH_RUNGS.join(", ")}`
217
+ );
218
+ }
219
+ if (auth.rung === "cli" && !auth.probe?.command?.trim()) {
220
+ throw new Error(`Connector ${id} borrows a CLI login but declares no probe command to ask it`);
221
+ }
222
+ if (auth.rung === "key") {
223
+ const keys = auth.keys ?? [];
224
+ if (keys.length === 0) {
225
+ throw new Error(`Connector ${id} signs in with a key but names no config field holding it`);
226
+ }
227
+ const declared = new Set((definition.config ?? []).map((field) => field.key));
228
+ for (const key of keys) {
229
+ if (!declared.has(key)) {
230
+ throw new Error(`Connector ${id} names auth key "${key}", which is not a config field`);
231
+ }
232
+ }
233
+ }
234
+ if (auth.rung === "browser") assertBrowserSignIn(id, auth.browser);
235
+ if (auth.rung === "none" || auth.rung === "browser") {
236
+ const secret = (definition.config ?? []).find((field) => field.secret === true);
237
+ if (secret) {
238
+ const claim = auth.rung === "none" ? "needs no sign-in" : "signs in through a Vorn window";
239
+ throw new Error(
240
+ `Connector ${id} claims it ${claim} but declares secret field "${secret.key}"`
241
+ );
242
+ }
243
+ }
244
+ }
245
+ function assertBrowserSignIn(id, browser) {
246
+ if (!browser) {
247
+ throw new Error(
248
+ `Connector ${id} signs in through a Vorn window but declares no browser sign-in`
249
+ );
250
+ }
251
+ const origins = Array.isArray(browser.origins) ? browser.origins : [];
252
+ const bad = origins.find((origin) => typeof origin !== "string" || !ORIGIN_PATTERN.test(origin));
253
+ if (origins.length === 0 || bad !== void 0) {
254
+ throw new Error(
255
+ `Connector ${id} must name its origins as https://host or https://*.host` + (bad !== void 0 ? `; ${JSON.stringify(bad)} is neither` : "")
256
+ );
257
+ }
258
+ const places = [
259
+ ["sign-in page", browser.signInUrl],
260
+ ["signed-in check", browser.check?.url]
261
+ ];
262
+ for (const [what, url] of places) {
263
+ if (typeof url !== "string" || !withinOrigins(origins, url)) {
264
+ throw new Error(
265
+ `Connector ${id} puts its ${what} ${JSON.stringify(url ?? "")} outside its origins`
266
+ );
267
+ }
268
+ }
269
+ const identity = browser.check?.identity;
270
+ if (!Array.isArray(identity) || identity.some((path) => typeof path !== "string" || !path.trim())) {
271
+ throw new Error(`Connector ${id} must name its identity fields as non-empty strings`);
272
+ }
273
+ }
274
+ function assertIdentity(kind, definition) {
275
+ if (!KEY_PATTERN.test(definition.id ?? "")) {
276
+ throw new Error(`${kind} id "${definition.id}" must start with a letter and be url-safe`);
277
+ }
278
+ if (!definition.name?.trim()) {
279
+ throw new Error(`${kind} ${definition.id} is missing a name`);
280
+ }
281
+ assertIcon(`${kind} ${definition.id}`, definition.icon);
282
+ }
283
+ function assertIcon(subject, icon) {
284
+ if (!icon) return;
285
+ const { viewBox, paths } = icon;
286
+ if (!Array.isArray(paths) || paths.length === 0) {
287
+ throw new Error(`${subject} has an icon with no paths`);
288
+ }
289
+ for (const path of paths) {
290
+ if (typeof path !== "string" || !PATH_DATA_PATTERN.test(path)) {
291
+ throw new Error(
292
+ `${subject} has an icon path that is not SVG path data. Only path data is accepted, not markup.`
293
+ );
294
+ }
295
+ }
296
+ if (viewBox !== void 0 && !VIEW_BOX_PATTERN.test(viewBox)) {
297
+ throw new Error(`${subject} has an icon viewBox that is not four numbers`);
298
+ }
299
+ }
300
+ function envNameFor(key, explicit) {
301
+ if (explicit) return explicit;
302
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
303
+ }
304
+ function defineConnector(definition) {
305
+ assertIdentity("Connector", definition);
306
+ const triggers = definition.triggers ?? [];
307
+ const actions = definition.actions ?? [];
308
+ if (triggers.length === 0 && actions.length === 0) {
309
+ throw new Error(`Connector ${definition.id} declares no triggers and no actions`);
310
+ }
311
+ for (const trigger of triggers) {
312
+ if (!KEY_PATTERN.test(trigger.type ?? "")) {
313
+ throw new Error(`Trigger type "${trigger.type}" must start with a letter and be url-safe`);
314
+ }
315
+ const loose = trigger;
316
+ const declarative = typeof loose.fetch === "function";
317
+ const imperative = typeof loose.poll === "function";
318
+ if (declarative && imperative) {
319
+ throw new Error(`Trigger ${trigger.type} declares both fetch() and poll(); pick one`);
320
+ }
321
+ if (declarative !== (loose.dedupe !== void 0)) {
322
+ throw new Error(
323
+ `Trigger ${trigger.type} needs fetch() and a dedupe strategy together, not one alone`
324
+ );
325
+ }
326
+ if (loose.dedupe !== void 0 && !DEDUPE_STRATEGIES.includes(loose.dedupe)) {
327
+ throw new Error(
328
+ `Trigger ${trigger.type} has unknown dedupe strategy ${JSON.stringify(loose.dedupe)}; expected ${DEDUPE_STRATEGIES.join(" or ")}`
329
+ );
330
+ }
331
+ if (loose.poll !== void 0 && !imperative) {
332
+ throw new Error(`Trigger ${trigger.type} declares poll but it is not a function`);
333
+ }
334
+ if (!declarative && !imperative) {
335
+ throw new Error(`Trigger ${trigger.type} is missing a fetch() or poll() implementation`);
336
+ }
337
+ }
338
+ for (const action of actions) {
339
+ if (!KEY_PATTERN.test(action.type ?? "")) {
340
+ throw new Error(`Action type "${action.type}" must start with a letter and be url-safe`);
341
+ }
342
+ const loose = action;
343
+ const written = typeof loose.run === "function";
344
+ const declared = loose.request !== void 0;
345
+ if (written && declared) {
346
+ throw new Error(`Action ${action.type} declares both run() and a request; pick one`);
347
+ }
348
+ if (!written && !declared) {
349
+ throw new Error(`Action ${action.type} is missing a run() implementation or a request`);
350
+ }
351
+ if (declared) {
352
+ const request = loose.request;
353
+ if (typeof request?.url !== "string" || request.url.trim() === "") {
354
+ throw new Error(`Action ${action.type} declares a request with no URL`);
355
+ }
356
+ const url = request.url.trim();
357
+ if (!ABSOLUTE_URL_PATTERN.test(url) && !CONFIG_ROOTED_URL_PATTERN.test(url)) {
358
+ throw new Error(
359
+ `Action ${action.type} declares the request URL "${url}", which is neither absolute nor rooted in a {{config.\u2026}} value`
360
+ );
361
+ }
362
+ }
363
+ if (!declared && loose.postReceive !== void 0) {
364
+ throw new Error(`Action ${action.type} has postReceive but no request for it to reshape`);
365
+ }
366
+ for (const input of action.inputs ?? []) {
367
+ if (input.loadOptions !== void 0 && definition.options?.[input.loadOptions] === void 0) {
368
+ throw new Error(
369
+ `Action ${action.type} argument "${input.key}" loads options from "${input.loadOptions}", which the connector does not serve`
370
+ );
371
+ }
372
+ }
373
+ }
374
+ assertUnique(
375
+ "trigger",
376
+ triggers.map((trigger) => trigger.type)
377
+ );
378
+ assertUnique(
379
+ "action",
380
+ actions.map((action) => action.type)
381
+ );
382
+ assertUnique(
383
+ "config field",
384
+ (definition.config ?? []).map((field) => field.key)
385
+ );
386
+ assertAuth(definition);
387
+ return {
388
+ ...definition,
389
+ kind: "connector",
390
+ version: definition.version ?? "0.0.0",
391
+ config: definition.config ?? [],
392
+ triggers,
393
+ actions
394
+ };
395
+ }
396
+ function assertPredicate(id, where, predicate) {
397
+ if (!predicate) return;
398
+ const lists = [
399
+ ["workspaceContains", predicate.workspaceContains],
400
+ ["remoteHost", predicate.remoteHost],
401
+ ["agent", predicate.agent],
402
+ ["platform", predicate.platform]
403
+ ];
404
+ for (const [field, value] of lists) {
405
+ if (value === void 0) continue;
406
+ if (!Array.isArray(value) || value.length === 0) {
407
+ throw new Error(`Extension ${id} ${where} declares "${field}" with nothing in it`);
408
+ }
409
+ for (const entry of value) {
410
+ if (typeof entry !== "string" || entry.trim() === "") {
411
+ throw new Error(`Extension ${id} ${where} declares an empty "${field}" value`);
412
+ }
413
+ }
414
+ }
415
+ for (const glob of predicate.workspaceContains ?? []) {
416
+ if (glob.startsWith("/") || glob.split("/").includes("..")) {
417
+ throw new Error(
418
+ `Extension ${id} ${where} looks for "${glob}", which is not inside the worktree`
419
+ );
420
+ }
421
+ }
422
+ for (const agent of predicate.agent ?? []) {
423
+ if (!EXTENSION_AGENTS.includes(agent)) {
424
+ throw new Error(
425
+ `Extension ${id} ${where} names unknown agent ${JSON.stringify(agent)}; expected ${EXTENSION_AGENTS.join(", ")}`
426
+ );
427
+ }
428
+ }
429
+ for (const platform of predicate.platform ?? []) {
430
+ if (!EXTENSION_PLATFORMS.includes(platform)) {
431
+ throw new Error(
432
+ `Extension ${id} ${where} names unknown platform ${JSON.stringify(platform)}; expected ${EXTENSION_PLATFORMS.join(", ")}`
433
+ );
434
+ }
435
+ }
436
+ }
437
+ function assertPane(id, pane) {
438
+ assertIcon(`Extension ${id} pane ${pane.id}`, pane.icon);
439
+ const loose = pane;
440
+ const page2 = loose.web !== void 0;
441
+ const program = loose.command !== void 0;
442
+ if (page2 && program) {
443
+ throw new Error(`Extension ${id} pane ${pane.id} declares both a web page and a command`);
444
+ }
445
+ if (!page2 && !program) {
446
+ throw new Error(`Extension ${id} pane ${pane.id} declares neither a web page nor a command`);
447
+ }
448
+ if (page2) {
449
+ const web = loose.web;
450
+ if (typeof web !== "string" || !WEB_ENTRY_PATTERN.test(web) || web.split("/").includes("..")) {
451
+ throw new Error(
452
+ `Extension ${id} pane ${pane.id} declares the page ${JSON.stringify(web)}; a page is an .html file under web/ in the package`
453
+ );
454
+ }
455
+ return;
456
+ }
457
+ const command = loose.command;
458
+ if (!Array.isArray(command) || command.length === 0) {
459
+ throw new Error(`Extension ${id} pane ${pane.id} declares a command with nothing to run`);
460
+ }
461
+ for (const arg of command) {
462
+ if (typeof arg !== "string" || arg === "") {
463
+ throw new Error(`Extension ${id} pane ${pane.id} declares a command with an empty argument`);
464
+ }
465
+ }
466
+ }
467
+ function defineExtension(definition) {
468
+ assertIdentity("Extension", definition);
469
+ const id = definition.id;
470
+ const panes = definition.panes ?? [];
471
+ const footers = definition.footers ?? [];
472
+ const linkHandlers = definition.linkHandlers ?? [];
473
+ if (panes.length === 0 && footers.length === 0 && linkHandlers.length === 0) {
474
+ throw new Error(`Extension ${id} contributes nothing`);
475
+ }
476
+ const permissions = definition.permissions ?? [];
477
+ if (!Array.isArray(permissions)) {
478
+ throw new Error(`Extension ${id} declares permissions that are not a list`);
479
+ }
480
+ for (const permission of permissions) {
481
+ if (!EXTENSION_PERMISSIONS.includes(permission)) {
482
+ throw new Error(
483
+ `Extension ${id} asks for unknown permission ${JSON.stringify(permission)}; expected ${EXTENSION_PERMISSIONS.join(", ")}`
484
+ );
485
+ }
486
+ }
487
+ assertUnique("permission", permissions);
488
+ const contributions = [...panes, ...footers, ...linkHandlers];
489
+ for (const contribution of contributions) {
490
+ if (!KEY_PATTERN.test(contribution.id ?? "")) {
491
+ throw new Error(
492
+ `Contribution id "${contribution.id}" must start with a letter and be url-safe`
493
+ );
494
+ }
495
+ if (!contribution.title?.trim()) {
496
+ throw new Error(`Extension ${id} contribution ${contribution.id} is missing a title`);
497
+ }
498
+ assertPredicate(id, `contribution ${contribution.id}`, contribution.when);
499
+ }
500
+ assertUnique(
501
+ "contribution",
502
+ contributions.map((contribution) => contribution.id)
503
+ );
504
+ assertPredicate(id, "activates", definition.activates);
505
+ for (const pane of panes) assertPane(id, pane);
506
+ for (const footer of footers) {
507
+ if (typeof footer.run !== "function") {
508
+ throw new Error(`Extension ${id} footer ${footer.id} is missing a run() implementation`);
509
+ }
510
+ if (!Number.isFinite(footer.every) || footer.every < MIN_FOOTER_SECONDS) {
511
+ throw new Error(
512
+ `Extension ${id} footer ${footer.id} asks to run every ${footer.every}s; ${MIN_FOOTER_SECONDS}s is the shortest interval a footer may ask for`
513
+ );
514
+ }
515
+ }
516
+ for (const handler of linkHandlers) {
517
+ if (typeof handler.run !== "function") {
518
+ throw new Error(
519
+ `Extension ${id} link handler ${handler.id} is missing a run() implementation`
520
+ );
521
+ }
522
+ if (typeof handler.pattern !== "string" || handler.pattern.length > MAX_PATTERN_LENGTH) {
523
+ throw new Error(
524
+ `Extension ${id} link handler ${handler.id} has a pattern longer than ${MAX_PATTERN_LENGTH} characters; it is matched on every click`
525
+ );
526
+ }
527
+ if (hasNestedQuantifier(handler.pattern)) {
528
+ throw new Error(
529
+ `Extension ${id} link handler ${handler.id} has a pattern that repeats a group which already repeats; matching it can take exponential time on a click`
530
+ );
531
+ }
532
+ let matcher;
533
+ try {
534
+ matcher = new RegExp(handler.pattern);
535
+ } catch (error) {
536
+ const reason = error instanceof Error ? error.message : String(error);
537
+ throw new Error(
538
+ `Extension ${id} link handler ${handler.id} has a pattern that is not a regular expression: ${reason}`,
539
+ { cause: error }
540
+ );
541
+ }
542
+ if (typeof handler.example !== "string" || handler.example.trim() === "") {
543
+ throw new Error(
544
+ `Extension ${id} link handler ${handler.id} names no example link its pattern matches`
545
+ );
546
+ }
547
+ if (!matcher.test(handler.example)) {
548
+ throw new Error(
549
+ `Extension ${id} link handler ${handler.id} has the example ${JSON.stringify(handler.example)}, which its own pattern ${JSON.stringify(handler.pattern)} does not match`
550
+ );
551
+ }
552
+ }
553
+ return {
554
+ id,
555
+ name: definition.name,
556
+ ...definition.description !== void 0 && { description: definition.description },
557
+ ...definition.icon !== void 0 && { icon: definition.icon },
558
+ kind: "extension",
559
+ version: definition.version ?? "0.0.0",
560
+ // Its credential is the host's own token, so there is nothing to sign in to.
561
+ auth: { rung: "none" },
562
+ config: [],
563
+ triggers: [],
564
+ actions: [],
565
+ permissions,
566
+ ...definition.activates !== void 0 && { activates: definition.activates },
567
+ contributes: {
568
+ ...panes.length > 0 && { panes },
569
+ ...footers.length > 0 && { footers },
570
+ ...linkHandlers.length > 0 && { linkHandlers }
571
+ }
572
+ };
573
+ }
574
+ function resolveConfig(connector, env = process.env) {
575
+ const config = {};
576
+ const missing = [];
577
+ for (const field of connector.config) {
578
+ const name = envNameFor(field.key, field.env);
579
+ const value = env[name] ?? field.default;
580
+ if (value === void 0 || value === "") {
581
+ if (field.required) missing.push(`${field.key} (${name})`);
582
+ continue;
583
+ }
584
+ config[field.key] = value;
585
+ }
586
+ if (missing.length > 0) {
587
+ throw new Error(
588
+ `Connector ${connector.id} is missing required configuration: ${missing.join(", ")}`
589
+ );
590
+ }
591
+ return config;
592
+ }
593
+
594
+ // src/setup.ts
595
+ function pollToolName(triggerType) {
596
+ return `poll_${triggerType}`;
597
+ }
598
+ function footerToolName(footerId) {
599
+ return `vorn_footer_${footerId}`;
600
+ }
601
+ function handlerToolName(handlerId) {
602
+ return `vorn_handler_${handlerId}`;
603
+ }
604
+ var MANIFEST_TOOL = "vorn_connector_manifest";
605
+ var PREFLIGHT_TOOL = "vorn_connector_preflight";
606
+ var OPTIONS_TOOL = "vorn_connector_options";
607
+ function connectionSetup(connector, triggerType) {
608
+ const trigger = connector.triggers.find((entry) => entry.type === triggerType);
609
+ if (!trigger) {
610
+ throw new Error(`Connector ${connector.id} has no trigger "${triggerType}"`);
611
+ }
612
+ return {
613
+ connectorId: connector.id,
614
+ triggerType,
615
+ filters: {
616
+ pollTool: pollToolName(triggerType),
617
+ itemsPath: "items",
618
+ idField: "externalId",
619
+ timestampField: "updatedAt",
620
+ titleField: "title",
621
+ urlField: "url",
622
+ cursorArg: "cursor",
623
+ cursorPath: "nextCursor"
624
+ },
625
+ env: connector.config.map((field) => ({
626
+ name: envNameFor(field.key, field.env),
627
+ required: field.required === true,
628
+ secret: field.secret === true,
629
+ ...field.description !== void 0 && { description: field.description },
630
+ ...field.builderHint !== void 0 && { builderHint: field.builderHint }
631
+ }))
632
+ };
633
+ }
634
+ function manifestContributions(connector) {
635
+ const contributes = connector.contributes;
636
+ if (!contributes) return void 0;
637
+ const shared = (contribution) => ({
638
+ id: contribution.id,
639
+ title: contribution.title,
640
+ ...contribution.description !== void 0 && { description: contribution.description },
641
+ ...contribution.when !== void 0 && { when: contribution.when }
642
+ });
643
+ return {
644
+ ...contributes.panes !== void 0 && {
645
+ panes: contributes.panes.map((pane) => ({
646
+ ...shared(pane),
647
+ ...pane.icon !== void 0 && { icon: pane.icon },
648
+ ...pane.web !== void 0 && { web: pane.web },
649
+ ...pane.command !== void 0 && { command: pane.command }
650
+ }))
651
+ },
652
+ ...contributes.footers !== void 0 && {
653
+ footers: contributes.footers.map((footer) => ({ ...shared(footer), every: footer.every }))
654
+ },
655
+ ...contributes.linkHandlers !== void 0 && {
656
+ linkHandlers: contributes.linkHandlers.map((handler) => ({
657
+ ...shared(handler),
658
+ pattern: handler.pattern,
659
+ example: handler.example
660
+ }))
661
+ }
662
+ };
663
+ }
664
+ function connectorManifest(connector) {
665
+ const contributes = manifestContributions(connector);
666
+ return {
667
+ id: connector.id,
668
+ name: connector.name,
669
+ version: connector.version,
670
+ kind: connector.kind,
671
+ ...connector.description !== void 0 && { description: connector.description },
672
+ ...connector.icon !== void 0 && { icon: connector.icon },
673
+ ...connector.auth !== void 0 && { auth: connector.auth },
674
+ ...contributes !== void 0 && { contributes },
675
+ ...connector.permissions !== void 0 && { permissions: connector.permissions },
676
+ ...connector.activates !== void 0 && { activates: connector.activates },
677
+ triggers: connector.triggers.map((trigger) => ({
678
+ type: trigger.type,
679
+ label: trigger.label,
680
+ ...trigger.description !== void 0 && { description: trigger.description },
681
+ // Carried through so the app can seed a connection's status mapping and
682
+ // its polling workflow. Absent when the connector said nothing, which is
683
+ // different from saying there is nothing.
684
+ ...trigger.statusMapping !== void 0 && { statusMapping: trigger.statusMapping },
685
+ ...trigger.defaultWorkflow !== void 0 && { defaultWorkflow: trigger.defaultWorkflow },
686
+ setup: connectionSetup(connector, trigger.type)
687
+ })),
688
+ actions: connector.actions.map((action) => ({
689
+ type: action.type,
690
+ label: action.label,
691
+ ...action.description !== void 0 && { description: action.description },
692
+ inputs: (action.inputs ?? []).map((input) => ({
693
+ key: input.key,
694
+ label: input.label,
695
+ type: input.type ?? "string",
696
+ required: input.required === true,
697
+ ...input.options !== void 0 && { options: input.options },
698
+ ...input.loadOptions !== void 0 && { loadOptions: input.loadOptions },
699
+ ...input.builderHint !== void 0 && { builderHint: input.builderHint }
700
+ })),
701
+ ...action.outputs !== void 0 && { outputs: action.outputs },
702
+ ...action.sample !== void 0 && { sample: action.sample }
703
+ }))
704
+ };
705
+ }
706
+
707
+ // src/packaging.ts
708
+ import { builtinModules } from "module";
709
+ import { existsSync, readFileSync } from "fs";
710
+ import { cp, mkdtemp, readdir, stat, writeFile } from "fs/promises";
711
+ import { tmpdir } from "os";
712
+ import { dirname, isAbsolute, join, resolve } from "path";
713
+ var MAX_PACK_BYTES = 8 * 1024 * 1024;
714
+ var MAX_UNPACKED_BYTES = 32 * 1024 * 1024;
715
+ var LIFECYCLE_SCRIPTS = [
716
+ "preinstall",
717
+ "install",
718
+ "postinstall",
719
+ "prepare",
720
+ "prepublish",
721
+ "prepublishOnly",
722
+ "postpublish"
723
+ ];
724
+ var BUILTINS = new Set(builtinModules);
725
+ function finding(code, target, message, level = "error") {
726
+ return { level, code, target, message };
727
+ }
728
+ function lifecycleScriptFindings(pkg) {
729
+ const scripts = pkg?.scripts;
730
+ if (!scripts || typeof scripts !== "object") return [];
731
+ const named = LIFECYCLE_SCRIPTS.filter((name) => typeof scripts[name] === "string");
732
+ if (named.length === 0) return [];
733
+ return [
734
+ finding(
735
+ "lifecycle-scripts",
736
+ "package.json",
737
+ `Remove the ${named.join(", ")} script(s); a pack is installed by copying files, never by running them`
738
+ )
739
+ ];
740
+ }
741
+ function bundleDependencyFindings(external) {
742
+ const specifiers = /* @__PURE__ */ new Set();
743
+ for (const specifier of external) {
744
+ if (specifier.startsWith(".") || specifier.startsWith("/")) continue;
745
+ if (specifier.startsWith("node:") || BUILTINS.has(specifier)) continue;
746
+ specifiers.add(specifier);
747
+ }
748
+ if (specifiers.size === 0) return [];
749
+ return [
750
+ finding(
751
+ "runtime-dependencies",
752
+ "bundle",
753
+ `${[...specifiers].sort().join(", ")} stayed outside the bundle; a pack must launch with no install step`
754
+ )
755
+ ];
756
+ }
757
+ var RELATIVE_REQUIRE = /(?:__)?(?:require(?:\.resolve)?|import)\(\s*(['"])(\.\.?\/[^'"]*)\1\s*\)/y;
758
+ var RELATIVE_CREATE_REQUIRE = /createRequire\([^()]*\)\(\s*(['"])(\.\.?\/[^'"]*)\1\s*\)/y;
759
+ var CALL_WORDS = /* @__PURE__ */ new Set(["require", "__require", "createRequire", "import"]);
760
+ var BEFORE_REGEX = /* @__PURE__ */ new Set(["", ..."(,=:[!&|?{};+-*%~^<>"]);
761
+ var BEFORE_REGEX_WORDS = /* @__PURE__ */ new Set([
762
+ "return",
763
+ "typeof",
764
+ "instanceof",
765
+ "in",
766
+ "of",
767
+ "new",
768
+ "delete",
769
+ "void",
770
+ "case",
771
+ "do",
772
+ "else",
773
+ "yield",
774
+ "await"
775
+ ]);
776
+ var WORD = /[\w$]/;
777
+ function endOfQuoted(code, start) {
778
+ const quote2 = code[start];
779
+ let i = start + 1;
780
+ while (i < code.length) {
781
+ if (code[i] === "\\") {
782
+ i += 2;
783
+ continue;
784
+ }
785
+ if (code[i] === quote2) return i + 1;
786
+ i += 1;
787
+ }
788
+ return code.length;
789
+ }
790
+ function endOfRegex(code, start) {
791
+ let i = start + 1;
792
+ let inClass = false;
793
+ while (i < code.length) {
794
+ const ch = code[i];
795
+ if (ch === "\\") {
796
+ i += 2;
797
+ continue;
798
+ }
799
+ if (ch === "\n") return i;
800
+ if (ch === "[") inClass = true;
801
+ else if (ch === "]") inClass = false;
802
+ else if (ch === "/" && !inClass) return i + 1;
803
+ i += 1;
804
+ }
805
+ return code.length;
806
+ }
807
+ function relativeRuntimeSpecifiers(code) {
808
+ const found = /* @__PURE__ */ new Set();
809
+ let previous = "";
810
+ let previousWord = "";
811
+ let i = 0;
812
+ while (i < code.length) {
813
+ const ch = code[i];
814
+ if (ch === "/" && code[i + 1] === "/") {
815
+ const end = code.indexOf("\n", i);
816
+ i = end === -1 ? code.length : end;
817
+ continue;
818
+ }
819
+ if (ch === "/" && code[i + 1] === "*") {
820
+ const end = code.indexOf("*/", i + 2);
821
+ i = end === -1 ? code.length : end + 2;
822
+ continue;
823
+ }
824
+ if (ch === '"' || ch === "'" || ch === "`") {
825
+ i = endOfQuoted(code, i);
826
+ previous = ch;
827
+ previousWord = "";
828
+ continue;
829
+ }
830
+ if (ch === "/" && (BEFORE_REGEX.has(previous) || BEFORE_REGEX_WORDS.has(previousWord))) {
831
+ i = endOfRegex(code, i);
832
+ previous = "/";
833
+ previousWord = "";
834
+ continue;
835
+ }
836
+ if (WORD.test(ch)) {
837
+ const start = i;
838
+ while (i < code.length && WORD.test(code[i])) i += 1;
839
+ const word = code.slice(start, i);
840
+ if (CALL_WORDS.has(word) && code[start - 1] !== ".") {
841
+ for (const pattern of [RELATIVE_REQUIRE, RELATIVE_CREATE_REQUIRE]) {
842
+ pattern.lastIndex = start;
843
+ const match = pattern.exec(code);
844
+ if (match) found.add(match[2]);
845
+ }
846
+ }
847
+ previous = code[i - 1];
848
+ previousWord = word;
849
+ continue;
850
+ }
851
+ if (!/\s/.test(ch)) {
852
+ previous = ch;
853
+ previousWord = "";
854
+ }
855
+ i += 1;
856
+ }
857
+ return [...found];
858
+ }
859
+ function bundledRequireFindings(code) {
860
+ const specifiers = relativeRuntimeSpecifiers(code);
861
+ if (specifiers.length === 0) return [];
862
+ return [
863
+ finding(
864
+ "runtime-dependencies",
865
+ "bundle",
866
+ `${specifiers.sort().join(", ")} ${specifiers.length === 1 ? "is" : "are"} required at runtime; a pack is one file, so nothing beside it survives packing`,
867
+ "warn"
868
+ )
869
+ ];
870
+ }
871
+ function packageDirFor(resolveDir, entry) {
872
+ const from = resolve(resolveDir);
873
+ if (entry === void 0) return from;
874
+ return entry.startsWith(".") || isAbsolute(entry) ? dirname(resolve(from, entry)) : from;
875
+ }
876
+ function readNearestPackageJson(fromDir) {
877
+ let current = resolve(fromDir);
878
+ for (; ; ) {
879
+ try {
880
+ return JSON.parse(readFileSync(join(current, "package.json"), "utf8"));
881
+ } catch {
882
+ const parent = dirname(current);
883
+ if (parent === current) return void 0;
884
+ current = parent;
885
+ }
886
+ }
887
+ }
888
+ function packageRootFor(fromDir) {
889
+ let current = resolve(fromDir);
890
+ for (; ; ) {
891
+ if (existsSync(join(current, "package.json"))) return current;
892
+ const parent = dirname(current);
893
+ if (parent === current) return resolve(fromDir);
894
+ current = parent;
895
+ }
896
+ }
897
+ function packEntryContents(entry, sdkModule = "@vornrun/connector-sdk") {
898
+ return [
899
+ `import { serveConnector } from ${JSON.stringify(sdkModule)}`,
900
+ `import * as entry from ${JSON.stringify(entry)}`,
901
+ "const exported = Object.values(entry).find((value) => value && Array.isArray(value.triggers))",
902
+ `if (!exported) throw new Error(${JSON.stringify(`${entry} exports no connector`)})`,
903
+ "await serveConnector(exported)",
904
+ ""
905
+ ].join("\n");
906
+ }
907
+ var WEB_DIR = "web";
908
+ async function directoryBytes(dir) {
909
+ let total = 0;
910
+ for (const entry of await readdir(dir, { withFileTypes: true, recursive: true })) {
911
+ if (!entry.isFile()) continue;
912
+ total += (await stat(join(entry.parentPath, entry.name))).size;
913
+ }
914
+ return total;
915
+ }
916
+ function webDirectories(connector) {
917
+ const dirs = (connector.contributes?.panes ?? []).map((pane) => pane.web).filter((web) => web !== void 0).map((web) => dirname(web));
918
+ return [...new Set(dirs)];
919
+ }
920
+ async function stagePack(connector, code, packageRoot) {
921
+ const dir = await mkdtemp(join(tmpdir(), "vorn-pack-"));
922
+ await writeFile(join(dir, "index.js"), code, "utf8");
923
+ await writeFile(
924
+ join(dir, "manifest.json"),
925
+ `${JSON.stringify(connectorManifest(connector), null, 2)}
926
+ `,
927
+ "utf8"
928
+ );
929
+ if (packageRoot !== void 0) {
930
+ for (const relative of webDirectories(connector)) {
931
+ const from = join(packageRoot, relative);
932
+ if (existsSync(from)) await cp(from, join(dir, relative), { recursive: true });
933
+ }
934
+ }
935
+ return dir;
936
+ }
937
+ var LAUNCH_TIMEOUT_MS = 15e3;
938
+ var LAUNCH_ENV_KEYS = [
939
+ "PATH",
940
+ "HOME",
941
+ "USERPROFILE",
942
+ "HOMEDRIVE",
943
+ "HOMEPATH",
944
+ "APPDATA",
945
+ "LOCALAPPDATA",
946
+ "PROGRAMDATA",
947
+ "PROGRAMFILES",
948
+ "SystemRoot",
949
+ "SYSTEMDRIVE",
950
+ "COMSPEC",
951
+ "PATHEXT",
952
+ "TMPDIR",
953
+ "TEMP",
954
+ "TMP",
955
+ "LANG",
956
+ "LC_ALL",
957
+ "LC_CTYPE",
958
+ "TZ",
959
+ "SHELL",
960
+ "TERM",
961
+ "USER",
962
+ "LOGNAME",
963
+ "NODE_EXTRA_CA_CERTS"
964
+ ];
965
+ function launchEnv() {
966
+ const env = {};
967
+ for (const key of LAUNCH_ENV_KEYS) {
968
+ const value = process.env[key];
969
+ if (value !== void 0) env[key] = value;
970
+ }
971
+ return env;
972
+ }
973
+ function errorLine(text) {
974
+ const lines = text.split("\n").map((line) => line.trim()).filter((line) => line !== "");
975
+ return [...lines].reverse().find((line) => /Error\b/.test(line)) ?? lines[lines.length - 1];
976
+ }
977
+ function withTimeout(promise, ms, message) {
978
+ let timer;
979
+ return Promise.race([
980
+ promise.finally(() => clearTimeout(timer)),
981
+ new Promise((_, reject) => {
982
+ timer = setTimeout(() => reject(new Error(message)), ms);
983
+ })
984
+ ]);
985
+ }
986
+ async function packLaunchFindings(dir) {
987
+ const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
988
+ const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
989
+ const transport = new StdioClientTransport({
990
+ command: process.execPath,
991
+ args: ["index.js"],
992
+ cwd: dir,
993
+ env: launchEnv(),
994
+ stderr: "pipe"
995
+ });
996
+ const client = new Client({ name: "vorn-connector-check", version: "1" }, { capabilities: {} });
997
+ let stderr = "";
998
+ transport.stderr?.on("data", (chunk) => {
999
+ stderr += chunk.toString();
1000
+ });
1001
+ try {
1002
+ await withTimeout(
1003
+ client.connect(transport),
1004
+ LAUNCH_TIMEOUT_MS,
1005
+ `did not answer within ${LAUNCH_TIMEOUT_MS / 1e3}s of starting`
1006
+ );
1007
+ return [];
1008
+ } catch (error) {
1009
+ const said = error instanceof Error ? error.message : String(error);
1010
+ return [
1011
+ finding("pack-launch", "bundle", `did not start as a pack: ${errorLine(stderr) ?? said}`)
1012
+ ];
1013
+ } finally {
1014
+ await client.close().catch(() => {
1015
+ });
1016
+ await transport.close().catch(() => {
1017
+ });
1018
+ }
1019
+ }
1020
+ async function esbuildBundle(request) {
1021
+ const { build } = await import("esbuild");
1022
+ const result = await build({
1023
+ stdin: {
1024
+ contents: request.contents,
1025
+ resolveDir: request.resolveDir,
1026
+ sourcefile: "vorn-connector-pack.js",
1027
+ loader: "js"
1028
+ },
1029
+ bundle: true,
1030
+ platform: "node",
1031
+ target: "node20",
1032
+ format: "esm",
1033
+ write: false,
1034
+ metafile: true,
1035
+ legalComments: "none",
1036
+ // A bundled CommonJS dependency asks for its builtins through esbuild's shim, which throws unless a real require is in scope.
1037
+ banner: {
1038
+ js: [
1039
+ "import { createRequire as __vornCreateRequire } from 'node:module'",
1040
+ "const require = __vornCreateRequire(import.meta.url)",
1041
+ ""
1042
+ ].join("\n")
1043
+ }
1044
+ });
1045
+ const output = Object.values(result.metafile.outputs)[0];
1046
+ return {
1047
+ code: result.outputFiles[0].text,
1048
+ external: (output?.imports ?? []).filter((item) => item.external).map((item) => item.path)
1049
+ };
1050
+ }
1051
+
1052
+ // src/host.ts
1053
+ var HOST_URL_ENV = "VORN_EXTENSION_HOST";
1054
+ var HOST_TOKEN_ENV = "VORN_EXTENSION_TOKEN";
1055
+ var PermissionDeniedError = class extends Error {
1056
+ constructor(method, detail) {
1057
+ super(`The host refused ${method}: ${detail}`);
1058
+ this.name = "PermissionDeniedError";
1059
+ }
1060
+ };
1061
+ var HostReplyError = class extends Error {
1062
+ constructor(method, detail) {
1063
+ super(`The host answered ${method} with ${detail}`);
1064
+ this.name = "HostReplyError";
1065
+ }
1066
+ };
1067
+ var HOST_TIMEOUT_MS = 15e3;
1068
+ function endpoint(env) {
1069
+ return loopbackEndpoint(env, {
1070
+ urlVar: HOST_URL_ENV,
1071
+ tokenVar: HOST_TOKEN_ENV,
1072
+ missing: `This extension was started without a host bridge; ${HOST_URL_ENV} and ${HOST_TOKEN_ENV} are set by Vorn`,
1073
+ served: "the bridge is served on this machine"
1074
+ });
1075
+ }
1076
+ function createExtensionHost(options) {
1077
+ const env = options.env ?? process.env;
1078
+ const call = options.fetchImpl ?? fetch;
1079
+ async function ask(method, params) {
1080
+ const { url, token } = endpoint(env);
1081
+ const response = await call(`${url}/${method}`, {
1082
+ method: "POST",
1083
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
1084
+ body: JSON.stringify({ sessionId: options.sessionId, ...params }),
1085
+ signal: AbortSignal.timeout(HOST_TIMEOUT_MS)
1086
+ });
1087
+ const text = await response.text();
1088
+ if (response.status === 403) throw new PermissionDeniedError(method, text || "not granted");
1089
+ if (!response.ok) throw new Error(`The host answered ${method} with HTTP ${response.status}`);
1090
+ if (text === "") return void 0;
1091
+ let parsed;
1092
+ try {
1093
+ parsed = JSON.parse(text);
1094
+ } catch {
1095
+ throw new HostReplyError(method, "a body that is not JSON");
1096
+ }
1097
+ if (!parsed || typeof parsed !== "object" || !("result" in parsed)) {
1098
+ throw new HostReplyError(method, "a body carrying no result");
1099
+ }
1100
+ return parsed.result;
1101
+ }
1102
+ return {
1103
+ diff: () => ask("diff", {}),
1104
+ status: () => ask("status", {}),
1105
+ output: (opts) => ask("output", { ...opts?.lines !== void 0 && { lines: opts.lines } }),
1106
+ selection: () => ask("selection", {}),
1107
+ send: (text) => ask("send", { text }),
1108
+ rename: (name) => ask("rename", { name }),
1109
+ usage: () => ask("usage", {})
1110
+ };
1111
+ }
1112
+
1113
+ // src/normalize.ts
1114
+ var RESERVED_KEYS = [
1115
+ "externalId",
1116
+ "title",
1117
+ "url",
1118
+ "description",
1119
+ "status",
1120
+ "labels",
1121
+ "assignee",
1122
+ "updatedAt"
1123
+ ];
1124
+ var UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
1125
+ function itemExternalId(item) {
1126
+ return String(item.externalId ?? "").trim();
1127
+ }
1128
+ function itemTimestamp(item, fallback) {
1129
+ return isoTimestamp(item.updatedAt, fallback);
1130
+ }
1131
+ function isoTimestamp(value, fallback) {
1132
+ if (value === void 0) return fallback;
1133
+ const date = value instanceof Date ? value : new Date(value);
1134
+ if (Number.isNaN(date.getTime())) {
1135
+ throw new Error(`Invalid updatedAt: ${String(value)}`);
1136
+ }
1137
+ return date.toISOString();
1138
+ }
1139
+ function normalizeItem(item, polledAt) {
1140
+ const externalId = itemExternalId(item);
1141
+ if (!externalId) {
1142
+ throw new Error("Connector item is missing externalId");
1143
+ }
1144
+ if (!item.title || !item.title.trim()) {
1145
+ throw new Error(`Connector item ${externalId} is missing title`);
1146
+ }
1147
+ const extra = {};
1148
+ for (const [key, value] of Object.entries(item.data ?? {})) {
1149
+ if (RESERVED_KEYS.includes(key)) continue;
1150
+ if (UNSAFE_KEYS.includes(key)) continue;
1151
+ extra[key] = value;
1152
+ }
1153
+ return {
1154
+ ...extra,
1155
+ externalId,
1156
+ title: item.title,
1157
+ url: item.url ?? "",
1158
+ description: item.description ?? "",
1159
+ status: item.status ?? "open",
1160
+ labels: item.labels ?? [],
1161
+ ...item.assignee !== void 0 && { assignee: item.assignee },
1162
+ updatedAt: isoTimestamp(item.updatedAt, polledAt)
1163
+ };
1164
+ }
1165
+ function normalizeItems(items, polledAt) {
1166
+ const seen = /* @__PURE__ */ new Set();
1167
+ return items.map((item) => {
1168
+ const normalized = normalizeItem(item, polledAt);
1169
+ if (seen.has(normalized.externalId)) {
1170
+ throw new Error(`Duplicate externalId "${normalized.externalId}" in one poll page`);
1171
+ }
1172
+ seen.add(normalized.externalId);
1173
+ return normalized;
1174
+ });
1175
+ }
1176
+
1177
+ // src/dedupe.ts
1178
+ var MAX_BOUNDARY_IDS = 500;
1179
+ function decodeCursor(cursor, strategy) {
1180
+ if (!cursor) return void 0;
1181
+ let parsed;
1182
+ try {
1183
+ parsed = JSON.parse(cursor);
1184
+ } catch (error) {
1185
+ throw new Error(`Cursor is not valid SDK cursor JSON: ${cursor}`, { cause: error });
1186
+ }
1187
+ const state = parsed;
1188
+ if (!state || typeof state !== "object" || state.v !== 1 || state.s !== strategy) {
1189
+ throw new Error(`Cursor does not belong to the "${strategy}" strategy: ${cursor}`);
1190
+ }
1191
+ const wellFormed = state.s === "timestamp" ? typeof state.t === "string" && Array.isArray(state.ids) && state.ids.every((id) => typeof id === "string") : typeof state.id === "string";
1192
+ if (!wellFormed) {
1193
+ throw new Error(`Cursor is missing the fields the "${strategy}" strategy needs: ${cursor}`);
1194
+ }
1195
+ return state;
1196
+ }
1197
+ function compare(left, right) {
1198
+ if (left === right) return 0;
1199
+ return left < right ? -1 : 1;
1200
+ }
1201
+ function page(chronological, context, hadCursor, nextCursor) {
1202
+ const delivered = context.limit === void 0 ? chronological : chronological.slice(0, context.limit);
1203
+ if (delivered.length === 0) {
1204
+ return { items: [], ...context.cursor !== void 0 && { nextCursor: context.cursor } };
1205
+ }
1206
+ return {
1207
+ items: delivered.map((entry) => entry.item),
1208
+ nextCursor: JSON.stringify(nextCursor(delivered)),
1209
+ // Only drain a backlog we know we truncated, and only once a cursor
1210
+ // exists — a first poll should not pull the source's entire history.
1211
+ hasMore: chronological.length > delivered.length && hadCursor
1212
+ };
1213
+ }
1214
+ function timestampPoll(fetched, state, context, polledAt) {
1215
+ const boundary = state?.t ?? context.since;
1216
+ const seen = new Set(state?.ids ?? []);
1217
+ const fresh = [];
1218
+ const pinnedAlreadySeen = [];
1219
+ for (const item of fetched) {
1220
+ const id = itemExternalId(item);
1221
+ const pinned = item.updatedAt === void 0 && boundary !== void 0;
1222
+ const at = pinned ? boundary : itemTimestamp(item, polledAt);
1223
+ const isNew = boundary === void 0 || at > boundary || at === boundary && !seen.has(id);
1224
+ if (isNew) fresh.push({ item, at, id, ...pinned && { pinned: true } });
1225
+ else if (pinned) pinnedAlreadySeen.push(id);
1226
+ }
1227
+ fresh.sort((left, right) => compare(left.at, right.at) || compare(left.id, right.id));
1228
+ return page(fresh, context, state !== void 0, (delivered) => {
1229
+ const newest = delivered[delivered.length - 1].at;
1230
+ const atNewest = [];
1231
+ for (let i = delivered.length - 1; i >= 0 && delivered[i].at === newest; i -= 1) {
1232
+ atNewest.push(delivered[i].id);
1233
+ }
1234
+ const carried = newest === boundary ? [...seen, ...atNewest] : [
1235
+ ...pinnedAlreadySeen,
1236
+ ...delivered.filter((entry) => entry.pinned).map((entry) => entry.id),
1237
+ ...atNewest
1238
+ ];
1239
+ return { v: 1, s: "timestamp", t: newest, ids: carried.slice(-MAX_BOUNDARY_IDS) };
1240
+ });
1241
+ }
1242
+ function lastItemPoll(fetched, state, context, polledAt) {
1243
+ const keyed = fetched.map((item) => ({
1244
+ item,
1245
+ at: itemTimestamp(item, polledAt),
1246
+ id: itemExternalId(item)
1247
+ }));
1248
+ const stopAt = state ? keyed.findIndex((entry) => entry.id === state.id) : -1;
1249
+ const chronological = (stopAt === -1 ? keyed : keyed.slice(0, stopAt)).reverse();
1250
+ return page(chronological, context, state !== void 0, (delivered) => ({
1251
+ v: 1,
1252
+ s: "lastItem",
1253
+ id: delivered[delivered.length - 1].id
1254
+ }));
1255
+ }
1256
+ async function pollWithDedupe(trigger, context) {
1257
+ const strategy = trigger.dedupe;
1258
+ const fetchItems = trigger.fetch;
1259
+ if (!strategy || !fetchItems) {
1260
+ throw new Error(`Trigger ${trigger.type} is not a declarative trigger`);
1261
+ }
1262
+ const polledAt = context.now();
1263
+ if (strategy === "lastItem") {
1264
+ const state2 = decodeCursor(context.cursor, "lastItem");
1265
+ const fetched2 = await runFetch(trigger.type, fetchItems, {
1266
+ config: context.config,
1267
+ ...state2 && { lastItemId: state2.id },
1268
+ ...context.limit !== void 0 && { limit: context.limit },
1269
+ now: context.now,
1270
+ fetch: context.fetch,
1271
+ ...context.session && { session: context.session }
1272
+ });
1273
+ return lastItemPoll(fetched2, state2, context, polledAt);
1274
+ }
1275
+ const state = decodeCursor(context.cursor, "timestamp");
1276
+ const since = state?.t ?? context.since;
1277
+ const fetched = await runFetch(trigger.type, fetchItems, {
1278
+ config: context.config,
1279
+ ...since !== void 0 && { since },
1280
+ ...context.limit !== void 0 && { limit: context.limit },
1281
+ now: context.now,
1282
+ fetch: context.fetch,
1283
+ ...context.session && { session: context.session }
1284
+ });
1285
+ return timestampPoll(fetched, state, context, polledAt);
1286
+ }
1287
+ async function runFetch(type, fetchItems, context) {
1288
+ const fetched = await fetchItems(context);
1289
+ if (!Array.isArray(fetched)) {
1290
+ throw new Error(`Trigger ${type} fetch() did not return an array`);
1291
+ }
1292
+ return fetched;
1293
+ }
1294
+
1295
+ // src/post-receive.ts
1296
+ var UNSAFE_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1297
+ function isRecord(value) {
1298
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1299
+ }
1300
+ function segments(path) {
1301
+ return path.split(".").map((part) => part.trim()).filter((part) => part !== "");
1302
+ }
1303
+ function valueAt(value, path) {
1304
+ let current = value;
1305
+ for (const key of segments(path)) {
1306
+ if (UNSAFE_KEYS2.has(key)) return void 0;
1307
+ if (Array.isArray(current)) {
1308
+ const index = Number(key);
1309
+ if (!Number.isInteger(index)) return void 0;
1310
+ current = current[index];
1311
+ continue;
1312
+ }
1313
+ if (!isRecord(current)) return void 0;
1314
+ current = Object.prototype.hasOwnProperty.call(current, key) ? current[key] : void 0;
1315
+ }
1316
+ return current;
1317
+ }
1318
+ function withValueAt(value, path, next) {
1319
+ const keys = segments(path);
1320
+ if (keys.length === 0) return next;
1321
+ const [head, ...rest] = keys;
1322
+ if (UNSAFE_KEYS2.has(head)) return value;
1323
+ if (Array.isArray(value)) {
1324
+ const index = Number(head);
1325
+ if (!Number.isInteger(index)) return value;
1326
+ const copy = [...value];
1327
+ copy[index] = rest.length === 0 ? next : withValueAt(copy[index], rest.join("."), next);
1328
+ return copy;
1329
+ }
1330
+ const base = isRecord(value) ? value : {};
1331
+ return {
1332
+ ...base,
1333
+ [head]: rest.length === 0 ? next : withValueAt(base[head], rest.join("."), next)
1334
+ };
1335
+ }
1336
+ function pick(value, keys) {
1337
+ if (Array.isArray(value)) return value.map((entry) => pick(entry, keys));
1338
+ if (!isRecord(value)) return value;
1339
+ const out = {};
1340
+ for (const key of keys) {
1341
+ if (UNSAFE_KEYS2.has(key)) continue;
1342
+ if (Object.prototype.hasOwnProperty.call(value, key)) out[key] = value[key];
1343
+ }
1344
+ return out;
1345
+ }
1346
+ function rename(value, from, to) {
1347
+ if (Array.isArray(value)) return value.map((entry) => rename(entry, from, to));
1348
+ if (!isRecord(value)) return value;
1349
+ if (UNSAFE_KEYS2.has(from) || UNSAFE_KEYS2.has(to)) return value;
1350
+ if (!Object.prototype.hasOwnProperty.call(value, from)) return value;
1351
+ const out = {};
1352
+ for (const [key, entry] of Object.entries(value)) {
1353
+ if (key === from) out[to] = entry;
1354
+ else if (key !== to) out[key] = entry;
1355
+ }
1356
+ return out;
1357
+ }
1358
+ function applyOp(value, op) {
1359
+ if (op.op === "flatten") return valueAt(value, op.path);
1360
+ const target = op.path === void 0 ? value : valueAt(value, op.path);
1361
+ if (op.path !== void 0 && target === void 0) return value;
1362
+ let next;
1363
+ if (op.op === "pick") next = pick(target, op.keys);
1364
+ else if (op.op === "rename") next = rename(target, op.from, op.to);
1365
+ else if (op.op === "filter") {
1366
+ next = Array.isArray(target) ? target.filter((entry) => isRecord(entry) && valueAt(entry, op.key) === op.equals) : target;
1367
+ } else {
1368
+ next = Array.isArray(target) ? target.map((entry) => applyPostReceive(entry, op.ops)) : target;
1369
+ }
1370
+ return op.path === void 0 ? next : withValueAt(value, op.path, next);
1371
+ }
1372
+ function applyPostReceive(value, ops) {
1373
+ return (ops ?? []).reduce(applyOp, value);
1374
+ }
1375
+
1376
+ // src/request.ts
1377
+ var MAX_ERROR_BODY = 500;
1378
+ var PLACEHOLDER = /\{\{\s*(args|config)\.([A-Za-z0-9_.-]+)\s*\}\}/g;
1379
+ var WHOLE_PLACEHOLDER = /^\{\{\s*(args|config)\.([A-Za-z0-9_.-]+)\s*\}\}$/;
1380
+ function lookup(source, path, scope) {
1381
+ return valueAt(source === "args" ? scope.args : scope.config, path);
1382
+ }
1383
+ var intoUrl = (value, source) => source === "config" ? value : encodeURIComponent(value);
1384
+ function intoHeader(name) {
1385
+ return (value) => {
1386
+ if (/[\r\n]/.test(value)) {
1387
+ throw new Error(`Header "${name}" would carry a line ending, which is not allowed`);
1388
+ }
1389
+ return value;
1390
+ };
1391
+ }
1392
+ function resolveTemplates(value, scope, substitute) {
1393
+ if (typeof value === "string") {
1394
+ const whole = WHOLE_PLACEHOLDER.exec(value);
1395
+ if (whole) {
1396
+ const resolved = lookup(whole[1], whole[2], scope);
1397
+ if (substitute === void 0 || resolved === void 0 || resolved === null) return resolved;
1398
+ return substitute(String(resolved), whole[1]);
1399
+ }
1400
+ return value.replace(PLACEHOLDER, (_match, source, path) => {
1401
+ const resolved = lookup(source, path, scope);
1402
+ if (resolved === void 0 || resolved === null) return "";
1403
+ const text = String(resolved);
1404
+ return substitute === void 0 ? text : substitute(text, source);
1405
+ });
1406
+ }
1407
+ if (Array.isArray(value)) return value.map((entry) => resolveTemplates(entry, scope, substitute));
1408
+ if (typeof value === "object" && value !== null) {
1409
+ const out = {};
1410
+ for (const [key, entry] of Object.entries(value)) {
1411
+ out[key] = resolveTemplates(entry, scope, substitute);
1412
+ }
1413
+ return out;
1414
+ }
1415
+ return value;
1416
+ }
1417
+ function resolveHeaders(raw, scope) {
1418
+ const out = {};
1419
+ for (const [name, value] of Object.entries(raw ?? {})) {
1420
+ const resolved = resolveTemplates(value, scope, intoHeader(name));
1421
+ if (resolved === void 0 || resolved === null || resolved === "") continue;
1422
+ out[name] = String(resolved);
1423
+ }
1424
+ return out;
1425
+ }
1426
+ function stringMap(raw) {
1427
+ const out = {};
1428
+ if (typeof raw !== "object" || raw === null) return out;
1429
+ for (const [key, value] of Object.entries(raw)) {
1430
+ if (value === void 0 || value === null || value === "") continue;
1431
+ out[key] = String(value);
1432
+ }
1433
+ return out;
1434
+ }
1435
+ function resolveRequest(request, scope) {
1436
+ const method = (request.method ?? "GET").toUpperCase();
1437
+ const rawUrl = resolveTemplates(request.url, scope, intoUrl);
1438
+ if (typeof rawUrl !== "string" || rawUrl.trim() === "") {
1439
+ throw new Error("Request has no URL once its templates are resolved");
1440
+ }
1441
+ let url;
1442
+ try {
1443
+ url = new URL(rawUrl);
1444
+ } catch {
1445
+ throw new Error(`Request URL is not a URL once its templates are resolved: "${rawUrl}"`);
1446
+ }
1447
+ for (const [key, value] of Object.entries(stringMap(resolveTemplates(request.query, scope)))) {
1448
+ url.searchParams.set(key, value);
1449
+ }
1450
+ const headers = resolveHeaders(request.headers, scope);
1451
+ const resolved = { url: url.toString(), method, headers };
1452
+ if (request.body !== void 0 && method !== "GET" && method !== "HEAD") {
1453
+ const body = resolveTemplates(request.body, scope);
1454
+ if (body !== void 0) {
1455
+ resolved.body = typeof body === "string" ? body : JSON.stringify(body);
1456
+ if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) {
1457
+ resolved.headers["content-type"] = typeof body === "string" ? "text/plain" : "application/json";
1458
+ }
1459
+ }
1460
+ }
1461
+ return resolved;
1462
+ }
1463
+ async function readBody(response) {
1464
+ const text = await response.text();
1465
+ if (text === "") return void 0;
1466
+ const type = response.headers.get("content-type") ?? "";
1467
+ if (!type.includes("json")) return text;
1468
+ try {
1469
+ return JSON.parse(text);
1470
+ } catch {
1471
+ return text;
1472
+ }
1473
+ }
1474
+ function describeFailure(response, body) {
1475
+ const detail = typeof body === "string" ? body : body === void 0 ? "" : JSON.stringify(body);
1476
+ const quoted = detail.length > MAX_ERROR_BODY ? `${detail.slice(0, MAX_ERROR_BODY)}\u2026` : detail;
1477
+ return `Request failed with ${response.status} ${response.statusText}${quoted ? `: ${quoted}` : ""}`;
1478
+ }
1479
+ async function sendRequest(resolved, options) {
1480
+ const response = await options.fetchImpl(resolved.url, {
1481
+ method: resolved.method,
1482
+ headers: resolved.headers,
1483
+ ...resolved.body !== void 0 && { body: resolved.body }
1484
+ });
1485
+ const body = await readBody(response);
1486
+ if (!response.ok) throw new Error(describeFailure(response, body));
1487
+ return { response, body };
1488
+ }
1489
+ function asOutput(value) {
1490
+ if (Array.isArray(value)) return { items: value };
1491
+ if (typeof value === "object" && value !== null) return value;
1492
+ return value === void 0 ? {} : { result: value };
1493
+ }
1494
+ var MAX_REQUEST_PAGES = 100;
1495
+ var LINK_NEXT = /<([^>]+)>\s*;[^,]*\brel\s*=\s*"?next"?/i;
1496
+ function nextLink(header) {
1497
+ const match = header === null ? null : LINK_NEXT.exec(header);
1498
+ return match ? match[1] : void 0;
1499
+ }
1500
+ function pageItems(body, itemsPath) {
1501
+ const value = itemsPath === void 0 ? body : valueAt(body, itemsPath);
1502
+ return Array.isArray(value) ? value : void 0;
1503
+ }
1504
+ async function collectPages(request, strategy, scope, options) {
1505
+ const collected = [];
1506
+ const seen = /* @__PURE__ */ new Set();
1507
+ let page2 = strategy.kind === "page" ? strategy.startPage ?? 1 : 0;
1508
+ let cursor;
1509
+ let nextUrl;
1510
+ for (let index = 0; index < MAX_REQUEST_PAGES; index++) {
1511
+ const resolved = resolveRequest(request, scope);
1512
+ if (nextUrl !== void 0) resolved.url = nextUrl;
1513
+ if (strategy.kind === "cursor" && cursor !== void 0) {
1514
+ const url = new URL(resolved.url);
1515
+ url.searchParams.set(strategy.param, cursor);
1516
+ resolved.url = url.toString();
1517
+ }
1518
+ if (strategy.kind === "page") {
1519
+ const url = new URL(resolved.url);
1520
+ url.searchParams.set(strategy.param, String(page2));
1521
+ resolved.url = url.toString();
1522
+ }
1523
+ if (seen.has(resolved.url)) {
1524
+ throw new Error(`Request for ${request.url} asked for the same page twice`);
1525
+ }
1526
+ seen.add(resolved.url);
1527
+ const { response, body } = await sendRequest(resolved, options);
1528
+ const items = pageItems(body, strategy.itemsPath);
1529
+ if (items === void 0) {
1530
+ if (index === 0) {
1531
+ const where = strategy.itemsPath === void 0 ? "the response is not a list" : `the response has no list at "${strategy.itemsPath}"`;
1532
+ throw new Error(`Cannot page through this request: ${where}`);
1533
+ }
1534
+ return collected;
1535
+ }
1536
+ collected.push(...items);
1537
+ if (items.length === 0) return collected;
1538
+ if (strategy.kind === "cursor") {
1539
+ const next = valueAt(body, strategy.cursorPath);
1540
+ if (next === void 0 || next === null || next === "") return collected;
1541
+ cursor = String(next);
1542
+ continue;
1543
+ }
1544
+ if (strategy.kind === "link") {
1545
+ nextUrl = nextLink(response.headers.get("link"));
1546
+ if (nextUrl === void 0) return collected;
1547
+ continue;
1548
+ }
1549
+ page2 += 1;
1550
+ }
1551
+ throw new Error(`Request for ${request.url} exceeded ${MAX_REQUEST_PAGES} pages`);
1552
+ }
1553
+ async function executeRequest(request, postReceive, scope, options) {
1554
+ if (request.paginate) {
1555
+ const items = await collectPages(request, request.paginate, scope, options);
1556
+ return asOutput(applyPostReceive(items, postReceive));
1557
+ }
1558
+ const { body } = await sendRequest(resolveRequest(request, scope), options);
1559
+ return asOutput(applyPostReceive(body, postReceive));
1560
+ }
1561
+
1562
+ // src/resilience.ts
1563
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
1564
+ var DEFAULT_ATTEMPTS = 3;
1565
+ var DEFAULT_BASE_DELAY_MS = 250;
1566
+ var DEFAULT_MAX_DELAY_MS = 3e4;
1567
+ var MAX_ATTEMPTS = 10;
1568
+ var MAX_TOTAL_WAIT_MS = 12e4;
1569
+ var wait = (ms) => new Promise((resolve4) => {
1570
+ setTimeout(resolve4, ms);
1571
+ });
1572
+ function retryAfterMs(header, now) {
1573
+ if (!header) return void 0;
1574
+ const trimmed = header.trim();
1575
+ if (trimmed === "") return void 0;
1576
+ const seconds = Number(trimmed);
1577
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
1578
+ const at = Date.parse(trimmed);
1579
+ if (Number.isNaN(at)) return void 0;
1580
+ return Math.max(0, at - now);
1581
+ }
1582
+ function backoffMs(attempt, policy = {}) {
1583
+ const base = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
1584
+ const max = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
1585
+ return Math.min(max, base * 2 ** attempt);
1586
+ }
1587
+ function isFinal(error) {
1588
+ return error?.retryable === false;
1589
+ }
1590
+ function resilientFetch(options) {
1591
+ const attempts = Math.min(MAX_ATTEMPTS, Math.max(1, options.retry?.attempts ?? DEFAULT_ATTEMPTS));
1592
+ const sleep = options.sleep ?? wait;
1593
+ const ceiling = options.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
1594
+ const send = async (input, init) => {
1595
+ let waited = 0;
1596
+ const pause = async (ms) => {
1597
+ if (waited + ms > MAX_TOTAL_WAIT_MS) return false;
1598
+ waited += ms;
1599
+ await sleep(ms);
1600
+ return true;
1601
+ };
1602
+ for (let attempt = 0; attempt < attempts; attempt++) {
1603
+ const last = attempt === attempts - 1;
1604
+ try {
1605
+ const response = await options.fetchImpl(input, init);
1606
+ if (!RETRYABLE_STATUS.has(response.status)) return response;
1607
+ if (!options.retryable || last) return response;
1608
+ const asked = retryAfterMs(response.headers.get("retry-after"), Date.now());
1609
+ const delay = asked === void 0 ? backoffMs(attempt, options.retry) : Math.min(asked, ceiling);
1610
+ if (!await pause(delay)) return response;
1611
+ } catch (error) {
1612
+ if (!options.retryable || last || isFinal(error)) throw error;
1613
+ if (!await pause(backoffMs(attempt, options.retry))) throw error;
1614
+ }
1615
+ }
1616
+ throw new Error("Request was never attempted");
1617
+ };
1618
+ return send;
1619
+ }
1620
+
1621
+ // src/runtime.ts
1622
+ function wrap(fetchImpl, options, retryable) {
1623
+ return resilientFetch({
1624
+ fetchImpl,
1625
+ retryable,
1626
+ ...options.retry !== void 0 && { retry: options.retry },
1627
+ ...options.sleep !== void 0 && { sleep: options.sleep }
1628
+ });
1629
+ }
1630
+ function sessionFor(connector, options, retryable) {
1631
+ if (connector.auth?.rung !== "browser") return void 0;
1632
+ const fetchImpl = options.sessionFetchImpl ?? createSessionFetch(options.sessionCall ? { call: options.sessionCall } : {});
1633
+ return { fetch: wrap(fetchImpl, options, retryable) };
1634
+ }
1635
+ var MAX_POLL_PAGES = 1e3;
1636
+ async function runPoll(connector, triggerType, options = {}) {
1637
+ const trigger = connector.triggers.find((entry) => entry.type === triggerType);
1638
+ if (!trigger) {
1639
+ throw new Error(`Connector ${connector.id} has no trigger "${triggerType}"`);
1640
+ }
1641
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1642
+ const polledAt = now();
1643
+ const session = sessionFor(connector, options, true);
1644
+ const context = {
1645
+ config: options.config ?? {},
1646
+ ...options.since !== void 0 && { since: options.since },
1647
+ ...options.cursor !== void 0 && { cursor: options.cursor },
1648
+ ...options.limit !== void 0 && { limit: options.limit },
1649
+ now,
1650
+ // A poll only reads, so every failure it meets is worth trying again.
1651
+ fetch: wrap(options.fetchImpl ?? globalThis.fetch, options, true),
1652
+ ...session && { session }
1653
+ };
1654
+ const outcome = typeof trigger.poll === "function" ? await trigger.poll(context) : await pollWithDedupe(trigger, context);
1655
+ if (!outcome || !Array.isArray(outcome.items)) {
1656
+ throw new Error(`Trigger ${triggerType} did not return an items array`);
1657
+ }
1658
+ if (outcome.hasMore && !outcome.nextCursor) {
1659
+ throw new Error(`Trigger ${triggerType} reported more pages without a nextCursor`);
1660
+ }
1661
+ return {
1662
+ items: normalizeItems(outcome.items, polledAt),
1663
+ ...outcome.nextCursor !== void 0 && { nextCursor: outcome.nextCursor },
1664
+ hasMore: outcome.hasMore === true
1665
+ };
1666
+ }
1667
+ async function drainPoll(connector, triggerType, options = {}) {
1668
+ const collected = [];
1669
+ let cursor = options.cursor;
1670
+ for (let page2 = 0; page2 < MAX_POLL_PAGES; page2++) {
1671
+ const result = await runPoll(connector, triggerType, {
1672
+ ...options,
1673
+ ...cursor !== void 0 && { cursor }
1674
+ });
1675
+ collected.push(...result.items);
1676
+ if (!result.hasMore) return collected;
1677
+ if (result.nextCursor === cursor) {
1678
+ throw new Error(`Trigger ${triggerType} did not advance its cursor`);
1679
+ }
1680
+ cursor = result.nextCursor;
1681
+ }
1682
+ throw new Error(`Trigger ${triggerType} exceeded ${MAX_POLL_PAGES} pages`);
1683
+ }
1684
+ var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
1685
+ async function runOptions(connector, name, options = {}) {
1686
+ const loader = connector.options?.[name];
1687
+ if (!loader) {
1688
+ throw new Error(`Connector ${connector.id} serves no options set "${name}"`);
1689
+ }
1690
+ const session = sessionFor(connector, options, true);
1691
+ const loaded = await loader({
1692
+ config: options.config ?? {},
1693
+ now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
1694
+ fetch: wrap(options.fetchImpl ?? globalThis.fetch, options, true),
1695
+ ...session && { session }
1696
+ });
1697
+ if (!Array.isArray(loaded)) {
1698
+ throw new Error(`Options set "${name}" did not return an array`);
1699
+ }
1700
+ return loaded.map(
1701
+ (entry) => typeof entry === "string" ? { value: entry } : { ...entry, value: String(entry.value) }
1702
+ );
1703
+ }
1704
+ var MAX_QUOTED_VALUE = 80;
1705
+ function quote(value) {
1706
+ return value.length > MAX_QUOTED_VALUE ? `${value.slice(0, MAX_QUOTED_VALUE)}\u2026` : value;
1707
+ }
1708
+ function coerceArg(value, type) {
1709
+ if (typeof value !== "string") return value;
1710
+ if (type === "number") {
1711
+ const parsed = Number(value);
1712
+ if (Number.isNaN(parsed)) throw new Error(`Expected a number, got "${quote(value)}"`);
1713
+ return parsed;
1714
+ }
1715
+ if (type === "boolean") {
1716
+ if (value === "true") return true;
1717
+ if (value === "false") return false;
1718
+ throw new Error(`Expected a boolean, got "${quote(value)}"`);
1719
+ }
1720
+ if (type === "json") {
1721
+ try {
1722
+ return JSON.parse(value);
1723
+ } catch {
1724
+ throw new Error(`Expected JSON, got "${quote(value)}"`);
1725
+ }
1726
+ }
1727
+ return value;
1728
+ }
1729
+ async function runAction(connector, actionType, args, options = {}) {
1730
+ const action = connector.actions.find((entry) => entry.type === actionType);
1731
+ if (!action) {
1732
+ throw new Error(`Connector ${connector.id} has no action "${actionType}"`);
1733
+ }
1734
+ const coerced = { ...args };
1735
+ for (const input of action.inputs ?? []) {
1736
+ const value = coerced[input.key];
1737
+ if (value === void 0 || value === "") {
1738
+ if (input.required) throw new Error(`Action ${actionType} requires "${input.key}"`);
1739
+ delete coerced[input.key];
1740
+ continue;
1741
+ }
1742
+ try {
1743
+ coerced[input.key] = coerceArg(value, input.type);
1744
+ } catch (error) {
1745
+ throw new Error(
1746
+ `Action ${actionType} argument "${input.key}": ${error instanceof Error ? error.message : String(error)}`,
1747
+ { cause: error }
1748
+ );
1749
+ }
1750
+ }
1751
+ const config = options.config ?? {};
1752
+ const method = (action.request?.method ?? "GET").toUpperCase();
1753
+ const retryable = action.idempotent === true || action.request !== void 0 && SAFE_METHODS.has(method);
1754
+ const fetchImpl = wrap(options.fetchImpl ?? globalThis.fetch, options, retryable);
1755
+ const session = sessionFor(connector, options, retryable);
1756
+ if (action.request !== void 0) {
1757
+ try {
1758
+ return await executeRequest(
1759
+ action.request,
1760
+ action.postReceive,
1761
+ { args: coerced, config },
1762
+ { fetchImpl: session?.fetch ?? fetchImpl }
1763
+ );
1764
+ } catch (error) {
1765
+ throw new Error(
1766
+ `Action ${actionType}: ${error instanceof Error ? error.message : String(error)}`,
1767
+ { cause: error }
1768
+ );
1769
+ }
1770
+ }
1771
+ if (typeof action.run !== "function") {
1772
+ throw new Error(`Action ${actionType} has neither a run() implementation nor a request`);
1773
+ }
1774
+ const output = await action.run(coerced, {
1775
+ config,
1776
+ now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
1777
+ fetch: fetchImpl,
1778
+ ...session && { session }
1779
+ });
1780
+ return output ?? {};
1781
+ }
1782
+
1783
+ // src/harness.ts
1784
+ function matches(route, method, url) {
1785
+ if (route.method && route.method.toUpperCase() !== method) return false;
1786
+ if (route.url instanceof RegExp) return route.url.test(url);
1787
+ let pathname;
1788
+ try {
1789
+ pathname = new URL(url).pathname;
1790
+ } catch {
1791
+ return false;
1792
+ }
1793
+ return route.url.endsWith("/") ? pathname.startsWith(route.url) : pathname === route.url;
1794
+ }
1795
+ function reply(route) {
1796
+ const body = typeof route.body === "string" ? route.body : JSON.stringify(route.body ?? {});
1797
+ return new Response(body, {
1798
+ status: route.status ?? 200,
1799
+ headers: { "content-type": "application/json", ...route.headers }
1800
+ });
1801
+ }
1802
+ var MockRouteMissError = class extends Error {
1803
+ constructor(method, url) {
1804
+ super(`No mock route for ${method} ${url}`);
1805
+ this.name = "MockRouteMissError";
1806
+ }
1807
+ };
1808
+ function escapedMockHttp(error) {
1809
+ for (let current = error; current instanceof Error; current = current.cause) {
1810
+ if (current instanceof MockRouteMissError) return true;
1811
+ if (current.message.includes("No mock route for ")) return true;
1812
+ }
1813
+ return false;
1814
+ }
1815
+ var serving = false;
1816
+ async function withMockHttp(routes, body) {
1817
+ if (serving) {
1818
+ throw new Error(
1819
+ "withMockHttp is already serving; give one call every route it needs rather than installing a second stub"
1820
+ );
1821
+ }
1822
+ serving = true;
1823
+ const calls = [];
1824
+ const original = globalThis.fetch;
1825
+ globalThis.fetch = (async (input, init) => {
1826
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
1827
+ const own = typeof input === "object" && "method" in input ? input.method : void 0;
1828
+ const method = (init?.method ?? own ?? "GET").toUpperCase();
1829
+ calls.push({
1830
+ method,
1831
+ url,
1832
+ ...typeof init?.body === "string" && { body: init.body }
1833
+ });
1834
+ const route = routes.find((candidate) => matches(candidate, method, url));
1835
+ if (!route) throw new MockRouteMissError(method, url);
1836
+ return reply(route);
1837
+ });
1838
+ try {
1839
+ return { result: await body(), calls };
1840
+ } finally {
1841
+ globalThis.fetch = original;
1842
+ serving = false;
1843
+ }
1844
+ }
1845
+ function createConnectorHarness(connector, harnessOptions = {}) {
1846
+ const signedIn = harnessOptions.sessionFetchImpl ?? harnessOptions.fetchImpl;
1847
+ const defaults = (options = {}) => ({
1848
+ ...harnessOptions.config && { config: harnessOptions.config },
1849
+ ...harnessOptions.now && { now: harnessOptions.now },
1850
+ ...harnessOptions.fetchImpl && { fetchImpl: harnessOptions.fetchImpl },
1851
+ ...signedIn && { sessionFetchImpl: signedIn },
1852
+ ...harnessOptions.sleep && { sleep: harnessOptions.sleep },
1853
+ ...options
1854
+ });
1855
+ return {
1856
+ poll: (triggerType, options) => runPoll(connector, triggerType, defaults(options)),
1857
+ drain: (triggerType, options) => drainPoll(connector, triggerType, defaults(options)),
1858
+ execute: (actionType, args = {}) => runAction(connector, actionType, args, {
1859
+ ...harnessOptions.config && { config: harnessOptions.config },
1860
+ ...harnessOptions.now && { now: harnessOptions.now },
1861
+ ...harnessOptions.fetchImpl && { fetchImpl: harnessOptions.fetchImpl },
1862
+ ...signedIn && { sessionFetchImpl: signedIn },
1863
+ ...harnessOptions.sleep && { sleep: harnessOptions.sleep }
1864
+ }),
1865
+ manifest: () => connectorManifest(connector),
1866
+ async pollTwice(triggerType, options) {
1867
+ const first = await drainPoll(connector, triggerType, defaults(options));
1868
+ const watermark = first.reduce(
1869
+ (newest, item) => newest === void 0 || item.updatedAt > newest ? item.updatedAt : newest,
1870
+ options?.since
1871
+ );
1872
+ const second = await drainPoll(
1873
+ connector,
1874
+ triggerType,
1875
+ defaults({ ...options, ...watermark !== void 0 && { since: watermark } })
1876
+ );
1877
+ return watermark === void 0 ? second : second.filter((item) => item.updatedAt > watermark);
1878
+ },
1879
+ withMockHttp
1880
+ };
1881
+ }
1882
+ var HOST_FIXTURES = {
1883
+ diff: async () => "diff --git a/src/index.ts b/src/index.ts\n@@ -1 +1 @@\n-const a = 1\n+const a = 2\n",
1884
+ status: async () => " M src/index.ts\n",
1885
+ output: async () => "$ yarn test\n Test Files 1 passed (1)\n",
1886
+ selection: async () => "",
1887
+ send: async () => {
1888
+ },
1889
+ rename: async () => {
1890
+ },
1891
+ usage: async () => ({
1892
+ contextTokens: 13e4,
1893
+ contextWindow: 1e6,
1894
+ cacheHitRate: 0.93,
1895
+ limits: [{ window: "5h", remaining: 0.72 }]
1896
+ })
1897
+ };
1898
+ function mockExtensionHost(granted, answers = {}) {
1899
+ const allowed = new Set(granted);
1900
+ const used = /* @__PURE__ */ new Set();
1901
+ const host = {};
1902
+ for (const name of Object.keys(HOST_FIXTURES)) {
1903
+ const permission = HOST_PERMISSIONS[name];
1904
+ host[name] = async (...args) => {
1905
+ used.add(permission);
1906
+ if (!allowed.has(permission)) {
1907
+ throw new PermissionDeniedError(name, `this extension does not ask for ${permission}`);
1908
+ }
1909
+ const answer = answers[name] ?? HOST_FIXTURES[name];
1910
+ return answer(...args);
1911
+ };
1912
+ }
1913
+ return { host, used };
1914
+ }
1915
+
1916
+ // src/check.ts
1917
+ import { existsSync as existsSync2 } from "fs";
1918
+ import { rm } from "fs/promises";
1919
+ import { resolve as resolve2, sep } from "path";
1920
+ var EXECUTABLE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1921
+ var CREDENTIAL_NAME = /(secret|token|password|passphrase|api[-_]?key|credential)/i;
1922
+ var INPUT_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "select", "json"]);
1923
+ function finding2(level, code, target, message) {
1924
+ return { level, code, target, message };
1925
+ }
1926
+ function authFindings(connector) {
1927
+ const auth = connector.auth;
1928
+ if (!auth) {
1929
+ return [
1930
+ finding2(
1931
+ "warn",
1932
+ "auth-undeclared",
1933
+ connector.id,
1934
+ "does not say how it signs in, so the app cannot tell anyone before they install it"
1935
+ )
1936
+ ];
1937
+ }
1938
+ const found = [];
1939
+ const command = auth.probe?.command?.trim() ?? "";
1940
+ const args = auth.probe?.args ?? [];
1941
+ if (auth.rung === "cli") {
1942
+ if (!EXECUTABLE_NAME.test(command)) {
1943
+ found.push(
1944
+ finding2(
1945
+ "error",
1946
+ "auth-probe-missing",
1947
+ `${connector.id} auth`,
1948
+ `probe command "${command}" is not a bare executable name, so the host drops it and the rung promises a sign-in it cannot ask for`
1949
+ )
1950
+ );
1951
+ }
1952
+ if (args.some((arg) => typeof arg !== "string")) {
1953
+ found.push(
1954
+ finding2(
1955
+ "error",
1956
+ "auth-probe-missing",
1957
+ `${connector.id} auth`,
1958
+ "probe arguments must all be strings, or the host drops the probe"
1959
+ )
1960
+ );
1961
+ }
1962
+ }
1963
+ return found;
1964
+ }
1965
+ function secretFindings(connector) {
1966
+ const named = new Set(connector.auth?.keys ?? []);
1967
+ return connector.config.filter((field) => !field.secret).filter((field) => named.has(field.key) || CREDENTIAL_NAME.test(field.key)).map(
1968
+ (field) => finding2(
1969
+ named.has(field.key) ? "error" : "warn",
1970
+ "secret-not-marked",
1971
+ `config ${field.key}`,
1972
+ "holds a credential but is not marked `secret`, so Vorn would store it unencrypted"
1973
+ )
1974
+ );
1975
+ }
1976
+ function actionShapeFindings(action) {
1977
+ const target = `action ${action.type}`;
1978
+ const found = [];
1979
+ if (!action.outputs?.length) {
1980
+ found.push(
1981
+ finding2(
1982
+ "warn",
1983
+ "action-no-outputs",
1984
+ target,
1985
+ "declares no outputs, so a later step has nothing to autocomplete from"
1986
+ )
1987
+ );
1988
+ }
1989
+ for (const input of action.inputs ?? []) {
1990
+ if (input.type !== void 0 && !INPUT_TYPES.has(input.type)) {
1991
+ found.push(
1992
+ finding2(
1993
+ "error",
1994
+ "input-type-unsupported",
1995
+ `${target} input ${input.key}`,
1996
+ `declares type "${input.type}", which Vorn cannot draw a field for`
1997
+ )
1998
+ );
1999
+ }
2000
+ if (input.type === "select" && !input.options?.length && !input.loadOptions) {
2001
+ found.push(
2002
+ finding2(
2003
+ "error",
2004
+ "input-type-unsupported",
2005
+ `${target} input ${input.key}`,
2006
+ "is a select with neither fixed options nor a loadOptions set to draw from"
2007
+ )
2008
+ );
2009
+ }
2010
+ }
2011
+ return found;
2012
+ }
2013
+ function bundles(options) {
2014
+ return Boolean(options.bundle && options.entry !== void 0 && options.packageDir !== void 0);
2015
+ }
2016
+ function launches(options) {
2017
+ return bundles(options) && options.mock === true;
2018
+ }
2019
+ function packageRootOf(options) {
2020
+ if (options.packageDir === void 0) return void 0;
2021
+ return packageRootFor(packageDirFor(options.packageDir, options.entry));
2022
+ }
2023
+ async function packageFindings(connector, options) {
2024
+ if (options.packageDir === void 0) return [];
2025
+ const pkg = readNearestPackageJson(packageDirFor(options.packageDir, options.entry));
2026
+ const found = [...lifecycleScriptFindings(pkg)];
2027
+ const vorn = pkg?.vorn;
2028
+ const keywords = Array.isArray(vorn?.keywords) ? vorn.keywords : [];
2029
+ if (keywords.length === 0) {
2030
+ found.push(
2031
+ finding2(
2032
+ "warn",
2033
+ "keywords-missing",
2034
+ "package.json",
2035
+ "names no keywords, so the connector is findable only by its own name"
2036
+ )
2037
+ );
2038
+ }
2039
+ if (options.bundle && options.entry !== void 0) {
2040
+ const built = await options.bundle({
2041
+ contents: packEntryContents(options.entry),
2042
+ resolveDir: options.packageDir
2043
+ });
2044
+ found.push(...bundleDependencyFindings(built.external), ...bundledRequireFindings(built.code));
2045
+ if (options.mock) {
2046
+ const dir = await stagePack(connector, built.code, packageRootOf(options));
2047
+ try {
2048
+ found.push(...await packLaunchFindings(dir));
2049
+ } finally {
2050
+ await rm(dir, { recursive: true, force: true });
2051
+ }
2052
+ }
2053
+ }
2054
+ return found;
2055
+ }
2056
+ function sampleArg(input) {
2057
+ if (input.type === "number") return "1";
2058
+ if (input.type === "boolean") return "false";
2059
+ if (input.type === "json") return "{}";
2060
+ if (input.type === "select") return input.options?.[0]?.value ?? "check";
2061
+ return "check";
2062
+ }
2063
+ function mockConfig(connector) {
2064
+ const config = {};
2065
+ for (const field of connector.config) {
2066
+ config[field.key] = field.default ?? `mock-${field.key}`;
2067
+ }
2068
+ return config;
2069
+ }
2070
+ async function mockFindings(connector, options) {
2071
+ if (!options.mock) return [];
2072
+ const config = options.config ?? mockConfig(connector);
2073
+ const routes = options.mockRoutes ?? [{ url: /.*/ }];
2074
+ const level = options.mockRoutes?.length ? "error" : "warn";
2075
+ const found = [];
2076
+ for (const action of connector.actions) {
2077
+ const args = Object.fromEntries(
2078
+ (action.inputs ?? []).map((input) => [input.key, sampleArg(input)])
2079
+ );
2080
+ const { result: thrown, calls } = await withMockHttp(routes, async () => {
2081
+ try {
2082
+ await runAction(connector, action.type, args, {
2083
+ config,
2084
+ ...options.now && { now: options.now },
2085
+ sessionFetchImpl: globalThis.fetch
2086
+ });
2087
+ return void 0;
2088
+ } catch (error) {
2089
+ return error;
2090
+ }
2091
+ });
2092
+ if (thrown !== void 0) {
2093
+ const reason = thrown instanceof Error ? thrown.message : String(thrown);
2094
+ const escaped = escapedMockHttp(thrown);
2095
+ found.push(
2096
+ finding2(
2097
+ escaped ? "error" : level,
2098
+ escaped ? "mock-network-escape" : "mock-action-failed",
2099
+ `action ${action.type}`,
2100
+ `did not run against served HTTP: ${reason}`
2101
+ )
2102
+ );
2103
+ continue;
2104
+ }
2105
+ if (calls.length === 0) {
2106
+ found.push(
2107
+ finding2(
2108
+ "warn",
2109
+ "mock-not-observed",
2110
+ `action ${action.type}`,
2111
+ "made no request the stub could see, so this run vouches for nothing it did"
2112
+ )
2113
+ );
2114
+ }
2115
+ }
2116
+ return found;
2117
+ }
2118
+ var CHECK_SESSION = {
2119
+ sessionId: "check",
2120
+ worktreePath: process.cwd(),
2121
+ agent: "claude"
2122
+ };
2123
+ var FOOTER_TONES = ["default", "ok", "danger"];
2124
+ var FOOTER_HREF_PROTOCOLS = ["http:", "https:"];
2125
+ function invalidItem(item) {
2126
+ if (!item || typeof item !== "object") return "is not an object";
2127
+ const reading = item;
2128
+ if (typeof reading.label !== "string" || reading.label === "") return "has no label";
2129
+ if (typeof reading.value !== "string") return "has no value";
2130
+ if (reading.tone !== void 0 && !FOOTER_TONES.includes(reading.tone)) {
2131
+ return `has unknown tone ${JSON.stringify(reading.tone)}`;
2132
+ }
2133
+ if (reading.href === void 0) return void 0;
2134
+ if (typeof reading.href !== "string") return "has a link that is not text";
2135
+ let protocol;
2136
+ try {
2137
+ protocol = new URL(reading.href).protocol;
2138
+ } catch {
2139
+ return `has the link ${JSON.stringify(reading.href)}, which is not a URL`;
2140
+ }
2141
+ if (!FOOTER_HREF_PROTOCOLS.includes(protocol)) {
2142
+ return `has the link ${JSON.stringify(reading.href)}; a reading links to ${FOOTER_HREF_PROTOCOLS.join(" or ")} and nothing else`;
2143
+ }
2144
+ return void 0;
2145
+ }
2146
+ async function contributionFindings(connector) {
2147
+ const contributes = connector.contributes;
2148
+ if (!contributes) return [];
2149
+ const found = [];
2150
+ const declared = connector.permissions ?? [];
2151
+ const spent = /* @__PURE__ */ new Set();
2152
+ const ran = async (kind, id, code, body) => {
2153
+ const { host, used } = mockExtensionHost(declared);
2154
+ try {
2155
+ return await body(host);
2156
+ } catch (error) {
2157
+ const reason = error instanceof Error ? error.message : String(error);
2158
+ found.push(
2159
+ finding2(
2160
+ "error",
2161
+ error instanceof PermissionDeniedError ? "permission-undeclared" : code,
2162
+ `${kind} ${id}`,
2163
+ error instanceof PermissionDeniedError ? `asked the host for something this extension does not declare: ${reason}` : `threw: ${reason}`
2164
+ )
2165
+ );
2166
+ return void 0;
2167
+ } finally {
2168
+ for (const permission of used) spent.add(permission);
2169
+ }
2170
+ };
2171
+ for (const footer of contributes.footers ?? []) {
2172
+ const items = await ran(
2173
+ "footer",
2174
+ footer.id,
2175
+ "footer-failed",
2176
+ (host) => Promise.resolve(footer.run({ ...CHECK_SESSION, host, now: () => (/* @__PURE__ */ new Date()).toISOString() }))
2177
+ );
2178
+ if (items === void 0) continue;
2179
+ if (!Array.isArray(items)) {
2180
+ found.push(
2181
+ finding2(
2182
+ "error",
2183
+ "footer-items-invalid",
2184
+ `footer ${footer.id}`,
2185
+ "returned something that is not a list of readings"
2186
+ )
2187
+ );
2188
+ continue;
2189
+ }
2190
+ for (const item of items) {
2191
+ const wrong = invalidItem(item);
2192
+ if (wrong) {
2193
+ found.push(
2194
+ finding2(
2195
+ "error",
2196
+ "footer-items-invalid",
2197
+ `footer ${footer.id}`,
2198
+ `returned a reading that ${wrong}`
2199
+ )
2200
+ );
2201
+ }
2202
+ }
2203
+ }
2204
+ for (const handler of contributes.linkHandlers ?? []) {
2205
+ await ran(
2206
+ "link handler",
2207
+ handler.id,
2208
+ "handler-failed",
2209
+ (host) => Promise.resolve(
2210
+ handler.run({
2211
+ ...CHECK_SESSION,
2212
+ host,
2213
+ now: () => (/* @__PURE__ */ new Date()).toISOString(),
2214
+ url: handler.example
2215
+ })
2216
+ )
2217
+ );
2218
+ }
2219
+ const observable = !(contributes.panes ?? []).some((pane) => pane.web !== void 0);
2220
+ for (const permission of declared) {
2221
+ if (observable && !spent.has(permission)) {
2222
+ found.push(
2223
+ finding2(
2224
+ "warn",
2225
+ "permission-unused",
2226
+ `${connector.id} permissions`,
2227
+ `asks for ${permission} but nothing this run exercised used it; ask only for what it spends`
2228
+ )
2229
+ );
2230
+ }
2231
+ }
2232
+ return found;
2233
+ }
2234
+ function paneFindings(connector, packageRoot) {
2235
+ if (packageRoot === void 0) return [];
2236
+ const root = resolve2(packageRoot);
2237
+ const found = [];
2238
+ for (const pane of connector.contributes?.panes ?? []) {
2239
+ if (pane.web === void 0) continue;
2240
+ const full = resolve2(root, pane.web);
2241
+ if (full !== root && !full.startsWith(`${root}${sep}`)) {
2242
+ found.push(
2243
+ finding2(
2244
+ "error",
2245
+ "web-entry-outside-package",
2246
+ `pane ${pane.id}`,
2247
+ `declares the page "${pane.web}", which resolves outside the package`
2248
+ )
2249
+ );
2250
+ continue;
2251
+ }
2252
+ if (!existsSync2(full)) {
2253
+ found.push(
2254
+ finding2(
2255
+ "error",
2256
+ "web-entry-missing",
2257
+ `pane ${pane.id}`,
2258
+ `declares the page "${pane.web}", which the package does not carry`
2259
+ )
2260
+ );
2261
+ }
2262
+ }
2263
+ return found;
2264
+ }
2265
+ var AUTH_FAILURE = /\b(401|403|unauthor|unauthenticat|forbidden|invalid[- ]?(token|credential))/i;
2266
+ function liveRunnable(action) {
2267
+ if (action.idempotent !== true) return false;
2268
+ if (action.sample !== void 0) return true;
2269
+ return !(action.inputs ?? []).some((input) => input.required === true);
2270
+ }
2271
+ function liveExamines(connector) {
2272
+ return connector.preflight !== void 0 || connector.actions.some(liveRunnable);
2273
+ }
2274
+ function needsWindow(error) {
2275
+ return error instanceof SessionUnavailableError || error instanceof Error && error.cause instanceof SessionUnavailableError;
2276
+ }
2277
+ async function liveFindings(connector, options) {
2278
+ if (!options.live) return [];
2279
+ const found = [];
2280
+ if (connector.preflight) {
2281
+ try {
2282
+ const result = await connector.preflight();
2283
+ if (!result.ok) {
2284
+ found.push(
2285
+ finding2(
2286
+ "error",
2287
+ "preflight-failed",
2288
+ connector.id,
2289
+ result.message ?? "reported that it is not ready, without saying why"
2290
+ )
2291
+ );
2292
+ return found;
2293
+ }
2294
+ } catch (error) {
2295
+ const reason = error instanceof Error ? error.message : String(error);
2296
+ found.push(finding2("error", "preflight-failed", connector.id, `threw: ${reason}`));
2297
+ return found;
2298
+ }
2299
+ }
2300
+ for (const action of connector.actions.filter(liveRunnable)) {
2301
+ try {
2302
+ await runAction(connector, action.type, action.sample ?? {}, {
2303
+ config: options.config ?? {},
2304
+ ...options.now && { now: options.now }
2305
+ });
2306
+ } catch (error) {
2307
+ if (needsWindow(error)) continue;
2308
+ const reason = error instanceof Error ? error.message : String(error);
2309
+ found.push(
2310
+ finding2(
2311
+ AUTH_FAILURE.test(reason) ? "error" : "warn",
2312
+ "live-action-failed",
2313
+ `action ${action.type}`,
2314
+ `threw: ${reason}`
2315
+ )
2316
+ );
2317
+ }
2318
+ }
2319
+ return found;
2320
+ }
2321
+ function sampleTrigger(trigger) {
2322
+ return { ...trigger, poll: void 0, fetch: () => trigger.sample ?? [] };
2323
+ }
2324
+ async function checkPollBehaviour(connector, trigger, options) {
2325
+ const found = [];
2326
+ const probe = { ...connector, triggers: [trigger] };
2327
+ const target = `trigger ${trigger.type}`;
2328
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
2329
+ const attempt = async (cursor, code, what) => {
2330
+ try {
2331
+ return await runPoll(probe, trigger.type, {
2332
+ config: options.config ?? {},
2333
+ ...cursor !== void 0 && { cursor },
2334
+ now
2335
+ });
2336
+ } catch (error) {
2337
+ const reason = error instanceof Error ? error.message : String(error);
2338
+ return finding2("error", code, target, `${what} threw: ${reason}`);
2339
+ }
2340
+ };
2341
+ const first = await attempt(void 0, "poll-failed", "first poll");
2342
+ if ("level" in first) return [first];
2343
+ if (first.items.length === 0) {
2344
+ found.push(
2345
+ finding2("warn", "no-items", target, "returned nothing, so delivery could not be verified")
2346
+ );
2347
+ return found;
2348
+ }
2349
+ if (first.nextCursor === void 0) {
2350
+ found.push(
2351
+ finding2(
2352
+ "error",
2353
+ "no-cursor",
2354
+ target,
2355
+ "returned items but no nextCursor, so every poll will redeliver them"
2356
+ )
2357
+ );
2358
+ return found;
2359
+ }
2360
+ const second = await attempt(
2361
+ first.nextCursor,
2362
+ "cursor-rejected",
2363
+ "re-polling with its own nextCursor"
2364
+ );
2365
+ if ("level" in second) return [...found, second];
2366
+ const delivered = new Set(first.items.map((item) => item.externalId));
2367
+ const repeated = second.items.filter((item) => delivered.has(item.externalId));
2368
+ if (repeated.length > 0) {
2369
+ found.push(
2370
+ finding2(
2371
+ "error",
2372
+ "redelivers-items",
2373
+ target,
2374
+ `re-polling with its own nextCursor returned ${repeated.length} already-delivered item(s), starting with "${repeated[0].externalId}"`
2375
+ )
2376
+ );
2377
+ }
2378
+ if (second.hasMore && second.nextCursor === first.nextCursor) {
2379
+ found.push(
2380
+ finding2("error", "stuck-cursor", target, "reports more pages but its cursor never advances")
2381
+ );
2382
+ }
2383
+ return found;
2384
+ }
2385
+ async function checkConnector(connector, options = {}) {
2386
+ const found = [];
2387
+ if (!connector.description?.trim()) {
2388
+ found.push(
2389
+ finding2(
2390
+ "warn",
2391
+ "missing-description",
2392
+ connector.id,
2393
+ "has no description; agents use it to decide when the connector applies"
2394
+ )
2395
+ );
2396
+ }
2397
+ if (connector.kind !== "extension") found.push(...authFindings(connector));
2398
+ found.push(...secretFindings(connector));
2399
+ found.push(...paneFindings(connector, packageRootOf(options)));
2400
+ found.push(...await contributionFindings(connector));
2401
+ found.push(...await packageFindings(connector, options));
2402
+ found.push(...await mockFindings(connector, options));
2403
+ found.push(...await liveFindings(connector, options));
2404
+ const perTrigger = await Promise.all(
2405
+ connector.triggers.map(async (trigger) => {
2406
+ const target = `trigger ${trigger.type}`;
2407
+ const triggerFindings = [];
2408
+ if (!trigger.description?.trim()) {
2409
+ triggerFindings.push(finding2("warn", "missing-description", target, "has no description"));
2410
+ }
2411
+ if (options.live) {
2412
+ triggerFindings.push(...await checkPollBehaviour(connector, trigger, options));
2413
+ } else if (!trigger.sample?.length) {
2414
+ triggerFindings.push(
2415
+ finding2(
2416
+ "warn",
2417
+ "unverifiable",
2418
+ target,
2419
+ "has no sample items and no credentials were supplied, so nothing could be verified"
2420
+ )
2421
+ );
2422
+ } else if (!trigger.dedupe) {
2423
+ triggerFindings.push(
2424
+ finding2(
2425
+ "warn",
2426
+ "sample-unusable",
2427
+ target,
2428
+ "declares sample items but implements poll() directly, so they cannot be replayed; re-run with --live"
2429
+ )
2430
+ );
2431
+ } else {
2432
+ triggerFindings.push(
2433
+ ...await checkPollBehaviour(connector, sampleTrigger(trigger), options)
2434
+ );
2435
+ }
2436
+ return triggerFindings;
2437
+ })
2438
+ );
2439
+ found.push(...perTrigger.flat());
2440
+ for (const action of connector.actions) {
2441
+ const target = `action ${action.type}`;
2442
+ if (!action.description?.trim()) {
2443
+ found.push(
2444
+ finding2("warn", "missing-description", target, "has no description for the agent to read")
2445
+ );
2446
+ }
2447
+ if (action.idempotent === void 0) {
2448
+ found.push(
2449
+ finding2(
2450
+ "warn",
2451
+ "missing-idempotent",
2452
+ target,
2453
+ "does not declare `idempotent`, so an agent cannot tell whether retrying is safe"
2454
+ )
2455
+ );
2456
+ }
2457
+ found.push(...actionShapeFindings(action));
2458
+ for (const input of action.inputs ?? []) {
2459
+ if (!input.description?.trim()) {
2460
+ found.push(
2461
+ finding2(
2462
+ "warn",
2463
+ "missing-description",
2464
+ `${target} input ${input.key}`,
2465
+ "has no description"
2466
+ )
2467
+ );
2468
+ }
2469
+ }
2470
+ }
2471
+ return found;
2472
+ }
2473
+ var CHECK_OWNERS = {
2474
+ "pack-too-large": null,
2475
+ "missing-description": "manifest",
2476
+ "auth-undeclared": "auth",
2477
+ "auth-probe-missing": "auth",
2478
+ "secret-not-marked": "secrets",
2479
+ "action-no-outputs": "actions",
2480
+ "input-type-unsupported": "actions",
2481
+ "missing-idempotent": "actions",
2482
+ "poll-failed": "dedupe",
2483
+ "no-items": "dedupe",
2484
+ "no-cursor": "dedupe",
2485
+ "cursor-rejected": "dedupe",
2486
+ "redelivers-items": "dedupe",
2487
+ "stuck-cursor": "dedupe",
2488
+ unverifiable: "dedupe",
2489
+ "sample-unusable": "dedupe",
2490
+ "lifecycle-scripts": "no-lifecycle-scripts",
2491
+ "keywords-missing": "keywords",
2492
+ "runtime-dependencies": "no-runtime-deps",
2493
+ "mock-action-failed": "mock",
2494
+ "mock-network-escape": "mock",
2495
+ "mock-not-observed": "mock",
2496
+ "preflight-failed": "live",
2497
+ "live-action-failed": "live",
2498
+ "pack-launch": "launch",
2499
+ "web-entry-missing": "contributes",
2500
+ "web-entry-outside-package": "contributes",
2501
+ "footer-failed": "footers",
2502
+ "footer-items-invalid": "footers",
2503
+ "handler-failed": "handlers",
2504
+ "permission-undeclared": "permissions",
2505
+ "permission-unused": "permissions"
2506
+ };
2507
+ function checksRun(connector, options) {
2508
+ const names = ["manifest"];
2509
+ if (connector.kind !== "extension") names.push("auth");
2510
+ const contributes = connector.contributes;
2511
+ if (contributes?.panes?.length && options.packageDir !== void 0) names.push("contributes");
2512
+ if (contributes?.footers?.length) names.push("footers");
2513
+ if (contributes?.linkHandlers?.length) names.push("handlers");
2514
+ if (connector.permissions !== void 0) names.push("permissions");
2515
+ if (connector.config.length > 0) names.push("secrets");
2516
+ if (connector.actions.length > 0) names.push("actions");
2517
+ if (connector.triggers.length > 0) names.push("dedupe");
2518
+ if (options.packageDir !== void 0) names.push("no-lifecycle-scripts", "keywords");
2519
+ if (bundles(options)) names.push("no-runtime-deps");
2520
+ if (launches(options)) names.push("launch");
2521
+ if (options.mock && connector.actions.length > 0) names.push("mock");
2522
+ if (options.live && liveExamines(connector)) names.push("live");
2523
+ return names;
2524
+ }
2525
+ async function runConformance(connector, options = {}) {
2526
+ const findings = await checkConnector(connector, options);
2527
+ const spoiled = new Set(findings.map((item) => CHECK_OWNERS[item.code]).filter(Boolean));
2528
+ const passed = checksRun(connector, options).filter((name) => !spoiled.has(name));
2529
+ const failed = findings.some((item) => item.level === "error");
2530
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
2531
+ return {
2532
+ findings,
2533
+ passed,
2534
+ // A receipt listing nothing would read as verified while vouching for
2535
+ // nothing at all, which is worse than saying nothing.
2536
+ ...!failed && passed.length > 0 && {
2537
+ receipt: {
2538
+ schema: 1,
2539
+ version: connector.version,
2540
+ checkedAt: now(),
2541
+ checks: passed
2542
+ }
2543
+ }
2544
+ };
2545
+ }
2546
+ function formatFindings(findings) {
2547
+ return findings.map((item) => `${item.level.padEnd(5)} ${item.target}: ${item.message} [${item.code}]`).join("\n");
2548
+ }
2549
+
2550
+ // src/pack.ts
2551
+ import { existsSync as existsSync3 } from "fs";
2552
+ import { mkdir, rm as rm2, stat as stat2 } from "fs/promises";
2553
+ import { join as join2, resolve as resolve3 } from "path";
2554
+ function finding3(code, target, message) {
2555
+ return { level: "error", code, target, message };
2556
+ }
2557
+ function packFileName(connector) {
2558
+ return `${connector.id}-${connector.version}.vorn.tgz`;
2559
+ }
2560
+ async function packConnector(connector, options) {
2561
+ const resolveDir = resolve3(options.resolveDir ?? process.cwd());
2562
+ const entryDir = packageDirFor(resolveDir, options.entry);
2563
+ const packageRoot = packageRootFor(entryDir);
2564
+ const findings = await checkConnector(connector, { packageDir: packageRoot });
2565
+ findings.push(...lifecycleScriptFindings(readNearestPackageJson(entryDir)));
2566
+ if (findings.some((item) => item.level === "error")) return { findings };
2567
+ const contents = packEntryContents(options.entry, options.sdkModule);
2568
+ const bundle = options.bundle ?? esbuildBundle;
2569
+ const built = await bundle({ contents, resolveDir });
2570
+ findings.push(...bundleDependencyFindings(built.external), ...bundledRequireFindings(built.code));
2571
+ if (findings.some((item) => item.level === "error")) return { findings };
2572
+ const outDir = resolve3(options.outDir ?? process.cwd());
2573
+ await mkdir(outDir, { recursive: true });
2574
+ const file = join2(outDir, packFileName(connector));
2575
+ const staging = await stagePack(connector, built.code, packageRoot);
2576
+ try {
2577
+ findings.push(...await (options.launch ?? packLaunchFindings)(staging));
2578
+ if (findings.some((item) => item.level === "error")) return { findings };
2579
+ const unpacked = await directoryBytes(staging);
2580
+ const maxUnpacked = options.maxUnpackedBytes ?? MAX_UNPACKED_BYTES;
2581
+ if (unpacked > maxUnpacked) {
2582
+ return {
2583
+ findings: [
2584
+ ...findings,
2585
+ finding3(
2586
+ "pack-too-large",
2587
+ "bundle",
2588
+ `The pack unpacks to ${Math.round(unpacked / 1024)} KB; Vorn unpacks at most ${Math.round(maxUnpacked / 1024)} KB`
2589
+ )
2590
+ ]
2591
+ };
2592
+ }
2593
+ const { create } = await import("tar");
2594
+ await create({ gzip: true, file, cwd: staging }, [
2595
+ "manifest.json",
2596
+ "index.js",
2597
+ ...existsSync3(join2(staging, WEB_DIR)) ? [WEB_DIR] : []
2598
+ ]);
2599
+ } finally {
2600
+ await rm2(staging, { recursive: true, force: true });
2601
+ }
2602
+ const bytes = (await stat2(file)).size;
2603
+ const maxBytes = options.maxBytes ?? MAX_PACK_BYTES;
2604
+ if (bytes > maxBytes) {
2605
+ await rm2(file, { force: true });
2606
+ return {
2607
+ findings: [
2608
+ ...findings,
2609
+ finding3(
2610
+ "pack-too-large",
2611
+ "bundle",
2612
+ `The pack is ${Math.round(bytes / 1024)} KB; Vorn installs at most ${Math.round(maxBytes / 1024)} KB`
2613
+ )
2614
+ ]
2615
+ };
2616
+ }
2617
+ return { findings, file, bytes };
2618
+ }
2619
+
2620
+ // src/scaffold.ts
2621
+ var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
2622
+ var SDK_DEPENDENCY_RANGE = "^0.7.0-beta.14";
2623
+ var SCAFFOLD_VERSION = "0.1.0";
2624
+ var VITEST_RANGE = "^4.1.10";
2625
+ function jsonFile(value) {
2626
+ return `${JSON.stringify(value, null, 2)}
2627
+ `;
2628
+ }
2629
+ function titleCase(id) {
2630
+ return id.split(/[-_]+/).filter((part) => part !== "").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2631
+ }
2632
+ function packageJson(id, description, inRepo, kind) {
2633
+ const scoped = kind === "extension" ? "extension" : "connector";
2634
+ return jsonFile({
2635
+ name: inRepo ? `@vornrun/${scoped}-${id}` : `vorn-${scoped}-${id}`,
2636
+ version: SCAFFOLD_VERSION,
2637
+ description,
2638
+ type: "module",
2639
+ license: "MIT",
2640
+ bin: { [`vorn-${scoped}-${id}`]: "dist/index.js" },
2641
+ main: "./dist/index.js",
2642
+ types: "./dist/index.d.ts",
2643
+ // A pane's page ships beside the bundle, so `web` is published like `dist`.
2644
+ files: [
2645
+ "dist",
2646
+ "README.md",
2647
+ ...kind === "extension" ? ["web"] : [],
2648
+ ...inRepo ? ["CHANGELOG.md"] : []
2649
+ ],
2650
+ ...inRepo && {
2651
+ repository: {
2652
+ type: "git",
2653
+ url: "git+https://github.com/vorn-run/connectors.git",
2654
+ directory: `packages/${id}`
2655
+ }
2656
+ },
2657
+ scripts: {
2658
+ // In the repository the config file is the one definition of the build.
2659
+ build: inRepo ? "tsup" : "tsup src/index.ts --format esm --target node22 --clean --dts",
2660
+ check: "vorn-connector check src/index.ts",
2661
+ pack: "vorn-connector pack src/index.ts",
2662
+ test: "vitest run",
2663
+ typecheck: "tsc --noEmit"
2664
+ },
2665
+ dependencies: { "@vornrun/connector-sdk": SDK_DEPENDENCY_RANGE },
2666
+ devDependencies: {
2667
+ ...inRepo && { "@types/node": "^22.10.2", "@vitest/coverage-v8": VITEST_RANGE },
2668
+ tsup: "^8.5.1",
2669
+ typescript: "^6.0.3",
2670
+ vitest: VITEST_RANGE
2671
+ },
2672
+ // Read by the catalog build: how this is filed, found, and what it asks of you.
2673
+ vorn: {
2674
+ category: kind === "extension" ? "Extensions" : "Other",
2675
+ keywords: [id],
2676
+ ...kind === "extension" && { kind: "extension" },
2677
+ ...inRepo && kind === "connector" && { auth: "Say in one line what signing in takes." }
2678
+ }
2679
+ });
2680
+ }
2681
+ function tsconfig(inRepo) {
2682
+ return jsonFile({
2683
+ compilerOptions: {
2684
+ target: "ES2022",
2685
+ lib: ["ES2022"],
2686
+ module: "ESNext",
2687
+ moduleResolution: "bundler",
2688
+ allowSyntheticDefaultImports: true,
2689
+ esModuleInterop: true,
2690
+ strict: true,
2691
+ skipLibCheck: true,
2692
+ types: ["node"],
2693
+ noEmit: true,
2694
+ resolveJsonModule: true,
2695
+ ignoreDeprecations: "6.0",
2696
+ allowImportingTsExtensions: true
2697
+ },
2698
+ include: ["src/**/*", ...inRepo ? ["vitest.config.ts"] : []]
2699
+ });
2700
+ }
2701
+ function tsupConfig() {
2702
+ return `import { defineConfig } from 'tsup'
2703
+
2704
+ export default defineConfig({
2705
+ entry: ['src/index.ts'],
2706
+ format: ['esm'],
2707
+ target: 'node22',
2708
+ clean: true,
2709
+ dts: true,
2710
+ // Vorn spawns the built file directly.
2711
+ banner: { js: '#!/usr/bin/env node' }
2712
+ })
2713
+ `;
2714
+ }
2715
+ function vitestConfig() {
2716
+ return `import shared from '../../vitest.shared.ts'
2717
+
2718
+ export default shared
2719
+ `;
2720
+ }
2721
+ function changelog() {
2722
+ return `# Changelog
2723
+
2724
+ ## ${SCAFFOLD_VERSION}
2725
+
2726
+ - First release.
2727
+ `;
2728
+ }
2729
+ function connectorSource(id, name, description) {
2730
+ return `import { defineConnector } from '@vornrun/connector-sdk'
2731
+ // Bundled at build time: a pack is one file, so a version read from disk is not there to read.
2732
+ import pkg from '../package.json'
2733
+
2734
+ export const connector = defineConnector({
2735
+ id: ${JSON.stringify(id)},
2736
+ name: ${JSON.stringify(name)},
2737
+ description: ${JSON.stringify(description)},
2738
+ version: pkg.version,
2739
+ // Prefer a login the machine already has: { rung: 'cli', probe: { command: 'tool', args: ['auth', 'status'] } }
2740
+ auth: { rung: 'key', keys: ['apiToken'] },
2741
+ config: [
2742
+ {
2743
+ key: 'apiToken',
2744
+ label: 'API token',
2745
+ required: true,
2746
+ secret: true,
2747
+ builderHint: 'Say where a token is created and which scopes it needs'
2748
+ },
2749
+ { key: 'baseUrl', label: 'Base URL', default: 'https://api.example.com' }
2750
+ ],
2751
+ triggers: [
2752
+ {
2753
+ type: 'itemCreated',
2754
+ label: 'Item created',
2755
+ description: 'Items created since the last poll',
2756
+ // Return what is there; the SDK handles cursors and de-duplication.
2757
+ dedupe: 'timestamp',
2758
+ async fetch(context) {
2759
+ const url = new URL('/v1/items', context.config.baseUrl)
2760
+ if (context.since) url.searchParams.set('updated_since', context.since)
2761
+ // \`context.fetch\` retries and backs off; the global one does not.
2762
+ const response = await context.fetch(url, {
2763
+ headers: { authorization: 'Bearer ' + context.config.apiToken }
2764
+ })
2765
+ if (!response.ok) throw new Error('Listing items failed with ' + response.status)
2766
+ const body = (await response.json()) as { items: Array<Record<string, string>> }
2767
+ return body.items.map((item) => ({
2768
+ externalId: item.id,
2769
+ title: item.title,
2770
+ url: item.html_url,
2771
+ updatedAt: item.updated_at
2772
+ }))
2773
+ }
2774
+ }
2775
+ ],
2776
+ actions: [
2777
+ {
2778
+ type: 'createItem',
2779
+ label: 'Create item',
2780
+ description: 'Create one item',
2781
+ inputs: [
2782
+ { key: 'title', label: 'Title', required: true },
2783
+ { key: 'body', label: 'Body' }
2784
+ ],
2785
+ outputs: [{ key: 'id', type: 'string', description: 'The created item' }],
2786
+ // Declared, not written: the SDK fills the templates, sends it, and keeps what postReceive names.
2787
+ request: {
2788
+ method: 'POST',
2789
+ url: '{{config.baseUrl}}/v1/items',
2790
+ headers: { authorization: 'Bearer {{config.apiToken}}' },
2791
+ body: { title: '{{args.title}}', body: '{{args.body}}' }
2792
+ },
2793
+ postReceive: [{ op: 'pick', keys: ['id'] }]
2794
+ }
2795
+ ]
2796
+ })
2797
+ `;
2798
+ }
2799
+ function extensionSource(id, name, description) {
2800
+ return `import { defineExtension } from '@vornrun/connector-sdk'
2801
+ // Bundled at build time: a pack is one file, so a version read from disk is not there to read.
2802
+ import pkg from '../package.json'
2803
+
2804
+ export const connector = defineExtension({
2805
+ id: ${JSON.stringify(id)},
2806
+ name: ${JSON.stringify(name)},
2807
+ description: ${JSON.stringify(description)},
2808
+ version: pkg.version,
2809
+ // Only what this actually spends: the check names one it declared and never used.
2810
+ permissions: ['terminal.read'],
2811
+ // Absent where none of these hold, rather than showing a band with nothing in it.
2812
+ activates: { workspaceContains: ['package.json'] },
2813
+ footers: [
2814
+ {
2815
+ id: 'checks',
2816
+ title: 'Checks',
2817
+ description: 'What the last commands in this session said',
2818
+ every: 30,
2819
+ async run(context) {
2820
+ const output = await context.host.output({ lines: 200 })
2821
+ const failed = /\\b(FAIL|failed|error)\\b/i.test(output)
2822
+ return [
2823
+ {
2824
+ label: 'tests',
2825
+ value: failed ? 'failing' : 'passing',
2826
+ tone: failed ? 'danger' : 'ok'
2827
+ }
2828
+ ]
2829
+ }
2830
+ }
2831
+ ],
2832
+ panes: [
2833
+ {
2834
+ id: 'report',
2835
+ title: 'Report',
2836
+ description: 'The reading, in full, beside the terminal',
2837
+ // Drawn beside the pane's name wherever it is offered; path data only.
2838
+ icon: { viewBox: '0 0 24 24', paths: ['M4 4h16v16H4z M8 9h8 M8 13h8 M8 17h5'] },
2839
+ // Served from the pack; everything the page needs lives under web/.
2840
+ web: 'web/report/index.html'
2841
+ }
2842
+ ]
2843
+ })
2844
+ `;
2845
+ }
2846
+ function extensionPage(name) {
2847
+ return `<!doctype html>
2848
+ <html lang="en">
2849
+ <head>
2850
+ <meta charset="utf-8" />
2851
+ <title>${name}</title>
2852
+ <style>
2853
+ body {
2854
+ margin: 0;
2855
+ padding: 12px;
2856
+ font: 12px ui-sans-serif, system-ui, sans-serif;
2857
+ color: #faf9f7;
2858
+ background: #101012;
2859
+ }
2860
+ h1 {
2861
+ font-size: 13px;
2862
+ font-weight: 500;
2863
+ margin: 0 0 8px;
2864
+ }
2865
+ pre {
2866
+ margin: 0;
2867
+ white-space: pre-wrap;
2868
+ color: rgba(255, 255, 255, 0.55);
2869
+ }
2870
+ </style>
2871
+ </head>
2872
+ <body>
2873
+ <h1>${name}</h1>
2874
+ <pre id="output">Reading the session\u2026</pre>
2875
+ <script type="module">
2876
+ // Same origin, so the page carries no credential: Vorn knows which pane is
2877
+ // asking and grants exactly the permissions the manifest declared.
2878
+ const ask = async (method, body = {}) => {
2879
+ const response = await fetch('bridge/' + method, {
2880
+ method: 'POST',
2881
+ headers: { 'content-type': 'application/json' },
2882
+ body: JSON.stringify(body)
2883
+ })
2884
+ if (!response.ok) throw new Error(method + ' answered ' + response.status)
2885
+ return (await response.json()).result
2886
+ }
2887
+
2888
+ const node = document.getElementById('output')
2889
+ try {
2890
+ node.textContent = await ask('output', { lines: 200 })
2891
+ } catch (error) {
2892
+ node.textContent = String(error)
2893
+ }
2894
+ </script>
2895
+ </body>
2896
+ </html>
2897
+ `;
2898
+ }
2899
+ function extensionTestSource(name) {
2900
+ return `import { describe, expect, it } from 'vitest'
2901
+ import { mockExtensionHost } from '@vornrun/connector-sdk'
2902
+ import { connector } from './extension'
2903
+
2904
+ /** Runs one footer the way the host will, against a stub that enforces the manifest. */
2905
+ async function footer(id: string, output: string) {
2906
+ const { host } = mockExtensionHost(connector.permissions ?? [], { output: async () => output })
2907
+ const declared = connector.contributes?.footers?.find((entry) => entry.id === id)
2908
+ if (!declared) throw new Error('no footer ' + id)
2909
+ return declared.run({
2910
+ sessionId: 'test',
2911
+ worktreePath: process.cwd(),
2912
+ agent: 'claude',
2913
+ host,
2914
+ now: () => '2026-01-01T00:00:00.000Z'
2915
+ })
2916
+ }
2917
+
2918
+ describe(${JSON.stringify(name)}, () => {
2919
+ it('reads the session as passing when nothing failed', async () => {
2920
+ expect(await footer('checks', 'Test Files 1 passed (1)')).toEqual([
2921
+ { label: 'tests', value: 'passing', tone: 'ok' }
2922
+ ])
2923
+ })
2924
+
2925
+ it('reads it as failing when the output says so', async () => {
2926
+ expect(await footer('checks', 'FAIL src/index.test.ts')).toEqual([
2927
+ { label: 'tests', value: 'failing', tone: 'danger' }
2928
+ ])
2929
+ })
2930
+
2931
+ it('asks for nothing it did not declare', () => {
2932
+ expect(connector.permissions).toEqual(['terminal.read'])
2933
+ })
2934
+ })
2935
+ `;
2936
+ }
2937
+ function entrySource(module) {
2938
+ return `import { realpathSync } from 'node:fs'
2939
+ import { fileURLToPath } from 'node:url'
2940
+ import { serveConnector } from '@vornrun/connector-sdk'
2941
+ import { connector } from './${module}'
2942
+
2943
+ /** True when this file was run directly rather than imported. */
2944
+ export function isEntryPoint(moduleUrl: string, argv = process.argv): boolean {
2945
+ const invoked = argv[1]
2946
+ if (invoked === undefined) return false
2947
+ try {
2948
+ return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(invoked)
2949
+ } catch {
2950
+ return false
2951
+ }
2952
+ }
2953
+
2954
+ /** Serve on stdio when run directly, which is what Vorn spawns; says whether it did. */
2955
+ export async function serveIfEntryPoint(
2956
+ moduleUrl: string,
2957
+ serve: (c: typeof connector) => Promise<void> = serveConnector
2958
+ ): Promise<boolean> {
2959
+ if (!isEntryPoint(moduleUrl)) return false
2960
+ await serve(connector)
2961
+ return true
2962
+ }
2963
+ `;
2964
+ }
2965
+ function entryTestSource() {
2966
+ return `import { describe, expect, it, vi } from 'vitest'
2967
+ import { realpathSync } from 'node:fs'
2968
+ import { fileURLToPath } from 'node:url'
2969
+ import { isEntryPoint, serveIfEntryPoint } from './entry'
2970
+ import connector, { connector as named } from './index'
2971
+
2972
+ const HERE = import.meta.url
2973
+
2974
+ describe('isEntryPoint', () => {
2975
+ it('is false when the process was started without a script', () => {
2976
+ expect(isEntryPoint(HERE, ['node'])).toBe(false)
2977
+ })
2978
+
2979
+ it('is true when argv points at this module, through a symlink or not', () => {
2980
+ expect(isEntryPoint(HERE, ['node', fileURLToPath(HERE)])).toBe(true)
2981
+ expect(isEntryPoint(HERE, ['node', realpathSync(fileURLToPath(HERE))])).toBe(true)
2982
+ })
2983
+
2984
+ it('is false when the module is running under the test runner', () => {
2985
+ expect(isEntryPoint(HERE)).toBe(false)
2986
+ })
2987
+
2988
+ it('is false rather than throwing when a path cannot be resolved', () => {
2989
+ expect(isEntryPoint(HERE, ['node', '/nowhere/that/exists'])).toBe(false)
2990
+ })
2991
+ })
2992
+
2993
+ describe('serveIfEntryPoint', () => {
2994
+ it('starts nothing when the module was merely imported', async () => {
2995
+ const serve = vi.fn(async () => {})
2996
+ expect(await serveIfEntryPoint(HERE, serve)).toBe(false)
2997
+ expect(serve).not.toHaveBeenCalled()
2998
+ })
2999
+ })
3000
+
3001
+ describe('the packaged connector', () => {
3002
+ it('is the same connector under both exports', () => {
3003
+ expect(connector).toBe(named)
3004
+ expect(connector.version).toMatch(/^\\d+\\.\\d+\\.\\d+/)
3005
+ })
3006
+ })
3007
+ `;
3008
+ }
3009
+ function indexSource(module) {
3010
+ return `import { connector } from './${module}'
3011
+ import { serveIfEntryPoint } from './entry'
3012
+
3013
+ export { connector }
3014
+ export default connector
3015
+
3016
+ await serveIfEntryPoint(import.meta.url)
3017
+ `;
3018
+ }
3019
+ function testSource(name) {
3020
+ return `import { describe, expect, it, vi } from 'vitest'
3021
+ import { createConnectorHarness } from '@vornrun/connector-sdk'
3022
+ import { connector } from './connector'
3023
+
3024
+ /** Answers the connector's calls from here, so the test needs no network. */
3025
+ function fakeFetch(body: unknown) {
3026
+ return vi.fn(async () =>
3027
+ new Response(JSON.stringify(body), { headers: { 'content-type': 'application/json' } })
3028
+ ) as unknown as typeof fetch
3029
+ }
3030
+
3031
+ const config = { apiToken: 'test-token', baseUrl: 'https://api.example.com' }
3032
+
3033
+ describe(${JSON.stringify(name)}, () => {
3034
+ it('reports the items the source lists', async () => {
3035
+ const harness = createConnectorHarness(connector, {
3036
+ config,
3037
+ fetchImpl: fakeFetch({
3038
+ items: [
3039
+ {
3040
+ id: '1',
3041
+ title: 'First item',
3042
+ html_url: 'https://example.com/1',
3043
+ updated_at: '2026-01-01T00:00:00.000Z'
3044
+ }
3045
+ ]
3046
+ })
3047
+ })
3048
+
3049
+ const page = await harness.poll('itemCreated')
3050
+
3051
+ expect(page.items).toHaveLength(1)
3052
+ expect(page.items[0].externalId).toBe('1')
3053
+ })
3054
+
3055
+ it('does not deliver the same item twice', async () => {
3056
+ const harness = createConnectorHarness(connector, {
3057
+ config,
3058
+ fetchImpl: fakeFetch({
3059
+ items: [
3060
+ {
3061
+ id: '1',
3062
+ title: 'First item',
3063
+ html_url: 'https://example.com/1',
3064
+ updated_at: '2026-01-01T00:00:00.000Z'
3065
+ }
3066
+ ]
3067
+ })
3068
+ })
3069
+
3070
+ expect(await harness.pollTwice('itemCreated')).toEqual([])
3071
+ })
3072
+
3073
+ it('creates an item and keeps only its id', async () => {
3074
+ const harness = createConnectorHarness(connector, {
3075
+ config,
3076
+ fetchImpl: fakeFetch({ id: '42', extra: 'ignored' })
3077
+ })
3078
+
3079
+ expect(await harness.execute('createItem', { title: 'A title' })).toEqual({ id: '42' })
3080
+ })
3081
+ })
3082
+ `;
3083
+ }
3084
+ function readme(id, name, description) {
3085
+ return `# ${name}
3086
+
3087
+ ${description}
3088
+
3089
+ ## Build and check
3090
+
3091
+ \`\`\`sh
3092
+ yarn install
3093
+ yarn build
3094
+ yarn check # verifies the connector against Vorn's contract
3095
+ yarn test
3096
+ yarn pack # writes ${id}-${SCAFFOLD_VERSION}.vorn.tgz, installable in Vorn
3097
+ \`\`\`
3098
+
3099
+ ## Settings
3100
+
3101
+ | Setting | Environment | Required |
3102
+ | --- | --- | --- |
3103
+ | API token | \`API_TOKEN\` | yes |
3104
+ | Base URL | \`BASE_URL\` | no |
3105
+
3106
+ ## What it offers
3107
+
3108
+ - **Item created** \u2014 polls for items created since the last run.
3109
+ - **Create item** \u2014 creates one item and returns its id.
3110
+
3111
+ Rename the trigger, the action and the settings to whatever this connector
3112
+ really talks to; the shapes here are a starting point, not a rule.
3113
+ `;
3114
+ }
3115
+ function extensionReadme(id, name, description) {
3116
+ return `# ${name}
3117
+
3118
+ ${description}
3119
+
3120
+ ## Build and check
3121
+
3122
+ \`\`\`sh
3123
+ yarn install
3124
+ yarn build
3125
+ yarn check # verifies the extension against Vorn's contract
3126
+ yarn test
3127
+ yarn pack # writes ${id}-${SCAFFOLD_VERSION}.vorn.tgz, installable in Vorn
3128
+ \`\`\`
3129
+
3130
+ ## What it contributes
3131
+
3132
+ | Kind | Name | What it does |
3133
+ | --- | --- | --- |
3134
+ | Footer | Checks | A band under the card's status bar, recomputed every 30s |
3135
+ | Pane | Report | A page beside the terminal, served from \`web/report\` |
3136
+
3137
+ ## What it asks for
3138
+
3139
+ | Permission | What it grants |
3140
+ | --- | --- |
3141
+ | \`terminal.read\` | The session's recent terminal output |
3142
+
3143
+ Ask for only what the extension spends: \`check\` names a permission that was
3144
+ declared and never used.
3145
+
3146
+ ## Where it shows
3147
+
3148
+ Sessions whose worktree has a \`package.json\`. Widen or narrow that in
3149
+ \`activates\`, and narrow one contribution further with its own \`when\`.
3150
+ `;
3151
+ }
3152
+ function scaffoldFiles(options) {
3153
+ const kind = options.kind ?? "connector";
3154
+ if (!ID_PATTERN.test(options.id ?? "")) {
3155
+ throw new Error(
3156
+ `${kind === "extension" ? "Extension" : "Connector"} id "${options.id}" must start with a letter and be url-safe`
3157
+ );
3158
+ }
3159
+ const name = options.name?.trim() || titleCase(options.id);
3160
+ const description = options.description?.trim() || `${name} ${kind} for Vorn`;
3161
+ const inRepo = options.repoConventions ?? false;
3162
+ const module = kind === "extension" ? "extension" : "connector";
3163
+ return [
3164
+ { path: "package.json", contents: packageJson(options.id, description, inRepo, kind) },
3165
+ kind === "extension" ? { path: "src/extension.ts", contents: extensionSource(options.id, name, description) } : { path: "src/connector.ts", contents: connectorSource(options.id, name, description) },
3166
+ { path: "src/entry.ts", contents: entrySource(module) },
3167
+ { path: "src/index.ts", contents: indexSource(module) },
3168
+ kind === "extension" ? { path: "src/extension.test.ts", contents: extensionTestSource(name) } : { path: "src/connector.test.ts", contents: testSource(name) },
3169
+ { path: "src/entry.test.ts", contents: entryTestSource() },
3170
+ // The page a pane is drawn from, carried into the pack as it stands here.
3171
+ ...kind === "extension" ? [{ path: "web/report/index.html", contents: extensionPage(name) }] : [],
3172
+ {
3173
+ path: "README.md",
3174
+ contents: kind === "extension" ? extensionReadme(options.id, name, description) : readme(options.id, name, description)
3175
+ },
3176
+ // Everywhere: the generated source imports its package.json, which needs resolveJsonModule to compile.
3177
+ { path: "tsconfig.json", contents: tsconfig(inRepo) },
3178
+ ...inRepo ? [
3179
+ { path: "CHANGELOG.md", contents: changelog() },
3180
+ { path: "tsup.config.ts", contents: tsupConfig() },
3181
+ { path: "vitest.config.ts", contents: vitestConfig() }
3182
+ ] : []
3183
+ ];
3184
+ }
3185
+
3186
+ // src/server.ts
3187
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3188
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3189
+ import { z } from "zod";
3190
+ function sessionCallOf(extra) {
3191
+ const key = extra._meta?.[SESSION_CALL_META];
3192
+ return typeof key === "string" ? { sessionCall: key } : {};
3193
+ }
3194
+ function json(value) {
3195
+ return {
3196
+ // Vorn reads `structuredContent` to build step output and to find the
3197
+ // `items` array a poll returned; the text block keeps the result readable
3198
+ // in any generic MCP client.
3199
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
3200
+ structuredContent: value
3201
+ };
3202
+ }
3203
+ function failure(error) {
3204
+ return {
3205
+ content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }],
3206
+ isError: true
3207
+ };
3208
+ }
3209
+ function describeInput(input) {
3210
+ const base = input.description ?? input.label;
3211
+ if (input.loadOptions !== void 0) {
3212
+ return `${base}. Choices come from this connector's "${input.loadOptions}" list.`;
3213
+ }
3214
+ if (input.type === "json") return `${base}. Takes JSON.`;
3215
+ const choices = (input.options ?? []).map((option) => option.value).filter((value) => typeof value === "string" && value !== "");
3216
+ if (choices.length > 0) return `${base}. Suggested values: ${choices.join(", ")}.`;
3217
+ return base;
3218
+ }
3219
+ function inputShape(inputs) {
3220
+ const shape = {};
3221
+ for (const input of inputs) {
3222
+ const base = z.string().describe(describeInput(input));
3223
+ shape[input.key] = input.required ? base : base.optional();
3224
+ }
3225
+ return shape;
3226
+ }
3227
+ function scalar(type) {
3228
+ if (type === "number") return z.number();
3229
+ if (type === "boolean") return z.boolean();
3230
+ return z.string();
3231
+ }
3232
+ function outputSchema(outputs) {
3233
+ const shape = {};
3234
+ for (const output of outputs) {
3235
+ shape[output.key] = scalar(output.type).optional().describe(output.description ?? output.key);
3236
+ }
3237
+ return z.looseObject(shape);
3238
+ }
3239
+ function createConnectorServer(connector, options = {}) {
3240
+ const server = new McpServer(
3241
+ { name: connector.id, version: connector.version },
3242
+ { capabilities: { tools: {} } }
3243
+ );
3244
+ let cached = options.config;
3245
+ const config = () => cached ??= resolveConfig(connector);
3246
+ server.registerTool(
3247
+ MANIFEST_TOOL,
3248
+ {
3249
+ description: `Describe the ${connector.name} connector and how to configure it`,
3250
+ inputSchema: {},
3251
+ outputSchema: z.looseObject({})
3252
+ },
3253
+ () => json(connectorManifest(connector))
3254
+ );
3255
+ if (connector.preflight) {
3256
+ const preflight = connector.preflight.bind(connector);
3257
+ server.registerTool(
3258
+ PREFLIGHT_TOOL,
3259
+ {
3260
+ description: `Check whether ${connector.name} can run right now`,
3261
+ inputSchema: {},
3262
+ // Declared rather than left open like the manifest's: this shape is
3263
+ // fixed, so a caller can validate against it. Still loose, because a
3264
+ // connector adding a field of its own should not fail the call.
3265
+ outputSchema: z.looseObject({
3266
+ ok: z.boolean().describe("Whether the connector could run right now"),
3267
+ message: z.string().optional().describe("What to do about it, when it could not")
3268
+ })
3269
+ },
3270
+ async () => {
3271
+ try {
3272
+ return json({ ...await preflight() });
3273
+ } catch (error) {
3274
+ return json({
3275
+ ok: false,
3276
+ message: error instanceof Error ? error.message : String(error)
3277
+ });
3278
+ }
3279
+ }
3280
+ );
3281
+ }
3282
+ const optionSets = Object.keys(connector.options ?? {});
3283
+ if (optionSets.length > 0) {
3284
+ server.registerTool(
3285
+ OPTIONS_TOOL,
3286
+ {
3287
+ description: `List what one of ${connector.name}'s fields can be set to`,
3288
+ inputSchema: {
3289
+ name: z.enum(optionSets).describe("Which options set to list")
3290
+ },
3291
+ outputSchema: z.looseObject({
3292
+ options: z.array(z.looseObject({ value: z.string(), label: z.string().optional() })).describe("The choices, each a value to send and words to show")
3293
+ })
3294
+ },
3295
+ async (args, extra) => {
3296
+ try {
3297
+ return json({
3298
+ options: await runOptions(connector, args.name, {
3299
+ config: config(),
3300
+ ...options.now && { now: options.now },
3301
+ ...sessionCallOf(extra)
3302
+ })
3303
+ });
3304
+ } catch (error) {
3305
+ return failure(error);
3306
+ }
3307
+ }
3308
+ );
3309
+ }
3310
+ for (const trigger of connector.triggers) {
3311
+ server.registerTool(
3312
+ pollToolName(trigger.type),
3313
+ {
3314
+ description: trigger.description ?? `Poll ${connector.name} for ${trigger.label}`,
3315
+ inputSchema: {
3316
+ since: z.string().optional().describe("Only return items changed after this ISO timestamp"),
3317
+ cursor: z.string().optional().describe("Opaque cursor from a previous page"),
3318
+ limit: z.string().optional().describe("Maximum number of items to return")
3319
+ },
3320
+ outputSchema: z.looseObject({
3321
+ items: z.array(z.looseObject({})).describe("Normalized items"),
3322
+ nextCursor: z.string().optional().describe("Cursor for the next page"),
3323
+ hasMore: z.boolean().describe("Whether another page is immediately available")
3324
+ })
3325
+ },
3326
+ async (args, extra) => {
3327
+ try {
3328
+ const limit = args.limit === void 0 ? void 0 : Number(args.limit);
3329
+ if (limit !== void 0 && !Number.isFinite(limit)) {
3330
+ throw new Error(`Invalid limit "${args.limit}"`);
3331
+ }
3332
+ return json(
3333
+ await runPoll(connector, trigger.type, {
3334
+ config: config(),
3335
+ ...args.since !== void 0 && { since: args.since },
3336
+ ...args.cursor !== void 0 && { cursor: args.cursor },
3337
+ ...limit !== void 0 && { limit },
3338
+ ...options.now && { now: options.now },
3339
+ ...sessionCallOf(extra)
3340
+ })
3341
+ );
3342
+ } catch (error) {
3343
+ return failure(error);
3344
+ }
3345
+ }
3346
+ );
3347
+ }
3348
+ const sessionShape = {
3349
+ sessionId: z.string().describe("The session this is being computed for"),
3350
+ worktreePath: z.string().describe("Where the session's work is"),
3351
+ agent: z.enum(EXTENSION_AGENTS).describe("Which agent runs in the session")
3352
+ };
3353
+ const sessionContext = (args, host) => ({
3354
+ sessionId: args.sessionId,
3355
+ worktreePath: args.worktreePath,
3356
+ agent: args.agent,
3357
+ host,
3358
+ now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString())
3359
+ });
3360
+ const hostFor = (sessionId) => options.host?.(sessionId) ?? createExtensionHost({ sessionId });
3361
+ for (const footer of connector.contributes?.footers ?? []) {
3362
+ server.registerTool(
3363
+ footerToolName(footer.id),
3364
+ {
3365
+ title: footer.title,
3366
+ description: footer.description ?? `Recompute ${footer.title} for one session`,
3367
+ inputSchema: sessionShape,
3368
+ outputSchema: z.looseObject({
3369
+ items: z.array(z.looseObject({ label: z.string(), value: z.string() })).describe("The readings to show in the band")
3370
+ })
3371
+ },
3372
+ async (args) => {
3373
+ try {
3374
+ const items = await footer.run(sessionContext(args, hostFor(args.sessionId)));
3375
+ return json({ items });
3376
+ } catch (error) {
3377
+ return failure(error);
3378
+ }
3379
+ }
3380
+ );
3381
+ }
3382
+ for (const handler of connector.contributes?.linkHandlers ?? []) {
3383
+ server.registerTool(
3384
+ handlerToolName(handler.id),
3385
+ {
3386
+ title: handler.title,
3387
+ description: handler.description ?? `Open ${handler.title} for a clicked link`,
3388
+ inputSchema: {
3389
+ ...sessionShape,
3390
+ url: z.string().describe("The clicked text, which matched this handler")
3391
+ },
3392
+ outputSchema: z.looseObject({
3393
+ openPane: z.string().optional().describe("Id of one of this extension's panes to open")
3394
+ })
3395
+ },
3396
+ async (args) => {
3397
+ try {
3398
+ const handled = await handler.run({
3399
+ ...sessionContext(args, hostFor(args.sessionId)),
3400
+ url: args.url
3401
+ });
3402
+ return json({ ...handled ?? {} });
3403
+ } catch (error) {
3404
+ return failure(error);
3405
+ }
3406
+ }
3407
+ );
3408
+ }
3409
+ for (const action of connector.actions) {
3410
+ const base = action.description ?? `${action.label} in ${connector.name}`;
3411
+ const retryHint = action.idempotent === void 0 ? "" : action.idempotent ? " Safe to retry: repeating this call with the same arguments has no additional effect." : " Not idempotent: repeating this call performs the operation again.";
3412
+ server.registerTool(
3413
+ action.type,
3414
+ {
3415
+ // Carries the authored label, so a picker can name the action rather than its tool.
3416
+ title: action.label,
3417
+ description: `${base}${retryHint}`,
3418
+ inputSchema: inputShape(action.inputs ?? []),
3419
+ outputSchema: outputSchema(action.outputs ?? [])
3420
+ },
3421
+ async (args, extra) => {
3422
+ try {
3423
+ return json(
3424
+ await runAction(connector, action.type, args, {
3425
+ config: config(),
3426
+ ...options.now && { now: options.now },
3427
+ ...sessionCallOf(extra)
3428
+ })
3429
+ );
3430
+ } catch (error) {
3431
+ return failure(error);
3432
+ }
3433
+ }
3434
+ );
3435
+ }
3436
+ return server;
3437
+ }
3438
+ async function serveConnector(connector, options = {}) {
3439
+ const server = createConnectorServer(connector, options);
3440
+ await server.connect(new StdioServerTransport());
3441
+ }
3442
+
3443
+ export {
3444
+ BROWSER_HOST_ENV,
3445
+ BROWSER_TOKEN_ENV,
3446
+ SESSION_CALL_META,
3447
+ SESSION_CALL_HEADER,
3448
+ SessionUnavailableError,
3449
+ SessionRefusedError,
3450
+ createSessionFetch,
3451
+ ORIGIN_PATTERN,
3452
+ withinOrigins,
3453
+ EXTENSION_PERMISSIONS,
3454
+ HOST_PERMISSIONS,
3455
+ envNameFor,
3456
+ defineConnector,
3457
+ defineExtension,
3458
+ resolveConfig,
3459
+ pollToolName,
3460
+ footerToolName,
3461
+ handlerToolName,
3462
+ MANIFEST_TOOL,
3463
+ PREFLIGHT_TOOL,
3464
+ OPTIONS_TOOL,
3465
+ connectionSetup,
3466
+ connectorManifest,
3467
+ MAX_PACK_BYTES,
3468
+ lifecycleScriptFindings,
3469
+ bundleDependencyFindings,
3470
+ bundledRequireFindings,
3471
+ readNearestPackageJson,
3472
+ esbuildBundle,
3473
+ HOST_URL_ENV,
3474
+ HOST_TOKEN_ENV,
3475
+ PermissionDeniedError,
3476
+ createExtensionHost,
3477
+ normalizeItem,
3478
+ normalizeItems,
3479
+ pollWithDedupe,
3480
+ valueAt,
3481
+ applyPostReceive,
3482
+ resolveTemplates,
3483
+ resolveRequest,
3484
+ asOutput,
3485
+ MAX_REQUEST_PAGES,
3486
+ nextLink,
3487
+ executeRequest,
3488
+ retryAfterMs,
3489
+ backoffMs,
3490
+ resilientFetch,
3491
+ MAX_POLL_PAGES,
3492
+ runPoll,
3493
+ drainPoll,
3494
+ runOptions,
3495
+ runAction,
3496
+ MockRouteMissError,
3497
+ escapedMockHttp,
3498
+ withMockHttp,
3499
+ createConnectorHarness,
3500
+ mockExtensionHost,
3501
+ checkConnector,
3502
+ CHECK_OWNERS,
3503
+ runConformance,
3504
+ formatFindings,
3505
+ packFileName,
3506
+ packConnector,
3507
+ titleCase,
3508
+ scaffoldFiles,
3509
+ createConnectorServer,
3510
+ serveConnector
3511
+ };