@lanes-sh/link 0.6.11 → 0.7.1
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 +62 -45
- package/instructions/skills/lanes-link/SKILL.md +25 -0
- package/package.json +3 -2
- package/src/audit/conformance.ts +1 -1
- package/src/cli/argv.ts +14 -1
- package/src/cli/commands/connect/acquire.ts +100 -0
- package/src/cli/commands/connect/custom/index.ts +1 -1
- package/src/cli/commands/connect/declare.ts +19 -0
- package/src/cli/commands/connect/index.ts +54 -56
- package/src/cli/commands/connect/requirements.ts +23 -1
- package/src/cli/commands/connect/setup.ts +1 -1
- package/src/cli/commands/connect/variables.ts +220 -0
- package/src/cli/commands/knowledge/index.ts +22 -4
- package/src/cli/commands/knowledge/migrate.ts +61 -43
- package/src/cli/commands/operate/tools.ts +1 -1
- package/src/cli/commands/owner/entities.ts +244 -0
- package/src/cli/commands/owner/shared.ts +14 -0
- package/src/cli/commands/owner/vault.ts +2 -2
- package/src/cli/commands/owner.ts +13 -3
- package/src/cli/commands/setup.ts +12 -0
- package/src/cli/config-edit.ts +16 -6
- package/src/cli/config-repair.ts +22 -3
- package/src/cli/dispatch-owner.ts +30 -1
- package/src/cli/main.ts +2 -1
- package/src/cli/runtime/open.ts +14 -7
- package/src/cli/runtime/registry.ts +6 -1
- package/src/cli/selection.ts +11 -0
- package/src/cli/usage.ts +13 -2
- package/src/connectivity/auth/strategy/index.ts +1 -1
- package/src/connectivity/capability.ts +1 -1
- package/src/connectivity/context.ts +1 -1
- package/src/connectivity/index.ts +1 -1
- package/src/connectivity/mail/attachments.ts +42 -73
- package/src/connectivity/mail/content-type.ts +71 -0
- package/src/connectivity/mail/index.ts +1 -1
- package/src/connectivity/manifest/provider.ts +50 -0
- package/src/connectivity/manifest/variables.ts +173 -0
- package/src/connectivity/transports/factory.ts +74 -1
- package/src/connectivity/transports/http/index.ts +8 -2
- package/src/connectivity/transports/http/keys.ts +83 -0
- package/src/connectivity/transports/imap/attachment.ts +53 -10
- package/src/connectivity/transports/imap/capabilities.ts +28 -0
- package/src/connectivity/transports/imap/download.ts +111 -0
- package/src/connectivity/transports/imap/index.ts +13 -0
- package/src/connectivity/transports/imap/operations.ts +1 -0
- package/src/deployments/adapters/s3.ts +1 -1
- package/src/deployments/gcp/provision.ts +1 -1
- package/src/deployments/knowledge.ts +44 -18
- package/src/deployments/prepare.ts +1 -1
- package/src/deployments/serving.ts +1 -1
- package/src/dispatch/dispatch.ts +17 -10
- package/src/dispatch/staging.ts +76 -1
- package/src/profile/knowledge.ts +19 -11
- package/src/profile/schema.ts +2 -2
- package/src/providers/README.md +189 -0
- package/src/providers/airtable/index.ts +10 -0
- package/src/providers/algolia/index.ts +10 -0
- package/src/providers/amplitude/index.ts +10 -0
- package/src/providers/apify/index.ts +10 -0
- package/src/providers/asana/index.ts +10 -0
- package/src/providers/assets/provider.ts +11 -4
- package/src/providers/atlassian/index.ts +19 -0
- package/src/providers/attio/index.ts +10 -0
- package/src/providers/betterstack/index.ts +10 -0
- package/src/providers/box/index.ts +64 -0
- package/src/providers/brightdata/index.ts +10 -0
- package/src/providers/buildkite/index.ts +10 -0
- package/src/providers/bunq/index.ts +2 -2
- package/src/providers/calendly/index.ts +10 -0
- package/src/providers/canva/index.ts +10 -0
- package/src/providers/circleci/index.ts +10 -0
- package/src/providers/clickup/index.ts +10 -0
- package/src/providers/close/index.ts +10 -0
- package/src/providers/cloudflare_bindings/index.ts +14 -0
- package/src/providers/cloudflare_observability/index.ts +10 -0
- package/src/providers/contentful/index.ts +10 -0
- package/src/providers/custom/load.ts +1 -1
- package/src/providers/datadog/index.ts +10 -0
- package/src/providers/discord/index.ts +1 -1
- package/src/providers/discord/specs/discord.v10.json +0 -88
- package/src/providers/discord/specs/vendor.ts +1 -1
- package/src/providers/dropbox/index.ts +10 -0
- package/src/providers/entities/catalogue.ts +366 -0
- package/src/providers/entities/find.ts +279 -0
- package/src/providers/entities/provider.ts +338 -0
- package/src/providers/entities/render.ts +142 -0
- package/src/providers/entities/store.ts +276 -0
- package/src/providers/entities/writes.ts +279 -0
- package/src/providers/example/provider.ts +1 -1
- package/src/providers/expensify/index.ts +10 -0
- package/src/providers/fastmail/calendar/index.ts +21 -0
- package/src/providers/fastmail/contacts/index.ts +17 -0
- package/src/providers/fastmail/index.ts +4 -0
- package/src/providers/fastmail/mail/index.ts +38 -0
- package/src/providers/fastmail/shared/setup.ts +52 -0
- package/src/providers/figma/index.ts +10 -0
- package/src/providers/fireflies/index.ts +10 -0
- package/src/providers/flagsmith/index.ts +10 -0
- package/src/providers/gamma/index.ts +10 -0
- package/src/providers/github/index.ts +2 -2
- package/src/providers/google/gmail/attachment.ts +16 -1
- package/src/providers/google/gmail-imap/index.ts +7 -2
- package/src/providers/google/shared/oauth.ts +1 -1
- package/src/providers/google/shared/service-account.ts +1 -1
- package/src/providers/google/shared/setup.ts +1 -1
- package/src/providers/google/specs/calendar.v3.json +0 -32
- package/src/providers/google/specs/docs.v1.json +0 -76
- package/src/providers/google/specs/drive.v3.json +0 -32
- package/src/providers/google/specs/gmail.v1.json +0 -76
- package/src/providers/google/specs/people.v1.json +0 -77
- package/src/providers/google/specs/sheets.v4.json +0 -76
- package/src/providers/google/specs/tasks.v1.json +0 -76
- package/src/providers/grafana/index.ts +10 -0
- package/src/providers/heroku/index.ts +10 -0
- package/src/providers/hubspot/index.ts +75 -0
- package/src/providers/hygraph/index.ts +10 -0
- package/src/providers/icloud/mail/index.ts +5 -0
- package/src/providers/icloud/shared/setup.ts +1 -1
- package/src/providers/index.ts +255 -0
- package/src/providers/insightly/index.ts +10 -0
- package/src/providers/jam/index.ts +10 -0
- package/src/providers/klaviyo/index.ts +10 -0
- package/src/providers/mailbox/index.ts +87 -0
- package/src/providers/memory/provider.ts +3 -8
- package/src/providers/mercury/index.ts +10 -0
- package/src/providers/microsoft/calendar/index.ts +49 -0
- package/src/providers/microsoft/contacts/index.ts +50 -0
- package/src/providers/microsoft/drive/index.ts +62 -0
- package/src/providers/microsoft/index.ts +12 -0
- package/src/providers/microsoft/mail/index.ts +76 -0
- package/src/providers/microsoft/shared/oauth.ts +58 -0
- package/src/providers/microsoft/shared/scopes.ts +56 -0
- package/src/providers/microsoft/shared/setup.ts +45 -0
- package/src/providers/microsoft/specs/microsoft-todo.v1.json +755 -0
- package/src/providers/microsoft/specs/onedrive.v1.json +993 -0
- package/src/providers/microsoft/specs/outlook-calendar.v1.json +1743 -0
- package/src/providers/microsoft/specs/outlook-contacts.v1.json +430 -0
- package/src/providers/microsoft/specs/outlook-mail.v1.json +901 -0
- package/src/providers/microsoft/specs/vendor.ts +232 -0
- package/src/providers/microsoft/todo/index.ts +54 -0
- package/src/providers/miro/index.ts +10 -0
- package/src/providers/mixpanel/index.ts +10 -0
- package/src/providers/monday/index.ts +10 -0
- package/src/providers/mux/index.ts +10 -0
- package/src/providers/navan/index.ts +10 -0
- package/src/providers/neon/index.ts +10 -0
- package/src/providers/netlify/index.ts +10 -0
- package/src/providers/nextcloud/calendar/index.ts +28 -0
- package/src/providers/nextcloud/contacts/index.ts +18 -0
- package/src/providers/nextcloud/index.ts +3 -0
- package/src/providers/nextcloud/shared/setup.ts +59 -0
- package/src/providers/owner.ts +15 -3
- package/src/providers/paddle/index.ts +10 -0
- package/src/providers/paypal/index.ts +10 -0
- package/src/providers/posthog/index.ts +10 -0
- package/src/providers/prisma/index.ts +10 -0
- package/src/providers/ramp/index.ts +10 -0
- package/src/providers/recurly/index.ts +10 -0
- package/src/providers/reddit/index.ts +1 -1
- package/src/providers/remote/index.ts +10 -0
- package/src/providers/render/index.ts +35 -0
- package/src/providers/replicate/index.ts +10 -0
- package/src/providers/resend/index.ts +10 -0
- package/src/providers/riverside/index.ts +10 -0
- package/src/providers/rootly/index.ts +10 -0
- package/src/providers/rudderstack/index.ts +10 -0
- package/src/providers/salesloft/index.ts +10 -0
- package/src/providers/sanity/index.ts +10 -0
- package/src/providers/scopes.ts +2 -0
- package/src/providers/sentry/index.ts +10 -0
- package/src/providers/setup/plan.ts +11 -0
- package/src/providers/setup/provider.ts +12 -0
- package/src/providers/shared/openapi.ts +70 -0
- package/src/providers/shared/slug.ts +28 -0
- package/src/providers/shared/vendor-operations.ts +43 -1
- package/src/providers/shared/vendor-spec.ts +98 -17
- package/src/providers/shortcut/index.ts +10 -0
- package/src/providers/slack/index.ts +2 -2
- package/src/providers/square/index.ts +10 -0
- package/src/providers/storyblok/index.ts +10 -0
- package/src/providers/stripe/index.ts +10 -0
- package/src/providers/supabase/index.ts +10 -0
- package/src/providers/tasks/store.ts +2 -7
- package/src/providers/tavily/index.ts +10 -0
- package/src/providers/todoist/index.ts +10 -0
- package/src/providers/untested.ts +105 -0
- package/src/providers/vault/provider.ts +1 -1
- package/src/providers/vercel/index.ts +10 -0
- package/src/providers/vimeo/index.ts +10 -0
- package/src/providers/webflow/index.ts +10 -0
- package/src/providers/whimsical/index.ts +10 -0
- package/src/providers/wix/index.ts +10 -0
- package/src/providers/workable/index.ts +10 -0
- package/src/providers/yahoo_mail/index.ts +73 -0
- package/src/providers/zapier/index.ts +10 -0
- package/src/providers/zoho_mail/index.ts +79 -0
- package/src/secrets/document.ts +1 -1
- package/src/server/attachments.ts +129 -5
- package/src/server/index.ts +3 -3
- package/src/server/mcp/instructions.ts +88 -18
- package/src/server/stdio.ts +1 -1
package/src/cli/usage.ts
CHANGED
|
@@ -100,6 +100,17 @@ ${style.bold('Your own context')}
|
|
|
100
100
|
${PROGRAM} skills add <name> [--file f] document on stdin
|
|
101
101
|
${PROGRAM} skills remove <name>
|
|
102
102
|
|
|
103
|
+
${PROGRAM} entities who and what everyone else is
|
|
104
|
+
${PROGRAM} entities find [query] [--type t] [--tag t] [--attr kind[=value]]
|
|
105
|
+
[--related predicate=id] every match, never a choice
|
|
106
|
+
${PROGRAM} entities get <id> with its relationships, both ways
|
|
107
|
+
${PROGRAM} entities write <name> [--type t] [--name id] [--alias a]
|
|
108
|
+
[--attr kind=value] [--related predicate=id]
|
|
109
|
+
notes on stdin; a flag you omit keeps what is stored
|
|
110
|
+
${PROGRAM} entities link <from> <predicate>=<to> one edge, written on <from> only
|
|
111
|
+
${PROGRAM} entities forget <id>
|
|
112
|
+
${PROGRAM} entities reindex rebuild the lookup index from the files
|
|
113
|
+
|
|
103
114
|
${PROGRAM} knowledge show where memory and skills are kept, and how many
|
|
104
115
|
${PROGRAM} knowledge use github --repo <owner/name> [--branch b] [--path p]
|
|
105
116
|
keep both in a private repository, over the GitHub API
|
|
@@ -152,8 +163,8 @@ ${style.bold('Naming what a command acts on')}
|
|
|
152
163
|
command that names neither refuses and lists what exists.
|
|
153
164
|
|
|
154
165
|
${style.bold('Other flags')}
|
|
155
|
-
--connection <id> which memory/tasks/assets/skills/vault
|
|
156
|
-
a profile has several of one kind
|
|
166
|
+
--connection <id> which memory/tasks/assets/skills/vault/entities
|
|
167
|
+
connection, where a profile has several of one kind
|
|
157
168
|
--yes skip the confirmation a destructive command would ask for
|
|
158
169
|
--json machine-readable output, where a command offers it
|
|
159
170
|
--non-interactive never prompt: connect refuses with what to store,
|
|
@@ -133,6 +133,6 @@ export function strategyContextFrom(input: {
|
|
|
133
133
|
export function refuseStrategy(strategy: string): never {
|
|
134
134
|
throw new Error(
|
|
135
135
|
`Auth strategy "${strategy}" is not registered. ` +
|
|
136
|
-
`Strategies are the only place per-vendor code belongs; see docs/
|
|
136
|
+
`Strategies are the only place per-vendor code belongs; see https://lanes.sh/docs/link/creating-a-provider.`,
|
|
137
137
|
);
|
|
138
138
|
}
|
|
@@ -8,7 +8,7 @@ import type { ProviderContext } from './context.ts';
|
|
|
8
8
|
* Do not make everything a tool. Use resources for read-oriented document or
|
|
9
9
|
* structured context, tools for actions and parameterised queries, prompts for
|
|
10
10
|
* reusable procedures. Decide per capability and record the reasoning in
|
|
11
|
-
* `docs/
|
|
11
|
+
* `https://lanes.sh/docs/link/capabilities` — ADR-006.
|
|
12
12
|
*/
|
|
13
13
|
export type Capability = ToolCapability | ResourceCapability | PromptCapability;
|
|
14
14
|
|
|
@@ -49,7 +49,7 @@ export interface ConnectionInfo {
|
|
|
49
49
|
* provider can do anything that credential permits. There is no provider
|
|
50
50
|
* sandbox in M1. Installing a third-party provider is equivalent to running
|
|
51
51
|
* arbitrary code with access to that account, and
|
|
52
|
-
* `docs/
|
|
52
|
+
* `https://lanes.sh/docs/link/creating-a-provider` says so plainly.
|
|
53
53
|
*/
|
|
54
54
|
export interface ProviderContext {
|
|
55
55
|
readonly connection: ConnectionInfo;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* This barrel is also the **provider authoring surface**: a provider must be
|
|
15
15
|
* writable without reading the rest of the codebase. If you need something not
|
|
16
16
|
* exported here, that is a bug in this file rather than a reason to reach into
|
|
17
|
-
* another component. See `docs/
|
|
17
|
+
* another component. See `https://lanes.sh/docs/link/creating-a-provider`.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
export type { ScopedStore, Logger, ConnectionInfo, ProviderContext } from './context.ts';
|
|
@@ -5,6 +5,7 @@ import type { BlobStore } from '#stores/blobs';
|
|
|
5
5
|
import type { ResolvedAttachment } from './message.ts';
|
|
6
6
|
import { getStaged } from './staging.ts';
|
|
7
7
|
import { fetchFromUrl, type AddressLookup } from './url.ts';
|
|
8
|
+
import { basename, guessContentType } from './content-type.ts';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Turning a named attachment into bytes.
|
|
@@ -21,11 +22,12 @@ import { fetchFromUrl, type AddressLookup } from './url.ts';
|
|
|
21
22
|
* url fetched over HTTPS, with the checks in `url.ts`
|
|
22
23
|
* handle bytes staged earlier through the upload route
|
|
23
24
|
* message_id an attachment already sitting in the mailbox being used
|
|
25
|
+
* uid the same, addressed the way the mailbox itself addresses it
|
|
24
26
|
* data base64 inline — the escape hatch, not the path
|
|
25
27
|
*
|
|
26
28
|
* `path` is deliberately unrestricted: no allowlist, no confinement to a root.
|
|
27
29
|
* The endpoint already holds its owner's credentials, so the filesystem is
|
|
28
|
-
* treated the same way, and `docs/
|
|
30
|
+
* treated the same way, and `https://lanes.sh/docs/link/creating-a-provider`'s note that provider
|
|
29
31
|
* code is trusted code applies here too. What makes that defensible is the audit
|
|
30
32
|
* trail rather than a sandbox — every resolved attachment carries its origin and
|
|
31
33
|
* a SHA-256, and the manifest's `redact` block keeps both, so "was this file ever
|
|
@@ -34,7 +36,7 @@ import { fetchFromUrl, type AddressLookup } from './url.ts';
|
|
|
34
36
|
*/
|
|
35
37
|
|
|
36
38
|
/** The source keys, in the order they are reported when a caller supplies two. */
|
|
37
|
-
const SOURCE_KEYS = ['path', 'url', 'handle', 'message_id', 'data'] as const;
|
|
39
|
+
const SOURCE_KEYS = ['path', 'url', 'handle', 'message_id', 'uid', 'data'] as const;
|
|
38
40
|
|
|
39
41
|
export const attachmentRefSchema = z
|
|
40
42
|
.strictObject({
|
|
@@ -48,10 +50,22 @@ export const attachmentRefSchema = z
|
|
|
48
50
|
.string()
|
|
49
51
|
.optional()
|
|
50
52
|
.describe('Re-attach an attachment already on a message in this mailbox.'),
|
|
53
|
+
uid: z
|
|
54
|
+
.number()
|
|
55
|
+
.int()
|
|
56
|
+
.positive()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe(
|
|
59
|
+
'Re-attach from a message named by its mailbox UID, as search_messages and get_message report it. Pair with mailbox. Prefer this to message_id on a mailbox that has both: it addresses the message directly instead of searching every folder for a header.',
|
|
60
|
+
),
|
|
61
|
+
mailbox: z
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('Which mailbox the uid belongs to. Defaults to INBOX.'),
|
|
51
65
|
attachment_id: z
|
|
52
66
|
.string()
|
|
53
67
|
.optional()
|
|
54
|
-
.describe('Which attachment on that message. Required with message_id where the provider ids them.'),
|
|
68
|
+
.describe('Which attachment on that message. Required with message_id or uid where the provider ids them.'),
|
|
55
69
|
data: z
|
|
56
70
|
.string()
|
|
57
71
|
.optional()
|
|
@@ -61,7 +75,7 @@ export const attachmentRefSchema = z
|
|
|
61
75
|
filename: z.string().optional().describe('Overrides the name derived from the source.'),
|
|
62
76
|
content_type: z.string().optional().describe('Overrides the type derived from the source.'),
|
|
63
77
|
})
|
|
64
|
-
.describe('One attachment, named by exactly one of path, url, handle, message_id, or data.');
|
|
78
|
+
.describe('One attachment, named by exactly one of path, url, handle, message_id, uid, or data.');
|
|
65
79
|
|
|
66
80
|
export type AttachmentRef = z.infer<typeof attachmentRefSchema>;
|
|
67
81
|
|
|
@@ -82,9 +96,23 @@ export const attachmentsJsonSchema = ((): Record<string, unknown> => {
|
|
|
82
96
|
return schema;
|
|
83
97
|
})();
|
|
84
98
|
|
|
85
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Pulls bytes for `message_id` or `uid` out of the mailbox the caller is already in.
|
|
101
|
+
*
|
|
102
|
+
* Two ways to name the message, because the two protocols name it differently
|
|
103
|
+
* and only one of them can do both. A `uid` is what IMAP itself uses and what
|
|
104
|
+
* `search_messages` and `get_message` already report, so it addresses a message
|
|
105
|
+
* directly; a `messageId` has to be *searched* for, folder by folder, and a
|
|
106
|
+
* server whose `HEADER MESSAGE-ID` matching is unreliable fails that search on a
|
|
107
|
+
* message it is holding. Gmail's REST API has no uid and takes its own id.
|
|
108
|
+
*
|
|
109
|
+
* Exactly one arrives set. An implementation that cannot serve the one it is
|
|
110
|
+
* given should say so naming the other, rather than returning nothing.
|
|
111
|
+
*/
|
|
86
112
|
export type MailboxAttachmentSource = (reference: {
|
|
87
|
-
readonly messageId: string;
|
|
113
|
+
readonly messageId: string | undefined;
|
|
114
|
+
readonly mailbox: string | undefined;
|
|
115
|
+
readonly uid: number | undefined;
|
|
88
116
|
readonly attachmentId: string | undefined;
|
|
89
117
|
}) => Promise<{
|
|
90
118
|
readonly bytes: Uint8Array;
|
|
@@ -213,21 +241,25 @@ async function bytesFor(
|
|
|
213
241
|
case 'handle':
|
|
214
242
|
return await fromHandle(ref.handle!, where, options.storage);
|
|
215
243
|
|
|
216
|
-
case 'message_id':
|
|
244
|
+
case 'message_id':
|
|
245
|
+
case 'uid': {
|
|
217
246
|
if (!options.mailbox) {
|
|
218
247
|
throw new Error(
|
|
219
|
-
`${where} uses
|
|
248
|
+
`${where} uses ${source}, which only a mail connection can resolve — this one holds no mailbox. ` +
|
|
249
|
+
`Name the file with path, url, or handle instead, or ask the mail connection itself to send it somewhere.`,
|
|
220
250
|
);
|
|
221
251
|
}
|
|
222
252
|
const found = await options.mailbox({
|
|
223
|
-
messageId: ref.message_id
|
|
253
|
+
messageId: ref.message_id,
|
|
254
|
+
mailbox: ref.mailbox,
|
|
255
|
+
uid: ref.uid,
|
|
224
256
|
attachmentId: ref.attachment_id,
|
|
225
257
|
});
|
|
226
258
|
return {
|
|
227
259
|
bytes: found.bytes,
|
|
228
260
|
filename: found.filename,
|
|
229
261
|
contentType: found.contentType,
|
|
230
|
-
origin: `mailbox:${ref.message_id!}`,
|
|
262
|
+
origin: source === 'uid' ? `mailbox:${ref.mailbox ?? 'INBOX'}:${ref.uid!}` : `mailbox:${ref.message_id!}`,
|
|
231
263
|
};
|
|
232
264
|
}
|
|
233
265
|
|
|
@@ -307,66 +339,3 @@ function decodeBase64(value: string, where: string): Uint8Array {
|
|
|
307
339
|
throw new Error(`${where}: data is not valid base64.`);
|
|
308
340
|
}
|
|
309
341
|
}
|
|
310
|
-
|
|
311
|
-
function basename(path: string): string | null {
|
|
312
|
-
const name = path.split(/[/\\]/).pop();
|
|
313
|
-
return name ? decodeURIComponent(name) : null;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Type from the filename, since three of the five sources do not report one.
|
|
318
|
-
*
|
|
319
|
-
* A table rather than `Bun.file().type`, which would resolve this in one line:
|
|
320
|
-
* Bun-specific APIs are confined to two named files, and a mail composer is not
|
|
321
|
-
* one of them. Deliberately short — what an attachment actually is, not a mime
|
|
322
|
-
* database. Anything unlisted becomes `application/octet-stream`, which every
|
|
323
|
-
* mail client handles as "a file", so the failure mode is a generic icon rather
|
|
324
|
-
* than a broken attachment.
|
|
325
|
-
*/
|
|
326
|
-
const CONTENT_TYPES: Readonly<Record<string, string>> = {
|
|
327
|
-
pdf: 'application/pdf',
|
|
328
|
-
txt: 'text/plain',
|
|
329
|
-
md: 'text/markdown',
|
|
330
|
-
csv: 'text/csv',
|
|
331
|
-
html: 'text/html',
|
|
332
|
-
json: 'application/json',
|
|
333
|
-
xml: 'application/xml',
|
|
334
|
-
ics: 'text/calendar',
|
|
335
|
-
vcf: 'text/vcard',
|
|
336
|
-
png: 'image/png',
|
|
337
|
-
jpg: 'image/jpeg',
|
|
338
|
-
jpeg: 'image/jpeg',
|
|
339
|
-
gif: 'image/gif',
|
|
340
|
-
webp: 'image/webp',
|
|
341
|
-
svg: 'image/svg+xml',
|
|
342
|
-
heic: 'image/heic',
|
|
343
|
-
tiff: 'image/tiff',
|
|
344
|
-
zip: 'application/zip',
|
|
345
|
-
gz: 'application/gzip',
|
|
346
|
-
mp3: 'audio/mpeg',
|
|
347
|
-
m4a: 'audio/mp4',
|
|
348
|
-
wav: 'audio/wav',
|
|
349
|
-
mp4: 'video/mp4',
|
|
350
|
-
mov: 'video/quicktime',
|
|
351
|
-
doc: 'application/msword',
|
|
352
|
-
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
353
|
-
xls: 'application/vnd.ms-excel',
|
|
354
|
-
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
355
|
-
ppt: 'application/vnd.ms-powerpoint',
|
|
356
|
-
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
357
|
-
// `.pages`, `.numbers` and `.key` are absent on purpose. Their registered
|
|
358
|
-
// media types carry a vendor name, and `architecture.test.ts` refuses one
|
|
359
|
-
// anywhere a request passes through — correctly, even though an IANA type is
|
|
360
|
-
// not the vendor *knowledge* that rule is aimed at. They fall through to
|
|
361
|
-
// octet-stream, which mail clients show as a file and the recipient's system
|
|
362
|
-
// opens by extension anyway. Adding them back will fail the suite.
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Exported because `assets` needs exactly this and the table above must not be
|
|
367
|
-
* copied — its `.pages`/`.numbers` note is a rule the copy would not carry.
|
|
368
|
-
*/
|
|
369
|
-
export function guessContentType(filename: string): string {
|
|
370
|
-
const extension = filename.split('.').pop()?.toLowerCase();
|
|
371
|
-
return (extension ? CONTENT_TYPES[extension] : undefined) ?? 'application/octet-stream';
|
|
372
|
-
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a file is, from its name.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `attachments.ts` when that file passed the size budget, and it is
|
|
5
|
+
* the right seam rather than a convenient one: resolving a *source* into bytes
|
|
6
|
+
* and deciding what those bytes *are* are separate questions, and only the
|
|
7
|
+
* second one is a lookup table nobody needs to read.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function basename(path: string): string | null {
|
|
11
|
+
const name = path.split(/[/\\]/).pop();
|
|
12
|
+
return name ? decodeURIComponent(name) : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type from the filename, since three of the five sources do not report one.
|
|
17
|
+
*
|
|
18
|
+
* A table rather than `Bun.file().type`, which would resolve this in one line:
|
|
19
|
+
* Bun-specific APIs are confined to two named files, and a mail composer is not
|
|
20
|
+
* one of them. Deliberately short — what an attachment actually is, not a mime
|
|
21
|
+
* database. Anything unlisted becomes `application/octet-stream`, which every
|
|
22
|
+
* mail client handles as "a file", so the failure mode is a generic icon rather
|
|
23
|
+
* than a broken attachment.
|
|
24
|
+
*/
|
|
25
|
+
const CONTENT_TYPES: Readonly<Record<string, string>> = {
|
|
26
|
+
pdf: 'application/pdf',
|
|
27
|
+
txt: 'text/plain',
|
|
28
|
+
md: 'text/markdown',
|
|
29
|
+
csv: 'text/csv',
|
|
30
|
+
html: 'text/html',
|
|
31
|
+
json: 'application/json',
|
|
32
|
+
xml: 'application/xml',
|
|
33
|
+
ics: 'text/calendar',
|
|
34
|
+
vcf: 'text/vcard',
|
|
35
|
+
png: 'image/png',
|
|
36
|
+
jpg: 'image/jpeg',
|
|
37
|
+
jpeg: 'image/jpeg',
|
|
38
|
+
gif: 'image/gif',
|
|
39
|
+
webp: 'image/webp',
|
|
40
|
+
svg: 'image/svg+xml',
|
|
41
|
+
heic: 'image/heic',
|
|
42
|
+
tiff: 'image/tiff',
|
|
43
|
+
zip: 'application/zip',
|
|
44
|
+
gz: 'application/gzip',
|
|
45
|
+
mp3: 'audio/mpeg',
|
|
46
|
+
m4a: 'audio/mp4',
|
|
47
|
+
wav: 'audio/wav',
|
|
48
|
+
mp4: 'video/mp4',
|
|
49
|
+
mov: 'video/quicktime',
|
|
50
|
+
doc: 'application/msword',
|
|
51
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
52
|
+
xls: 'application/vnd.ms-excel',
|
|
53
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
54
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
55
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
56
|
+
// `.pages`, `.numbers` and `.key` are absent on purpose. Their registered
|
|
57
|
+
// media types carry a vendor name, and `architecture.test.ts` refuses one
|
|
58
|
+
// anywhere a request passes through — correctly, even though an IANA type is
|
|
59
|
+
// not the vendor *knowledge* that rule is aimed at. They fall through to
|
|
60
|
+
// octet-stream, which mail clients show as a file and the recipient's system
|
|
61
|
+
// opens by extension anyway. Adding them back will fail the suite.
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Exported because `assets` needs exactly this and the table above must not be
|
|
66
|
+
* copied — its `.pages`/`.numbers` note is a rule the copy would not carry.
|
|
67
|
+
*/
|
|
68
|
+
export function guessContentType(filename: string): string {
|
|
69
|
+
const extension = filename.split('.').pop()?.toLowerCase();
|
|
70
|
+
return (extension ? CONTENT_TYPES[extension] : undefined) ?? 'application/octet-stream';
|
|
71
|
+
}
|
|
@@ -21,10 +21,10 @@ export { receiptFor } from './message.ts';
|
|
|
21
21
|
export { composeMime } from './compose.ts';
|
|
22
22
|
|
|
23
23
|
export type { AttachmentRef, MailboxAttachmentSource, ResolveOptions } from './attachments.ts';
|
|
24
|
+
export { guessContentType } from './content-type.ts';
|
|
24
25
|
export {
|
|
25
26
|
attachmentRefSchema,
|
|
26
27
|
attachmentsJsonSchema,
|
|
27
|
-
guessContentType,
|
|
28
28
|
resolveAttachments,
|
|
29
29
|
} from './attachments.ts';
|
|
30
30
|
|
|
@@ -5,6 +5,7 @@ import { connectorSchema } from './connector.ts';
|
|
|
5
5
|
import { identitySchema } from './identity.ts';
|
|
6
6
|
import { identifier } from './primitives.ts';
|
|
7
7
|
import { setupSchema } from './setup.ts';
|
|
8
|
+
import { connectionVariableSchema, placeholdersInConnector } from './variables.ts';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* A provider manifest.
|
|
@@ -60,6 +61,14 @@ export const providerManifestSchema = z.object({
|
|
|
60
61
|
* not silently discarded.
|
|
61
62
|
*/
|
|
62
63
|
hints: z.record(z.string(), z.string()).optional(),
|
|
64
|
+
/**
|
|
65
|
+
* What this connection has to say about *where* the service is.
|
|
66
|
+
*
|
|
67
|
+
* Absent for every provider whose address is the same for everybody, which is
|
|
68
|
+
* almost all of them. Present for a multi-tenant host that carries the tenant,
|
|
69
|
+
* or anything self-hosted — see `./variables.ts`.
|
|
70
|
+
*/
|
|
71
|
+
variables: z.array(connectionVariableSchema).default([]),
|
|
63
72
|
});
|
|
64
73
|
|
|
65
74
|
export type ProviderManifest = z.infer<typeof providerManifestSchema>;
|
|
@@ -69,6 +78,9 @@ export type ProviderManifest = z.infer<typeof providerManifestSchema>;
|
|
|
69
78
|
*
|
|
70
79
|
* The order is read: `#server/mcp`'s instructions emit one paragraph per
|
|
71
80
|
* reachable id in this sequence, so it is the order an agent meets them in.
|
|
81
|
+
* `entities` is appended rather than inserted alphabetically so that it lands
|
|
82
|
+
* beside `identity`: the two answer the same question about different people,
|
|
83
|
+
* and the instructions collapse them into one paragraph when both are reachable.
|
|
72
84
|
*/
|
|
73
85
|
export const RESERVED_PROVIDER_IDS: readonly string[] = [
|
|
74
86
|
'memory',
|
|
@@ -78,6 +90,7 @@ export const RESERVED_PROVIDER_IDS: readonly string[] = [
|
|
|
78
90
|
'vault',
|
|
79
91
|
'setup',
|
|
80
92
|
'identity',
|
|
93
|
+
'entities',
|
|
81
94
|
];
|
|
82
95
|
|
|
83
96
|
/**
|
|
@@ -104,6 +117,43 @@ export function defineProvider(input: unknown): ProviderManifest {
|
|
|
104
117
|
|
|
105
118
|
const manifest = parsed.data;
|
|
106
119
|
|
|
120
|
+
// R12 and R13. The two halves of a variable have to agree, and neither failure
|
|
121
|
+
// is visible without this: a placeholder nothing fills reaches the vendor
|
|
122
|
+
// verbatim, so the request goes to a host literally called `{site}.zendesk.com`
|
|
123
|
+
// and fails as DNS rather than as configuration. A variable nothing uses is the
|
|
124
|
+
// opposite and quieter — `connect` asks the operator a question, stores the
|
|
125
|
+
// answer, and nothing ever reads it.
|
|
126
|
+
{
|
|
127
|
+
// `fs` and `local` are the two that hold no address a caller supplies.
|
|
128
|
+
// `local` is our own code, and `fs.root` *is* substitutable — a vault path
|
|
129
|
+
// differs per person — so only `local` is refused here.
|
|
130
|
+
if (manifest.connector.kind === 'local' && manifest.variables.length > 0) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Provider "${manifest.id}": a local connector is our own code and has no address to fill in, so it cannot declare variables.`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const placeholders = placeholdersInConnector(
|
|
137
|
+
manifest.connector as unknown as Record<string, unknown>,
|
|
138
|
+
);
|
|
139
|
+
const declared = new Set(manifest.variables.map((variable) => variable.key));
|
|
140
|
+
|
|
141
|
+
const unfilled = placeholders.filter((name) => !declared.has(name));
|
|
142
|
+
if (unfilled.length > 0) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Provider "${manifest.id}": the connector names {${unfilled.join('}, {')}} and the manifest declares no variable for ${unfilled.length === 1 ? 'it' : 'them'}. Add one under "variables", or the address is sent to the vendor with the braces still in it.`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const unused = [...declared].filter((key) => !placeholders.includes(key));
|
|
149
|
+
if (unused.length > 0) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
`Provider "${manifest.id}": variable${unused.length === 1 ? '' : 's'} ${unused.join(', ')} ${unused.length === 1 ? 'appears' : 'appear'} in no address, so connect would ask for ${unused.length === 1 ? 'a value' : 'values'} nothing reads. Put {${unused[0]}} in the connector's address — or drop it.`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
}
|
|
156
|
+
|
|
107
157
|
if (manifest.auth.kind === 'oauth' && manifest.auth.registration === 'manual') {
|
|
108
158
|
if (!manifest.auth.app) {
|
|
109
159
|
throw new Error(
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { identifier } from './primitives.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where a service lives, when that is a property of the connection rather than
|
|
6
|
+
* of the provider.
|
|
7
|
+
*
|
|
8
|
+
* `base_url`, `endpoint` and `host` are one value per manifest, which is right
|
|
9
|
+
* for a vendor that runs one address for everybody — Gmail is
|
|
10
|
+
* `gmail.googleapis.com` for every account there has ever been. It is wrong for
|
|
11
|
+
* two large families:
|
|
12
|
+
*
|
|
13
|
+
* - **multi-tenant SaaS whose host carries the tenant** — Zendesk is
|
|
14
|
+
* `<subdomain>.zendesk.com`, Shopify `<shop>.myshopify.com`, Atlassian
|
|
15
|
+
* `api.atlassian.com/ex/jira/<cloudid>`
|
|
16
|
+
* - **anything self-hosted** — a Nextcloud, a Gitea, a Home Assistant, each at
|
|
17
|
+
* an address only its owner knows
|
|
18
|
+
*
|
|
19
|
+
* Both were reachable already, but only by hand-writing a YAML manifest per
|
|
20
|
+
* instance in `providers.d/`. What was impossible was a *built-in* for any of
|
|
21
|
+
* them, because a built-in cannot know the address. A declared variable is the
|
|
22
|
+
* missing half: the manifest says a placeholder is there and what fills it, and
|
|
23
|
+
* the connection supplies the value.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately not a credential. A hostname is not a secret and does not belong
|
|
26
|
+
* in the secret store; it lives in the connection's own `config`, which is what
|
|
27
|
+
* that field has always been for.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* What a value is allowed to be, and the reason this is not cosmetic.
|
|
32
|
+
*
|
|
33
|
+
* A variable is substituted into a URL, so an unconstrained value chooses the
|
|
34
|
+
* host the operator's credential is sent to: `acme.zendesk.com/../..@evil.test`
|
|
35
|
+
* in a `{site}` would leave a manifest that reads as Zendesk and authenticates
|
|
36
|
+
* to somebody else. This is the check that stops that, and it runs at
|
|
37
|
+
* *substitution* rather than only at the prompt — config is a file that can be
|
|
38
|
+
* edited by hand, and a deployed revision reads it without ever seeing a prompt.
|
|
39
|
+
*
|
|
40
|
+
* One DNS label or path segment: letters, digits, and the three separators that
|
|
41
|
+
* appear inside real subdomains and tenant ids. No dots by default, because a
|
|
42
|
+
* dot is how you leave the domain the manifest named; a provider whose value
|
|
43
|
+
* genuinely contains one says so with its own `pattern`.
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_VARIABLE_PATTERN = '^[A-Za-z0-9][A-Za-z0-9_-]*$';
|
|
46
|
+
|
|
47
|
+
export const connectionVariableSchema = z.object({
|
|
48
|
+
/** The name inside the braces. `{site}` is `key: 'site'`. */
|
|
49
|
+
key: identifier,
|
|
50
|
+
/** What to call it when asking, in the vendor's own word — "subdomain", not "variable". */
|
|
51
|
+
label: z.string().min(1),
|
|
52
|
+
/** One line: what it is and where the operator finds it. */
|
|
53
|
+
description: z.string().min(1),
|
|
54
|
+
/** A real-looking value, shown in the prompt and in the refusal. */
|
|
55
|
+
example: z.string().min(1),
|
|
56
|
+
/**
|
|
57
|
+
* Override the default only to *narrow* it or to admit a character the
|
|
58
|
+
* default excludes — a cloud id with dots, a path segment with slashes. A
|
|
59
|
+
* pattern that admits `/` or `@` is choosing to trust the value with the host,
|
|
60
|
+
* and should say why.
|
|
61
|
+
*/
|
|
62
|
+
pattern: z
|
|
63
|
+
.string()
|
|
64
|
+
.default(DEFAULT_VARIABLE_PATTERN)
|
|
65
|
+
// Anchored here rather than trusted to be anchored, and compiled here
|
|
66
|
+
// rather than at the first call.
|
|
67
|
+
//
|
|
68
|
+
// `test` matches a substring, so an unanchored pattern silently voids the
|
|
69
|
+
// guarantee this field exists for: `[a-z.]+` accepts `acme.evil.test/@x`,
|
|
70
|
+
// which is then substituted into the URL the operator's credential is sent
|
|
71
|
+
// to. Both built-ins anchor; a manifest in `providers.d/` is not obliged to
|
|
72
|
+
// read this file first, and a security control that depends on remembering
|
|
73
|
+
// is not one.
|
|
74
|
+
//
|
|
75
|
+
// Wrapped rather than refused, because what an author means by a pattern is
|
|
76
|
+
// always "the value looks like this" — nobody writes one intending a
|
|
77
|
+
// substring match. An invalid regex is refused, though, and refused at load
|
|
78
|
+
// rather than surfacing as a raw SyntaxError when a connector is built.
|
|
79
|
+
.transform((pattern) => (/^\^.*\$$/.test(pattern) ? pattern : `^(?:${pattern})$`))
|
|
80
|
+
.superRefine((pattern, context) => {
|
|
81
|
+
try {
|
|
82
|
+
new RegExp(pattern);
|
|
83
|
+
} catch (failure) {
|
|
84
|
+
context.addIssue({
|
|
85
|
+
code: 'custom',
|
|
86
|
+
message: `is not a usable regular expression: ${(failure as Error).message}`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export type ConnectionVariable = z.infer<typeof connectionVariableSchema>;
|
|
93
|
+
|
|
94
|
+
/** Every `{name}` in a string, in the order they appear, without duplicates. */
|
|
95
|
+
export function placeholdersIn(text: string): string[] {
|
|
96
|
+
return [...new Set([...text.matchAll(/\{([a-z][a-z0-9_]*)\}/g)].map((match) => match[1]!))];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Every placeholder anywhere in a connector, however deep.
|
|
101
|
+
*
|
|
102
|
+
* A field list was the first shape and it was wrong within one provider: the
|
|
103
|
+
* `imap` connector's submission host is `smtp.host`, a level down, so a generic
|
|
104
|
+
* mailbox declaring `{smtp_host}` was refused for using a variable that
|
|
105
|
+
* "appears in no address". Walking the object has no list to keep in step with
|
|
106
|
+
* a transport schema, and a `{name}` in a field where it means nothing is
|
|
107
|
+
* simply never substituted rather than silently wrong.
|
|
108
|
+
*/
|
|
109
|
+
export function placeholdersInConnector(connector: Record<string, unknown>): string[] {
|
|
110
|
+
const found = new Set<string>();
|
|
111
|
+
|
|
112
|
+
const walk = (node: unknown): void => {
|
|
113
|
+
if (typeof node === 'string') {
|
|
114
|
+
for (const name of placeholdersIn(node)) found.add(name);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (node === null || typeof node !== 'object') return;
|
|
118
|
+
for (const value of Object.values(node as Record<string, unknown>)) walk(value);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
walk(connector);
|
|
122
|
+
return [...found];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Fill a connector's address in, or say exactly what is wrong.
|
|
127
|
+
*
|
|
128
|
+
* Returns the connector untouched when it declares no variables, which is every
|
|
129
|
+
* provider but a handful — so this costs nothing where it is not used and cannot
|
|
130
|
+
* quietly reshape a connector that was already complete.
|
|
131
|
+
*/
|
|
132
|
+
export function applyVariables(
|
|
133
|
+
connector: Record<string, unknown>,
|
|
134
|
+
variables: readonly ConnectionVariable[],
|
|
135
|
+
values: Readonly<Record<string, unknown>>,
|
|
136
|
+
): Record<string, unknown> {
|
|
137
|
+
if (variables.length === 0) return connector;
|
|
138
|
+
|
|
139
|
+
const resolved = new Map<string, string>();
|
|
140
|
+
for (const variable of variables) {
|
|
141
|
+
const value = values[variable.key];
|
|
142
|
+
if (typeof value !== 'string' || value === '') {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`This connection does not say what "${variable.key}" is (${variable.label}). ` +
|
|
145
|
+
`Reconnect it, or add it under this connection's config: ${variable.key}: ${variable.example}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
if (!new RegExp(variable.pattern).test(value)) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`"${value}" is not a usable ${variable.label} — it must match ${variable.pattern}, ` +
|
|
151
|
+
`like ${variable.example}. The value is put into the address this connection calls, ` +
|
|
152
|
+
`so anything that could change the host is refused rather than sent.`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
resolved.set(variable.key, value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const substitute = (node: unknown): unknown => {
|
|
159
|
+
if (typeof node === 'string') {
|
|
160
|
+
return node.replace(/\{([a-z][a-z0-9_]*)\}/g, (whole, name: string) =>
|
|
161
|
+
resolved.get(name) ?? whole,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (node === null || typeof node !== 'object') return node;
|
|
165
|
+
if (Array.isArray(node)) return node.map(substitute);
|
|
166
|
+
|
|
167
|
+
return Object.fromEntries(
|
|
168
|
+
Object.entries(node as Record<string, unknown>).map(([key, value]) => [key, substitute(value)]),
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return substitute(connector) as Record<string, unknown>;
|
|
173
|
+
}
|