@vornrun/connector-sdk 0.7.0-beta.13 → 0.7.0-beta.15

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.
@@ -1,1017 +1,1633 @@
1
- // src/packaging.ts
2
- import { builtinModules } from "module";
3
- import { readFileSync } from "fs";
4
- import { dirname, isAbsolute, join, resolve } from "path";
5
- var MAX_PACK_BYTES = 8 * 1024 * 1024;
6
- var LIFECYCLE_SCRIPTS = [
7
- "preinstall",
8
- "install",
9
- "postinstall",
10
- "prepare",
11
- "prepublish",
12
- "prepublishOnly",
13
- "postpublish"
1
+ // src/define.ts
2
+ var KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
3
+ var PATH_DATA_PATTERN = /^[MmZzLlHhVvCcSsQqTtAa0-9\s,.\-+eE]+$/;
4
+ var VIEW_BOX_PATTERN = /^-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+$/;
5
+ var DEDUPE_STRATEGIES = ["timestamp", "lastItem"];
6
+ var AUTH_RUNGS = ["none", "cli", "key", "oauth"];
7
+ var EXTENSION_PERMISSIONS = [
8
+ "git.read",
9
+ "terminal.read",
10
+ "terminal.selection",
11
+ "terminal.send",
12
+ "card.rename",
13
+ "agent.usage"
14
14
  ];
15
- var BUILTINS = new Set(builtinModules);
16
- function finding(code, target, message) {
17
- return { level: "error", code, target, message };
18
- }
19
- function lifecycleScriptFindings(pkg) {
20
- const scripts = pkg?.scripts;
21
- if (!scripts || typeof scripts !== "object") return [];
22
- const named = LIFECYCLE_SCRIPTS.filter((name) => typeof scripts[name] === "string");
23
- if (named.length === 0) return [];
24
- return [
25
- finding(
26
- "lifecycle-scripts",
27
- "package.json",
28
- `Remove the ${named.join(", ")} script(s); a pack is installed by copying files, never by running them`
29
- )
30
- ];
31
- }
32
- function bundleDependencyFindings(external) {
33
- const specifiers = /* @__PURE__ */ new Set();
34
- for (const specifier of external) {
35
- if (specifier.startsWith(".") || specifier.startsWith("/")) continue;
36
- if (specifier.startsWith("node:") || BUILTINS.has(specifier)) continue;
37
- specifiers.add(specifier);
38
- }
39
- if (specifiers.size === 0) return [];
40
- return [
41
- finding(
42
- "runtime-dependencies",
43
- "bundle",
44
- `${[...specifiers].sort().join(", ")} stayed outside the bundle; a pack must launch with no install step`
45
- )
46
- ];
47
- }
48
- function packageDirFor(resolveDir, entry) {
49
- const from = resolve(resolveDir);
50
- if (entry === void 0) return from;
51
- return entry.startsWith(".") || isAbsolute(entry) ? dirname(resolve(from, entry)) : from;
52
- }
53
- function readNearestPackageJson(fromDir) {
54
- let current = resolve(fromDir);
55
- for (; ; ) {
56
- try {
57
- return JSON.parse(readFileSync(join(current, "package.json"), "utf8"));
58
- } catch {
59
- const parent = dirname(current);
60
- if (parent === current) return void 0;
61
- current = parent;
15
+ var HOST_PERMISSIONS = {
16
+ diff: "git.read",
17
+ status: "git.read",
18
+ output: "terminal.read",
19
+ selection: "terminal.selection",
20
+ send: "terminal.send",
21
+ rename: "card.rename",
22
+ usage: "agent.usage"
23
+ };
24
+ var EXTENSION_AGENTS = [
25
+ "claude",
26
+ "copilot",
27
+ "codex",
28
+ "opencode",
29
+ "gemini",
30
+ "shell"
31
+ ];
32
+ var EXTENSION_PLATFORMS = ["darwin", "linux", "win32"];
33
+ var WEB_ENTRY_PATTERN = /^web\/[A-Za-z0-9._/-]+\.html$/;
34
+ var MIN_FOOTER_SECONDS = 5;
35
+ var MAX_PATTERN_LENGTH = 256;
36
+ var GROUP_OPEN = /^\((\?(:|=|!|<=|<!|<[A-Za-z_$][\w$]*>))?/;
37
+ function hasNestedQuantifier(pattern) {
38
+ const quantifierAt = (at) => {
39
+ const ch = pattern[at];
40
+ return ch !== void 0 && "*+?{".includes(ch);
41
+ };
42
+ const quantified = [];
43
+ let inClass = false;
44
+ for (let i = 0; i < pattern.length; i++) {
45
+ const ch = pattern[i];
46
+ if (ch === "\\") {
47
+ i++;
48
+ continue;
49
+ }
50
+ if (inClass) {
51
+ if (ch === "]") inClass = false;
52
+ continue;
53
+ }
54
+ if (ch === "[") {
55
+ inClass = true;
56
+ continue;
62
57
  }
58
+ if (ch === "(") {
59
+ quantified.push(false);
60
+ i += (GROUP_OPEN.exec(pattern.slice(i))?.[0].length ?? 1) - 1;
61
+ continue;
62
+ }
63
+ if (ch === ")") {
64
+ const heldOne = quantified.pop() ?? false;
65
+ const repeated = quantifierAt(i + 1);
66
+ if (heldOne && repeated) return true;
67
+ if ((heldOne || repeated) && quantified.length > 0) {
68
+ quantified[quantified.length - 1] = true;
69
+ }
70
+ continue;
71
+ }
72
+ if (quantifierAt(i) && quantified.length > 0) quantified[quantified.length - 1] = true;
63
73
  }
74
+ return false;
64
75
  }
65
- function packEntryContents(entry, sdkModule = "@vornrun/connector-sdk") {
66
- return [
67
- `import { serveConnector } from ${JSON.stringify(sdkModule)}`,
68
- `import * as entry from ${JSON.stringify(entry)}`,
69
- "const exported = Object.values(entry).find((value) => value && Array.isArray(value.triggers))",
70
- `if (!exported) throw new Error(${JSON.stringify(`${entry} exports no connector`)})`,
71
- "await serveConnector(exported)",
72
- ""
73
- ].join("\n");
74
- }
75
- async function esbuildBundle(request) {
76
- const { build } = await import("esbuild");
77
- const result = await build({
78
- stdin: {
79
- contents: request.contents,
80
- resolveDir: request.resolveDir,
81
- sourcefile: "vorn-connector-pack.js",
82
- loader: "js"
83
- },
84
- bundle: true,
85
- platform: "node",
86
- target: "node20",
87
- format: "esm",
88
- write: false,
89
- metafile: true,
90
- legalComments: "none"
91
- });
92
- const output = Object.values(result.metafile.outputs)[0];
93
- return {
94
- code: result.outputFiles[0].text,
95
- external: (output?.imports ?? []).filter((item) => item.external).map((item) => item.path)
96
- };
97
- }
98
-
99
- // src/normalize.ts
100
- var RESERVED_KEYS = [
101
- "externalId",
102
- "title",
103
- "url",
104
- "description",
105
- "status",
106
- "labels",
107
- "assignee",
108
- "updatedAt"
109
- ];
110
- var UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
111
- function itemExternalId(item) {
112
- return String(item.externalId ?? "").trim();
113
- }
114
- function itemTimestamp(item, fallback) {
115
- return isoTimestamp(item.updatedAt, fallback);
116
- }
117
- function isoTimestamp(value, fallback) {
118
- if (value === void 0) return fallback;
119
- const date = value instanceof Date ? value : new Date(value);
120
- if (Number.isNaN(date.getTime())) {
121
- throw new Error(`Invalid updatedAt: ${String(value)}`);
76
+ var ABSOLUTE_URL_PATTERN = /^https?:\/\//i;
77
+ var CONFIG_ROOTED_URL_PATTERN = /^\{\{\s*config\./;
78
+ function assertUnique(kind, keys) {
79
+ const seen = /* @__PURE__ */ new Set();
80
+ for (const key of keys) {
81
+ if (seen.has(key)) throw new Error(`Duplicate ${kind} "${key}"`);
82
+ seen.add(key);
122
83
  }
123
- return date.toISOString();
124
84
  }
125
- function normalizeItem(item, polledAt) {
126
- const externalId = itemExternalId(item);
127
- if (!externalId) {
128
- throw new Error("Connector item is missing externalId");
85
+ function assertAuth(definition) {
86
+ const auth = definition.auth;
87
+ if (!auth) return;
88
+ const id = definition.id;
89
+ if (!AUTH_RUNGS.includes(auth.rung)) {
90
+ throw new Error(
91
+ `Connector ${id} declares unknown auth rung ${JSON.stringify(auth.rung)}; expected ${AUTH_RUNGS.join(", ")}`
92
+ );
129
93
  }
130
- if (!item.title || !item.title.trim()) {
131
- throw new Error(`Connector item ${externalId} is missing title`);
94
+ if (auth.rung === "cli" && !auth.probe?.command?.trim()) {
95
+ throw new Error(`Connector ${id} borrows a CLI login but declares no probe command to ask it`);
132
96
  }
133
- const extra = {};
134
- for (const [key, value] of Object.entries(item.data ?? {})) {
135
- if (RESERVED_KEYS.includes(key)) continue;
136
- if (UNSAFE_KEYS.includes(key)) continue;
137
- extra[key] = value;
97
+ if (auth.rung === "key") {
98
+ const keys = auth.keys ?? [];
99
+ if (keys.length === 0) {
100
+ throw new Error(`Connector ${id} signs in with a key but names no config field holding it`);
101
+ }
102
+ const declared = new Set((definition.config ?? []).map((field) => field.key));
103
+ for (const key of keys) {
104
+ if (!declared.has(key)) {
105
+ throw new Error(`Connector ${id} names auth key "${key}", which is not a config field`);
106
+ }
107
+ }
138
108
  }
139
- return {
140
- ...extra,
141
- externalId,
142
- title: item.title,
143
- url: item.url ?? "",
144
- description: item.description ?? "",
145
- status: item.status ?? "open",
146
- labels: item.labels ?? [],
147
- ...item.assignee !== void 0 && { assignee: item.assignee },
148
- updatedAt: isoTimestamp(item.updatedAt, polledAt)
149
- };
150
- }
151
- function normalizeItems(items, polledAt) {
152
- const seen = /* @__PURE__ */ new Set();
153
- return items.map((item) => {
154
- const normalized = normalizeItem(item, polledAt);
155
- if (seen.has(normalized.externalId)) {
156
- throw new Error(`Duplicate externalId "${normalized.externalId}" in one poll page`);
109
+ if (auth.rung === "none") {
110
+ const secret = (definition.config ?? []).find((field) => field.secret === true);
111
+ if (secret) {
112
+ throw new Error(
113
+ `Connector ${id} claims it needs no sign-in but declares secret field "${secret.key}"`
114
+ );
157
115
  }
158
- seen.add(normalized.externalId);
159
- return normalized;
160
- });
116
+ }
161
117
  }
162
-
163
- // src/dedupe.ts
164
- var MAX_BOUNDARY_IDS = 500;
165
- function decodeCursor(cursor, strategy) {
166
- if (!cursor) return void 0;
167
- let parsed;
168
- try {
169
- parsed = JSON.parse(cursor);
170
- } catch (error) {
171
- throw new Error(`Cursor is not valid SDK cursor JSON: ${cursor}`, { cause: error });
118
+ function assertIdentity(kind, definition) {
119
+ if (!KEY_PATTERN.test(definition.id ?? "")) {
120
+ throw new Error(`${kind} id "${definition.id}" must start with a letter and be url-safe`);
172
121
  }
173
- const state = parsed;
174
- if (!state || typeof state !== "object" || state.v !== 1 || state.s !== strategy) {
175
- throw new Error(`Cursor does not belong to the "${strategy}" strategy: ${cursor}`);
122
+ if (!definition.name?.trim()) {
123
+ throw new Error(`${kind} ${definition.id} is missing a name`);
176
124
  }
177
- const wellFormed = state.s === "timestamp" ? typeof state.t === "string" && Array.isArray(state.ids) && state.ids.every((id) => typeof id === "string") : typeof state.id === "string";
178
- if (!wellFormed) {
179
- throw new Error(`Cursor is missing the fields the "${strategy}" strategy needs: ${cursor}`);
125
+ assertIcon(`${kind} ${definition.id}`, definition.icon);
126
+ }
127
+ function assertIcon(subject, icon) {
128
+ if (!icon) return;
129
+ const { viewBox, paths } = icon;
130
+ if (!Array.isArray(paths) || paths.length === 0) {
131
+ throw new Error(`${subject} has an icon with no paths`);
132
+ }
133
+ for (const path of paths) {
134
+ if (typeof path !== "string" || !PATH_DATA_PATTERN.test(path)) {
135
+ throw new Error(
136
+ `${subject} has an icon path that is not SVG path data. Only path data is accepted, not markup.`
137
+ );
138
+ }
139
+ }
140
+ if (viewBox !== void 0 && !VIEW_BOX_PATTERN.test(viewBox)) {
141
+ throw new Error(`${subject} has an icon viewBox that is not four numbers`);
180
142
  }
181
- return state;
182
143
  }
183
- function compare(left, right) {
184
- if (left === right) return 0;
185
- return left < right ? -1 : 1;
144
+ function envNameFor(key, explicit) {
145
+ if (explicit) return explicit;
146
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
186
147
  }
187
- function page(chronological, context, hadCursor, nextCursor) {
188
- const delivered = context.limit === void 0 ? chronological : chronological.slice(0, context.limit);
189
- if (delivered.length === 0) {
190
- return { items: [], ...context.cursor !== void 0 && { nextCursor: context.cursor } };
148
+ function defineConnector(definition) {
149
+ assertIdentity("Connector", definition);
150
+ const triggers = definition.triggers ?? [];
151
+ const actions = definition.actions ?? [];
152
+ if (triggers.length === 0 && actions.length === 0) {
153
+ throw new Error(`Connector ${definition.id} declares no triggers and no actions`);
191
154
  }
192
- return {
193
- items: delivered.map((entry) => entry.item),
194
- nextCursor: JSON.stringify(nextCursor(delivered)),
195
- // Only drain a backlog we know we truncated, and only once a cursor
196
- // exists a first poll should not pull the source's entire history.
197
- hasMore: chronological.length > delivered.length && hadCursor
198
- };
199
- }
200
- function timestampPoll(fetched, state, context, polledAt) {
201
- const boundary = state?.t ?? context.since;
202
- const seen = new Set(state?.ids ?? []);
203
- const fresh = [];
204
- const pinnedAlreadySeen = [];
205
- for (const item of fetched) {
206
- const id = itemExternalId(item);
207
- const pinned = item.updatedAt === void 0 && boundary !== void 0;
208
- const at = pinned ? boundary : itemTimestamp(item, polledAt);
209
- const isNew = boundary === void 0 || at > boundary || at === boundary && !seen.has(id);
210
- if (isNew) fresh.push({ item, at, id, ...pinned && { pinned: true } });
211
- else if (pinned) pinnedAlreadySeen.push(id);
155
+ for (const trigger of triggers) {
156
+ if (!KEY_PATTERN.test(trigger.type ?? "")) {
157
+ throw new Error(`Trigger type "${trigger.type}" must start with a letter and be url-safe`);
158
+ }
159
+ const loose = trigger;
160
+ const declarative = typeof loose.fetch === "function";
161
+ const imperative = typeof loose.poll === "function";
162
+ if (declarative && imperative) {
163
+ throw new Error(`Trigger ${trigger.type} declares both fetch() and poll(); pick one`);
164
+ }
165
+ if (declarative !== (loose.dedupe !== void 0)) {
166
+ throw new Error(
167
+ `Trigger ${trigger.type} needs fetch() and a dedupe strategy together, not one alone`
168
+ );
169
+ }
170
+ if (loose.dedupe !== void 0 && !DEDUPE_STRATEGIES.includes(loose.dedupe)) {
171
+ throw new Error(
172
+ `Trigger ${trigger.type} has unknown dedupe strategy ${JSON.stringify(loose.dedupe)}; expected ${DEDUPE_STRATEGIES.join(" or ")}`
173
+ );
174
+ }
175
+ if (loose.poll !== void 0 && !imperative) {
176
+ throw new Error(`Trigger ${trigger.type} declares poll but it is not a function`);
177
+ }
178
+ if (!declarative && !imperative) {
179
+ throw new Error(`Trigger ${trigger.type} is missing a fetch() or poll() implementation`);
180
+ }
212
181
  }
213
- fresh.sort((left, right) => compare(left.at, right.at) || compare(left.id, right.id));
214
- return page(fresh, context, state !== void 0, (delivered) => {
215
- const newest = delivered[delivered.length - 1].at;
216
- const atNewest = [];
217
- for (let i = delivered.length - 1; i >= 0 && delivered[i].at === newest; i -= 1) {
218
- atNewest.push(delivered[i].id);
182
+ for (const action of actions) {
183
+ if (!KEY_PATTERN.test(action.type ?? "")) {
184
+ throw new Error(`Action type "${action.type}" must start with a letter and be url-safe`);
219
185
  }
220
- const carried = newest === boundary ? [...seen, ...atNewest] : [
221
- ...pinnedAlreadySeen,
222
- ...delivered.filter((entry) => entry.pinned).map((entry) => entry.id),
223
- ...atNewest
224
- ];
225
- return { v: 1, s: "timestamp", t: newest, ids: carried.slice(-MAX_BOUNDARY_IDS) };
226
- });
227
- }
228
- function lastItemPoll(fetched, state, context, polledAt) {
229
- const keyed = fetched.map((item) => ({
230
- item,
231
- at: itemTimestamp(item, polledAt),
232
- id: itemExternalId(item)
233
- }));
234
- const stopAt = state ? keyed.findIndex((entry) => entry.id === state.id) : -1;
235
- const chronological = (stopAt === -1 ? keyed : keyed.slice(0, stopAt)).reverse();
236
- return page(chronological, context, state !== void 0, (delivered) => ({
237
- v: 1,
238
- s: "lastItem",
239
- id: delivered[delivered.length - 1].id
240
- }));
186
+ const loose = action;
187
+ const written = typeof loose.run === "function";
188
+ const declared = loose.request !== void 0;
189
+ if (written && declared) {
190
+ throw new Error(`Action ${action.type} declares both run() and a request; pick one`);
191
+ }
192
+ if (!written && !declared) {
193
+ throw new Error(`Action ${action.type} is missing a run() implementation or a request`);
194
+ }
195
+ if (declared) {
196
+ const request = loose.request;
197
+ if (typeof request?.url !== "string" || request.url.trim() === "") {
198
+ throw new Error(`Action ${action.type} declares a request with no URL`);
199
+ }
200
+ const url = request.url.trim();
201
+ if (!ABSOLUTE_URL_PATTERN.test(url) && !CONFIG_ROOTED_URL_PATTERN.test(url)) {
202
+ throw new Error(
203
+ `Action ${action.type} declares the request URL "${url}", which is neither absolute nor rooted in a {{config.\u2026}} value`
204
+ );
205
+ }
206
+ }
207
+ if (!declared && loose.postReceive !== void 0) {
208
+ throw new Error(`Action ${action.type} has postReceive but no request for it to reshape`);
209
+ }
210
+ for (const input of action.inputs ?? []) {
211
+ if (input.loadOptions !== void 0 && definition.options?.[input.loadOptions] === void 0) {
212
+ throw new Error(
213
+ `Action ${action.type} argument "${input.key}" loads options from "${input.loadOptions}", which the connector does not serve`
214
+ );
215
+ }
216
+ }
217
+ }
218
+ assertUnique(
219
+ "trigger",
220
+ triggers.map((trigger) => trigger.type)
221
+ );
222
+ assertUnique(
223
+ "action",
224
+ actions.map((action) => action.type)
225
+ );
226
+ assertUnique(
227
+ "config field",
228
+ (definition.config ?? []).map((field) => field.key)
229
+ );
230
+ assertAuth(definition);
231
+ return {
232
+ ...definition,
233
+ kind: "connector",
234
+ version: definition.version ?? "0.0.0",
235
+ config: definition.config ?? [],
236
+ triggers,
237
+ actions
238
+ };
241
239
  }
242
- async function pollWithDedupe(trigger, context) {
243
- const strategy = trigger.dedupe;
244
- const fetchItems = trigger.fetch;
245
- if (!strategy || !fetchItems) {
246
- throw new Error(`Trigger ${trigger.type} is not a declarative trigger`);
240
+ function assertPredicate(id, where, predicate) {
241
+ if (!predicate) return;
242
+ const lists = [
243
+ ["workspaceContains", predicate.workspaceContains],
244
+ ["remoteHost", predicate.remoteHost],
245
+ ["agent", predicate.agent],
246
+ ["platform", predicate.platform]
247
+ ];
248
+ for (const [field, value] of lists) {
249
+ if (value === void 0) continue;
250
+ if (!Array.isArray(value) || value.length === 0) {
251
+ throw new Error(`Extension ${id} ${where} declares "${field}" with nothing in it`);
252
+ }
253
+ for (const entry of value) {
254
+ if (typeof entry !== "string" || entry.trim() === "") {
255
+ throw new Error(`Extension ${id} ${where} declares an empty "${field}" value`);
256
+ }
257
+ }
247
258
  }
248
- const polledAt = context.now();
249
- if (strategy === "lastItem") {
250
- const state2 = decodeCursor(context.cursor, "lastItem");
251
- const fetched2 = await runFetch(trigger.type, fetchItems, {
252
- config: context.config,
253
- ...state2 && { lastItemId: state2.id },
254
- ...context.limit !== void 0 && { limit: context.limit },
255
- now: context.now,
256
- fetch: context.fetch
257
- });
258
- return lastItemPoll(fetched2, state2, context, polledAt);
259
+ for (const glob of predicate.workspaceContains ?? []) {
260
+ if (glob.startsWith("/") || glob.split("/").includes("..")) {
261
+ throw new Error(
262
+ `Extension ${id} ${where} looks for "${glob}", which is not inside the worktree`
263
+ );
264
+ }
259
265
  }
260
- const state = decodeCursor(context.cursor, "timestamp");
261
- const since = state?.t ?? context.since;
262
- const fetched = await runFetch(trigger.type, fetchItems, {
263
- config: context.config,
264
- ...since !== void 0 && { since },
265
- ...context.limit !== void 0 && { limit: context.limit },
266
- now: context.now,
267
- fetch: context.fetch
268
- });
269
- return timestampPoll(fetched, state, context, polledAt);
270
- }
271
- async function runFetch(type, fetchItems, context) {
272
- const fetched = await fetchItems(context);
273
- if (!Array.isArray(fetched)) {
274
- throw new Error(`Trigger ${type} fetch() did not return an array`);
266
+ for (const agent of predicate.agent ?? []) {
267
+ if (!EXTENSION_AGENTS.includes(agent)) {
268
+ throw new Error(
269
+ `Extension ${id} ${where} names unknown agent ${JSON.stringify(agent)}; expected ${EXTENSION_AGENTS.join(", ")}`
270
+ );
271
+ }
275
272
  }
276
- return fetched;
277
- }
278
-
279
- // src/post-receive.ts
280
- var UNSAFE_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
281
- function isRecord(value) {
282
- return typeof value === "object" && value !== null && !Array.isArray(value);
283
- }
284
- function segments(path) {
285
- return path.split(".").map((part) => part.trim()).filter((part) => part !== "");
286
- }
287
- function valueAt(value, path) {
288
- let current = value;
289
- for (const key of segments(path)) {
290
- if (UNSAFE_KEYS2.has(key)) return void 0;
291
- if (Array.isArray(current)) {
292
- const index = Number(key);
293
- if (!Number.isInteger(index)) return void 0;
294
- current = current[index];
295
- continue;
273
+ for (const platform of predicate.platform ?? []) {
274
+ if (!EXTENSION_PLATFORMS.includes(platform)) {
275
+ throw new Error(
276
+ `Extension ${id} ${where} names unknown platform ${JSON.stringify(platform)}; expected ${EXTENSION_PLATFORMS.join(", ")}`
277
+ );
296
278
  }
297
- if (!isRecord(current)) return void 0;
298
- current = Object.prototype.hasOwnProperty.call(current, key) ? current[key] : void 0;
299
279
  }
300
- return current;
301
280
  }
302
- function withValueAt(value, path, next) {
303
- const keys = segments(path);
304
- if (keys.length === 0) return next;
305
- const [head, ...rest] = keys;
306
- if (UNSAFE_KEYS2.has(head)) return value;
307
- if (Array.isArray(value)) {
308
- const index = Number(head);
309
- if (!Number.isInteger(index)) return value;
310
- const copy = [...value];
311
- copy[index] = rest.length === 0 ? next : withValueAt(copy[index], rest.join("."), next);
312
- return copy;
281
+ function assertPane(id, pane) {
282
+ assertIcon(`Extension ${id} pane ${pane.id}`, pane.icon);
283
+ const loose = pane;
284
+ const page2 = loose.web !== void 0;
285
+ const program = loose.command !== void 0;
286
+ if (page2 && program) {
287
+ throw new Error(`Extension ${id} pane ${pane.id} declares both a web page and a command`);
313
288
  }
314
- const base = isRecord(value) ? value : {};
315
- return {
316
- ...base,
317
- [head]: rest.length === 0 ? next : withValueAt(base[head], rest.join("."), next)
318
- };
319
- }
320
- function pick(value, keys) {
321
- if (Array.isArray(value)) return value.map((entry) => pick(entry, keys));
322
- if (!isRecord(value)) return value;
323
- const out = {};
324
- for (const key of keys) {
325
- if (UNSAFE_KEYS2.has(key)) continue;
326
- if (Object.prototype.hasOwnProperty.call(value, key)) out[key] = value[key];
289
+ if (!page2 && !program) {
290
+ throw new Error(`Extension ${id} pane ${pane.id} declares neither a web page nor a command`);
327
291
  }
328
- return out;
329
- }
330
- function rename(value, from, to) {
331
- if (Array.isArray(value)) return value.map((entry) => rename(entry, from, to));
332
- if (!isRecord(value)) return value;
333
- if (UNSAFE_KEYS2.has(from) || UNSAFE_KEYS2.has(to)) return value;
334
- if (!Object.prototype.hasOwnProperty.call(value, from)) return value;
335
- const out = {};
336
- for (const [key, entry] of Object.entries(value)) {
337
- if (key === from) out[to] = entry;
338
- else if (key !== to) out[key] = entry;
292
+ if (page2) {
293
+ const web = loose.web;
294
+ if (typeof web !== "string" || !WEB_ENTRY_PATTERN.test(web) || web.split("/").includes("..")) {
295
+ throw new Error(
296
+ `Extension ${id} pane ${pane.id} declares the page ${JSON.stringify(web)}; a page is an .html file under web/ in the package`
297
+ );
298
+ }
299
+ return;
339
300
  }
340
- return out;
341
- }
342
- function applyOp(value, op) {
343
- if (op.op === "flatten") return valueAt(value, op.path);
344
- const target = op.path === void 0 ? value : valueAt(value, op.path);
345
- if (op.path !== void 0 && target === void 0) return value;
346
- let next;
347
- if (op.op === "pick") next = pick(target, op.keys);
348
- else if (op.op === "rename") next = rename(target, op.from, op.to);
349
- else if (op.op === "filter") {
350
- next = Array.isArray(target) ? target.filter((entry) => isRecord(entry) && valueAt(entry, op.key) === op.equals) : target;
351
- } else {
352
- next = Array.isArray(target) ? target.map((entry) => applyPostReceive(entry, op.ops)) : target;
301
+ const command = loose.command;
302
+ if (!Array.isArray(command) || command.length === 0) {
303
+ throw new Error(`Extension ${id} pane ${pane.id} declares a command with nothing to run`);
353
304
  }
354
- return op.path === void 0 ? next : withValueAt(value, op.path, next);
355
- }
356
- function applyPostReceive(value, ops) {
357
- return (ops ?? []).reduce(applyOp, value);
358
- }
359
-
360
- // src/request.ts
361
- var MAX_ERROR_BODY = 500;
362
- var PLACEHOLDER = /\{\{\s*(args|config)\.([A-Za-z0-9_.-]+)\s*\}\}/g;
363
- var WHOLE_PLACEHOLDER = /^\{\{\s*(args|config)\.([A-Za-z0-9_.-]+)\s*\}\}$/;
364
- function lookup(source, path, scope) {
365
- return valueAt(source === "args" ? scope.args : scope.config, path);
366
- }
367
- var intoUrl = (value, source) => source === "config" ? value : encodeURIComponent(value);
368
- function intoHeader(name) {
369
- return (value) => {
370
- if (/[\r\n]/.test(value)) {
371
- throw new Error(`Header "${name}" would carry a line ending, which is not allowed`);
305
+ for (const arg of command) {
306
+ if (typeof arg !== "string" || arg === "") {
307
+ throw new Error(`Extension ${id} pane ${pane.id} declares a command with an empty argument`);
372
308
  }
373
- return value;
374
- };
309
+ }
375
310
  }
376
- function resolveTemplates(value, scope, substitute) {
377
- if (typeof value === "string") {
378
- const whole = WHOLE_PLACEHOLDER.exec(value);
379
- if (whole) {
380
- const resolved = lookup(whole[1], whole[2], scope);
381
- if (substitute === void 0 || resolved === void 0 || resolved === null) return resolved;
382
- return substitute(String(resolved), whole[1]);
311
+ function defineExtension(definition) {
312
+ assertIdentity("Extension", definition);
313
+ const id = definition.id;
314
+ const panes = definition.panes ?? [];
315
+ const footers = definition.footers ?? [];
316
+ const linkHandlers = definition.linkHandlers ?? [];
317
+ if (panes.length === 0 && footers.length === 0 && linkHandlers.length === 0) {
318
+ throw new Error(`Extension ${id} contributes nothing`);
319
+ }
320
+ const permissions = definition.permissions ?? [];
321
+ if (!Array.isArray(permissions)) {
322
+ throw new Error(`Extension ${id} declares permissions that are not a list`);
323
+ }
324
+ for (const permission of permissions) {
325
+ if (!EXTENSION_PERMISSIONS.includes(permission)) {
326
+ throw new Error(
327
+ `Extension ${id} asks for unknown permission ${JSON.stringify(permission)}; expected ${EXTENSION_PERMISSIONS.join(", ")}`
328
+ );
383
329
  }
384
- return value.replace(PLACEHOLDER, (_match, source, path) => {
385
- const resolved = lookup(source, path, scope);
386
- if (resolved === void 0 || resolved === null) return "";
387
- const text = String(resolved);
388
- return substitute === void 0 ? text : substitute(text, source);
389
- });
390
330
  }
391
- if (Array.isArray(value)) return value.map((entry) => resolveTemplates(entry, scope, substitute));
392
- if (typeof value === "object" && value !== null) {
393
- const out = {};
394
- for (const [key, entry] of Object.entries(value)) {
395
- out[key] = resolveTemplates(entry, scope, substitute);
331
+ assertUnique("permission", permissions);
332
+ const contributions = [...panes, ...footers, ...linkHandlers];
333
+ for (const contribution of contributions) {
334
+ if (!KEY_PATTERN.test(contribution.id ?? "")) {
335
+ throw new Error(
336
+ `Contribution id "${contribution.id}" must start with a letter and be url-safe`
337
+ );
338
+ }
339
+ if (!contribution.title?.trim()) {
340
+ throw new Error(`Extension ${id} contribution ${contribution.id} is missing a title`);
341
+ }
342
+ assertPredicate(id, `contribution ${contribution.id}`, contribution.when);
343
+ }
344
+ assertUnique(
345
+ "contribution",
346
+ contributions.map((contribution) => contribution.id)
347
+ );
348
+ assertPredicate(id, "activates", definition.activates);
349
+ for (const pane of panes) assertPane(id, pane);
350
+ for (const footer of footers) {
351
+ if (typeof footer.run !== "function") {
352
+ throw new Error(`Extension ${id} footer ${footer.id} is missing a run() implementation`);
353
+ }
354
+ if (!Number.isFinite(footer.every) || footer.every < MIN_FOOTER_SECONDS) {
355
+ throw new Error(
356
+ `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`
357
+ );
358
+ }
359
+ }
360
+ for (const handler of linkHandlers) {
361
+ if (typeof handler.run !== "function") {
362
+ throw new Error(
363
+ `Extension ${id} link handler ${handler.id} is missing a run() implementation`
364
+ );
365
+ }
366
+ if (typeof handler.pattern !== "string" || handler.pattern.length > MAX_PATTERN_LENGTH) {
367
+ throw new Error(
368
+ `Extension ${id} link handler ${handler.id} has a pattern longer than ${MAX_PATTERN_LENGTH} characters; it is matched on every click`
369
+ );
370
+ }
371
+ if (hasNestedQuantifier(handler.pattern)) {
372
+ throw new Error(
373
+ `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`
374
+ );
375
+ }
376
+ let matcher;
377
+ try {
378
+ matcher = new RegExp(handler.pattern);
379
+ } catch (error) {
380
+ const reason = error instanceof Error ? error.message : String(error);
381
+ throw new Error(
382
+ `Extension ${id} link handler ${handler.id} has a pattern that is not a regular expression: ${reason}`,
383
+ { cause: error }
384
+ );
385
+ }
386
+ if (typeof handler.example !== "string" || handler.example.trim() === "") {
387
+ throw new Error(
388
+ `Extension ${id} link handler ${handler.id} names no example link its pattern matches`
389
+ );
390
+ }
391
+ if (!matcher.test(handler.example)) {
392
+ throw new Error(
393
+ `Extension ${id} link handler ${handler.id} has the example ${JSON.stringify(handler.example)}, which its own pattern ${JSON.stringify(handler.pattern)} does not match`
394
+ );
395
+ }
396
+ }
397
+ return {
398
+ id,
399
+ name: definition.name,
400
+ ...definition.description !== void 0 && { description: definition.description },
401
+ ...definition.icon !== void 0 && { icon: definition.icon },
402
+ kind: "extension",
403
+ version: definition.version ?? "0.0.0",
404
+ // Its credential is the host's own token, so there is nothing to sign in to.
405
+ auth: { rung: "none" },
406
+ config: [],
407
+ triggers: [],
408
+ actions: [],
409
+ permissions,
410
+ ...definition.activates !== void 0 && { activates: definition.activates },
411
+ contributes: {
412
+ ...panes.length > 0 && { panes },
413
+ ...footers.length > 0 && { footers },
414
+ ...linkHandlers.length > 0 && { linkHandlers }
415
+ }
416
+ };
417
+ }
418
+ function resolveConfig(connector, env = process.env) {
419
+ const config = {};
420
+ const missing = [];
421
+ for (const field of connector.config) {
422
+ const name = envNameFor(field.key, field.env);
423
+ const value = env[name] ?? field.default;
424
+ if (value === void 0 || value === "") {
425
+ if (field.required) missing.push(`${field.key} (${name})`);
426
+ continue;
396
427
  }
397
- return out;
428
+ config[field.key] = value;
398
429
  }
399
- return value;
400
- }
401
- function resolveHeaders(raw, scope) {
402
- const out = {};
403
- for (const [name, value] of Object.entries(raw ?? {})) {
404
- const resolved = resolveTemplates(value, scope, intoHeader(name));
405
- if (resolved === void 0 || resolved === null || resolved === "") continue;
406
- out[name] = String(resolved);
430
+ if (missing.length > 0) {
431
+ throw new Error(
432
+ `Connector ${connector.id} is missing required configuration: ${missing.join(", ")}`
433
+ );
407
434
  }
408
- return out;
435
+ return config;
409
436
  }
410
- function stringMap(raw) {
411
- const out = {};
412
- if (typeof raw !== "object" || raw === null) return out;
413
- for (const [key, value] of Object.entries(raw)) {
414
- if (value === void 0 || value === null || value === "") continue;
415
- out[key] = String(value);
416
- }
417
- return out;
437
+
438
+ // src/setup.ts
439
+ function pollToolName(triggerType) {
440
+ return `poll_${triggerType}`;
418
441
  }
419
- function resolveRequest(request, scope) {
420
- const method = (request.method ?? "GET").toUpperCase();
421
- const rawUrl = resolveTemplates(request.url, scope, intoUrl);
422
- if (typeof rawUrl !== "string" || rawUrl.trim() === "") {
423
- throw new Error("Request has no URL once its templates are resolved");
424
- }
425
- let url;
426
- try {
427
- url = new URL(rawUrl);
428
- } catch {
429
- throw new Error(`Request URL is not a URL once its templates are resolved: "${rawUrl}"`);
430
- }
431
- for (const [key, value] of Object.entries(stringMap(resolveTemplates(request.query, scope)))) {
432
- url.searchParams.set(key, value);
433
- }
434
- const headers = resolveHeaders(request.headers, scope);
435
- const resolved = { url: url.toString(), method, headers };
436
- if (request.body !== void 0 && method !== "GET" && method !== "HEAD") {
437
- const body = resolveTemplates(request.body, scope);
438
- if (body !== void 0) {
439
- resolved.body = typeof body === "string" ? body : JSON.stringify(body);
440
- if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) {
441
- resolved.headers["content-type"] = typeof body === "string" ? "text/plain" : "application/json";
442
- }
443
- }
444
- }
445
- return resolved;
442
+ function footerToolName(footerId) {
443
+ return `vorn_footer_${footerId}`;
446
444
  }
447
- async function readBody(response) {
448
- const text = await response.text();
449
- if (text === "") return void 0;
450
- const type = response.headers.get("content-type") ?? "";
451
- if (!type.includes("json")) return text;
452
- try {
453
- return JSON.parse(text);
454
- } catch {
455
- return text;
456
- }
445
+ function handlerToolName(handlerId) {
446
+ return `vorn_handler_${handlerId}`;
457
447
  }
458
- function describeFailure(response, body) {
459
- const detail = typeof body === "string" ? body : body === void 0 ? "" : JSON.stringify(body);
460
- const quoted = detail.length > MAX_ERROR_BODY ? `${detail.slice(0, MAX_ERROR_BODY)}\u2026` : detail;
461
- return `Request failed with ${response.status} ${response.statusText}${quoted ? `: ${quoted}` : ""}`;
448
+ var MANIFEST_TOOL = "vorn_connector_manifest";
449
+ var PREFLIGHT_TOOL = "vorn_connector_preflight";
450
+ var OPTIONS_TOOL = "vorn_connector_options";
451
+ function connectionSetup(connector, triggerType) {
452
+ const trigger = connector.triggers.find((entry) => entry.type === triggerType);
453
+ if (!trigger) {
454
+ throw new Error(`Connector ${connector.id} has no trigger "${triggerType}"`);
455
+ }
456
+ return {
457
+ connectorId: connector.id,
458
+ triggerType,
459
+ filters: {
460
+ pollTool: pollToolName(triggerType),
461
+ itemsPath: "items",
462
+ idField: "externalId",
463
+ timestampField: "updatedAt",
464
+ titleField: "title",
465
+ urlField: "url",
466
+ cursorArg: "cursor",
467
+ cursorPath: "nextCursor"
468
+ },
469
+ env: connector.config.map((field) => ({
470
+ name: envNameFor(field.key, field.env),
471
+ required: field.required === true,
472
+ secret: field.secret === true,
473
+ ...field.description !== void 0 && { description: field.description },
474
+ ...field.builderHint !== void 0 && { builderHint: field.builderHint }
475
+ }))
476
+ };
462
477
  }
463
- async function sendRequest(resolved, options) {
464
- const response = await options.fetchImpl(resolved.url, {
465
- method: resolved.method,
466
- headers: resolved.headers,
467
- ...resolved.body !== void 0 && { body: resolved.body }
478
+ function manifestContributions(connector) {
479
+ const contributes = connector.contributes;
480
+ if (!contributes) return void 0;
481
+ const shared = (contribution) => ({
482
+ id: contribution.id,
483
+ title: contribution.title,
484
+ ...contribution.description !== void 0 && { description: contribution.description },
485
+ ...contribution.when !== void 0 && { when: contribution.when }
468
486
  });
469
- const body = await readBody(response);
470
- if (!response.ok) throw new Error(describeFailure(response, body));
471
- return { response, body };
487
+ return {
488
+ ...contributes.panes !== void 0 && {
489
+ panes: contributes.panes.map((pane) => ({
490
+ ...shared(pane),
491
+ ...pane.icon !== void 0 && { icon: pane.icon },
492
+ ...pane.web !== void 0 && { web: pane.web },
493
+ ...pane.command !== void 0 && { command: pane.command }
494
+ }))
495
+ },
496
+ ...contributes.footers !== void 0 && {
497
+ footers: contributes.footers.map((footer) => ({ ...shared(footer), every: footer.every }))
498
+ },
499
+ ...contributes.linkHandlers !== void 0 && {
500
+ linkHandlers: contributes.linkHandlers.map((handler) => ({
501
+ ...shared(handler),
502
+ pattern: handler.pattern,
503
+ example: handler.example
504
+ }))
505
+ }
506
+ };
472
507
  }
473
- function asOutput(value) {
474
- if (Array.isArray(value)) return { items: value };
475
- if (typeof value === "object" && value !== null) return value;
476
- return value === void 0 ? {} : { result: value };
508
+ function connectorManifest(connector) {
509
+ const contributes = manifestContributions(connector);
510
+ return {
511
+ id: connector.id,
512
+ name: connector.name,
513
+ version: connector.version,
514
+ kind: connector.kind,
515
+ ...connector.description !== void 0 && { description: connector.description },
516
+ ...connector.icon !== void 0 && { icon: connector.icon },
517
+ ...connector.auth !== void 0 && { auth: connector.auth },
518
+ ...contributes !== void 0 && { contributes },
519
+ ...connector.permissions !== void 0 && { permissions: connector.permissions },
520
+ ...connector.activates !== void 0 && { activates: connector.activates },
521
+ triggers: connector.triggers.map((trigger) => ({
522
+ type: trigger.type,
523
+ label: trigger.label,
524
+ ...trigger.description !== void 0 && { description: trigger.description },
525
+ // Carried through so the app can seed a connection's status mapping and
526
+ // its polling workflow. Absent when the connector said nothing, which is
527
+ // different from saying there is nothing.
528
+ ...trigger.statusMapping !== void 0 && { statusMapping: trigger.statusMapping },
529
+ ...trigger.defaultWorkflow !== void 0 && { defaultWorkflow: trigger.defaultWorkflow },
530
+ setup: connectionSetup(connector, trigger.type)
531
+ })),
532
+ actions: connector.actions.map((action) => ({
533
+ type: action.type,
534
+ label: action.label,
535
+ ...action.description !== void 0 && { description: action.description },
536
+ inputs: (action.inputs ?? []).map((input) => ({
537
+ key: input.key,
538
+ label: input.label,
539
+ type: input.type ?? "string",
540
+ required: input.required === true,
541
+ ...input.options !== void 0 && { options: input.options },
542
+ ...input.loadOptions !== void 0 && { loadOptions: input.loadOptions },
543
+ ...input.builderHint !== void 0 && { builderHint: input.builderHint }
544
+ })),
545
+ ...action.outputs !== void 0 && { outputs: action.outputs },
546
+ ...action.sample !== void 0 && { sample: action.sample }
547
+ }))
548
+ };
477
549
  }
478
- var MAX_REQUEST_PAGES = 100;
479
- var LINK_NEXT = /<([^>]+)>\s*;[^,]*\brel\s*=\s*"?next"?/i;
480
- function nextLink(header) {
481
- const match = header === null ? null : LINK_NEXT.exec(header);
482
- return match ? match[1] : void 0;
550
+
551
+ // src/packaging.ts
552
+ import { builtinModules } from "module";
553
+ import { existsSync, readFileSync } from "fs";
554
+ import { cp, mkdtemp, readdir, stat, writeFile } from "fs/promises";
555
+ import { tmpdir } from "os";
556
+ import { dirname, isAbsolute, join, resolve } from "path";
557
+ var MAX_PACK_BYTES = 8 * 1024 * 1024;
558
+ var MAX_UNPACKED_BYTES = 32 * 1024 * 1024;
559
+ var LIFECYCLE_SCRIPTS = [
560
+ "preinstall",
561
+ "install",
562
+ "postinstall",
563
+ "prepare",
564
+ "prepublish",
565
+ "prepublishOnly",
566
+ "postpublish"
567
+ ];
568
+ var BUILTINS = new Set(builtinModules);
569
+ function finding(code, target, message, level = "error") {
570
+ return { level, code, target, message };
483
571
  }
484
- function pageItems(body, itemsPath) {
485
- const value = itemsPath === void 0 ? body : valueAt(body, itemsPath);
486
- return Array.isArray(value) ? value : void 0;
572
+ function lifecycleScriptFindings(pkg) {
573
+ const scripts = pkg?.scripts;
574
+ if (!scripts || typeof scripts !== "object") return [];
575
+ const named = LIFECYCLE_SCRIPTS.filter((name) => typeof scripts[name] === "string");
576
+ if (named.length === 0) return [];
577
+ return [
578
+ finding(
579
+ "lifecycle-scripts",
580
+ "package.json",
581
+ `Remove the ${named.join(", ")} script(s); a pack is installed by copying files, never by running them`
582
+ )
583
+ ];
487
584
  }
488
- async function collectPages(request, strategy, scope, options) {
489
- const collected = [];
490
- const seen = /* @__PURE__ */ new Set();
491
- let page2 = strategy.kind === "page" ? strategy.startPage ?? 1 : 0;
492
- let cursor;
493
- let nextUrl;
494
- for (let index = 0; index < MAX_REQUEST_PAGES; index++) {
495
- const resolved = resolveRequest(request, scope);
496
- if (nextUrl !== void 0) resolved.url = nextUrl;
497
- if (strategy.kind === "cursor" && cursor !== void 0) {
498
- const url = new URL(resolved.url);
499
- url.searchParams.set(strategy.param, cursor);
500
- resolved.url = url.toString();
585
+ function bundleDependencyFindings(external) {
586
+ const specifiers = /* @__PURE__ */ new Set();
587
+ for (const specifier of external) {
588
+ if (specifier.startsWith(".") || specifier.startsWith("/")) continue;
589
+ if (specifier.startsWith("node:") || BUILTINS.has(specifier)) continue;
590
+ specifiers.add(specifier);
591
+ }
592
+ if (specifiers.size === 0) return [];
593
+ return [
594
+ finding(
595
+ "runtime-dependencies",
596
+ "bundle",
597
+ `${[...specifiers].sort().join(", ")} stayed outside the bundle; a pack must launch with no install step`
598
+ )
599
+ ];
600
+ }
601
+ var RELATIVE_REQUIRE = /(?:__)?(?:require(?:\.resolve)?|import)\(\s*(['"])(\.\.?\/[^'"]*)\1\s*\)/y;
602
+ var RELATIVE_CREATE_REQUIRE = /createRequire\([^()]*\)\(\s*(['"])(\.\.?\/[^'"]*)\1\s*\)/y;
603
+ var CALL_WORDS = /* @__PURE__ */ new Set(["require", "__require", "createRequire", "import"]);
604
+ var BEFORE_REGEX = /* @__PURE__ */ new Set(["", ..."(,=:[!&|?{};+-*%~^<>"]);
605
+ var BEFORE_REGEX_WORDS = /* @__PURE__ */ new Set([
606
+ "return",
607
+ "typeof",
608
+ "instanceof",
609
+ "in",
610
+ "of",
611
+ "new",
612
+ "delete",
613
+ "void",
614
+ "case",
615
+ "do",
616
+ "else",
617
+ "yield",
618
+ "await"
619
+ ]);
620
+ var WORD = /[\w$]/;
621
+ function endOfQuoted(code, start) {
622
+ const quote2 = code[start];
623
+ let i = start + 1;
624
+ while (i < code.length) {
625
+ if (code[i] === "\\") {
626
+ i += 2;
627
+ continue;
501
628
  }
502
- if (strategy.kind === "page") {
503
- const url = new URL(resolved.url);
504
- url.searchParams.set(strategy.param, String(page2));
505
- resolved.url = url.toString();
629
+ if (code[i] === quote2) return i + 1;
630
+ i += 1;
631
+ }
632
+ return code.length;
633
+ }
634
+ function endOfRegex(code, start) {
635
+ let i = start + 1;
636
+ let inClass = false;
637
+ while (i < code.length) {
638
+ const ch = code[i];
639
+ if (ch === "\\") {
640
+ i += 2;
641
+ continue;
506
642
  }
507
- if (seen.has(resolved.url)) {
508
- throw new Error(`Request for ${request.url} asked for the same page twice`);
643
+ if (ch === "\n") return i;
644
+ if (ch === "[") inClass = true;
645
+ else if (ch === "]") inClass = false;
646
+ else if (ch === "/" && !inClass) return i + 1;
647
+ i += 1;
648
+ }
649
+ return code.length;
650
+ }
651
+ function relativeRuntimeSpecifiers(code) {
652
+ const found = /* @__PURE__ */ new Set();
653
+ let previous = "";
654
+ let previousWord = "";
655
+ let i = 0;
656
+ while (i < code.length) {
657
+ const ch = code[i];
658
+ if (ch === "/" && code[i + 1] === "/") {
659
+ const end = code.indexOf("\n", i);
660
+ i = end === -1 ? code.length : end;
661
+ continue;
509
662
  }
510
- seen.add(resolved.url);
511
- const { response, body } = await sendRequest(resolved, options);
512
- const items = pageItems(body, strategy.itemsPath);
513
- if (items === void 0) {
514
- if (index === 0) {
515
- const where = strategy.itemsPath === void 0 ? "the response is not a list" : `the response has no list at "${strategy.itemsPath}"`;
516
- throw new Error(`Cannot page through this request: ${where}`);
517
- }
518
- return collected;
663
+ if (ch === "/" && code[i + 1] === "*") {
664
+ const end = code.indexOf("*/", i + 2);
665
+ i = end === -1 ? code.length : end + 2;
666
+ continue;
519
667
  }
520
- collected.push(...items);
521
- if (items.length === 0) return collected;
522
- if (strategy.kind === "cursor") {
523
- const next = valueAt(body, strategy.cursorPath);
524
- if (next === void 0 || next === null || next === "") return collected;
525
- cursor = String(next);
668
+ if (ch === '"' || ch === "'" || ch === "`") {
669
+ i = endOfQuoted(code, i);
670
+ previous = ch;
671
+ previousWord = "";
526
672
  continue;
527
673
  }
528
- if (strategy.kind === "link") {
529
- nextUrl = nextLink(response.headers.get("link"));
530
- if (nextUrl === void 0) return collected;
674
+ if (ch === "/" && (BEFORE_REGEX.has(previous) || BEFORE_REGEX_WORDS.has(previousWord))) {
675
+ i = endOfRegex(code, i);
676
+ previous = "/";
677
+ previousWord = "";
678
+ continue;
679
+ }
680
+ if (WORD.test(ch)) {
681
+ const start = i;
682
+ while (i < code.length && WORD.test(code[i])) i += 1;
683
+ const word = code.slice(start, i);
684
+ if (CALL_WORDS.has(word) && code[start - 1] !== ".") {
685
+ for (const pattern of [RELATIVE_REQUIRE, RELATIVE_CREATE_REQUIRE]) {
686
+ pattern.lastIndex = start;
687
+ const match = pattern.exec(code);
688
+ if (match) found.add(match[2]);
689
+ }
690
+ }
691
+ previous = code[i - 1];
692
+ previousWord = word;
531
693
  continue;
532
694
  }
533
- page2 += 1;
695
+ if (!/\s/.test(ch)) {
696
+ previous = ch;
697
+ previousWord = "";
698
+ }
699
+ i += 1;
700
+ }
701
+ return [...found];
702
+ }
703
+ function bundledRequireFindings(code) {
704
+ const specifiers = relativeRuntimeSpecifiers(code);
705
+ if (specifiers.length === 0) return [];
706
+ return [
707
+ finding(
708
+ "runtime-dependencies",
709
+ "bundle",
710
+ `${specifiers.sort().join(", ")} ${specifiers.length === 1 ? "is" : "are"} required at runtime; a pack is one file, so nothing beside it survives packing`,
711
+ "warn"
712
+ )
713
+ ];
714
+ }
715
+ function packageDirFor(resolveDir, entry) {
716
+ const from = resolve(resolveDir);
717
+ if (entry === void 0) return from;
718
+ return entry.startsWith(".") || isAbsolute(entry) ? dirname(resolve(from, entry)) : from;
719
+ }
720
+ function readNearestPackageJson(fromDir) {
721
+ let current = resolve(fromDir);
722
+ for (; ; ) {
723
+ try {
724
+ return JSON.parse(readFileSync(join(current, "package.json"), "utf8"));
725
+ } catch {
726
+ const parent = dirname(current);
727
+ if (parent === current) return void 0;
728
+ current = parent;
729
+ }
534
730
  }
535
- throw new Error(`Request for ${request.url} exceeded ${MAX_REQUEST_PAGES} pages`);
536
731
  }
537
- async function executeRequest(request, postReceive, scope, options) {
538
- if (request.paginate) {
539
- const items = await collectPages(request, request.paginate, scope, options);
540
- return asOutput(applyPostReceive(items, postReceive));
732
+ function packageRootFor(fromDir) {
733
+ let current = resolve(fromDir);
734
+ for (; ; ) {
735
+ if (existsSync(join(current, "package.json"))) return current;
736
+ const parent = dirname(current);
737
+ if (parent === current) return resolve(fromDir);
738
+ current = parent;
541
739
  }
542
- const { body } = await sendRequest(resolveRequest(request, scope), options);
543
- return asOutput(applyPostReceive(body, postReceive));
544
740
  }
545
-
546
- // src/resilience.ts
547
- var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
548
- var DEFAULT_ATTEMPTS = 3;
549
- var DEFAULT_BASE_DELAY_MS = 250;
550
- var DEFAULT_MAX_DELAY_MS = 3e4;
551
- var MAX_ATTEMPTS = 10;
552
- var MAX_TOTAL_WAIT_MS = 12e4;
553
- var wait = (ms) => new Promise((resolve3) => {
554
- setTimeout(resolve3, ms);
555
- });
556
- function retryAfterMs(header, now) {
557
- if (!header) return void 0;
558
- const trimmed = header.trim();
559
- if (trimmed === "") return void 0;
560
- const seconds = Number(trimmed);
561
- if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
562
- const at = Date.parse(trimmed);
563
- if (Number.isNaN(at)) return void 0;
564
- return Math.max(0, at - now);
741
+ function packEntryContents(entry, sdkModule = "@vornrun/connector-sdk") {
742
+ return [
743
+ `import { serveConnector } from ${JSON.stringify(sdkModule)}`,
744
+ `import * as entry from ${JSON.stringify(entry)}`,
745
+ "const exported = Object.values(entry).find((value) => value && Array.isArray(value.triggers))",
746
+ `if (!exported) throw new Error(${JSON.stringify(`${entry} exports no connector`)})`,
747
+ "await serveConnector(exported)",
748
+ ""
749
+ ].join("\n");
565
750
  }
566
- function backoffMs(attempt, policy = {}) {
567
- const base = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
568
- const max = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
569
- return Math.min(max, base * 2 ** attempt);
751
+ var WEB_DIR = "web";
752
+ async function directoryBytes(dir) {
753
+ let total = 0;
754
+ for (const entry of await readdir(dir, { withFileTypes: true, recursive: true })) {
755
+ if (!entry.isFile()) continue;
756
+ total += (await stat(join(entry.parentPath, entry.name))).size;
757
+ }
758
+ return total;
759
+ }
760
+ function webDirectories(connector) {
761
+ const dirs = (connector.contributes?.panes ?? []).map((pane) => pane.web).filter((web) => web !== void 0).map((web) => dirname(web));
762
+ return [...new Set(dirs)];
763
+ }
764
+ async function stagePack(connector, code, packageRoot) {
765
+ const dir = await mkdtemp(join(tmpdir(), "vorn-pack-"));
766
+ await writeFile(join(dir, "index.js"), code, "utf8");
767
+ await writeFile(
768
+ join(dir, "manifest.json"),
769
+ `${JSON.stringify(connectorManifest(connector), null, 2)}
770
+ `,
771
+ "utf8"
772
+ );
773
+ if (packageRoot !== void 0) {
774
+ for (const relative of webDirectories(connector)) {
775
+ const from = join(packageRoot, relative);
776
+ if (existsSync(from)) await cp(from, join(dir, relative), { recursive: true });
777
+ }
778
+ }
779
+ return dir;
780
+ }
781
+ var LAUNCH_TIMEOUT_MS = 15e3;
782
+ var LAUNCH_ENV_KEYS = [
783
+ "PATH",
784
+ "HOME",
785
+ "USERPROFILE",
786
+ "HOMEDRIVE",
787
+ "HOMEPATH",
788
+ "APPDATA",
789
+ "LOCALAPPDATA",
790
+ "PROGRAMDATA",
791
+ "PROGRAMFILES",
792
+ "SystemRoot",
793
+ "SYSTEMDRIVE",
794
+ "COMSPEC",
795
+ "PATHEXT",
796
+ "TMPDIR",
797
+ "TEMP",
798
+ "TMP",
799
+ "LANG",
800
+ "LC_ALL",
801
+ "LC_CTYPE",
802
+ "TZ",
803
+ "SHELL",
804
+ "TERM",
805
+ "USER",
806
+ "LOGNAME",
807
+ "NODE_EXTRA_CA_CERTS"
808
+ ];
809
+ function launchEnv() {
810
+ const env = {};
811
+ for (const key of LAUNCH_ENV_KEYS) {
812
+ const value = process.env[key];
813
+ if (value !== void 0) env[key] = value;
814
+ }
815
+ return env;
816
+ }
817
+ function errorLine(text) {
818
+ const lines = text.split("\n").map((line) => line.trim()).filter((line) => line !== "");
819
+ return [...lines].reverse().find((line) => /Error\b/.test(line)) ?? lines[lines.length - 1];
820
+ }
821
+ function withTimeout(promise, ms, message) {
822
+ let timer;
823
+ return Promise.race([
824
+ promise.finally(() => clearTimeout(timer)),
825
+ new Promise((_, reject) => {
826
+ timer = setTimeout(() => reject(new Error(message)), ms);
827
+ })
828
+ ]);
829
+ }
830
+ async function packLaunchFindings(dir) {
831
+ const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
832
+ const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
833
+ const transport = new StdioClientTransport({
834
+ command: process.execPath,
835
+ args: ["index.js"],
836
+ cwd: dir,
837
+ env: launchEnv(),
838
+ stderr: "pipe"
839
+ });
840
+ const client = new Client({ name: "vorn-connector-check", version: "1" }, { capabilities: {} });
841
+ let stderr = "";
842
+ transport.stderr?.on("data", (chunk) => {
843
+ stderr += chunk.toString();
844
+ });
845
+ try {
846
+ await withTimeout(
847
+ client.connect(transport),
848
+ LAUNCH_TIMEOUT_MS,
849
+ `did not answer within ${LAUNCH_TIMEOUT_MS / 1e3}s of starting`
850
+ );
851
+ return [];
852
+ } catch (error) {
853
+ const said = error instanceof Error ? error.message : String(error);
854
+ return [
855
+ finding("pack-launch", "bundle", `did not start as a pack: ${errorLine(stderr) ?? said}`)
856
+ ];
857
+ } finally {
858
+ await client.close().catch(() => {
859
+ });
860
+ await transport.close().catch(() => {
861
+ });
862
+ }
570
863
  }
571
- function resilientFetch(options) {
572
- const attempts = Math.min(MAX_ATTEMPTS, Math.max(1, options.retry?.attempts ?? DEFAULT_ATTEMPTS));
573
- const sleep = options.sleep ?? wait;
574
- const ceiling = options.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
575
- const send = async (input, init) => {
576
- let waited = 0;
577
- const pause = async (ms) => {
578
- if (waited + ms > MAX_TOTAL_WAIT_MS) return false;
579
- waited += ms;
580
- await sleep(ms);
581
- return true;
582
- };
583
- for (let attempt = 0; attempt < attempts; attempt++) {
584
- const last = attempt === attempts - 1;
585
- try {
586
- const response = await options.fetchImpl(input, init);
587
- if (!RETRYABLE_STATUS.has(response.status)) return response;
588
- if (!options.retryable || last) return response;
589
- const asked = retryAfterMs(response.headers.get("retry-after"), Date.now());
590
- const delay = asked === void 0 ? backoffMs(attempt, options.retry) : Math.min(asked, ceiling);
591
- if (!await pause(delay)) return response;
592
- } catch (error) {
593
- if (!options.retryable || last) throw error;
594
- if (!await pause(backoffMs(attempt, options.retry))) throw error;
595
- }
864
+ async function esbuildBundle(request) {
865
+ const { build } = await import("esbuild");
866
+ const result = await build({
867
+ stdin: {
868
+ contents: request.contents,
869
+ resolveDir: request.resolveDir,
870
+ sourcefile: "vorn-connector-pack.js",
871
+ loader: "js"
872
+ },
873
+ bundle: true,
874
+ platform: "node",
875
+ target: "node20",
876
+ format: "esm",
877
+ write: false,
878
+ metafile: true,
879
+ legalComments: "none",
880
+ // A bundled CommonJS dependency asks for its builtins through esbuild's shim, which throws unless a real require is in scope.
881
+ banner: {
882
+ js: [
883
+ "import { createRequire as __vornCreateRequire } from 'node:module'",
884
+ "const require = __vornCreateRequire(import.meta.url)",
885
+ ""
886
+ ].join("\n")
596
887
  }
597
- throw new Error("Request was never attempted");
888
+ });
889
+ const output = Object.values(result.metafile.outputs)[0];
890
+ return {
891
+ code: result.outputFiles[0].text,
892
+ external: (output?.imports ?? []).filter((item) => item.external).map((item) => item.path)
598
893
  };
599
- return send;
600
894
  }
601
895
 
602
- // src/runtime.ts
603
- var MAX_POLL_PAGES = 1e3;
604
- async function runPoll(connector, triggerType, options = {}) {
605
- const trigger = connector.triggers.find((entry) => entry.type === triggerType);
606
- if (!trigger) {
607
- throw new Error(`Connector ${connector.id} has no trigger "${triggerType}"`);
896
+ // src/host.ts
897
+ var HOST_URL_ENV = "VORN_EXTENSION_HOST";
898
+ var HOST_TOKEN_ENV = "VORN_EXTENSION_TOKEN";
899
+ var PermissionDeniedError = class extends Error {
900
+ constructor(method, detail) {
901
+ super(`The host refused ${method}: ${detail}`);
902
+ this.name = "PermissionDeniedError";
608
903
  }
609
- const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
610
- const polledAt = now();
611
- const context = {
612
- config: options.config ?? {},
613
- ...options.since !== void 0 && { since: options.since },
614
- ...options.cursor !== void 0 && { cursor: options.cursor },
615
- ...options.limit !== void 0 && { limit: options.limit },
616
- now,
617
- // A poll only reads, so every failure it meets is worth trying again.
618
- fetch: resilientFetch({
619
- fetchImpl: options.fetchImpl ?? globalThis.fetch,
620
- retryable: true,
621
- ...options.retry !== void 0 && { retry: options.retry },
622
- ...options.sleep !== void 0 && { sleep: options.sleep }
623
- })
624
- };
625
- const outcome = typeof trigger.poll === "function" ? await trigger.poll(context) : await pollWithDedupe(trigger, context);
626
- if (!outcome || !Array.isArray(outcome.items)) {
627
- throw new Error(`Trigger ${triggerType} did not return an items array`);
904
+ };
905
+ var HostReplyError = class extends Error {
906
+ constructor(method, detail) {
907
+ super(`The host answered ${method} with ${detail}`);
908
+ this.name = "HostReplyError";
628
909
  }
629
- if (outcome.hasMore && !outcome.nextCursor) {
630
- throw new Error(`Trigger ${triggerType} reported more pages without a nextCursor`);
910
+ };
911
+ var HOST_TIMEOUT_MS = 15e3;
912
+ var LOOPBACK_HOSTS = ["127.0.0.1", "localhost", "[::1]"];
913
+ function endpoint(env) {
914
+ const url = env[HOST_URL_ENV]?.trim();
915
+ const token = env[HOST_TOKEN_ENV]?.trim();
916
+ if (!url || !token) {
917
+ throw new Error(
918
+ `This extension was started without a host bridge; ${HOST_URL_ENV} and ${HOST_TOKEN_ENV} are set by Vorn`
919
+ );
920
+ }
921
+ let parsed;
922
+ try {
923
+ parsed = new URL(url);
924
+ } catch {
925
+ throw new Error(`${HOST_URL_ENV} is ${JSON.stringify(url)}, which is not a URL`);
926
+ }
927
+ if (parsed.protocol !== "http:" || !LOOPBACK_HOSTS.includes(parsed.hostname)) {
928
+ throw new Error(
929
+ `${HOST_URL_ENV} is ${JSON.stringify(url)}; the bridge is served on this machine, over http on ${LOOPBACK_HOSTS.join(", ")}`
930
+ );
931
+ }
932
+ return { url: url.replace(/\/$/, ""), token };
933
+ }
934
+ function createExtensionHost(options) {
935
+ const env = options.env ?? process.env;
936
+ const call = options.fetchImpl ?? fetch;
937
+ async function ask(method, params) {
938
+ const { url, token } = endpoint(env);
939
+ const response = await call(`${url}/${method}`, {
940
+ method: "POST",
941
+ headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
942
+ body: JSON.stringify({ sessionId: options.sessionId, ...params }),
943
+ signal: AbortSignal.timeout(HOST_TIMEOUT_MS)
944
+ });
945
+ const text = await response.text();
946
+ if (response.status === 403) throw new PermissionDeniedError(method, text || "not granted");
947
+ if (!response.ok) throw new Error(`The host answered ${method} with HTTP ${response.status}`);
948
+ if (text === "") return void 0;
949
+ let parsed;
950
+ try {
951
+ parsed = JSON.parse(text);
952
+ } catch {
953
+ throw new HostReplyError(method, "a body that is not JSON");
954
+ }
955
+ if (!parsed || typeof parsed !== "object" || !("result" in parsed)) {
956
+ throw new HostReplyError(method, "a body carrying no result");
957
+ }
958
+ return parsed.result;
631
959
  }
632
960
  return {
633
- items: normalizeItems(outcome.items, polledAt),
634
- ...outcome.nextCursor !== void 0 && { nextCursor: outcome.nextCursor },
635
- hasMore: outcome.hasMore === true
961
+ diff: () => ask("diff", {}),
962
+ status: () => ask("status", {}),
963
+ output: (opts) => ask("output", { ...opts?.lines !== void 0 && { lines: opts.lines } }),
964
+ selection: () => ask("selection", {}),
965
+ send: (text) => ask("send", { text }),
966
+ rename: (name) => ask("rename", { name }),
967
+ usage: () => ask("usage", {})
636
968
  };
637
969
  }
638
- async function drainPoll(connector, triggerType, options = {}) {
639
- const collected = [];
640
- let cursor = options.cursor;
641
- for (let page2 = 0; page2 < MAX_POLL_PAGES; page2++) {
642
- const result = await runPoll(connector, triggerType, {
643
- ...options,
644
- ...cursor !== void 0 && { cursor }
645
- });
646
- collected.push(...result.items);
647
- if (!result.hasMore) return collected;
648
- if (result.nextCursor === cursor) {
649
- throw new Error(`Trigger ${triggerType} did not advance its cursor`);
650
- }
651
- cursor = result.nextCursor;
970
+
971
+ // src/normalize.ts
972
+ var RESERVED_KEYS = [
973
+ "externalId",
974
+ "title",
975
+ "url",
976
+ "description",
977
+ "status",
978
+ "labels",
979
+ "assignee",
980
+ "updatedAt"
981
+ ];
982
+ var UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
983
+ function itemExternalId(item) {
984
+ return String(item.externalId ?? "").trim();
985
+ }
986
+ function itemTimestamp(item, fallback) {
987
+ return isoTimestamp(item.updatedAt, fallback);
988
+ }
989
+ function isoTimestamp(value, fallback) {
990
+ if (value === void 0) return fallback;
991
+ const date = value instanceof Date ? value : new Date(value);
992
+ if (Number.isNaN(date.getTime())) {
993
+ throw new Error(`Invalid updatedAt: ${String(value)}`);
652
994
  }
653
- throw new Error(`Trigger ${triggerType} exceeded ${MAX_POLL_PAGES} pages`);
995
+ return date.toISOString();
654
996
  }
655
- var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
656
- async function runOptions(connector, name, options = {}) {
657
- const loader = connector.options?.[name];
658
- if (!loader) {
659
- throw new Error(`Connector ${connector.id} serves no options set "${name}"`);
997
+ function normalizeItem(item, polledAt) {
998
+ const externalId = itemExternalId(item);
999
+ if (!externalId) {
1000
+ throw new Error("Connector item is missing externalId");
660
1001
  }
661
- const loaded = await loader({
662
- config: options.config ?? {},
663
- now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
664
- fetch: resilientFetch({
665
- fetchImpl: options.fetchImpl ?? globalThis.fetch,
666
- retryable: true,
667
- ...options.retry !== void 0 && { retry: options.retry },
668
- ...options.sleep !== void 0 && { sleep: options.sleep }
669
- })
1002
+ if (!item.title || !item.title.trim()) {
1003
+ throw new Error(`Connector item ${externalId} is missing title`);
1004
+ }
1005
+ const extra = {};
1006
+ for (const [key, value] of Object.entries(item.data ?? {})) {
1007
+ if (RESERVED_KEYS.includes(key)) continue;
1008
+ if (UNSAFE_KEYS.includes(key)) continue;
1009
+ extra[key] = value;
1010
+ }
1011
+ return {
1012
+ ...extra,
1013
+ externalId,
1014
+ title: item.title,
1015
+ url: item.url ?? "",
1016
+ description: item.description ?? "",
1017
+ status: item.status ?? "open",
1018
+ labels: item.labels ?? [],
1019
+ ...item.assignee !== void 0 && { assignee: item.assignee },
1020
+ updatedAt: isoTimestamp(item.updatedAt, polledAt)
1021
+ };
1022
+ }
1023
+ function normalizeItems(items, polledAt) {
1024
+ const seen = /* @__PURE__ */ new Set();
1025
+ return items.map((item) => {
1026
+ const normalized = normalizeItem(item, polledAt);
1027
+ if (seen.has(normalized.externalId)) {
1028
+ throw new Error(`Duplicate externalId "${normalized.externalId}" in one poll page`);
1029
+ }
1030
+ seen.add(normalized.externalId);
1031
+ return normalized;
670
1032
  });
671
- if (!Array.isArray(loaded)) {
672
- throw new Error(`Options set "${name}" did not return an array`);
1033
+ }
1034
+
1035
+ // src/dedupe.ts
1036
+ var MAX_BOUNDARY_IDS = 500;
1037
+ function decodeCursor(cursor, strategy) {
1038
+ if (!cursor) return void 0;
1039
+ let parsed;
1040
+ try {
1041
+ parsed = JSON.parse(cursor);
1042
+ } catch (error) {
1043
+ throw new Error(`Cursor is not valid SDK cursor JSON: ${cursor}`, { cause: error });
673
1044
  }
674
- return loaded.map(
675
- (entry) => typeof entry === "string" ? { value: entry } : { ...entry, value: String(entry.value) }
676
- );
1045
+ const state = parsed;
1046
+ if (!state || typeof state !== "object" || state.v !== 1 || state.s !== strategy) {
1047
+ throw new Error(`Cursor does not belong to the "${strategy}" strategy: ${cursor}`);
1048
+ }
1049
+ const wellFormed = state.s === "timestamp" ? typeof state.t === "string" && Array.isArray(state.ids) && state.ids.every((id) => typeof id === "string") : typeof state.id === "string";
1050
+ if (!wellFormed) {
1051
+ throw new Error(`Cursor is missing the fields the "${strategy}" strategy needs: ${cursor}`);
1052
+ }
1053
+ return state;
677
1054
  }
678
- var MAX_QUOTED_VALUE = 80;
679
- function quote(value) {
680
- return value.length > MAX_QUOTED_VALUE ? `${value.slice(0, MAX_QUOTED_VALUE)}\u2026` : value;
1055
+ function compare(left, right) {
1056
+ if (left === right) return 0;
1057
+ return left < right ? -1 : 1;
681
1058
  }
682
- function coerceArg(value, type) {
683
- if (typeof value !== "string") return value;
684
- if (type === "number") {
685
- const parsed = Number(value);
686
- if (Number.isNaN(parsed)) throw new Error(`Expected a number, got "${quote(value)}"`);
687
- return parsed;
1059
+ function page(chronological, context, hadCursor, nextCursor) {
1060
+ const delivered = context.limit === void 0 ? chronological : chronological.slice(0, context.limit);
1061
+ if (delivered.length === 0) {
1062
+ return { items: [], ...context.cursor !== void 0 && { nextCursor: context.cursor } };
688
1063
  }
689
- if (type === "boolean") {
690
- if (value === "true") return true;
691
- if (value === "false") return false;
692
- throw new Error(`Expected a boolean, got "${quote(value)}"`);
1064
+ return {
1065
+ items: delivered.map((entry) => entry.item),
1066
+ nextCursor: JSON.stringify(nextCursor(delivered)),
1067
+ // Only drain a backlog we know we truncated, and only once a cursor
1068
+ // exists — a first poll should not pull the source's entire history.
1069
+ hasMore: chronological.length > delivered.length && hadCursor
1070
+ };
1071
+ }
1072
+ function timestampPoll(fetched, state, context, polledAt) {
1073
+ const boundary = state?.t ?? context.since;
1074
+ const seen = new Set(state?.ids ?? []);
1075
+ const fresh = [];
1076
+ const pinnedAlreadySeen = [];
1077
+ for (const item of fetched) {
1078
+ const id = itemExternalId(item);
1079
+ const pinned = item.updatedAt === void 0 && boundary !== void 0;
1080
+ const at = pinned ? boundary : itemTimestamp(item, polledAt);
1081
+ const isNew = boundary === void 0 || at > boundary || at === boundary && !seen.has(id);
1082
+ if (isNew) fresh.push({ item, at, id, ...pinned && { pinned: true } });
1083
+ else if (pinned) pinnedAlreadySeen.push(id);
693
1084
  }
694
- if (type === "json") {
695
- try {
696
- return JSON.parse(value);
697
- } catch {
698
- throw new Error(`Expected JSON, got "${quote(value)}"`);
1085
+ fresh.sort((left, right) => compare(left.at, right.at) || compare(left.id, right.id));
1086
+ return page(fresh, context, state !== void 0, (delivered) => {
1087
+ const newest = delivered[delivered.length - 1].at;
1088
+ const atNewest = [];
1089
+ for (let i = delivered.length - 1; i >= 0 && delivered[i].at === newest; i -= 1) {
1090
+ atNewest.push(delivered[i].id);
699
1091
  }
700
- }
701
- return value;
1092
+ const carried = newest === boundary ? [...seen, ...atNewest] : [
1093
+ ...pinnedAlreadySeen,
1094
+ ...delivered.filter((entry) => entry.pinned).map((entry) => entry.id),
1095
+ ...atNewest
1096
+ ];
1097
+ return { v: 1, s: "timestamp", t: newest, ids: carried.slice(-MAX_BOUNDARY_IDS) };
1098
+ });
702
1099
  }
703
- async function runAction(connector, actionType, args, options = {}) {
704
- const action = connector.actions.find((entry) => entry.type === actionType);
705
- if (!action) {
706
- throw new Error(`Connector ${connector.id} has no action "${actionType}"`);
1100
+ function lastItemPoll(fetched, state, context, polledAt) {
1101
+ const keyed = fetched.map((item) => ({
1102
+ item,
1103
+ at: itemTimestamp(item, polledAt),
1104
+ id: itemExternalId(item)
1105
+ }));
1106
+ const stopAt = state ? keyed.findIndex((entry) => entry.id === state.id) : -1;
1107
+ const chronological = (stopAt === -1 ? keyed : keyed.slice(0, stopAt)).reverse();
1108
+ return page(chronological, context, state !== void 0, (delivered) => ({
1109
+ v: 1,
1110
+ s: "lastItem",
1111
+ id: delivered[delivered.length - 1].id
1112
+ }));
1113
+ }
1114
+ async function pollWithDedupe(trigger, context) {
1115
+ const strategy = trigger.dedupe;
1116
+ const fetchItems = trigger.fetch;
1117
+ if (!strategy || !fetchItems) {
1118
+ throw new Error(`Trigger ${trigger.type} is not a declarative trigger`);
707
1119
  }
708
- const coerced = { ...args };
709
- for (const input of action.inputs ?? []) {
710
- const value = coerced[input.key];
711
- if (value === void 0 || value === "") {
712
- if (input.required) throw new Error(`Action ${actionType} requires "${input.key}"`);
713
- delete coerced[input.key];
714
- continue;
715
- }
716
- try {
717
- coerced[input.key] = coerceArg(value, input.type);
718
- } catch (error) {
719
- throw new Error(
720
- `Action ${actionType} argument "${input.key}": ${error instanceof Error ? error.message : String(error)}`,
721
- { cause: error }
722
- );
723
- }
1120
+ const polledAt = context.now();
1121
+ if (strategy === "lastItem") {
1122
+ const state2 = decodeCursor(context.cursor, "lastItem");
1123
+ const fetched2 = await runFetch(trigger.type, fetchItems, {
1124
+ config: context.config,
1125
+ ...state2 && { lastItemId: state2.id },
1126
+ ...context.limit !== void 0 && { limit: context.limit },
1127
+ now: context.now,
1128
+ fetch: context.fetch
1129
+ });
1130
+ return lastItemPoll(fetched2, state2, context, polledAt);
724
1131
  }
725
- const config = options.config ?? {};
726
- const method = (action.request?.method ?? "GET").toUpperCase();
727
- const retryable = action.idempotent === true || action.request !== void 0 && SAFE_METHODS.has(method);
728
- const fetchImpl = resilientFetch({
729
- fetchImpl: options.fetchImpl ?? globalThis.fetch,
730
- retryable,
731
- ...options.retry !== void 0 && { retry: options.retry },
732
- ...options.sleep !== void 0 && { sleep: options.sleep }
1132
+ const state = decodeCursor(context.cursor, "timestamp");
1133
+ const since = state?.t ?? context.since;
1134
+ const fetched = await runFetch(trigger.type, fetchItems, {
1135
+ config: context.config,
1136
+ ...since !== void 0 && { since },
1137
+ ...context.limit !== void 0 && { limit: context.limit },
1138
+ now: context.now,
1139
+ fetch: context.fetch
733
1140
  });
734
- if (action.request !== void 0) {
735
- try {
736
- return await executeRequest(
737
- action.request,
738
- action.postReceive,
739
- { args: coerced, config },
740
- { fetchImpl }
741
- );
742
- } catch (error) {
743
- throw new Error(
744
- `Action ${actionType}: ${error instanceof Error ? error.message : String(error)}`,
745
- { cause: error }
746
- );
1141
+ return timestampPoll(fetched, state, context, polledAt);
1142
+ }
1143
+ async function runFetch(type, fetchItems, context) {
1144
+ const fetched = await fetchItems(context);
1145
+ if (!Array.isArray(fetched)) {
1146
+ throw new Error(`Trigger ${type} fetch() did not return an array`);
1147
+ }
1148
+ return fetched;
1149
+ }
1150
+
1151
+ // src/post-receive.ts
1152
+ var UNSAFE_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1153
+ function isRecord(value) {
1154
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1155
+ }
1156
+ function segments(path) {
1157
+ return path.split(".").map((part) => part.trim()).filter((part) => part !== "");
1158
+ }
1159
+ function valueAt(value, path) {
1160
+ let current = value;
1161
+ for (const key of segments(path)) {
1162
+ if (UNSAFE_KEYS2.has(key)) return void 0;
1163
+ if (Array.isArray(current)) {
1164
+ const index = Number(key);
1165
+ if (!Number.isInteger(index)) return void 0;
1166
+ current = current[index];
1167
+ continue;
747
1168
  }
1169
+ if (!isRecord(current)) return void 0;
1170
+ current = Object.prototype.hasOwnProperty.call(current, key) ? current[key] : void 0;
748
1171
  }
749
- if (typeof action.run !== "function") {
750
- throw new Error(`Action ${actionType} has neither a run() implementation nor a request`);
1172
+ return current;
1173
+ }
1174
+ function withValueAt(value, path, next) {
1175
+ const keys = segments(path);
1176
+ if (keys.length === 0) return next;
1177
+ const [head, ...rest] = keys;
1178
+ if (UNSAFE_KEYS2.has(head)) return value;
1179
+ if (Array.isArray(value)) {
1180
+ const index = Number(head);
1181
+ if (!Number.isInteger(index)) return value;
1182
+ const copy = [...value];
1183
+ copy[index] = rest.length === 0 ? next : withValueAt(copy[index], rest.join("."), next);
1184
+ return copy;
751
1185
  }
752
- const output = await action.run(coerced, {
753
- config,
754
- now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
755
- fetch: fetchImpl
756
- });
757
- return output ?? {};
1186
+ const base = isRecord(value) ? value : {};
1187
+ return {
1188
+ ...base,
1189
+ [head]: rest.length === 0 ? next : withValueAt(base[head], rest.join("."), next)
1190
+ };
758
1191
  }
759
-
760
- // src/define.ts
761
- var KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
762
- var PATH_DATA_PATTERN = /^[MmZzLlHhVvCcSsQqTtAa0-9\s,.\-+eE]+$/;
763
- var VIEW_BOX_PATTERN = /^-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+$/;
764
- var DEDUPE_STRATEGIES = ["timestamp", "lastItem"];
765
- var AUTH_RUNGS = ["none", "cli", "key", "oauth"];
766
- var ABSOLUTE_URL_PATTERN = /^https?:\/\//i;
767
- var CONFIG_ROOTED_URL_PATTERN = /^\{\{\s*config\./;
768
- function assertUnique(kind, keys) {
769
- const seen = /* @__PURE__ */ new Set();
1192
+ function pick(value, keys) {
1193
+ if (Array.isArray(value)) return value.map((entry) => pick(entry, keys));
1194
+ if (!isRecord(value)) return value;
1195
+ const out = {};
770
1196
  for (const key of keys) {
771
- if (seen.has(key)) throw new Error(`Duplicate ${kind} "${key}"`);
772
- seen.add(key);
1197
+ if (UNSAFE_KEYS2.has(key)) continue;
1198
+ if (Object.prototype.hasOwnProperty.call(value, key)) out[key] = value[key];
773
1199
  }
1200
+ return out;
774
1201
  }
775
- function assertAuth(definition) {
776
- const auth = definition.auth;
777
- if (!auth) return;
778
- const id = definition.id;
779
- if (!AUTH_RUNGS.includes(auth.rung)) {
780
- throw new Error(
781
- `Connector ${id} declares unknown auth rung ${JSON.stringify(auth.rung)}; expected ${AUTH_RUNGS.join(", ")}`
782
- );
1202
+ function rename(value, from, to) {
1203
+ if (Array.isArray(value)) return value.map((entry) => rename(entry, from, to));
1204
+ if (!isRecord(value)) return value;
1205
+ if (UNSAFE_KEYS2.has(from) || UNSAFE_KEYS2.has(to)) return value;
1206
+ if (!Object.prototype.hasOwnProperty.call(value, from)) return value;
1207
+ const out = {};
1208
+ for (const [key, entry] of Object.entries(value)) {
1209
+ if (key === from) out[to] = entry;
1210
+ else if (key !== to) out[key] = entry;
783
1211
  }
784
- if (auth.rung === "cli" && !auth.probe?.command?.trim()) {
785
- throw new Error(`Connector ${id} borrows a CLI login but declares no probe command to ask it`);
1212
+ return out;
1213
+ }
1214
+ function applyOp(value, op) {
1215
+ if (op.op === "flatten") return valueAt(value, op.path);
1216
+ const target = op.path === void 0 ? value : valueAt(value, op.path);
1217
+ if (op.path !== void 0 && target === void 0) return value;
1218
+ let next;
1219
+ if (op.op === "pick") next = pick(target, op.keys);
1220
+ else if (op.op === "rename") next = rename(target, op.from, op.to);
1221
+ else if (op.op === "filter") {
1222
+ next = Array.isArray(target) ? target.filter((entry) => isRecord(entry) && valueAt(entry, op.key) === op.equals) : target;
1223
+ } else {
1224
+ next = Array.isArray(target) ? target.map((entry) => applyPostReceive(entry, op.ops)) : target;
786
1225
  }
787
- if (auth.rung === "key") {
788
- const keys = auth.keys ?? [];
789
- if (keys.length === 0) {
790
- throw new Error(`Connector ${id} signs in with a key but names no config field holding it`);
1226
+ return op.path === void 0 ? next : withValueAt(value, op.path, next);
1227
+ }
1228
+ function applyPostReceive(value, ops) {
1229
+ return (ops ?? []).reduce(applyOp, value);
1230
+ }
1231
+
1232
+ // src/request.ts
1233
+ var MAX_ERROR_BODY = 500;
1234
+ var PLACEHOLDER = /\{\{\s*(args|config)\.([A-Za-z0-9_.-]+)\s*\}\}/g;
1235
+ var WHOLE_PLACEHOLDER = /^\{\{\s*(args|config)\.([A-Za-z0-9_.-]+)\s*\}\}$/;
1236
+ function lookup(source, path, scope) {
1237
+ return valueAt(source === "args" ? scope.args : scope.config, path);
1238
+ }
1239
+ var intoUrl = (value, source) => source === "config" ? value : encodeURIComponent(value);
1240
+ function intoHeader(name) {
1241
+ return (value) => {
1242
+ if (/[\r\n]/.test(value)) {
1243
+ throw new Error(`Header "${name}" would carry a line ending, which is not allowed`);
791
1244
  }
792
- const declared = new Set((definition.config ?? []).map((field) => field.key));
793
- for (const key of keys) {
794
- if (!declared.has(key)) {
795
- throw new Error(`Connector ${id} names auth key "${key}", which is not a config field`);
796
- }
1245
+ return value;
1246
+ };
1247
+ }
1248
+ function resolveTemplates(value, scope, substitute) {
1249
+ if (typeof value === "string") {
1250
+ const whole = WHOLE_PLACEHOLDER.exec(value);
1251
+ if (whole) {
1252
+ const resolved = lookup(whole[1], whole[2], scope);
1253
+ if (substitute === void 0 || resolved === void 0 || resolved === null) return resolved;
1254
+ return substitute(String(resolved), whole[1]);
797
1255
  }
1256
+ return value.replace(PLACEHOLDER, (_match, source, path) => {
1257
+ const resolved = lookup(source, path, scope);
1258
+ if (resolved === void 0 || resolved === null) return "";
1259
+ const text = String(resolved);
1260
+ return substitute === void 0 ? text : substitute(text, source);
1261
+ });
798
1262
  }
799
- if (auth.rung === "none") {
800
- const secret = (definition.config ?? []).find((field) => field.secret === true);
801
- if (secret) {
802
- throw new Error(
803
- `Connector ${id} claims it needs no sign-in but declares secret field "${secret.key}"`
804
- );
1263
+ if (Array.isArray(value)) return value.map((entry) => resolveTemplates(entry, scope, substitute));
1264
+ if (typeof value === "object" && value !== null) {
1265
+ const out = {};
1266
+ for (const [key, entry] of Object.entries(value)) {
1267
+ out[key] = resolveTemplates(entry, scope, substitute);
805
1268
  }
1269
+ return out;
806
1270
  }
1271
+ return value;
807
1272
  }
808
- function envNameFor(key, explicit) {
809
- if (explicit) return explicit;
810
- return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
1273
+ function resolveHeaders(raw, scope) {
1274
+ const out = {};
1275
+ for (const [name, value] of Object.entries(raw ?? {})) {
1276
+ const resolved = resolveTemplates(value, scope, intoHeader(name));
1277
+ if (resolved === void 0 || resolved === null || resolved === "") continue;
1278
+ out[name] = String(resolved);
1279
+ }
1280
+ return out;
811
1281
  }
812
- function defineConnector(definition) {
813
- if (!KEY_PATTERN.test(definition.id ?? "")) {
814
- throw new Error(`Connector id "${definition.id}" must start with a letter and be url-safe`);
1282
+ function stringMap(raw) {
1283
+ const out = {};
1284
+ if (typeof raw !== "object" || raw === null) return out;
1285
+ for (const [key, value] of Object.entries(raw)) {
1286
+ if (value === void 0 || value === null || value === "") continue;
1287
+ out[key] = String(value);
815
1288
  }
816
- if (!definition.name?.trim()) {
817
- throw new Error(`Connector ${definition.id} is missing a name`);
1289
+ return out;
1290
+ }
1291
+ function resolveRequest(request, scope) {
1292
+ const method = (request.method ?? "GET").toUpperCase();
1293
+ const rawUrl = resolveTemplates(request.url, scope, intoUrl);
1294
+ if (typeof rawUrl !== "string" || rawUrl.trim() === "") {
1295
+ throw new Error("Request has no URL once its templates are resolved");
818
1296
  }
819
- if (definition.icon) {
820
- const { viewBox, paths } = definition.icon;
821
- if (!Array.isArray(paths) || paths.length === 0) {
822
- throw new Error(`Connector ${definition.id} has an icon with no paths`);
823
- }
824
- for (const path of paths) {
825
- if (typeof path !== "string" || !PATH_DATA_PATTERN.test(path)) {
826
- throw new Error(
827
- `Connector ${definition.id} has an icon path that is not SVG path data. Only path data is accepted, not markup.`
828
- );
829
- }
830
- }
831
- if (viewBox !== void 0 && !VIEW_BOX_PATTERN.test(viewBox)) {
832
- throw new Error(`Connector ${definition.id} has an icon viewBox that is not four numbers`);
833
- }
1297
+ let url;
1298
+ try {
1299
+ url = new URL(rawUrl);
1300
+ } catch {
1301
+ throw new Error(`Request URL is not a URL once its templates are resolved: "${rawUrl}"`);
834
1302
  }
835
- const triggers = definition.triggers ?? [];
836
- const actions = definition.actions ?? [];
837
- if (triggers.length === 0 && actions.length === 0) {
838
- throw new Error(`Connector ${definition.id} declares no triggers and no actions`);
1303
+ for (const [key, value] of Object.entries(stringMap(resolveTemplates(request.query, scope)))) {
1304
+ url.searchParams.set(key, value);
839
1305
  }
840
- for (const trigger of triggers) {
841
- if (!KEY_PATTERN.test(trigger.type ?? "")) {
842
- throw new Error(`Trigger type "${trigger.type}" must start with a letter and be url-safe`);
843
- }
844
- const loose = trigger;
845
- const declarative = typeof loose.fetch === "function";
846
- const imperative = typeof loose.poll === "function";
847
- if (declarative && imperative) {
848
- throw new Error(`Trigger ${trigger.type} declares both fetch() and poll(); pick one`);
849
- }
850
- if (declarative !== (loose.dedupe !== void 0)) {
851
- throw new Error(
852
- `Trigger ${trigger.type} needs fetch() and a dedupe strategy together, not one alone`
853
- );
854
- }
855
- if (loose.dedupe !== void 0 && !DEDUPE_STRATEGIES.includes(loose.dedupe)) {
856
- throw new Error(
857
- `Trigger ${trigger.type} has unknown dedupe strategy ${JSON.stringify(loose.dedupe)}; expected ${DEDUPE_STRATEGIES.join(" or ")}`
858
- );
859
- }
860
- if (loose.poll !== void 0 && !imperative) {
861
- throw new Error(`Trigger ${trigger.type} declares poll but it is not a function`);
862
- }
863
- if (!declarative && !imperative) {
864
- throw new Error(`Trigger ${trigger.type} is missing a fetch() or poll() implementation`);
1306
+ const headers = resolveHeaders(request.headers, scope);
1307
+ const resolved = { url: url.toString(), method, headers };
1308
+ if (request.body !== void 0 && method !== "GET" && method !== "HEAD") {
1309
+ const body = resolveTemplates(request.body, scope);
1310
+ if (body !== void 0) {
1311
+ resolved.body = typeof body === "string" ? body : JSON.stringify(body);
1312
+ if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) {
1313
+ resolved.headers["content-type"] = typeof body === "string" ? "text/plain" : "application/json";
1314
+ }
865
1315
  }
866
1316
  }
867
- for (const action of actions) {
868
- if (!KEY_PATTERN.test(action.type ?? "")) {
869
- throw new Error(`Action type "${action.type}" must start with a letter and be url-safe`);
1317
+ return resolved;
1318
+ }
1319
+ async function readBody(response) {
1320
+ const text = await response.text();
1321
+ if (text === "") return void 0;
1322
+ const type = response.headers.get("content-type") ?? "";
1323
+ if (!type.includes("json")) return text;
1324
+ try {
1325
+ return JSON.parse(text);
1326
+ } catch {
1327
+ return text;
1328
+ }
1329
+ }
1330
+ function describeFailure(response, body) {
1331
+ const detail = typeof body === "string" ? body : body === void 0 ? "" : JSON.stringify(body);
1332
+ const quoted = detail.length > MAX_ERROR_BODY ? `${detail.slice(0, MAX_ERROR_BODY)}\u2026` : detail;
1333
+ return `Request failed with ${response.status} ${response.statusText}${quoted ? `: ${quoted}` : ""}`;
1334
+ }
1335
+ async function sendRequest(resolved, options) {
1336
+ const response = await options.fetchImpl(resolved.url, {
1337
+ method: resolved.method,
1338
+ headers: resolved.headers,
1339
+ ...resolved.body !== void 0 && { body: resolved.body }
1340
+ });
1341
+ const body = await readBody(response);
1342
+ if (!response.ok) throw new Error(describeFailure(response, body));
1343
+ return { response, body };
1344
+ }
1345
+ function asOutput(value) {
1346
+ if (Array.isArray(value)) return { items: value };
1347
+ if (typeof value === "object" && value !== null) return value;
1348
+ return value === void 0 ? {} : { result: value };
1349
+ }
1350
+ var MAX_REQUEST_PAGES = 100;
1351
+ var LINK_NEXT = /<([^>]+)>\s*;[^,]*\brel\s*=\s*"?next"?/i;
1352
+ function nextLink(header) {
1353
+ const match = header === null ? null : LINK_NEXT.exec(header);
1354
+ return match ? match[1] : void 0;
1355
+ }
1356
+ function pageItems(body, itemsPath) {
1357
+ const value = itemsPath === void 0 ? body : valueAt(body, itemsPath);
1358
+ return Array.isArray(value) ? value : void 0;
1359
+ }
1360
+ async function collectPages(request, strategy, scope, options) {
1361
+ const collected = [];
1362
+ const seen = /* @__PURE__ */ new Set();
1363
+ let page2 = strategy.kind === "page" ? strategy.startPage ?? 1 : 0;
1364
+ let cursor;
1365
+ let nextUrl;
1366
+ for (let index = 0; index < MAX_REQUEST_PAGES; index++) {
1367
+ const resolved = resolveRequest(request, scope);
1368
+ if (nextUrl !== void 0) resolved.url = nextUrl;
1369
+ if (strategy.kind === "cursor" && cursor !== void 0) {
1370
+ const url = new URL(resolved.url);
1371
+ url.searchParams.set(strategy.param, cursor);
1372
+ resolved.url = url.toString();
870
1373
  }
871
- const loose = action;
872
- const written = typeof loose.run === "function";
873
- const declared = loose.request !== void 0;
874
- if (written && declared) {
875
- throw new Error(`Action ${action.type} declares both run() and a request; pick one`);
1374
+ if (strategy.kind === "page") {
1375
+ const url = new URL(resolved.url);
1376
+ url.searchParams.set(strategy.param, String(page2));
1377
+ resolved.url = url.toString();
876
1378
  }
877
- if (!written && !declared) {
878
- throw new Error(`Action ${action.type} is missing a run() implementation or a request`);
1379
+ if (seen.has(resolved.url)) {
1380
+ throw new Error(`Request for ${request.url} asked for the same page twice`);
879
1381
  }
880
- if (declared) {
881
- const request = loose.request;
882
- if (typeof request?.url !== "string" || request.url.trim() === "") {
883
- throw new Error(`Action ${action.type} declares a request with no URL`);
884
- }
885
- const url = request.url.trim();
886
- if (!ABSOLUTE_URL_PATTERN.test(url) && !CONFIG_ROOTED_URL_PATTERN.test(url)) {
887
- throw new Error(
888
- `Action ${action.type} declares the request URL "${url}", which is neither absolute nor rooted in a {{config.\u2026}} value`
889
- );
1382
+ seen.add(resolved.url);
1383
+ const { response, body } = await sendRequest(resolved, options);
1384
+ const items = pageItems(body, strategy.itemsPath);
1385
+ if (items === void 0) {
1386
+ if (index === 0) {
1387
+ const where = strategy.itemsPath === void 0 ? "the response is not a list" : `the response has no list at "${strategy.itemsPath}"`;
1388
+ throw new Error(`Cannot page through this request: ${where}`);
890
1389
  }
1390
+ return collected;
891
1391
  }
892
- if (!declared && loose.postReceive !== void 0) {
893
- throw new Error(`Action ${action.type} has postReceive but no request for it to reshape`);
894
- }
895
- for (const input of action.inputs ?? []) {
896
- if (input.loadOptions !== void 0 && definition.options?.[input.loadOptions] === void 0) {
897
- throw new Error(
898
- `Action ${action.type} argument "${input.key}" loads options from "${input.loadOptions}", which the connector does not serve`
899
- );
900
- }
1392
+ collected.push(...items);
1393
+ if (items.length === 0) return collected;
1394
+ if (strategy.kind === "cursor") {
1395
+ const next = valueAt(body, strategy.cursorPath);
1396
+ if (next === void 0 || next === null || next === "") return collected;
1397
+ cursor = String(next);
1398
+ continue;
901
1399
  }
902
- }
903
- assertUnique(
904
- "trigger",
905
- triggers.map((trigger) => trigger.type)
906
- );
907
- assertUnique(
908
- "action",
909
- actions.map((action) => action.type)
910
- );
911
- assertUnique(
912
- "config field",
913
- (definition.config ?? []).map((field) => field.key)
914
- );
915
- assertAuth(definition);
916
- return {
917
- ...definition,
918
- version: definition.version ?? "0.0.0",
919
- config: definition.config ?? [],
920
- triggers,
921
- actions
922
- };
923
- }
924
- function resolveConfig(connector, env = process.env) {
925
- const config = {};
926
- const missing = [];
927
- for (const field of connector.config) {
928
- const name = envNameFor(field.key, field.env);
929
- const value = env[name] ?? field.default;
930
- if (value === void 0 || value === "") {
931
- if (field.required) missing.push(`${field.key} (${name})`);
1400
+ if (strategy.kind === "link") {
1401
+ nextUrl = nextLink(response.headers.get("link"));
1402
+ if (nextUrl === void 0) return collected;
932
1403
  continue;
933
1404
  }
934
- config[field.key] = value;
1405
+ page2 += 1;
935
1406
  }
936
- if (missing.length > 0) {
937
- throw new Error(
938
- `Connector ${connector.id} is missing required configuration: ${missing.join(", ")}`
939
- );
1407
+ throw new Error(`Request for ${request.url} exceeded ${MAX_REQUEST_PAGES} pages`);
1408
+ }
1409
+ async function executeRequest(request, postReceive, scope, options) {
1410
+ if (request.paginate) {
1411
+ const items = await collectPages(request, request.paginate, scope, options);
1412
+ return asOutput(applyPostReceive(items, postReceive));
940
1413
  }
941
- return config;
1414
+ const { body } = await sendRequest(resolveRequest(request, scope), options);
1415
+ return asOutput(applyPostReceive(body, postReceive));
942
1416
  }
943
1417
 
944
- // src/setup.ts
945
- function pollToolName(triggerType) {
946
- return `poll_${triggerType}`;
1418
+ // src/resilience.ts
1419
+ var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
1420
+ var DEFAULT_ATTEMPTS = 3;
1421
+ var DEFAULT_BASE_DELAY_MS = 250;
1422
+ var DEFAULT_MAX_DELAY_MS = 3e4;
1423
+ var MAX_ATTEMPTS = 10;
1424
+ var MAX_TOTAL_WAIT_MS = 12e4;
1425
+ var wait = (ms) => new Promise((resolve4) => {
1426
+ setTimeout(resolve4, ms);
1427
+ });
1428
+ function retryAfterMs(header, now) {
1429
+ if (!header) return void 0;
1430
+ const trimmed = header.trim();
1431
+ if (trimmed === "") return void 0;
1432
+ const seconds = Number(trimmed);
1433
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
1434
+ const at = Date.parse(trimmed);
1435
+ if (Number.isNaN(at)) return void 0;
1436
+ return Math.max(0, at - now);
947
1437
  }
948
- var MANIFEST_TOOL = "vorn_connector_manifest";
949
- var PREFLIGHT_TOOL = "vorn_connector_preflight";
950
- var OPTIONS_TOOL = "vorn_connector_options";
951
- function connectionSetup(connector, triggerType) {
1438
+ function backoffMs(attempt, policy = {}) {
1439
+ const base = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
1440
+ const max = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
1441
+ return Math.min(max, base * 2 ** attempt);
1442
+ }
1443
+ function resilientFetch(options) {
1444
+ const attempts = Math.min(MAX_ATTEMPTS, Math.max(1, options.retry?.attempts ?? DEFAULT_ATTEMPTS));
1445
+ const sleep = options.sleep ?? wait;
1446
+ const ceiling = options.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
1447
+ const send = async (input, init) => {
1448
+ let waited = 0;
1449
+ const pause = async (ms) => {
1450
+ if (waited + ms > MAX_TOTAL_WAIT_MS) return false;
1451
+ waited += ms;
1452
+ await sleep(ms);
1453
+ return true;
1454
+ };
1455
+ for (let attempt = 0; attempt < attempts; attempt++) {
1456
+ const last = attempt === attempts - 1;
1457
+ try {
1458
+ const response = await options.fetchImpl(input, init);
1459
+ if (!RETRYABLE_STATUS.has(response.status)) return response;
1460
+ if (!options.retryable || last) return response;
1461
+ const asked = retryAfterMs(response.headers.get("retry-after"), Date.now());
1462
+ const delay = asked === void 0 ? backoffMs(attempt, options.retry) : Math.min(asked, ceiling);
1463
+ if (!await pause(delay)) return response;
1464
+ } catch (error) {
1465
+ if (!options.retryable || last) throw error;
1466
+ if (!await pause(backoffMs(attempt, options.retry))) throw error;
1467
+ }
1468
+ }
1469
+ throw new Error("Request was never attempted");
1470
+ };
1471
+ return send;
1472
+ }
1473
+
1474
+ // src/runtime.ts
1475
+ var MAX_POLL_PAGES = 1e3;
1476
+ async function runPoll(connector, triggerType, options = {}) {
952
1477
  const trigger = connector.triggers.find((entry) => entry.type === triggerType);
953
1478
  if (!trigger) {
954
1479
  throw new Error(`Connector ${connector.id} has no trigger "${triggerType}"`);
955
1480
  }
956
- return {
957
- connectorId: connector.id,
958
- triggerType,
959
- filters: {
960
- pollTool: pollToolName(triggerType),
961
- itemsPath: "items",
962
- idField: "externalId",
963
- timestampField: "updatedAt",
964
- titleField: "title",
965
- urlField: "url",
966
- cursorArg: "cursor",
967
- cursorPath: "nextCursor"
968
- },
969
- env: connector.config.map((field) => ({
970
- name: envNameFor(field.key, field.env),
971
- required: field.required === true,
972
- secret: field.secret === true,
973
- ...field.description !== void 0 && { description: field.description },
974
- ...field.builderHint !== void 0 && { builderHint: field.builderHint }
975
- }))
1481
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
1482
+ const polledAt = now();
1483
+ const context = {
1484
+ config: options.config ?? {},
1485
+ ...options.since !== void 0 && { since: options.since },
1486
+ ...options.cursor !== void 0 && { cursor: options.cursor },
1487
+ ...options.limit !== void 0 && { limit: options.limit },
1488
+ now,
1489
+ // A poll only reads, so every failure it meets is worth trying again.
1490
+ fetch: resilientFetch({
1491
+ fetchImpl: options.fetchImpl ?? globalThis.fetch,
1492
+ retryable: true,
1493
+ ...options.retry !== void 0 && { retry: options.retry },
1494
+ ...options.sleep !== void 0 && { sleep: options.sleep }
1495
+ })
976
1496
  };
977
- }
978
- function connectorManifest(connector) {
1497
+ const outcome = typeof trigger.poll === "function" ? await trigger.poll(context) : await pollWithDedupe(trigger, context);
1498
+ if (!outcome || !Array.isArray(outcome.items)) {
1499
+ throw new Error(`Trigger ${triggerType} did not return an items array`);
1500
+ }
1501
+ if (outcome.hasMore && !outcome.nextCursor) {
1502
+ throw new Error(`Trigger ${triggerType} reported more pages without a nextCursor`);
1503
+ }
979
1504
  return {
980
- id: connector.id,
981
- name: connector.name,
982
- version: connector.version,
983
- ...connector.description !== void 0 && { description: connector.description },
984
- ...connector.icon !== void 0 && { icon: connector.icon },
985
- ...connector.auth !== void 0 && { auth: connector.auth },
986
- triggers: connector.triggers.map((trigger) => ({
987
- type: trigger.type,
988
- label: trigger.label,
989
- ...trigger.description !== void 0 && { description: trigger.description },
990
- // Carried through so the app can seed a connection's status mapping and
991
- // its polling workflow. Absent when the connector said nothing, which is
992
- // different from saying there is nothing.
993
- ...trigger.statusMapping !== void 0 && { statusMapping: trigger.statusMapping },
994
- ...trigger.defaultWorkflow !== void 0 && { defaultWorkflow: trigger.defaultWorkflow },
995
- setup: connectionSetup(connector, trigger.type)
996
- })),
997
- actions: connector.actions.map((action) => ({
998
- type: action.type,
999
- label: action.label,
1000
- ...action.description !== void 0 && { description: action.description },
1001
- inputs: (action.inputs ?? []).map((input) => ({
1002
- key: input.key,
1003
- label: input.label,
1004
- type: input.type ?? "string",
1005
- required: input.required === true,
1006
- ...input.options !== void 0 && { options: input.options },
1007
- ...input.loadOptions !== void 0 && { loadOptions: input.loadOptions },
1008
- ...input.builderHint !== void 0 && { builderHint: input.builderHint }
1009
- })),
1010
- ...action.outputs !== void 0 && { outputs: action.outputs },
1011
- ...action.sample !== void 0 && { sample: action.sample }
1012
- }))
1505
+ items: normalizeItems(outcome.items, polledAt),
1506
+ ...outcome.nextCursor !== void 0 && { nextCursor: outcome.nextCursor },
1507
+ hasMore: outcome.hasMore === true
1013
1508
  };
1014
1509
  }
1510
+ async function drainPoll(connector, triggerType, options = {}) {
1511
+ const collected = [];
1512
+ let cursor = options.cursor;
1513
+ for (let page2 = 0; page2 < MAX_POLL_PAGES; page2++) {
1514
+ const result = await runPoll(connector, triggerType, {
1515
+ ...options,
1516
+ ...cursor !== void 0 && { cursor }
1517
+ });
1518
+ collected.push(...result.items);
1519
+ if (!result.hasMore) return collected;
1520
+ if (result.nextCursor === cursor) {
1521
+ throw new Error(`Trigger ${triggerType} did not advance its cursor`);
1522
+ }
1523
+ cursor = result.nextCursor;
1524
+ }
1525
+ throw new Error(`Trigger ${triggerType} exceeded ${MAX_POLL_PAGES} pages`);
1526
+ }
1527
+ var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
1528
+ async function runOptions(connector, name, options = {}) {
1529
+ const loader = connector.options?.[name];
1530
+ if (!loader) {
1531
+ throw new Error(`Connector ${connector.id} serves no options set "${name}"`);
1532
+ }
1533
+ const loaded = await loader({
1534
+ config: options.config ?? {},
1535
+ now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
1536
+ fetch: resilientFetch({
1537
+ fetchImpl: options.fetchImpl ?? globalThis.fetch,
1538
+ retryable: true,
1539
+ ...options.retry !== void 0 && { retry: options.retry },
1540
+ ...options.sleep !== void 0 && { sleep: options.sleep }
1541
+ })
1542
+ });
1543
+ if (!Array.isArray(loaded)) {
1544
+ throw new Error(`Options set "${name}" did not return an array`);
1545
+ }
1546
+ return loaded.map(
1547
+ (entry) => typeof entry === "string" ? { value: entry } : { ...entry, value: String(entry.value) }
1548
+ );
1549
+ }
1550
+ var MAX_QUOTED_VALUE = 80;
1551
+ function quote(value) {
1552
+ return value.length > MAX_QUOTED_VALUE ? `${value.slice(0, MAX_QUOTED_VALUE)}\u2026` : value;
1553
+ }
1554
+ function coerceArg(value, type) {
1555
+ if (typeof value !== "string") return value;
1556
+ if (type === "number") {
1557
+ const parsed = Number(value);
1558
+ if (Number.isNaN(parsed)) throw new Error(`Expected a number, got "${quote(value)}"`);
1559
+ return parsed;
1560
+ }
1561
+ if (type === "boolean") {
1562
+ if (value === "true") return true;
1563
+ if (value === "false") return false;
1564
+ throw new Error(`Expected a boolean, got "${quote(value)}"`);
1565
+ }
1566
+ if (type === "json") {
1567
+ try {
1568
+ return JSON.parse(value);
1569
+ } catch {
1570
+ throw new Error(`Expected JSON, got "${quote(value)}"`);
1571
+ }
1572
+ }
1573
+ return value;
1574
+ }
1575
+ async function runAction(connector, actionType, args, options = {}) {
1576
+ const action = connector.actions.find((entry) => entry.type === actionType);
1577
+ if (!action) {
1578
+ throw new Error(`Connector ${connector.id} has no action "${actionType}"`);
1579
+ }
1580
+ const coerced = { ...args };
1581
+ for (const input of action.inputs ?? []) {
1582
+ const value = coerced[input.key];
1583
+ if (value === void 0 || value === "") {
1584
+ if (input.required) throw new Error(`Action ${actionType} requires "${input.key}"`);
1585
+ delete coerced[input.key];
1586
+ continue;
1587
+ }
1588
+ try {
1589
+ coerced[input.key] = coerceArg(value, input.type);
1590
+ } catch (error) {
1591
+ throw new Error(
1592
+ `Action ${actionType} argument "${input.key}": ${error instanceof Error ? error.message : String(error)}`,
1593
+ { cause: error }
1594
+ );
1595
+ }
1596
+ }
1597
+ const config = options.config ?? {};
1598
+ const method = (action.request?.method ?? "GET").toUpperCase();
1599
+ const retryable = action.idempotent === true || action.request !== void 0 && SAFE_METHODS.has(method);
1600
+ const fetchImpl = resilientFetch({
1601
+ fetchImpl: options.fetchImpl ?? globalThis.fetch,
1602
+ retryable,
1603
+ ...options.retry !== void 0 && { retry: options.retry },
1604
+ ...options.sleep !== void 0 && { sleep: options.sleep }
1605
+ });
1606
+ if (action.request !== void 0) {
1607
+ try {
1608
+ return await executeRequest(
1609
+ action.request,
1610
+ action.postReceive,
1611
+ { args: coerced, config },
1612
+ { fetchImpl }
1613
+ );
1614
+ } catch (error) {
1615
+ throw new Error(
1616
+ `Action ${actionType}: ${error instanceof Error ? error.message : String(error)}`,
1617
+ { cause: error }
1618
+ );
1619
+ }
1620
+ }
1621
+ if (typeof action.run !== "function") {
1622
+ throw new Error(`Action ${actionType} has neither a run() implementation nor a request`);
1623
+ }
1624
+ const output = await action.run(coerced, {
1625
+ config,
1626
+ now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
1627
+ fetch: fetchImpl
1628
+ });
1629
+ return output ?? {};
1630
+ }
1015
1631
 
1016
1632
  // src/harness.ts
1017
1633
  function matches(route, method, url) {
@@ -1109,8 +1725,44 @@ function createConnectorHarness(connector, harnessOptions = {}) {
1109
1725
  withMockHttp
1110
1726
  };
1111
1727
  }
1728
+ var HOST_FIXTURES = {
1729
+ diff: async () => "diff --git a/src/index.ts b/src/index.ts\n@@ -1 +1 @@\n-const a = 1\n+const a = 2\n",
1730
+ status: async () => " M src/index.ts\n",
1731
+ output: async () => "$ yarn test\n Test Files 1 passed (1)\n",
1732
+ selection: async () => "",
1733
+ send: async () => {
1734
+ },
1735
+ rename: async () => {
1736
+ },
1737
+ usage: async () => ({
1738
+ contextTokens: 13e4,
1739
+ contextWindow: 1e6,
1740
+ cacheHitRate: 0.93,
1741
+ limits: [{ window: "5h", remaining: 0.72 }]
1742
+ })
1743
+ };
1744
+ function mockExtensionHost(granted, answers = {}) {
1745
+ const allowed = new Set(granted);
1746
+ const used = /* @__PURE__ */ new Set();
1747
+ const host = {};
1748
+ for (const name of Object.keys(HOST_FIXTURES)) {
1749
+ const permission = HOST_PERMISSIONS[name];
1750
+ host[name] = async (...args) => {
1751
+ used.add(permission);
1752
+ if (!allowed.has(permission)) {
1753
+ throw new PermissionDeniedError(name, `this extension does not ask for ${permission}`);
1754
+ }
1755
+ const answer = answers[name] ?? HOST_FIXTURES[name];
1756
+ return answer(...args);
1757
+ };
1758
+ }
1759
+ return { host, used };
1760
+ }
1112
1761
 
1113
1762
  // src/check.ts
1763
+ import { existsSync as existsSync2 } from "fs";
1764
+ import { rm } from "fs/promises";
1765
+ import { resolve as resolve2, sep } from "path";
1114
1766
  var EXECUTABLE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1115
1767
  var CREDENTIAL_NAME = /(secret|token|password|passphrase|api[-_]?key|credential)/i;
1116
1768
  var INPUT_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "select", "json"]);
@@ -1204,7 +1856,17 @@ function actionShapeFindings(action) {
1204
1856
  }
1205
1857
  return found;
1206
1858
  }
1207
- async function packageFindings(options) {
1859
+ function bundles(options) {
1860
+ return Boolean(options.bundle && options.entry !== void 0 && options.packageDir !== void 0);
1861
+ }
1862
+ function launches(options) {
1863
+ return bundles(options) && options.mock === true;
1864
+ }
1865
+ function packageRootOf(options) {
1866
+ if (options.packageDir === void 0) return void 0;
1867
+ return packageRootFor(packageDirFor(options.packageDir, options.entry));
1868
+ }
1869
+ async function packageFindings(connector, options) {
1208
1870
  if (options.packageDir === void 0) return [];
1209
1871
  const pkg = readNearestPackageJson(packageDirFor(options.packageDir, options.entry));
1210
1872
  const found = [...lifecycleScriptFindings(pkg)];
@@ -1225,7 +1887,15 @@ async function packageFindings(options) {
1225
1887
  contents: packEntryContents(options.entry),
1226
1888
  resolveDir: options.packageDir
1227
1889
  });
1228
- found.push(...bundleDependencyFindings(built.external));
1890
+ found.push(...bundleDependencyFindings(built.external), ...bundledRequireFindings(built.code));
1891
+ if (options.mock) {
1892
+ const dir = await stagePack(connector, built.code, packageRootOf(options));
1893
+ try {
1894
+ found.push(...await packLaunchFindings(dir));
1895
+ } finally {
1896
+ await rm(dir, { recursive: true, force: true });
1897
+ }
1898
+ }
1229
1899
  }
1230
1900
  return found;
1231
1901
  }
@@ -1290,6 +1960,153 @@ async function mockFindings(connector, options) {
1290
1960
  }
1291
1961
  return found;
1292
1962
  }
1963
+ var CHECK_SESSION = {
1964
+ sessionId: "check",
1965
+ worktreePath: process.cwd(),
1966
+ agent: "claude"
1967
+ };
1968
+ var FOOTER_TONES = ["default", "ok", "danger"];
1969
+ var FOOTER_HREF_PROTOCOLS = ["http:", "https:"];
1970
+ function invalidItem(item) {
1971
+ if (!item || typeof item !== "object") return "is not an object";
1972
+ const reading = item;
1973
+ if (typeof reading.label !== "string" || reading.label === "") return "has no label";
1974
+ if (typeof reading.value !== "string") return "has no value";
1975
+ if (reading.tone !== void 0 && !FOOTER_TONES.includes(reading.tone)) {
1976
+ return `has unknown tone ${JSON.stringify(reading.tone)}`;
1977
+ }
1978
+ if (reading.href === void 0) return void 0;
1979
+ if (typeof reading.href !== "string") return "has a link that is not text";
1980
+ let protocol;
1981
+ try {
1982
+ protocol = new URL(reading.href).protocol;
1983
+ } catch {
1984
+ return `has the link ${JSON.stringify(reading.href)}, which is not a URL`;
1985
+ }
1986
+ if (!FOOTER_HREF_PROTOCOLS.includes(protocol)) {
1987
+ return `has the link ${JSON.stringify(reading.href)}; a reading links to ${FOOTER_HREF_PROTOCOLS.join(" or ")} and nothing else`;
1988
+ }
1989
+ return void 0;
1990
+ }
1991
+ async function contributionFindings(connector) {
1992
+ const contributes = connector.contributes;
1993
+ if (!contributes) return [];
1994
+ const found = [];
1995
+ const declared = connector.permissions ?? [];
1996
+ const spent = /* @__PURE__ */ new Set();
1997
+ const ran = async (kind, id, code, body) => {
1998
+ const { host, used } = mockExtensionHost(declared);
1999
+ try {
2000
+ return await body(host);
2001
+ } catch (error) {
2002
+ const reason = error instanceof Error ? error.message : String(error);
2003
+ found.push(
2004
+ finding2(
2005
+ "error",
2006
+ error instanceof PermissionDeniedError ? "permission-undeclared" : code,
2007
+ `${kind} ${id}`,
2008
+ error instanceof PermissionDeniedError ? `asked the host for something this extension does not declare: ${reason}` : `threw: ${reason}`
2009
+ )
2010
+ );
2011
+ return void 0;
2012
+ } finally {
2013
+ for (const permission of used) spent.add(permission);
2014
+ }
2015
+ };
2016
+ for (const footer of contributes.footers ?? []) {
2017
+ const items = await ran(
2018
+ "footer",
2019
+ footer.id,
2020
+ "footer-failed",
2021
+ (host) => Promise.resolve(footer.run({ ...CHECK_SESSION, host, now: () => (/* @__PURE__ */ new Date()).toISOString() }))
2022
+ );
2023
+ if (items === void 0) continue;
2024
+ if (!Array.isArray(items)) {
2025
+ found.push(
2026
+ finding2(
2027
+ "error",
2028
+ "footer-items-invalid",
2029
+ `footer ${footer.id}`,
2030
+ "returned something that is not a list of readings"
2031
+ )
2032
+ );
2033
+ continue;
2034
+ }
2035
+ for (const item of items) {
2036
+ const wrong = invalidItem(item);
2037
+ if (wrong) {
2038
+ found.push(
2039
+ finding2(
2040
+ "error",
2041
+ "footer-items-invalid",
2042
+ `footer ${footer.id}`,
2043
+ `returned a reading that ${wrong}`
2044
+ )
2045
+ );
2046
+ }
2047
+ }
2048
+ }
2049
+ for (const handler of contributes.linkHandlers ?? []) {
2050
+ await ran(
2051
+ "link handler",
2052
+ handler.id,
2053
+ "handler-failed",
2054
+ (host) => Promise.resolve(
2055
+ handler.run({
2056
+ ...CHECK_SESSION,
2057
+ host,
2058
+ now: () => (/* @__PURE__ */ new Date()).toISOString(),
2059
+ url: handler.example
2060
+ })
2061
+ )
2062
+ );
2063
+ }
2064
+ const observable = !(contributes.panes ?? []).some((pane) => pane.web !== void 0);
2065
+ for (const permission of declared) {
2066
+ if (observable && !spent.has(permission)) {
2067
+ found.push(
2068
+ finding2(
2069
+ "warn",
2070
+ "permission-unused",
2071
+ `${connector.id} permissions`,
2072
+ `asks for ${permission} but nothing this run exercised used it; ask only for what it spends`
2073
+ )
2074
+ );
2075
+ }
2076
+ }
2077
+ return found;
2078
+ }
2079
+ function paneFindings(connector, packageRoot) {
2080
+ if (packageRoot === void 0) return [];
2081
+ const root = resolve2(packageRoot);
2082
+ const found = [];
2083
+ for (const pane of connector.contributes?.panes ?? []) {
2084
+ if (pane.web === void 0) continue;
2085
+ const full = resolve2(root, pane.web);
2086
+ if (full !== root && !full.startsWith(`${root}${sep}`)) {
2087
+ found.push(
2088
+ finding2(
2089
+ "error",
2090
+ "web-entry-outside-package",
2091
+ `pane ${pane.id}`,
2092
+ `declares the page "${pane.web}", which resolves outside the package`
2093
+ )
2094
+ );
2095
+ continue;
2096
+ }
2097
+ if (!existsSync2(full)) {
2098
+ found.push(
2099
+ finding2(
2100
+ "error",
2101
+ "web-entry-missing",
2102
+ `pane ${pane.id}`,
2103
+ `declares the page "${pane.web}", which the package does not carry`
2104
+ )
2105
+ );
2106
+ }
2107
+ }
2108
+ return found;
2109
+ }
1293
2110
  var AUTH_FAILURE = /\b(401|403|unauthor|unauthenticat|forbidden|invalid[- ]?(token|credential))/i;
1294
2111
  function liveRunnable(action) {
1295
2112
  if (action.idempotent !== true) return false;
@@ -1418,9 +2235,11 @@ async function checkConnector(connector, options = {}) {
1418
2235
  )
1419
2236
  );
1420
2237
  }
1421
- found.push(...authFindings(connector));
2238
+ if (connector.kind !== "extension") found.push(...authFindings(connector));
1422
2239
  found.push(...secretFindings(connector));
1423
- found.push(...await packageFindings(options));
2240
+ found.push(...paneFindings(connector, packageRootOf(options)));
2241
+ found.push(...await contributionFindings(connector));
2242
+ found.push(...await packageFindings(connector, options));
1424
2243
  found.push(...await mockFindings(connector, options));
1425
2244
  found.push(...await liveFindings(connector, options));
1426
2245
  const perTrigger = await Promise.all(
@@ -1516,15 +2335,30 @@ var CHECK_OWNERS = {
1516
2335
  "mock-network-escape": "mock",
1517
2336
  "mock-not-observed": "mock",
1518
2337
  "preflight-failed": "live",
1519
- "live-action-failed": "live"
2338
+ "live-action-failed": "live",
2339
+ "pack-launch": "launch",
2340
+ "web-entry-missing": "contributes",
2341
+ "web-entry-outside-package": "contributes",
2342
+ "footer-failed": "footers",
2343
+ "footer-items-invalid": "footers",
2344
+ "handler-failed": "handlers",
2345
+ "permission-undeclared": "permissions",
2346
+ "permission-unused": "permissions"
1520
2347
  };
1521
2348
  function checksRun(connector, options) {
1522
- const names = ["manifest", "auth"];
2349
+ const names = ["manifest"];
2350
+ if (connector.kind !== "extension") names.push("auth");
2351
+ const contributes = connector.contributes;
2352
+ if (contributes?.panes?.length && options.packageDir !== void 0) names.push("contributes");
2353
+ if (contributes?.footers?.length) names.push("footers");
2354
+ if (contributes?.linkHandlers?.length) names.push("handlers");
2355
+ if (connector.permissions !== void 0) names.push("permissions");
1523
2356
  if (connector.config.length > 0) names.push("secrets");
1524
2357
  if (connector.actions.length > 0) names.push("actions");
1525
2358
  if (connector.triggers.length > 0) names.push("dedupe");
1526
2359
  if (options.packageDir !== void 0) names.push("no-lifecycle-scripts", "keywords");
1527
- if (options.bundle && options.entry !== void 0) names.push("no-runtime-deps");
2360
+ if (bundles(options)) names.push("no-runtime-deps");
2361
+ if (launches(options)) names.push("launch");
1528
2362
  if (options.mock && connector.actions.length > 0) names.push("mock");
1529
2363
  if (options.live && liveExamines(connector)) names.push("live");
1530
2364
  return names;
@@ -1555,9 +2389,9 @@ function formatFindings(findings) {
1555
2389
  }
1556
2390
 
1557
2391
  // src/pack.ts
1558
- import { mkdtemp, mkdir, rm, stat, writeFile } from "fs/promises";
1559
- import { tmpdir } from "os";
1560
- import { join as join2, resolve as resolve2 } from "path";
2392
+ import { existsSync as existsSync3 } from "fs";
2393
+ import { mkdir, rm as rm2, stat as stat2 } from "fs/promises";
2394
+ import { join as join2, resolve as resolve3 } from "path";
1561
2395
  function finding3(code, target, message) {
1562
2396
  return { level: "error", code, target, message };
1563
2397
  }
@@ -1565,37 +2399,51 @@ function packFileName(connector) {
1565
2399
  return `${connector.id}-${connector.version}.vorn.tgz`;
1566
2400
  }
1567
2401
  async function packConnector(connector, options) {
1568
- const resolveDir = resolve2(options.resolveDir ?? process.cwd());
2402
+ const resolveDir = resolve3(options.resolveDir ?? process.cwd());
1569
2403
  const entryDir = packageDirFor(resolveDir, options.entry);
1570
- const findings = await checkConnector(connector);
2404
+ const packageRoot = packageRootFor(entryDir);
2405
+ const findings = await checkConnector(connector, { packageDir: packageRoot });
1571
2406
  findings.push(...lifecycleScriptFindings(readNearestPackageJson(entryDir)));
1572
2407
  if (findings.some((item) => item.level === "error")) return { findings };
1573
2408
  const contents = packEntryContents(options.entry, options.sdkModule);
1574
2409
  const bundle = options.bundle ?? esbuildBundle;
1575
2410
  const built = await bundle({ contents, resolveDir });
1576
- findings.push(...bundleDependencyFindings(built.external));
2411
+ findings.push(...bundleDependencyFindings(built.external), ...bundledRequireFindings(built.code));
1577
2412
  if (findings.some((item) => item.level === "error")) return { findings };
1578
- const outDir = resolve2(options.outDir ?? process.cwd());
2413
+ const outDir = resolve3(options.outDir ?? process.cwd());
1579
2414
  await mkdir(outDir, { recursive: true });
1580
2415
  const file = join2(outDir, packFileName(connector));
1581
- const staging = await mkdtemp(join2(tmpdir(), "vorn-pack-"));
2416
+ const staging = await stagePack(connector, built.code, packageRoot);
1582
2417
  try {
1583
- await writeFile(join2(staging, "index.js"), built.code, "utf8");
1584
- await writeFile(
1585
- join2(staging, "manifest.json"),
1586
- `${JSON.stringify(connectorManifest(connector), null, 2)}
1587
- `,
1588
- "utf8"
1589
- );
2418
+ findings.push(...await (options.launch ?? packLaunchFindings)(staging));
2419
+ if (findings.some((item) => item.level === "error")) return { findings };
2420
+ const unpacked = await directoryBytes(staging);
2421
+ const maxUnpacked = options.maxUnpackedBytes ?? MAX_UNPACKED_BYTES;
2422
+ if (unpacked > maxUnpacked) {
2423
+ return {
2424
+ findings: [
2425
+ ...findings,
2426
+ finding3(
2427
+ "pack-too-large",
2428
+ "bundle",
2429
+ `The pack unpacks to ${Math.round(unpacked / 1024)} KB; Vorn unpacks at most ${Math.round(maxUnpacked / 1024)} KB`
2430
+ )
2431
+ ]
2432
+ };
2433
+ }
1590
2434
  const { create } = await import("tar");
1591
- await create({ gzip: true, file, cwd: staging }, ["manifest.json", "index.js"]);
2435
+ await create({ gzip: true, file, cwd: staging }, [
2436
+ "manifest.json",
2437
+ "index.js",
2438
+ ...existsSync3(join2(staging, WEB_DIR)) ? [WEB_DIR] : []
2439
+ ]);
1592
2440
  } finally {
1593
- await rm(staging, { recursive: true, force: true });
2441
+ await rm2(staging, { recursive: true, force: true });
1594
2442
  }
1595
- const bytes = (await stat(file)).size;
2443
+ const bytes = (await stat2(file)).size;
1596
2444
  const maxBytes = options.maxBytes ?? MAX_PACK_BYTES;
1597
2445
  if (bytes > maxBytes) {
1598
- await rm(file, { force: true });
2446
+ await rm2(file, { force: true });
1599
2447
  return {
1600
2448
  findings: [
1601
2449
  ...findings,
@@ -1612,7 +2460,7 @@ async function packConnector(connector, options) {
1612
2460
 
1613
2461
  // src/scaffold.ts
1614
2462
  var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
1615
- var SDK_DEPENDENCY_RANGE = "^0.7.0-beta.9";
2463
+ var SDK_DEPENDENCY_RANGE = "^0.7.0-beta.14";
1616
2464
  var SCAFFOLD_VERSION = "0.1.0";
1617
2465
  var VITEST_RANGE = "^4.1.10";
1618
2466
  function jsonFile(value) {
@@ -1622,17 +2470,24 @@ function jsonFile(value) {
1622
2470
  function titleCase(id) {
1623
2471
  return id.split(/[-_]+/).filter((part) => part !== "").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1624
2472
  }
1625
- function packageJson(id, description, inRepo) {
2473
+ function packageJson(id, description, inRepo, kind) {
2474
+ const scoped = kind === "extension" ? "extension" : "connector";
1626
2475
  return jsonFile({
1627
- name: inRepo ? `@vornrun/connector-${id}` : `vorn-connector-${id}`,
2476
+ name: inRepo ? `@vornrun/${scoped}-${id}` : `vorn-${scoped}-${id}`,
1628
2477
  version: SCAFFOLD_VERSION,
1629
2478
  description,
1630
2479
  type: "module",
1631
2480
  license: "MIT",
1632
- bin: { [`vorn-connector-${id}`]: "dist/index.js" },
2481
+ bin: { [`vorn-${scoped}-${id}`]: "dist/index.js" },
1633
2482
  main: "./dist/index.js",
1634
2483
  types: "./dist/index.d.ts",
1635
- files: ["dist", "README.md", ...inRepo ? ["CHANGELOG.md"] : []],
2484
+ // A pane's page ships beside the bundle, so `web` is published like `dist`.
2485
+ files: [
2486
+ "dist",
2487
+ "README.md",
2488
+ ...kind === "extension" ? ["web"] : [],
2489
+ ...inRepo ? ["CHANGELOG.md"] : []
2490
+ ],
1636
2491
  ...inRepo && {
1637
2492
  repository: {
1638
2493
  type: "git",
@@ -1655,15 +2510,16 @@ function packageJson(id, description, inRepo) {
1655
2510
  typescript: "^6.0.3",
1656
2511
  vitest: VITEST_RANGE
1657
2512
  },
1658
- // Read by the catalog build: how this connector is filed, found, and what it asks of you.
2513
+ // Read by the catalog build: how this is filed, found, and what it asks of you.
1659
2514
  vorn: {
1660
- category: "Other",
2515
+ category: kind === "extension" ? "Extensions" : "Other",
1661
2516
  keywords: [id],
1662
- ...inRepo && { auth: "Say in one line what signing in takes." }
2517
+ ...kind === "extension" && { kind: "extension" },
2518
+ ...inRepo && kind === "connector" && { auth: "Say in one line what signing in takes." }
1663
2519
  }
1664
2520
  });
1665
2521
  }
1666
- function tsconfig() {
2522
+ function tsconfig(inRepo) {
1667
2523
  return jsonFile({
1668
2524
  compilerOptions: {
1669
2525
  target: "ES2022",
@@ -1676,10 +2532,11 @@ function tsconfig() {
1676
2532
  skipLibCheck: true,
1677
2533
  types: ["node"],
1678
2534
  noEmit: true,
2535
+ resolveJsonModule: true,
1679
2536
  ignoreDeprecations: "6.0",
1680
2537
  allowImportingTsExtensions: true
1681
2538
  },
1682
- include: ["src/**/*", "vitest.config.ts"]
2539
+ include: ["src/**/*", ...inRepo ? ["vitest.config.ts"] : []]
1683
2540
  });
1684
2541
  }
1685
2542
  function tsupConfig() {
@@ -1712,12 +2569,14 @@ function changelog() {
1712
2569
  }
1713
2570
  function connectorSource(id, name, description) {
1714
2571
  return `import { defineConnector } from '@vornrun/connector-sdk'
2572
+ // Bundled at build time: a pack is one file, so a version read from disk is not there to read.
2573
+ import pkg from '../package.json'
1715
2574
 
1716
2575
  export const connector = defineConnector({
1717
2576
  id: ${JSON.stringify(id)},
1718
2577
  name: ${JSON.stringify(name)},
1719
2578
  description: ${JSON.stringify(description)},
1720
- version: '${SCAFFOLD_VERSION}',
2579
+ version: pkg.version,
1721
2580
  // Prefer a login the machine already has: { rung: 'cli', probe: { command: 'tool', args: ['auth', 'status'] } }
1722
2581
  auth: { rung: 'key', keys: ['apiToken'] },
1723
2582
  config: [
@@ -1778,11 +2637,149 @@ export const connector = defineConnector({
1778
2637
  })
1779
2638
  `;
1780
2639
  }
1781
- function entrySource() {
2640
+ function extensionSource(id, name, description) {
2641
+ return `import { defineExtension } from '@vornrun/connector-sdk'
2642
+ // Bundled at build time: a pack is one file, so a version read from disk is not there to read.
2643
+ import pkg from '../package.json'
2644
+
2645
+ export const connector = defineExtension({
2646
+ id: ${JSON.stringify(id)},
2647
+ name: ${JSON.stringify(name)},
2648
+ description: ${JSON.stringify(description)},
2649
+ version: pkg.version,
2650
+ // Only what this actually spends: the check names one it declared and never used.
2651
+ permissions: ['terminal.read'],
2652
+ // Absent where none of these hold, rather than showing a band with nothing in it.
2653
+ activates: { workspaceContains: ['package.json'] },
2654
+ footers: [
2655
+ {
2656
+ id: 'checks',
2657
+ title: 'Checks',
2658
+ description: 'What the last commands in this session said',
2659
+ every: 30,
2660
+ async run(context) {
2661
+ const output = await context.host.output({ lines: 200 })
2662
+ const failed = /\\b(FAIL|failed|error)\\b/i.test(output)
2663
+ return [
2664
+ {
2665
+ label: 'tests',
2666
+ value: failed ? 'failing' : 'passing',
2667
+ tone: failed ? 'danger' : 'ok'
2668
+ }
2669
+ ]
2670
+ }
2671
+ }
2672
+ ],
2673
+ panes: [
2674
+ {
2675
+ id: 'report',
2676
+ title: 'Report',
2677
+ description: 'The reading, in full, beside the terminal',
2678
+ // Drawn beside the pane's name wherever it is offered; path data only.
2679
+ icon: { viewBox: '0 0 24 24', paths: ['M4 4h16v16H4z M8 9h8 M8 13h8 M8 17h5'] },
2680
+ // Served from the pack; everything the page needs lives under web/.
2681
+ web: 'web/report/index.html'
2682
+ }
2683
+ ]
2684
+ })
2685
+ `;
2686
+ }
2687
+ function extensionPage(name) {
2688
+ return `<!doctype html>
2689
+ <html lang="en">
2690
+ <head>
2691
+ <meta charset="utf-8" />
2692
+ <title>${name}</title>
2693
+ <style>
2694
+ body {
2695
+ margin: 0;
2696
+ padding: 12px;
2697
+ font: 12px ui-sans-serif, system-ui, sans-serif;
2698
+ color: #faf9f7;
2699
+ background: #101012;
2700
+ }
2701
+ h1 {
2702
+ font-size: 13px;
2703
+ font-weight: 500;
2704
+ margin: 0 0 8px;
2705
+ }
2706
+ pre {
2707
+ margin: 0;
2708
+ white-space: pre-wrap;
2709
+ color: rgba(255, 255, 255, 0.55);
2710
+ }
2711
+ </style>
2712
+ </head>
2713
+ <body>
2714
+ <h1>${name}</h1>
2715
+ <pre id="output">Reading the session\u2026</pre>
2716
+ <script type="module">
2717
+ // Same origin, so the page carries no credential: Vorn knows which pane is
2718
+ // asking and grants exactly the permissions the manifest declared.
2719
+ const ask = async (method, body = {}) => {
2720
+ const response = await fetch('bridge/' + method, {
2721
+ method: 'POST',
2722
+ headers: { 'content-type': 'application/json' },
2723
+ body: JSON.stringify(body)
2724
+ })
2725
+ if (!response.ok) throw new Error(method + ' answered ' + response.status)
2726
+ return (await response.json()).result
2727
+ }
2728
+
2729
+ const node = document.getElementById('output')
2730
+ try {
2731
+ node.textContent = await ask('output', { lines: 200 })
2732
+ } catch (error) {
2733
+ node.textContent = String(error)
2734
+ }
2735
+ </script>
2736
+ </body>
2737
+ </html>
2738
+ `;
2739
+ }
2740
+ function extensionTestSource(name) {
2741
+ return `import { describe, expect, it } from 'vitest'
2742
+ import { mockExtensionHost } from '@vornrun/connector-sdk'
2743
+ import { connector } from './extension'
2744
+
2745
+ /** Runs one footer the way the host will, against a stub that enforces the manifest. */
2746
+ async function footer(id: string, output: string) {
2747
+ const { host } = mockExtensionHost(connector.permissions ?? [], { output: async () => output })
2748
+ const declared = connector.contributes?.footers?.find((entry) => entry.id === id)
2749
+ if (!declared) throw new Error('no footer ' + id)
2750
+ return declared.run({
2751
+ sessionId: 'test',
2752
+ worktreePath: process.cwd(),
2753
+ agent: 'claude',
2754
+ host,
2755
+ now: () => '2026-01-01T00:00:00.000Z'
2756
+ })
2757
+ }
2758
+
2759
+ describe(${JSON.stringify(name)}, () => {
2760
+ it('reads the session as passing when nothing failed', async () => {
2761
+ expect(await footer('checks', 'Test Files 1 passed (1)')).toEqual([
2762
+ { label: 'tests', value: 'passing', tone: 'ok' }
2763
+ ])
2764
+ })
2765
+
2766
+ it('reads it as failing when the output says so', async () => {
2767
+ expect(await footer('checks', 'FAIL src/index.test.ts')).toEqual([
2768
+ { label: 'tests', value: 'failing', tone: 'danger' }
2769
+ ])
2770
+ })
2771
+
2772
+ it('asks for nothing it did not declare', () => {
2773
+ expect(connector.permissions).toEqual(['terminal.read'])
2774
+ })
2775
+ })
2776
+ `;
2777
+ }
2778
+ function entrySource(module) {
1782
2779
  return `import { realpathSync } from 'node:fs'
1783
2780
  import { fileURLToPath } from 'node:url'
1784
2781
  import { serveConnector } from '@vornrun/connector-sdk'
1785
- import { connector } from './connector'
2782
+ import { connector } from './${module}'
1786
2783
 
1787
2784
  /** True when this file was run directly rather than imported. */
1788
2785
  export function isEntryPoint(moduleUrl: string, argv = process.argv): boolean {
@@ -1850,8 +2847,8 @@ describe('the packaged connector', () => {
1850
2847
  })
1851
2848
  `;
1852
2849
  }
1853
- function indexSource() {
1854
- return `import { connector } from './connector'
2850
+ function indexSource(module) {
2851
+ return `import { connector } from './${module}'
1855
2852
  import { serveIfEntryPoint } from './entry'
1856
2853
 
1857
2854
  export { connector }
@@ -1956,24 +2953,71 @@ Rename the trigger, the action and the settings to whatever this connector
1956
2953
  really talks to; the shapes here are a starting point, not a rule.
1957
2954
  `;
1958
2955
  }
2956
+ function extensionReadme(id, name, description) {
2957
+ return `# ${name}
2958
+
2959
+ ${description}
2960
+
2961
+ ## Build and check
2962
+
2963
+ \`\`\`sh
2964
+ yarn install
2965
+ yarn build
2966
+ yarn check # verifies the extension against Vorn's contract
2967
+ yarn test
2968
+ yarn pack # writes ${id}-${SCAFFOLD_VERSION}.vorn.tgz, installable in Vorn
2969
+ \`\`\`
2970
+
2971
+ ## What it contributes
2972
+
2973
+ | Kind | Name | What it does |
2974
+ | --- | --- | --- |
2975
+ | Footer | Checks | A band under the card's status bar, recomputed every 30s |
2976
+ | Pane | Report | A page beside the terminal, served from \`web/report\` |
2977
+
2978
+ ## What it asks for
2979
+
2980
+ | Permission | What it grants |
2981
+ | --- | --- |
2982
+ | \`terminal.read\` | The session's recent terminal output |
2983
+
2984
+ Ask for only what the extension spends: \`check\` names a permission that was
2985
+ declared and never used.
2986
+
2987
+ ## Where it shows
2988
+
2989
+ Sessions whose worktree has a \`package.json\`. Widen or narrow that in
2990
+ \`activates\`, and narrow one contribution further with its own \`when\`.
2991
+ `;
2992
+ }
1959
2993
  function scaffoldFiles(options) {
2994
+ const kind = options.kind ?? "connector";
1960
2995
  if (!ID_PATTERN.test(options.id ?? "")) {
1961
- throw new Error(`Connector id "${options.id}" must start with a letter and be url-safe`);
2996
+ throw new Error(
2997
+ `${kind === "extension" ? "Extension" : "Connector"} id "${options.id}" must start with a letter and be url-safe`
2998
+ );
1962
2999
  }
1963
3000
  const name = options.name?.trim() || titleCase(options.id);
1964
- const description = options.description?.trim() || `${name} connector for Vorn`;
3001
+ const description = options.description?.trim() || `${name} ${kind} for Vorn`;
1965
3002
  const inRepo = options.repoConventions ?? false;
3003
+ const module = kind === "extension" ? "extension" : "connector";
1966
3004
  return [
1967
- { path: "package.json", contents: packageJson(options.id, description, inRepo) },
1968
- { path: "src/connector.ts", contents: connectorSource(options.id, name, description) },
1969
- { path: "src/entry.ts", contents: entrySource() },
1970
- { path: "src/index.ts", contents: indexSource() },
1971
- { path: "src/connector.test.ts", contents: testSource(name) },
3005
+ { path: "package.json", contents: packageJson(options.id, description, inRepo, kind) },
3006
+ kind === "extension" ? { path: "src/extension.ts", contents: extensionSource(options.id, name, description) } : { path: "src/connector.ts", contents: connectorSource(options.id, name, description) },
3007
+ { path: "src/entry.ts", contents: entrySource(module) },
3008
+ { path: "src/index.ts", contents: indexSource(module) },
3009
+ kind === "extension" ? { path: "src/extension.test.ts", contents: extensionTestSource(name) } : { path: "src/connector.test.ts", contents: testSource(name) },
1972
3010
  { path: "src/entry.test.ts", contents: entryTestSource() },
1973
- { path: "README.md", contents: readme(options.id, name, description) },
3011
+ // The page a pane is drawn from, carried into the pack as it stands here.
3012
+ ...kind === "extension" ? [{ path: "web/report/index.html", contents: extensionPage(name) }] : [],
3013
+ {
3014
+ path: "README.md",
3015
+ contents: kind === "extension" ? extensionReadme(options.id, name, description) : readme(options.id, name, description)
3016
+ },
3017
+ // Everywhere: the generated source imports its package.json, which needs resolveJsonModule to compile.
3018
+ { path: "tsconfig.json", contents: tsconfig(inRepo) },
1974
3019
  ...inRepo ? [
1975
3020
  { path: "CHANGELOG.md", contents: changelog() },
1976
- { path: "tsconfig.json", contents: tsconfig() },
1977
3021
  { path: "tsup.config.ts", contents: tsupConfig() },
1978
3022
  { path: "vitest.config.ts", contents: vitestConfig() }
1979
3023
  ] : []
@@ -2136,6 +3180,67 @@ function createConnectorServer(connector, options = {}) {
2136
3180
  }
2137
3181
  );
2138
3182
  }
3183
+ const sessionShape = {
3184
+ sessionId: z.string().describe("The session this is being computed for"),
3185
+ worktreePath: z.string().describe("Where the session's work is"),
3186
+ agent: z.enum(EXTENSION_AGENTS).describe("Which agent runs in the session")
3187
+ };
3188
+ const sessionContext = (args, host) => ({
3189
+ sessionId: args.sessionId,
3190
+ worktreePath: args.worktreePath,
3191
+ agent: args.agent,
3192
+ host,
3193
+ now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString())
3194
+ });
3195
+ const hostFor = (sessionId) => options.host?.(sessionId) ?? createExtensionHost({ sessionId });
3196
+ for (const footer of connector.contributes?.footers ?? []) {
3197
+ server.registerTool(
3198
+ footerToolName(footer.id),
3199
+ {
3200
+ title: footer.title,
3201
+ description: footer.description ?? `Recompute ${footer.title} for one session`,
3202
+ inputSchema: sessionShape,
3203
+ outputSchema: z.looseObject({
3204
+ items: z.array(z.looseObject({ label: z.string(), value: z.string() })).describe("The readings to show in the band")
3205
+ })
3206
+ },
3207
+ async (args) => {
3208
+ try {
3209
+ const items = await footer.run(sessionContext(args, hostFor(args.sessionId)));
3210
+ return json({ items });
3211
+ } catch (error) {
3212
+ return failure(error);
3213
+ }
3214
+ }
3215
+ );
3216
+ }
3217
+ for (const handler of connector.contributes?.linkHandlers ?? []) {
3218
+ server.registerTool(
3219
+ handlerToolName(handler.id),
3220
+ {
3221
+ title: handler.title,
3222
+ description: handler.description ?? `Open ${handler.title} for a clicked link`,
3223
+ inputSchema: {
3224
+ ...sessionShape,
3225
+ url: z.string().describe("The clicked text, which matched this handler")
3226
+ },
3227
+ outputSchema: z.looseObject({
3228
+ openPane: z.string().optional().describe("Id of one of this extension's panes to open")
3229
+ })
3230
+ },
3231
+ async (args) => {
3232
+ try {
3233
+ const handled = await handler.run({
3234
+ ...sessionContext(args, hostFor(args.sessionId)),
3235
+ url: args.url
3236
+ });
3237
+ return json({ ...handled ?? {} });
3238
+ } catch (error) {
3239
+ return failure(error);
3240
+ }
3241
+ }
3242
+ );
3243
+ }
2139
3244
  for (const action of connector.actions) {
2140
3245
  const base = action.description ?? `${action.label} in ${connector.name}`;
2141
3246
  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.";
@@ -2170,11 +3275,30 @@ async function serveConnector(connector, options = {}) {
2170
3275
  }
2171
3276
 
2172
3277
  export {
3278
+ EXTENSION_PERMISSIONS,
3279
+ HOST_PERMISSIONS,
3280
+ envNameFor,
3281
+ defineConnector,
3282
+ defineExtension,
3283
+ resolveConfig,
3284
+ pollToolName,
3285
+ footerToolName,
3286
+ handlerToolName,
3287
+ MANIFEST_TOOL,
3288
+ PREFLIGHT_TOOL,
3289
+ OPTIONS_TOOL,
3290
+ connectionSetup,
3291
+ connectorManifest,
2173
3292
  MAX_PACK_BYTES,
2174
3293
  lifecycleScriptFindings,
2175
3294
  bundleDependencyFindings,
3295
+ bundledRequireFindings,
2176
3296
  readNearestPackageJson,
2177
3297
  esbuildBundle,
3298
+ HOST_URL_ENV,
3299
+ HOST_TOKEN_ENV,
3300
+ PermissionDeniedError,
3301
+ createExtensionHost,
2178
3302
  normalizeItem,
2179
3303
  normalizeItems,
2180
3304
  pollWithDedupe,
@@ -2194,19 +3318,11 @@ export {
2194
3318
  drainPoll,
2195
3319
  runOptions,
2196
3320
  runAction,
2197
- envNameFor,
2198
- defineConnector,
2199
- resolveConfig,
2200
- pollToolName,
2201
- MANIFEST_TOOL,
2202
- PREFLIGHT_TOOL,
2203
- OPTIONS_TOOL,
2204
- connectionSetup,
2205
- connectorManifest,
2206
3321
  MockRouteMissError,
2207
3322
  escapedMockHttp,
2208
3323
  withMockHttp,
2209
3324
  createConnectorHarness,
3325
+ mockExtensionHost,
2210
3326
  checkConnector,
2211
3327
  CHECK_OWNERS,
2212
3328
  runConformance,