@lanes-sh/link 0.6.11 → 0.7.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 (201) hide show
  1. package/README.md +62 -45
  2. package/instructions/skills/lanes-link/SKILL.md +25 -0
  3. package/package.json +3 -2
  4. package/src/audit/conformance.ts +1 -1
  5. package/src/cli/argv.ts +14 -1
  6. package/src/cli/commands/connect/acquire.ts +100 -0
  7. package/src/cli/commands/connect/custom/index.ts +1 -1
  8. package/src/cli/commands/connect/declare.ts +19 -0
  9. package/src/cli/commands/connect/index.ts +54 -56
  10. package/src/cli/commands/connect/requirements.ts +23 -1
  11. package/src/cli/commands/connect/setup.ts +1 -1
  12. package/src/cli/commands/connect/variables.ts +220 -0
  13. package/src/cli/commands/knowledge/index.ts +22 -4
  14. package/src/cli/commands/knowledge/migrate.ts +61 -43
  15. package/src/cli/commands/operate/tools.ts +1 -1
  16. package/src/cli/commands/owner/entities.ts +244 -0
  17. package/src/cli/commands/owner/shared.ts +14 -0
  18. package/src/cli/commands/owner/vault.ts +2 -2
  19. package/src/cli/commands/owner.ts +13 -3
  20. package/src/cli/commands/setup.ts +12 -0
  21. package/src/cli/config-edit.ts +16 -6
  22. package/src/cli/config-repair.ts +10 -0
  23. package/src/cli/dispatch-owner.ts +30 -1
  24. package/src/cli/main.ts +2 -1
  25. package/src/cli/runtime/open.ts +14 -7
  26. package/src/cli/runtime/registry.ts +6 -1
  27. package/src/cli/selection.ts +11 -0
  28. package/src/cli/usage.ts +13 -2
  29. package/src/connectivity/auth/strategy/index.ts +1 -1
  30. package/src/connectivity/capability.ts +1 -1
  31. package/src/connectivity/context.ts +1 -1
  32. package/src/connectivity/index.ts +1 -1
  33. package/src/connectivity/mail/attachments.ts +42 -73
  34. package/src/connectivity/mail/content-type.ts +71 -0
  35. package/src/connectivity/mail/index.ts +1 -1
  36. package/src/connectivity/manifest/provider.ts +50 -0
  37. package/src/connectivity/manifest/variables.ts +173 -0
  38. package/src/connectivity/transports/factory.ts +74 -1
  39. package/src/connectivity/transports/http/index.ts +8 -2
  40. package/src/connectivity/transports/http/keys.ts +83 -0
  41. package/src/connectivity/transports/imap/attachment.ts +53 -10
  42. package/src/connectivity/transports/imap/capabilities.ts +28 -0
  43. package/src/connectivity/transports/imap/download.ts +111 -0
  44. package/src/connectivity/transports/imap/index.ts +13 -0
  45. package/src/connectivity/transports/imap/operations.ts +1 -0
  46. package/src/deployments/adapters/s3.ts +1 -1
  47. package/src/deployments/gcp/provision.ts +1 -1
  48. package/src/deployments/knowledge.ts +44 -18
  49. package/src/deployments/prepare.ts +1 -1
  50. package/src/deployments/serving.ts +1 -1
  51. package/src/dispatch/dispatch.ts +17 -10
  52. package/src/dispatch/staging.ts +76 -1
  53. package/src/profile/knowledge.ts +19 -11
  54. package/src/profile/schema.ts +2 -2
  55. package/src/providers/README.md +189 -0
  56. package/src/providers/airtable/index.ts +10 -0
  57. package/src/providers/algolia/index.ts +10 -0
  58. package/src/providers/amplitude/index.ts +10 -0
  59. package/src/providers/apify/index.ts +10 -0
  60. package/src/providers/asana/index.ts +10 -0
  61. package/src/providers/assets/provider.ts +11 -4
  62. package/src/providers/atlassian/index.ts +19 -0
  63. package/src/providers/attio/index.ts +10 -0
  64. package/src/providers/betterstack/index.ts +10 -0
  65. package/src/providers/box/index.ts +64 -0
  66. package/src/providers/brightdata/index.ts +10 -0
  67. package/src/providers/buildkite/index.ts +10 -0
  68. package/src/providers/bunq/index.ts +2 -2
  69. package/src/providers/calendly/index.ts +10 -0
  70. package/src/providers/canva/index.ts +10 -0
  71. package/src/providers/circleci/index.ts +10 -0
  72. package/src/providers/clickup/index.ts +10 -0
  73. package/src/providers/close/index.ts +10 -0
  74. package/src/providers/cloudflare_bindings/index.ts +14 -0
  75. package/src/providers/cloudflare_observability/index.ts +10 -0
  76. package/src/providers/contentful/index.ts +10 -0
  77. package/src/providers/custom/load.ts +1 -1
  78. package/src/providers/datadog/index.ts +10 -0
  79. package/src/providers/discord/index.ts +1 -1
  80. package/src/providers/discord/specs/discord.v10.json +0 -88
  81. package/src/providers/discord/specs/vendor.ts +1 -1
  82. package/src/providers/dropbox/index.ts +10 -0
  83. package/src/providers/entities/catalogue.ts +366 -0
  84. package/src/providers/entities/find.ts +279 -0
  85. package/src/providers/entities/provider.ts +334 -0
  86. package/src/providers/entities/render.ts +142 -0
  87. package/src/providers/entities/store.ts +276 -0
  88. package/src/providers/entities/writes.ts +279 -0
  89. package/src/providers/example/provider.ts +1 -1
  90. package/src/providers/expensify/index.ts +10 -0
  91. package/src/providers/fastmail/calendar/index.ts +21 -0
  92. package/src/providers/fastmail/contacts/index.ts +17 -0
  93. package/src/providers/fastmail/index.ts +4 -0
  94. package/src/providers/fastmail/mail/index.ts +38 -0
  95. package/src/providers/fastmail/shared/setup.ts +52 -0
  96. package/src/providers/figma/index.ts +10 -0
  97. package/src/providers/fireflies/index.ts +10 -0
  98. package/src/providers/flagsmith/index.ts +10 -0
  99. package/src/providers/gamma/index.ts +10 -0
  100. package/src/providers/github/index.ts +2 -2
  101. package/src/providers/google/gmail/attachment.ts +16 -1
  102. package/src/providers/google/gmail-imap/index.ts +7 -2
  103. package/src/providers/google/shared/oauth.ts +1 -1
  104. package/src/providers/google/shared/service-account.ts +1 -1
  105. package/src/providers/google/shared/setup.ts +1 -1
  106. package/src/providers/google/specs/calendar.v3.json +0 -32
  107. package/src/providers/google/specs/docs.v1.json +0 -76
  108. package/src/providers/google/specs/drive.v3.json +0 -32
  109. package/src/providers/google/specs/gmail.v1.json +0 -76
  110. package/src/providers/google/specs/people.v1.json +0 -77
  111. package/src/providers/google/specs/sheets.v4.json +0 -76
  112. package/src/providers/google/specs/tasks.v1.json +0 -76
  113. package/src/providers/grafana/index.ts +10 -0
  114. package/src/providers/heroku/index.ts +10 -0
  115. package/src/providers/hubspot/index.ts +75 -0
  116. package/src/providers/hygraph/index.ts +10 -0
  117. package/src/providers/icloud/mail/index.ts +5 -0
  118. package/src/providers/icloud/shared/setup.ts +1 -1
  119. package/src/providers/index.ts +255 -0
  120. package/src/providers/insightly/index.ts +10 -0
  121. package/src/providers/jam/index.ts +10 -0
  122. package/src/providers/klaviyo/index.ts +10 -0
  123. package/src/providers/mailbox/index.ts +87 -0
  124. package/src/providers/memory/provider.ts +3 -8
  125. package/src/providers/mercury/index.ts +10 -0
  126. package/src/providers/microsoft/calendar/index.ts +49 -0
  127. package/src/providers/microsoft/contacts/index.ts +50 -0
  128. package/src/providers/microsoft/drive/index.ts +62 -0
  129. package/src/providers/microsoft/index.ts +12 -0
  130. package/src/providers/microsoft/mail/index.ts +76 -0
  131. package/src/providers/microsoft/shared/oauth.ts +58 -0
  132. package/src/providers/microsoft/shared/scopes.ts +56 -0
  133. package/src/providers/microsoft/shared/setup.ts +45 -0
  134. package/src/providers/microsoft/specs/microsoft-todo.v1.json +755 -0
  135. package/src/providers/microsoft/specs/onedrive.v1.json +993 -0
  136. package/src/providers/microsoft/specs/outlook-calendar.v1.json +1743 -0
  137. package/src/providers/microsoft/specs/outlook-contacts.v1.json +430 -0
  138. package/src/providers/microsoft/specs/outlook-mail.v1.json +901 -0
  139. package/src/providers/microsoft/specs/vendor.ts +232 -0
  140. package/src/providers/microsoft/todo/index.ts +54 -0
  141. package/src/providers/miro/index.ts +10 -0
  142. package/src/providers/mixpanel/index.ts +10 -0
  143. package/src/providers/monday/index.ts +10 -0
  144. package/src/providers/mux/index.ts +10 -0
  145. package/src/providers/navan/index.ts +10 -0
  146. package/src/providers/neon/index.ts +10 -0
  147. package/src/providers/netlify/index.ts +10 -0
  148. package/src/providers/nextcloud/calendar/index.ts +28 -0
  149. package/src/providers/nextcloud/contacts/index.ts +18 -0
  150. package/src/providers/nextcloud/index.ts +3 -0
  151. package/src/providers/nextcloud/shared/setup.ts +59 -0
  152. package/src/providers/owner.ts +15 -3
  153. package/src/providers/paddle/index.ts +10 -0
  154. package/src/providers/paypal/index.ts +10 -0
  155. package/src/providers/posthog/index.ts +10 -0
  156. package/src/providers/prisma/index.ts +10 -0
  157. package/src/providers/ramp/index.ts +10 -0
  158. package/src/providers/recurly/index.ts +10 -0
  159. package/src/providers/reddit/index.ts +1 -1
  160. package/src/providers/remote/index.ts +10 -0
  161. package/src/providers/render/index.ts +35 -0
  162. package/src/providers/replicate/index.ts +10 -0
  163. package/src/providers/resend/index.ts +10 -0
  164. package/src/providers/riverside/index.ts +10 -0
  165. package/src/providers/rootly/index.ts +10 -0
  166. package/src/providers/rudderstack/index.ts +10 -0
  167. package/src/providers/salesloft/index.ts +10 -0
  168. package/src/providers/sanity/index.ts +10 -0
  169. package/src/providers/scopes.ts +2 -0
  170. package/src/providers/sentry/index.ts +10 -0
  171. package/src/providers/setup/plan.ts +11 -0
  172. package/src/providers/setup/provider.ts +12 -0
  173. package/src/providers/shared/openapi.ts +70 -0
  174. package/src/providers/shared/slug.ts +28 -0
  175. package/src/providers/shared/vendor-operations.ts +43 -1
  176. package/src/providers/shared/vendor-spec.ts +98 -17
  177. package/src/providers/shortcut/index.ts +10 -0
  178. package/src/providers/slack/index.ts +2 -2
  179. package/src/providers/square/index.ts +10 -0
  180. package/src/providers/storyblok/index.ts +10 -0
  181. package/src/providers/stripe/index.ts +10 -0
  182. package/src/providers/supabase/index.ts +10 -0
  183. package/src/providers/tasks/store.ts +2 -7
  184. package/src/providers/tavily/index.ts +10 -0
  185. package/src/providers/todoist/index.ts +10 -0
  186. package/src/providers/untested.ts +105 -0
  187. package/src/providers/vault/provider.ts +1 -1
  188. package/src/providers/vercel/index.ts +10 -0
  189. package/src/providers/vimeo/index.ts +10 -0
  190. package/src/providers/webflow/index.ts +10 -0
  191. package/src/providers/whimsical/index.ts +10 -0
  192. package/src/providers/wix/index.ts +10 -0
  193. package/src/providers/workable/index.ts +10 -0
  194. package/src/providers/yahoo_mail/index.ts +73 -0
  195. package/src/providers/zapier/index.ts +10 -0
  196. package/src/providers/zoho_mail/index.ts +79 -0
  197. package/src/secrets/document.ts +1 -1
  198. package/src/server/attachments.ts +129 -5
  199. package/src/server/index.ts +3 -3
  200. package/src/server/mcp/instructions.ts +88 -18
  201. package/src/server/stdio.ts +1 -1
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Replicate registers us at connect time — nothing for an operator to set up. */
4
+ export const replicate = defineProvider({
5
+ id: 'replicate',
6
+ name: 'Replicate',
7
+ description: 'Models, predictions, and deployments, via Replicate\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.replicate.com/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Resend registers us at connect time — nothing for an operator to set up. */
4
+ export const resend = defineProvider({
5
+ id: 'resend',
6
+ name: 'Resend',
7
+ description: 'Transactional email, domains, and delivery events, via Resend\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.resend.com/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Riverside registers us at connect time — nothing for an operator to set up. */
4
+ export const riverside = defineProvider({
5
+ id: 'riverside',
6
+ name: 'Riverside',
7
+ description: 'Recordings, transcripts, and clips, via Riverside\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.riverside.com/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Rootly registers us at connect time — nothing for an operator to set up. */
4
+ export const rootly = defineProvider({
5
+ id: 'rootly',
6
+ name: 'Rootly',
7
+ description: 'Incidents, alerts, retrospectives, and on-call schedules, via Rootly\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.rootly.com/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** RudderStack registers us at connect time — nothing for an operator to set up. */
4
+ export const rudderstack = defineProvider({
5
+ id: 'rudderstack',
6
+ name: 'RudderStack',
7
+ description: 'Sources, destinations, and event streams, via RudderStack\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.rudderstack.com/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Salesloft registers us at connect time — nothing for an operator to set up. */
4
+ export const salesloft = defineProvider({
5
+ id: 'salesloft',
6
+ name: 'Salesloft',
7
+ description: 'Cadences, people, and sales activity, via Salesloft\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.salesloft.com/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Sanity registers us at connect time — nothing for an operator to set up. */
4
+ export const sanity = defineProvider({
5
+ id: 'sanity',
6
+ name: 'Sanity',
7
+ description: 'Documents, datasets, schema, and content releases, via Sanity\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.sanity.io/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -1,5 +1,6 @@
1
1
  import { GOOGLE_SCOPE_MEANINGS } from './google/shared/scopes.ts';
2
2
  import { LINEAR_SCOPE_MEANINGS } from './linear/scopes.ts';
3
+ import { MICROSOFT_SCOPE_MEANINGS } from './microsoft/shared/scopes.ts';
3
4
  import { REDDIT_SCOPE_MEANINGS } from './reddit/scopes.ts';
4
5
  import { SLACK_SCOPE_MEANINGS } from './slack/scopes.ts';
5
6
 
@@ -25,6 +26,7 @@ export interface ScopeMeaning {
25
26
  export const SCOPE_MEANINGS: Record<string, ScopeMeaning> = {
26
27
  ...GOOGLE_SCOPE_MEANINGS,
27
28
  ...LINEAR_SCOPE_MEANINGS,
29
+ ...MICROSOFT_SCOPE_MEANINGS,
28
30
  ...REDDIT_SCOPE_MEANINGS,
29
31
  ...SLACK_SCOPE_MEANINGS,
30
32
  };
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Sentry registers us at connect time — nothing for an operator to set up. */
4
+ export const sentry = defineProvider({
5
+ id: 'sentry',
6
+ name: 'Sentry',
7
+ description: 'Issues, events, stack traces, and releases, via Sentry\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.sentry.dev/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -16,6 +16,13 @@ import { hasOwnClientPath, RESERVED_PROVIDER_IDS, setupRequirements, type SetupR
16
16
  */
17
17
 
18
18
  export interface ProviderPlan {
19
+ /** Where the service is, for a provider whose address the connection supplies. */
20
+ readonly variables: readonly {
21
+ readonly key: string;
22
+ readonly label: string;
23
+ readonly description: string;
24
+ readonly example: string;
25
+ }[];
19
26
  readonly id: string;
20
27
  readonly name: string;
21
28
  readonly description: string;
@@ -145,6 +152,10 @@ export function planFor(
145
152
  ...(manifest.setup?.docs_url ? { docsUrl: manifest.setup.docs_url } : {}),
146
153
  steps: manifest.setup?.steps ?? [],
147
154
  requires: requirements,
155
+ // Not a `requires` entry, and the difference is the point: a variable is not
156
+ // a secret, has no ref, and no `secrets set` line would place it. It is
157
+ // asked for at connect and written to the connection's own row.
158
+ variables: manifest.variables,
148
159
  needsId,
149
160
  command,
150
161
  brokered,
@@ -305,6 +305,18 @@ function renderProvider(plan: ProviderPlan): string {
305
305
  for (const requirement of plan.requires) lines.push(` ${requirement.label}`);
306
306
  }
307
307
 
308
+ // Listed alongside the credentials, because to the person answering they are
309
+ // the same thing — another question `connect` will ask. Omitted here would be
310
+ // worse than for a credential: an agent relaying a setup plan for Nextcloud
311
+ // would say "your username and password" and never mention the one value that
312
+ // provider cannot be connected without.
313
+ if (plan.variables.length > 0) {
314
+ lines.push('', 'And where the service is:');
315
+ for (const variable of plan.variables) {
316
+ lines.push(` ${variable.label} — ${variable.description} For example: ${variable.example}`);
317
+ }
318
+ }
319
+
308
320
  if (plan.needsId) {
309
321
  lines.push(
310
322
  '',
@@ -62,6 +62,76 @@ export function referenced(root: unknown, schemas: Record<string, unknown>): Set
62
62
  return found;
63
63
  }
64
64
 
65
+ /**
66
+ * Every component the document actually reaches, section by section.
67
+ *
68
+ * `referenced` above answers the same question for `schemas` alone, which was
69
+ * enough while every vendored document kept its other component sections small.
70
+ * Microsoft Graph does not: its published OpenAPI carries 726 shared responses
71
+ * and 1,419 examples, and copying those through untouched left a seven-operation
72
+ * spec at 962 KB with **733 dangling references** — because the responses that
73
+ * survived pointed at schemas the trim had just removed. The generator answered
74
+ * that with `Invalid OpenAPI document` and no tools at all.
75
+ *
76
+ * So reachability has to cross sections rather than stop at schemas: a path
77
+ * reaches a response, which reaches a schema, which reaches another. One walk,
78
+ * following `#/components/<section>/<name>` wherever it points, and whatever is
79
+ * not reached is not carried.
80
+ *
81
+ * `securitySchemes` is the exception and is never returned here — it is named by
82
+ * the `security` array rather than by `$ref`, so a reachability walk cannot see
83
+ * it and its caller keeps it whole.
84
+ */
85
+ export function reachableComponents(
86
+ root: unknown,
87
+ components: Record<string, unknown>,
88
+ ): Record<string, Set<string>> {
89
+ const found: Record<string, Set<string>> = {};
90
+ const queue: unknown[] = [root];
91
+
92
+ const resolve = (pointer: string): unknown => {
93
+ const rest = pointer.slice('#/components/'.length);
94
+ const slash = rest.indexOf('/');
95
+ if (slash < 0) return undefined;
96
+
97
+ const section = rest.slice(0, slash);
98
+ // JSON Pointer escaping. Component names rarely need it, and a name that
99
+ // does would otherwise be recorded under a spelling the filter never matches.
100
+ const name = rest.slice(slash + 1).replace(/~1/g, '/').replace(/~0/g, '~');
101
+
102
+ const entries = components[section];
103
+ if (entries === null || typeof entries !== 'object') return undefined;
104
+ if (!(name in (entries as Record<string, unknown>))) return undefined;
105
+
106
+ const seen = (found[section] ??= new Set());
107
+ if (seen.has(name)) return undefined;
108
+ seen.add(name);
109
+
110
+ return (entries as Record<string, unknown>)[name];
111
+ };
112
+
113
+ while (queue.length > 0) {
114
+ const node = queue.pop();
115
+ if (node === null || typeof node !== 'object') continue;
116
+
117
+ if (Array.isArray(node)) {
118
+ queue.push(...node);
119
+ continue;
120
+ }
121
+
122
+ for (const [key, value] of Object.entries(node)) {
123
+ if (key === '$ref' && typeof value === 'string' && value.startsWith('#/components/')) {
124
+ const target = resolve(value);
125
+ if (target !== undefined) queue.push(target);
126
+ continue;
127
+ }
128
+ queue.push(value);
129
+ }
130
+ }
131
+
132
+ return found;
133
+ }
134
+
65
135
  /**
66
136
  * Cut reference cycles, replacing the back-edge with an open object.
67
137
  *
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A stable id from something the owner typed, so writing does not demand one be
3
+ * invented.
4
+ *
5
+ * Three owner-layer stores derive an id this way — a memory entry from its
6
+ * title, a task from its title, an entity from its name — and they must derive
7
+ * it *identically*, because the id is the filename and a person who has learned
8
+ * what one store does with an apostrophe has learned what all three do.
9
+ *
10
+ * The 60-character cap is the whole of the length policy. It is not about any
11
+ * filesystem limit: it is that an id appears in a policy rule, a resource URI
12
+ * and an audit line, and a title-length one is unreadable in all three.
13
+ *
14
+ * `fallback` is the prefix for a name that slugifies to nothing — a title of
15
+ * only punctuation, or of a script this transliterates away. Producing
16
+ * `entry-14` rather than throwing is deliberate: the caller has content to
17
+ * store, and refusing it because its title is CJK would be a worse answer than
18
+ * an ugly id the owner can rename.
19
+ */
20
+ export function slugify(text: string, fallback: string): string {
21
+ const slug = text
22
+ .toLowerCase()
23
+ .replace(/[^a-z0-9]+/g, '-')
24
+ .replace(/^-+|-+$/g, '')
25
+ .slice(0, 60);
26
+
27
+ return slug.length > 0 ? slug : `${fallback}-${text.length}`;
28
+ }
@@ -127,6 +127,48 @@ export function narrowRequestBody(
127
127
  * Apply before reachability, so a schema the projection no longer reaches leaves
128
128
  * the document rather than lingering unused.
129
129
  */
130
+ /**
131
+ * Every property a schema describes, including the ones it inherits.
132
+ *
133
+ * OpenAPI spells inheritance `allOf: [{ $ref: parent }, { properties: … }]`, and
134
+ * a schema written that way has no `properties` of its own at the top level.
135
+ * Microsoft Graph uses it for every entity: `microsoft.graph.message` declares
136
+ * thirty-one properties and inherits `categories` from `outlookItem`, so reading
137
+ * `properties` off the schema alone reports a field the vendor plainly does
138
+ * describe as missing — and the refusal that exists to catch a rename fires on a
139
+ * document that never changed.
140
+ *
141
+ * Nearest definition wins, which is what `allOf` means: a later member overrides
142
+ * an earlier one, and the schema's own `properties` override everything it
143
+ * inherits. `seen` is against a cycle in the inheritance chain rather than in the
144
+ * data — `cutCycles` handles the other kind, and this walk would not reach it.
145
+ */
146
+ function describedProperties(
147
+ schema: unknown,
148
+ schemas: Record<string, unknown>,
149
+ seen: Set<string> = new Set(),
150
+ ): Record<string, unknown> {
151
+ if (schema === null || typeof schema !== 'object') return {};
152
+ const record = schema as Record<string, unknown>;
153
+
154
+ const reference = record['$ref'];
155
+ if (typeof reference === 'string') {
156
+ if (!reference.startsWith('#/components/schemas/')) return {};
157
+ const name = reference.slice('#/components/schemas/'.length);
158
+ if (seen.has(name)) return {};
159
+ seen.add(name);
160
+ return describedProperties(schemas[name], schemas, seen);
161
+ }
162
+
163
+ const collected: Record<string, unknown> = {};
164
+ for (const member of (record['allOf'] as unknown[] | undefined) ?? []) {
165
+ Object.assign(collected, describedProperties(member, schemas, seen));
166
+ }
167
+ Object.assign(collected, (record['properties'] as Record<string, unknown> | undefined) ?? {});
168
+
169
+ return collected;
170
+ }
171
+
130
172
  export function projectRequestBody(
131
173
  operation: Record<string, unknown>,
132
174
  operationId: string,
@@ -154,7 +196,7 @@ export function projectRequestBody(
154
196
  }
155
197
 
156
198
  const name = reference.slice('#/components/schemas/'.length);
157
- const source = (schemas[name] as { properties?: Record<string, unknown> } | undefined)?.properties ?? {};
199
+ const source = describedProperties(schemas[name], schemas);
158
200
 
159
201
  const properties: Record<string, unknown> = {};
160
202
  for (const field of fields) {
@@ -16,15 +16,16 @@
16
16
  * provider that has that shape.
17
17
  */
18
18
 
19
- import { mkdir, writeFile } from 'node:fs/promises';
19
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
20
20
  import { join } from 'node:path';
21
21
  import { OpenAPIToolGenerator, type McpOpenAPITool } from 'mcp-from-openapi';
22
- import { cutCycles, makeOpaque, referenced, type Spec } from './openapi.ts';
22
+ import { cutCycles, makeOpaque, reachableComponents, type Spec } from './openapi.ts';
23
23
  import {
24
24
  METHODS,
25
25
  dropSystemParameters,
26
26
  hoistParameters,
27
27
  narrowRequestBody,
28
+ projectRequestBody,
28
29
  } from './vendor-operations.ts';
29
30
 
30
31
  /** Kept in step with `BUDGET_KB` in `src/cli/tools.test.ts`, which enforces it. */
@@ -98,6 +99,23 @@ export interface VendorSpecOptions {
98
99
  * the document rather than lingering unused.
99
100
  */
100
101
  readonly rewriteRequestBody?: Readonly<Record<string, unknown>>;
102
+ /**
103
+ * Narrow an operation's request body to named fields, keeping the vendor's own
104
+ * types and descriptions.
105
+ *
106
+ * The remedy for a body that references a wide entity. Microsoft Graph's
107
+ * `PATCH /me/messages/{id}` takes a whole `microsoft.graph.message`, which
108
+ * inlines to 346 KB against a 64 KB budget — and almost all of it is fields
109
+ * the server computes and ignores on a write.
110
+ *
111
+ * Preferred over `rewriteRequestBody` wherever the body is a schema reference,
112
+ * because it is checked: a field the vendor has renamed, removed, or made
113
+ * read-only fails the refresh instead of becoming an argument the API quietly
114
+ * discards. Reach for the rewrite only where there is no schema to project.
115
+ */
116
+ readonly projectRequestBody?: Readonly<Record<string, readonly string[]>>;
117
+ /** What a projected body says about itself. One sentence, baked into the spec. */
118
+ readonly projectionNote?: string;
101
119
  }
102
120
 
103
121
  export async function vendorSpec(id: string, options: VendorSpecOptions): Promise<void> {
@@ -161,6 +179,7 @@ export async function vendorSpec(id: string, options: VendorSpecOptions): Promis
161
179
  }
162
180
 
163
181
  let narrowed = 0;
182
+ let projected = 0;
164
183
  for (const item of Object.values(paths)) {
165
184
  for (const [method, operation] of Object.entries(item)) {
166
185
  if (!METHODS.includes(method)) continue;
@@ -174,13 +193,44 @@ export async function vendorSpec(id: string, options: VendorSpecOptions): Promis
174
193
  );
175
194
  }
176
195
 
196
+ const fields = operation.operationId
197
+ ? options.projectRequestBody?.[operation.operationId]
198
+ : undefined;
199
+ if (fields && operation.operationId) {
200
+ projectRequestBody(
201
+ holder,
202
+ operation.operationId,
203
+ (spec.components?.schemas ?? {}) as Record<string, unknown>,
204
+ fields,
205
+ options.projectionNote ??
206
+ 'The fields this call actually writes. The entity it shares a schema with is far wider, ' +
207
+ 'and the rest of it is computed by the service rather than set here.',
208
+ );
209
+ projected += 1;
210
+ }
211
+
177
212
  const replacement = operation.operationId
178
213
  ? options.rewriteRequestBody?.[operation.operationId]
179
214
  : undefined;
180
215
  if (replacement) {
181
- const body = holder['requestBody'] as { content?: Record<string, unknown> } | undefined;
182
- for (const type of Object.keys(body?.content ?? {})) {
183
- (body!.content![type] as Record<string, unknown>)['schema'] = replacement;
216
+ const body = holder['requestBody'] as
217
+ | { $ref?: string; content?: Record<string, unknown> }
218
+ | undefined;
219
+
220
+ // A body declared as a shared `requestBodies` component has no `content`
221
+ // here to overwrite — Microsoft Graph's `sendMail` is one, and the
222
+ // rewrite silently did nothing, leaving the 346 KB entity it was meant
223
+ // to replace. Replacing the whole node is the only way in, and it also
224
+ // detaches the operation from a component the trim would otherwise carry.
225
+ if (!body || typeof body.$ref === 'string' || !body.content) {
226
+ holder['requestBody'] = {
227
+ required: true,
228
+ content: { 'application/json': { schema: replacement } },
229
+ };
230
+ } else {
231
+ for (const type of Object.keys(body.content)) {
232
+ (body.content[type] as Record<string, unknown>)['schema'] = replacement;
233
+ }
184
234
  }
185
235
  }
186
236
  }
@@ -212,10 +262,33 @@ export async function vendorSpec(id: string, options: VendorSpecOptions): Promis
212
262
 
213
263
  const schemas = spec.components?.schemas ?? {};
214
264
  const opaqued = makeOpaque(schemas, options.opaque ?? [], options.opaqueNote ?? '');
215
- const keep = referenced(paths, schemas);
216
- const trimmedSchemas = Object.fromEntries(
217
- Object.entries(schemas).filter(([name]) => keep.has(name)),
218
- );
265
+
266
+ // Reachability across *every* component section, not just schemas. A shared
267
+ // response that survives while the schema it points at is trimmed away is a
268
+ // dangling reference, and a document full of those is one the generator
269
+ // refuses outright — see `reachableComponents`.
270
+ const reach = reachableComponents(paths, (spec.components ?? {}) as Record<string, unknown>);
271
+
272
+ const carried: Record<string, unknown> = {};
273
+ for (const [section, entries] of Object.entries(spec.components ?? {})) {
274
+ if (entries === null || typeof entries !== 'object') continue;
275
+
276
+ // Named by the `security` array rather than by `$ref`, so no walk can see
277
+ // it and dropping it would leave operations declaring auth that resolves to
278
+ // nothing.
279
+ if (section === 'securitySchemes') {
280
+ carried[section] = entries;
281
+ continue;
282
+ }
283
+
284
+ const keep = reach[section];
285
+ if (!keep || keep.size === 0) continue;
286
+ carried[section] = Object.fromEntries(
287
+ Object.entries(entries as Record<string, unknown>).filter(([name]) => keep.has(name)),
288
+ );
289
+ }
290
+
291
+ const trimmedSchemas = (carried['schemas'] ?? {}) as Record<string, unknown>;
219
292
  const cuts = cutCycles(trimmedSchemas);
220
293
 
221
294
  const trimmed: Spec = {
@@ -227,19 +300,18 @@ export async function vendorSpec(id: string, options: VendorSpecOptions): Promis
227
300
  },
228
301
  ...(spec.servers ? { servers: spec.servers } : {}),
229
302
  paths,
230
- components: { ...spec.components, schemas: trimmedSchemas },
303
+ components: carried,
231
304
  };
232
305
 
233
306
  await mkdir(options.outputDirectory, { recursive: true });
234
- await writeFile(
235
- join(options.outputDirectory, options.out),
236
- `${JSON.stringify(trimmed, null, 2)}\n`,
237
- );
307
+ const outputPath = join(options.outputDirectory, options.out);
308
+ await writeFile(outputPath, `${JSON.stringify(trimmed, null, 2)}\n`);
238
309
 
239
310
  const size = Math.round(JSON.stringify(trimmed).length / 1024);
240
311
  const extra = [
241
312
  options.hoistPathParameters ? `${hoisted} params hoisted` : '',
242
313
  narrowed > 0 ? `${narrowed} body types dropped` : '',
314
+ projected > 0 ? `${projected} bodies projected` : '',
243
315
  ].filter(Boolean);
244
316
  console.log(
245
317
  ` ${id.padEnd(6)} ${String(Object.keys(paths).length).padStart(2)} paths, ` +
@@ -249,7 +321,7 @@ export async function vendorSpec(id: string, options: VendorSpecOptions): Promis
249
321
  (extra.length > 0 ? `, ${extra.join(', ')}` : ''),
250
322
  );
251
323
 
252
- await reportLargestTools(id, trimmed, seen.size);
324
+ await reportLargestTools(id, outputPath, seen.size);
253
325
  }
254
326
 
255
327
  /**
@@ -265,17 +337,26 @@ export async function vendorSpec(id: string, options: VendorSpecOptions): Promis
265
337
  * Printed here so the refresh that adds an operation shows its cost, rather than
266
338
  * leaving it to a test failure to say so after the fact.
267
339
  *
340
+ * Measured by reading back the file that was just written, rather than the
341
+ * object it was written from. Those are not reliably the same document — a key
342
+ * holding `undefined` is present in memory and gone through `JSON.stringify` —
343
+ * and the validator refused an in-memory shape whose committed form it accepted,
344
+ * reporting "could not measure" against a spec that was in fact fine. The
345
+ * artifact is what the budget is about, so the artifact is what is measured.
346
+ *
268
347
  * The count is the other half. The generator answers an unresolvable reference by
269
348
  * logging to the console and omitting that one tool, so a document can trim
270
349
  * cleanly, write successfully, and quietly advertise less than it selected.
271
350
  * `tools.test.ts` only asserts the surface is non-empty, so nothing downstream
272
351
  * would notice.
273
352
  */
274
- async function reportLargestTools(id: string, trimmed: Spec, expected: number): Promise<void> {
353
+ async function reportLargestTools(id: string, path: string, expected: number): Promise<void> {
275
354
  let measured: Array<{ name: string; kb: number }>;
276
355
 
277
356
  try {
278
- const generator = await OpenAPIToolGenerator.fromJSON(trimmed);
357
+ const generator = await OpenAPIToolGenerator.fromJSON(
358
+ JSON.parse(await readFile(path, 'utf8')) as Spec,
359
+ );
279
360
  const tools = await generator.generateTools();
280
361
 
281
362
  if (tools.length !== expected) {
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Shortcut registers us at connect time — nothing for an operator to set up. */
4
+ export const shortcut = defineProvider({
5
+ id: 'shortcut',
6
+ name: 'Shortcut',
7
+ description: 'Stories, epics, iterations, and workflows, via Shortcut\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.shortcut.com/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -98,11 +98,11 @@ export const slack = defineProvider({
98
98
  'Slack normally needs nothing set up — one browser round trip against the app Lanes ' +
99
99
  'registered. Pasting a token is the way past a workspace whose admin has not approved ' +
100
100
  'that app, using one from an app the workspace already trusts.',
101
- docs: 'docs/detailed/setup/slack.md',
101
+ docs: 'https://lanes.sh/docs/link/slack',
102
102
  docs_url: 'https://api.slack.com/apps',
103
103
  steps: [
104
104
  'Open https://api.slack.com/apps and choose "Create New App" → "From scratch". Name it and pick the workspace.',
105
- 'Open "OAuth & Permissions" and add the scopes you need under USER TOKEN SCOPES — not Bot Token Scopes; the MCP server reads the user token. The full set this provider asks for in the browser is listed in docs/detailed/setup/slack.md.',
105
+ 'Open "OAuth & Permissions" and add the scopes you need under USER TOKEN SCOPES — not Bot Token Scopes; the MCP server reads the user token. The full set this provider asks for in the browser is listed in https://lanes.sh/docs/link/slack.',
106
106
  'Choose "Install to Workspace" and approve. A Slack admin may have to approve it for you.',
107
107
  'Copy the "User OAuth Token". It starts with xoxp- — not the bot token, which starts with xoxb- and will not work here.',
108
108
  'The token does not expire unless you enable token rotation on the app. If you rotate or reinstall, run: lanes link connect slack --profile personal --target local --auth pasted_token --replace.',
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Square registers us at connect time — nothing for an operator to set up. */
4
+ export const square = defineProvider({
5
+ id: 'square',
6
+ name: 'Square',
7
+ description: 'Payments, orders, catalog, inventory, and customers, via Square\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.squareup.com/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Storyblok registers us at connect time — nothing for an operator to set up. */
4
+ export const storyblok = defineProvider({
5
+ id: 'storyblok',
6
+ name: 'Storyblok',
7
+ description: 'Stories, components, assets, and spaces, via Storyblok\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.storyblok.com/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Stripe registers us at connect time — nothing for an operator to set up. */
4
+ export const stripe = defineProvider({
5
+ id: 'stripe',
6
+ name: 'Stripe',
7
+ description: 'Payments, customers, invoices, subscriptions, and balances, via Stripe\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.stripe.com' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Supabase registers us at connect time — nothing for an operator to set up. */
4
+ export const supabase = defineProvider({
5
+ id: 'supabase',
6
+ name: 'Supabase',
7
+ description: 'Projects, database schema, SQL, edge functions, and docs, via Supabase\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.supabase.com/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -4,6 +4,7 @@ import {
4
4
  stringList,
5
5
  withFrontmatter,
6
6
  } from '#providers/shared/frontmatter.ts';
7
+ import { slugify as slugifyText } from '#providers/shared/slug.ts';
7
8
 
8
9
  /**
9
10
  * How a task is stored, and the only place that knows.
@@ -99,13 +100,7 @@ export function assertTaskId(id: string): void {
99
100
 
100
101
  /** A stable id from a title, so adding a task does not demand one be invented. */
101
102
  export function slugify(title: string): string {
102
- const slug = title
103
- .toLowerCase()
104
- .replace(/[^a-z0-9]+/g, '-')
105
- .replace(/^-+|-+$/g, '')
106
- .slice(0, 60);
107
-
108
- return slug.length > 0 ? slug : `task-${title.length}`;
103
+ return slugifyText(title, 'task');
109
104
  }
110
105
 
111
106
  /**
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Tavily registers us at connect time — nothing for an operator to set up. */
4
+ export const tavily = defineProvider({
5
+ id: 'tavily',
6
+ name: 'Tavily',
7
+ description: 'Web search and page content extraction, via Tavily\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://mcp.tavily.com/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });
@@ -0,0 +1,10 @@
1
+ import { defineProvider } from '#connectivity';
2
+
3
+ /** Todoist registers us at connect time — nothing for an operator to set up. */
4
+ export const todoist = defineProvider({
5
+ id: 'todoist',
6
+ name: 'Todoist',
7
+ description: 'Tasks, projects, sections, labels, and filters, via Todoist\'s official MCP server.',
8
+ connector: { kind: 'mcp', endpoint: 'https://ai.todoist.net/mcp' },
9
+ auth: { kind: 'oauth', registration: 'dynamic' },
10
+ });