@vornrun/connector-sdk 0.7.0-beta.13 → 0.7.0-beta.15
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 +118 -2
- package/dist/{packaging-DFTuP4fr.d.ts → check-Cv6UNBA4.d.ts} +292 -75
- package/dist/{chunk-XOP6JKBX.js → chunk-FHGC7LE7.js} +2061 -945
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +9 -6
- package/dist/index.d.ts +51 -4
- package/dist/index.js +23 -1
- package/package.json +2 -2
|
@@ -1,1017 +1,1633 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
1
|
+
// src/define.ts
|
|
2
|
+
var KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
3
|
+
var PATH_DATA_PATTERN = /^[MmZzLlHhVvCcSsQqTtAa0-9\s,.\-+eE]+$/;
|
|
4
|
+
var VIEW_BOX_PATTERN = /^-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+$/;
|
|
5
|
+
var DEDUPE_STRATEGIES = ["timestamp", "lastItem"];
|
|
6
|
+
var AUTH_RUNGS = ["none", "cli", "key", "oauth"];
|
|
7
|
+
var EXTENSION_PERMISSIONS = [
|
|
8
|
+
"git.read",
|
|
9
|
+
"terminal.read",
|
|
10
|
+
"terminal.selection",
|
|
11
|
+
"terminal.send",
|
|
12
|
+
"card.rename",
|
|
13
|
+
"agent.usage"
|
|
14
14
|
];
|
|
15
|
-
var
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return JSON.parse(readFileSync(join(current, "package.json"), "utf8"));
|
|
58
|
-
} catch {
|
|
59
|
-
const parent = dirname(current);
|
|
60
|
-
if (parent === current) return void 0;
|
|
61
|
-
current = parent;
|
|
15
|
+
var HOST_PERMISSIONS = {
|
|
16
|
+
diff: "git.read",
|
|
17
|
+
status: "git.read",
|
|
18
|
+
output: "terminal.read",
|
|
19
|
+
selection: "terminal.selection",
|
|
20
|
+
send: "terminal.send",
|
|
21
|
+
rename: "card.rename",
|
|
22
|
+
usage: "agent.usage"
|
|
23
|
+
};
|
|
24
|
+
var EXTENSION_AGENTS = [
|
|
25
|
+
"claude",
|
|
26
|
+
"copilot",
|
|
27
|
+
"codex",
|
|
28
|
+
"opencode",
|
|
29
|
+
"gemini",
|
|
30
|
+
"shell"
|
|
31
|
+
];
|
|
32
|
+
var EXTENSION_PLATFORMS = ["darwin", "linux", "win32"];
|
|
33
|
+
var WEB_ENTRY_PATTERN = /^web\/[A-Za-z0-9._/-]+\.html$/;
|
|
34
|
+
var MIN_FOOTER_SECONDS = 5;
|
|
35
|
+
var MAX_PATTERN_LENGTH = 256;
|
|
36
|
+
var GROUP_OPEN = /^\((\?(:|=|!|<=|<!|<[A-Za-z_$][\w$]*>))?/;
|
|
37
|
+
function hasNestedQuantifier(pattern) {
|
|
38
|
+
const quantifierAt = (at) => {
|
|
39
|
+
const ch = pattern[at];
|
|
40
|
+
return ch !== void 0 && "*+?{".includes(ch);
|
|
41
|
+
};
|
|
42
|
+
const quantified = [];
|
|
43
|
+
let inClass = false;
|
|
44
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
45
|
+
const ch = pattern[i];
|
|
46
|
+
if (ch === "\\") {
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (inClass) {
|
|
51
|
+
if (ch === "]") inClass = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (ch === "[") {
|
|
55
|
+
inClass = true;
|
|
56
|
+
continue;
|
|
62
57
|
}
|
|
58
|
+
if (ch === "(") {
|
|
59
|
+
quantified.push(false);
|
|
60
|
+
i += (GROUP_OPEN.exec(pattern.slice(i))?.[0].length ?? 1) - 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (ch === ")") {
|
|
64
|
+
const heldOne = quantified.pop() ?? false;
|
|
65
|
+
const repeated = quantifierAt(i + 1);
|
|
66
|
+
if (heldOne && repeated) return true;
|
|
67
|
+
if ((heldOne || repeated) && quantified.length > 0) {
|
|
68
|
+
quantified[quantified.length - 1] = true;
|
|
69
|
+
}
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (quantifierAt(i) && quantified.length > 0) quantified[quantified.length - 1] = true;
|
|
63
73
|
}
|
|
74
|
+
return false;
|
|
64
75
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
""
|
|
73
|
-
].join("\n");
|
|
74
|
-
}
|
|
75
|
-
async function esbuildBundle(request) {
|
|
76
|
-
const { build } = await import("esbuild");
|
|
77
|
-
const result = await build({
|
|
78
|
-
stdin: {
|
|
79
|
-
contents: request.contents,
|
|
80
|
-
resolveDir: request.resolveDir,
|
|
81
|
-
sourcefile: "vorn-connector-pack.js",
|
|
82
|
-
loader: "js"
|
|
83
|
-
},
|
|
84
|
-
bundle: true,
|
|
85
|
-
platform: "node",
|
|
86
|
-
target: "node20",
|
|
87
|
-
format: "esm",
|
|
88
|
-
write: false,
|
|
89
|
-
metafile: true,
|
|
90
|
-
legalComments: "none"
|
|
91
|
-
});
|
|
92
|
-
const output = Object.values(result.metafile.outputs)[0];
|
|
93
|
-
return {
|
|
94
|
-
code: result.outputFiles[0].text,
|
|
95
|
-
external: (output?.imports ?? []).filter((item) => item.external).map((item) => item.path)
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// src/normalize.ts
|
|
100
|
-
var RESERVED_KEYS = [
|
|
101
|
-
"externalId",
|
|
102
|
-
"title",
|
|
103
|
-
"url",
|
|
104
|
-
"description",
|
|
105
|
-
"status",
|
|
106
|
-
"labels",
|
|
107
|
-
"assignee",
|
|
108
|
-
"updatedAt"
|
|
109
|
-
];
|
|
110
|
-
var UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
|
|
111
|
-
function itemExternalId(item) {
|
|
112
|
-
return String(item.externalId ?? "").trim();
|
|
113
|
-
}
|
|
114
|
-
function itemTimestamp(item, fallback) {
|
|
115
|
-
return isoTimestamp(item.updatedAt, fallback);
|
|
116
|
-
}
|
|
117
|
-
function isoTimestamp(value, fallback) {
|
|
118
|
-
if (value === void 0) return fallback;
|
|
119
|
-
const date = value instanceof Date ? value : new Date(value);
|
|
120
|
-
if (Number.isNaN(date.getTime())) {
|
|
121
|
-
throw new Error(`Invalid updatedAt: ${String(value)}`);
|
|
76
|
+
var ABSOLUTE_URL_PATTERN = /^https?:\/\//i;
|
|
77
|
+
var CONFIG_ROOTED_URL_PATTERN = /^\{\{\s*config\./;
|
|
78
|
+
function assertUnique(kind, keys) {
|
|
79
|
+
const seen = /* @__PURE__ */ new Set();
|
|
80
|
+
for (const key of keys) {
|
|
81
|
+
if (seen.has(key)) throw new Error(`Duplicate ${kind} "${key}"`);
|
|
82
|
+
seen.add(key);
|
|
122
83
|
}
|
|
123
|
-
return date.toISOString();
|
|
124
84
|
}
|
|
125
|
-
function
|
|
126
|
-
const
|
|
127
|
-
if (!
|
|
128
|
-
|
|
85
|
+
function assertAuth(definition) {
|
|
86
|
+
const auth = definition.auth;
|
|
87
|
+
if (!auth) return;
|
|
88
|
+
const id = definition.id;
|
|
89
|
+
if (!AUTH_RUNGS.includes(auth.rung)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Connector ${id} declares unknown auth rung ${JSON.stringify(auth.rung)}; expected ${AUTH_RUNGS.join(", ")}`
|
|
92
|
+
);
|
|
129
93
|
}
|
|
130
|
-
if (
|
|
131
|
-
throw new Error(`Connector
|
|
94
|
+
if (auth.rung === "cli" && !auth.probe?.command?.trim()) {
|
|
95
|
+
throw new Error(`Connector ${id} borrows a CLI login but declares no probe command to ask it`);
|
|
132
96
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
97
|
+
if (auth.rung === "key") {
|
|
98
|
+
const keys = auth.keys ?? [];
|
|
99
|
+
if (keys.length === 0) {
|
|
100
|
+
throw new Error(`Connector ${id} signs in with a key but names no config field holding it`);
|
|
101
|
+
}
|
|
102
|
+
const declared = new Set((definition.config ?? []).map((field) => field.key));
|
|
103
|
+
for (const key of keys) {
|
|
104
|
+
if (!declared.has(key)) {
|
|
105
|
+
throw new Error(`Connector ${id} names auth key "${key}", which is not a config field`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
138
108
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
status: item.status ?? "open",
|
|
146
|
-
labels: item.labels ?? [],
|
|
147
|
-
...item.assignee !== void 0 && { assignee: item.assignee },
|
|
148
|
-
updatedAt: isoTimestamp(item.updatedAt, polledAt)
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
function normalizeItems(items, polledAt) {
|
|
152
|
-
const seen = /* @__PURE__ */ new Set();
|
|
153
|
-
return items.map((item) => {
|
|
154
|
-
const normalized = normalizeItem(item, polledAt);
|
|
155
|
-
if (seen.has(normalized.externalId)) {
|
|
156
|
-
throw new Error(`Duplicate externalId "${normalized.externalId}" in one poll page`);
|
|
109
|
+
if (auth.rung === "none") {
|
|
110
|
+
const secret = (definition.config ?? []).find((field) => field.secret === true);
|
|
111
|
+
if (secret) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Connector ${id} claims it needs no sign-in but declares secret field "${secret.key}"`
|
|
114
|
+
);
|
|
157
115
|
}
|
|
158
|
-
|
|
159
|
-
return normalized;
|
|
160
|
-
});
|
|
116
|
+
}
|
|
161
117
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
function decodeCursor(cursor, strategy) {
|
|
166
|
-
if (!cursor) return void 0;
|
|
167
|
-
let parsed;
|
|
168
|
-
try {
|
|
169
|
-
parsed = JSON.parse(cursor);
|
|
170
|
-
} catch (error) {
|
|
171
|
-
throw new Error(`Cursor is not valid SDK cursor JSON: ${cursor}`, { cause: error });
|
|
118
|
+
function assertIdentity(kind, definition) {
|
|
119
|
+
if (!KEY_PATTERN.test(definition.id ?? "")) {
|
|
120
|
+
throw new Error(`${kind} id "${definition.id}" must start with a letter and be url-safe`);
|
|
172
121
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
throw new Error(`Cursor does not belong to the "${strategy}" strategy: ${cursor}`);
|
|
122
|
+
if (!definition.name?.trim()) {
|
|
123
|
+
throw new Error(`${kind} ${definition.id} is missing a name`);
|
|
176
124
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
125
|
+
assertIcon(`${kind} ${definition.id}`, definition.icon);
|
|
126
|
+
}
|
|
127
|
+
function assertIcon(subject, icon) {
|
|
128
|
+
if (!icon) return;
|
|
129
|
+
const { viewBox, paths } = icon;
|
|
130
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
131
|
+
throw new Error(`${subject} has an icon with no paths`);
|
|
132
|
+
}
|
|
133
|
+
for (const path of paths) {
|
|
134
|
+
if (typeof path !== "string" || !PATH_DATA_PATTERN.test(path)) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`${subject} has an icon path that is not SVG path data. Only path data is accepted, not markup.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (viewBox !== void 0 && !VIEW_BOX_PATTERN.test(viewBox)) {
|
|
141
|
+
throw new Error(`${subject} has an icon viewBox that is not four numbers`);
|
|
180
142
|
}
|
|
181
|
-
return state;
|
|
182
143
|
}
|
|
183
|
-
function
|
|
184
|
-
if (
|
|
185
|
-
return
|
|
144
|
+
function envNameFor(key, explicit) {
|
|
145
|
+
if (explicit) return explicit;
|
|
146
|
+
return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
|
|
186
147
|
}
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
148
|
+
function defineConnector(definition) {
|
|
149
|
+
assertIdentity("Connector", definition);
|
|
150
|
+
const triggers = definition.triggers ?? [];
|
|
151
|
+
const actions = definition.actions ?? [];
|
|
152
|
+
if (triggers.length === 0 && actions.length === 0) {
|
|
153
|
+
throw new Error(`Connector ${definition.id} declares no triggers and no actions`);
|
|
191
154
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
155
|
+
for (const trigger of triggers) {
|
|
156
|
+
if (!KEY_PATTERN.test(trigger.type ?? "")) {
|
|
157
|
+
throw new Error(`Trigger type "${trigger.type}" must start with a letter and be url-safe`);
|
|
158
|
+
}
|
|
159
|
+
const loose = trigger;
|
|
160
|
+
const declarative = typeof loose.fetch === "function";
|
|
161
|
+
const imperative = typeof loose.poll === "function";
|
|
162
|
+
if (declarative && imperative) {
|
|
163
|
+
throw new Error(`Trigger ${trigger.type} declares both fetch() and poll(); pick one`);
|
|
164
|
+
}
|
|
165
|
+
if (declarative !== (loose.dedupe !== void 0)) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Trigger ${trigger.type} needs fetch() and a dedupe strategy together, not one alone`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (loose.dedupe !== void 0 && !DEDUPE_STRATEGIES.includes(loose.dedupe)) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Trigger ${trigger.type} has unknown dedupe strategy ${JSON.stringify(loose.dedupe)}; expected ${DEDUPE_STRATEGIES.join(" or ")}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
if (loose.poll !== void 0 && !imperative) {
|
|
176
|
+
throw new Error(`Trigger ${trigger.type} declares poll but it is not a function`);
|
|
177
|
+
}
|
|
178
|
+
if (!declarative && !imperative) {
|
|
179
|
+
throw new Error(`Trigger ${trigger.type} is missing a fetch() or poll() implementation`);
|
|
180
|
+
}
|
|
212
181
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const atNewest = [];
|
|
217
|
-
for (let i = delivered.length - 1; i >= 0 && delivered[i].at === newest; i -= 1) {
|
|
218
|
-
atNewest.push(delivered[i].id);
|
|
182
|
+
for (const action of actions) {
|
|
183
|
+
if (!KEY_PATTERN.test(action.type ?? "")) {
|
|
184
|
+
throw new Error(`Action type "${action.type}" must start with a letter and be url-safe`);
|
|
219
185
|
}
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
186
|
+
const loose = action;
|
|
187
|
+
const written = typeof loose.run === "function";
|
|
188
|
+
const declared = loose.request !== void 0;
|
|
189
|
+
if (written && declared) {
|
|
190
|
+
throw new Error(`Action ${action.type} declares both run() and a request; pick one`);
|
|
191
|
+
}
|
|
192
|
+
if (!written && !declared) {
|
|
193
|
+
throw new Error(`Action ${action.type} is missing a run() implementation or a request`);
|
|
194
|
+
}
|
|
195
|
+
if (declared) {
|
|
196
|
+
const request = loose.request;
|
|
197
|
+
if (typeof request?.url !== "string" || request.url.trim() === "") {
|
|
198
|
+
throw new Error(`Action ${action.type} declares a request with no URL`);
|
|
199
|
+
}
|
|
200
|
+
const url = request.url.trim();
|
|
201
|
+
if (!ABSOLUTE_URL_PATTERN.test(url) && !CONFIG_ROOTED_URL_PATTERN.test(url)) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`Action ${action.type} declares the request URL "${url}", which is neither absolute nor rooted in a {{config.\u2026}} value`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (!declared && loose.postReceive !== void 0) {
|
|
208
|
+
throw new Error(`Action ${action.type} has postReceive but no request for it to reshape`);
|
|
209
|
+
}
|
|
210
|
+
for (const input of action.inputs ?? []) {
|
|
211
|
+
if (input.loadOptions !== void 0 && definition.options?.[input.loadOptions] === void 0) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Action ${action.type} argument "${input.key}" loads options from "${input.loadOptions}", which the connector does not serve`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
assertUnique(
|
|
219
|
+
"trigger",
|
|
220
|
+
triggers.map((trigger) => trigger.type)
|
|
221
|
+
);
|
|
222
|
+
assertUnique(
|
|
223
|
+
"action",
|
|
224
|
+
actions.map((action) => action.type)
|
|
225
|
+
);
|
|
226
|
+
assertUnique(
|
|
227
|
+
"config field",
|
|
228
|
+
(definition.config ?? []).map((field) => field.key)
|
|
229
|
+
);
|
|
230
|
+
assertAuth(definition);
|
|
231
|
+
return {
|
|
232
|
+
...definition,
|
|
233
|
+
kind: "connector",
|
|
234
|
+
version: definition.version ?? "0.0.0",
|
|
235
|
+
config: definition.config ?? [],
|
|
236
|
+
triggers,
|
|
237
|
+
actions
|
|
238
|
+
};
|
|
241
239
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
240
|
+
function assertPredicate(id, where, predicate) {
|
|
241
|
+
if (!predicate) return;
|
|
242
|
+
const lists = [
|
|
243
|
+
["workspaceContains", predicate.workspaceContains],
|
|
244
|
+
["remoteHost", predicate.remoteHost],
|
|
245
|
+
["agent", predicate.agent],
|
|
246
|
+
["platform", predicate.platform]
|
|
247
|
+
];
|
|
248
|
+
for (const [field, value] of lists) {
|
|
249
|
+
if (value === void 0) continue;
|
|
250
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
251
|
+
throw new Error(`Extension ${id} ${where} declares "${field}" with nothing in it`);
|
|
252
|
+
}
|
|
253
|
+
for (const entry of value) {
|
|
254
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
255
|
+
throw new Error(`Extension ${id} ${where} declares an empty "${field}" value`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
247
258
|
}
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
...context.limit !== void 0 && { limit: context.limit },
|
|
255
|
-
now: context.now,
|
|
256
|
-
fetch: context.fetch
|
|
257
|
-
});
|
|
258
|
-
return lastItemPoll(fetched2, state2, context, polledAt);
|
|
259
|
+
for (const glob of predicate.workspaceContains ?? []) {
|
|
260
|
+
if (glob.startsWith("/") || glob.split("/").includes("..")) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`Extension ${id} ${where} looks for "${glob}", which is not inside the worktree`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
259
265
|
}
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
now: context.now,
|
|
267
|
-
fetch: context.fetch
|
|
268
|
-
});
|
|
269
|
-
return timestampPoll(fetched, state, context, polledAt);
|
|
270
|
-
}
|
|
271
|
-
async function runFetch(type, fetchItems, context) {
|
|
272
|
-
const fetched = await fetchItems(context);
|
|
273
|
-
if (!Array.isArray(fetched)) {
|
|
274
|
-
throw new Error(`Trigger ${type} fetch() did not return an array`);
|
|
266
|
+
for (const agent of predicate.agent ?? []) {
|
|
267
|
+
if (!EXTENSION_AGENTS.includes(agent)) {
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Extension ${id} ${where} names unknown agent ${JSON.stringify(agent)}; expected ${EXTENSION_AGENTS.join(", ")}`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
275
272
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
function isRecord(value) {
|
|
282
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
283
|
-
}
|
|
284
|
-
function segments(path) {
|
|
285
|
-
return path.split(".").map((part) => part.trim()).filter((part) => part !== "");
|
|
286
|
-
}
|
|
287
|
-
function valueAt(value, path) {
|
|
288
|
-
let current = value;
|
|
289
|
-
for (const key of segments(path)) {
|
|
290
|
-
if (UNSAFE_KEYS2.has(key)) return void 0;
|
|
291
|
-
if (Array.isArray(current)) {
|
|
292
|
-
const index = Number(key);
|
|
293
|
-
if (!Number.isInteger(index)) return void 0;
|
|
294
|
-
current = current[index];
|
|
295
|
-
continue;
|
|
273
|
+
for (const platform of predicate.platform ?? []) {
|
|
274
|
+
if (!EXTENSION_PLATFORMS.includes(platform)) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Extension ${id} ${where} names unknown platform ${JSON.stringify(platform)}; expected ${EXTENSION_PLATFORMS.join(", ")}`
|
|
277
|
+
);
|
|
296
278
|
}
|
|
297
|
-
if (!isRecord(current)) return void 0;
|
|
298
|
-
current = Object.prototype.hasOwnProperty.call(current, key) ? current[key] : void 0;
|
|
299
279
|
}
|
|
300
|
-
return current;
|
|
301
280
|
}
|
|
302
|
-
function
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
if (!Number.isInteger(index)) return value;
|
|
310
|
-
const copy = [...value];
|
|
311
|
-
copy[index] = rest.length === 0 ? next : withValueAt(copy[index], rest.join("."), next);
|
|
312
|
-
return copy;
|
|
281
|
+
function assertPane(id, pane) {
|
|
282
|
+
assertIcon(`Extension ${id} pane ${pane.id}`, pane.icon);
|
|
283
|
+
const loose = pane;
|
|
284
|
+
const page2 = loose.web !== void 0;
|
|
285
|
+
const program = loose.command !== void 0;
|
|
286
|
+
if (page2 && program) {
|
|
287
|
+
throw new Error(`Extension ${id} pane ${pane.id} declares both a web page and a command`);
|
|
313
288
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
...base,
|
|
317
|
-
[head]: rest.length === 0 ? next : withValueAt(base[head], rest.join("."), next)
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
function pick(value, keys) {
|
|
321
|
-
if (Array.isArray(value)) return value.map((entry) => pick(entry, keys));
|
|
322
|
-
if (!isRecord(value)) return value;
|
|
323
|
-
const out = {};
|
|
324
|
-
for (const key of keys) {
|
|
325
|
-
if (UNSAFE_KEYS2.has(key)) continue;
|
|
326
|
-
if (Object.prototype.hasOwnProperty.call(value, key)) out[key] = value[key];
|
|
289
|
+
if (!page2 && !program) {
|
|
290
|
+
throw new Error(`Extension ${id} pane ${pane.id} declares neither a web page nor a command`);
|
|
327
291
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
337
|
-
if (key === from) out[to] = entry;
|
|
338
|
-
else if (key !== to) out[key] = entry;
|
|
292
|
+
if (page2) {
|
|
293
|
+
const web = loose.web;
|
|
294
|
+
if (typeof web !== "string" || !WEB_ENTRY_PATTERN.test(web) || web.split("/").includes("..")) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Extension ${id} pane ${pane.id} declares the page ${JSON.stringify(web)}; a page is an .html file under web/ in the package`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
339
300
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (op.op === "flatten") return valueAt(value, op.path);
|
|
344
|
-
const target = op.path === void 0 ? value : valueAt(value, op.path);
|
|
345
|
-
if (op.path !== void 0 && target === void 0) return value;
|
|
346
|
-
let next;
|
|
347
|
-
if (op.op === "pick") next = pick(target, op.keys);
|
|
348
|
-
else if (op.op === "rename") next = rename(target, op.from, op.to);
|
|
349
|
-
else if (op.op === "filter") {
|
|
350
|
-
next = Array.isArray(target) ? target.filter((entry) => isRecord(entry) && valueAt(entry, op.key) === op.equals) : target;
|
|
351
|
-
} else {
|
|
352
|
-
next = Array.isArray(target) ? target.map((entry) => applyPostReceive(entry, op.ops)) : target;
|
|
301
|
+
const command = loose.command;
|
|
302
|
+
if (!Array.isArray(command) || command.length === 0) {
|
|
303
|
+
throw new Error(`Extension ${id} pane ${pane.id} declares a command with nothing to run`);
|
|
353
304
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
return (ops ?? []).reduce(applyOp, value);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// src/request.ts
|
|
361
|
-
var MAX_ERROR_BODY = 500;
|
|
362
|
-
var PLACEHOLDER = /\{\{\s*(args|config)\.([A-Za-z0-9_.-]+)\s*\}\}/g;
|
|
363
|
-
var WHOLE_PLACEHOLDER = /^\{\{\s*(args|config)\.([A-Za-z0-9_.-]+)\s*\}\}$/;
|
|
364
|
-
function lookup(source, path, scope) {
|
|
365
|
-
return valueAt(source === "args" ? scope.args : scope.config, path);
|
|
366
|
-
}
|
|
367
|
-
var intoUrl = (value, source) => source === "config" ? value : encodeURIComponent(value);
|
|
368
|
-
function intoHeader(name) {
|
|
369
|
-
return (value) => {
|
|
370
|
-
if (/[\r\n]/.test(value)) {
|
|
371
|
-
throw new Error(`Header "${name}" would carry a line ending, which is not allowed`);
|
|
305
|
+
for (const arg of command) {
|
|
306
|
+
if (typeof arg !== "string" || arg === "") {
|
|
307
|
+
throw new Error(`Extension ${id} pane ${pane.id} declares a command with an empty argument`);
|
|
372
308
|
}
|
|
373
|
-
|
|
374
|
-
};
|
|
309
|
+
}
|
|
375
310
|
}
|
|
376
|
-
function
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
311
|
+
function defineExtension(definition) {
|
|
312
|
+
assertIdentity("Extension", definition);
|
|
313
|
+
const id = definition.id;
|
|
314
|
+
const panes = definition.panes ?? [];
|
|
315
|
+
const footers = definition.footers ?? [];
|
|
316
|
+
const linkHandlers = definition.linkHandlers ?? [];
|
|
317
|
+
if (panes.length === 0 && footers.length === 0 && linkHandlers.length === 0) {
|
|
318
|
+
throw new Error(`Extension ${id} contributes nothing`);
|
|
319
|
+
}
|
|
320
|
+
const permissions = definition.permissions ?? [];
|
|
321
|
+
if (!Array.isArray(permissions)) {
|
|
322
|
+
throw new Error(`Extension ${id} declares permissions that are not a list`);
|
|
323
|
+
}
|
|
324
|
+
for (const permission of permissions) {
|
|
325
|
+
if (!EXTENSION_PERMISSIONS.includes(permission)) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`Extension ${id} asks for unknown permission ${JSON.stringify(permission)}; expected ${EXTENSION_PERMISSIONS.join(", ")}`
|
|
328
|
+
);
|
|
383
329
|
}
|
|
384
|
-
return value.replace(PLACEHOLDER, (_match, source, path) => {
|
|
385
|
-
const resolved = lookup(source, path, scope);
|
|
386
|
-
if (resolved === void 0 || resolved === null) return "";
|
|
387
|
-
const text = String(resolved);
|
|
388
|
-
return substitute === void 0 ? text : substitute(text, source);
|
|
389
|
-
});
|
|
390
330
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
331
|
+
assertUnique("permission", permissions);
|
|
332
|
+
const contributions = [...panes, ...footers, ...linkHandlers];
|
|
333
|
+
for (const contribution of contributions) {
|
|
334
|
+
if (!KEY_PATTERN.test(contribution.id ?? "")) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`Contribution id "${contribution.id}" must start with a letter and be url-safe`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
if (!contribution.title?.trim()) {
|
|
340
|
+
throw new Error(`Extension ${id} contribution ${contribution.id} is missing a title`);
|
|
341
|
+
}
|
|
342
|
+
assertPredicate(id, `contribution ${contribution.id}`, contribution.when);
|
|
343
|
+
}
|
|
344
|
+
assertUnique(
|
|
345
|
+
"contribution",
|
|
346
|
+
contributions.map((contribution) => contribution.id)
|
|
347
|
+
);
|
|
348
|
+
assertPredicate(id, "activates", definition.activates);
|
|
349
|
+
for (const pane of panes) assertPane(id, pane);
|
|
350
|
+
for (const footer of footers) {
|
|
351
|
+
if (typeof footer.run !== "function") {
|
|
352
|
+
throw new Error(`Extension ${id} footer ${footer.id} is missing a run() implementation`);
|
|
353
|
+
}
|
|
354
|
+
if (!Number.isFinite(footer.every) || footer.every < MIN_FOOTER_SECONDS) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Extension ${id} footer ${footer.id} asks to run every ${footer.every}s; ${MIN_FOOTER_SECONDS}s is the shortest interval a footer may ask for`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
for (const handler of linkHandlers) {
|
|
361
|
+
if (typeof handler.run !== "function") {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Extension ${id} link handler ${handler.id} is missing a run() implementation`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
if (typeof handler.pattern !== "string" || handler.pattern.length > MAX_PATTERN_LENGTH) {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`Extension ${id} link handler ${handler.id} has a pattern longer than ${MAX_PATTERN_LENGTH} characters; it is matched on every click`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if (hasNestedQuantifier(handler.pattern)) {
|
|
372
|
+
throw new Error(
|
|
373
|
+
`Extension ${id} link handler ${handler.id} has a pattern that repeats a group which already repeats; matching it can take exponential time on a click`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
let matcher;
|
|
377
|
+
try {
|
|
378
|
+
matcher = new RegExp(handler.pattern);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
381
|
+
throw new Error(
|
|
382
|
+
`Extension ${id} link handler ${handler.id} has a pattern that is not a regular expression: ${reason}`,
|
|
383
|
+
{ cause: error }
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (typeof handler.example !== "string" || handler.example.trim() === "") {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Extension ${id} link handler ${handler.id} names no example link its pattern matches`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (!matcher.test(handler.example)) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`Extension ${id} link handler ${handler.id} has the example ${JSON.stringify(handler.example)}, which its own pattern ${JSON.stringify(handler.pattern)} does not match`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
id,
|
|
399
|
+
name: definition.name,
|
|
400
|
+
...definition.description !== void 0 && { description: definition.description },
|
|
401
|
+
...definition.icon !== void 0 && { icon: definition.icon },
|
|
402
|
+
kind: "extension",
|
|
403
|
+
version: definition.version ?? "0.0.0",
|
|
404
|
+
// Its credential is the host's own token, so there is nothing to sign in to.
|
|
405
|
+
auth: { rung: "none" },
|
|
406
|
+
config: [],
|
|
407
|
+
triggers: [],
|
|
408
|
+
actions: [],
|
|
409
|
+
permissions,
|
|
410
|
+
...definition.activates !== void 0 && { activates: definition.activates },
|
|
411
|
+
contributes: {
|
|
412
|
+
...panes.length > 0 && { panes },
|
|
413
|
+
...footers.length > 0 && { footers },
|
|
414
|
+
...linkHandlers.length > 0 && { linkHandlers }
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function resolveConfig(connector, env = process.env) {
|
|
419
|
+
const config = {};
|
|
420
|
+
const missing = [];
|
|
421
|
+
for (const field of connector.config) {
|
|
422
|
+
const name = envNameFor(field.key, field.env);
|
|
423
|
+
const value = env[name] ?? field.default;
|
|
424
|
+
if (value === void 0 || value === "") {
|
|
425
|
+
if (field.required) missing.push(`${field.key} (${name})`);
|
|
426
|
+
continue;
|
|
396
427
|
}
|
|
397
|
-
|
|
428
|
+
config[field.key] = value;
|
|
398
429
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
for (const [name, value] of Object.entries(raw ?? {})) {
|
|
404
|
-
const resolved = resolveTemplates(value, scope, intoHeader(name));
|
|
405
|
-
if (resolved === void 0 || resolved === null || resolved === "") continue;
|
|
406
|
-
out[name] = String(resolved);
|
|
430
|
+
if (missing.length > 0) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`Connector ${connector.id} is missing required configuration: ${missing.join(", ")}`
|
|
433
|
+
);
|
|
407
434
|
}
|
|
408
|
-
return
|
|
435
|
+
return config;
|
|
409
436
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (value === void 0 || value === null || value === "") continue;
|
|
415
|
-
out[key] = String(value);
|
|
416
|
-
}
|
|
417
|
-
return out;
|
|
437
|
+
|
|
438
|
+
// src/setup.ts
|
|
439
|
+
function pollToolName(triggerType) {
|
|
440
|
+
return `poll_${triggerType}`;
|
|
418
441
|
}
|
|
419
|
-
function
|
|
420
|
-
|
|
421
|
-
const rawUrl = resolveTemplates(request.url, scope, intoUrl);
|
|
422
|
-
if (typeof rawUrl !== "string" || rawUrl.trim() === "") {
|
|
423
|
-
throw new Error("Request has no URL once its templates are resolved");
|
|
424
|
-
}
|
|
425
|
-
let url;
|
|
426
|
-
try {
|
|
427
|
-
url = new URL(rawUrl);
|
|
428
|
-
} catch {
|
|
429
|
-
throw new Error(`Request URL is not a URL once its templates are resolved: "${rawUrl}"`);
|
|
430
|
-
}
|
|
431
|
-
for (const [key, value] of Object.entries(stringMap(resolveTemplates(request.query, scope)))) {
|
|
432
|
-
url.searchParams.set(key, value);
|
|
433
|
-
}
|
|
434
|
-
const headers = resolveHeaders(request.headers, scope);
|
|
435
|
-
const resolved = { url: url.toString(), method, headers };
|
|
436
|
-
if (request.body !== void 0 && method !== "GET" && method !== "HEAD") {
|
|
437
|
-
const body = resolveTemplates(request.body, scope);
|
|
438
|
-
if (body !== void 0) {
|
|
439
|
-
resolved.body = typeof body === "string" ? body : JSON.stringify(body);
|
|
440
|
-
if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) {
|
|
441
|
-
resolved.headers["content-type"] = typeof body === "string" ? "text/plain" : "application/json";
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
return resolved;
|
|
442
|
+
function footerToolName(footerId) {
|
|
443
|
+
return `vorn_footer_${footerId}`;
|
|
446
444
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (text === "") return void 0;
|
|
450
|
-
const type = response.headers.get("content-type") ?? "";
|
|
451
|
-
if (!type.includes("json")) return text;
|
|
452
|
-
try {
|
|
453
|
-
return JSON.parse(text);
|
|
454
|
-
} catch {
|
|
455
|
-
return text;
|
|
456
|
-
}
|
|
445
|
+
function handlerToolName(handlerId) {
|
|
446
|
+
return `vorn_handler_${handlerId}`;
|
|
457
447
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
448
|
+
var MANIFEST_TOOL = "vorn_connector_manifest";
|
|
449
|
+
var PREFLIGHT_TOOL = "vorn_connector_preflight";
|
|
450
|
+
var OPTIONS_TOOL = "vorn_connector_options";
|
|
451
|
+
function connectionSetup(connector, triggerType) {
|
|
452
|
+
const trigger = connector.triggers.find((entry) => entry.type === triggerType);
|
|
453
|
+
if (!trigger) {
|
|
454
|
+
throw new Error(`Connector ${connector.id} has no trigger "${triggerType}"`);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
connectorId: connector.id,
|
|
458
|
+
triggerType,
|
|
459
|
+
filters: {
|
|
460
|
+
pollTool: pollToolName(triggerType),
|
|
461
|
+
itemsPath: "items",
|
|
462
|
+
idField: "externalId",
|
|
463
|
+
timestampField: "updatedAt",
|
|
464
|
+
titleField: "title",
|
|
465
|
+
urlField: "url",
|
|
466
|
+
cursorArg: "cursor",
|
|
467
|
+
cursorPath: "nextCursor"
|
|
468
|
+
},
|
|
469
|
+
env: connector.config.map((field) => ({
|
|
470
|
+
name: envNameFor(field.key, field.env),
|
|
471
|
+
required: field.required === true,
|
|
472
|
+
secret: field.secret === true,
|
|
473
|
+
...field.description !== void 0 && { description: field.description },
|
|
474
|
+
...field.builderHint !== void 0 && { builderHint: field.builderHint }
|
|
475
|
+
}))
|
|
476
|
+
};
|
|
462
477
|
}
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
478
|
+
function manifestContributions(connector) {
|
|
479
|
+
const contributes = connector.contributes;
|
|
480
|
+
if (!contributes) return void 0;
|
|
481
|
+
const shared = (contribution) => ({
|
|
482
|
+
id: contribution.id,
|
|
483
|
+
title: contribution.title,
|
|
484
|
+
...contribution.description !== void 0 && { description: contribution.description },
|
|
485
|
+
...contribution.when !== void 0 && { when: contribution.when }
|
|
468
486
|
});
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
487
|
+
return {
|
|
488
|
+
...contributes.panes !== void 0 && {
|
|
489
|
+
panes: contributes.panes.map((pane) => ({
|
|
490
|
+
...shared(pane),
|
|
491
|
+
...pane.icon !== void 0 && { icon: pane.icon },
|
|
492
|
+
...pane.web !== void 0 && { web: pane.web },
|
|
493
|
+
...pane.command !== void 0 && { command: pane.command }
|
|
494
|
+
}))
|
|
495
|
+
},
|
|
496
|
+
...contributes.footers !== void 0 && {
|
|
497
|
+
footers: contributes.footers.map((footer) => ({ ...shared(footer), every: footer.every }))
|
|
498
|
+
},
|
|
499
|
+
...contributes.linkHandlers !== void 0 && {
|
|
500
|
+
linkHandlers: contributes.linkHandlers.map((handler) => ({
|
|
501
|
+
...shared(handler),
|
|
502
|
+
pattern: handler.pattern,
|
|
503
|
+
example: handler.example
|
|
504
|
+
}))
|
|
505
|
+
}
|
|
506
|
+
};
|
|
472
507
|
}
|
|
473
|
-
function
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
508
|
+
function connectorManifest(connector) {
|
|
509
|
+
const contributes = manifestContributions(connector);
|
|
510
|
+
return {
|
|
511
|
+
id: connector.id,
|
|
512
|
+
name: connector.name,
|
|
513
|
+
version: connector.version,
|
|
514
|
+
kind: connector.kind,
|
|
515
|
+
...connector.description !== void 0 && { description: connector.description },
|
|
516
|
+
...connector.icon !== void 0 && { icon: connector.icon },
|
|
517
|
+
...connector.auth !== void 0 && { auth: connector.auth },
|
|
518
|
+
...contributes !== void 0 && { contributes },
|
|
519
|
+
...connector.permissions !== void 0 && { permissions: connector.permissions },
|
|
520
|
+
...connector.activates !== void 0 && { activates: connector.activates },
|
|
521
|
+
triggers: connector.triggers.map((trigger) => ({
|
|
522
|
+
type: trigger.type,
|
|
523
|
+
label: trigger.label,
|
|
524
|
+
...trigger.description !== void 0 && { description: trigger.description },
|
|
525
|
+
// Carried through so the app can seed a connection's status mapping and
|
|
526
|
+
// its polling workflow. Absent when the connector said nothing, which is
|
|
527
|
+
// different from saying there is nothing.
|
|
528
|
+
...trigger.statusMapping !== void 0 && { statusMapping: trigger.statusMapping },
|
|
529
|
+
...trigger.defaultWorkflow !== void 0 && { defaultWorkflow: trigger.defaultWorkflow },
|
|
530
|
+
setup: connectionSetup(connector, trigger.type)
|
|
531
|
+
})),
|
|
532
|
+
actions: connector.actions.map((action) => ({
|
|
533
|
+
type: action.type,
|
|
534
|
+
label: action.label,
|
|
535
|
+
...action.description !== void 0 && { description: action.description },
|
|
536
|
+
inputs: (action.inputs ?? []).map((input) => ({
|
|
537
|
+
key: input.key,
|
|
538
|
+
label: input.label,
|
|
539
|
+
type: input.type ?? "string",
|
|
540
|
+
required: input.required === true,
|
|
541
|
+
...input.options !== void 0 && { options: input.options },
|
|
542
|
+
...input.loadOptions !== void 0 && { loadOptions: input.loadOptions },
|
|
543
|
+
...input.builderHint !== void 0 && { builderHint: input.builderHint }
|
|
544
|
+
})),
|
|
545
|
+
...action.outputs !== void 0 && { outputs: action.outputs },
|
|
546
|
+
...action.sample !== void 0 && { sample: action.sample }
|
|
547
|
+
}))
|
|
548
|
+
};
|
|
477
549
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
550
|
+
|
|
551
|
+
// src/packaging.ts
|
|
552
|
+
import { builtinModules } from "module";
|
|
553
|
+
import { existsSync, readFileSync } from "fs";
|
|
554
|
+
import { cp, mkdtemp, readdir, stat, writeFile } from "fs/promises";
|
|
555
|
+
import { tmpdir } from "os";
|
|
556
|
+
import { dirname, isAbsolute, join, resolve } from "path";
|
|
557
|
+
var MAX_PACK_BYTES = 8 * 1024 * 1024;
|
|
558
|
+
var MAX_UNPACKED_BYTES = 32 * 1024 * 1024;
|
|
559
|
+
var LIFECYCLE_SCRIPTS = [
|
|
560
|
+
"preinstall",
|
|
561
|
+
"install",
|
|
562
|
+
"postinstall",
|
|
563
|
+
"prepare",
|
|
564
|
+
"prepublish",
|
|
565
|
+
"prepublishOnly",
|
|
566
|
+
"postpublish"
|
|
567
|
+
];
|
|
568
|
+
var BUILTINS = new Set(builtinModules);
|
|
569
|
+
function finding(code, target, message, level = "error") {
|
|
570
|
+
return { level, code, target, message };
|
|
483
571
|
}
|
|
484
|
-
function
|
|
485
|
-
const
|
|
486
|
-
|
|
572
|
+
function lifecycleScriptFindings(pkg) {
|
|
573
|
+
const scripts = pkg?.scripts;
|
|
574
|
+
if (!scripts || typeof scripts !== "object") return [];
|
|
575
|
+
const named = LIFECYCLE_SCRIPTS.filter((name) => typeof scripts[name] === "string");
|
|
576
|
+
if (named.length === 0) return [];
|
|
577
|
+
return [
|
|
578
|
+
finding(
|
|
579
|
+
"lifecycle-scripts",
|
|
580
|
+
"package.json",
|
|
581
|
+
`Remove the ${named.join(", ")} script(s); a pack is installed by copying files, never by running them`
|
|
582
|
+
)
|
|
583
|
+
];
|
|
487
584
|
}
|
|
488
|
-
|
|
489
|
-
const
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
585
|
+
function bundleDependencyFindings(external) {
|
|
586
|
+
const specifiers = /* @__PURE__ */ new Set();
|
|
587
|
+
for (const specifier of external) {
|
|
588
|
+
if (specifier.startsWith(".") || specifier.startsWith("/")) continue;
|
|
589
|
+
if (specifier.startsWith("node:") || BUILTINS.has(specifier)) continue;
|
|
590
|
+
specifiers.add(specifier);
|
|
591
|
+
}
|
|
592
|
+
if (specifiers.size === 0) return [];
|
|
593
|
+
return [
|
|
594
|
+
finding(
|
|
595
|
+
"runtime-dependencies",
|
|
596
|
+
"bundle",
|
|
597
|
+
`${[...specifiers].sort().join(", ")} stayed outside the bundle; a pack must launch with no install step`
|
|
598
|
+
)
|
|
599
|
+
];
|
|
600
|
+
}
|
|
601
|
+
var RELATIVE_REQUIRE = /(?:__)?(?:require(?:\.resolve)?|import)\(\s*(['"])(\.\.?\/[^'"]*)\1\s*\)/y;
|
|
602
|
+
var RELATIVE_CREATE_REQUIRE = /createRequire\([^()]*\)\(\s*(['"])(\.\.?\/[^'"]*)\1\s*\)/y;
|
|
603
|
+
var CALL_WORDS = /* @__PURE__ */ new Set(["require", "__require", "createRequire", "import"]);
|
|
604
|
+
var BEFORE_REGEX = /* @__PURE__ */ new Set(["", ..."(,=:[!&|?{};+-*%~^<>"]);
|
|
605
|
+
var BEFORE_REGEX_WORDS = /* @__PURE__ */ new Set([
|
|
606
|
+
"return",
|
|
607
|
+
"typeof",
|
|
608
|
+
"instanceof",
|
|
609
|
+
"in",
|
|
610
|
+
"of",
|
|
611
|
+
"new",
|
|
612
|
+
"delete",
|
|
613
|
+
"void",
|
|
614
|
+
"case",
|
|
615
|
+
"do",
|
|
616
|
+
"else",
|
|
617
|
+
"yield",
|
|
618
|
+
"await"
|
|
619
|
+
]);
|
|
620
|
+
var WORD = /[\w$]/;
|
|
621
|
+
function endOfQuoted(code, start) {
|
|
622
|
+
const quote2 = code[start];
|
|
623
|
+
let i = start + 1;
|
|
624
|
+
while (i < code.length) {
|
|
625
|
+
if (code[i] === "\\") {
|
|
626
|
+
i += 2;
|
|
627
|
+
continue;
|
|
501
628
|
}
|
|
502
|
-
if (
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
629
|
+
if (code[i] === quote2) return i + 1;
|
|
630
|
+
i += 1;
|
|
631
|
+
}
|
|
632
|
+
return code.length;
|
|
633
|
+
}
|
|
634
|
+
function endOfRegex(code, start) {
|
|
635
|
+
let i = start + 1;
|
|
636
|
+
let inClass = false;
|
|
637
|
+
while (i < code.length) {
|
|
638
|
+
const ch = code[i];
|
|
639
|
+
if (ch === "\\") {
|
|
640
|
+
i += 2;
|
|
641
|
+
continue;
|
|
506
642
|
}
|
|
507
|
-
if (
|
|
508
|
-
|
|
643
|
+
if (ch === "\n") return i;
|
|
644
|
+
if (ch === "[") inClass = true;
|
|
645
|
+
else if (ch === "]") inClass = false;
|
|
646
|
+
else if (ch === "/" && !inClass) return i + 1;
|
|
647
|
+
i += 1;
|
|
648
|
+
}
|
|
649
|
+
return code.length;
|
|
650
|
+
}
|
|
651
|
+
function relativeRuntimeSpecifiers(code) {
|
|
652
|
+
const found = /* @__PURE__ */ new Set();
|
|
653
|
+
let previous = "";
|
|
654
|
+
let previousWord = "";
|
|
655
|
+
let i = 0;
|
|
656
|
+
while (i < code.length) {
|
|
657
|
+
const ch = code[i];
|
|
658
|
+
if (ch === "/" && code[i + 1] === "/") {
|
|
659
|
+
const end = code.indexOf("\n", i);
|
|
660
|
+
i = end === -1 ? code.length : end;
|
|
661
|
+
continue;
|
|
509
662
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if (index === 0) {
|
|
515
|
-
const where = strategy.itemsPath === void 0 ? "the response is not a list" : `the response has no list at "${strategy.itemsPath}"`;
|
|
516
|
-
throw new Error(`Cannot page through this request: ${where}`);
|
|
517
|
-
}
|
|
518
|
-
return collected;
|
|
663
|
+
if (ch === "/" && code[i + 1] === "*") {
|
|
664
|
+
const end = code.indexOf("*/", i + 2);
|
|
665
|
+
i = end === -1 ? code.length : end + 2;
|
|
666
|
+
continue;
|
|
519
667
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
if (next === void 0 || next === null || next === "") return collected;
|
|
525
|
-
cursor = String(next);
|
|
668
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
669
|
+
i = endOfQuoted(code, i);
|
|
670
|
+
previous = ch;
|
|
671
|
+
previousWord = "";
|
|
526
672
|
continue;
|
|
527
673
|
}
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
|
|
674
|
+
if (ch === "/" && (BEFORE_REGEX.has(previous) || BEFORE_REGEX_WORDS.has(previousWord))) {
|
|
675
|
+
i = endOfRegex(code, i);
|
|
676
|
+
previous = "/";
|
|
677
|
+
previousWord = "";
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
if (WORD.test(ch)) {
|
|
681
|
+
const start = i;
|
|
682
|
+
while (i < code.length && WORD.test(code[i])) i += 1;
|
|
683
|
+
const word = code.slice(start, i);
|
|
684
|
+
if (CALL_WORDS.has(word) && code[start - 1] !== ".") {
|
|
685
|
+
for (const pattern of [RELATIVE_REQUIRE, RELATIVE_CREATE_REQUIRE]) {
|
|
686
|
+
pattern.lastIndex = start;
|
|
687
|
+
const match = pattern.exec(code);
|
|
688
|
+
if (match) found.add(match[2]);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
previous = code[i - 1];
|
|
692
|
+
previousWord = word;
|
|
531
693
|
continue;
|
|
532
694
|
}
|
|
533
|
-
|
|
695
|
+
if (!/\s/.test(ch)) {
|
|
696
|
+
previous = ch;
|
|
697
|
+
previousWord = "";
|
|
698
|
+
}
|
|
699
|
+
i += 1;
|
|
700
|
+
}
|
|
701
|
+
return [...found];
|
|
702
|
+
}
|
|
703
|
+
function bundledRequireFindings(code) {
|
|
704
|
+
const specifiers = relativeRuntimeSpecifiers(code);
|
|
705
|
+
if (specifiers.length === 0) return [];
|
|
706
|
+
return [
|
|
707
|
+
finding(
|
|
708
|
+
"runtime-dependencies",
|
|
709
|
+
"bundle",
|
|
710
|
+
`${specifiers.sort().join(", ")} ${specifiers.length === 1 ? "is" : "are"} required at runtime; a pack is one file, so nothing beside it survives packing`,
|
|
711
|
+
"warn"
|
|
712
|
+
)
|
|
713
|
+
];
|
|
714
|
+
}
|
|
715
|
+
function packageDirFor(resolveDir, entry) {
|
|
716
|
+
const from = resolve(resolveDir);
|
|
717
|
+
if (entry === void 0) return from;
|
|
718
|
+
return entry.startsWith(".") || isAbsolute(entry) ? dirname(resolve(from, entry)) : from;
|
|
719
|
+
}
|
|
720
|
+
function readNearestPackageJson(fromDir) {
|
|
721
|
+
let current = resolve(fromDir);
|
|
722
|
+
for (; ; ) {
|
|
723
|
+
try {
|
|
724
|
+
return JSON.parse(readFileSync(join(current, "package.json"), "utf8"));
|
|
725
|
+
} catch {
|
|
726
|
+
const parent = dirname(current);
|
|
727
|
+
if (parent === current) return void 0;
|
|
728
|
+
current = parent;
|
|
729
|
+
}
|
|
534
730
|
}
|
|
535
|
-
throw new Error(`Request for ${request.url} exceeded ${MAX_REQUEST_PAGES} pages`);
|
|
536
731
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
732
|
+
function packageRootFor(fromDir) {
|
|
733
|
+
let current = resolve(fromDir);
|
|
734
|
+
for (; ; ) {
|
|
735
|
+
if (existsSync(join(current, "package.json"))) return current;
|
|
736
|
+
const parent = dirname(current);
|
|
737
|
+
if (parent === current) return resolve(fromDir);
|
|
738
|
+
current = parent;
|
|
541
739
|
}
|
|
542
|
-
const { body } = await sendRequest(resolveRequest(request, scope), options);
|
|
543
|
-
return asOutput(applyPostReceive(body, postReceive));
|
|
544
740
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
setTimeout(resolve3, ms);
|
|
555
|
-
});
|
|
556
|
-
function retryAfterMs(header, now) {
|
|
557
|
-
if (!header) return void 0;
|
|
558
|
-
const trimmed = header.trim();
|
|
559
|
-
if (trimmed === "") return void 0;
|
|
560
|
-
const seconds = Number(trimmed);
|
|
561
|
-
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
562
|
-
const at = Date.parse(trimmed);
|
|
563
|
-
if (Number.isNaN(at)) return void 0;
|
|
564
|
-
return Math.max(0, at - now);
|
|
741
|
+
function packEntryContents(entry, sdkModule = "@vornrun/connector-sdk") {
|
|
742
|
+
return [
|
|
743
|
+
`import { serveConnector } from ${JSON.stringify(sdkModule)}`,
|
|
744
|
+
`import * as entry from ${JSON.stringify(entry)}`,
|
|
745
|
+
"const exported = Object.values(entry).find((value) => value && Array.isArray(value.triggers))",
|
|
746
|
+
`if (!exported) throw new Error(${JSON.stringify(`${entry} exports no connector`)})`,
|
|
747
|
+
"await serveConnector(exported)",
|
|
748
|
+
""
|
|
749
|
+
].join("\n");
|
|
565
750
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
751
|
+
var WEB_DIR = "web";
|
|
752
|
+
async function directoryBytes(dir) {
|
|
753
|
+
let total = 0;
|
|
754
|
+
for (const entry of await readdir(dir, { withFileTypes: true, recursive: true })) {
|
|
755
|
+
if (!entry.isFile()) continue;
|
|
756
|
+
total += (await stat(join(entry.parentPath, entry.name))).size;
|
|
757
|
+
}
|
|
758
|
+
return total;
|
|
759
|
+
}
|
|
760
|
+
function webDirectories(connector) {
|
|
761
|
+
const dirs = (connector.contributes?.panes ?? []).map((pane) => pane.web).filter((web) => web !== void 0).map((web) => dirname(web));
|
|
762
|
+
return [...new Set(dirs)];
|
|
763
|
+
}
|
|
764
|
+
async function stagePack(connector, code, packageRoot) {
|
|
765
|
+
const dir = await mkdtemp(join(tmpdir(), "vorn-pack-"));
|
|
766
|
+
await writeFile(join(dir, "index.js"), code, "utf8");
|
|
767
|
+
await writeFile(
|
|
768
|
+
join(dir, "manifest.json"),
|
|
769
|
+
`${JSON.stringify(connectorManifest(connector), null, 2)}
|
|
770
|
+
`,
|
|
771
|
+
"utf8"
|
|
772
|
+
);
|
|
773
|
+
if (packageRoot !== void 0) {
|
|
774
|
+
for (const relative of webDirectories(connector)) {
|
|
775
|
+
const from = join(packageRoot, relative);
|
|
776
|
+
if (existsSync(from)) await cp(from, join(dir, relative), { recursive: true });
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return dir;
|
|
780
|
+
}
|
|
781
|
+
var LAUNCH_TIMEOUT_MS = 15e3;
|
|
782
|
+
var LAUNCH_ENV_KEYS = [
|
|
783
|
+
"PATH",
|
|
784
|
+
"HOME",
|
|
785
|
+
"USERPROFILE",
|
|
786
|
+
"HOMEDRIVE",
|
|
787
|
+
"HOMEPATH",
|
|
788
|
+
"APPDATA",
|
|
789
|
+
"LOCALAPPDATA",
|
|
790
|
+
"PROGRAMDATA",
|
|
791
|
+
"PROGRAMFILES",
|
|
792
|
+
"SystemRoot",
|
|
793
|
+
"SYSTEMDRIVE",
|
|
794
|
+
"COMSPEC",
|
|
795
|
+
"PATHEXT",
|
|
796
|
+
"TMPDIR",
|
|
797
|
+
"TEMP",
|
|
798
|
+
"TMP",
|
|
799
|
+
"LANG",
|
|
800
|
+
"LC_ALL",
|
|
801
|
+
"LC_CTYPE",
|
|
802
|
+
"TZ",
|
|
803
|
+
"SHELL",
|
|
804
|
+
"TERM",
|
|
805
|
+
"USER",
|
|
806
|
+
"LOGNAME",
|
|
807
|
+
"NODE_EXTRA_CA_CERTS"
|
|
808
|
+
];
|
|
809
|
+
function launchEnv() {
|
|
810
|
+
const env = {};
|
|
811
|
+
for (const key of LAUNCH_ENV_KEYS) {
|
|
812
|
+
const value = process.env[key];
|
|
813
|
+
if (value !== void 0) env[key] = value;
|
|
814
|
+
}
|
|
815
|
+
return env;
|
|
816
|
+
}
|
|
817
|
+
function errorLine(text) {
|
|
818
|
+
const lines = text.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
819
|
+
return [...lines].reverse().find((line) => /Error\b/.test(line)) ?? lines[lines.length - 1];
|
|
820
|
+
}
|
|
821
|
+
function withTimeout(promise, ms, message) {
|
|
822
|
+
let timer;
|
|
823
|
+
return Promise.race([
|
|
824
|
+
promise.finally(() => clearTimeout(timer)),
|
|
825
|
+
new Promise((_, reject) => {
|
|
826
|
+
timer = setTimeout(() => reject(new Error(message)), ms);
|
|
827
|
+
})
|
|
828
|
+
]);
|
|
829
|
+
}
|
|
830
|
+
async function packLaunchFindings(dir) {
|
|
831
|
+
const { Client } = await import("@modelcontextprotocol/sdk/client/index.js");
|
|
832
|
+
const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
|
|
833
|
+
const transport = new StdioClientTransport({
|
|
834
|
+
command: process.execPath,
|
|
835
|
+
args: ["index.js"],
|
|
836
|
+
cwd: dir,
|
|
837
|
+
env: launchEnv(),
|
|
838
|
+
stderr: "pipe"
|
|
839
|
+
});
|
|
840
|
+
const client = new Client({ name: "vorn-connector-check", version: "1" }, { capabilities: {} });
|
|
841
|
+
let stderr = "";
|
|
842
|
+
transport.stderr?.on("data", (chunk) => {
|
|
843
|
+
stderr += chunk.toString();
|
|
844
|
+
});
|
|
845
|
+
try {
|
|
846
|
+
await withTimeout(
|
|
847
|
+
client.connect(transport),
|
|
848
|
+
LAUNCH_TIMEOUT_MS,
|
|
849
|
+
`did not answer within ${LAUNCH_TIMEOUT_MS / 1e3}s of starting`
|
|
850
|
+
);
|
|
851
|
+
return [];
|
|
852
|
+
} catch (error) {
|
|
853
|
+
const said = error instanceof Error ? error.message : String(error);
|
|
854
|
+
return [
|
|
855
|
+
finding("pack-launch", "bundle", `did not start as a pack: ${errorLine(stderr) ?? said}`)
|
|
856
|
+
];
|
|
857
|
+
} finally {
|
|
858
|
+
await client.close().catch(() => {
|
|
859
|
+
});
|
|
860
|
+
await transport.close().catch(() => {
|
|
861
|
+
});
|
|
862
|
+
}
|
|
570
863
|
}
|
|
571
|
-
function
|
|
572
|
-
const
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (!await pause(backoffMs(attempt, options.retry))) throw error;
|
|
595
|
-
}
|
|
864
|
+
async function esbuildBundle(request) {
|
|
865
|
+
const { build } = await import("esbuild");
|
|
866
|
+
const result = await build({
|
|
867
|
+
stdin: {
|
|
868
|
+
contents: request.contents,
|
|
869
|
+
resolveDir: request.resolveDir,
|
|
870
|
+
sourcefile: "vorn-connector-pack.js",
|
|
871
|
+
loader: "js"
|
|
872
|
+
},
|
|
873
|
+
bundle: true,
|
|
874
|
+
platform: "node",
|
|
875
|
+
target: "node20",
|
|
876
|
+
format: "esm",
|
|
877
|
+
write: false,
|
|
878
|
+
metafile: true,
|
|
879
|
+
legalComments: "none",
|
|
880
|
+
// A bundled CommonJS dependency asks for its builtins through esbuild's shim, which throws unless a real require is in scope.
|
|
881
|
+
banner: {
|
|
882
|
+
js: [
|
|
883
|
+
"import { createRequire as __vornCreateRequire } from 'node:module'",
|
|
884
|
+
"const require = __vornCreateRequire(import.meta.url)",
|
|
885
|
+
""
|
|
886
|
+
].join("\n")
|
|
596
887
|
}
|
|
597
|
-
|
|
888
|
+
});
|
|
889
|
+
const output = Object.values(result.metafile.outputs)[0];
|
|
890
|
+
return {
|
|
891
|
+
code: result.outputFiles[0].text,
|
|
892
|
+
external: (output?.imports ?? []).filter((item) => item.external).map((item) => item.path)
|
|
598
893
|
};
|
|
599
|
-
return send;
|
|
600
894
|
}
|
|
601
895
|
|
|
602
|
-
// src/
|
|
603
|
-
var
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
896
|
+
// src/host.ts
|
|
897
|
+
var HOST_URL_ENV = "VORN_EXTENSION_HOST";
|
|
898
|
+
var HOST_TOKEN_ENV = "VORN_EXTENSION_TOKEN";
|
|
899
|
+
var PermissionDeniedError = class extends Error {
|
|
900
|
+
constructor(method, detail) {
|
|
901
|
+
super(`The host refused ${method}: ${detail}`);
|
|
902
|
+
this.name = "PermissionDeniedError";
|
|
608
903
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
...options.cursor !== void 0 && { cursor: options.cursor },
|
|
615
|
-
...options.limit !== void 0 && { limit: options.limit },
|
|
616
|
-
now,
|
|
617
|
-
// A poll only reads, so every failure it meets is worth trying again.
|
|
618
|
-
fetch: resilientFetch({
|
|
619
|
-
fetchImpl: options.fetchImpl ?? globalThis.fetch,
|
|
620
|
-
retryable: true,
|
|
621
|
-
...options.retry !== void 0 && { retry: options.retry },
|
|
622
|
-
...options.sleep !== void 0 && { sleep: options.sleep }
|
|
623
|
-
})
|
|
624
|
-
};
|
|
625
|
-
const outcome = typeof trigger.poll === "function" ? await trigger.poll(context) : await pollWithDedupe(trigger, context);
|
|
626
|
-
if (!outcome || !Array.isArray(outcome.items)) {
|
|
627
|
-
throw new Error(`Trigger ${triggerType} did not return an items array`);
|
|
904
|
+
};
|
|
905
|
+
var HostReplyError = class extends Error {
|
|
906
|
+
constructor(method, detail) {
|
|
907
|
+
super(`The host answered ${method} with ${detail}`);
|
|
908
|
+
this.name = "HostReplyError";
|
|
628
909
|
}
|
|
629
|
-
|
|
630
|
-
|
|
910
|
+
};
|
|
911
|
+
var HOST_TIMEOUT_MS = 15e3;
|
|
912
|
+
var LOOPBACK_HOSTS = ["127.0.0.1", "localhost", "[::1]"];
|
|
913
|
+
function endpoint(env) {
|
|
914
|
+
const url = env[HOST_URL_ENV]?.trim();
|
|
915
|
+
const token = env[HOST_TOKEN_ENV]?.trim();
|
|
916
|
+
if (!url || !token) {
|
|
917
|
+
throw new Error(
|
|
918
|
+
`This extension was started without a host bridge; ${HOST_URL_ENV} and ${HOST_TOKEN_ENV} are set by Vorn`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
let parsed;
|
|
922
|
+
try {
|
|
923
|
+
parsed = new URL(url);
|
|
924
|
+
} catch {
|
|
925
|
+
throw new Error(`${HOST_URL_ENV} is ${JSON.stringify(url)}, which is not a URL`);
|
|
926
|
+
}
|
|
927
|
+
if (parsed.protocol !== "http:" || !LOOPBACK_HOSTS.includes(parsed.hostname)) {
|
|
928
|
+
throw new Error(
|
|
929
|
+
`${HOST_URL_ENV} is ${JSON.stringify(url)}; the bridge is served on this machine, over http on ${LOOPBACK_HOSTS.join(", ")}`
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
return { url: url.replace(/\/$/, ""), token };
|
|
933
|
+
}
|
|
934
|
+
function createExtensionHost(options) {
|
|
935
|
+
const env = options.env ?? process.env;
|
|
936
|
+
const call = options.fetchImpl ?? fetch;
|
|
937
|
+
async function ask(method, params) {
|
|
938
|
+
const { url, token } = endpoint(env);
|
|
939
|
+
const response = await call(`${url}/${method}`, {
|
|
940
|
+
method: "POST",
|
|
941
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
942
|
+
body: JSON.stringify({ sessionId: options.sessionId, ...params }),
|
|
943
|
+
signal: AbortSignal.timeout(HOST_TIMEOUT_MS)
|
|
944
|
+
});
|
|
945
|
+
const text = await response.text();
|
|
946
|
+
if (response.status === 403) throw new PermissionDeniedError(method, text || "not granted");
|
|
947
|
+
if (!response.ok) throw new Error(`The host answered ${method} with HTTP ${response.status}`);
|
|
948
|
+
if (text === "") return void 0;
|
|
949
|
+
let parsed;
|
|
950
|
+
try {
|
|
951
|
+
parsed = JSON.parse(text);
|
|
952
|
+
} catch {
|
|
953
|
+
throw new HostReplyError(method, "a body that is not JSON");
|
|
954
|
+
}
|
|
955
|
+
if (!parsed || typeof parsed !== "object" || !("result" in parsed)) {
|
|
956
|
+
throw new HostReplyError(method, "a body carrying no result");
|
|
957
|
+
}
|
|
958
|
+
return parsed.result;
|
|
631
959
|
}
|
|
632
960
|
return {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
961
|
+
diff: () => ask("diff", {}),
|
|
962
|
+
status: () => ask("status", {}),
|
|
963
|
+
output: (opts) => ask("output", { ...opts?.lines !== void 0 && { lines: opts.lines } }),
|
|
964
|
+
selection: () => ask("selection", {}),
|
|
965
|
+
send: (text) => ask("send", { text }),
|
|
966
|
+
rename: (name) => ask("rename", { name }),
|
|
967
|
+
usage: () => ask("usage", {})
|
|
636
968
|
};
|
|
637
969
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
970
|
+
|
|
971
|
+
// src/normalize.ts
|
|
972
|
+
var RESERVED_KEYS = [
|
|
973
|
+
"externalId",
|
|
974
|
+
"title",
|
|
975
|
+
"url",
|
|
976
|
+
"description",
|
|
977
|
+
"status",
|
|
978
|
+
"labels",
|
|
979
|
+
"assignee",
|
|
980
|
+
"updatedAt"
|
|
981
|
+
];
|
|
982
|
+
var UNSAFE_KEYS = ["__proto__", "constructor", "prototype"];
|
|
983
|
+
function itemExternalId(item) {
|
|
984
|
+
return String(item.externalId ?? "").trim();
|
|
985
|
+
}
|
|
986
|
+
function itemTimestamp(item, fallback) {
|
|
987
|
+
return isoTimestamp(item.updatedAt, fallback);
|
|
988
|
+
}
|
|
989
|
+
function isoTimestamp(value, fallback) {
|
|
990
|
+
if (value === void 0) return fallback;
|
|
991
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
992
|
+
if (Number.isNaN(date.getTime())) {
|
|
993
|
+
throw new Error(`Invalid updatedAt: ${String(value)}`);
|
|
652
994
|
}
|
|
653
|
-
|
|
995
|
+
return date.toISOString();
|
|
654
996
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
throw new Error(`Connector ${connector.id} serves no options set "${name}"`);
|
|
997
|
+
function normalizeItem(item, polledAt) {
|
|
998
|
+
const externalId = itemExternalId(item);
|
|
999
|
+
if (!externalId) {
|
|
1000
|
+
throw new Error("Connector item is missing externalId");
|
|
660
1001
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1002
|
+
if (!item.title || !item.title.trim()) {
|
|
1003
|
+
throw new Error(`Connector item ${externalId} is missing title`);
|
|
1004
|
+
}
|
|
1005
|
+
const extra = {};
|
|
1006
|
+
for (const [key, value] of Object.entries(item.data ?? {})) {
|
|
1007
|
+
if (RESERVED_KEYS.includes(key)) continue;
|
|
1008
|
+
if (UNSAFE_KEYS.includes(key)) continue;
|
|
1009
|
+
extra[key] = value;
|
|
1010
|
+
}
|
|
1011
|
+
return {
|
|
1012
|
+
...extra,
|
|
1013
|
+
externalId,
|
|
1014
|
+
title: item.title,
|
|
1015
|
+
url: item.url ?? "",
|
|
1016
|
+
description: item.description ?? "",
|
|
1017
|
+
status: item.status ?? "open",
|
|
1018
|
+
labels: item.labels ?? [],
|
|
1019
|
+
...item.assignee !== void 0 && { assignee: item.assignee },
|
|
1020
|
+
updatedAt: isoTimestamp(item.updatedAt, polledAt)
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
function normalizeItems(items, polledAt) {
|
|
1024
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1025
|
+
return items.map((item) => {
|
|
1026
|
+
const normalized = normalizeItem(item, polledAt);
|
|
1027
|
+
if (seen.has(normalized.externalId)) {
|
|
1028
|
+
throw new Error(`Duplicate externalId "${normalized.externalId}" in one poll page`);
|
|
1029
|
+
}
|
|
1030
|
+
seen.add(normalized.externalId);
|
|
1031
|
+
return normalized;
|
|
670
1032
|
});
|
|
671
|
-
|
|
672
|
-
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/dedupe.ts
|
|
1036
|
+
var MAX_BOUNDARY_IDS = 500;
|
|
1037
|
+
function decodeCursor(cursor, strategy) {
|
|
1038
|
+
if (!cursor) return void 0;
|
|
1039
|
+
let parsed;
|
|
1040
|
+
try {
|
|
1041
|
+
parsed = JSON.parse(cursor);
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
throw new Error(`Cursor is not valid SDK cursor JSON: ${cursor}`, { cause: error });
|
|
673
1044
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
1045
|
+
const state = parsed;
|
|
1046
|
+
if (!state || typeof state !== "object" || state.v !== 1 || state.s !== strategy) {
|
|
1047
|
+
throw new Error(`Cursor does not belong to the "${strategy}" strategy: ${cursor}`);
|
|
1048
|
+
}
|
|
1049
|
+
const wellFormed = state.s === "timestamp" ? typeof state.t === "string" && Array.isArray(state.ids) && state.ids.every((id) => typeof id === "string") : typeof state.id === "string";
|
|
1050
|
+
if (!wellFormed) {
|
|
1051
|
+
throw new Error(`Cursor is missing the fields the "${strategy}" strategy needs: ${cursor}`);
|
|
1052
|
+
}
|
|
1053
|
+
return state;
|
|
677
1054
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
return
|
|
1055
|
+
function compare(left, right) {
|
|
1056
|
+
if (left === right) return 0;
|
|
1057
|
+
return left < right ? -1 : 1;
|
|
681
1058
|
}
|
|
682
|
-
function
|
|
683
|
-
|
|
684
|
-
if (
|
|
685
|
-
|
|
686
|
-
if (Number.isNaN(parsed)) throw new Error(`Expected a number, got "${quote(value)}"`);
|
|
687
|
-
return parsed;
|
|
1059
|
+
function page(chronological, context, hadCursor, nextCursor) {
|
|
1060
|
+
const delivered = context.limit === void 0 ? chronological : chronological.slice(0, context.limit);
|
|
1061
|
+
if (delivered.length === 0) {
|
|
1062
|
+
return { items: [], ...context.cursor !== void 0 && { nextCursor: context.cursor } };
|
|
688
1063
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
1064
|
+
return {
|
|
1065
|
+
items: delivered.map((entry) => entry.item),
|
|
1066
|
+
nextCursor: JSON.stringify(nextCursor(delivered)),
|
|
1067
|
+
// Only drain a backlog we know we truncated, and only once a cursor
|
|
1068
|
+
// exists — a first poll should not pull the source's entire history.
|
|
1069
|
+
hasMore: chronological.length > delivered.length && hadCursor
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
function timestampPoll(fetched, state, context, polledAt) {
|
|
1073
|
+
const boundary = state?.t ?? context.since;
|
|
1074
|
+
const seen = new Set(state?.ids ?? []);
|
|
1075
|
+
const fresh = [];
|
|
1076
|
+
const pinnedAlreadySeen = [];
|
|
1077
|
+
for (const item of fetched) {
|
|
1078
|
+
const id = itemExternalId(item);
|
|
1079
|
+
const pinned = item.updatedAt === void 0 && boundary !== void 0;
|
|
1080
|
+
const at = pinned ? boundary : itemTimestamp(item, polledAt);
|
|
1081
|
+
const isNew = boundary === void 0 || at > boundary || at === boundary && !seen.has(id);
|
|
1082
|
+
if (isNew) fresh.push({ item, at, id, ...pinned && { pinned: true } });
|
|
1083
|
+
else if (pinned) pinnedAlreadySeen.push(id);
|
|
693
1084
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
1085
|
+
fresh.sort((left, right) => compare(left.at, right.at) || compare(left.id, right.id));
|
|
1086
|
+
return page(fresh, context, state !== void 0, (delivered) => {
|
|
1087
|
+
const newest = delivered[delivered.length - 1].at;
|
|
1088
|
+
const atNewest = [];
|
|
1089
|
+
for (let i = delivered.length - 1; i >= 0 && delivered[i].at === newest; i -= 1) {
|
|
1090
|
+
atNewest.push(delivered[i].id);
|
|
699
1091
|
}
|
|
700
|
-
|
|
701
|
-
|
|
1092
|
+
const carried = newest === boundary ? [...seen, ...atNewest] : [
|
|
1093
|
+
...pinnedAlreadySeen,
|
|
1094
|
+
...delivered.filter((entry) => entry.pinned).map((entry) => entry.id),
|
|
1095
|
+
...atNewest
|
|
1096
|
+
];
|
|
1097
|
+
return { v: 1, s: "timestamp", t: newest, ids: carried.slice(-MAX_BOUNDARY_IDS) };
|
|
1098
|
+
});
|
|
702
1099
|
}
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
1100
|
+
function lastItemPoll(fetched, state, context, polledAt) {
|
|
1101
|
+
const keyed = fetched.map((item) => ({
|
|
1102
|
+
item,
|
|
1103
|
+
at: itemTimestamp(item, polledAt),
|
|
1104
|
+
id: itemExternalId(item)
|
|
1105
|
+
}));
|
|
1106
|
+
const stopAt = state ? keyed.findIndex((entry) => entry.id === state.id) : -1;
|
|
1107
|
+
const chronological = (stopAt === -1 ? keyed : keyed.slice(0, stopAt)).reverse();
|
|
1108
|
+
return page(chronological, context, state !== void 0, (delivered) => ({
|
|
1109
|
+
v: 1,
|
|
1110
|
+
s: "lastItem",
|
|
1111
|
+
id: delivered[delivered.length - 1].id
|
|
1112
|
+
}));
|
|
1113
|
+
}
|
|
1114
|
+
async function pollWithDedupe(trigger, context) {
|
|
1115
|
+
const strategy = trigger.dedupe;
|
|
1116
|
+
const fetchItems = trigger.fetch;
|
|
1117
|
+
if (!strategy || !fetchItems) {
|
|
1118
|
+
throw new Error(`Trigger ${trigger.type} is not a declarative trigger`);
|
|
707
1119
|
}
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
throw new Error(
|
|
720
|
-
`Action ${actionType} argument "${input.key}": ${error instanceof Error ? error.message : String(error)}`,
|
|
721
|
-
{ cause: error }
|
|
722
|
-
);
|
|
723
|
-
}
|
|
1120
|
+
const polledAt = context.now();
|
|
1121
|
+
if (strategy === "lastItem") {
|
|
1122
|
+
const state2 = decodeCursor(context.cursor, "lastItem");
|
|
1123
|
+
const fetched2 = await runFetch(trigger.type, fetchItems, {
|
|
1124
|
+
config: context.config,
|
|
1125
|
+
...state2 && { lastItemId: state2.id },
|
|
1126
|
+
...context.limit !== void 0 && { limit: context.limit },
|
|
1127
|
+
now: context.now,
|
|
1128
|
+
fetch: context.fetch
|
|
1129
|
+
});
|
|
1130
|
+
return lastItemPoll(fetched2, state2, context, polledAt);
|
|
724
1131
|
}
|
|
725
|
-
const
|
|
726
|
-
const
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1132
|
+
const state = decodeCursor(context.cursor, "timestamp");
|
|
1133
|
+
const since = state?.t ?? context.since;
|
|
1134
|
+
const fetched = await runFetch(trigger.type, fetchItems, {
|
|
1135
|
+
config: context.config,
|
|
1136
|
+
...since !== void 0 && { since },
|
|
1137
|
+
...context.limit !== void 0 && { limit: context.limit },
|
|
1138
|
+
now: context.now,
|
|
1139
|
+
fetch: context.fetch
|
|
733
1140
|
});
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1141
|
+
return timestampPoll(fetched, state, context, polledAt);
|
|
1142
|
+
}
|
|
1143
|
+
async function runFetch(type, fetchItems, context) {
|
|
1144
|
+
const fetched = await fetchItems(context);
|
|
1145
|
+
if (!Array.isArray(fetched)) {
|
|
1146
|
+
throw new Error(`Trigger ${type} fetch() did not return an array`);
|
|
1147
|
+
}
|
|
1148
|
+
return fetched;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/post-receive.ts
|
|
1152
|
+
var UNSAFE_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1153
|
+
function isRecord(value) {
|
|
1154
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1155
|
+
}
|
|
1156
|
+
function segments(path) {
|
|
1157
|
+
return path.split(".").map((part) => part.trim()).filter((part) => part !== "");
|
|
1158
|
+
}
|
|
1159
|
+
function valueAt(value, path) {
|
|
1160
|
+
let current = value;
|
|
1161
|
+
for (const key of segments(path)) {
|
|
1162
|
+
if (UNSAFE_KEYS2.has(key)) return void 0;
|
|
1163
|
+
if (Array.isArray(current)) {
|
|
1164
|
+
const index = Number(key);
|
|
1165
|
+
if (!Number.isInteger(index)) return void 0;
|
|
1166
|
+
current = current[index];
|
|
1167
|
+
continue;
|
|
747
1168
|
}
|
|
1169
|
+
if (!isRecord(current)) return void 0;
|
|
1170
|
+
current = Object.prototype.hasOwnProperty.call(current, key) ? current[key] : void 0;
|
|
748
1171
|
}
|
|
749
|
-
|
|
750
|
-
|
|
1172
|
+
return current;
|
|
1173
|
+
}
|
|
1174
|
+
function withValueAt(value, path, next) {
|
|
1175
|
+
const keys = segments(path);
|
|
1176
|
+
if (keys.length === 0) return next;
|
|
1177
|
+
const [head, ...rest] = keys;
|
|
1178
|
+
if (UNSAFE_KEYS2.has(head)) return value;
|
|
1179
|
+
if (Array.isArray(value)) {
|
|
1180
|
+
const index = Number(head);
|
|
1181
|
+
if (!Number.isInteger(index)) return value;
|
|
1182
|
+
const copy = [...value];
|
|
1183
|
+
copy[index] = rest.length === 0 ? next : withValueAt(copy[index], rest.join("."), next);
|
|
1184
|
+
return copy;
|
|
751
1185
|
}
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
}
|
|
757
|
-
return output ?? {};
|
|
1186
|
+
const base = isRecord(value) ? value : {};
|
|
1187
|
+
return {
|
|
1188
|
+
...base,
|
|
1189
|
+
[head]: rest.length === 0 ? next : withValueAt(base[head], rest.join("."), next)
|
|
1190
|
+
};
|
|
758
1191
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
var VIEW_BOX_PATTERN = /^-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+\s+-?[\d.]+$/;
|
|
764
|
-
var DEDUPE_STRATEGIES = ["timestamp", "lastItem"];
|
|
765
|
-
var AUTH_RUNGS = ["none", "cli", "key", "oauth"];
|
|
766
|
-
var ABSOLUTE_URL_PATTERN = /^https?:\/\//i;
|
|
767
|
-
var CONFIG_ROOTED_URL_PATTERN = /^\{\{\s*config\./;
|
|
768
|
-
function assertUnique(kind, keys) {
|
|
769
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1192
|
+
function pick(value, keys) {
|
|
1193
|
+
if (Array.isArray(value)) return value.map((entry) => pick(entry, keys));
|
|
1194
|
+
if (!isRecord(value)) return value;
|
|
1195
|
+
const out = {};
|
|
770
1196
|
for (const key of keys) {
|
|
771
|
-
if (
|
|
772
|
-
|
|
1197
|
+
if (UNSAFE_KEYS2.has(key)) continue;
|
|
1198
|
+
if (Object.prototype.hasOwnProperty.call(value, key)) out[key] = value[key];
|
|
773
1199
|
}
|
|
1200
|
+
return out;
|
|
774
1201
|
}
|
|
775
|
-
function
|
|
776
|
-
|
|
777
|
-
if (!
|
|
778
|
-
|
|
779
|
-
if (!
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
);
|
|
1202
|
+
function rename(value, from, to) {
|
|
1203
|
+
if (Array.isArray(value)) return value.map((entry) => rename(entry, from, to));
|
|
1204
|
+
if (!isRecord(value)) return value;
|
|
1205
|
+
if (UNSAFE_KEYS2.has(from) || UNSAFE_KEYS2.has(to)) return value;
|
|
1206
|
+
if (!Object.prototype.hasOwnProperty.call(value, from)) return value;
|
|
1207
|
+
const out = {};
|
|
1208
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1209
|
+
if (key === from) out[to] = entry;
|
|
1210
|
+
else if (key !== to) out[key] = entry;
|
|
783
1211
|
}
|
|
784
|
-
|
|
785
|
-
|
|
1212
|
+
return out;
|
|
1213
|
+
}
|
|
1214
|
+
function applyOp(value, op) {
|
|
1215
|
+
if (op.op === "flatten") return valueAt(value, op.path);
|
|
1216
|
+
const target = op.path === void 0 ? value : valueAt(value, op.path);
|
|
1217
|
+
if (op.path !== void 0 && target === void 0) return value;
|
|
1218
|
+
let next;
|
|
1219
|
+
if (op.op === "pick") next = pick(target, op.keys);
|
|
1220
|
+
else if (op.op === "rename") next = rename(target, op.from, op.to);
|
|
1221
|
+
else if (op.op === "filter") {
|
|
1222
|
+
next = Array.isArray(target) ? target.filter((entry) => isRecord(entry) && valueAt(entry, op.key) === op.equals) : target;
|
|
1223
|
+
} else {
|
|
1224
|
+
next = Array.isArray(target) ? target.map((entry) => applyPostReceive(entry, op.ops)) : target;
|
|
786
1225
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1226
|
+
return op.path === void 0 ? next : withValueAt(value, op.path, next);
|
|
1227
|
+
}
|
|
1228
|
+
function applyPostReceive(value, ops) {
|
|
1229
|
+
return (ops ?? []).reduce(applyOp, value);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// src/request.ts
|
|
1233
|
+
var MAX_ERROR_BODY = 500;
|
|
1234
|
+
var PLACEHOLDER = /\{\{\s*(args|config)\.([A-Za-z0-9_.-]+)\s*\}\}/g;
|
|
1235
|
+
var WHOLE_PLACEHOLDER = /^\{\{\s*(args|config)\.([A-Za-z0-9_.-]+)\s*\}\}$/;
|
|
1236
|
+
function lookup(source, path, scope) {
|
|
1237
|
+
return valueAt(source === "args" ? scope.args : scope.config, path);
|
|
1238
|
+
}
|
|
1239
|
+
var intoUrl = (value, source) => source === "config" ? value : encodeURIComponent(value);
|
|
1240
|
+
function intoHeader(name) {
|
|
1241
|
+
return (value) => {
|
|
1242
|
+
if (/[\r\n]/.test(value)) {
|
|
1243
|
+
throw new Error(`Header "${name}" would carry a line ending, which is not allowed`);
|
|
791
1244
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1245
|
+
return value;
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
function resolveTemplates(value, scope, substitute) {
|
|
1249
|
+
if (typeof value === "string") {
|
|
1250
|
+
const whole = WHOLE_PLACEHOLDER.exec(value);
|
|
1251
|
+
if (whole) {
|
|
1252
|
+
const resolved = lookup(whole[1], whole[2], scope);
|
|
1253
|
+
if (substitute === void 0 || resolved === void 0 || resolved === null) return resolved;
|
|
1254
|
+
return substitute(String(resolved), whole[1]);
|
|
797
1255
|
}
|
|
1256
|
+
return value.replace(PLACEHOLDER, (_match, source, path) => {
|
|
1257
|
+
const resolved = lookup(source, path, scope);
|
|
1258
|
+
if (resolved === void 0 || resolved === null) return "";
|
|
1259
|
+
const text = String(resolved);
|
|
1260
|
+
return substitute === void 0 ? text : substitute(text, source);
|
|
1261
|
+
});
|
|
798
1262
|
}
|
|
799
|
-
if (
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
);
|
|
1263
|
+
if (Array.isArray(value)) return value.map((entry) => resolveTemplates(entry, scope, substitute));
|
|
1264
|
+
if (typeof value === "object" && value !== null) {
|
|
1265
|
+
const out = {};
|
|
1266
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1267
|
+
out[key] = resolveTemplates(entry, scope, substitute);
|
|
805
1268
|
}
|
|
1269
|
+
return out;
|
|
806
1270
|
}
|
|
1271
|
+
return value;
|
|
807
1272
|
}
|
|
808
|
-
function
|
|
809
|
-
|
|
810
|
-
|
|
1273
|
+
function resolveHeaders(raw, scope) {
|
|
1274
|
+
const out = {};
|
|
1275
|
+
for (const [name, value] of Object.entries(raw ?? {})) {
|
|
1276
|
+
const resolved = resolveTemplates(value, scope, intoHeader(name));
|
|
1277
|
+
if (resolved === void 0 || resolved === null || resolved === "") continue;
|
|
1278
|
+
out[name] = String(resolved);
|
|
1279
|
+
}
|
|
1280
|
+
return out;
|
|
811
1281
|
}
|
|
812
|
-
function
|
|
813
|
-
|
|
814
|
-
|
|
1282
|
+
function stringMap(raw) {
|
|
1283
|
+
const out = {};
|
|
1284
|
+
if (typeof raw !== "object" || raw === null) return out;
|
|
1285
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
1286
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
1287
|
+
out[key] = String(value);
|
|
815
1288
|
}
|
|
816
|
-
|
|
817
|
-
|
|
1289
|
+
return out;
|
|
1290
|
+
}
|
|
1291
|
+
function resolveRequest(request, scope) {
|
|
1292
|
+
const method = (request.method ?? "GET").toUpperCase();
|
|
1293
|
+
const rawUrl = resolveTemplates(request.url, scope, intoUrl);
|
|
1294
|
+
if (typeof rawUrl !== "string" || rawUrl.trim() === "") {
|
|
1295
|
+
throw new Error("Request has no URL once its templates are resolved");
|
|
818
1296
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
}
|
|
824
|
-
for (const path of paths) {
|
|
825
|
-
if (typeof path !== "string" || !PATH_DATA_PATTERN.test(path)) {
|
|
826
|
-
throw new Error(
|
|
827
|
-
`Connector ${definition.id} has an icon path that is not SVG path data. Only path data is accepted, not markup.`
|
|
828
|
-
);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
if (viewBox !== void 0 && !VIEW_BOX_PATTERN.test(viewBox)) {
|
|
832
|
-
throw new Error(`Connector ${definition.id} has an icon viewBox that is not four numbers`);
|
|
833
|
-
}
|
|
1297
|
+
let url;
|
|
1298
|
+
try {
|
|
1299
|
+
url = new URL(rawUrl);
|
|
1300
|
+
} catch {
|
|
1301
|
+
throw new Error(`Request URL is not a URL once its templates are resolved: "${rawUrl}"`);
|
|
834
1302
|
}
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
if (triggers.length === 0 && actions.length === 0) {
|
|
838
|
-
throw new Error(`Connector ${definition.id} declares no triggers and no actions`);
|
|
1303
|
+
for (const [key, value] of Object.entries(stringMap(resolveTemplates(request.query, scope)))) {
|
|
1304
|
+
url.searchParams.set(key, value);
|
|
839
1305
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
if (declarative !== (loose.dedupe !== void 0)) {
|
|
851
|
-
throw new Error(
|
|
852
|
-
`Trigger ${trigger.type} needs fetch() and a dedupe strategy together, not one alone`
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
if (loose.dedupe !== void 0 && !DEDUPE_STRATEGIES.includes(loose.dedupe)) {
|
|
856
|
-
throw new Error(
|
|
857
|
-
`Trigger ${trigger.type} has unknown dedupe strategy ${JSON.stringify(loose.dedupe)}; expected ${DEDUPE_STRATEGIES.join(" or ")}`
|
|
858
|
-
);
|
|
859
|
-
}
|
|
860
|
-
if (loose.poll !== void 0 && !imperative) {
|
|
861
|
-
throw new Error(`Trigger ${trigger.type} declares poll but it is not a function`);
|
|
862
|
-
}
|
|
863
|
-
if (!declarative && !imperative) {
|
|
864
|
-
throw new Error(`Trigger ${trigger.type} is missing a fetch() or poll() implementation`);
|
|
1306
|
+
const headers = resolveHeaders(request.headers, scope);
|
|
1307
|
+
const resolved = { url: url.toString(), method, headers };
|
|
1308
|
+
if (request.body !== void 0 && method !== "GET" && method !== "HEAD") {
|
|
1309
|
+
const body = resolveTemplates(request.body, scope);
|
|
1310
|
+
if (body !== void 0) {
|
|
1311
|
+
resolved.body = typeof body === "string" ? body : JSON.stringify(body);
|
|
1312
|
+
if (!Object.keys(headers).some((key) => key.toLowerCase() === "content-type")) {
|
|
1313
|
+
resolved.headers["content-type"] = typeof body === "string" ? "text/plain" : "application/json";
|
|
1314
|
+
}
|
|
865
1315
|
}
|
|
866
1316
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1317
|
+
return resolved;
|
|
1318
|
+
}
|
|
1319
|
+
async function readBody(response) {
|
|
1320
|
+
const text = await response.text();
|
|
1321
|
+
if (text === "") return void 0;
|
|
1322
|
+
const type = response.headers.get("content-type") ?? "";
|
|
1323
|
+
if (!type.includes("json")) return text;
|
|
1324
|
+
try {
|
|
1325
|
+
return JSON.parse(text);
|
|
1326
|
+
} catch {
|
|
1327
|
+
return text;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function describeFailure(response, body) {
|
|
1331
|
+
const detail = typeof body === "string" ? body : body === void 0 ? "" : JSON.stringify(body);
|
|
1332
|
+
const quoted = detail.length > MAX_ERROR_BODY ? `${detail.slice(0, MAX_ERROR_BODY)}\u2026` : detail;
|
|
1333
|
+
return `Request failed with ${response.status} ${response.statusText}${quoted ? `: ${quoted}` : ""}`;
|
|
1334
|
+
}
|
|
1335
|
+
async function sendRequest(resolved, options) {
|
|
1336
|
+
const response = await options.fetchImpl(resolved.url, {
|
|
1337
|
+
method: resolved.method,
|
|
1338
|
+
headers: resolved.headers,
|
|
1339
|
+
...resolved.body !== void 0 && { body: resolved.body }
|
|
1340
|
+
});
|
|
1341
|
+
const body = await readBody(response);
|
|
1342
|
+
if (!response.ok) throw new Error(describeFailure(response, body));
|
|
1343
|
+
return { response, body };
|
|
1344
|
+
}
|
|
1345
|
+
function asOutput(value) {
|
|
1346
|
+
if (Array.isArray(value)) return { items: value };
|
|
1347
|
+
if (typeof value === "object" && value !== null) return value;
|
|
1348
|
+
return value === void 0 ? {} : { result: value };
|
|
1349
|
+
}
|
|
1350
|
+
var MAX_REQUEST_PAGES = 100;
|
|
1351
|
+
var LINK_NEXT = /<([^>]+)>\s*;[^,]*\brel\s*=\s*"?next"?/i;
|
|
1352
|
+
function nextLink(header) {
|
|
1353
|
+
const match = header === null ? null : LINK_NEXT.exec(header);
|
|
1354
|
+
return match ? match[1] : void 0;
|
|
1355
|
+
}
|
|
1356
|
+
function pageItems(body, itemsPath) {
|
|
1357
|
+
const value = itemsPath === void 0 ? body : valueAt(body, itemsPath);
|
|
1358
|
+
return Array.isArray(value) ? value : void 0;
|
|
1359
|
+
}
|
|
1360
|
+
async function collectPages(request, strategy, scope, options) {
|
|
1361
|
+
const collected = [];
|
|
1362
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1363
|
+
let page2 = strategy.kind === "page" ? strategy.startPage ?? 1 : 0;
|
|
1364
|
+
let cursor;
|
|
1365
|
+
let nextUrl;
|
|
1366
|
+
for (let index = 0; index < MAX_REQUEST_PAGES; index++) {
|
|
1367
|
+
const resolved = resolveRequest(request, scope);
|
|
1368
|
+
if (nextUrl !== void 0) resolved.url = nextUrl;
|
|
1369
|
+
if (strategy.kind === "cursor" && cursor !== void 0) {
|
|
1370
|
+
const url = new URL(resolved.url);
|
|
1371
|
+
url.searchParams.set(strategy.param, cursor);
|
|
1372
|
+
resolved.url = url.toString();
|
|
870
1373
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
throw new Error(`Action ${action.type} declares both run() and a request; pick one`);
|
|
1374
|
+
if (strategy.kind === "page") {
|
|
1375
|
+
const url = new URL(resolved.url);
|
|
1376
|
+
url.searchParams.set(strategy.param, String(page2));
|
|
1377
|
+
resolved.url = url.toString();
|
|
876
1378
|
}
|
|
877
|
-
if (
|
|
878
|
-
throw new Error(`
|
|
1379
|
+
if (seen.has(resolved.url)) {
|
|
1380
|
+
throw new Error(`Request for ${request.url} asked for the same page twice`);
|
|
879
1381
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
throw new Error(
|
|
888
|
-
`Action ${action.type} declares the request URL "${url}", which is neither absolute nor rooted in a {{config.\u2026}} value`
|
|
889
|
-
);
|
|
1382
|
+
seen.add(resolved.url);
|
|
1383
|
+
const { response, body } = await sendRequest(resolved, options);
|
|
1384
|
+
const items = pageItems(body, strategy.itemsPath);
|
|
1385
|
+
if (items === void 0) {
|
|
1386
|
+
if (index === 0) {
|
|
1387
|
+
const where = strategy.itemsPath === void 0 ? "the response is not a list" : `the response has no list at "${strategy.itemsPath}"`;
|
|
1388
|
+
throw new Error(`Cannot page through this request: ${where}`);
|
|
890
1389
|
}
|
|
1390
|
+
return collected;
|
|
891
1391
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
if (
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
);
|
|
900
|
-
}
|
|
1392
|
+
collected.push(...items);
|
|
1393
|
+
if (items.length === 0) return collected;
|
|
1394
|
+
if (strategy.kind === "cursor") {
|
|
1395
|
+
const next = valueAt(body, strategy.cursorPath);
|
|
1396
|
+
if (next === void 0 || next === null || next === "") return collected;
|
|
1397
|
+
cursor = String(next);
|
|
1398
|
+
continue;
|
|
901
1399
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
triggers.map((trigger) => trigger.type)
|
|
906
|
-
);
|
|
907
|
-
assertUnique(
|
|
908
|
-
"action",
|
|
909
|
-
actions.map((action) => action.type)
|
|
910
|
-
);
|
|
911
|
-
assertUnique(
|
|
912
|
-
"config field",
|
|
913
|
-
(definition.config ?? []).map((field) => field.key)
|
|
914
|
-
);
|
|
915
|
-
assertAuth(definition);
|
|
916
|
-
return {
|
|
917
|
-
...definition,
|
|
918
|
-
version: definition.version ?? "0.0.0",
|
|
919
|
-
config: definition.config ?? [],
|
|
920
|
-
triggers,
|
|
921
|
-
actions
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
function resolveConfig(connector, env = process.env) {
|
|
925
|
-
const config = {};
|
|
926
|
-
const missing = [];
|
|
927
|
-
for (const field of connector.config) {
|
|
928
|
-
const name = envNameFor(field.key, field.env);
|
|
929
|
-
const value = env[name] ?? field.default;
|
|
930
|
-
if (value === void 0 || value === "") {
|
|
931
|
-
if (field.required) missing.push(`${field.key} (${name})`);
|
|
1400
|
+
if (strategy.kind === "link") {
|
|
1401
|
+
nextUrl = nextLink(response.headers.get("link"));
|
|
1402
|
+
if (nextUrl === void 0) return collected;
|
|
932
1403
|
continue;
|
|
933
1404
|
}
|
|
934
|
-
|
|
1405
|
+
page2 += 1;
|
|
935
1406
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1407
|
+
throw new Error(`Request for ${request.url} exceeded ${MAX_REQUEST_PAGES} pages`);
|
|
1408
|
+
}
|
|
1409
|
+
async function executeRequest(request, postReceive, scope, options) {
|
|
1410
|
+
if (request.paginate) {
|
|
1411
|
+
const items = await collectPages(request, request.paginate, scope, options);
|
|
1412
|
+
return asOutput(applyPostReceive(items, postReceive));
|
|
940
1413
|
}
|
|
941
|
-
|
|
1414
|
+
const { body } = await sendRequest(resolveRequest(request, scope), options);
|
|
1415
|
+
return asOutput(applyPostReceive(body, postReceive));
|
|
942
1416
|
}
|
|
943
1417
|
|
|
944
|
-
// src/
|
|
945
|
-
|
|
946
|
-
|
|
1418
|
+
// src/resilience.ts
|
|
1419
|
+
var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
1420
|
+
var DEFAULT_ATTEMPTS = 3;
|
|
1421
|
+
var DEFAULT_BASE_DELAY_MS = 250;
|
|
1422
|
+
var DEFAULT_MAX_DELAY_MS = 3e4;
|
|
1423
|
+
var MAX_ATTEMPTS = 10;
|
|
1424
|
+
var MAX_TOTAL_WAIT_MS = 12e4;
|
|
1425
|
+
var wait = (ms) => new Promise((resolve4) => {
|
|
1426
|
+
setTimeout(resolve4, ms);
|
|
1427
|
+
});
|
|
1428
|
+
function retryAfterMs(header, now) {
|
|
1429
|
+
if (!header) return void 0;
|
|
1430
|
+
const trimmed = header.trim();
|
|
1431
|
+
if (trimmed === "") return void 0;
|
|
1432
|
+
const seconds = Number(trimmed);
|
|
1433
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
1434
|
+
const at = Date.parse(trimmed);
|
|
1435
|
+
if (Number.isNaN(at)) return void 0;
|
|
1436
|
+
return Math.max(0, at - now);
|
|
947
1437
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1438
|
+
function backoffMs(attempt, policy = {}) {
|
|
1439
|
+
const base = policy.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
1440
|
+
const max = policy.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
1441
|
+
return Math.min(max, base * 2 ** attempt);
|
|
1442
|
+
}
|
|
1443
|
+
function resilientFetch(options) {
|
|
1444
|
+
const attempts = Math.min(MAX_ATTEMPTS, Math.max(1, options.retry?.attempts ?? DEFAULT_ATTEMPTS));
|
|
1445
|
+
const sleep = options.sleep ?? wait;
|
|
1446
|
+
const ceiling = options.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
1447
|
+
const send = async (input, init) => {
|
|
1448
|
+
let waited = 0;
|
|
1449
|
+
const pause = async (ms) => {
|
|
1450
|
+
if (waited + ms > MAX_TOTAL_WAIT_MS) return false;
|
|
1451
|
+
waited += ms;
|
|
1452
|
+
await sleep(ms);
|
|
1453
|
+
return true;
|
|
1454
|
+
};
|
|
1455
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
1456
|
+
const last = attempt === attempts - 1;
|
|
1457
|
+
try {
|
|
1458
|
+
const response = await options.fetchImpl(input, init);
|
|
1459
|
+
if (!RETRYABLE_STATUS.has(response.status)) return response;
|
|
1460
|
+
if (!options.retryable || last) return response;
|
|
1461
|
+
const asked = retryAfterMs(response.headers.get("retry-after"), Date.now());
|
|
1462
|
+
const delay = asked === void 0 ? backoffMs(attempt, options.retry) : Math.min(asked, ceiling);
|
|
1463
|
+
if (!await pause(delay)) return response;
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
if (!options.retryable || last) throw error;
|
|
1466
|
+
if (!await pause(backoffMs(attempt, options.retry))) throw error;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
throw new Error("Request was never attempted");
|
|
1470
|
+
};
|
|
1471
|
+
return send;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// src/runtime.ts
|
|
1475
|
+
var MAX_POLL_PAGES = 1e3;
|
|
1476
|
+
async function runPoll(connector, triggerType, options = {}) {
|
|
952
1477
|
const trigger = connector.triggers.find((entry) => entry.type === triggerType);
|
|
953
1478
|
if (!trigger) {
|
|
954
1479
|
throw new Error(`Connector ${connector.id} has no trigger "${triggerType}"`);
|
|
955
1480
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
required: field.required === true,
|
|
972
|
-
secret: field.secret === true,
|
|
973
|
-
...field.description !== void 0 && { description: field.description },
|
|
974
|
-
...field.builderHint !== void 0 && { builderHint: field.builderHint }
|
|
975
|
-
}))
|
|
1481
|
+
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
1482
|
+
const polledAt = now();
|
|
1483
|
+
const context = {
|
|
1484
|
+
config: options.config ?? {},
|
|
1485
|
+
...options.since !== void 0 && { since: options.since },
|
|
1486
|
+
...options.cursor !== void 0 && { cursor: options.cursor },
|
|
1487
|
+
...options.limit !== void 0 && { limit: options.limit },
|
|
1488
|
+
now,
|
|
1489
|
+
// A poll only reads, so every failure it meets is worth trying again.
|
|
1490
|
+
fetch: resilientFetch({
|
|
1491
|
+
fetchImpl: options.fetchImpl ?? globalThis.fetch,
|
|
1492
|
+
retryable: true,
|
|
1493
|
+
...options.retry !== void 0 && { retry: options.retry },
|
|
1494
|
+
...options.sleep !== void 0 && { sleep: options.sleep }
|
|
1495
|
+
})
|
|
976
1496
|
};
|
|
977
|
-
|
|
978
|
-
|
|
1497
|
+
const outcome = typeof trigger.poll === "function" ? await trigger.poll(context) : await pollWithDedupe(trigger, context);
|
|
1498
|
+
if (!outcome || !Array.isArray(outcome.items)) {
|
|
1499
|
+
throw new Error(`Trigger ${triggerType} did not return an items array`);
|
|
1500
|
+
}
|
|
1501
|
+
if (outcome.hasMore && !outcome.nextCursor) {
|
|
1502
|
+
throw new Error(`Trigger ${triggerType} reported more pages without a nextCursor`);
|
|
1503
|
+
}
|
|
979
1504
|
return {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
...connector.description !== void 0 && { description: connector.description },
|
|
984
|
-
...connector.icon !== void 0 && { icon: connector.icon },
|
|
985
|
-
...connector.auth !== void 0 && { auth: connector.auth },
|
|
986
|
-
triggers: connector.triggers.map((trigger) => ({
|
|
987
|
-
type: trigger.type,
|
|
988
|
-
label: trigger.label,
|
|
989
|
-
...trigger.description !== void 0 && { description: trigger.description },
|
|
990
|
-
// Carried through so the app can seed a connection's status mapping and
|
|
991
|
-
// its polling workflow. Absent when the connector said nothing, which is
|
|
992
|
-
// different from saying there is nothing.
|
|
993
|
-
...trigger.statusMapping !== void 0 && { statusMapping: trigger.statusMapping },
|
|
994
|
-
...trigger.defaultWorkflow !== void 0 && { defaultWorkflow: trigger.defaultWorkflow },
|
|
995
|
-
setup: connectionSetup(connector, trigger.type)
|
|
996
|
-
})),
|
|
997
|
-
actions: connector.actions.map((action) => ({
|
|
998
|
-
type: action.type,
|
|
999
|
-
label: action.label,
|
|
1000
|
-
...action.description !== void 0 && { description: action.description },
|
|
1001
|
-
inputs: (action.inputs ?? []).map((input) => ({
|
|
1002
|
-
key: input.key,
|
|
1003
|
-
label: input.label,
|
|
1004
|
-
type: input.type ?? "string",
|
|
1005
|
-
required: input.required === true,
|
|
1006
|
-
...input.options !== void 0 && { options: input.options },
|
|
1007
|
-
...input.loadOptions !== void 0 && { loadOptions: input.loadOptions },
|
|
1008
|
-
...input.builderHint !== void 0 && { builderHint: input.builderHint }
|
|
1009
|
-
})),
|
|
1010
|
-
...action.outputs !== void 0 && { outputs: action.outputs },
|
|
1011
|
-
...action.sample !== void 0 && { sample: action.sample }
|
|
1012
|
-
}))
|
|
1505
|
+
items: normalizeItems(outcome.items, polledAt),
|
|
1506
|
+
...outcome.nextCursor !== void 0 && { nextCursor: outcome.nextCursor },
|
|
1507
|
+
hasMore: outcome.hasMore === true
|
|
1013
1508
|
};
|
|
1014
1509
|
}
|
|
1510
|
+
async function drainPoll(connector, triggerType, options = {}) {
|
|
1511
|
+
const collected = [];
|
|
1512
|
+
let cursor = options.cursor;
|
|
1513
|
+
for (let page2 = 0; page2 < MAX_POLL_PAGES; page2++) {
|
|
1514
|
+
const result = await runPoll(connector, triggerType, {
|
|
1515
|
+
...options,
|
|
1516
|
+
...cursor !== void 0 && { cursor }
|
|
1517
|
+
});
|
|
1518
|
+
collected.push(...result.items);
|
|
1519
|
+
if (!result.hasMore) return collected;
|
|
1520
|
+
if (result.nextCursor === cursor) {
|
|
1521
|
+
throw new Error(`Trigger ${triggerType} did not advance its cursor`);
|
|
1522
|
+
}
|
|
1523
|
+
cursor = result.nextCursor;
|
|
1524
|
+
}
|
|
1525
|
+
throw new Error(`Trigger ${triggerType} exceeded ${MAX_POLL_PAGES} pages`);
|
|
1526
|
+
}
|
|
1527
|
+
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD"]);
|
|
1528
|
+
async function runOptions(connector, name, options = {}) {
|
|
1529
|
+
const loader = connector.options?.[name];
|
|
1530
|
+
if (!loader) {
|
|
1531
|
+
throw new Error(`Connector ${connector.id} serves no options set "${name}"`);
|
|
1532
|
+
}
|
|
1533
|
+
const loaded = await loader({
|
|
1534
|
+
config: options.config ?? {},
|
|
1535
|
+
now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
|
|
1536
|
+
fetch: resilientFetch({
|
|
1537
|
+
fetchImpl: options.fetchImpl ?? globalThis.fetch,
|
|
1538
|
+
retryable: true,
|
|
1539
|
+
...options.retry !== void 0 && { retry: options.retry },
|
|
1540
|
+
...options.sleep !== void 0 && { sleep: options.sleep }
|
|
1541
|
+
})
|
|
1542
|
+
});
|
|
1543
|
+
if (!Array.isArray(loaded)) {
|
|
1544
|
+
throw new Error(`Options set "${name}" did not return an array`);
|
|
1545
|
+
}
|
|
1546
|
+
return loaded.map(
|
|
1547
|
+
(entry) => typeof entry === "string" ? { value: entry } : { ...entry, value: String(entry.value) }
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
var MAX_QUOTED_VALUE = 80;
|
|
1551
|
+
function quote(value) {
|
|
1552
|
+
return value.length > MAX_QUOTED_VALUE ? `${value.slice(0, MAX_QUOTED_VALUE)}\u2026` : value;
|
|
1553
|
+
}
|
|
1554
|
+
function coerceArg(value, type) {
|
|
1555
|
+
if (typeof value !== "string") return value;
|
|
1556
|
+
if (type === "number") {
|
|
1557
|
+
const parsed = Number(value);
|
|
1558
|
+
if (Number.isNaN(parsed)) throw new Error(`Expected a number, got "${quote(value)}"`);
|
|
1559
|
+
return parsed;
|
|
1560
|
+
}
|
|
1561
|
+
if (type === "boolean") {
|
|
1562
|
+
if (value === "true") return true;
|
|
1563
|
+
if (value === "false") return false;
|
|
1564
|
+
throw new Error(`Expected a boolean, got "${quote(value)}"`);
|
|
1565
|
+
}
|
|
1566
|
+
if (type === "json") {
|
|
1567
|
+
try {
|
|
1568
|
+
return JSON.parse(value);
|
|
1569
|
+
} catch {
|
|
1570
|
+
throw new Error(`Expected JSON, got "${quote(value)}"`);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
return value;
|
|
1574
|
+
}
|
|
1575
|
+
async function runAction(connector, actionType, args, options = {}) {
|
|
1576
|
+
const action = connector.actions.find((entry) => entry.type === actionType);
|
|
1577
|
+
if (!action) {
|
|
1578
|
+
throw new Error(`Connector ${connector.id} has no action "${actionType}"`);
|
|
1579
|
+
}
|
|
1580
|
+
const coerced = { ...args };
|
|
1581
|
+
for (const input of action.inputs ?? []) {
|
|
1582
|
+
const value = coerced[input.key];
|
|
1583
|
+
if (value === void 0 || value === "") {
|
|
1584
|
+
if (input.required) throw new Error(`Action ${actionType} requires "${input.key}"`);
|
|
1585
|
+
delete coerced[input.key];
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
try {
|
|
1589
|
+
coerced[input.key] = coerceArg(value, input.type);
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
throw new Error(
|
|
1592
|
+
`Action ${actionType} argument "${input.key}": ${error instanceof Error ? error.message : String(error)}`,
|
|
1593
|
+
{ cause: error }
|
|
1594
|
+
);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
const config = options.config ?? {};
|
|
1598
|
+
const method = (action.request?.method ?? "GET").toUpperCase();
|
|
1599
|
+
const retryable = action.idempotent === true || action.request !== void 0 && SAFE_METHODS.has(method);
|
|
1600
|
+
const fetchImpl = resilientFetch({
|
|
1601
|
+
fetchImpl: options.fetchImpl ?? globalThis.fetch,
|
|
1602
|
+
retryable,
|
|
1603
|
+
...options.retry !== void 0 && { retry: options.retry },
|
|
1604
|
+
...options.sleep !== void 0 && { sleep: options.sleep }
|
|
1605
|
+
});
|
|
1606
|
+
if (action.request !== void 0) {
|
|
1607
|
+
try {
|
|
1608
|
+
return await executeRequest(
|
|
1609
|
+
action.request,
|
|
1610
|
+
action.postReceive,
|
|
1611
|
+
{ args: coerced, config },
|
|
1612
|
+
{ fetchImpl }
|
|
1613
|
+
);
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
throw new Error(
|
|
1616
|
+
`Action ${actionType}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1617
|
+
{ cause: error }
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
if (typeof action.run !== "function") {
|
|
1622
|
+
throw new Error(`Action ${actionType} has neither a run() implementation nor a request`);
|
|
1623
|
+
}
|
|
1624
|
+
const output = await action.run(coerced, {
|
|
1625
|
+
config,
|
|
1626
|
+
now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()),
|
|
1627
|
+
fetch: fetchImpl
|
|
1628
|
+
});
|
|
1629
|
+
return output ?? {};
|
|
1630
|
+
}
|
|
1015
1631
|
|
|
1016
1632
|
// src/harness.ts
|
|
1017
1633
|
function matches(route, method, url) {
|
|
@@ -1109,8 +1725,44 @@ function createConnectorHarness(connector, harnessOptions = {}) {
|
|
|
1109
1725
|
withMockHttp
|
|
1110
1726
|
};
|
|
1111
1727
|
}
|
|
1728
|
+
var HOST_FIXTURES = {
|
|
1729
|
+
diff: async () => "diff --git a/src/index.ts b/src/index.ts\n@@ -1 +1 @@\n-const a = 1\n+const a = 2\n",
|
|
1730
|
+
status: async () => " M src/index.ts\n",
|
|
1731
|
+
output: async () => "$ yarn test\n Test Files 1 passed (1)\n",
|
|
1732
|
+
selection: async () => "",
|
|
1733
|
+
send: async () => {
|
|
1734
|
+
},
|
|
1735
|
+
rename: async () => {
|
|
1736
|
+
},
|
|
1737
|
+
usage: async () => ({
|
|
1738
|
+
contextTokens: 13e4,
|
|
1739
|
+
contextWindow: 1e6,
|
|
1740
|
+
cacheHitRate: 0.93,
|
|
1741
|
+
limits: [{ window: "5h", remaining: 0.72 }]
|
|
1742
|
+
})
|
|
1743
|
+
};
|
|
1744
|
+
function mockExtensionHost(granted, answers = {}) {
|
|
1745
|
+
const allowed = new Set(granted);
|
|
1746
|
+
const used = /* @__PURE__ */ new Set();
|
|
1747
|
+
const host = {};
|
|
1748
|
+
for (const name of Object.keys(HOST_FIXTURES)) {
|
|
1749
|
+
const permission = HOST_PERMISSIONS[name];
|
|
1750
|
+
host[name] = async (...args) => {
|
|
1751
|
+
used.add(permission);
|
|
1752
|
+
if (!allowed.has(permission)) {
|
|
1753
|
+
throw new PermissionDeniedError(name, `this extension does not ask for ${permission}`);
|
|
1754
|
+
}
|
|
1755
|
+
const answer = answers[name] ?? HOST_FIXTURES[name];
|
|
1756
|
+
return answer(...args);
|
|
1757
|
+
};
|
|
1758
|
+
}
|
|
1759
|
+
return { host, used };
|
|
1760
|
+
}
|
|
1112
1761
|
|
|
1113
1762
|
// src/check.ts
|
|
1763
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1764
|
+
import { rm } from "fs/promises";
|
|
1765
|
+
import { resolve as resolve2, sep } from "path";
|
|
1114
1766
|
var EXECUTABLE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
1115
1767
|
var CREDENTIAL_NAME = /(secret|token|password|passphrase|api[-_]?key|credential)/i;
|
|
1116
1768
|
var INPUT_TYPES = /* @__PURE__ */ new Set(["string", "number", "boolean", "select", "json"]);
|
|
@@ -1204,7 +1856,17 @@ function actionShapeFindings(action) {
|
|
|
1204
1856
|
}
|
|
1205
1857
|
return found;
|
|
1206
1858
|
}
|
|
1207
|
-
|
|
1859
|
+
function bundles(options) {
|
|
1860
|
+
return Boolean(options.bundle && options.entry !== void 0 && options.packageDir !== void 0);
|
|
1861
|
+
}
|
|
1862
|
+
function launches(options) {
|
|
1863
|
+
return bundles(options) && options.mock === true;
|
|
1864
|
+
}
|
|
1865
|
+
function packageRootOf(options) {
|
|
1866
|
+
if (options.packageDir === void 0) return void 0;
|
|
1867
|
+
return packageRootFor(packageDirFor(options.packageDir, options.entry));
|
|
1868
|
+
}
|
|
1869
|
+
async function packageFindings(connector, options) {
|
|
1208
1870
|
if (options.packageDir === void 0) return [];
|
|
1209
1871
|
const pkg = readNearestPackageJson(packageDirFor(options.packageDir, options.entry));
|
|
1210
1872
|
const found = [...lifecycleScriptFindings(pkg)];
|
|
@@ -1225,7 +1887,15 @@ async function packageFindings(options) {
|
|
|
1225
1887
|
contents: packEntryContents(options.entry),
|
|
1226
1888
|
resolveDir: options.packageDir
|
|
1227
1889
|
});
|
|
1228
|
-
found.push(...bundleDependencyFindings(built.external));
|
|
1890
|
+
found.push(...bundleDependencyFindings(built.external), ...bundledRequireFindings(built.code));
|
|
1891
|
+
if (options.mock) {
|
|
1892
|
+
const dir = await stagePack(connector, built.code, packageRootOf(options));
|
|
1893
|
+
try {
|
|
1894
|
+
found.push(...await packLaunchFindings(dir));
|
|
1895
|
+
} finally {
|
|
1896
|
+
await rm(dir, { recursive: true, force: true });
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1229
1899
|
}
|
|
1230
1900
|
return found;
|
|
1231
1901
|
}
|
|
@@ -1290,6 +1960,153 @@ async function mockFindings(connector, options) {
|
|
|
1290
1960
|
}
|
|
1291
1961
|
return found;
|
|
1292
1962
|
}
|
|
1963
|
+
var CHECK_SESSION = {
|
|
1964
|
+
sessionId: "check",
|
|
1965
|
+
worktreePath: process.cwd(),
|
|
1966
|
+
agent: "claude"
|
|
1967
|
+
};
|
|
1968
|
+
var FOOTER_TONES = ["default", "ok", "danger"];
|
|
1969
|
+
var FOOTER_HREF_PROTOCOLS = ["http:", "https:"];
|
|
1970
|
+
function invalidItem(item) {
|
|
1971
|
+
if (!item || typeof item !== "object") return "is not an object";
|
|
1972
|
+
const reading = item;
|
|
1973
|
+
if (typeof reading.label !== "string" || reading.label === "") return "has no label";
|
|
1974
|
+
if (typeof reading.value !== "string") return "has no value";
|
|
1975
|
+
if (reading.tone !== void 0 && !FOOTER_TONES.includes(reading.tone)) {
|
|
1976
|
+
return `has unknown tone ${JSON.stringify(reading.tone)}`;
|
|
1977
|
+
}
|
|
1978
|
+
if (reading.href === void 0) return void 0;
|
|
1979
|
+
if (typeof reading.href !== "string") return "has a link that is not text";
|
|
1980
|
+
let protocol;
|
|
1981
|
+
try {
|
|
1982
|
+
protocol = new URL(reading.href).protocol;
|
|
1983
|
+
} catch {
|
|
1984
|
+
return `has the link ${JSON.stringify(reading.href)}, which is not a URL`;
|
|
1985
|
+
}
|
|
1986
|
+
if (!FOOTER_HREF_PROTOCOLS.includes(protocol)) {
|
|
1987
|
+
return `has the link ${JSON.stringify(reading.href)}; a reading links to ${FOOTER_HREF_PROTOCOLS.join(" or ")} and nothing else`;
|
|
1988
|
+
}
|
|
1989
|
+
return void 0;
|
|
1990
|
+
}
|
|
1991
|
+
async function contributionFindings(connector) {
|
|
1992
|
+
const contributes = connector.contributes;
|
|
1993
|
+
if (!contributes) return [];
|
|
1994
|
+
const found = [];
|
|
1995
|
+
const declared = connector.permissions ?? [];
|
|
1996
|
+
const spent = /* @__PURE__ */ new Set();
|
|
1997
|
+
const ran = async (kind, id, code, body) => {
|
|
1998
|
+
const { host, used } = mockExtensionHost(declared);
|
|
1999
|
+
try {
|
|
2000
|
+
return await body(host);
|
|
2001
|
+
} catch (error) {
|
|
2002
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2003
|
+
found.push(
|
|
2004
|
+
finding2(
|
|
2005
|
+
"error",
|
|
2006
|
+
error instanceof PermissionDeniedError ? "permission-undeclared" : code,
|
|
2007
|
+
`${kind} ${id}`,
|
|
2008
|
+
error instanceof PermissionDeniedError ? `asked the host for something this extension does not declare: ${reason}` : `threw: ${reason}`
|
|
2009
|
+
)
|
|
2010
|
+
);
|
|
2011
|
+
return void 0;
|
|
2012
|
+
} finally {
|
|
2013
|
+
for (const permission of used) spent.add(permission);
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
for (const footer of contributes.footers ?? []) {
|
|
2017
|
+
const items = await ran(
|
|
2018
|
+
"footer",
|
|
2019
|
+
footer.id,
|
|
2020
|
+
"footer-failed",
|
|
2021
|
+
(host) => Promise.resolve(footer.run({ ...CHECK_SESSION, host, now: () => (/* @__PURE__ */ new Date()).toISOString() }))
|
|
2022
|
+
);
|
|
2023
|
+
if (items === void 0) continue;
|
|
2024
|
+
if (!Array.isArray(items)) {
|
|
2025
|
+
found.push(
|
|
2026
|
+
finding2(
|
|
2027
|
+
"error",
|
|
2028
|
+
"footer-items-invalid",
|
|
2029
|
+
`footer ${footer.id}`,
|
|
2030
|
+
"returned something that is not a list of readings"
|
|
2031
|
+
)
|
|
2032
|
+
);
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
for (const item of items) {
|
|
2036
|
+
const wrong = invalidItem(item);
|
|
2037
|
+
if (wrong) {
|
|
2038
|
+
found.push(
|
|
2039
|
+
finding2(
|
|
2040
|
+
"error",
|
|
2041
|
+
"footer-items-invalid",
|
|
2042
|
+
`footer ${footer.id}`,
|
|
2043
|
+
`returned a reading that ${wrong}`
|
|
2044
|
+
)
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
for (const handler of contributes.linkHandlers ?? []) {
|
|
2050
|
+
await ran(
|
|
2051
|
+
"link handler",
|
|
2052
|
+
handler.id,
|
|
2053
|
+
"handler-failed",
|
|
2054
|
+
(host) => Promise.resolve(
|
|
2055
|
+
handler.run({
|
|
2056
|
+
...CHECK_SESSION,
|
|
2057
|
+
host,
|
|
2058
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
2059
|
+
url: handler.example
|
|
2060
|
+
})
|
|
2061
|
+
)
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
const observable = !(contributes.panes ?? []).some((pane) => pane.web !== void 0);
|
|
2065
|
+
for (const permission of declared) {
|
|
2066
|
+
if (observable && !spent.has(permission)) {
|
|
2067
|
+
found.push(
|
|
2068
|
+
finding2(
|
|
2069
|
+
"warn",
|
|
2070
|
+
"permission-unused",
|
|
2071
|
+
`${connector.id} permissions`,
|
|
2072
|
+
`asks for ${permission} but nothing this run exercised used it; ask only for what it spends`
|
|
2073
|
+
)
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
return found;
|
|
2078
|
+
}
|
|
2079
|
+
function paneFindings(connector, packageRoot) {
|
|
2080
|
+
if (packageRoot === void 0) return [];
|
|
2081
|
+
const root = resolve2(packageRoot);
|
|
2082
|
+
const found = [];
|
|
2083
|
+
for (const pane of connector.contributes?.panes ?? []) {
|
|
2084
|
+
if (pane.web === void 0) continue;
|
|
2085
|
+
const full = resolve2(root, pane.web);
|
|
2086
|
+
if (full !== root && !full.startsWith(`${root}${sep}`)) {
|
|
2087
|
+
found.push(
|
|
2088
|
+
finding2(
|
|
2089
|
+
"error",
|
|
2090
|
+
"web-entry-outside-package",
|
|
2091
|
+
`pane ${pane.id}`,
|
|
2092
|
+
`declares the page "${pane.web}", which resolves outside the package`
|
|
2093
|
+
)
|
|
2094
|
+
);
|
|
2095
|
+
continue;
|
|
2096
|
+
}
|
|
2097
|
+
if (!existsSync2(full)) {
|
|
2098
|
+
found.push(
|
|
2099
|
+
finding2(
|
|
2100
|
+
"error",
|
|
2101
|
+
"web-entry-missing",
|
|
2102
|
+
`pane ${pane.id}`,
|
|
2103
|
+
`declares the page "${pane.web}", which the package does not carry`
|
|
2104
|
+
)
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
return found;
|
|
2109
|
+
}
|
|
1293
2110
|
var AUTH_FAILURE = /\b(401|403|unauthor|unauthenticat|forbidden|invalid[- ]?(token|credential))/i;
|
|
1294
2111
|
function liveRunnable(action) {
|
|
1295
2112
|
if (action.idempotent !== true) return false;
|
|
@@ -1418,9 +2235,11 @@ async function checkConnector(connector, options = {}) {
|
|
|
1418
2235
|
)
|
|
1419
2236
|
);
|
|
1420
2237
|
}
|
|
1421
|
-
found.push(...authFindings(connector));
|
|
2238
|
+
if (connector.kind !== "extension") found.push(...authFindings(connector));
|
|
1422
2239
|
found.push(...secretFindings(connector));
|
|
1423
|
-
found.push(...
|
|
2240
|
+
found.push(...paneFindings(connector, packageRootOf(options)));
|
|
2241
|
+
found.push(...await contributionFindings(connector));
|
|
2242
|
+
found.push(...await packageFindings(connector, options));
|
|
1424
2243
|
found.push(...await mockFindings(connector, options));
|
|
1425
2244
|
found.push(...await liveFindings(connector, options));
|
|
1426
2245
|
const perTrigger = await Promise.all(
|
|
@@ -1516,15 +2335,30 @@ var CHECK_OWNERS = {
|
|
|
1516
2335
|
"mock-network-escape": "mock",
|
|
1517
2336
|
"mock-not-observed": "mock",
|
|
1518
2337
|
"preflight-failed": "live",
|
|
1519
|
-
"live-action-failed": "live"
|
|
2338
|
+
"live-action-failed": "live",
|
|
2339
|
+
"pack-launch": "launch",
|
|
2340
|
+
"web-entry-missing": "contributes",
|
|
2341
|
+
"web-entry-outside-package": "contributes",
|
|
2342
|
+
"footer-failed": "footers",
|
|
2343
|
+
"footer-items-invalid": "footers",
|
|
2344
|
+
"handler-failed": "handlers",
|
|
2345
|
+
"permission-undeclared": "permissions",
|
|
2346
|
+
"permission-unused": "permissions"
|
|
1520
2347
|
};
|
|
1521
2348
|
function checksRun(connector, options) {
|
|
1522
|
-
const names = ["manifest"
|
|
2349
|
+
const names = ["manifest"];
|
|
2350
|
+
if (connector.kind !== "extension") names.push("auth");
|
|
2351
|
+
const contributes = connector.contributes;
|
|
2352
|
+
if (contributes?.panes?.length && options.packageDir !== void 0) names.push("contributes");
|
|
2353
|
+
if (contributes?.footers?.length) names.push("footers");
|
|
2354
|
+
if (contributes?.linkHandlers?.length) names.push("handlers");
|
|
2355
|
+
if (connector.permissions !== void 0) names.push("permissions");
|
|
1523
2356
|
if (connector.config.length > 0) names.push("secrets");
|
|
1524
2357
|
if (connector.actions.length > 0) names.push("actions");
|
|
1525
2358
|
if (connector.triggers.length > 0) names.push("dedupe");
|
|
1526
2359
|
if (options.packageDir !== void 0) names.push("no-lifecycle-scripts", "keywords");
|
|
1527
|
-
if (options
|
|
2360
|
+
if (bundles(options)) names.push("no-runtime-deps");
|
|
2361
|
+
if (launches(options)) names.push("launch");
|
|
1528
2362
|
if (options.mock && connector.actions.length > 0) names.push("mock");
|
|
1529
2363
|
if (options.live && liveExamines(connector)) names.push("live");
|
|
1530
2364
|
return names;
|
|
@@ -1555,9 +2389,9 @@ function formatFindings(findings) {
|
|
|
1555
2389
|
}
|
|
1556
2390
|
|
|
1557
2391
|
// src/pack.ts
|
|
1558
|
-
import {
|
|
1559
|
-
import {
|
|
1560
|
-
import { join as join2, resolve as
|
|
2392
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2393
|
+
import { mkdir, rm as rm2, stat as stat2 } from "fs/promises";
|
|
2394
|
+
import { join as join2, resolve as resolve3 } from "path";
|
|
1561
2395
|
function finding3(code, target, message) {
|
|
1562
2396
|
return { level: "error", code, target, message };
|
|
1563
2397
|
}
|
|
@@ -1565,37 +2399,51 @@ function packFileName(connector) {
|
|
|
1565
2399
|
return `${connector.id}-${connector.version}.vorn.tgz`;
|
|
1566
2400
|
}
|
|
1567
2401
|
async function packConnector(connector, options) {
|
|
1568
|
-
const resolveDir =
|
|
2402
|
+
const resolveDir = resolve3(options.resolveDir ?? process.cwd());
|
|
1569
2403
|
const entryDir = packageDirFor(resolveDir, options.entry);
|
|
1570
|
-
const
|
|
2404
|
+
const packageRoot = packageRootFor(entryDir);
|
|
2405
|
+
const findings = await checkConnector(connector, { packageDir: packageRoot });
|
|
1571
2406
|
findings.push(...lifecycleScriptFindings(readNearestPackageJson(entryDir)));
|
|
1572
2407
|
if (findings.some((item) => item.level === "error")) return { findings };
|
|
1573
2408
|
const contents = packEntryContents(options.entry, options.sdkModule);
|
|
1574
2409
|
const bundle = options.bundle ?? esbuildBundle;
|
|
1575
2410
|
const built = await bundle({ contents, resolveDir });
|
|
1576
|
-
findings.push(...bundleDependencyFindings(built.external));
|
|
2411
|
+
findings.push(...bundleDependencyFindings(built.external), ...bundledRequireFindings(built.code));
|
|
1577
2412
|
if (findings.some((item) => item.level === "error")) return { findings };
|
|
1578
|
-
const outDir =
|
|
2413
|
+
const outDir = resolve3(options.outDir ?? process.cwd());
|
|
1579
2414
|
await mkdir(outDir, { recursive: true });
|
|
1580
2415
|
const file = join2(outDir, packFileName(connector));
|
|
1581
|
-
const staging = await
|
|
2416
|
+
const staging = await stagePack(connector, built.code, packageRoot);
|
|
1582
2417
|
try {
|
|
1583
|
-
await
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
2418
|
+
findings.push(...await (options.launch ?? packLaunchFindings)(staging));
|
|
2419
|
+
if (findings.some((item) => item.level === "error")) return { findings };
|
|
2420
|
+
const unpacked = await directoryBytes(staging);
|
|
2421
|
+
const maxUnpacked = options.maxUnpackedBytes ?? MAX_UNPACKED_BYTES;
|
|
2422
|
+
if (unpacked > maxUnpacked) {
|
|
2423
|
+
return {
|
|
2424
|
+
findings: [
|
|
2425
|
+
...findings,
|
|
2426
|
+
finding3(
|
|
2427
|
+
"pack-too-large",
|
|
2428
|
+
"bundle",
|
|
2429
|
+
`The pack unpacks to ${Math.round(unpacked / 1024)} KB; Vorn unpacks at most ${Math.round(maxUnpacked / 1024)} KB`
|
|
2430
|
+
)
|
|
2431
|
+
]
|
|
2432
|
+
};
|
|
2433
|
+
}
|
|
1590
2434
|
const { create } = await import("tar");
|
|
1591
|
-
await create({ gzip: true, file, cwd: staging }, [
|
|
2435
|
+
await create({ gzip: true, file, cwd: staging }, [
|
|
2436
|
+
"manifest.json",
|
|
2437
|
+
"index.js",
|
|
2438
|
+
...existsSync3(join2(staging, WEB_DIR)) ? [WEB_DIR] : []
|
|
2439
|
+
]);
|
|
1592
2440
|
} finally {
|
|
1593
|
-
await
|
|
2441
|
+
await rm2(staging, { recursive: true, force: true });
|
|
1594
2442
|
}
|
|
1595
|
-
const bytes = (await
|
|
2443
|
+
const bytes = (await stat2(file)).size;
|
|
1596
2444
|
const maxBytes = options.maxBytes ?? MAX_PACK_BYTES;
|
|
1597
2445
|
if (bytes > maxBytes) {
|
|
1598
|
-
await
|
|
2446
|
+
await rm2(file, { force: true });
|
|
1599
2447
|
return {
|
|
1600
2448
|
findings: [
|
|
1601
2449
|
...findings,
|
|
@@ -1612,7 +2460,7 @@ async function packConnector(connector, options) {
|
|
|
1612
2460
|
|
|
1613
2461
|
// src/scaffold.ts
|
|
1614
2462
|
var ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
|
|
1615
|
-
var SDK_DEPENDENCY_RANGE = "^0.7.0-beta.
|
|
2463
|
+
var SDK_DEPENDENCY_RANGE = "^0.7.0-beta.14";
|
|
1616
2464
|
var SCAFFOLD_VERSION = "0.1.0";
|
|
1617
2465
|
var VITEST_RANGE = "^4.1.10";
|
|
1618
2466
|
function jsonFile(value) {
|
|
@@ -1622,17 +2470,24 @@ function jsonFile(value) {
|
|
|
1622
2470
|
function titleCase(id) {
|
|
1623
2471
|
return id.split(/[-_]+/).filter((part) => part !== "").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
1624
2472
|
}
|
|
1625
|
-
function packageJson(id, description, inRepo) {
|
|
2473
|
+
function packageJson(id, description, inRepo, kind) {
|
|
2474
|
+
const scoped = kind === "extension" ? "extension" : "connector";
|
|
1626
2475
|
return jsonFile({
|
|
1627
|
-
name: inRepo ? `@vornrun
|
|
2476
|
+
name: inRepo ? `@vornrun/${scoped}-${id}` : `vorn-${scoped}-${id}`,
|
|
1628
2477
|
version: SCAFFOLD_VERSION,
|
|
1629
2478
|
description,
|
|
1630
2479
|
type: "module",
|
|
1631
2480
|
license: "MIT",
|
|
1632
|
-
bin: { [`vorn
|
|
2481
|
+
bin: { [`vorn-${scoped}-${id}`]: "dist/index.js" },
|
|
1633
2482
|
main: "./dist/index.js",
|
|
1634
2483
|
types: "./dist/index.d.ts",
|
|
1635
|
-
|
|
2484
|
+
// A pane's page ships beside the bundle, so `web` is published like `dist`.
|
|
2485
|
+
files: [
|
|
2486
|
+
"dist",
|
|
2487
|
+
"README.md",
|
|
2488
|
+
...kind === "extension" ? ["web"] : [],
|
|
2489
|
+
...inRepo ? ["CHANGELOG.md"] : []
|
|
2490
|
+
],
|
|
1636
2491
|
...inRepo && {
|
|
1637
2492
|
repository: {
|
|
1638
2493
|
type: "git",
|
|
@@ -1655,15 +2510,16 @@ function packageJson(id, description, inRepo) {
|
|
|
1655
2510
|
typescript: "^6.0.3",
|
|
1656
2511
|
vitest: VITEST_RANGE
|
|
1657
2512
|
},
|
|
1658
|
-
// Read by the catalog build: how this
|
|
2513
|
+
// Read by the catalog build: how this is filed, found, and what it asks of you.
|
|
1659
2514
|
vorn: {
|
|
1660
|
-
category: "Other",
|
|
2515
|
+
category: kind === "extension" ? "Extensions" : "Other",
|
|
1661
2516
|
keywords: [id],
|
|
1662
|
-
...
|
|
2517
|
+
...kind === "extension" && { kind: "extension" },
|
|
2518
|
+
...inRepo && kind === "connector" && { auth: "Say in one line what signing in takes." }
|
|
1663
2519
|
}
|
|
1664
2520
|
});
|
|
1665
2521
|
}
|
|
1666
|
-
function tsconfig() {
|
|
2522
|
+
function tsconfig(inRepo) {
|
|
1667
2523
|
return jsonFile({
|
|
1668
2524
|
compilerOptions: {
|
|
1669
2525
|
target: "ES2022",
|
|
@@ -1676,10 +2532,11 @@ function tsconfig() {
|
|
|
1676
2532
|
skipLibCheck: true,
|
|
1677
2533
|
types: ["node"],
|
|
1678
2534
|
noEmit: true,
|
|
2535
|
+
resolveJsonModule: true,
|
|
1679
2536
|
ignoreDeprecations: "6.0",
|
|
1680
2537
|
allowImportingTsExtensions: true
|
|
1681
2538
|
},
|
|
1682
|
-
include: ["src/**/*", "vitest.config.ts"]
|
|
2539
|
+
include: ["src/**/*", ...inRepo ? ["vitest.config.ts"] : []]
|
|
1683
2540
|
});
|
|
1684
2541
|
}
|
|
1685
2542
|
function tsupConfig() {
|
|
@@ -1712,12 +2569,14 @@ function changelog() {
|
|
|
1712
2569
|
}
|
|
1713
2570
|
function connectorSource(id, name, description) {
|
|
1714
2571
|
return `import { defineConnector } from '@vornrun/connector-sdk'
|
|
2572
|
+
// Bundled at build time: a pack is one file, so a version read from disk is not there to read.
|
|
2573
|
+
import pkg from '../package.json'
|
|
1715
2574
|
|
|
1716
2575
|
export const connector = defineConnector({
|
|
1717
2576
|
id: ${JSON.stringify(id)},
|
|
1718
2577
|
name: ${JSON.stringify(name)},
|
|
1719
2578
|
description: ${JSON.stringify(description)},
|
|
1720
|
-
version:
|
|
2579
|
+
version: pkg.version,
|
|
1721
2580
|
// Prefer a login the machine already has: { rung: 'cli', probe: { command: 'tool', args: ['auth', 'status'] } }
|
|
1722
2581
|
auth: { rung: 'key', keys: ['apiToken'] },
|
|
1723
2582
|
config: [
|
|
@@ -1778,11 +2637,149 @@ export const connector = defineConnector({
|
|
|
1778
2637
|
})
|
|
1779
2638
|
`;
|
|
1780
2639
|
}
|
|
1781
|
-
function
|
|
2640
|
+
function extensionSource(id, name, description) {
|
|
2641
|
+
return `import { defineExtension } from '@vornrun/connector-sdk'
|
|
2642
|
+
// Bundled at build time: a pack is one file, so a version read from disk is not there to read.
|
|
2643
|
+
import pkg from '../package.json'
|
|
2644
|
+
|
|
2645
|
+
export const connector = defineExtension({
|
|
2646
|
+
id: ${JSON.stringify(id)},
|
|
2647
|
+
name: ${JSON.stringify(name)},
|
|
2648
|
+
description: ${JSON.stringify(description)},
|
|
2649
|
+
version: pkg.version,
|
|
2650
|
+
// Only what this actually spends: the check names one it declared and never used.
|
|
2651
|
+
permissions: ['terminal.read'],
|
|
2652
|
+
// Absent where none of these hold, rather than showing a band with nothing in it.
|
|
2653
|
+
activates: { workspaceContains: ['package.json'] },
|
|
2654
|
+
footers: [
|
|
2655
|
+
{
|
|
2656
|
+
id: 'checks',
|
|
2657
|
+
title: 'Checks',
|
|
2658
|
+
description: 'What the last commands in this session said',
|
|
2659
|
+
every: 30,
|
|
2660
|
+
async run(context) {
|
|
2661
|
+
const output = await context.host.output({ lines: 200 })
|
|
2662
|
+
const failed = /\\b(FAIL|failed|error)\\b/i.test(output)
|
|
2663
|
+
return [
|
|
2664
|
+
{
|
|
2665
|
+
label: 'tests',
|
|
2666
|
+
value: failed ? 'failing' : 'passing',
|
|
2667
|
+
tone: failed ? 'danger' : 'ok'
|
|
2668
|
+
}
|
|
2669
|
+
]
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
],
|
|
2673
|
+
panes: [
|
|
2674
|
+
{
|
|
2675
|
+
id: 'report',
|
|
2676
|
+
title: 'Report',
|
|
2677
|
+
description: 'The reading, in full, beside the terminal',
|
|
2678
|
+
// Drawn beside the pane's name wherever it is offered; path data only.
|
|
2679
|
+
icon: { viewBox: '0 0 24 24', paths: ['M4 4h16v16H4z M8 9h8 M8 13h8 M8 17h5'] },
|
|
2680
|
+
// Served from the pack; everything the page needs lives under web/.
|
|
2681
|
+
web: 'web/report/index.html'
|
|
2682
|
+
}
|
|
2683
|
+
]
|
|
2684
|
+
})
|
|
2685
|
+
`;
|
|
2686
|
+
}
|
|
2687
|
+
function extensionPage(name) {
|
|
2688
|
+
return `<!doctype html>
|
|
2689
|
+
<html lang="en">
|
|
2690
|
+
<head>
|
|
2691
|
+
<meta charset="utf-8" />
|
|
2692
|
+
<title>${name}</title>
|
|
2693
|
+
<style>
|
|
2694
|
+
body {
|
|
2695
|
+
margin: 0;
|
|
2696
|
+
padding: 12px;
|
|
2697
|
+
font: 12px ui-sans-serif, system-ui, sans-serif;
|
|
2698
|
+
color: #faf9f7;
|
|
2699
|
+
background: #101012;
|
|
2700
|
+
}
|
|
2701
|
+
h1 {
|
|
2702
|
+
font-size: 13px;
|
|
2703
|
+
font-weight: 500;
|
|
2704
|
+
margin: 0 0 8px;
|
|
2705
|
+
}
|
|
2706
|
+
pre {
|
|
2707
|
+
margin: 0;
|
|
2708
|
+
white-space: pre-wrap;
|
|
2709
|
+
color: rgba(255, 255, 255, 0.55);
|
|
2710
|
+
}
|
|
2711
|
+
</style>
|
|
2712
|
+
</head>
|
|
2713
|
+
<body>
|
|
2714
|
+
<h1>${name}</h1>
|
|
2715
|
+
<pre id="output">Reading the session\u2026</pre>
|
|
2716
|
+
<script type="module">
|
|
2717
|
+
// Same origin, so the page carries no credential: Vorn knows which pane is
|
|
2718
|
+
// asking and grants exactly the permissions the manifest declared.
|
|
2719
|
+
const ask = async (method, body = {}) => {
|
|
2720
|
+
const response = await fetch('bridge/' + method, {
|
|
2721
|
+
method: 'POST',
|
|
2722
|
+
headers: { 'content-type': 'application/json' },
|
|
2723
|
+
body: JSON.stringify(body)
|
|
2724
|
+
})
|
|
2725
|
+
if (!response.ok) throw new Error(method + ' answered ' + response.status)
|
|
2726
|
+
return (await response.json()).result
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
const node = document.getElementById('output')
|
|
2730
|
+
try {
|
|
2731
|
+
node.textContent = await ask('output', { lines: 200 })
|
|
2732
|
+
} catch (error) {
|
|
2733
|
+
node.textContent = String(error)
|
|
2734
|
+
}
|
|
2735
|
+
</script>
|
|
2736
|
+
</body>
|
|
2737
|
+
</html>
|
|
2738
|
+
`;
|
|
2739
|
+
}
|
|
2740
|
+
function extensionTestSource(name) {
|
|
2741
|
+
return `import { describe, expect, it } from 'vitest'
|
|
2742
|
+
import { mockExtensionHost } from '@vornrun/connector-sdk'
|
|
2743
|
+
import { connector } from './extension'
|
|
2744
|
+
|
|
2745
|
+
/** Runs one footer the way the host will, against a stub that enforces the manifest. */
|
|
2746
|
+
async function footer(id: string, output: string) {
|
|
2747
|
+
const { host } = mockExtensionHost(connector.permissions ?? [], { output: async () => output })
|
|
2748
|
+
const declared = connector.contributes?.footers?.find((entry) => entry.id === id)
|
|
2749
|
+
if (!declared) throw new Error('no footer ' + id)
|
|
2750
|
+
return declared.run({
|
|
2751
|
+
sessionId: 'test',
|
|
2752
|
+
worktreePath: process.cwd(),
|
|
2753
|
+
agent: 'claude',
|
|
2754
|
+
host,
|
|
2755
|
+
now: () => '2026-01-01T00:00:00.000Z'
|
|
2756
|
+
})
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
describe(${JSON.stringify(name)}, () => {
|
|
2760
|
+
it('reads the session as passing when nothing failed', async () => {
|
|
2761
|
+
expect(await footer('checks', 'Test Files 1 passed (1)')).toEqual([
|
|
2762
|
+
{ label: 'tests', value: 'passing', tone: 'ok' }
|
|
2763
|
+
])
|
|
2764
|
+
})
|
|
2765
|
+
|
|
2766
|
+
it('reads it as failing when the output says so', async () => {
|
|
2767
|
+
expect(await footer('checks', 'FAIL src/index.test.ts')).toEqual([
|
|
2768
|
+
{ label: 'tests', value: 'failing', tone: 'danger' }
|
|
2769
|
+
])
|
|
2770
|
+
})
|
|
2771
|
+
|
|
2772
|
+
it('asks for nothing it did not declare', () => {
|
|
2773
|
+
expect(connector.permissions).toEqual(['terminal.read'])
|
|
2774
|
+
})
|
|
2775
|
+
})
|
|
2776
|
+
`;
|
|
2777
|
+
}
|
|
2778
|
+
function entrySource(module) {
|
|
1782
2779
|
return `import { realpathSync } from 'node:fs'
|
|
1783
2780
|
import { fileURLToPath } from 'node:url'
|
|
1784
2781
|
import { serveConnector } from '@vornrun/connector-sdk'
|
|
1785
|
-
import { connector } from '
|
|
2782
|
+
import { connector } from './${module}'
|
|
1786
2783
|
|
|
1787
2784
|
/** True when this file was run directly rather than imported. */
|
|
1788
2785
|
export function isEntryPoint(moduleUrl: string, argv = process.argv): boolean {
|
|
@@ -1850,8 +2847,8 @@ describe('the packaged connector', () => {
|
|
|
1850
2847
|
})
|
|
1851
2848
|
`;
|
|
1852
2849
|
}
|
|
1853
|
-
function indexSource() {
|
|
1854
|
-
return `import { connector } from '
|
|
2850
|
+
function indexSource(module) {
|
|
2851
|
+
return `import { connector } from './${module}'
|
|
1855
2852
|
import { serveIfEntryPoint } from './entry'
|
|
1856
2853
|
|
|
1857
2854
|
export { connector }
|
|
@@ -1956,24 +2953,71 @@ Rename the trigger, the action and the settings to whatever this connector
|
|
|
1956
2953
|
really talks to; the shapes here are a starting point, not a rule.
|
|
1957
2954
|
`;
|
|
1958
2955
|
}
|
|
2956
|
+
function extensionReadme(id, name, description) {
|
|
2957
|
+
return `# ${name}
|
|
2958
|
+
|
|
2959
|
+
${description}
|
|
2960
|
+
|
|
2961
|
+
## Build and check
|
|
2962
|
+
|
|
2963
|
+
\`\`\`sh
|
|
2964
|
+
yarn install
|
|
2965
|
+
yarn build
|
|
2966
|
+
yarn check # verifies the extension against Vorn's contract
|
|
2967
|
+
yarn test
|
|
2968
|
+
yarn pack # writes ${id}-${SCAFFOLD_VERSION}.vorn.tgz, installable in Vorn
|
|
2969
|
+
\`\`\`
|
|
2970
|
+
|
|
2971
|
+
## What it contributes
|
|
2972
|
+
|
|
2973
|
+
| Kind | Name | What it does |
|
|
2974
|
+
| --- | --- | --- |
|
|
2975
|
+
| Footer | Checks | A band under the card's status bar, recomputed every 30s |
|
|
2976
|
+
| Pane | Report | A page beside the terminal, served from \`web/report\` |
|
|
2977
|
+
|
|
2978
|
+
## What it asks for
|
|
2979
|
+
|
|
2980
|
+
| Permission | What it grants |
|
|
2981
|
+
| --- | --- |
|
|
2982
|
+
| \`terminal.read\` | The session's recent terminal output |
|
|
2983
|
+
|
|
2984
|
+
Ask for only what the extension spends: \`check\` names a permission that was
|
|
2985
|
+
declared and never used.
|
|
2986
|
+
|
|
2987
|
+
## Where it shows
|
|
2988
|
+
|
|
2989
|
+
Sessions whose worktree has a \`package.json\`. Widen or narrow that in
|
|
2990
|
+
\`activates\`, and narrow one contribution further with its own \`when\`.
|
|
2991
|
+
`;
|
|
2992
|
+
}
|
|
1959
2993
|
function scaffoldFiles(options) {
|
|
2994
|
+
const kind = options.kind ?? "connector";
|
|
1960
2995
|
if (!ID_PATTERN.test(options.id ?? "")) {
|
|
1961
|
-
throw new Error(
|
|
2996
|
+
throw new Error(
|
|
2997
|
+
`${kind === "extension" ? "Extension" : "Connector"} id "${options.id}" must start with a letter and be url-safe`
|
|
2998
|
+
);
|
|
1962
2999
|
}
|
|
1963
3000
|
const name = options.name?.trim() || titleCase(options.id);
|
|
1964
|
-
const description = options.description?.trim() || `${name}
|
|
3001
|
+
const description = options.description?.trim() || `${name} ${kind} for Vorn`;
|
|
1965
3002
|
const inRepo = options.repoConventions ?? false;
|
|
3003
|
+
const module = kind === "extension" ? "extension" : "connector";
|
|
1966
3004
|
return [
|
|
1967
|
-
{ path: "package.json", contents: packageJson(options.id, description, inRepo) },
|
|
1968
|
-
{ path: "src/connector.ts", contents: connectorSource(options.id, name, description) },
|
|
1969
|
-
{ path: "src/entry.ts", contents: entrySource() },
|
|
1970
|
-
{ path: "src/index.ts", contents: indexSource() },
|
|
1971
|
-
{ path: "src/connector.test.ts", contents: testSource(name) },
|
|
3005
|
+
{ path: "package.json", contents: packageJson(options.id, description, inRepo, kind) },
|
|
3006
|
+
kind === "extension" ? { path: "src/extension.ts", contents: extensionSource(options.id, name, description) } : { path: "src/connector.ts", contents: connectorSource(options.id, name, description) },
|
|
3007
|
+
{ path: "src/entry.ts", contents: entrySource(module) },
|
|
3008
|
+
{ path: "src/index.ts", contents: indexSource(module) },
|
|
3009
|
+
kind === "extension" ? { path: "src/extension.test.ts", contents: extensionTestSource(name) } : { path: "src/connector.test.ts", contents: testSource(name) },
|
|
1972
3010
|
{ path: "src/entry.test.ts", contents: entryTestSource() },
|
|
1973
|
-
|
|
3011
|
+
// The page a pane is drawn from, carried into the pack as it stands here.
|
|
3012
|
+
...kind === "extension" ? [{ path: "web/report/index.html", contents: extensionPage(name) }] : [],
|
|
3013
|
+
{
|
|
3014
|
+
path: "README.md",
|
|
3015
|
+
contents: kind === "extension" ? extensionReadme(options.id, name, description) : readme(options.id, name, description)
|
|
3016
|
+
},
|
|
3017
|
+
// Everywhere: the generated source imports its package.json, which needs resolveJsonModule to compile.
|
|
3018
|
+
{ path: "tsconfig.json", contents: tsconfig(inRepo) },
|
|
1974
3019
|
...inRepo ? [
|
|
1975
3020
|
{ path: "CHANGELOG.md", contents: changelog() },
|
|
1976
|
-
{ path: "tsconfig.json", contents: tsconfig() },
|
|
1977
3021
|
{ path: "tsup.config.ts", contents: tsupConfig() },
|
|
1978
3022
|
{ path: "vitest.config.ts", contents: vitestConfig() }
|
|
1979
3023
|
] : []
|
|
@@ -2136,6 +3180,67 @@ function createConnectorServer(connector, options = {}) {
|
|
|
2136
3180
|
}
|
|
2137
3181
|
);
|
|
2138
3182
|
}
|
|
3183
|
+
const sessionShape = {
|
|
3184
|
+
sessionId: z.string().describe("The session this is being computed for"),
|
|
3185
|
+
worktreePath: z.string().describe("Where the session's work is"),
|
|
3186
|
+
agent: z.enum(EXTENSION_AGENTS).describe("Which agent runs in the session")
|
|
3187
|
+
};
|
|
3188
|
+
const sessionContext = (args, host) => ({
|
|
3189
|
+
sessionId: args.sessionId,
|
|
3190
|
+
worktreePath: args.worktreePath,
|
|
3191
|
+
agent: args.agent,
|
|
3192
|
+
host,
|
|
3193
|
+
now: options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString())
|
|
3194
|
+
});
|
|
3195
|
+
const hostFor = (sessionId) => options.host?.(sessionId) ?? createExtensionHost({ sessionId });
|
|
3196
|
+
for (const footer of connector.contributes?.footers ?? []) {
|
|
3197
|
+
server.registerTool(
|
|
3198
|
+
footerToolName(footer.id),
|
|
3199
|
+
{
|
|
3200
|
+
title: footer.title,
|
|
3201
|
+
description: footer.description ?? `Recompute ${footer.title} for one session`,
|
|
3202
|
+
inputSchema: sessionShape,
|
|
3203
|
+
outputSchema: z.looseObject({
|
|
3204
|
+
items: z.array(z.looseObject({ label: z.string(), value: z.string() })).describe("The readings to show in the band")
|
|
3205
|
+
})
|
|
3206
|
+
},
|
|
3207
|
+
async (args) => {
|
|
3208
|
+
try {
|
|
3209
|
+
const items = await footer.run(sessionContext(args, hostFor(args.sessionId)));
|
|
3210
|
+
return json({ items });
|
|
3211
|
+
} catch (error) {
|
|
3212
|
+
return failure(error);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
);
|
|
3216
|
+
}
|
|
3217
|
+
for (const handler of connector.contributes?.linkHandlers ?? []) {
|
|
3218
|
+
server.registerTool(
|
|
3219
|
+
handlerToolName(handler.id),
|
|
3220
|
+
{
|
|
3221
|
+
title: handler.title,
|
|
3222
|
+
description: handler.description ?? `Open ${handler.title} for a clicked link`,
|
|
3223
|
+
inputSchema: {
|
|
3224
|
+
...sessionShape,
|
|
3225
|
+
url: z.string().describe("The clicked text, which matched this handler")
|
|
3226
|
+
},
|
|
3227
|
+
outputSchema: z.looseObject({
|
|
3228
|
+
openPane: z.string().optional().describe("Id of one of this extension's panes to open")
|
|
3229
|
+
})
|
|
3230
|
+
},
|
|
3231
|
+
async (args) => {
|
|
3232
|
+
try {
|
|
3233
|
+
const handled = await handler.run({
|
|
3234
|
+
...sessionContext(args, hostFor(args.sessionId)),
|
|
3235
|
+
url: args.url
|
|
3236
|
+
});
|
|
3237
|
+
return json({ ...handled ?? {} });
|
|
3238
|
+
} catch (error) {
|
|
3239
|
+
return failure(error);
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
2139
3244
|
for (const action of connector.actions) {
|
|
2140
3245
|
const base = action.description ?? `${action.label} in ${connector.name}`;
|
|
2141
3246
|
const retryHint = action.idempotent === void 0 ? "" : action.idempotent ? " Safe to retry: repeating this call with the same arguments has no additional effect." : " Not idempotent: repeating this call performs the operation again.";
|
|
@@ -2170,11 +3275,30 @@ async function serveConnector(connector, options = {}) {
|
|
|
2170
3275
|
}
|
|
2171
3276
|
|
|
2172
3277
|
export {
|
|
3278
|
+
EXTENSION_PERMISSIONS,
|
|
3279
|
+
HOST_PERMISSIONS,
|
|
3280
|
+
envNameFor,
|
|
3281
|
+
defineConnector,
|
|
3282
|
+
defineExtension,
|
|
3283
|
+
resolveConfig,
|
|
3284
|
+
pollToolName,
|
|
3285
|
+
footerToolName,
|
|
3286
|
+
handlerToolName,
|
|
3287
|
+
MANIFEST_TOOL,
|
|
3288
|
+
PREFLIGHT_TOOL,
|
|
3289
|
+
OPTIONS_TOOL,
|
|
3290
|
+
connectionSetup,
|
|
3291
|
+
connectorManifest,
|
|
2173
3292
|
MAX_PACK_BYTES,
|
|
2174
3293
|
lifecycleScriptFindings,
|
|
2175
3294
|
bundleDependencyFindings,
|
|
3295
|
+
bundledRequireFindings,
|
|
2176
3296
|
readNearestPackageJson,
|
|
2177
3297
|
esbuildBundle,
|
|
3298
|
+
HOST_URL_ENV,
|
|
3299
|
+
HOST_TOKEN_ENV,
|
|
3300
|
+
PermissionDeniedError,
|
|
3301
|
+
createExtensionHost,
|
|
2178
3302
|
normalizeItem,
|
|
2179
3303
|
normalizeItems,
|
|
2180
3304
|
pollWithDedupe,
|
|
@@ -2194,19 +3318,11 @@ export {
|
|
|
2194
3318
|
drainPoll,
|
|
2195
3319
|
runOptions,
|
|
2196
3320
|
runAction,
|
|
2197
|
-
envNameFor,
|
|
2198
|
-
defineConnector,
|
|
2199
|
-
resolveConfig,
|
|
2200
|
-
pollToolName,
|
|
2201
|
-
MANIFEST_TOOL,
|
|
2202
|
-
PREFLIGHT_TOOL,
|
|
2203
|
-
OPTIONS_TOOL,
|
|
2204
|
-
connectionSetup,
|
|
2205
|
-
connectorManifest,
|
|
2206
3321
|
MockRouteMissError,
|
|
2207
3322
|
escapedMockHttp,
|
|
2208
3323
|
withMockHttp,
|
|
2209
3324
|
createConnectorHarness,
|
|
3325
|
+
mockExtensionHost,
|
|
2210
3326
|
checkConnector,
|
|
2211
3327
|
CHECK_OWNERS,
|
|
2212
3328
|
runConformance,
|