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

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,27 @@ 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
+ Add `headers` to `check` for a site whose reads need one, such as a CSRF flag. Headers the browser sets itself or that carry who you are, such as `cookie`, `authorization`, `origin` or any `sec-` header, are refused.
221
+
222
+ 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.
223
+
224
+ `--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.
225
+
138
226
  ## Dedupe strategies
139
227
 
140
228
  `dedupe` tells the SDK how to recognize new items, and it then owns the cursor
@@ -198,6 +286,48 @@ trigger and they are replayed through the real dedupe pipeline, so a connector
198
286
  can be checked before anyone has credentials for it; pass `--live` to poll the
199
287
  real source instead.
200
288
 
289
+ `--mock` is the full gate, and the one to run in CI. It answers every HTTP
290
+ request from routes instead of the network, runs each action on its own
291
+ declared arguments, and asks the questions `pack` asks about the package —
292
+ install-time scripts, and anything that would still need a registry at launch:
293
+
294
+ ```console
295
+ $ npx vorn-connector check ./dist/index.js --mock --receipt verified.json
296
+ Verified manifest, auth, secrets, actions, dedupe, no-lifecycle-scripts, keywords, no-runtime-deps, mock — wrote verified.json
297
+ ```
298
+
299
+ `--receipt <file>` writes what was verified, for a catalog to carry:
300
+
301
+ ```json
302
+ { "schema": 1, "version": "1.2.0", "checkedAt": "…", "checks": ["manifest", "…"] }
303
+ ```
304
+
305
+ A check that could not run is left out rather than listed as passed, and a
306
+ connector with any error gets no receipt at all.
307
+
308
+ `--live` additionally asks `preflight` whether the connector can sign in, then
309
+ runs each action that declared `idempotent: true`. Actions that did not are
310
+ never called: a smoke test must leave nothing behind.
311
+
312
+ In a unit test the same stub is available directly, so an author can assert on
313
+ what their connector sent:
314
+
315
+ ```ts
316
+ const { result, calls } = await harness.withMockHttp(
317
+ [{ url: '/api/messages', method: 'POST', body: { id: 'm-1' } }],
318
+ () => harness.execute('post', { text: 'hi' })
319
+ )
320
+ expect(calls[0].url).toBe('https://acme.test/api/messages')
321
+ ```
322
+
323
+ A request no route matches is refused rather than served, so a test says which
324
+ call escaped instead of quietly reaching a real service.
325
+
326
+ The stub replaces `fetch`, and only `fetch`. A connector that shells out to a
327
+ CLI or opens its own socket is not intercepted by it, so `--mock` reports any
328
+ action the stub never heard from as `mock-not-observed` and leaves `mock` out
329
+ of the receipt rather than vouching for a run it did not see.
330
+
201
331
  ```ts
202
332
  {
203
333
  type: 'newTicket',
@@ -257,6 +387,23 @@ hand:
257
387
  npx vorn-connector setup ./dist/index.js
258
388
  ```
259
389
 
390
+ ### Where a config field is read from
391
+
392
+ A field is read from the environment variable it names in `env`, or from its
393
+ key in CONSTANT_CASE when it names none — `apiToken` becomes `API_TOKEN`. The
394
+ same rule decides what `--live` reads and what the host must set when it hands
395
+ a connector its credentials, so it is exported rather than kept private:
396
+
397
+ ```ts
398
+ import { envNameFor } from '@vornrun/connector-sdk'
399
+
400
+ envNameFor('apiToken') // API_TOKEN
401
+ envNameFor('apiToken', 'GH_TOKEN') // GH_TOKEN — an explicit env always wins
402
+ ```
403
+
404
+ Anything computing these names on the host side should call this rather than
405
+ re-implement it, or the two will disagree about a field named `oauth2Token`.
406
+
260
407
  ## Pack it as a file
261
408
 
262
409
  `vorn-connector pack` builds a single installable file: the manifest plus one
@@ -298,9 +445,125 @@ reads the manifest.
298
445
  Paths are filled with `currentColor`, so the icon picks up the surrounding
299
446
  text color instead of fighting the theme.
300
447
 
448
+ ## Extensions
449
+
450
+ A connector polls a service. An **extension** contributes to a session card
451
+ instead: a footer band under the status bar, a pane beside the terminal, or a
452
+ handler offered when a link in the terminal is clicked. It is the same pack —
453
+ same manifest, same check, same receipt, same catalog — declared with
454
+ `defineExtension` rather than `defineConnector`.
455
+
456
+ ```ts
457
+ import { defineExtension } from '@vornrun/connector-sdk'
458
+
459
+ export const connector = defineExtension({
460
+ id: 'review',
461
+ name: 'Review',
462
+ description: 'Reads the session',
463
+ permissions: ['terminal.read'],
464
+ activates: { workspaceContains: ['package.json'] },
465
+ footers: [
466
+ {
467
+ id: 'checks',
468
+ title: 'Checks',
469
+ every: 30,
470
+ async run(context) {
471
+ const output = await context.host.output({ lines: 200 })
472
+ return [{ label: 'tests', value: /FAIL/.test(output) ? 'failing' : 'passing' }]
473
+ }
474
+ }
475
+ ],
476
+ panes: [{ id: 'report', title: 'Report', web: 'web/report/index.html' }]
477
+ })
478
+ ```
479
+
480
+ Start one with `vorn-connector new review --extension`.
481
+
482
+ ### What it may ask the host for
483
+
484
+ Every method of `context.host` costs one permission, and the manifest has to
485
+ declare it. A call outside what was declared is refused rather than answered —
486
+ by `check` against its stub host, and by Vorn at run time — so what a person
487
+ agreed to when installing is what the extension can reach.
488
+
489
+ | Permission | Host method | What it grants |
490
+ | -------------------- | -------------------- | ------------------------------------ |
491
+ | `git.read` | `diff()`, `status()` | The worktree's diff and status |
492
+ | `terminal.read` | `output()` | The session's recent terminal output |
493
+ | `terminal.selection` | `selection()` | The text selected in the terminal |
494
+ | `terminal.send` | `send(text)` | Typing into the session's terminal |
495
+ | `card.rename` | `rename(name)` | Naming the session card |
496
+ | `agent.usage` | `usage()` | Context and provider allowance |
497
+
498
+ Ask for only what the extension spends: `check` names a permission that was
499
+ declared and never used.
500
+
501
+ ### Where it shows
502
+
503
+ `activates` on the extension, and `when` on any one contribution, narrow where
504
+ it appears. Every declared field has to hold, and each is satisfied by any one
505
+ of its values, so an extension is simply absent where it has nothing to say.
506
+
507
+ ```ts
508
+ activates: {
509
+ workspaceContains: ['Cargo.toml'], // paths relative to the worktree
510
+ remoteHost: ['github.com'],
511
+ agent: ['claude', 'shell'],
512
+ platform: ['darwin', 'linux']
513
+ }
514
+ ```
515
+
516
+ ### What a pane is drawn from
517
+
518
+ A pane is either a page the pack carries or a program it runs. A page lives
519
+ under `web/` in the package, and the directory it sits in is carried into the
520
+ pack, so its stylesheet and script travel with it. Vorn serves the page and
521
+ answers `bridge/<method>` beside it, on the page's own origin: the page holds no
522
+ token, and Vorn grants the call exactly the permissions the manifest declared,
523
+ knowing from the origin which pane is asking. A program is argv, run in the
524
+ session's worktree and drawn as a terminal.
525
+
526
+ ```js
527
+ const response = await fetch('bridge/output', {
528
+ method: 'POST',
529
+ headers: { 'content-type': 'application/json' },
530
+ body: JSON.stringify({ lines: 200 })
531
+ })
532
+ const output = (await response.json()).result
533
+ ```
534
+
535
+ ```ts
536
+ panes: [
537
+ { id: 'report', title: 'Report', web: 'web/report/index.html' },
538
+ { id: 'log', title: 'Log', command: ['./bin/log'], when: { agent: ['shell'] } }
539
+ ]
540
+ ```
541
+
542
+ ### What a link handler is offered for
543
+
544
+ A handler names the pattern it matches against clicked text, and one example
545
+ link it is for. The example is what `check` runs the handler on, so a handler is
546
+ proved against a link it will really be offered for rather than a made-up one.
547
+
548
+ ```ts
549
+ linkHandlers: [
550
+ {
551
+ id: 'pull-request',
552
+ title: 'Pull request',
553
+ pattern: 'https://github\\.com/[^/]+/[^/]+/pull/\\d+',
554
+ example: 'https://github.com/vorn-run/vorn/pull/1',
555
+ async run(context) {
556
+ await context.host.send(`Look at ${context.url}`)
557
+ return { openPane: 'report' }
558
+ }
559
+ }
560
+ ]
561
+ ```
562
+
301
563
  ## CLI
302
564
 
303
565
  ```
566
+ vorn-connector new <id> Scaffold a new connector, ready to build
304
567
  vorn-connector manifest <module> Print the manifest as JSON
305
568
  vorn-connector setup <module> [trigger] Print the Vorn connection settings
306
569
  vorn-connector poll <module> <trigger> Run one poll against the environment
@@ -309,11 +572,15 @@ vorn-connector pack <module> Build an installable .vorn.tgz pack
309
572
  vorn-connector serve <module> Serve on stdio (what Vorn runs)
310
573
  ```
311
574
 
312
- `pack` accepts `--out <dir>`.
575
+ `new` accepts `--out <dir>`, `--name "Display Name"`, `--repo-conventions`,
576
+ which shapes the package the way the connectors repository expects it (scoped
577
+ name, changelog, compiler and test settings), and `--extension`, which
578
+ scaffolds an extension rather than a connector; `pack` accepts `--out <dir>`.
313
579
 
314
580
  `poll` accepts `--since <iso>` and `--limit <n>`, and reads the connector's
315
581
  declared config from your shell environment — the fastest way to confirm
316
582
  credentials and field mapping before wiring anything up.
317
583
 
318
- `check` runs against declared `sample` data by default and takes `--live` to
319
- poll the real source instead.
584
+ `check` runs against declared `sample` data by default. `--mock` serves its
585
+ HTTP and runs every action, `--live` polls the real source and runs the actions
586
+ that are safe to repeat, and `--receipt <file>` writes down what was verified.