@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.
package/README.md CHANGED
@@ -424,6 +424,121 @@ reads the manifest.
424
424
  Paths are filled with `currentColor`, so the icon picks up the surrounding
425
425
  text color instead of fighting the theme.
426
426
 
427
+ ## Extensions
428
+
429
+ A connector polls a service. An **extension** contributes to a session card
430
+ instead: a footer band under the status bar, a pane beside the terminal, or a
431
+ handler offered when a link in the terminal is clicked. It is the same pack —
432
+ same manifest, same check, same receipt, same catalog — declared with
433
+ `defineExtension` rather than `defineConnector`.
434
+
435
+ ```ts
436
+ import { defineExtension } from '@vornrun/connector-sdk'
437
+
438
+ export const connector = defineExtension({
439
+ id: 'review',
440
+ name: 'Review',
441
+ description: 'Reads the session',
442
+ permissions: ['terminal.read'],
443
+ activates: { workspaceContains: ['package.json'] },
444
+ footers: [
445
+ {
446
+ id: 'checks',
447
+ title: 'Checks',
448
+ every: 30,
449
+ async run(context) {
450
+ const output = await context.host.output({ lines: 200 })
451
+ return [{ label: 'tests', value: /FAIL/.test(output) ? 'failing' : 'passing' }]
452
+ }
453
+ }
454
+ ],
455
+ panes: [{ id: 'report', title: 'Report', web: 'web/report/index.html' }]
456
+ })
457
+ ```
458
+
459
+ Start one with `vorn-connector new review --extension`.
460
+
461
+ ### What it may ask the host for
462
+
463
+ Every method of `context.host` costs one permission, and the manifest has to
464
+ declare it. A call outside what was declared is refused rather than answered —
465
+ by `check` against its stub host, and by Vorn at run time — so what a person
466
+ agreed to when installing is what the extension can reach.
467
+
468
+ | Permission | Host method | What it grants |
469
+ | -------------------- | -------------------- | ------------------------------------ |
470
+ | `git.read` | `diff()`, `status()` | The worktree's diff and status |
471
+ | `terminal.read` | `output()` | The session's recent terminal output |
472
+ | `terminal.selection` | `selection()` | The text selected in the terminal |
473
+ | `terminal.send` | `send(text)` | Typing into the session's terminal |
474
+ | `card.rename` | `rename(name)` | Naming the session card |
475
+ | `agent.usage` | `usage()` | Context and provider allowance |
476
+
477
+ Ask for only what the extension spends: `check` names a permission that was
478
+ declared and never used.
479
+
480
+ ### Where it shows
481
+
482
+ `activates` on the extension, and `when` on any one contribution, narrow where
483
+ it appears. Every declared field has to hold, and each is satisfied by any one
484
+ of its values, so an extension is simply absent where it has nothing to say.
485
+
486
+ ```ts
487
+ activates: {
488
+ workspaceContains: ['Cargo.toml'], // paths relative to the worktree
489
+ remoteHost: ['github.com'],
490
+ agent: ['claude', 'shell'],
491
+ platform: ['darwin', 'linux']
492
+ }
493
+ ```
494
+
495
+ ### What a pane is drawn from
496
+
497
+ A pane is either a page the pack carries or a program it runs. A page lives
498
+ under `web/` in the package, and the directory it sits in is carried into the
499
+ pack, so its stylesheet and script travel with it. Vorn serves the page and
500
+ answers `bridge/<method>` beside it, on the page's own origin: the page holds no
501
+ token, and Vorn grants the call exactly the permissions the manifest declared,
502
+ knowing from the origin which pane is asking. A program is argv, run in the
503
+ session's worktree and drawn as a terminal.
504
+
505
+ ```js
506
+ const response = await fetch('bridge/output', {
507
+ method: 'POST',
508
+ headers: { 'content-type': 'application/json' },
509
+ body: JSON.stringify({ lines: 200 })
510
+ })
511
+ const output = (await response.json()).result
512
+ ```
513
+
514
+ ```ts
515
+ panes: [
516
+ { id: 'report', title: 'Report', web: 'web/report/index.html' },
517
+ { id: 'log', title: 'Log', command: ['./bin/log'], when: { agent: ['shell'] } }
518
+ ]
519
+ ```
520
+
521
+ ### What a link handler is offered for
522
+
523
+ A handler names the pattern it matches against clicked text, and one example
524
+ link it is for. The example is what `check` runs the handler on, so a handler is
525
+ proved against a link it will really be offered for rather than a made-up one.
526
+
527
+ ```ts
528
+ linkHandlers: [
529
+ {
530
+ id: 'pull-request',
531
+ title: 'Pull request',
532
+ pattern: 'https://github\\.com/[^/]+/[^/]+/pull/\\d+',
533
+ example: 'https://github.com/vorn-run/vorn/pull/1',
534
+ async run(context) {
535
+ await context.host.send(`Look at ${context.url}`)
536
+ return { openPane: 'report' }
537
+ }
538
+ }
539
+ ]
540
+ ```
541
+
427
542
  ## CLI
428
543
 
429
544
  ```
@@ -436,9 +551,10 @@ vorn-connector pack <module> Build an installable .vorn.tgz pack
436
551
  vorn-connector serve <module> Serve on stdio (what Vorn runs)
437
552
  ```
438
553
 
439
- `new` accepts `--out <dir>`, `--name "Display Name"`, and `--repo-conventions`,
554
+ `new` accepts `--out <dir>`, `--name "Display Name"`, `--repo-conventions`,
440
555
  which shapes the package the way the connectors repository expects it (scoped
441
- name, changelog, compiler and test settings); `pack` accepts `--out <dir>`.
556
+ name, changelog, compiler and test settings), and `--extension`, which
557
+ scaffolds an extension rather than a connector; `pack` accepts `--out <dir>`.
442
558
 
443
559
  `poll` accepts `--since <iso>` and `--limit <n>`, and reads the connector's
444
560
  declared config from your shell environment — the fastest way to confirm
@@ -465,12 +465,177 @@ interface ConnectorDefinition {
465
465
  */
466
466
  preflight?(): Promise<PreflightResult> | PreflightResult;
467
467
  }
468
+ /** What a pack is: a connector polls a service, an extension contributes to a session card. */
469
+ type ConnectorKind = 'connector' | 'extension';
468
470
  /** A validated definition. Every accessor below is guaranteed non-null. */
469
471
  interface Connector extends ConnectorDefinition {
470
472
  readonly version: string;
471
473
  readonly config: ConnectorConfigField[];
472
474
  readonly triggers: TriggerDefinition[];
473
475
  readonly actions: ActionDefinition[];
476
+ readonly kind: ConnectorKind;
477
+ /** What an extension adds to a card. Absent on a connector. */
478
+ readonly contributes?: ExtensionContributions;
479
+ /** What an extension may ask the host for. Absent on a connector. */
480
+ readonly permissions?: ExtensionPermission[];
481
+ /** Where an extension shows at all. Absent on a connector. */
482
+ readonly activates?: ActivationPredicate;
483
+ }
484
+ /**
485
+ * What an extension may ask the host for, named by what it grants rather than
486
+ * by the method that spends it.
487
+ *
488
+ * A closed set on purpose: a permission is shown to a person before they
489
+ * install, so every one of them has to be a sentence someone can weigh.
490
+ */
491
+ type ExtensionPermission = 'git.read' | 'terminal.read' | 'terminal.selection' | 'terminal.send' | 'card.rename' | 'agent.usage';
492
+ /** Session types an extension can name; `shell` is a plain terminal. */
493
+ type ExtensionAgent = 'claude' | 'copilot' | 'codex' | 'opencode' | 'gemini' | 'shell';
494
+ type ExtensionPlatform = 'darwin' | 'linux' | 'win32';
495
+ /**
496
+ * Where a contribution shows.
497
+ *
498
+ * Every declared field must hold for it to show, and each is satisfied by any
499
+ * one of its values: a Rust footer says `workspaceContains: ['Cargo.toml']`
500
+ * and is simply absent everywhere else, rather than reporting nothing.
501
+ */
502
+ interface ActivationPredicate {
503
+ /** Paths relative to the session's worktree; any one of them existing is enough. */
504
+ workspaceContains?: string[];
505
+ /** Host of the worktree's git remote, e.g. `github.com`. */
506
+ remoteHost?: string[];
507
+ agent?: ExtensionAgent[];
508
+ platform?: ExtensionPlatform[];
509
+ }
510
+ interface ContributionBase {
511
+ /** Stable within the extension; the host addresses the contribution by it. */
512
+ id: string;
513
+ title: string;
514
+ description?: string;
515
+ /** Narrows where this one shows, inside where the extension is active at all. */
516
+ when?: ActivationPredicate;
517
+ }
518
+ /**
519
+ * A pane the extension adds beside the terminal: either a page it ships or a
520
+ * program it runs, never both — a union, so the invalid pair is a type error
521
+ * while the extension is being written.
522
+ */
523
+ type PaneContribution = ContributionBase & {
524
+ /** Glyph for the menu row and the pane's own bar; the extension's is used when absent. */
525
+ icon?: ConnectorIcon;
526
+ } & ({
527
+ /** Page inside the pack, under `web/`, rendered in a pane. */
528
+ web: string;
529
+ command?: never;
530
+ } | {
531
+ /** Argv run in the session's worktree, drawn as a terminal. */
532
+ command: string[];
533
+ web?: never;
534
+ });
535
+ /** One reading in a footer band: a label, its value, and how the value reads. */
536
+ interface FooterItem {
537
+ label: string;
538
+ value: string;
539
+ /** `ok` and `danger` colour the value; anything else is ordinary text. */
540
+ tone?: 'default' | 'ok' | 'danger';
541
+ /** Opened when the item is clicked, for a reading that points somewhere. */
542
+ href?: string;
543
+ }
544
+ /** What the agent's provider says is left, for the windows it publishes. */
545
+ interface ExtensionUsageWindow {
546
+ /** The window's own name, e.g. `5h`. */
547
+ window: string;
548
+ /** How much of the allowance is left, 0 to 1. */
549
+ remaining: number;
550
+ resetsAt?: string;
551
+ }
552
+ interface ExtensionUsage {
553
+ contextTokens?: number;
554
+ contextWindow?: number;
555
+ /** Session-cumulative prompt-cache hit rate, 0 to 1. */
556
+ cacheHitRate?: number;
557
+ limits?: ExtensionUsageWindow[];
558
+ }
559
+ /**
560
+ * The host, as an extension sees it.
561
+ *
562
+ * Every method costs exactly one permission — `HOST_PERMISSIONS` says which —
563
+ * and calling one the manifest did not declare is refused rather than ignored,
564
+ * so an extension cannot quietly reach past what a person agreed to.
565
+ */
566
+ interface ExtensionHost {
567
+ /** The worktree's diff against its base. */
568
+ diff(): Promise<string>;
569
+ /** Porcelain status of the worktree. */
570
+ status(): Promise<string>;
571
+ /** The session's recent terminal output, newest last. */
572
+ output(options?: {
573
+ lines?: number;
574
+ }): Promise<string>;
575
+ /** The text selected in the terminal, empty when nothing is selected. */
576
+ selection(): Promise<string>;
577
+ /** Type text into the session's terminal, as a person would. */
578
+ send(text: string): Promise<void>;
579
+ /** Name the session card, until a person names it themselves. */
580
+ rename(name: string): Promise<void>;
581
+ /** Context and provider allowance for the session's agent. */
582
+ usage(): Promise<ExtensionUsage>;
583
+ }
584
+ /** Every method of the host, so the table naming what each one costs stays complete. */
585
+ type ExtensionHostMethod = keyof ExtensionHost;
586
+ /** What a contribution is told about the session it is running for. */
587
+ interface ExtensionContext {
588
+ sessionId: string;
589
+ /** Where the session's work is, so a contribution reads the tree it is about. */
590
+ worktreePath: string;
591
+ agent: ExtensionAgent;
592
+ host: ExtensionHost;
593
+ /** Injectable clock so tests are deterministic. */
594
+ now(): string;
595
+ }
596
+ /** What a link handler is told, on top of the session it was clicked in. */
597
+ interface LinkContext extends ExtensionContext {
598
+ /** The clicked text, which matched this handler's pattern. */
599
+ url: string;
600
+ }
601
+ /** What a link handler asks the app to do once it has run. */
602
+ interface LinkHandled {
603
+ /** Id of one of this extension's panes, opened for the session. */
604
+ openPane?: string;
605
+ }
606
+ /** A band under the card's status bar, recomputed on its own interval. */
607
+ interface FooterContribution extends ContributionBase {
608
+ /** Seconds between calls; the host polls no faster than this. */
609
+ every: number;
610
+ run(context: ExtensionContext): Promise<FooterItem[]> | FooterItem[];
611
+ }
612
+ /** Offers this extension when the clicked text in a terminal matches. */
613
+ interface LinkHandlerContribution extends ContributionBase {
614
+ /** Matched as a regular expression against clicked text, under a bound the app sets. */
615
+ pattern: string;
616
+ /** A link this handler is for, which its pattern must match; `check` runs the handler on it. */
617
+ example: string;
618
+ run(context: LinkContext): Promise<LinkHandled | void> | LinkHandled | void;
619
+ }
620
+ interface ExtensionContributions {
621
+ panes?: PaneContribution[];
622
+ footers?: FooterContribution[];
623
+ linkHandlers?: LinkHandlerContribution[];
624
+ }
625
+ interface ExtensionDefinition {
626
+ /** Stable extension id, e.g. `review`. */
627
+ id: string;
628
+ name: string;
629
+ version?: string;
630
+ description?: string;
631
+ icon?: ConnectorIcon;
632
+ /** Everything this extension may ask the host for, declared rather than inferred. */
633
+ permissions: ExtensionPermission[];
634
+ /** Where the extension shows at all; absent means every session. */
635
+ activates?: ActivationPredicate;
636
+ panes?: PaneContribution[];
637
+ footers?: FooterContribution[];
638
+ linkHandlers?: LinkHandlerContribution[];
474
639
  }
475
640
 
476
641
  /**
@@ -602,6 +767,10 @@ declare function runAction(connector: Connector, actionType: string, args: Recor
602
767
 
603
768
  /** MCP tool name a trigger is served under. */
604
769
  declare function pollToolName(triggerType: string): string;
770
+ /** MCP tool name a footer is recomputed under. */
771
+ declare function footerToolName(footerId: string): string;
772
+ /** MCP tool name a link handler is run under. */
773
+ declare function handlerToolName(handlerId: string): string;
605
774
  /** Tool that reports the connector's manifest and setup hints. */
606
775
  declare const MANIFEST_TOOL = "vorn_connector_manifest";
607
776
  /**
@@ -649,14 +818,43 @@ interface ConnectionSetup {
649
818
  * strategy — rather than Vorn's timestamp comparison — decide what is new.
650
819
  */
651
820
  declare function connectionSetup(connector: Connector, triggerType: string): ConnectionSetup;
821
+ /** A contribution as the manifest carries it: everything but the code that runs it. */
822
+ interface ManifestContribution {
823
+ id: string;
824
+ title: string;
825
+ description?: string;
826
+ when?: ActivationPredicate;
827
+ }
828
+ interface ManifestContributions {
829
+ panes?: Array<ManifestContribution & {
830
+ icon?: ConnectorIcon;
831
+ web?: string;
832
+ command?: string[];
833
+ }>;
834
+ footers?: Array<ManifestContribution & {
835
+ every: number;
836
+ }>;
837
+ linkHandlers?: Array<ManifestContribution & {
838
+ pattern: string;
839
+ example: string;
840
+ }>;
841
+ }
652
842
  interface ConnectorManifest {
653
843
  id: string;
654
844
  name: string;
655
845
  version: string;
846
+ /** Absent on a manifest written before extensions, which reads as a connector. */
847
+ kind?: ConnectorKind;
656
848
  description?: string;
657
849
  icon?: ConnectorIcon;
658
850
  /** How the connector signs in, so the app can say so before installing it. */
659
851
  auth?: ConnectorAuth;
852
+ /** What an extension adds to a card. Present only on an extension. */
853
+ contributes?: ManifestContributions;
854
+ /** What an extension may ask the host for. Present only on an extension. */
855
+ permissions?: ExtensionPermission[];
856
+ /** Where an extension shows at all. Present only on an extension. */
857
+ activates?: ActivationPredicate;
660
858
  triggers: Array<{
661
859
  type: string;
662
860
  label: string;
@@ -783,6 +981,24 @@ declare function withMockHttp<T>(routes: MockRoute[], body: () => Promise<T> | T
783
981
  * spawning anything. Authors get real assertions in a plain unit test.
784
982
  */
785
983
  declare function createConnectorHarness(connector: Connector, harnessOptions?: HarnessOptions): ConnectorHarness;
984
+ /** Answers a footer or handler from fixtures, and refuses what the manifest never asked for. */
985
+ interface MockHostRun {
986
+ host: ExtensionHost;
987
+ /** Permissions the run actually spent, so a declared-but-unused one can be named. */
988
+ used: Set<ExtensionPermission>;
989
+ }
990
+ /** Replaces what the stub answers, for a test whose subject is the reading rather than the plumbing. */
991
+ type MockHostAnswers = Partial<{
992
+ [K in ExtensionHostMethod]: ExtensionHost[K];
993
+ }>;
994
+ /**
995
+ * A host that answers from fixtures and enforces the manifest.
996
+ *
997
+ * The check runs every footer and handler against this rather than a real
998
+ * session, which is what lets a conformance run catch an extension reaching
999
+ * for something it never declared — before a person is asked to grant it.
1000
+ */
1001
+ declare function mockExtensionHost(granted: readonly ExtensionPermission[], answers?: MockHostAnswers): MockHostRun;
786
1002
 
787
1003
  /**
788
1004
  * Every finding this SDK can report.
@@ -791,7 +1007,7 @@ declare function createConnectorHarness(connector: Connector, harnessOptions?: H
791
1007
  * named check owns is a compile error rather than a receipt quietly vouching
792
1008
  * for a check whose failure nothing was watching.
793
1009
  */
794
- type CheckCode = 'missing-description' | 'auth-undeclared' | 'auth-probe-missing' | 'secret-not-marked' | 'action-no-outputs' | 'input-type-unsupported' | 'missing-idempotent' | 'unverifiable' | 'sample-unusable' | 'poll-failed' | 'no-items' | 'no-cursor' | 'cursor-rejected' | 'redelivers-items' | 'stuck-cursor' | 'lifecycle-scripts' | 'keywords-missing' | 'runtime-dependencies' | 'mock-action-failed' | 'mock-network-escape' | 'mock-not-observed' | 'preflight-failed' | 'live-action-failed' | 'pack-launch' | 'pack-too-large';
1010
+ type CheckCode = 'missing-description' | 'auth-undeclared' | 'auth-probe-missing' | 'secret-not-marked' | 'action-no-outputs' | 'input-type-unsupported' | 'missing-idempotent' | 'unverifiable' | 'sample-unusable' | 'poll-failed' | 'no-items' | 'no-cursor' | 'cursor-rejected' | 'redelivers-items' | 'stuck-cursor' | 'lifecycle-scripts' | 'keywords-missing' | 'runtime-dependencies' | 'mock-action-failed' | 'mock-network-escape' | 'mock-not-observed' | 'preflight-failed' | 'live-action-failed' | 'pack-launch' | 'pack-too-large' | 'web-entry-missing' | 'web-entry-outside-package' | 'footer-failed' | 'footer-items-invalid' | 'handler-failed' | 'permission-undeclared' | 'permission-unused';
795
1011
  interface CheckFinding {
796
1012
  /** `error` means the connector will misbehave in Vorn; `warn` is advisory. */
797
1013
  level: 'error' | 'warn';
@@ -880,4 +1096,4 @@ declare function runConformance(connector: Connector, options?: CheckOptions): P
880
1096
  /** Render findings for a terminal. Returns an empty string when all clear. */
881
1097
  declare function formatFindings(findings: CheckFinding[]): string;
882
1098
 
883
- export { type StatusSuggestion as $, type ActionRequest as A, type BundleRequest as B, type CheckFinding as C, type DedupeStrategy as D, MAX_PACK_BYTES as E, type FetchContext as F, MAX_POLL_PAGES as G, type HarnessOptions as H, type MockCall as I, type MockRoute as J, MockRouteMissError as K, type MockRun as L, MANIFEST_TOOL as M, type NormalizedItem as N, OPTIONS_TOOL as O, type PollContext as P, type OptionsContext as Q, type OptionsLoader as R, PREFLIGHT_TOOL as S, type TriggerDefinition as T, type PaginationStrategy as U, type PollPage as V, type PreflightResult as W, type ResilientFetchOptions as X, type RetryPolicy as Y, type RunActionOptions as Z, type RunPollOptions as _, type BundleOutput as a, backoffMs as a0, bundleDependencyFindings as a1, bundledRequireFindings as a2, checkConnector as a3, connectionSetup as a4, connectorManifest as a5, createConnectorHarness as a6, drainPoll as a7, esbuildBundle as a8, escapedMockHttp as a9, formatFindings as aa, lifecycleScriptFindings as ab, pollToolName as ac, readNearestPackageJson as ad, resilientFetch as ae, retryAfterMs as af, runAction as ag, runConformance as ah, runOptions as ai, runPoll as aj, withMockHttp as ak, type ConnectorDefinition as b, type Connector as c, type ConnectorConfig as d, type PollOutcome as e, type ConnectorItem as f, type PostReceiveOp as g, type ActionContext as h, type ActionDefinition as i, type ActionInputField as j, type ActionInputOption as k, type ActionInputType as l, type ActionOutputField as m, type AuthRung as n, CHECK_OWNERS as o, type CheckCode as p, type CheckOptions as q, type ConformanceRun as r, type ConnectionSetup as s, type ConnectorAuth as t, type ConnectorConfigField as u, type ConnectorHarness as v, type ConnectorIcon as w, type ConnectorManifest as x, type ConnectorVerification as y, type DefaultWorkflow as z };
1099
+ export { MAX_PACK_BYTES as $, type ActionRequest as A, type BundleRequest as B, type CheckFinding as C, type ConnectorIcon as D, type ExtensionPermission as E, type ConnectorKind as F, type ConnectorManifest as G, type ConnectorVerification as H, type DedupeStrategy as I, type DefaultWorkflow as J, type ExtensionAgent as K, type ExtensionContext as L, type ExtensionContributions as M, type NormalizedItem as N, type ExtensionPlatform as O, type PollContext as P, type ExtensionUsage as Q, type ExtensionUsageWindow as R, type FetchContext as S, type TriggerDefinition as T, type FooterContribution as U, type FooterItem as V, type HarnessOptions as W, type LinkContext as X, type LinkHandled as Y, type LinkHandlerContribution as Z, MANIFEST_TOOL as _, type BundleOutput as a, MAX_POLL_PAGES as a0, type ManifestContributions as a1, type MockCall as a2, type MockHostAnswers as a3, type MockHostRun as a4, type MockRoute as a5, MockRouteMissError as a6, type MockRun as a7, OPTIONS_TOOL as a8, type OptionsContext as a9, pollToolName as aA, readNearestPackageJson as aB, resilientFetch as aC, retryAfterMs as aD, runAction as aE, runConformance as aF, runOptions as aG, runPoll as aH, withMockHttp as aI, type OptionsLoader as aa, PREFLIGHT_TOOL as ab, type PaginationStrategy as ac, type PaneContribution as ad, type PollPage as ae, type PreflightResult as af, type ResilientFetchOptions as ag, type RetryPolicy as ah, type RunActionOptions as ai, type RunPollOptions as aj, type StatusSuggestion as ak, backoffMs as al, bundleDependencyFindings as am, bundledRequireFindings as an, checkConnector as ao, connectionSetup as ap, connectorManifest as aq, createConnectorHarness as ar, drainPoll as as, esbuildBundle as at, escapedMockHttp as au, footerToolName as av, formatFindings as aw, handlerToolName as ax, lifecycleScriptFindings as ay, mockExtensionHost as az, type ExtensionHostMethod as b, type ConnectorDefinition as c, type Connector as d, type ExtensionDefinition as e, type ConnectorConfig as f, type ExtensionHost as g, type PollOutcome as h, type ConnectorItem as i, type PostReceiveOp as j, type ActionContext as k, type ActionDefinition as l, type ActionInputField as m, type ActionInputOption as n, type ActionInputType as o, type ActionOutputField as p, type ActivationPredicate as q, type AuthRung as r, CHECK_OWNERS as s, type CheckCode as t, type CheckOptions as u, type ConformanceRun as v, type ConnectionSetup as w, type ConnectorAuth as x, type ConnectorConfigField as y, type ConnectorHarness as z };