@vornrun/connector-sdk 0.7.0-beta.14 → 0.7.0-beta.16

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.
@@ -4,6 +4,75 @@ var PATH_DATA_PATTERN = /^[MmZzLlHhVvCcSsQqTtAa0-9\s,.\-+eE]+$/;
4
4
  var VIEW_BOX_PATTERN = /^-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+$/;
5
5
  var DEDUPE_STRATEGIES = ["timestamp", "lastItem"];
6
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
+ ];
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;
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;
73
+ }
74
+ return false;
75
+ }
7
76
  var ABSOLUTE_URL_PATTERN = /^https?:\/\//i;
8
77
  var CONFIG_ROOTED_URL_PATTERN = /^\{\{\s*config\./;
9
78
  function assertUnique(kind, keys) {
@@ -46,33 +115,38 @@ function assertAuth(definition) {
46
115
  }
47
116
  }
48
117
  }
49
- function envNameFor(key, explicit) {
50
- if (explicit) return explicit;
51
- return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
52
- }
53
- function defineConnector(definition) {
118
+ function assertIdentity(kind, definition) {
54
119
  if (!KEY_PATTERN.test(definition.id ?? "")) {
55
- throw new Error(`Connector id "${definition.id}" must start with a letter and be url-safe`);
120
+ throw new Error(`${kind} id "${definition.id}" must start with a letter and be url-safe`);
56
121
  }
57
122
  if (!definition.name?.trim()) {
58
- throw new Error(`Connector ${definition.id} is missing a name`);
123
+ throw new Error(`${kind} ${definition.id} is missing a name`);
59
124
  }
60
- if (definition.icon) {
61
- const { viewBox, paths } = definition.icon;
62
- if (!Array.isArray(paths) || paths.length === 0) {
63
- throw new Error(`Connector ${definition.id} has an icon with no paths`);
64
- }
65
- for (const path of paths) {
66
- if (typeof path !== "string" || !PATH_DATA_PATTERN.test(path)) {
67
- throw new Error(
68
- `Connector ${definition.id} has an icon path that is not SVG path data. Only path data is accepted, not markup.`
69
- );
70
- }
71
- }
72
- if (viewBox !== void 0 && !VIEW_BOX_PATTERN.test(viewBox)) {
73
- throw new Error(`Connector ${definition.id} has an icon viewBox that is not four numbers`);
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
+ );
74
138
  }
75
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`);
142
+ }
143
+ }
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();
147
+ }
148
+ function defineConnector(definition) {
149
+ assertIdentity("Connector", definition);
76
150
  const triggers = definition.triggers ?? [];
77
151
  const actions = definition.actions ?? [];
78
152
  if (triggers.length === 0 && actions.length === 0) {
@@ -156,12 +230,191 @@ function defineConnector(definition) {
156
230
  assertAuth(definition);
157
231
  return {
158
232
  ...definition,
233
+ kind: "connector",
159
234
  version: definition.version ?? "0.0.0",
160
235
  config: definition.config ?? [],
161
236
  triggers,
162
237
  actions
163
238
  };
164
239
  }
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
+ }
258
+ }
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
+ }
265
+ }
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
+ }
272
+ }
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
+ );
278
+ }
279
+ }
280
+ }
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`);
288
+ }
289
+ if (!page2 && !program) {
290
+ throw new Error(`Extension ${id} pane ${pane.id} declares neither a web page nor a command`);
291
+ }
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;
300
+ }
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`);
304
+ }
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`);
308
+ }
309
+ }
310
+ }
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
+ );
329
+ }
330
+ }
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
+ }
165
418
  function resolveConfig(connector, env = process.env) {
166
419
  const config = {};
167
420
  const missing = [];
@@ -186,6 +439,12 @@ function resolveConfig(connector, env = process.env) {
186
439
  function pollToolName(triggerType) {
187
440
  return `poll_${triggerType}`;
188
441
  }
442
+ function footerToolName(footerId) {
443
+ return `vorn_footer_${footerId}`;
444
+ }
445
+ function handlerToolName(handlerId) {
446
+ return `vorn_handler_${handlerId}`;
447
+ }
189
448
  var MANIFEST_TOOL = "vorn_connector_manifest";
190
449
  var PREFLIGHT_TOOL = "vorn_connector_preflight";
191
450
  var OPTIONS_TOOL = "vorn_connector_options";
@@ -216,14 +475,49 @@ function connectionSetup(connector, triggerType) {
216
475
  }))
217
476
  };
218
477
  }
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 }
486
+ });
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
+ };
507
+ }
219
508
  function connectorManifest(connector) {
509
+ const contributes = manifestContributions(connector);
220
510
  return {
221
511
  id: connector.id,
222
512
  name: connector.name,
223
513
  version: connector.version,
514
+ kind: connector.kind,
224
515
  ...connector.description !== void 0 && { description: connector.description },
225
516
  ...connector.icon !== void 0 && { icon: connector.icon },
226
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 },
227
521
  triggers: connector.triggers.map((trigger) => ({
228
522
  type: trigger.type,
229
523
  label: trigger.label,
@@ -256,11 +550,12 @@ function connectorManifest(connector) {
256
550
 
257
551
  // src/packaging.ts
258
552
  import { builtinModules } from "module";
259
- import { readFileSync } from "fs";
260
- import { mkdtemp, writeFile } from "fs/promises";
553
+ import { existsSync, readFileSync } from "fs";
554
+ import { cp, mkdtemp, readdir, stat, writeFile } from "fs/promises";
261
555
  import { tmpdir } from "os";
262
556
  import { dirname, isAbsolute, join, resolve } from "path";
263
557
  var MAX_PACK_BYTES = 8 * 1024 * 1024;
558
+ var MAX_UNPACKED_BYTES = 32 * 1024 * 1024;
264
559
  var LIFECYCLE_SCRIPTS = [
265
560
  "preinstall",
266
561
  "install",
@@ -434,6 +729,15 @@ function readNearestPackageJson(fromDir) {
434
729
  }
435
730
  }
436
731
  }
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;
739
+ }
740
+ }
437
741
  function packEntryContents(entry, sdkModule = "@vornrun/connector-sdk") {
438
742
  return [
439
743
  `import { serveConnector } from ${JSON.stringify(sdkModule)}`,
@@ -444,7 +748,20 @@ function packEntryContents(entry, sdkModule = "@vornrun/connector-sdk") {
444
748
  ""
445
749
  ].join("\n");
446
750
  }
447
- async function stagePack(connector, code) {
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) {
448
765
  const dir = await mkdtemp(join(tmpdir(), "vorn-pack-"));
449
766
  await writeFile(join(dir, "index.js"), code, "utf8");
450
767
  await writeFile(
@@ -453,6 +770,12 @@ async function stagePack(connector, code) {
453
770
  `,
454
771
  "utf8"
455
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
+ }
456
779
  return dir;
457
780
  }
458
781
  var LAUNCH_TIMEOUT_MS = 15e3;
@@ -570,6 +893,81 @@ async function esbuildBundle(request) {
570
893
  };
571
894
  }
572
895
 
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";
903
+ }
904
+ };
905
+ var HostReplyError = class extends Error {
906
+ constructor(method, detail) {
907
+ super(`The host answered ${method} with ${detail}`);
908
+ this.name = "HostReplyError";
909
+ }
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;
959
+ }
960
+ return {
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", {})
968
+ };
969
+ }
970
+
573
971
  // src/normalize.ts
574
972
  var RESERVED_KEYS = [
575
973
  "externalId",
@@ -1024,8 +1422,8 @@ var DEFAULT_BASE_DELAY_MS = 250;
1024
1422
  var DEFAULT_MAX_DELAY_MS = 3e4;
1025
1423
  var MAX_ATTEMPTS = 10;
1026
1424
  var MAX_TOTAL_WAIT_MS = 12e4;
1027
- var wait = (ms) => new Promise((resolve3) => {
1028
- setTimeout(resolve3, ms);
1425
+ var wait = (ms) => new Promise((resolve4) => {
1426
+ setTimeout(resolve4, ms);
1029
1427
  });
1030
1428
  function retryAfterMs(header, now) {
1031
1429
  if (!header) return void 0;
@@ -1327,9 +1725,44 @@ function createConnectorHarness(connector, harnessOptions = {}) {
1327
1725
  withMockHttp
1328
1726
  };
1329
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
+ }
1330
1761
 
1331
1762
  // src/check.ts
1763
+ import { existsSync as existsSync2 } from "fs";
1332
1764
  import { rm } from "fs/promises";
1765
+ import { resolve as resolve2, sep } from "path";
1333
1766
  var EXECUTABLE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1334
1767
  var CREDENTIAL_NAME = /(secret|token|password|passphrase|api[-_]?key|credential)/i;
1335
1768
  var INPUT_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "select", "json"]);
@@ -1429,6 +1862,10 @@ function bundles(options) {
1429
1862
  function launches(options) {
1430
1863
  return bundles(options) && options.mock === true;
1431
1864
  }
1865
+ function packageRootOf(options) {
1866
+ if (options.packageDir === void 0) return void 0;
1867
+ return packageRootFor(packageDirFor(options.packageDir, options.entry));
1868
+ }
1432
1869
  async function packageFindings(connector, options) {
1433
1870
  if (options.packageDir === void 0) return [];
1434
1871
  const pkg = readNearestPackageJson(packageDirFor(options.packageDir, options.entry));
@@ -1452,7 +1889,7 @@ async function packageFindings(connector, options) {
1452
1889
  });
1453
1890
  found.push(...bundleDependencyFindings(built.external), ...bundledRequireFindings(built.code));
1454
1891
  if (options.mock) {
1455
- const dir = await stagePack(connector, built.code);
1892
+ const dir = await stagePack(connector, built.code, packageRootOf(options));
1456
1893
  try {
1457
1894
  found.push(...await packLaunchFindings(dir));
1458
1895
  } finally {
@@ -1523,6 +1960,153 @@ async function mockFindings(connector, options) {
1523
1960
  }
1524
1961
  return found;
1525
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
+ }
1526
2110
  var AUTH_FAILURE = /\b(401|403|unauthor|unauthenticat|forbidden|invalid[- ]?(token|credential))/i;
1527
2111
  function liveRunnable(action) {
1528
2112
  if (action.idempotent !== true) return false;
@@ -1651,8 +2235,10 @@ async function checkConnector(connector, options = {}) {
1651
2235
  )
1652
2236
  );
1653
2237
  }
1654
- found.push(...authFindings(connector));
2238
+ if (connector.kind !== "extension") found.push(...authFindings(connector));
1655
2239
  found.push(...secretFindings(connector));
2240
+ found.push(...paneFindings(connector, packageRootOf(options)));
2241
+ found.push(...await contributionFindings(connector));
1656
2242
  found.push(...await packageFindings(connector, options));
1657
2243
  found.push(...await mockFindings(connector, options));
1658
2244
  found.push(...await liveFindings(connector, options));
@@ -1750,10 +2336,23 @@ var CHECK_OWNERS = {
1750
2336
  "mock-not-observed": "mock",
1751
2337
  "preflight-failed": "live",
1752
2338
  "live-action-failed": "live",
1753
- "pack-launch": "launch"
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"
1754
2347
  };
1755
2348
  function checksRun(connector, options) {
1756
- 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");
1757
2356
  if (connector.config.length > 0) names.push("secrets");
1758
2357
  if (connector.actions.length > 0) names.push("actions");
1759
2358
  if (connector.triggers.length > 0) names.push("dedupe");
@@ -1790,8 +2389,9 @@ function formatFindings(findings) {
1790
2389
  }
1791
2390
 
1792
2391
  // src/pack.ts
1793
- import { mkdir, rm as rm2, stat } from "fs/promises";
1794
- 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";
1795
2395
  function finding3(code, target, message) {
1796
2396
  return { level: "error", code, target, message };
1797
2397
  }
@@ -1799,9 +2399,10 @@ function packFileName(connector) {
1799
2399
  return `${connector.id}-${connector.version}.vorn.tgz`;
1800
2400
  }
1801
2401
  async function packConnector(connector, options) {
1802
- const resolveDir = resolve2(options.resolveDir ?? process.cwd());
2402
+ const resolveDir = resolve3(options.resolveDir ?? process.cwd());
1803
2403
  const entryDir = packageDirFor(resolveDir, options.entry);
1804
- const findings = await checkConnector(connector);
2404
+ const packageRoot = packageRootFor(entryDir);
2405
+ const findings = await checkConnector(connector, { packageDir: packageRoot });
1805
2406
  findings.push(...lifecycleScriptFindings(readNearestPackageJson(entryDir)));
1806
2407
  if (findings.some((item) => item.level === "error")) return { findings };
1807
2408
  const contents = packEntryContents(options.entry, options.sdkModule);
@@ -1809,19 +2410,37 @@ async function packConnector(connector, options) {
1809
2410
  const built = await bundle({ contents, resolveDir });
1810
2411
  findings.push(...bundleDependencyFindings(built.external), ...bundledRequireFindings(built.code));
1811
2412
  if (findings.some((item) => item.level === "error")) return { findings };
1812
- const outDir = resolve2(options.outDir ?? process.cwd());
2413
+ const outDir = resolve3(options.outDir ?? process.cwd());
1813
2414
  await mkdir(outDir, { recursive: true });
1814
2415
  const file = join2(outDir, packFileName(connector));
1815
- const staging = await stagePack(connector, built.code);
2416
+ const staging = await stagePack(connector, built.code, packageRoot);
1816
2417
  try {
1817
2418
  findings.push(...await (options.launch ?? packLaunchFindings)(staging));
1818
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
+ }
1819
2434
  const { create } = await import("tar");
1820
- 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
+ ]);
1821
2440
  } finally {
1822
2441
  await rm2(staging, { recursive: true, force: true });
1823
2442
  }
1824
- const bytes = (await stat(file)).size;
2443
+ const bytes = (await stat2(file)).size;
1825
2444
  const maxBytes = options.maxBytes ?? MAX_PACK_BYTES;
1826
2445
  if (bytes > maxBytes) {
1827
2446
  await rm2(file, { force: true });
@@ -1841,7 +2460,7 @@ async function packConnector(connector, options) {
1841
2460
 
1842
2461
  // src/scaffold.ts
1843
2462
  var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
1844
- var SDK_DEPENDENCY_RANGE = "^0.7.0-beta.9";
2463
+ var SDK_DEPENDENCY_RANGE = "^0.7.0-beta.14";
1845
2464
  var SCAFFOLD_VERSION = "0.1.0";
1846
2465
  var VITEST_RANGE = "^4.1.10";
1847
2466
  function jsonFile(value) {
@@ -1851,17 +2470,24 @@ function jsonFile(value) {
1851
2470
  function titleCase(id) {
1852
2471
  return id.split(/[-_]+/).filter((part) => part !== "").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
1853
2472
  }
1854
- function packageJson(id, description, inRepo) {
2473
+ function packageJson(id, description, inRepo, kind) {
2474
+ const scoped = kind === "extension" ? "extension" : "connector";
1855
2475
  return jsonFile({
1856
- name: inRepo ? `@vornrun/connector-${id}` : `vorn-connector-${id}`,
2476
+ name: inRepo ? `@vornrun/${scoped}-${id}` : `vorn-${scoped}-${id}`,
1857
2477
  version: SCAFFOLD_VERSION,
1858
2478
  description,
1859
2479
  type: "module",
1860
2480
  license: "MIT",
1861
- bin: { [`vorn-connector-${id}`]: "dist/index.js" },
2481
+ bin: { [`vorn-${scoped}-${id}`]: "dist/index.js" },
1862
2482
  main: "./dist/index.js",
1863
2483
  types: "./dist/index.d.ts",
1864
- 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
+ ],
1865
2491
  ...inRepo && {
1866
2492
  repository: {
1867
2493
  type: "git",
@@ -1884,11 +2510,12 @@ function packageJson(id, description, inRepo) {
1884
2510
  typescript: "^6.0.3",
1885
2511
  vitest: VITEST_RANGE
1886
2512
  },
1887
- // 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.
1888
2514
  vorn: {
1889
- category: "Other",
2515
+ category: kind === "extension" ? "Extensions" : "Other",
1890
2516
  keywords: [id],
1891
- ...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." }
1892
2519
  }
1893
2520
  });
1894
2521
  }
@@ -2010,11 +2637,149 @@ export const connector = defineConnector({
2010
2637
  })
2011
2638
  `;
2012
2639
  }
2013
- 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) {
2014
2779
  return `import { realpathSync } from 'node:fs'
2015
2780
  import { fileURLToPath } from 'node:url'
2016
2781
  import { serveConnector } from '@vornrun/connector-sdk'
2017
- import { connector } from './connector'
2782
+ import { connector } from './${module}'
2018
2783
 
2019
2784
  /** True when this file was run directly rather than imported. */
2020
2785
  export function isEntryPoint(moduleUrl: string, argv = process.argv): boolean {
@@ -2082,8 +2847,8 @@ describe('the packaged connector', () => {
2082
2847
  })
2083
2848
  `;
2084
2849
  }
2085
- function indexSource() {
2086
- return `import { connector } from './connector'
2850
+ function indexSource(module) {
2851
+ return `import { connector } from './${module}'
2087
2852
  import { serveIfEntryPoint } from './entry'
2088
2853
 
2089
2854
  export { connector }
@@ -2188,21 +2953,67 @@ Rename the trigger, the action and the settings to whatever this connector
2188
2953
  really talks to; the shapes here are a starting point, not a rule.
2189
2954
  `;
2190
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
+ }
2191
2993
  function scaffoldFiles(options) {
2994
+ const kind = options.kind ?? "connector";
2192
2995
  if (!ID_PATTERN.test(options.id ?? "")) {
2193
- 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
+ );
2194
2999
  }
2195
3000
  const name = options.name?.trim() || titleCase(options.id);
2196
- const description = options.description?.trim() || `${name} connector for Vorn`;
3001
+ const description = options.description?.trim() || `${name} ${kind} for Vorn`;
2197
3002
  const inRepo = options.repoConventions ?? false;
3003
+ const module = kind === "extension" ? "extension" : "connector";
2198
3004
  return [
2199
- { path: "package.json", contents: packageJson(options.id, description, inRepo) },
2200
- { path: "src/connector.ts", contents: connectorSource(options.id, name, description) },
2201
- { path: "src/entry.ts", contents: entrySource() },
2202
- { path: "src/index.ts", contents: indexSource() },
2203
- { 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) },
2204
3010
  { path: "src/entry.test.ts", contents: entryTestSource() },
2205
- { 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
+ },
2206
3017
  // Everywhere: the generated source imports its package.json, which needs resolveJsonModule to compile.
2207
3018
  { path: "tsconfig.json", contents: tsconfig(inRepo) },
2208
3019
  ...inRepo ? [
@@ -2369,6 +3180,67 @@ function createConnectorServer(connector, options = {}) {
2369
3180
  }
2370
3181
  );
2371
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
+ }
2372
3244
  for (const action of connector.actions) {
2373
3245
  const base = action.description ?? `${action.label} in ${connector.name}`;
2374
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.";
@@ -2403,10 +3275,15 @@ async function serveConnector(connector, options = {}) {
2403
3275
  }
2404
3276
 
2405
3277
  export {
3278
+ EXTENSION_PERMISSIONS,
3279
+ HOST_PERMISSIONS,
2406
3280
  envNameFor,
2407
3281
  defineConnector,
3282
+ defineExtension,
2408
3283
  resolveConfig,
2409
3284
  pollToolName,
3285
+ footerToolName,
3286
+ handlerToolName,
2410
3287
  MANIFEST_TOOL,
2411
3288
  PREFLIGHT_TOOL,
2412
3289
  OPTIONS_TOOL,
@@ -2418,6 +3295,10 @@ export {
2418
3295
  bundledRequireFindings,
2419
3296
  readNearestPackageJson,
2420
3297
  esbuildBundle,
3298
+ HOST_URL_ENV,
3299
+ HOST_TOKEN_ENV,
3300
+ PermissionDeniedError,
3301
+ createExtensionHost,
2421
3302
  normalizeItem,
2422
3303
  normalizeItems,
2423
3304
  pollWithDedupe,
@@ -2441,6 +3322,7 @@ export {
2441
3322
  escapedMockHttp,
2442
3323
  withMockHttp,
2443
3324
  createConnectorHarness,
3325
+ mockExtensionHost,
2444
3326
  checkConnector,
2445
3327
  CHECK_OWNERS,
2446
3328
  runConformance,