@lanes-sh/link 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +144 -0
  3. package/bin/lanes +42 -0
  4. package/instructions/agents/lanes-link-scout.md +73 -0
  5. package/instructions/skills/lanes-link/SKILL.md +187 -0
  6. package/package.json +95 -0
  7. package/src/audit/chain.ts +278 -0
  8. package/src/audit/conformance.ts +374 -0
  9. package/src/audit/fanout.ts +97 -0
  10. package/src/audit/index.ts +218 -0
  11. package/src/audit/stdout.ts +60 -0
  12. package/src/auth/index.ts +220 -0
  13. package/src/auth/oauth/metadata.ts +75 -0
  14. package/src/auth/oauth/server.ts +385 -0
  15. package/src/auth/oauth/store.ts +215 -0
  16. package/src/auth/oidc.ts +206 -0
  17. package/src/auth/remote.ts +72 -0
  18. package/src/cli/argv.ts +94 -0
  19. package/src/cli/callback-page.ts +256 -0
  20. package/src/cli/commands/connect/accounts.ts +94 -0
  21. package/src/cli/commands/connect/authorise.ts +298 -0
  22. package/src/cli/commands/connect/client.ts +284 -0
  23. package/src/cli/commands/connect/index.ts +398 -0
  24. package/src/cli/commands/connect/outcome.ts +119 -0
  25. package/src/cli/commands/connect/requirements.ts +103 -0
  26. package/src/cli/commands/connect/scopes-gate.ts +146 -0
  27. package/src/cli/commands/connect/settle.ts +136 -0
  28. package/src/cli/commands/connect/setup.ts +276 -0
  29. package/src/cli/commands/mcp/assets.ts +189 -0
  30. package/src/cli/commands/mcp/harnesses.ts +143 -0
  31. package/src/cli/commands/mcp/list.ts +70 -0
  32. package/src/cli/commands/mcp/register.ts +199 -0
  33. package/src/cli/commands/mcp/stdio.ts +57 -0
  34. package/src/cli/commands/mcp.ts +22 -0
  35. package/src/cli/commands/operate/attach.ts +121 -0
  36. package/src/cli/commands/operate/audit.ts +119 -0
  37. package/src/cli/commands/operate/inspect.ts +355 -0
  38. package/src/cli/commands/operate/outputs.ts +206 -0
  39. package/src/cli/commands/operate/policy.ts +80 -0
  40. package/src/cli/commands/operate/serve.ts +55 -0
  41. package/src/cli/commands/operate/status.ts +133 -0
  42. package/src/cli/commands/operate/token.ts +70 -0
  43. package/src/cli/commands/operate.ts +27 -0
  44. package/src/cli/commands/owner/memory.ts +110 -0
  45. package/src/cli/commands/owner/shared.ts +125 -0
  46. package/src/cli/commands/owner/skills.ts +92 -0
  47. package/src/cli/commands/owner/vault.ts +140 -0
  48. package/src/cli/commands/owner.ts +46 -0
  49. package/src/cli/commands/profile/removal.ts +278 -0
  50. package/src/cli/commands/profile/remove.ts +315 -0
  51. package/src/cli/commands/profile.ts +156 -0
  52. package/src/cli/commands/secrets.ts +176 -0
  53. package/src/cli/commands/setup.ts +150 -0
  54. package/src/cli/commands/target.ts +310 -0
  55. package/src/cli/config-edit.ts +397 -0
  56. package/src/cli/endpoint-url.ts +77 -0
  57. package/src/cli/identity.ts +109 -0
  58. package/src/cli/lanes.ts +78 -0
  59. package/src/cli/main.ts +333 -0
  60. package/src/cli/oauth-error.ts +13 -0
  61. package/src/cli/oauth-exchange.ts +146 -0
  62. package/src/cli/oauth.ts +354 -0
  63. package/src/cli/output.ts +184 -0
  64. package/src/cli/prompt.ts +180 -0
  65. package/src/cli/publish.ts +185 -0
  66. package/src/cli/runtime/discovery.ts +123 -0
  67. package/src/cli/runtime/open.ts +342 -0
  68. package/src/cli/runtime/registry.ts +185 -0
  69. package/src/cli/runtime/select.ts +124 -0
  70. package/src/cli/runtime.ts +34 -0
  71. package/src/cli/scopes.ts +63 -0
  72. package/src/cli/usage.ts +111 -0
  73. package/src/cli/version.ts +25 -0
  74. package/src/connectivity/auth/README.md +36 -0
  75. package/src/connectivity/auth/api-key/index.ts +43 -0
  76. package/src/connectivity/auth/authorize.ts +49 -0
  77. package/src/connectivity/auth/basic/index.ts +68 -0
  78. package/src/connectivity/auth/bearer/index.ts +13 -0
  79. package/src/connectivity/auth/credential.ts +19 -0
  80. package/src/connectivity/auth/header/index.ts +18 -0
  81. package/src/connectivity/auth/index.ts +35 -0
  82. package/src/connectivity/auth/none/index.ts +12 -0
  83. package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
  84. package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
  85. package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
  86. package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
  87. package/src/connectivity/auth/resolve.ts +61 -0
  88. package/src/connectivity/auth/strategy/index.ts +14 -0
  89. package/src/connectivity/capability.ts +164 -0
  90. package/src/connectivity/connector.ts +175 -0
  91. package/src/connectivity/context.ts +77 -0
  92. package/src/connectivity/index.ts +94 -0
  93. package/src/connectivity/mail/attachments.ts +368 -0
  94. package/src/connectivity/mail/compose.ts +73 -0
  95. package/src/connectivity/mail/index.ts +40 -0
  96. package/src/connectivity/mail/message.ts +82 -0
  97. package/src/connectivity/mail/nodemailer.d.ts +83 -0
  98. package/src/connectivity/mail/staging.ts +143 -0
  99. package/src/connectivity/mail/url.ts +301 -0
  100. package/src/connectivity/manifest/auth.ts +135 -0
  101. package/src/connectivity/manifest/bundles.ts +23 -0
  102. package/src/connectivity/manifest/connector.ts +168 -0
  103. package/src/connectivity/manifest/credential-ref.ts +73 -0
  104. package/src/connectivity/manifest/identity.ts +43 -0
  105. package/src/connectivity/manifest/index.ts +46 -0
  106. package/src/connectivity/manifest/primitives.ts +20 -0
  107. package/src/connectivity/manifest/provider.ts +208 -0
  108. package/src/connectivity/manifest/requirements.ts +146 -0
  109. package/src/connectivity/manifest/setup.ts +55 -0
  110. package/src/connectivity/provider.ts +163 -0
  111. package/src/connectivity/transports/README.md +33 -0
  112. package/src/connectivity/transports/composite/index.ts +68 -0
  113. package/src/connectivity/transports/dav/calendar.ts +217 -0
  114. package/src/connectivity/transports/dav/capabilities.ts +151 -0
  115. package/src/connectivity/transports/dav/client.ts +226 -0
  116. package/src/connectivity/transports/dav/contacts.ts +75 -0
  117. package/src/connectivity/transports/dav/ical.ts +412 -0
  118. package/src/connectivity/transports/dav/index.ts +143 -0
  119. package/src/connectivity/transports/dav/operations.ts +43 -0
  120. package/src/connectivity/transports/dav/request.ts +161 -0
  121. package/src/connectivity/transports/dav/xml.ts +123 -0
  122. package/src/connectivity/transports/factory.ts +181 -0
  123. package/src/connectivity/transports/fs/capabilities.ts +88 -0
  124. package/src/connectivity/transports/fs/commands.ts +258 -0
  125. package/src/connectivity/transports/fs/index.ts +121 -0
  126. package/src/connectivity/transports/fs/operations.ts +45 -0
  127. package/src/connectivity/transports/fs/paths.ts +120 -0
  128. package/src/connectivity/transports/fs/result.ts +12 -0
  129. package/src/connectivity/transports/http/index.ts +255 -0
  130. package/src/connectivity/transports/imap/attachment.ts +166 -0
  131. package/src/connectivity/transports/imap/capabilities.ts +158 -0
  132. package/src/connectivity/transports/imap/client.ts +398 -0
  133. package/src/connectivity/transports/imap/commands.ts +385 -0
  134. package/src/connectivity/transports/imap/index.ts +152 -0
  135. package/src/connectivity/transports/imap/operations.ts +64 -0
  136. package/src/connectivity/transports/imap/parse.ts +130 -0
  137. package/src/connectivity/transports/imap/parser.ts +272 -0
  138. package/src/connectivity/transports/imap/result.ts +15 -0
  139. package/src/connectivity/transports/imap/send.ts +92 -0
  140. package/src/connectivity/transports/imap/socket.ts +111 -0
  141. package/src/connectivity/transports/imap/utf7.ts +136 -0
  142. package/src/connectivity/transports/index.ts +20 -0
  143. package/src/connectivity/transports/local/index.ts +173 -0
  144. package/src/connectivity/transports/mcp/index.ts +215 -0
  145. package/src/deployments/README.md +63 -0
  146. package/src/deployments/adapters/audit-blob.ts +203 -0
  147. package/src/deployments/adapters/filesystem.ts +184 -0
  148. package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
  149. package/src/deployments/adapters/gcs.ts +191 -0
  150. package/src/deployments/adapters/otlp.ts +128 -0
  151. package/src/deployments/adapters/s3.ts +195 -0
  152. package/src/deployments/azure/README.md +21 -0
  153. package/src/deployments/bootstrap.ts +177 -0
  154. package/src/deployments/deploy.ts +290 -0
  155. package/src/deployments/driver.ts +157 -0
  156. package/src/deployments/drivers.ts +35 -0
  157. package/src/deployments/gcp/Dockerfile +70 -0
  158. package/src/deployments/gcp/cloudbuild.yaml +31 -0
  159. package/src/deployments/gcp/driver.ts +175 -0
  160. package/src/deployments/gcp/gcloud.ts +178 -0
  161. package/src/deployments/gcp/provision.ts +290 -0
  162. package/src/deployments/gcp/survey.ts +319 -0
  163. package/src/deployments/local/README.md +12 -0
  164. package/src/deployments/prepare.ts +257 -0
  165. package/src/deployments/steps.ts +137 -0
  166. package/src/deployments/target.ts +295 -0
  167. package/src/deployments/upload.ts +207 -0
  168. package/src/dispatch/context.ts +195 -0
  169. package/src/dispatch/dispatch.ts +350 -0
  170. package/src/dispatch/index.ts +32 -0
  171. package/src/dispatch/staging.ts +102 -0
  172. package/src/policy/index.ts +179 -0
  173. package/src/policy/limits.ts +77 -0
  174. package/src/profile/authorization.ts +81 -0
  175. package/src/profile/files.ts +71 -0
  176. package/src/profile/index.ts +76 -0
  177. package/src/profile/layout.ts +123 -0
  178. package/src/profile/load.ts +199 -0
  179. package/src/profile/primitives.ts +45 -0
  180. package/src/profile/schema.ts +347 -0
  181. package/src/profile/secret-detection.ts +162 -0
  182. package/src/profile/targets.ts +152 -0
  183. package/src/profile/workspace.ts +262 -0
  184. package/src/providers/custom/index.ts +21 -0
  185. package/src/providers/custom/load.ts +115 -0
  186. package/src/providers/custom/template.ts +156 -0
  187. package/src/providers/example/provider.ts +207 -0
  188. package/src/providers/google/calendar/index.ts +66 -0
  189. package/src/providers/google/calendar/redact.ts +40 -0
  190. package/src/providers/google/contacts/index.ts +50 -0
  191. package/src/providers/google/contacts/redact.ts +21 -0
  192. package/src/providers/google/docs/index.ts +45 -0
  193. package/src/providers/google/drive/hints.ts +28 -0
  194. package/src/providers/google/drive/index.ts +34 -0
  195. package/src/providers/google/drive/redact.ts +39 -0
  196. package/src/providers/google/drive-mcp/index.ts +21 -0
  197. package/src/providers/google/gmail/api.ts +42 -0
  198. package/src/providers/google/gmail/attachment.ts +142 -0
  199. package/src/providers/google/gmail/hints.ts +55 -0
  200. package/src/providers/google/gmail/index.ts +112 -0
  201. package/src/providers/google/gmail/redact.ts +56 -0
  202. package/src/providers/google/gmail/send.ts +365 -0
  203. package/src/providers/google/gmail-mcp/index.ts +35 -0
  204. package/src/providers/google/index.ts +10 -0
  205. package/src/providers/google/shared/oauth.ts +122 -0
  206. package/src/providers/google/shared/scopes.ts +99 -0
  207. package/src/providers/google/shared/setup.ts +80 -0
  208. package/src/providers/google/sheets/hints.ts +45 -0
  209. package/src/providers/google/sheets/index.ts +70 -0
  210. package/src/providers/google/sheets/redact.ts +45 -0
  211. package/src/providers/google/specs/calendar.v3.json +1829 -0
  212. package/src/providers/google/specs/docs.v1.json +381 -0
  213. package/src/providers/google/specs/drive.v3.json +2208 -0
  214. package/src/providers/google/specs/gmail.v1.json +2578 -0
  215. package/src/providers/google/specs/people.v1.json +506 -0
  216. package/src/providers/google/specs/sheets.v4.json +1269 -0
  217. package/src/providers/google/specs/tasks.v1.json +840 -0
  218. package/src/providers/google/specs/vendor.ts +661 -0
  219. package/src/providers/google/tasks/index.ts +53 -0
  220. package/src/providers/google/tasks/redact.ts +34 -0
  221. package/src/providers/harness.ts +95 -0
  222. package/src/providers/icloud/calendar/index.ts +27 -0
  223. package/src/providers/icloud/contacts/index.ts +17 -0
  224. package/src/providers/icloud/drive/index.ts +47 -0
  225. package/src/providers/icloud/index.ts +8 -0
  226. package/src/providers/icloud/mail/index.ts +37 -0
  227. package/src/providers/icloud/shared/setup.ts +66 -0
  228. package/src/providers/index.ts +93 -0
  229. package/src/providers/linear/index.ts +11 -0
  230. package/src/providers/linear/scopes.ts +7 -0
  231. package/src/providers/memory/provider.ts +429 -0
  232. package/src/providers/notion/index.ts +19 -0
  233. package/src/providers/owner.ts +49 -0
  234. package/src/providers/scopes.ts +26 -0
  235. package/src/providers/setup/plan.ts +141 -0
  236. package/src/providers/setup/provider.ts +323 -0
  237. package/src/providers/shared/frontmatter.ts +119 -0
  238. package/src/providers/skills/provider.ts +283 -0
  239. package/src/providers/skills/store.ts +252 -0
  240. package/src/providers/vault/provider.ts +194 -0
  241. package/src/registry/index.ts +36 -0
  242. package/src/registry/policy-bridge.ts +32 -0
  243. package/src/registry/reconcile.ts +313 -0
  244. package/src/registry/registry.ts +240 -0
  245. package/src/secrets/document.ts +293 -0
  246. package/src/secrets/index.ts +154 -0
  247. package/src/secrets/system.ts +173 -0
  248. package/src/secrets/vault.ts +336 -0
  249. package/src/server/attachments.ts +197 -0
  250. package/src/server/container.ts +96 -0
  251. package/src/server/edge.ts +53 -0
  252. package/src/server/endpoint.ts +352 -0
  253. package/src/server/generations.ts +362 -0
  254. package/src/server/harness.ts +400 -0
  255. package/src/server/index.ts +331 -0
  256. package/src/server/logging.ts +41 -0
  257. package/src/server/mcp/build.ts +68 -0
  258. package/src/server/mcp/icon.ts +145 -0
  259. package/src/server/mcp/index.ts +32 -0
  260. package/src/server/mcp/instructions.ts +245 -0
  261. package/src/server/mcp/naming.ts +39 -0
  262. package/src/server/mcp/prompts.ts +78 -0
  263. package/src/server/mcp/resources.ts +106 -0
  264. package/src/server/mcp/routing.ts +117 -0
  265. package/src/server/mcp/schema.ts +78 -0
  266. package/src/server/mcp/tools.ts +186 -0
  267. package/src/server/mcp/visibility.ts +132 -0
  268. package/src/server/oauth.ts +222 -0
  269. package/src/server/rebinding.ts +53 -0
  270. package/src/server/stdio.ts +192 -0
  271. package/src/stores/blobs/conformance.ts +223 -0
  272. package/src/stores/blobs/index.ts +125 -0
  273. package/src/stores/blobs/testing.ts +49 -0
  274. package/src/stores/state/index.ts +247 -0
  275. package/src/stores/state/keys.ts +68 -0
  276. package/src/stores/state/testing.ts +41 -0
@@ -0,0 +1,661 @@
1
+ /**
2
+ * Vendor trimmed OpenAPI specs for Google's REST APIs.
3
+ *
4
+ * Google's MCP servers are gated behind a Workspace Developer Preview that a
5
+ * personal account cannot enrol in, so the REST APIs are the path that works
6
+ * for everyone. They publish Discovery documents rather than OpenAPI; APIs.guru
7
+ * generates OpenAPI from those, which is what this reads.
8
+ *
9
+ * The output is **committed**, and that is the point. A spec decides which
10
+ * paths get called with the operator's token, and `connect` now grants
11
+ * everything a provider discovers — so a spec fetched at connect time from a
12
+ * third party could introduce, say, a DELETE operation that lands on Google's
13
+ * own host holding a real mailbox token. Vendoring makes the surface reviewable
14
+ * and the build reproducible; this script exists so refreshing it is one
15
+ * command rather than a hand edit.
16
+ *
17
+ * bun run vendor:google
18
+ */
19
+
20
+ import { mkdir, writeFile } from 'node:fs/promises';
21
+ import { join } from 'node:path';
22
+ import { OpenAPIToolGenerator, type McpOpenAPITool } from 'mcp-from-openapi';
23
+
24
+ /** Kept in step with `BUDGET_KB` in `src/cli/tools.test.ts`, which enforces it. */
25
+ const BUDGET_KB = 64;
26
+
27
+ interface Spec {
28
+ openapi: string;
29
+ info: Record<string, unknown>;
30
+ servers?: Array<{ url: string }>;
31
+ paths: Record<string, Record<string, { operationId?: string } & Record<string, unknown>>>;
32
+ components?: { schemas?: Record<string, unknown> } & Record<string, unknown>;
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ /**
37
+ * The operations each provider exposes.
38
+ *
39
+ * Curated, and deliberately so: Gmail's spec has 79 operations and Drive's 48,
40
+ * which is more than an agent can reason over and far more than either provider
41
+ * claims to be. Everything here is reachable under the scopes the manifest
42
+ * requests — nothing is listed that would fail on permission.
43
+ */
44
+ const SELECTION: Record<
45
+ string,
46
+ { source: string; out: string; operations: string[]; opaque?: string[] }
47
+ > = {
48
+ gmail: {
49
+ source: 'https://api.apis.guru/v2/specs/googleapis.com/gmail/v1/openapi.json',
50
+ out: 'gmail.v1.json',
51
+ operations: [
52
+ 'gmail.users.getProfile',
53
+ 'gmail.users.labels.list',
54
+ 'gmail.users.labels.get',
55
+ 'gmail.users.messages.list',
56
+ 'gmail.users.messages.get',
57
+ 'gmail.users.messages.attachments.get',
58
+ 'gmail.users.threads.list',
59
+ 'gmail.users.threads.get',
60
+ 'gmail.users.drafts.list',
61
+ 'gmail.users.drafts.get',
62
+ // Write, all of it `gmail.compose`, all of it in the write bundle.
63
+ // `delete` addresses a draft that already exists, which is the half that
64
+ // was missing: without it the only way to discard a draft was the Gmail
65
+ // UI.
66
+ 'gmail.users.drafts.send',
67
+ 'gmail.users.drafts.delete',
68
+ // Organising, all of it `gmail.modify`. Gmail has no separate verb for
69
+ // read-state or spam: marking read is removing `UNREAD`, marking spam is
70
+ // adding `SPAM`, archiving is removing `INBOX`. It is all label edits, so
71
+ // `modify` is the whole feature and there is no narrower scope that does
72
+ // it — `gmail.labels` governs the label vocabulary, not its application.
73
+ 'gmail.users.messages.modify',
74
+ 'gmail.users.messages.batchModify',
75
+ 'gmail.users.messages.trash',
76
+ 'gmail.users.messages.untrash',
77
+ 'gmail.users.threads.modify',
78
+ 'gmail.users.threads.trash',
79
+ 'gmail.users.threads.untrash',
80
+ 'gmail.users.labels.create',
81
+ 'gmail.users.labels.update',
82
+ 'gmail.users.labels.delete',
83
+ // Blocking a sender, which is the one mail verb nothing above reaches.
84
+ // Adding `SPAM` to a message is Report-spam and trains the classifier;
85
+ // Block-sender is a filter, and a filter is the only thing here that acts
86
+ // on mail that has not arrived yet. `create` and `delete` accept
87
+ // `gmail.settings.basic` and nothing else, which is why the manifest asks
88
+ // for it and why it is marked broad.
89
+ //
90
+ // `list` is here because a create tool with no way to see what already
91
+ // exists is half a feature — and it is free, accepting `gmail.readonly`,
92
+ // which the manifest already held. `get` is not: `list` returns every
93
+ // filter in full, so it is the `labels.patch` objection again.
94
+ 'gmail.users.settings.filters.list',
95
+ 'gmail.users.settings.filters.create',
96
+ 'gmail.users.settings.filters.delete',
97
+ //
98
+ // Not listed, deliberately: `messages.delete` and `threads.delete` are
99
+ // permanent and would force `mail.google.com`, the one scope we refuse;
100
+ // `labels.patch` duplicates `update`, and a redundant tool costs context
101
+ // on every call; `messages.insert`/`import` take a full `Message` body,
102
+ // which is the `cutCycles` hazard below.
103
+ //
104
+ // `drafts.create` was here and is gone, which is the `labels.patch`
105
+ // objection with a bill attached. `send_message` already creates drafts —
106
+ // `draft_only: true` posts the assembled message to /drafts — so the two
107
+ // differed only in whether the endpoint composed the MIME for you, with
108
+ // nothing in the tool list saying so. The generated one took a base64url
109
+ // `raw` the caller had to assemble, which is the failure ADR-017 exists
110
+ // for: a 239 KB PDF is about 320,000 characters a model cannot write in
111
+ // one message. Two tools for one job, one of which cannot attach a file.
112
+ // The test in `specs.test.ts` that was supposed to catch this only knew
113
+ // about `messages.send`; it knows about drafts now.
114
+ //
115
+ // `drafts.update` followed it out, one change later and for exactly the
116
+ // same reason — it was kept at first only because nothing else could
117
+ // revise a draft, and that stopped being true when `send_message` grew
118
+ // `draft_id`. The `Draft` body is the same unusable shape either way:
119
+ // workable for a line of text, impossible for the attachment that is the
120
+ // whole reason someone is correcting a draft rather than retyping it.
121
+ ],
122
+ },
123
+ drive: {
124
+ source: 'https://api.apis.guru/v2/specs/googleapis.com/drive/v3/openapi.json',
125
+ out: 'drive.v3.json',
126
+ operations: [
127
+ 'drive.about.get',
128
+ 'drive.files.list',
129
+ 'drive.files.get',
130
+ 'drive.files.export',
131
+ 'drive.permissions.list',
132
+ // Write, and every one of them limited to files this app created by the
133
+ // `drive.file` scope. That bound is why there is no rename-anything tool
134
+ // here: `drive.file`'s other half is files the user picks, which arrives
135
+ // through the Google Picker, and there is no picker on an MCP endpoint.
136
+ 'drive.files.create',
137
+ 'drive.files.update',
138
+ 'drive.files.copy',
139
+ 'drive.permissions.create',
140
+ //
141
+ // Not listed, deliberately: `files.delete` is permanent, and Drive has a
142
+ // trash — `files.update` with `trashed: true` is the recoverable form of
143
+ // the same intent, which is the `messages.delete` reasoning again.
144
+ // `permissions.update` and `permissions.delete` would let an agent revoke
145
+ // access it did not grant, including somebody else's.
146
+ ],
147
+ },
148
+ //
149
+ // Sheets and Docs exist because Drive cannot do this. Drive treats a Google
150
+ // file as an opaque blob: there is no Drive operation that edits a cell, and
151
+ // the only route back in would be `files.update` with media — a whole-file
152
+ // replace via import conversion, which discards formulas, formatting, tabs,
153
+ // and comments. Cell-level editing lives solely in the Sheets API, and
154
+ // paragraph-level editing solely in the Docs API.
155
+ sheets: {
156
+ source: 'https://api.apis.guru/v2/specs/googleapis.com/sheets/v4/openapi.json',
157
+ out: 'sheets.v4.json',
158
+ opaque: ['Request', 'Response'],
159
+ operations: [
160
+ 'sheets.spreadsheets.get',
161
+ 'sheets.spreadsheets.values.get',
162
+ 'sheets.spreadsheets.values.batchGet',
163
+ // Write. `values.*` is the whole point of the provider — reading a range
164
+ // and writing it back is what "work on the file directly" means.
165
+ 'sheets.spreadsheets.values.update',
166
+ 'sheets.spreadsheets.values.batchUpdate',
167
+ 'sheets.spreadsheets.values.append',
168
+ 'sheets.spreadsheets.values.clear',
169
+ // The N-ary form of `values.clear`, and here for consistency rather than
170
+ // reach: `values.batchGet` and `values.batchUpdate` are both already the
171
+ // batch forms of vendored unary operations, so refusing this one alone
172
+ // was an inconsistency with no argument behind it. It also collapses
173
+ // clearing five tabs from five audit events into one that names all five
174
+ // ranges. Measured at 0.6 KB.
175
+ 'sheets.spreadsheets.values.batchClear',
176
+ // Copying a tab into a *different* spreadsheet, which nothing else here
177
+ // can do: `batchUpdate`'s `DuplicateSheetRequest` cannot cross a file
178
+ // boundary, and `drive.files.copy` copies the whole file. Not the
179
+ // `labels.patch` objection — that refused a redundant tool, and this is
180
+ // reachable no other way. Measured at 0.7 KB.
181
+ 'sheets.spreadsheets.sheets.copyTo',
182
+ // Structural edits — tabs, formatting, frozen rows, charts. Only reachable
183
+ // at all because `Request` is made opaque below; see `makeOpaque`.
184
+ 'sheets.spreadsheets.batchUpdate',
185
+ ],
186
+ },
187
+ docs: {
188
+ source: 'https://api.apis.guru/v2/specs/googleapis.com/docs/v1/openapi.json',
189
+ out: 'docs.v1.json',
190
+ opaque: ['Request', 'Response'],
191
+ operations: [
192
+ 'docs.documents.get',
193
+ // The only way to edit a document at all. Docs has no `values`-style
194
+ // shortcut, so this operation is the entire write surface.
195
+ 'docs.documents.batchUpdate',
196
+ ],
197
+ },
198
+ calendar: {
199
+ source: 'https://api.apis.guru/v2/specs/googleapis.com/calendar/v3/openapi.json',
200
+ out: 'calendar.v3.json',
201
+ operations: [
202
+ 'calendar.calendarList.list',
203
+ 'calendar.events.list',
204
+ 'calendar.events.get',
205
+ 'calendar.events.instances',
206
+ // Reading, but a POST: `freeBusy` takes a body because it asks about
207
+ // several calendars at once. It is the "when am I free" primitive, and
208
+ // the reason `calendar.readonly` is requested beside `calendar.events` —
209
+ // this operation and `calendarList.list` accept nothing narrower.
210
+ 'calendar.freebusy.query',
211
+ // Write, all of it `calendar.events`. Nothing here needs full `calendar`.
212
+ 'calendar.events.insert',
213
+ 'calendar.events.patch',
214
+ 'calendar.events.delete',
215
+ 'calendar.events.move',
216
+ //
217
+ // Not listed, deliberately. `events.update` is the PUT beside `patch`,
218
+ // and this is the inverse of the `labels.patch` decision above: there the
219
+ // two were the same call and the redundant one went, here they differ and
220
+ // the difference destroys data. PUT replaces the resource, so an agent
221
+ // that reads an event, edits `summary`, and sends it back drops
222
+ // everything it did not echo — attendees, reminders, recurrence,
223
+ // conferencing. One of them is needed and only one of them is safe.
224
+ // `events.quickAdd` parses "lunch with Bob Tuesday" into an event, which
225
+ // is a service for a caller that cannot do that itself; a model can, and
226
+ // `insert` takes the end time and time zone that quickAdd has to guess.
227
+ // `events.import`, `calendars.*`, `calendarList.insert`/`delete` and
228
+ // `acl.*` create, delete, or re-share whole calendars, and every one of
229
+ // them requires the full `calendar` scope — the scope this provider
230
+ // exists to avoid. The `watch` operations push to a webhook this endpoint
231
+ // does not have. `colors.get` and `settings.list` answer nothing anyone
232
+ // asked: the primary calendar's time zone is already in `calendarList`.
233
+ ],
234
+ },
235
+ tasks: {
236
+ source: 'https://api.apis.guru/v2/specs/googleapis.com/tasks/v1/openapi.json',
237
+ out: 'tasks.v1.json',
238
+ operations: [
239
+ 'tasks.tasklists.list',
240
+ 'tasks.tasks.list',
241
+ 'tasks.tasks.get',
242
+ // Write. Tasks publishes exactly two scopes — `tasks` and
243
+ // `tasks.readonly` — so every line below is the same grant as every
244
+ // other, and there is no narrower one to prefer.
245
+ 'tasks.tasklists.insert',
246
+ 'tasks.tasklists.patch',
247
+ 'tasks.tasks.insert',
248
+ 'tasks.tasks.patch',
249
+ 'tasks.tasks.delete',
250
+ 'tasks.tasks.move',
251
+ //
252
+ // Not listed, deliberately: `tasklists.delete` destroys a list and every
253
+ // task in it, and Tasks has no trash — the `messages.delete` refusal
254
+ // applied to the container rather than the item. `tasks.clear` hides
255
+ // every completed task in a list in one call, naming none of them, so the
256
+ // audit entry could not say what it did. `tasklists.update` and
257
+ // `tasks.update` are the PUTs beside the PATCHes; see `events.update`.
258
+ // `tasklists.get` is `tasklists.list` with an argument.
259
+ ],
260
+ },
261
+ //
262
+ // `contacts` rather than `people`: the API is People, but the thing an
263
+ // operator connects is their contacts, and `icloud_contacts` is already the
264
+ // neighbour. It is the one key here whose name differs from the API it
265
+ // vendors, which matters in `redact.ts` — see the note there.
266
+ contacts: {
267
+ source: 'https://api.apis.guru/v2/specs/googleapis.com/people/v1/openapi.json',
268
+ out: 'people.v1.json',
269
+ operations: [
270
+ 'people.people.searchContacts',
271
+ 'people.people.getBatchGet',
272
+ // A second store, and a second scope. "Other contacts" is where Gmail
273
+ // files an address written to but never saved, which for "email Bob" is
274
+ // more often than not where Bob actually is.
275
+ 'people.otherContacts.search',
276
+ //
277
+ // Not listed, and this one is not a judgement call: `people.people.get`
278
+ // and `people.people.connections.list` cannot work through this
279
+ // connector. Google's discovery document writes their paths as
280
+ // `v1/{+resourceName}` — RFC 6570 reserved expansion, which permits the
281
+ // slash that every value has (`people/me`, `people/c8891…`). The OpenAPI
282
+ // conversion drops the `+`, and `buildPath` percent-encodes a path value
283
+ // as a single segment, which is correct for a plain `{var}` and fatal
284
+ // here. What goes out is `/v1/people%2Fme`, and Google's frontend answers
285
+ // 404 with an HTML page; the same URL with a literal slash reaches the
286
+ // API and answers 403 for the missing credential. Measured against the
287
+ // live service, not reasoned. Both would list fine, pass policy, and fail
288
+ // every call — so the enumeration of every contact is deliberately absent
289
+ // and `searchContacts` is how a name becomes an address.
290
+ //
291
+ // Also not listed: everything under the `contacts` write scope, and
292
+ // `directory.readonly`, which is a Workspace directory and a 403 on a
293
+ // personal account. `contactGroups.*` only helps filter an enumeration
294
+ // this provider does not have.
295
+ ],
296
+ },
297
+ //
298
+ // Not listed for either, deliberately: `spreadsheets.create` and
299
+ // `documents.create`. `drive.files.create` already makes an empty Google-native
300
+ // file by `mimeType` under scopes we hold anyway, and their request bodies are
301
+ // whole `Spreadsheet`/`Document` objects — the same hazard class as the
302
+ // `messages.insert` rejected above.
303
+ //
304
+ // This used to say "122 and 101 extra schemas", which is the wrong number to
305
+ // argue with and nearly got the decision reversed: a schema count reads
306
+ // arguable, and the budget in `cli/tools.test.ts` is 64 KB of *generated*
307
+ // input schema. Those differ by 40× here, because `mcp-from-openapi` inlines
308
+ // `$ref`s and a shared schema is duplicated at every use site. Measured with
309
+ // the real generator, `spreadsheets.create` is **1,133 KB** — 17.7× the
310
+ // budget. `opaque: ['Request','Response']` does nothing for it: the body fans
311
+ // out `Spreadsheet → Sheet → GridData → CellData → CellFormat/ChartSpec` and
312
+ // `Spreadsheet → SpreadsheetProperties → CellFormat/SpreadsheetTheme`, and
313
+ // neither path passes through `Request`.
314
+ //
315
+ // Adding `Sheet` to `opaque` gets it to 80.7 KB, still over. The only
316
+ // configuration that fits also makes `SpreadsheetProperties` opaque — at
317
+ // which point the tool cannot describe `title`, and it is a tool that tells
318
+ // an agent nothing for a job `drive.files.create` does with typed `name`,
319
+ // `mimeType`, and `parents`, uniformly for Sheets, Docs, and folders.
320
+ // `documents.create` is the same shape at 98.1 KB, and Google ignores every
321
+ // field but `title` on it anyway.
322
+ };
323
+
324
+ /** Every `$ref` target reachable from a value, transitively. */
325
+ function referenced(root: unknown, schemas: Record<string, unknown>): Set<string> {
326
+ const found = new Set<string>();
327
+ const queue: unknown[] = [root];
328
+
329
+ while (queue.length > 0) {
330
+ const node = queue.pop();
331
+ if (node === null || typeof node !== 'object') continue;
332
+
333
+ if (Array.isArray(node)) {
334
+ queue.push(...node);
335
+ continue;
336
+ }
337
+
338
+ for (const [key, value] of Object.entries(node)) {
339
+ if (key === '$ref' && typeof value === 'string') {
340
+ const name = value.replace('#/components/schemas/', '');
341
+ if (!found.has(name) && name in schemas) {
342
+ found.add(name);
343
+ queue.push(schemas[name]);
344
+ }
345
+ continue;
346
+ }
347
+ queue.push(value);
348
+ }
349
+ }
350
+
351
+ return found;
352
+ }
353
+
354
+ /**
355
+ * Cut reference cycles, replacing the back-edge with an open object.
356
+ *
357
+ * Gmail's `MessagePart` contains `MessagePart[]` — a MIME tree, so the
358
+ * recursion is honest — and the OpenAPI tool generator inlines `$ref`s, so it
359
+ * recurses until the stack ends. That silently costs the two draft-writing
360
+ * operations, which are the useful half of `gmail.compose`.
361
+ *
362
+ * Cutting the back-edge rather than dropping the operation keeps the tool: the
363
+ * field that matters for creating a draft is `raw`, an RFC 2822 message, and
364
+ * nothing below the cut is required to fill it in. A depth-first walk marks the
365
+ * names currently on the path, and any `$ref` reaching back to one of them
366
+ * becomes a plain object.
367
+ */
368
+ function cutCycles(schemas: Record<string, unknown>): number {
369
+ let cuts = 0;
370
+
371
+ const walk = (node: unknown, path: Set<string>): void => {
372
+ if (node === null || typeof node !== 'object') return;
373
+
374
+ if (Array.isArray(node)) {
375
+ for (const item of node) walk(item, path);
376
+ return;
377
+ }
378
+
379
+ const record = node as Record<string, unknown>;
380
+ for (const [key, value] of Object.entries(record)) {
381
+ if (key === '$ref' && typeof value === 'string') {
382
+ const name = value.replace('#/components/schemas/', '');
383
+ if (path.has(name)) {
384
+ delete record['$ref'];
385
+ record['type'] = 'object';
386
+ record['additionalProperties'] = true;
387
+ record['description'] = `A nested ${name}. Structure omitted: it recurses.`;
388
+ cuts++;
389
+ } else if (name in schemas) {
390
+ walk(schemas[name], new Set([...path, name]));
391
+ }
392
+ continue;
393
+ }
394
+ walk(value, path);
395
+ }
396
+ };
397
+
398
+ for (const [name, schema] of Object.entries(schemas)) walk(schema, new Set([name]));
399
+ return cuts;
400
+ }
401
+
402
+ /**
403
+ * Replace a named schema with an open object.
404
+ *
405
+ * Same device as `cutCycles` and a different disease. That one cuts recursion;
406
+ * this one cuts *fan-out*. `mcp-from-openapi` inlines `$ref`s, so a schema's
407
+ * cost to us is not its size in the document but its size once every reference
408
+ * below it has been expanded — and Sheets' `Request` is a union of some eighty
409
+ * variants, each with its own nested grid, filter, and chart schemas, sharing
410
+ * sub-schemas that inlining duplicates per occurrence.
411
+ *
412
+ * Measured: `sheets.spreadsheets.batchUpdate` generates a 2,469KB input schema,
413
+ * against 45KB for the whole of Drive. That is unusable — it would be sent on
414
+ * every `tools/list` — and it is also the only operation that can add a tab,
415
+ * freeze a header, or format a cell, so dropping it is no better.
416
+ *
417
+ * Opaque keeps the operation and costs 3KB. The agent fills in `requests` from
418
+ * the API it already knows, which is the same bet `raw` makes for a Gmail draft:
419
+ * a well-known wire format is cheaper described than schematised. The pointer to
420
+ * Google's reference is in the description because that is the only thing lost.
421
+ *
422
+ * Applied before `referenced`, so the schemas that were reachable only through
423
+ * the replaced one leave the document entirely rather than lingering unused.
424
+ */
425
+ function makeOpaque(schemas: Record<string, unknown>, names: readonly string[]): number {
426
+ let replaced = 0;
427
+
428
+ for (const name of names) {
429
+ if (!(name in schemas)) continue;
430
+ const original = schemas[name] as { description?: string };
431
+ schemas[name] = {
432
+ type: 'object',
433
+ additionalProperties: true,
434
+ description:
435
+ `${original.description ?? `A ${name}.`} Structure omitted: it expands to megabytes ` +
436
+ 'when inlined. Pass the object as documented at https://developers.google.com/workspace.',
437
+ };
438
+ replaced++;
439
+ }
440
+
441
+ return replaced;
442
+ }
443
+
444
+ /**
445
+ * Google's system parameters, dropped from every operation.
446
+ *
447
+ * Three reasons, in descending order of how badly they bite.
448
+ *
449
+ * `$.xgafv` is not a legal tool-property name: the Anthropic API requires
450
+ * `^[a-zA-Z0-9_.-]{1,64}$`, and `$` fails it — so a single tool carrying it
451
+ * rejects the *entire* tools list with a 400, and every provider on the
452
+ * endpoint stops working, not just Gmail.
453
+ *
454
+ * `access_token`, `key`, and `oauth_token` let a caller supply their own
455
+ * credentials as query parameters. Authentication belongs to the connector,
456
+ * which sets a header; offering an agent a second way to authenticate is
457
+ * offering it a way to authenticate as something else.
458
+ *
459
+ * The rest is noise that costs schema for nothing.
460
+ *
461
+ * `fields` and `alt` are deliberately kept: `drive.about.get` *requires*
462
+ * `fields`, and `alt=media` is how file content is downloaded.
463
+ */
464
+ const SYSTEM_PARAMETERS = new Set([
465
+ '$.xgafv',
466
+ 'access_token',
467
+ 'callback',
468
+ 'key',
469
+ 'oauth_token',
470
+ 'prettyPrint',
471
+ 'quotaUser',
472
+ 'upload_protocol',
473
+ 'uploadType',
474
+ ]);
475
+
476
+ /**
477
+ * Strip system parameters from a path item or an operation.
478
+ *
479
+ * They arrive as `$ref`s into `components.parameters`, and the component *key*
480
+ * is not the parameter name — `$.xgafv` is keyed `_.xgafv`, because a `$` is
481
+ * awkward in a JSON pointer. Matching on the key would therefore miss exactly
482
+ * the one that breaks everything, so the ref is resolved first.
483
+ */
484
+ function dropSystemParameters(
485
+ holder: Record<string, unknown>,
486
+ components: Record<string, unknown>,
487
+ ): number {
488
+ const parameters = holder['parameters'];
489
+ if (!Array.isArray(parameters)) return 0;
490
+
491
+ const nameOf = (parameter: unknown): string | undefined => {
492
+ const record = parameter as { name?: string; $ref?: string };
493
+ if (record.name) return record.name;
494
+ if (!record.$ref) return undefined;
495
+
496
+ const key = record.$ref.replace('#/components/parameters/', '');
497
+ return (components[key] as { name?: string } | undefined)?.name;
498
+ };
499
+
500
+ const kept = parameters.filter((parameter) => {
501
+ const name = nameOf(parameter);
502
+ return !(name && SYSTEM_PARAMETERS.has(name));
503
+ });
504
+
505
+ holder['parameters'] = kept;
506
+ return parameters.length - kept.length;
507
+ }
508
+
509
+ const METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head'];
510
+
511
+ async function vendor(id: string): Promise<void> {
512
+ const { source, out, operations, opaque } = SELECTION[id]!;
513
+ const wanted = new Set(operations);
514
+
515
+ const response = await fetch(source);
516
+ if (!response.ok) throw new Error(`${source}: HTTP ${response.status}`);
517
+ const spec = (await response.json()) as Spec;
518
+
519
+ const paths: Spec['paths'] = {};
520
+ const seen = new Set<string>();
521
+
522
+ for (const [path, item] of Object.entries(spec.paths)) {
523
+ const kept: Record<string, unknown> = {};
524
+ for (const [method, operation] of Object.entries(item)) {
525
+ // Path-level keys are not operations and must survive: `parameters` here
526
+ // is where Google puts the query parameters shared by every method on the
527
+ // path, and `fields` is one of them — which `drive.about.get` *requires*.
528
+ // Dropping it produced a tool with no arguments at all and a 400 on every
529
+ // call.
530
+ if (!METHODS.includes(method)) {
531
+ kept[method] = operation;
532
+ continue;
533
+ }
534
+ const operationId = operation.operationId;
535
+ if (!operationId || !wanted.has(operationId)) continue;
536
+ kept[method] = operation;
537
+ seen.add(operationId);
538
+ }
539
+
540
+ // Only path-level keys survived, so no operation here was wanted.
541
+ if (!Object.keys(kept).some((key) => METHODS.includes(key))) continue;
542
+ if (Object.keys(kept).length > 0) paths[path] = kept as Spec['paths'][string];
543
+ }
544
+
545
+ const componentParameters = (spec.components?.['parameters'] ?? {}) as Record<string, unknown>;
546
+
547
+ let dropped = 0;
548
+ for (const item of Object.values(paths)) {
549
+ dropped += dropSystemParameters(item as unknown as Record<string, unknown>, componentParameters);
550
+ for (const [method, operation] of Object.entries(item)) {
551
+ if (!METHODS.includes(method)) continue;
552
+ dropped += dropSystemParameters(
553
+ operation as unknown as Record<string, unknown>,
554
+ componentParameters,
555
+ );
556
+ }
557
+ }
558
+
559
+ // Drop response schemas.
560
+ //
561
+ // Two reasons, and the second is the blocking one. Nothing reads them: the
562
+ // connector hands the response body back as text, because an agent wants the
563
+ // JSON, not a validated shape. And Gmail's response schemas are recursive —
564
+ // `Message.payload` is a `MessagePart`, which contains `MessagePart[]` — which
565
+ // sends the OpenAPI tool generator into infinite recursion and silently drops
566
+ // eight of Gmail's twelve operations from the tool list.
567
+ for (const item of Object.values(paths)) {
568
+ for (const [method, operation] of Object.entries(item)) {
569
+ if (!METHODS.includes(method)) continue;
570
+ (operation as { responses?: unknown }).responses = {
571
+ '200': { description: 'Success. The response body is returned verbatim.' },
572
+ };
573
+ }
574
+ }
575
+
576
+ const missing = operations.filter((operationId) => !seen.has(operationId));
577
+ if (missing.length > 0) {
578
+ // Loudly, rather than shipping a provider quietly missing capabilities: an
579
+ // upstream rename should fail the refresh, not shrink the tool list.
580
+ throw new Error(`${id}: these operations are not in the spec — ${missing.join(', ')}`);
581
+ }
582
+
583
+ const schemas = spec.components?.schemas ?? {};
584
+ const opaqued = makeOpaque(schemas, opaque ?? []);
585
+ const keep = referenced(paths, schemas);
586
+ const trimmedSchemas = Object.fromEntries(
587
+ Object.entries(schemas).filter(([name]) => keep.has(name)),
588
+ );
589
+ const cuts = cutCycles(trimmedSchemas);
590
+
591
+ const trimmed: Spec = {
592
+ openapi: spec.openapi,
593
+ info: {
594
+ ...spec.info,
595
+ 'x-vendored-from': source,
596
+ 'x-vendored-note':
597
+ 'Trimmed by src/providers/google/specs/vendor.ts. Committed deliberately: a spec decides which paths are called with the operator token, so it must be reviewable rather than fetched at connect time.',
598
+ },
599
+ ...(spec.servers ? { servers: spec.servers } : {}),
600
+ paths,
601
+ components: { ...spec.components, schemas: trimmedSchemas },
602
+ };
603
+
604
+ // `import.meta.dir` *is* the specs directory — this script lives beside what it
605
+ // writes. It used to be `scripts/vendor-google-specs.ts` and reached across the
606
+ // tree; commit 3cd03ce moved the script and the specs together but not the path
607
+ // arithmetic, so for a while this resolved to
608
+ // `src/providers/google/providers/builtin/specs`, created it, reported success,
609
+ // and left the committed spec untouched — making the documented "re-run the
610
+ // script" step a silent no-op.
611
+ const directory = import.meta.dir;
612
+ await mkdir(directory, { recursive: true });
613
+ await writeFile(join(directory, out), `${JSON.stringify(trimmed, null, 2)}\n`);
614
+
615
+ const size = Math.round(JSON.stringify(trimmed).length / 1024);
616
+ console.log(
617
+ ` ${id.padEnd(6)} ${String(Object.keys(paths).length).padStart(2)} paths, ` +
618
+ `${seen.size} operations, ${Object.keys(trimmedSchemas).length} schemas, ` +
619
+ `${cuts} cycle${cuts === 1 ? '' : 's'} cut, ${opaqued} made opaque, ` +
620
+ `${dropped} system params dropped, ${size}KB`,
621
+ );
622
+
623
+ await reportLargestTools(trimmed);
624
+ }
625
+
626
+ /**
627
+ * The number the budget is actually about.
628
+ *
629
+ * Everything on the line above counts the *spec*; `cli/tools.test.ts` measures
630
+ * the **generated input schema**, and the two differ by orders of magnitude
631
+ * because `mcp-from-openapi` inlines `$ref`s — a schema shared by ten fields is
632
+ * ten copies once generated. Reasoning about `spreadsheets.create` from a schema
633
+ * count put it at 122 KB when the real figure was 1,133 KB, which is the whole
634
+ * difference between "just over" and "seventeen times over".
635
+ *
636
+ * Printed here so the refresh that adds an operation shows its cost, rather than
637
+ * leaving it to a test failure to say so after the fact.
638
+ */
639
+ async function reportLargestTools(trimmed: Spec): Promise<void> {
640
+ try {
641
+ const generator = await OpenAPIToolGenerator.fromJSON(trimmed);
642
+ const measured = (await generator.generateTools())
643
+ .map((tool: McpOpenAPITool) => ({
644
+ name: tool.metadata.operationId ?? tool.name,
645
+ kb: JSON.stringify(tool.inputSchema).length / 1024,
646
+ }))
647
+ .sort((a, b) => b.kb - a.kb);
648
+
649
+ for (const { name, kb } of measured.slice(0, 3)) {
650
+ const over = kb > BUDGET_KB ? ` ✗ over the ${BUDGET_KB}KB budget` : '';
651
+ console.log(` ${name.padEnd(38)} ${kb.toFixed(1).padStart(8)}KB${over}`);
652
+ }
653
+ } catch (error) {
654
+ // A generator failure is a real problem, but it is `tools.test.ts`'s to
655
+ // report against every provider at once. Refusing to finish the vendoring
656
+ // over it would leave the specs half-written.
657
+ console.log(` (could not measure generated schemas: ${String(error)})`);
658
+ }
659
+ }
660
+
661
+ for (const id of Object.keys(SELECTION)) await vendor(id);