@vornrun/connector-sdk 0.5.2 → 0.5.3

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
@@ -1,9 +1,8 @@
1
1
  # @vornrun/connector-sdk
2
2
 
3
3
  Build a Vorn pull connector in TypeScript and share it as an ordinary npm
4
- package. No marketplace, no plugin host, no changes to Vorn itself: a connector
5
- built with this SDK runs as an MCP stdio server, and Vorn's generic MCP
6
- connector already knows how to talk to one.
4
+ package. A connector built with this SDK runs as an MCP stdio server, and
5
+ Vorn's generic MCP connector already knows how to talk to one.
7
6
 
8
7
  ```bash
9
8
  npm install @vornrun/connector-sdk
@@ -27,7 +26,10 @@ export default defineConnector({
27
26
  {
28
27
  type: 'newTicket',
29
28
  label: 'New ticket',
30
- async poll({ config, since, limit }) {
29
+ description: 'Tickets created or updated since the last poll',
30
+ // Say how new items are recognized and the SDK owns the cursor for you.
31
+ dedupe: 'timestamp',
32
+ async fetch({ config, since, limit }) {
31
33
  const url = new URL('/tickets', config.baseUrl)
32
34
  if (since) url.searchParams.set('updated_after', since)
33
35
  url.searchParams.set('per_page', String(limit ?? 100))
@@ -38,17 +40,15 @@ export default defineConnector({
38
40
  if (!response.ok) throw new Error(`Acme returned ${response.status}`)
39
41
  const tickets = (await response.json()) as AcmeTicket[]
40
42
 
41
- return {
42
- items: tickets.map((ticket) => ({
43
- externalId: ticket.id,
44
- title: ticket.subject,
45
- url: ticket.html_url,
46
- description: ticket.body,
47
- status: ticket.state,
48
- updatedAt: ticket.updated_at,
49
- data: { priority: ticket.priority }
50
- }))
51
- }
43
+ return tickets.map((ticket) => ({
44
+ externalId: ticket.id,
45
+ title: ticket.subject,
46
+ url: ticket.html_url,
47
+ description: ticket.body,
48
+ status: ticket.state,
49
+ updatedAt: ticket.updated_at,
50
+ data: { priority: ticket.priority }
51
+ }))
52
52
  }
53
53
  }
54
54
  ],
@@ -101,24 +101,23 @@ export default defineConnector({
101
101
  {
102
102
  type: 'newOrder',
103
103
  label: 'New order',
104
- async poll({ config, since, limit }) {
104
+ dedupe: 'timestamp',
105
+ async fetch({ config, since, limit }) {
105
106
  const sql = postgres(config.databaseUrl!)
106
107
  try {
107
108
  const rows = await sql`
108
109
  SELECT id, reference, status, updated_at
109
110
  FROM orders
110
- WHERE updated_at > ${since ?? '1970-01-01'}
111
+ WHERE updated_at >= ${since ?? '1970-01-01'}
111
112
  ORDER BY updated_at ASC
112
113
  LIMIT ${limit ?? 200}
113
114
  `
114
- return {
115
- items: rows.map((row) => ({
116
- externalId: row.id,
117
- title: `Order ${row.reference}`,
118
- status: row.status,
119
- updatedAt: row.updated_at
120
- }))
121
- }
115
+ return rows.map((row) => ({
116
+ externalId: row.id,
117
+ title: `Order ${row.reference}`,
118
+ status: row.status,
119
+ updatedAt: row.updated_at
120
+ }))
122
121
  } finally {
123
122
  await sql.end()
124
123
  }
@@ -132,13 +131,39 @@ Two rules make a pull trigger reliable, and the SDK enforces both:
132
131
 
133
132
  1. `externalId` must be stable — Vorn dedupes on it, so a changing id means
134
133
  duplicate work items.
135
- 2. `updatedAt` must be monotonic and ISO-comparable — Vorn advances its poll
136
- cursor from it. Sort ascending by that column and honor `since`.
134
+ 2. `updatedAt` must be monotonic and ISO-comparable — the cursor advances from
135
+ it. Use `>=` when filtering on `since` and sort ascending; returning a few
136
+ items again is free, because the SDK drops anything already delivered.
137
+
138
+ ## Dedupe strategies
139
+
140
+ `dedupe` tells the SDK how to recognize new items, and it then owns the cursor
141
+ — including the case that quietly breaks hand-written connectors, where several
142
+ items share the newest timestamp and are either dropped forever (`>`) or
143
+ redelivered on every poll (`>=`).
144
+
145
+ | strategy | your `fetch` receives | use it when |
146
+ | ----------- | --------------------- | ------------------------------------------------ |
147
+ | `timestamp` | `since` | the source has a reliable "last changed" field |
148
+ | `lastItem` | `lastItemId` | a newest-first feed with no dependable timestamp |
149
+
150
+ ```ts
151
+ {
152
+ type: 'newPost',
153
+ label: 'New post',
154
+ dedupe: 'lastItem',
155
+ // Return the feed newest-first; the SDK stops at the last id it delivered.
156
+ fetch: ({ config }) => fetchFeed(config)
157
+ }
158
+ ```
159
+
160
+ A first poll never drains the whole history — it delivers one page and starts
161
+ tracking from there.
137
162
 
138
- ## Paging a backlog
163
+ ## Paging a backlog by hand
139
164
 
140
- Return `hasMore: true` with a `nextCursor`, and Vorn (or `drainPoll` in tests)
141
- will keep pulling bounded pages until the backlog is drained:
165
+ When a source's paging cannot be expressed as "everything since X", implement
166
+ `poll` instead of `fetch` and own the cursor yourself:
142
167
 
143
168
  ```ts
144
169
  async poll({ cursor, config }) {
@@ -154,6 +179,35 @@ async poll({ cursor, config }) {
154
179
 
155
180
  A cursor that does not advance is rejected rather than looped on.
156
181
 
182
+ ## Check it before shipping
183
+
184
+ `vorn-connector check` verifies a connector against the contract Vorn relies
185
+ on — most importantly that re-polling with its own cursor does not redeliver
186
+ items it already handed over:
187
+
188
+ ```console
189
+ $ npx vorn-connector check ./dist/index.js
190
+ error trigger newTicket: re-polling with its own nextCursor returned 3 already-delivered item(s), starting with "1042" [redelivers-items]
191
+ warn action closeTicket: does not declare `idempotent`, so an agent cannot tell whether retrying is safe [missing-idempotent]
192
+
193
+ 1 error(s), 1 warning(s)
194
+ ```
195
+
196
+ It exits non-zero on errors, so it works as a CI gate. Add `sample` items to a
197
+ trigger and they are replayed through the real dedupe pipeline, so a connector
198
+ can be checked before anyone has credentials for it; pass `--live` to poll the
199
+ real source instead.
200
+
201
+ ```ts
202
+ {
203
+ type: 'newTicket',
204
+ label: 'New ticket',
205
+ dedupe: 'timestamp',
206
+ sample: [{ externalId: '1', title: 'Example ticket', updatedAt: '2026-01-01T00:00:00.000Z' }],
207
+ fetch: ({ config, since }) => fetchTickets(config, since)
208
+ }
209
+ ```
210
+
157
211
  ## Test it without running the app
158
212
 
159
213
  ```ts
@@ -181,23 +235,53 @@ test('does not redeliver the same backlog forever', async () => {
181
235
 
182
236
  ## Install it in Vorn
183
237
 
184
- Run the CLI to get the exact connection settings:
238
+ **Settings Connectors MCP → From a package**, then type the package name.
239
+
240
+ Vorn starts the connector once, asks it to describe itself, and fills in the
241
+ connection settings from the answer. All that is left on screen is the
242
+ connector's own name, its triggers, and the config it declared.
243
+
244
+ Nothing needs transcribing: `pollTool`, `itemsPath`, `idField`,
245
+ `timestampField`, `titleField`, `urlField`, `cursorArg` and `cursorPath` all
246
+ come from the manifest. `cursorArg` is what makes the dedupe strategy
247
+ load-bearing — Vorn hands the connector back its own cursor on every poll and
248
+ fires for whatever it returns, rather than re-filtering by timestamp itself.
249
+
250
+ Because the connector is a normal npm package, versions are pinned by the
251
+ package spec and upgrades are a version bump — no separate registry.
252
+
253
+ To see the same values on the command line, or to wire a connection up by
254
+ hand:
185
255
 
186
256
  ```bash
187
257
  npx vorn-connector setup ./dist/index.js
188
258
  ```
189
259
 
190
- It prints the values for **Settings → Connectors → MCP → New connection**:
260
+ ### Ship an icon
191
261
 
192
- | Field | Value |
193
- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
194
- | Command | `npx` |
195
- | Arguments | `["-y", "@your-scope/acme-connector"]` |
196
- | Secret env | `{"API_TOKEN": "…"}` |
197
- | Filters | `pollTool: poll_newTicket`, `itemsPath: items`, `idField: externalId`, `timestampField: updatedAt`, `titleField: title`, `urlField: url` |
262
+ Without one, a connector shows the generic MCP glyph and is hard to pick out
263
+ of a list of connections.
198
264
 
199
- Because the connector is a normal npm package, versions are pinned by the
200
- `npx` argument and upgrades are a version bump — no separate registry.
265
+ ```ts
266
+ defineConnector({
267
+ id: 'acme',
268
+ name: 'Acme',
269
+ icon: {
270
+ viewBox: '0 0 24 24',
271
+ paths: ['M12 2 2 22h20L12 2z']
272
+ }
273
+ // ...
274
+ })
275
+ ```
276
+
277
+ Path data only — no markup, no `<svg>` wrapper, no external references. Vorn
278
+ draws these as `<path d="...">` inside an element it owns, so an icon can
279
+ never contribute markup to the app rendering it. Anything that is not path
280
+ data is rejected by `defineConnector` at import time, and again when Vorn
281
+ reads the manifest.
282
+
283
+ Paths are filled with `currentColor`, so the icon picks up the surrounding
284
+ text color instead of fighting the theme.
201
285
 
202
286
  ## CLI
203
287
 
@@ -205,9 +289,13 @@ Because the connector is a normal npm package, versions are pinned by the
205
289
  vorn-connector manifest <module> Print the manifest as JSON
206
290
  vorn-connector setup <module> [trigger] Print the Vorn connection settings
207
291
  vorn-connector poll <module> <trigger> Run one poll against the environment
292
+ vorn-connector check <module> Verify the connector against the contract
208
293
  vorn-connector serve <module> Serve on stdio (what Vorn runs)
209
294
  ```
210
295
 
211
296
  `poll` accepts `--since <iso>` and `--limit <n>`, and reads the connector's
212
297
  declared config from your shell environment — the fastest way to confirm
213
298
  credentials and field mapping before wiring anything up.
299
+
300
+ `check` runs against declared `sample` data by default and takes `--live` to
301
+ poll the real source instead.
@@ -1,84 +1,3 @@
1
- // src/define.ts
2
- var KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
3
- function assertUnique(kind, keys) {
4
- const seen = /* @__PURE__ */ new Set();
5
- for (const key of keys) {
6
- if (seen.has(key)) throw new Error(`Duplicate ${kind} "${key}"`);
7
- seen.add(key);
8
- }
9
- }
10
- function envNameFor(key, explicit) {
11
- if (explicit) return explicit;
12
- return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
13
- }
14
- function defineConnector(definition) {
15
- if (!KEY_PATTERN.test(definition.id ?? "")) {
16
- throw new Error(`Connector id "${definition.id}" must start with a letter and be url-safe`);
17
- }
18
- if (!definition.name?.trim()) {
19
- throw new Error(`Connector ${definition.id} is missing a name`);
20
- }
21
- const triggers = definition.triggers ?? [];
22
- const actions = definition.actions ?? [];
23
- if (triggers.length === 0 && actions.length === 0) {
24
- throw new Error(`Connector ${definition.id} declares no triggers and no actions`);
25
- }
26
- for (const trigger of triggers) {
27
- if (!KEY_PATTERN.test(trigger.type ?? "")) {
28
- throw new Error(`Trigger type "${trigger.type}" must start with a letter and be url-safe`);
29
- }
30
- if (typeof trigger.poll !== "function") {
31
- throw new Error(`Trigger ${trigger.type} is missing a poll() implementation`);
32
- }
33
- }
34
- for (const action of actions) {
35
- if (!KEY_PATTERN.test(action.type ?? "")) {
36
- throw new Error(`Action type "${action.type}" must start with a letter and be url-safe`);
37
- }
38
- if (typeof action.run !== "function") {
39
- throw new Error(`Action ${action.type} is missing a run() implementation`);
40
- }
41
- }
42
- assertUnique(
43
- "trigger",
44
- triggers.map((trigger) => trigger.type)
45
- );
46
- assertUnique(
47
- "action",
48
- actions.map((action) => action.type)
49
- );
50
- assertUnique(
51
- "config field",
52
- (definition.config ?? []).map((field) => field.key)
53
- );
54
- return {
55
- ...definition,
56
- version: definition.version ?? "0.0.0",
57
- config: definition.config ?? [],
58
- triggers,
59
- actions
60
- };
61
- }
62
- function resolveConfig(connector, env = process.env) {
63
- const config = {};
64
- const missing = [];
65
- for (const field of connector.config) {
66
- const name = envNameFor(field.key, field.env);
67
- const value = env[name] ?? field.default;
68
- if (value === void 0 || value === "") {
69
- if (field.required) missing.push(`${field.key} (${name})`);
70
- continue;
71
- }
72
- config[field.key] = value;
73
- }
74
- if (missing.length > 0) {
75
- throw new Error(
76
- `Connector ${connector.id} is missing required configuration: ${missing.join(", ")}`
77
- );
78
- }
79
- return config;
80
- }
81
-
82
1
  // src/normalize.ts
83
2
  var RESERVED_KEYS = [
84
3
  "externalId",
@@ -91,6 +10,12 @@ var RESERVED_KEYS = [
91
10
  "updatedAt"
92
11
  ];
93
12
  var UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
13
+ function itemExternalId(item) {
14
+ return String(item.externalId ?? "").trim();
15
+ }
16
+ function itemTimestamp(item, fallback) {
17
+ return isoTimestamp(item.updatedAt, fallback);
18
+ }
94
19
  function isoTimestamp(value, fallback) {
95
20
  if (value === void 0) return fallback;
96
21
  const date = value instanceof Date ? value : new Date(value);
@@ -100,7 +25,7 @@ function isoTimestamp(value, fallback) {
100
25
  return date.toISOString();
101
26
  }
102
27
  function normalizeItem(item, polledAt) {
103
- const externalId = String(item.externalId ?? "").trim();
28
+ const externalId = itemExternalId(item);
104
29
  if (!externalId) {
105
30
  throw new Error("Connector item is missing externalId");
106
31
  }
@@ -137,6 +62,120 @@ function normalizeItems(items, polledAt) {
137
62
  });
138
63
  }
139
64
 
65
+ // src/dedupe.ts
66
+ var MAX_BOUNDARY_IDS = 500;
67
+ function decodeCursor(cursor, strategy) {
68
+ if (!cursor) return void 0;
69
+ let parsed;
70
+ try {
71
+ parsed = JSON.parse(cursor);
72
+ } catch (error) {
73
+ throw new Error(`Cursor is not valid SDK cursor JSON: ${cursor}`, { cause: error });
74
+ }
75
+ const state = parsed;
76
+ if (!state || typeof state !== "object" || state.v !== 1 || state.s !== strategy) {
77
+ throw new Error(`Cursor does not belong to the "${strategy}" strategy: ${cursor}`);
78
+ }
79
+ const wellFormed = state.s === "timestamp" ? typeof state.t === "string" && Array.isArray(state.ids) && state.ids.every((id) => typeof id === "string") : typeof state.id === "string";
80
+ if (!wellFormed) {
81
+ throw new Error(`Cursor is missing the fields the "${strategy}" strategy needs: ${cursor}`);
82
+ }
83
+ return state;
84
+ }
85
+ function compare(left, right) {
86
+ if (left === right) return 0;
87
+ return left < right ? -1 : 1;
88
+ }
89
+ function page(chronological, context, hadCursor, nextCursor) {
90
+ const delivered = context.limit === void 0 ? chronological : chronological.slice(0, context.limit);
91
+ if (delivered.length === 0) {
92
+ return { items: [], ...context.cursor !== void 0 && { nextCursor: context.cursor } };
93
+ }
94
+ return {
95
+ items: delivered.map((entry) => entry.item),
96
+ nextCursor: JSON.stringify(nextCursor(delivered)),
97
+ // Only drain a backlog we know we truncated, and only once a cursor
98
+ // exists — a first poll should not pull the source's entire history.
99
+ hasMore: chronological.length > delivered.length && hadCursor
100
+ };
101
+ }
102
+ function timestampPoll(fetched, state, context, polledAt) {
103
+ const boundary = state?.t ?? context.since;
104
+ const seen = new Set(state?.ids ?? []);
105
+ const fresh = [];
106
+ const pinnedAlreadySeen = [];
107
+ for (const item of fetched) {
108
+ const id = itemExternalId(item);
109
+ const pinned = item.updatedAt === void 0 && boundary !== void 0;
110
+ const at = pinned ? boundary : itemTimestamp(item, polledAt);
111
+ const isNew = boundary === void 0 || at > boundary || at === boundary && !seen.has(id);
112
+ if (isNew) fresh.push({ item, at, id, ...pinned && { pinned: true } });
113
+ else if (pinned) pinnedAlreadySeen.push(id);
114
+ }
115
+ fresh.sort((left, right) => compare(left.at, right.at) || compare(left.id, right.id));
116
+ return page(fresh, context, state !== void 0, (delivered) => {
117
+ const newest = delivered[delivered.length - 1].at;
118
+ const atNewest = [];
119
+ for (let i = delivered.length - 1; i >= 0 && delivered[i].at === newest; i -= 1) {
120
+ atNewest.push(delivered[i].id);
121
+ }
122
+ const carried = newest === boundary ? [...seen, ...atNewest] : [
123
+ ...pinnedAlreadySeen,
124
+ ...delivered.filter((entry) => entry.pinned).map((entry) => entry.id),
125
+ ...atNewest
126
+ ];
127
+ return { v: 1, s: "timestamp", t: newest, ids: carried.slice(-MAX_BOUNDARY_IDS) };
128
+ });
129
+ }
130
+ function lastItemPoll(fetched, state, context, polledAt) {
131
+ const keyed = fetched.map((item) => ({
132
+ item,
133
+ at: itemTimestamp(item, polledAt),
134
+ id: itemExternalId(item)
135
+ }));
136
+ const stopAt = state ? keyed.findIndex((entry) => entry.id === state.id) : -1;
137
+ const chronological = (stopAt === -1 ? keyed : keyed.slice(0, stopAt)).reverse();
138
+ return page(chronological, context, state !== void 0, (delivered) => ({
139
+ v: 1,
140
+ s: "lastItem",
141
+ id: delivered[delivered.length - 1].id
142
+ }));
143
+ }
144
+ async function pollWithDedupe(trigger, context) {
145
+ const strategy = trigger.dedupe;
146
+ const fetchItems = trigger.fetch;
147
+ if (!strategy || !fetchItems) {
148
+ throw new Error(`Trigger ${trigger.type} is not a declarative trigger`);
149
+ }
150
+ const polledAt = context.now();
151
+ if (strategy === "lastItem") {
152
+ const state2 = decodeCursor(context.cursor, "lastItem");
153
+ const fetched2 = await runFetch(trigger.type, fetchItems, {
154
+ config: context.config,
155
+ ...state2 && { lastItemId: state2.id },
156
+ ...context.limit !== void 0 && { limit: context.limit },
157
+ now: context.now
158
+ });
159
+ return lastItemPoll(fetched2, state2, context, polledAt);
160
+ }
161
+ const state = decodeCursor(context.cursor, "timestamp");
162
+ const since = state?.t ?? context.since;
163
+ const fetched = await runFetch(trigger.type, fetchItems, {
164
+ config: context.config,
165
+ ...since !== void 0 && { since },
166
+ ...context.limit !== void 0 && { limit: context.limit },
167
+ now: context.now
168
+ });
169
+ return timestampPoll(fetched, state, context, polledAt);
170
+ }
171
+ async function runFetch(type, fetchItems, context) {
172
+ const fetched = await fetchItems(context);
173
+ if (!Array.isArray(fetched)) {
174
+ throw new Error(`Trigger ${type} fetch() did not return an array`);
175
+ }
176
+ return fetched;
177
+ }
178
+
140
179
  // src/runtime.ts
141
180
  var MAX_POLL_PAGES = 1e3;
142
181
  async function runPoll(connector, triggerType, options = {}) {
@@ -153,7 +192,7 @@ async function runPoll(connector, triggerType, options = {}) {
153
192
  ...options.limit !== void 0 && { limit: options.limit },
154
193
  now
155
194
  };
156
- const outcome = await trigger.poll(context);
195
+ const outcome = typeof trigger.poll === "function" ? await trigger.poll(context) : await pollWithDedupe(trigger, context);
157
196
  if (!outcome || !Array.isArray(outcome.items)) {
158
197
  throw new Error(`Trigger ${triggerType} did not return an items array`);
159
198
  }
@@ -169,7 +208,7 @@ async function runPoll(connector, triggerType, options = {}) {
169
208
  async function drainPoll(connector, triggerType, options = {}) {
170
209
  const collected = [];
171
210
  let cursor = options.cursor;
172
- for (let page = 0; page < MAX_POLL_PAGES; page++) {
211
+ for (let page2 = 0; page2 < MAX_POLL_PAGES; page2++) {
173
212
  const result = await runPoll(connector, triggerType, {
174
213
  ...options,
175
214
  ...cursor !== void 0 && { cursor }
@@ -226,6 +265,277 @@ async function runAction(connector, actionType, args, options = {}) {
226
265
  return output ?? {};
227
266
  }
228
267
 
268
+ // src/check.ts
269
+ function finding(level, code, target, message) {
270
+ return { level, code, target, message };
271
+ }
272
+ function sampleTrigger(trigger) {
273
+ return { ...trigger, poll: void 0, fetch: () => trigger.sample ?? [] };
274
+ }
275
+ async function checkPollBehaviour(connector, trigger, options) {
276
+ const found = [];
277
+ const probe = { ...connector, triggers: [trigger] };
278
+ const target = `trigger ${trigger.type}`;
279
+ const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
280
+ const attempt = async (cursor, code, what) => {
281
+ try {
282
+ return await runPoll(probe, trigger.type, {
283
+ config: options.config ?? {},
284
+ ...cursor !== void 0 && { cursor },
285
+ now
286
+ });
287
+ } catch (error) {
288
+ const reason = error instanceof Error ? error.message : String(error);
289
+ return finding("error", code, target, `${what} threw: ${reason}`);
290
+ }
291
+ };
292
+ const first = await attempt(void 0, "poll-failed", "first poll");
293
+ if ("level" in first) return [first];
294
+ if (first.items.length === 0) {
295
+ found.push(
296
+ finding("warn", "no-items", target, "returned nothing, so delivery could not be verified")
297
+ );
298
+ return found;
299
+ }
300
+ if (first.nextCursor === void 0) {
301
+ found.push(
302
+ finding(
303
+ "error",
304
+ "no-cursor",
305
+ target,
306
+ "returned items but no nextCursor, so every poll will redeliver them"
307
+ )
308
+ );
309
+ return found;
310
+ }
311
+ const second = await attempt(
312
+ first.nextCursor,
313
+ "cursor-rejected",
314
+ "re-polling with its own nextCursor"
315
+ );
316
+ if ("level" in second) return [...found, second];
317
+ const delivered = new Set(first.items.map((item) => item.externalId));
318
+ const repeated = second.items.filter((item) => delivered.has(item.externalId));
319
+ if (repeated.length > 0) {
320
+ found.push(
321
+ finding(
322
+ "error",
323
+ "redelivers-items",
324
+ target,
325
+ `re-polling with its own nextCursor returned ${repeated.length} already-delivered item(s), starting with "${repeated[0].externalId}"`
326
+ )
327
+ );
328
+ }
329
+ if (second.hasMore && second.nextCursor === first.nextCursor) {
330
+ found.push(
331
+ finding("error", "stuck-cursor", target, "reports more pages but its cursor never advances")
332
+ );
333
+ }
334
+ return found;
335
+ }
336
+ async function checkConnector(connector, options = {}) {
337
+ const found = [];
338
+ if (!connector.description?.trim()) {
339
+ found.push(
340
+ finding(
341
+ "warn",
342
+ "missing-description",
343
+ connector.id,
344
+ "has no description; agents use it to decide when the connector applies"
345
+ )
346
+ );
347
+ }
348
+ const perTrigger = await Promise.all(
349
+ connector.triggers.map(async (trigger) => {
350
+ const target = `trigger ${trigger.type}`;
351
+ const triggerFindings = [];
352
+ if (!trigger.description?.trim()) {
353
+ triggerFindings.push(finding("warn", "missing-description", target, "has no description"));
354
+ }
355
+ if (options.live) {
356
+ triggerFindings.push(...await checkPollBehaviour(connector, trigger, options));
357
+ } else if (!trigger.sample?.length) {
358
+ triggerFindings.push(
359
+ finding(
360
+ "warn",
361
+ "unverifiable",
362
+ target,
363
+ "has no sample items and no credentials were supplied, so nothing could be verified"
364
+ )
365
+ );
366
+ } else if (!trigger.dedupe) {
367
+ triggerFindings.push(
368
+ finding(
369
+ "warn",
370
+ "sample-unusable",
371
+ target,
372
+ "declares sample items but implements poll() directly, so they cannot be replayed; re-run with --live"
373
+ )
374
+ );
375
+ } else {
376
+ triggerFindings.push(
377
+ ...await checkPollBehaviour(connector, sampleTrigger(trigger), options)
378
+ );
379
+ }
380
+ return triggerFindings;
381
+ })
382
+ );
383
+ found.push(...perTrigger.flat());
384
+ for (const action of connector.actions) {
385
+ const target = `action ${action.type}`;
386
+ if (!action.description?.trim()) {
387
+ found.push(
388
+ finding("warn", "missing-description", target, "has no description for the agent to read")
389
+ );
390
+ }
391
+ if (action.idempotent === void 0) {
392
+ found.push(
393
+ finding(
394
+ "warn",
395
+ "missing-idempotent",
396
+ target,
397
+ "does not declare `idempotent`, so an agent cannot tell whether retrying is safe"
398
+ )
399
+ );
400
+ }
401
+ for (const input of action.inputs ?? []) {
402
+ if (!input.description?.trim()) {
403
+ found.push(
404
+ finding(
405
+ "warn",
406
+ "missing-description",
407
+ `${target} input ${input.key}`,
408
+ "has no description"
409
+ )
410
+ );
411
+ }
412
+ }
413
+ }
414
+ return found;
415
+ }
416
+ function formatFindings(findings) {
417
+ return findings.map((item) => `${item.level.padEnd(5)} ${item.target}: ${item.message} [${item.code}]`).join("\n");
418
+ }
419
+
420
+ // src/define.ts
421
+ var KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
422
+ var PATH_DATA_PATTERN = /^[MmZzLlHhVvCcSsQqTtAa0-9\s,.\-+eE]+$/;
423
+ var VIEW_BOX_PATTERN = /^-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+$/;
424
+ var DEDUPE_STRATEGIES = ["timestamp", "lastItem"];
425
+ function assertUnique(kind, keys) {
426
+ const seen = /* @__PURE__ */ new Set();
427
+ for (const key of keys) {
428
+ if (seen.has(key)) throw new Error(`Duplicate ${kind} "${key}"`);
429
+ seen.add(key);
430
+ }
431
+ }
432
+ function envNameFor(key, explicit) {
433
+ if (explicit) return explicit;
434
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
435
+ }
436
+ function defineConnector(definition) {
437
+ if (!KEY_PATTERN.test(definition.id ?? "")) {
438
+ throw new Error(`Connector id "${definition.id}" must start with a letter and be url-safe`);
439
+ }
440
+ if (!definition.name?.trim()) {
441
+ throw new Error(`Connector ${definition.id} is missing a name`);
442
+ }
443
+ if (definition.icon) {
444
+ const { viewBox, paths } = definition.icon;
445
+ if (!Array.isArray(paths) || paths.length === 0) {
446
+ throw new Error(`Connector ${definition.id} has an icon with no paths`);
447
+ }
448
+ for (const path of paths) {
449
+ if (typeof path !== "string" || !PATH_DATA_PATTERN.test(path)) {
450
+ throw new Error(
451
+ `Connector ${definition.id} has an icon path that is not SVG path data. Only path data is accepted, not markup.`
452
+ );
453
+ }
454
+ }
455
+ if (viewBox !== void 0 && !VIEW_BOX_PATTERN.test(viewBox)) {
456
+ throw new Error(`Connector ${definition.id} has an icon viewBox that is not four numbers`);
457
+ }
458
+ }
459
+ const triggers = definition.triggers ?? [];
460
+ const actions = definition.actions ?? [];
461
+ if (triggers.length === 0 && actions.length === 0) {
462
+ throw new Error(`Connector ${definition.id} declares no triggers and no actions`);
463
+ }
464
+ for (const trigger of triggers) {
465
+ if (!KEY_PATTERN.test(trigger.type ?? "")) {
466
+ throw new Error(`Trigger type "${trigger.type}" must start with a letter and be url-safe`);
467
+ }
468
+ const loose = trigger;
469
+ const declarative = typeof loose.fetch === "function";
470
+ const imperative = typeof loose.poll === "function";
471
+ if (declarative && imperative) {
472
+ throw new Error(`Trigger ${trigger.type} declares both fetch() and poll(); pick one`);
473
+ }
474
+ if (declarative !== (loose.dedupe !== void 0)) {
475
+ throw new Error(
476
+ `Trigger ${trigger.type} needs fetch() and a dedupe strategy together, not one alone`
477
+ );
478
+ }
479
+ if (loose.dedupe !== void 0 && !DEDUPE_STRATEGIES.includes(loose.dedupe)) {
480
+ throw new Error(
481
+ `Trigger ${trigger.type} has unknown dedupe strategy ${JSON.stringify(loose.dedupe)}; expected ${DEDUPE_STRATEGIES.join(" or ")}`
482
+ );
483
+ }
484
+ if (loose.poll !== void 0 && !imperative) {
485
+ throw new Error(`Trigger ${trigger.type} declares poll but it is not a function`);
486
+ }
487
+ if (!declarative && !imperative) {
488
+ throw new Error(`Trigger ${trigger.type} is missing a fetch() or poll() implementation`);
489
+ }
490
+ }
491
+ for (const action of actions) {
492
+ if (!KEY_PATTERN.test(action.type ?? "")) {
493
+ throw new Error(`Action type "${action.type}" must start with a letter and be url-safe`);
494
+ }
495
+ if (typeof action.run !== "function") {
496
+ throw new Error(`Action ${action.type} is missing a run() implementation`);
497
+ }
498
+ }
499
+ assertUnique(
500
+ "trigger",
501
+ triggers.map((trigger) => trigger.type)
502
+ );
503
+ assertUnique(
504
+ "action",
505
+ actions.map((action) => action.type)
506
+ );
507
+ assertUnique(
508
+ "config field",
509
+ (definition.config ?? []).map((field) => field.key)
510
+ );
511
+ return {
512
+ ...definition,
513
+ version: definition.version ?? "0.0.0",
514
+ config: definition.config ?? [],
515
+ triggers,
516
+ actions
517
+ };
518
+ }
519
+ function resolveConfig(connector, env = process.env) {
520
+ const config = {};
521
+ const missing = [];
522
+ for (const field of connector.config) {
523
+ const name = envNameFor(field.key, field.env);
524
+ const value = env[name] ?? field.default;
525
+ if (value === void 0 || value === "") {
526
+ if (field.required) missing.push(`${field.key} (${name})`);
527
+ continue;
528
+ }
529
+ config[field.key] = value;
530
+ }
531
+ if (missing.length > 0) {
532
+ throw new Error(
533
+ `Connector ${connector.id} is missing required configuration: ${missing.join(", ")}`
534
+ );
535
+ }
536
+ return config;
537
+ }
538
+
229
539
  // src/setup.ts
230
540
  function pollToolName(triggerType) {
231
541
  return `poll_${triggerType}`;
@@ -245,7 +555,9 @@ function connectionSetup(connector, triggerType) {
245
555
  idField: "externalId",
246
556
  timestampField: "updatedAt",
247
557
  titleField: "title",
248
- urlField: "url"
558
+ urlField: "url",
559
+ cursorArg: "cursor",
560
+ cursorPath: "nextCursor"
249
561
  },
250
562
  env: connector.config.map((field) => ({
251
563
  name: envNameFor(field.key, field.env),
@@ -261,6 +573,7 @@ function connectorManifest(connector) {
261
573
  name: connector.name,
262
574
  version: connector.version,
263
575
  ...connector.description !== void 0 && { description: connector.description },
576
+ ...connector.icon !== void 0 && { icon: connector.icon },
264
577
  triggers: connector.triggers.map((trigger) => ({
265
578
  type: trigger.type,
266
579
  label: trigger.label,
@@ -374,10 +687,12 @@ function createConnectorServer(connector, options = {}) {
374
687
  );
375
688
  }
376
689
  for (const action of connector.actions) {
690
+ const base = action.description ?? `${action.label} in ${connector.name}`;
691
+ 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.";
377
692
  server.registerTool(
378
693
  action.type,
379
694
  {
380
- description: action.description ?? `${action.label} in ${connector.name}`,
695
+ description: `${base}${retryHint}`,
381
696
  inputSchema: inputShape(action.inputs ?? []),
382
697
  outputSchema: outputSchema(action.outputs ?? [])
383
698
  },
@@ -403,15 +718,18 @@ async function serveConnector(connector, options = {}) {
403
718
  }
404
719
 
405
720
  export {
406
- envNameFor,
407
- defineConnector,
408
- resolveConfig,
409
721
  normalizeItem,
410
722
  normalizeItems,
723
+ pollWithDedupe,
411
724
  MAX_POLL_PAGES,
412
725
  runPoll,
413
726
  drainPoll,
414
727
  runAction,
728
+ checkConnector,
729
+ formatFindings,
730
+ envNameFor,
731
+ defineConnector,
732
+ resolveConfig,
415
733
  pollToolName,
416
734
  MANIFEST_TOOL,
417
735
  connectionSetup,
package/dist/cli.js CHANGED
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ checkConnector,
3
4
  connectionSetup,
4
5
  connectorManifest,
6
+ formatFindings,
5
7
  resolveConfig,
6
8
  runPoll,
7
9
  serveConnector
8
- } from "./chunk-UANUTYUV.js";
10
+ } from "./chunk-W4GGTEUK.js";
9
11
 
10
12
  // src/cli.ts
11
13
  import { pathToFileURL } from "url";
@@ -15,25 +17,37 @@ var USAGE = `vorn-connector <command> <module> [options]
15
17
  Commands:
16
18
  manifest <module> Print the connector manifest as JSON
17
19
  setup <module> [trigger] Print the Vorn connection settings to paste
20
+ check <module> Verify the connector against Vorn's contract
18
21
  poll <module> <trigger> Run one poll against the current environment
19
22
  serve <module> Serve the connector on stdio (what Vorn runs)
20
23
 
21
24
  Options:
22
25
  --since <iso> Lower bound passed to poll
23
- --limit <n> Maximum items to request`;
24
- function parseFlags(args) {
26
+ --limit <n> Maximum items to request
27
+ --live Let check poll for real using the environment`;
28
+ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set(["live"]);
29
+ function parseArgs(args) {
25
30
  const flags = {};
31
+ const positional = [];
26
32
  for (let index = 0; index < args.length; index++) {
27
33
  const arg = args[index];
28
- if (!arg.startsWith("--")) continue;
34
+ if (!arg.startsWith("--")) {
35
+ positional.push(arg);
36
+ continue;
37
+ }
38
+ const name = arg.slice(2);
39
+ if (BOOLEAN_FLAGS.has(name)) {
40
+ flags[name] = "true";
41
+ continue;
42
+ }
29
43
  const value = args[index + 1];
30
44
  if (value === void 0 || value.startsWith("--")) {
31
45
  throw new Error(`Missing value for ${arg}`);
32
46
  }
33
- flags[arg.slice(2)] = value;
47
+ flags[name] = value;
34
48
  index++;
35
49
  }
36
- return flags;
50
+ return { flags, positional };
37
51
  }
38
52
  function pickConnector(loaded, modulePath) {
39
53
  const module = loaded;
@@ -59,8 +73,7 @@ ${USAGE}`);
59
73
  return 1;
60
74
  }
61
75
  const connector = pickConnector(await deps.load(modulePath), modulePath);
62
- const positional = rest.filter((arg) => !arg.startsWith("--"));
63
- const flags = parseFlags(rest);
76
+ const { flags, positional } = parseArgs(rest);
64
77
  switch (command) {
65
78
  case "manifest":
66
79
  deps.write(JSON.stringify(connectorManifest(connector), null, 2));
@@ -81,6 +94,22 @@ ${USAGE}`);
81
94
  }
82
95
  return 0;
83
96
  }
97
+ case "check": {
98
+ const findings = await checkConnector(connector, {
99
+ ...flags.live === "true" && {
100
+ live: true,
101
+ config: resolveConfig(connector, deps.env ?? process.env)
102
+ }
103
+ });
104
+ const errors = findings.filter((item) => item.level === "error");
105
+ if (findings.length > 0) deps.write(formatFindings(findings));
106
+ deps.write(
107
+ errors.length > 0 ? `
108
+ ${errors.length} error(s), ${findings.length - errors.length} warning(s)` : `
109
+ ${connector.id} passed with ${findings.length} warning(s)`
110
+ );
111
+ return errors.length > 0 ? 1 : 0;
112
+ }
84
113
  case "poll": {
85
114
  const triggerType = positional[0];
86
115
  if (!triggerType) {
package/dist/index.d.ts CHANGED
@@ -72,13 +72,74 @@ interface PollOutcome {
72
72
  nextCursor?: string;
73
73
  hasMore?: boolean;
74
74
  }
75
- interface TriggerDefinition {
75
+ /**
76
+ * How the SDK decides which fetched items are new.
77
+ *
78
+ * - `timestamp` — for sources that expose a reliable "last changed" field and
79
+ * can filter on it. Handles the boundary case where several items share the
80
+ * newest timestamp, which is the classic source of both duplicates and
81
+ * silently dropped items.
82
+ * - `lastItem` — for feeds that return newest-first with no dependable
83
+ * timestamp. The cursor is the newest id already delivered.
84
+ */
85
+ type DedupeStrategy = 'timestamp' | 'lastItem';
86
+ /**
87
+ * What a declarative trigger's `fetch` receives. Deliberately smaller than
88
+ * {@link PollContext}: cursor encoding, ordering, windowing and de-duplication
89
+ * are the SDK's job, so the author only has to answer "what is there now?".
90
+ */
91
+ interface FetchContext {
92
+ config: ConnectorConfig;
93
+ /**
94
+ * With `dedupe: 'timestamp'`, everything changed at or after this instant is
95
+ * worth returning. Absent on the very first poll. Returning a little too
96
+ * much is safe — the SDK drops what was already delivered.
97
+ */
98
+ since?: string;
99
+ /**
100
+ * With `dedupe: 'lastItem'`, the newest id already delivered. Absent on the
101
+ * very first poll. Return the feed newest-first and the SDK will stop there.
102
+ */
103
+ lastItemId?: string;
104
+ /** Upper bound on items worth returning in one call. */
105
+ limit?: number;
106
+ /** Injectable clock so tests are deterministic. */
107
+ now(): string;
108
+ }
109
+ interface TriggerBase {
76
110
  /** Event key, e.g. `workItemCreated`. Becomes the `poll_<type>` MCP tool. */
77
111
  type: string;
78
112
  label: string;
79
113
  description?: string;
80
- poll(context: PollContext): Promise<PollOutcome> | PollOutcome;
114
+ /**
115
+ * Representative items. `vorn-connector check` replays these through the
116
+ * real dedupe pipeline, so a connector can be verified before anyone has
117
+ * credentials for it.
118
+ */
119
+ sample?: ConnectorItem[];
81
120
  }
121
+ /**
122
+ * A trigger is either declarative or hand-written, never both — expressed as a
123
+ * union so the invalid combinations are a type error at authoring time rather
124
+ * than a throw when the connector is first loaded.
125
+ */
126
+ type TriggerDefinition = TriggerBase & ({
127
+ /**
128
+ * Declarative polling: return what the source has and let the SDK
129
+ * handle cursors and de-duplication.
130
+ */
131
+ dedupe: DedupeStrategy;
132
+ fetch(context: FetchContext): Promise<ConnectorItem[]> | ConnectorItem[];
133
+ poll?: never;
134
+ } | {
135
+ /**
136
+ * Full control over cursors and paging. Use only when the source's
137
+ * paging cannot be expressed as "give me everything since X".
138
+ */
139
+ poll(context: PollContext): Promise<PollOutcome> | PollOutcome;
140
+ dedupe?: never;
141
+ fetch?: never;
142
+ });
82
143
  interface ActionInputField {
83
144
  key: string;
84
145
  label: string;
@@ -105,16 +166,37 @@ interface ActionDefinition {
105
166
  type: string;
106
167
  label: string;
107
168
  description?: string;
169
+ /**
170
+ * Whether repeating the call with the same arguments is safe. Surfaced in
171
+ * the MCP tool description, because an agent retrying a failed step has no
172
+ * other way to know whether it is about to create a second issue.
173
+ */
174
+ idempotent?: boolean;
108
175
  inputs?: ActionInputField[];
109
176
  outputs?: ActionOutputField[];
110
177
  run(args: Record<string, unknown>, context: ActionContext): Promise<Record<string, unknown> | void> | Record<string, unknown> | void;
111
178
  }
179
+ /**
180
+ * A connector's own glyph, so an installed connector is recognizable in a list
181
+ * rather than sharing one generic icon with every other one.
182
+ *
183
+ * Path data only — deliberately not markup. Vorn draws these itself as
184
+ * `<path d="...">` inside an `<svg>` it owns, so a connector cannot inject
185
+ * elements, scripts or external references into the app rendering it.
186
+ */
187
+ interface ConnectorIcon {
188
+ /** Defaults to `0 0 24 24`. */
189
+ viewBox?: string;
190
+ /** SVG path `d` data, drawn with `fill="currentColor"` so it inherits color. */
191
+ paths: string[];
192
+ }
112
193
  interface ConnectorDefinition {
113
194
  /** Stable connector id, e.g. `azure-devops`. */
114
195
  id: string;
115
196
  name: string;
116
197
  version?: string;
117
198
  description?: string;
199
+ icon?: ConnectorIcon;
118
200
  config?: ConnectorConfigField[];
119
201
  triggers?: TriggerDefinition[];
120
202
  actions?: ActionDefinition[];
@@ -145,6 +227,47 @@ declare function defineConnector(definition: ConnectorDefinition): Connector;
145
227
  */
146
228
  declare function resolveConfig(connector: Connector, env?: NodeJS.ProcessEnv): ConnectorConfig;
147
229
 
230
+ interface CheckFinding {
231
+ /** `error` means the connector will misbehave in Vorn; `warn` is advisory. */
232
+ level: 'error' | 'warn';
233
+ code: string;
234
+ /** Which part of the connector the finding is about. */
235
+ target: string;
236
+ message: string;
237
+ }
238
+ interface CheckOptions {
239
+ /**
240
+ * Poll every trigger against the real source. Off by default, so a check
241
+ * runs on declared `sample` items and the definition alone.
242
+ */
243
+ live?: boolean;
244
+ /** Credentials, required by `live`. */
245
+ config?: ConnectorConfig;
246
+ now?: () => string;
247
+ }
248
+ /**
249
+ * Check a connector against the contract Vorn relies on.
250
+ *
251
+ * The point is a feedback loop: a connector — hand-written or generated — can
252
+ * be verified before it is ever installed, catching the failures that are
253
+ * otherwise invisible until duplicate tasks show up in someone's inbox days
254
+ * later.
255
+ */
256
+ declare function checkConnector(connector: Connector, options?: CheckOptions): Promise<CheckFinding[]>;
257
+ /** Render findings for a terminal. Returns an empty string when all clear. */
258
+ declare function formatFindings(findings: CheckFinding[]): string;
259
+
260
+ /**
261
+ * Run a declarative trigger: call the author's `fetch`, then apply the chosen
262
+ * dedupe strategy.
263
+ *
264
+ * This exists because cursor bookkeeping is where hand-written pull connectors
265
+ * go wrong — duplicate deliveries, items lost at the timestamp boundary, and
266
+ * cursors that never advance. Solving it once here means every connector, and
267
+ * every connector an agent generates, inherits the fix.
268
+ */
269
+ declare function pollWithDedupe(trigger: TriggerDefinition, context: PollContext): Promise<PollOutcome>;
270
+
148
271
  /**
149
272
  * Turn an author-supplied item into the flat JSON shape Vorn consumes.
150
273
  *
@@ -211,6 +334,8 @@ interface ConnectionSetup {
211
334
  timestampField: 'updatedAt';
212
335
  titleField: 'title';
213
336
  urlField: 'url';
337
+ cursorArg: 'cursor';
338
+ cursorPath: 'nextCursor';
214
339
  };
215
340
  /** Environment variable names the connector reads. */
216
341
  env: Array<{
@@ -225,7 +350,9 @@ interface ConnectionSetup {
225
350
  *
226
351
  * Every SDK connector normalizes to the same field names, so this mapping is
227
352
  * fixed; it is generated rather than documented so a rename in the SDK cannot
228
- * drift away from the setup instructions users copy.
353
+ * drift away from the setup instructions users copy. `cursorArg` hands the
354
+ * connector back its own cursor each poll, which is what lets its dedupe
355
+ * strategy — rather than Vorn's timestamp comparison — decide what is new.
229
356
  */
230
357
  declare function connectionSetup(connector: Connector, triggerType: string): ConnectionSetup;
231
358
  interface ConnectorManifest {
@@ -233,6 +360,7 @@ interface ConnectorManifest {
233
360
  name: string;
234
361
  version: string;
235
362
  description?: string;
363
+ icon?: ConnectorIcon;
236
364
  triggers: Array<{
237
365
  type: string;
238
366
  label: string;
@@ -295,4 +423,4 @@ interface ConnectorHarness {
295
423
  */
296
424
  declare function createConnectorHarness(connector: Connector, harnessOptions?: HarnessOptions): ConnectorHarness;
297
425
 
298
- export { type ActionContext, type ActionDefinition, type ActionInputField, type ConnectionSetup, type Connector, type ConnectorConfig, type ConnectorConfigField, type ConnectorDefinition, type ConnectorHarness, type ConnectorItem, type ConnectorManifest, type ConnectorServerOptions, type HarnessOptions, MANIFEST_TOOL, MAX_POLL_PAGES, type NormalizedItem, type PollContext, type PollOutcome, type PollPage, type RunActionOptions, type RunPollOptions, type TriggerDefinition, connectionSetup, connectorManifest, createConnectorHarness, createConnectorServer, defineConnector, drainPoll, envNameFor, normalizeItem, normalizeItems, pollToolName, resolveConfig, runAction, runPoll, serveConnector };
426
+ export { type ActionContext, type ActionDefinition, type ActionInputField, type CheckFinding, type CheckOptions, type ConnectionSetup, type Connector, type ConnectorConfig, type ConnectorConfigField, type ConnectorDefinition, type ConnectorHarness, type ConnectorIcon, type ConnectorItem, type ConnectorManifest, type ConnectorServerOptions, type DedupeStrategy, type FetchContext, type HarnessOptions, MANIFEST_TOOL, MAX_POLL_PAGES, type NormalizedItem, type PollContext, type PollOutcome, type PollPage, type RunActionOptions, type RunPollOptions, type TriggerDefinition, checkConnector, connectionSetup, connectorManifest, createConnectorHarness, createConnectorServer, defineConnector, drainPoll, envNameFor, formatFindings, normalizeItem, normalizeItems, pollToolName, pollWithDedupe, resolveConfig, runAction, runPoll, serveConnector };
package/dist/index.js CHANGED
@@ -1,20 +1,23 @@
1
1
  import {
2
2
  MANIFEST_TOOL,
3
3
  MAX_POLL_PAGES,
4
+ checkConnector,
4
5
  connectionSetup,
5
6
  connectorManifest,
6
7
  createConnectorServer,
7
8
  defineConnector,
8
9
  drainPoll,
9
10
  envNameFor,
11
+ formatFindings,
10
12
  normalizeItem,
11
13
  normalizeItems,
12
14
  pollToolName,
15
+ pollWithDedupe,
13
16
  resolveConfig,
14
17
  runAction,
15
18
  runPoll,
16
19
  serveConnector
17
- } from "./chunk-UANUTYUV.js";
20
+ } from "./chunk-W4GGTEUK.js";
18
21
 
19
22
  // src/harness.ts
20
23
  function createConnectorHarness(connector, harnessOptions = {}) {
@@ -49,6 +52,7 @@ function createConnectorHarness(connector, harnessOptions = {}) {
49
52
  export {
50
53
  MANIFEST_TOOL,
51
54
  MAX_POLL_PAGES,
55
+ checkConnector,
52
56
  connectionSetup,
53
57
  connectorManifest,
54
58
  createConnectorHarness,
@@ -56,9 +60,11 @@ export {
56
60
  defineConnector,
57
61
  drainPoll,
58
62
  envNameFor,
63
+ formatFindings,
59
64
  normalizeItem,
60
65
  normalizeItems,
61
66
  pollToolName,
67
+ pollWithDedupe,
62
68
  resolveConfig,
63
69
  runAction,
64
70
  runPoll,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vornrun/connector-sdk",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Build and share Vorn pull connectors as ordinary npm packages",
5
5
  "type": "module",
6
6
  "license": "MIT",