@vornrun/connector-sdk 0.7.0-beta.8 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,6 +84,73 @@ await serveConnector(connector)
84
84
 
85
85
  Publish it like any other package (`"bin": { "acme-connector": "dist/bin.js" }`).
86
86
 
87
+ `vorn-connector new acme` writes all of the above — package, entry, definition,
88
+ a test that needs no network — already building, checking and packing.
89
+
90
+ ## Declare an action instead of writing one
91
+
92
+ Most actions put arguments into a request and keep part of the answer. Say that
93
+ and the SDK does the rest: `{{args.x}}` and `{{config.y}}` are filled in, an
94
+ argument nobody supplied is left out, a failed status is raised with what the
95
+ body said, and `postReceive` reshapes what came back.
96
+
97
+ ```ts
98
+ {
99
+ type: 'createIssue',
100
+ label: 'Create issue',
101
+ idempotent: false,
102
+ inputs: [{ key: 'title', label: 'Title', required: true }],
103
+ request: {
104
+ method: 'POST',
105
+ url: '{{config.baseUrl}}/issues',
106
+ headers: { authorization: 'Bearer {{config.apiToken}}' },
107
+ body: { title: '{{args.title}}' }
108
+ },
109
+ // pick · rename · flatten · filter · map, applied left to right.
110
+ postReceive: [{ op: 'pick', keys: ['id', 'html_url'] }]
111
+ }
112
+ ```
113
+
114
+ Add `paginate` to follow every page rather than the first — `{ kind: 'cursor',
115
+ cursorPath: 'next', param: 'cursor' }`, `{ kind: 'page', param: 'page' }` or
116
+ `{ kind: 'link' }` — and the pages arrive concatenated.
117
+
118
+ Retry, backoff and rate-limit handling are applied for you, to declared
119
+ requests and to `context.fetch` alike. A read is always retried; a write only
120
+ when the action declares `idempotent: true`, because repeating a create makes a
121
+ second one. Prefer `context.fetch` over the global one in a hand-written action.
122
+
123
+ ## Offer a field the choices it has
124
+
125
+ A `select` with fixed choices carries them; one whose choices only exist against
126
+ a live connection names a set the connector serves.
127
+
128
+ Choices are **suggestions, not a closed set**. Vorn draws a picker while the
129
+ value is one of them and its template-aware input whenever it is not, because a
130
+ step is entitled to compute the value from an earlier one — so the served tool
131
+ schema keeps the argument a plain string and lists the choices in its
132
+ description. Your action still receives whatever was finally sent, and it is
133
+ the connector's job to refuse a value it cannot use.
134
+
135
+ ```ts
136
+ options: { channels: async ({ config, fetch }) => ['general', 'random'] },
137
+ actions: [{
138
+ type: 'post',
139
+ label: 'Post',
140
+ inputs: [{ key: 'channel', label: 'Channel', type: 'select', loadOptions: 'channels' }],
141
+ request: { method: 'POST', url: '{{config.baseUrl}}/post/{{args.channel}}' }
142
+ }]
143
+ ```
144
+
145
+ `loadOptions` is served today but not yet consumed: the SDK registers a
146
+ `vorn_connector_options` tool and answers it, and the manifest carries the set's
147
+ name, but the app does not fetch the list yet — such a field is edited as text
148
+ until it does. Declaring it now is what makes it work then; a field whose
149
+ choices are already known should use `options` instead.
150
+
151
+ A `json` argument arrives parsed. `builderHint` on a field is a note for whoever
152
+ writes the next connector, not for whoever runs this one.
153
+
87
154
  ## Poll a database instead of an API
88
155
 
89
156
  Nothing about a trigger is HTTP-specific — it just returns items. A SQL pull
@@ -135,6 +202,25 @@ Two rules make a pull trigger reliable, and the SDK enforces both:
135
202
  it. Use `>=` when filtering on `since` and sort ascending; returning a few
136
203
  items again is free, because the SDK drops anything already delivered.
137
204
 
205
+ ## Act through a signed-in window
206
+
207
+ A service with no API to key can still be reached as the person using it. Declare the `browser` rung with the page to sign in on, the origins the connector may act on, and a check that answers 2xx only while someone is signed in:
208
+
209
+ ```ts
210
+ auth: {
211
+ rung: 'browser',
212
+ browser: {
213
+ signInUrl: 'https://example.com/login',
214
+ origins: ['https://example.com', 'https://*.example.com'],
215
+ check: { url: 'https://example.com/api/me', identity: ['name', 'handle'] }
216
+ }
217
+ }
218
+ ```
219
+
220
+ Vorn opens a window on a browser profile that belongs to one connection, and the person signs in there. The connector's code then gets `ctx.session.fetch` beside `ctx.fetch`. A call through `ctx.session.fetch` runs inside that signed-in window as a same-origin request, so the service sees its own page asking and no cookie reaches the connector. Calls outside `origins` are refused. `ctx.fetch` stays a plain fetch for public reads, such as a feed, which work before anyone signs in. A `browser` connector's declared `request` actions go through the window.
221
+
222
+ `--mock` serves signed-in calls from the same routes as every other call. A `--live` run from a terminal has no window, so it skips them.
223
+
138
224
  ## Dedupe strategies
139
225
 
140
226
  `dedupe` tells the SDK how to recognize new items, and it then owns the cursor
@@ -198,6 +284,48 @@ trigger and they are replayed through the real dedupe pipeline, so a connector
198
284
  can be checked before anyone has credentials for it; pass `--live` to poll the
199
285
  real source instead.
200
286
 
287
+ `--mock` is the full gate, and the one to run in CI. It answers every HTTP
288
+ request from routes instead of the network, runs each action on its own
289
+ declared arguments, and asks the questions `pack` asks about the package —
290
+ install-time scripts, and anything that would still need a registry at launch:
291
+
292
+ ```console
293
+ $ npx vorn-connector check ./dist/index.js --mock --receipt verified.json
294
+ Verified manifest, auth, secrets, actions, dedupe, no-lifecycle-scripts, keywords, no-runtime-deps, mock — wrote verified.json
295
+ ```
296
+
297
+ `--receipt <file>` writes what was verified, for a catalog to carry:
298
+
299
+ ```json
300
+ { "schema": 1, "version": "1.2.0", "checkedAt": "…", "checks": ["manifest", "…"] }
301
+ ```
302
+
303
+ A check that could not run is left out rather than listed as passed, and a
304
+ connector with any error gets no receipt at all.
305
+
306
+ `--live` additionally asks `preflight` whether the connector can sign in, then
307
+ runs each action that declared `idempotent: true`. Actions that did not are
308
+ never called: a smoke test must leave nothing behind.
309
+
310
+ In a unit test the same stub is available directly, so an author can assert on
311
+ what their connector sent:
312
+
313
+ ```ts
314
+ const { result, calls } = await harness.withMockHttp(
315
+ [{ url: '/api/messages', method: 'POST', body: { id: 'm-1' } }],
316
+ () => harness.execute('post', { text: 'hi' })
317
+ )
318
+ expect(calls[0].url).toBe('https://acme.test/api/messages')
319
+ ```
320
+
321
+ A request no route matches is refused rather than served, so a test says which
322
+ call escaped instead of quietly reaching a real service.
323
+
324
+ The stub replaces `fetch`, and only `fetch`. A connector that shells out to a
325
+ CLI or opens its own socket is not intercepted by it, so `--mock` reports any
326
+ action the stub never heard from as `mock-not-observed` and leaves `mock` out
327
+ of the receipt rather than vouching for a run it did not see.
328
+
201
329
  ```ts
202
330
  {
203
331
  type: 'newTicket',
@@ -257,6 +385,23 @@ hand:
257
385
  npx vorn-connector setup ./dist/index.js
258
386
  ```
259
387
 
388
+ ### Where a config field is read from
389
+
390
+ A field is read from the environment variable it names in `env`, or from its
391
+ key in CONSTANT_CASE when it names none — `apiToken` becomes `API_TOKEN`. The
392
+ same rule decides what `--live` reads and what the host must set when it hands
393
+ a connector its credentials, so it is exported rather than kept private:
394
+
395
+ ```ts
396
+ import { envNameFor } from '@vornrun/connector-sdk'
397
+
398
+ envNameFor('apiToken') // API_TOKEN
399
+ envNameFor('apiToken', 'GH_TOKEN') // GH_TOKEN — an explicit env always wins
400
+ ```
401
+
402
+ Anything computing these names on the host side should call this rather than
403
+ re-implement it, or the two will disagree about a field named `oauth2Token`.
404
+
260
405
  ## Pack it as a file
261
406
 
262
407
  `vorn-connector pack` builds a single installable file: the manifest plus one
@@ -298,9 +443,125 @@ reads the manifest.
298
443
  Paths are filled with `currentColor`, so the icon picks up the surrounding
299
444
  text color instead of fighting the theme.
300
445
 
446
+ ## Extensions
447
+
448
+ A connector polls a service. An **extension** contributes to a session card
449
+ instead: a footer band under the status bar, a pane beside the terminal, or a
450
+ handler offered when a link in the terminal is clicked. It is the same pack —
451
+ same manifest, same check, same receipt, same catalog — declared with
452
+ `defineExtension` rather than `defineConnector`.
453
+
454
+ ```ts
455
+ import { defineExtension } from '@vornrun/connector-sdk'
456
+
457
+ export const connector = defineExtension({
458
+ id: 'review',
459
+ name: 'Review',
460
+ description: 'Reads the session',
461
+ permissions: ['terminal.read'],
462
+ activates: { workspaceContains: ['package.json'] },
463
+ footers: [
464
+ {
465
+ id: 'checks',
466
+ title: 'Checks',
467
+ every: 30,
468
+ async run(context) {
469
+ const output = await context.host.output({ lines: 200 })
470
+ return [{ label: 'tests', value: /FAIL/.test(output) ? 'failing' : 'passing' }]
471
+ }
472
+ }
473
+ ],
474
+ panes: [{ id: 'report', title: 'Report', web: 'web/report/index.html' }]
475
+ })
476
+ ```
477
+
478
+ Start one with `vorn-connector new review --extension`.
479
+
480
+ ### What it may ask the host for
481
+
482
+ Every method of `context.host` costs one permission, and the manifest has to
483
+ declare it. A call outside what was declared is refused rather than answered —
484
+ by `check` against its stub host, and by Vorn at run time — so what a person
485
+ agreed to when installing is what the extension can reach.
486
+
487
+ | Permission | Host method | What it grants |
488
+ | -------------------- | -------------------- | ------------------------------------ |
489
+ | `git.read` | `diff()`, `status()` | The worktree's diff and status |
490
+ | `terminal.read` | `output()` | The session's recent terminal output |
491
+ | `terminal.selection` | `selection()` | The text selected in the terminal |
492
+ | `terminal.send` | `send(text)` | Typing into the session's terminal |
493
+ | `card.rename` | `rename(name)` | Naming the session card |
494
+ | `agent.usage` | `usage()` | Context and provider allowance |
495
+
496
+ Ask for only what the extension spends: `check` names a permission that was
497
+ declared and never used.
498
+
499
+ ### Where it shows
500
+
501
+ `activates` on the extension, and `when` on any one contribution, narrow where
502
+ it appears. Every declared field has to hold, and each is satisfied by any one
503
+ of its values, so an extension is simply absent where it has nothing to say.
504
+
505
+ ```ts
506
+ activates: {
507
+ workspaceContains: ['Cargo.toml'], // paths relative to the worktree
508
+ remoteHost: ['github.com'],
509
+ agent: ['claude', 'shell'],
510
+ platform: ['darwin', 'linux']
511
+ }
512
+ ```
513
+
514
+ ### What a pane is drawn from
515
+
516
+ A pane is either a page the pack carries or a program it runs. A page lives
517
+ under `web/` in the package, and the directory it sits in is carried into the
518
+ pack, so its stylesheet and script travel with it. Vorn serves the page and
519
+ answers `bridge/<method>` beside it, on the page's own origin: the page holds no
520
+ token, and Vorn grants the call exactly the permissions the manifest declared,
521
+ knowing from the origin which pane is asking. A program is argv, run in the
522
+ session's worktree and drawn as a terminal.
523
+
524
+ ```js
525
+ const response = await fetch('bridge/output', {
526
+ method: 'POST',
527
+ headers: { 'content-type': 'application/json' },
528
+ body: JSON.stringify({ lines: 200 })
529
+ })
530
+ const output = (await response.json()).result
531
+ ```
532
+
533
+ ```ts
534
+ panes: [
535
+ { id: 'report', title: 'Report', web: 'web/report/index.html' },
536
+ { id: 'log', title: 'Log', command: ['./bin/log'], when: { agent: ['shell'] } }
537
+ ]
538
+ ```
539
+
540
+ ### What a link handler is offered for
541
+
542
+ A handler names the pattern it matches against clicked text, and one example
543
+ link it is for. The example is what `check` runs the handler on, so a handler is
544
+ proved against a link it will really be offered for rather than a made-up one.
545
+
546
+ ```ts
547
+ linkHandlers: [
548
+ {
549
+ id: 'pull-request',
550
+ title: 'Pull request',
551
+ pattern: 'https://github\\.com/[^/]+/[^/]+/pull/\\d+',
552
+ example: 'https://github.com/vorn-run/vorn/pull/1',
553
+ async run(context) {
554
+ await context.host.send(`Look at ${context.url}`)
555
+ return { openPane: 'report' }
556
+ }
557
+ }
558
+ ]
559
+ ```
560
+
301
561
  ## CLI
302
562
 
303
563
  ```
564
+ vorn-connector new <id> Scaffold a new connector, ready to build
304
565
  vorn-connector manifest <module> Print the manifest as JSON
305
566
  vorn-connector setup <module> [trigger] Print the Vorn connection settings
306
567
  vorn-connector poll <module> <trigger> Run one poll against the environment
@@ -309,11 +570,15 @@ vorn-connector pack <module> Build an installable .vorn.tgz pack
309
570
  vorn-connector serve <module> Serve on stdio (what Vorn runs)
310
571
  ```
311
572
 
312
- `pack` accepts `--out <dir>`.
573
+ `new` accepts `--out <dir>`, `--name "Display Name"`, `--repo-conventions`,
574
+ which shapes the package the way the connectors repository expects it (scoped
575
+ name, changelog, compiler and test settings), and `--extension`, which
576
+ scaffolds an extension rather than a connector; `pack` accepts `--out <dir>`.
313
577
 
314
578
  `poll` accepts `--since <iso>` and `--limit <n>`, and reads the connector's
315
579
  declared config from your shell environment — the fastest way to confirm
316
580
  credentials and field mapping before wiring anything up.
317
581
 
318
- `check` runs against declared `sample` data by default and takes `--live` to
319
- poll the real source instead.
582
+ `check` runs against declared `sample` data by default. `--mock` serves its
583
+ HTTP and runs every action, `--live` polls the real source and runs the actions
584
+ that are safe to repeat, and `--receipt <file>` writes down what was verified.