@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.
- package/LICENSE +202 -0
- package/README.md +144 -0
- package/bin/lanes +42 -0
- package/instructions/agents/lanes-link-scout.md +73 -0
- package/instructions/skills/lanes-link/SKILL.md +187 -0
- package/package.json +95 -0
- package/src/audit/chain.ts +278 -0
- package/src/audit/conformance.ts +374 -0
- package/src/audit/fanout.ts +97 -0
- package/src/audit/index.ts +218 -0
- package/src/audit/stdout.ts +60 -0
- package/src/auth/index.ts +220 -0
- package/src/auth/oauth/metadata.ts +75 -0
- package/src/auth/oauth/server.ts +385 -0
- package/src/auth/oauth/store.ts +215 -0
- package/src/auth/oidc.ts +206 -0
- package/src/auth/remote.ts +72 -0
- package/src/cli/argv.ts +94 -0
- package/src/cli/callback-page.ts +256 -0
- package/src/cli/commands/connect/accounts.ts +94 -0
- package/src/cli/commands/connect/authorise.ts +298 -0
- package/src/cli/commands/connect/client.ts +284 -0
- package/src/cli/commands/connect/index.ts +398 -0
- package/src/cli/commands/connect/outcome.ts +119 -0
- package/src/cli/commands/connect/requirements.ts +103 -0
- package/src/cli/commands/connect/scopes-gate.ts +146 -0
- package/src/cli/commands/connect/settle.ts +136 -0
- package/src/cli/commands/connect/setup.ts +276 -0
- package/src/cli/commands/mcp/assets.ts +189 -0
- package/src/cli/commands/mcp/harnesses.ts +143 -0
- package/src/cli/commands/mcp/list.ts +70 -0
- package/src/cli/commands/mcp/register.ts +199 -0
- package/src/cli/commands/mcp/stdio.ts +57 -0
- package/src/cli/commands/mcp.ts +22 -0
- package/src/cli/commands/operate/attach.ts +121 -0
- package/src/cli/commands/operate/audit.ts +119 -0
- package/src/cli/commands/operate/inspect.ts +355 -0
- package/src/cli/commands/operate/outputs.ts +206 -0
- package/src/cli/commands/operate/policy.ts +80 -0
- package/src/cli/commands/operate/serve.ts +55 -0
- package/src/cli/commands/operate/status.ts +133 -0
- package/src/cli/commands/operate/token.ts +70 -0
- package/src/cli/commands/operate.ts +27 -0
- package/src/cli/commands/owner/memory.ts +110 -0
- package/src/cli/commands/owner/shared.ts +125 -0
- package/src/cli/commands/owner/skills.ts +92 -0
- package/src/cli/commands/owner/vault.ts +140 -0
- package/src/cli/commands/owner.ts +46 -0
- package/src/cli/commands/profile/removal.ts +278 -0
- package/src/cli/commands/profile/remove.ts +315 -0
- package/src/cli/commands/profile.ts +156 -0
- package/src/cli/commands/secrets.ts +176 -0
- package/src/cli/commands/setup.ts +150 -0
- package/src/cli/commands/target.ts +310 -0
- package/src/cli/config-edit.ts +397 -0
- package/src/cli/endpoint-url.ts +77 -0
- package/src/cli/identity.ts +109 -0
- package/src/cli/lanes.ts +78 -0
- package/src/cli/main.ts +333 -0
- package/src/cli/oauth-error.ts +13 -0
- package/src/cli/oauth-exchange.ts +146 -0
- package/src/cli/oauth.ts +354 -0
- package/src/cli/output.ts +184 -0
- package/src/cli/prompt.ts +180 -0
- package/src/cli/publish.ts +185 -0
- package/src/cli/runtime/discovery.ts +123 -0
- package/src/cli/runtime/open.ts +342 -0
- package/src/cli/runtime/registry.ts +185 -0
- package/src/cli/runtime/select.ts +124 -0
- package/src/cli/runtime.ts +34 -0
- package/src/cli/scopes.ts +63 -0
- package/src/cli/usage.ts +111 -0
- package/src/cli/version.ts +25 -0
- package/src/connectivity/auth/README.md +36 -0
- package/src/connectivity/auth/api-key/index.ts +43 -0
- package/src/connectivity/auth/authorize.ts +49 -0
- package/src/connectivity/auth/basic/index.ts +68 -0
- package/src/connectivity/auth/bearer/index.ts +13 -0
- package/src/connectivity/auth/credential.ts +19 -0
- package/src/connectivity/auth/header/index.ts +18 -0
- package/src/connectivity/auth/index.ts +35 -0
- package/src/connectivity/auth/none/index.ts +12 -0
- package/src/connectivity/auth/oauth-authcode/broker.ts +261 -0
- package/src/connectivity/auth/oauth-authcode/index.ts +64 -0
- package/src/connectivity/auth/oauth-authcode/provider.ts +279 -0
- package/src/connectivity/auth/oauth-authcode/refresh.ts +118 -0
- package/src/connectivity/auth/resolve.ts +61 -0
- package/src/connectivity/auth/strategy/index.ts +14 -0
- package/src/connectivity/capability.ts +164 -0
- package/src/connectivity/connector.ts +175 -0
- package/src/connectivity/context.ts +77 -0
- package/src/connectivity/index.ts +94 -0
- package/src/connectivity/mail/attachments.ts +368 -0
- package/src/connectivity/mail/compose.ts +73 -0
- package/src/connectivity/mail/index.ts +40 -0
- package/src/connectivity/mail/message.ts +82 -0
- package/src/connectivity/mail/nodemailer.d.ts +83 -0
- package/src/connectivity/mail/staging.ts +143 -0
- package/src/connectivity/mail/url.ts +301 -0
- package/src/connectivity/manifest/auth.ts +135 -0
- package/src/connectivity/manifest/bundles.ts +23 -0
- package/src/connectivity/manifest/connector.ts +168 -0
- package/src/connectivity/manifest/credential-ref.ts +73 -0
- package/src/connectivity/manifest/identity.ts +43 -0
- package/src/connectivity/manifest/index.ts +46 -0
- package/src/connectivity/manifest/primitives.ts +20 -0
- package/src/connectivity/manifest/provider.ts +208 -0
- package/src/connectivity/manifest/requirements.ts +146 -0
- package/src/connectivity/manifest/setup.ts +55 -0
- package/src/connectivity/provider.ts +163 -0
- package/src/connectivity/transports/README.md +33 -0
- package/src/connectivity/transports/composite/index.ts +68 -0
- package/src/connectivity/transports/dav/calendar.ts +217 -0
- package/src/connectivity/transports/dav/capabilities.ts +151 -0
- package/src/connectivity/transports/dav/client.ts +226 -0
- package/src/connectivity/transports/dav/contacts.ts +75 -0
- package/src/connectivity/transports/dav/ical.ts +412 -0
- package/src/connectivity/transports/dav/index.ts +143 -0
- package/src/connectivity/transports/dav/operations.ts +43 -0
- package/src/connectivity/transports/dav/request.ts +161 -0
- package/src/connectivity/transports/dav/xml.ts +123 -0
- package/src/connectivity/transports/factory.ts +181 -0
- package/src/connectivity/transports/fs/capabilities.ts +88 -0
- package/src/connectivity/transports/fs/commands.ts +258 -0
- package/src/connectivity/transports/fs/index.ts +121 -0
- package/src/connectivity/transports/fs/operations.ts +45 -0
- package/src/connectivity/transports/fs/paths.ts +120 -0
- package/src/connectivity/transports/fs/result.ts +12 -0
- package/src/connectivity/transports/http/index.ts +255 -0
- package/src/connectivity/transports/imap/attachment.ts +166 -0
- package/src/connectivity/transports/imap/capabilities.ts +158 -0
- package/src/connectivity/transports/imap/client.ts +398 -0
- package/src/connectivity/transports/imap/commands.ts +385 -0
- package/src/connectivity/transports/imap/index.ts +152 -0
- package/src/connectivity/transports/imap/operations.ts +64 -0
- package/src/connectivity/transports/imap/parse.ts +130 -0
- package/src/connectivity/transports/imap/parser.ts +272 -0
- package/src/connectivity/transports/imap/result.ts +15 -0
- package/src/connectivity/transports/imap/send.ts +92 -0
- package/src/connectivity/transports/imap/socket.ts +111 -0
- package/src/connectivity/transports/imap/utf7.ts +136 -0
- package/src/connectivity/transports/index.ts +20 -0
- package/src/connectivity/transports/local/index.ts +173 -0
- package/src/connectivity/transports/mcp/index.ts +215 -0
- package/src/deployments/README.md +63 -0
- package/src/deployments/adapters/audit-blob.ts +203 -0
- package/src/deployments/adapters/filesystem.ts +184 -0
- package/src/deployments/adapters/gcp-secret-manager.ts +492 -0
- package/src/deployments/adapters/gcs.ts +191 -0
- package/src/deployments/adapters/otlp.ts +128 -0
- package/src/deployments/adapters/s3.ts +195 -0
- package/src/deployments/azure/README.md +21 -0
- package/src/deployments/bootstrap.ts +177 -0
- package/src/deployments/deploy.ts +290 -0
- package/src/deployments/driver.ts +157 -0
- package/src/deployments/drivers.ts +35 -0
- package/src/deployments/gcp/Dockerfile +70 -0
- package/src/deployments/gcp/cloudbuild.yaml +31 -0
- package/src/deployments/gcp/driver.ts +175 -0
- package/src/deployments/gcp/gcloud.ts +178 -0
- package/src/deployments/gcp/provision.ts +290 -0
- package/src/deployments/gcp/survey.ts +319 -0
- package/src/deployments/local/README.md +12 -0
- package/src/deployments/prepare.ts +257 -0
- package/src/deployments/steps.ts +137 -0
- package/src/deployments/target.ts +295 -0
- package/src/deployments/upload.ts +207 -0
- package/src/dispatch/context.ts +195 -0
- package/src/dispatch/dispatch.ts +350 -0
- package/src/dispatch/index.ts +32 -0
- package/src/dispatch/staging.ts +102 -0
- package/src/policy/index.ts +179 -0
- package/src/policy/limits.ts +77 -0
- package/src/profile/authorization.ts +81 -0
- package/src/profile/files.ts +71 -0
- package/src/profile/index.ts +76 -0
- package/src/profile/layout.ts +123 -0
- package/src/profile/load.ts +199 -0
- package/src/profile/primitives.ts +45 -0
- package/src/profile/schema.ts +347 -0
- package/src/profile/secret-detection.ts +162 -0
- package/src/profile/targets.ts +152 -0
- package/src/profile/workspace.ts +262 -0
- package/src/providers/custom/index.ts +21 -0
- package/src/providers/custom/load.ts +115 -0
- package/src/providers/custom/template.ts +156 -0
- package/src/providers/example/provider.ts +207 -0
- package/src/providers/google/calendar/index.ts +66 -0
- package/src/providers/google/calendar/redact.ts +40 -0
- package/src/providers/google/contacts/index.ts +50 -0
- package/src/providers/google/contacts/redact.ts +21 -0
- package/src/providers/google/docs/index.ts +45 -0
- package/src/providers/google/drive/hints.ts +28 -0
- package/src/providers/google/drive/index.ts +34 -0
- package/src/providers/google/drive/redact.ts +39 -0
- package/src/providers/google/drive-mcp/index.ts +21 -0
- package/src/providers/google/gmail/api.ts +42 -0
- package/src/providers/google/gmail/attachment.ts +142 -0
- package/src/providers/google/gmail/hints.ts +55 -0
- package/src/providers/google/gmail/index.ts +112 -0
- package/src/providers/google/gmail/redact.ts +56 -0
- package/src/providers/google/gmail/send.ts +365 -0
- package/src/providers/google/gmail-mcp/index.ts +35 -0
- package/src/providers/google/index.ts +10 -0
- package/src/providers/google/shared/oauth.ts +122 -0
- package/src/providers/google/shared/scopes.ts +99 -0
- package/src/providers/google/shared/setup.ts +80 -0
- package/src/providers/google/sheets/hints.ts +45 -0
- package/src/providers/google/sheets/index.ts +70 -0
- package/src/providers/google/sheets/redact.ts +45 -0
- package/src/providers/google/specs/calendar.v3.json +1829 -0
- package/src/providers/google/specs/docs.v1.json +381 -0
- package/src/providers/google/specs/drive.v3.json +2208 -0
- package/src/providers/google/specs/gmail.v1.json +2578 -0
- package/src/providers/google/specs/people.v1.json +506 -0
- package/src/providers/google/specs/sheets.v4.json +1269 -0
- package/src/providers/google/specs/tasks.v1.json +840 -0
- package/src/providers/google/specs/vendor.ts +661 -0
- package/src/providers/google/tasks/index.ts +53 -0
- package/src/providers/google/tasks/redact.ts +34 -0
- package/src/providers/harness.ts +95 -0
- package/src/providers/icloud/calendar/index.ts +27 -0
- package/src/providers/icloud/contacts/index.ts +17 -0
- package/src/providers/icloud/drive/index.ts +47 -0
- package/src/providers/icloud/index.ts +8 -0
- package/src/providers/icloud/mail/index.ts +37 -0
- package/src/providers/icloud/shared/setup.ts +66 -0
- package/src/providers/index.ts +93 -0
- package/src/providers/linear/index.ts +11 -0
- package/src/providers/linear/scopes.ts +7 -0
- package/src/providers/memory/provider.ts +429 -0
- package/src/providers/notion/index.ts +19 -0
- package/src/providers/owner.ts +49 -0
- package/src/providers/scopes.ts +26 -0
- package/src/providers/setup/plan.ts +141 -0
- package/src/providers/setup/provider.ts +323 -0
- package/src/providers/shared/frontmatter.ts +119 -0
- package/src/providers/skills/provider.ts +283 -0
- package/src/providers/skills/store.ts +252 -0
- package/src/providers/vault/provider.ts +194 -0
- package/src/registry/index.ts +36 -0
- package/src/registry/policy-bridge.ts +32 -0
- package/src/registry/reconcile.ts +313 -0
- package/src/registry/registry.ts +240 -0
- package/src/secrets/document.ts +293 -0
- package/src/secrets/index.ts +154 -0
- package/src/secrets/system.ts +173 -0
- package/src/secrets/vault.ts +336 -0
- package/src/server/attachments.ts +197 -0
- package/src/server/container.ts +96 -0
- package/src/server/edge.ts +53 -0
- package/src/server/endpoint.ts +352 -0
- package/src/server/generations.ts +362 -0
- package/src/server/harness.ts +400 -0
- package/src/server/index.ts +331 -0
- package/src/server/logging.ts +41 -0
- package/src/server/mcp/build.ts +68 -0
- package/src/server/mcp/icon.ts +145 -0
- package/src/server/mcp/index.ts +32 -0
- package/src/server/mcp/instructions.ts +245 -0
- package/src/server/mcp/naming.ts +39 -0
- package/src/server/mcp/prompts.ts +78 -0
- package/src/server/mcp/resources.ts +106 -0
- package/src/server/mcp/routing.ts +117 -0
- package/src/server/mcp/schema.ts +78 -0
- package/src/server/mcp/tools.ts +186 -0
- package/src/server/mcp/visibility.ts +132 -0
- package/src/server/oauth.ts +222 -0
- package/src/server/rebinding.ts +53 -0
- package/src/server/stdio.ts +192 -0
- package/src/stores/blobs/conformance.ts +223 -0
- package/src/stores/blobs/index.ts +125 -0
- package/src/stores/blobs/testing.ts +49 -0
- package/src/stores/state/index.ts +247 -0
- package/src/stores/state/keys.ts +68 -0
- package/src/stores/state/testing.ts +41 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { splitOptionalFrontmatter, stringList, withFrontmatter } from '#providers/shared/frontmatter.ts';
|
|
3
|
+
import {
|
|
4
|
+
defineLocalProvider,
|
|
5
|
+
keepKeys,
|
|
6
|
+
type BlobStore,
|
|
7
|
+
type ProviderDefinition,
|
|
8
|
+
} from '#connectivity';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `memory` — the owner's accumulated knowledge.
|
|
12
|
+
*
|
|
13
|
+
* **Reading and writing are separate capabilities**, and that separation is the
|
|
14
|
+
* whole security argument for this provider (ADR-012 §2, `docs/detailed/security.md`).
|
|
15
|
+
* Upstream content is already treated as potentially prompt-injecting and passed
|
|
16
|
+
* through unscreened; memory an agent can *write to* changes the shape of that
|
|
17
|
+
* risk rather than its size, because an injected instruction is stored once and
|
|
18
|
+
* re-served to every future session, including to a different agent. A read-only
|
|
19
|
+
* memory cannot do that. Nothing here screens what is written — this separates
|
|
20
|
+
* the privilege, and makes no claim to detect anything.
|
|
21
|
+
*
|
|
22
|
+
* **One entry is one Markdown file.** Title, tags, and timestamp are YAML
|
|
23
|
+
* frontmatter above the body, in the same format a skill uses, and the whole
|
|
24
|
+
* thing is a single `BlobStore` object — locally a file under the profile's
|
|
25
|
+
* storage directory, in a deployment an S3 object. There is no index row.
|
|
26
|
+
*
|
|
27
|
+
* That is a deliberate reversal of the original split (ADR-014). An index in
|
|
28
|
+
* `ScopedStore` beside a body in `BlobStore` made listing cheap, and cost more
|
|
29
|
+
* than it bought: the two could disagree, the entry could not be opened in an
|
|
30
|
+
* editor, and a hand-written file was not an entry at all. `lanes link memory` and a
|
|
31
|
+
* text editor now reach the same bytes, which is the point of the format.
|
|
32
|
+
*
|
|
33
|
+
* The store is namespaced `<provider>/<connection>` by core before this
|
|
34
|
+
* provider sees it, so one connection's memory is not addressable from another.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const DEFAULT_LIMIT = 10;
|
|
38
|
+
/**
|
|
39
|
+
* No key prefix.
|
|
40
|
+
*
|
|
41
|
+
* There was an `entry/` one, which put a third segment under a path that was
|
|
42
|
+
* already `<provider>/<connection>` — an entry landed at
|
|
43
|
+
* `memory/memory/entry/<id>.md`. The namespace is the isolation boundary and it
|
|
44
|
+
* is already applied by the time a key reaches here, so the prefix only ever
|
|
45
|
+
* separated entries from other things this provider does not store.
|
|
46
|
+
*/
|
|
47
|
+
const ENTRY_PREFIX = '';
|
|
48
|
+
|
|
49
|
+
/** An entry's frontmatter, parsed. Every field has a fallback — see `readEntry`. */
|
|
50
|
+
interface Entry {
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly title: string;
|
|
53
|
+
readonly tags: readonly string[];
|
|
54
|
+
readonly updatedAt: string;
|
|
55
|
+
readonly body: string;
|
|
56
|
+
readonly bytes: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const ENTRY_ID = /^[a-z0-9][a-z0-9_-]*$/;
|
|
60
|
+
|
|
61
|
+
function entryKey(id: string): string {
|
|
62
|
+
return `${ENTRY_PREFIX}${id}.md`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function idFromKey(key: string): string | null {
|
|
66
|
+
if (!key.startsWith(ENTRY_PREFIX) || !key.endsWith('.md')) return null;
|
|
67
|
+
const id = key.slice(ENTRY_PREFIX.length, -'.md'.length);
|
|
68
|
+
return ENTRY_ID.test(id) ? id : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse one stored entry.
|
|
73
|
+
*
|
|
74
|
+
* Frontmatter is optional, and its absence is not an error: these files are in
|
|
75
|
+
* a directory the owner may edit, and a plain Markdown file dropped in there
|
|
76
|
+
* should read as an entry titled after its id rather than break the listing
|
|
77
|
+
* that would have shown it. `lanes link memory write` puts the frontmatter back.
|
|
78
|
+
*/
|
|
79
|
+
function parseEntry(id: string, text: string, fallbackUpdatedAt: string): Entry {
|
|
80
|
+
const { frontmatter, body } = splitOptionalFrontmatter(text);
|
|
81
|
+
const title = frontmatter['title'];
|
|
82
|
+
const updatedAt = frontmatter['updated_at'];
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
id,
|
|
86
|
+
title: typeof title === 'string' && title.trim().length > 0 ? title : id,
|
|
87
|
+
tags: stringList(frontmatter['tags']),
|
|
88
|
+
updatedAt: typeof updatedAt === 'string' ? updatedAt : fallbackUpdatedAt,
|
|
89
|
+
body: body.trimEnd(),
|
|
90
|
+
bytes: new TextEncoder().encode(text).byteLength,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function serialiseEntry(entry: {
|
|
95
|
+
title: string;
|
|
96
|
+
tags: readonly string[];
|
|
97
|
+
updatedAt: string;
|
|
98
|
+
body: string;
|
|
99
|
+
}): string {
|
|
100
|
+
return withFrontmatter(
|
|
101
|
+
{
|
|
102
|
+
title: entry.title,
|
|
103
|
+
...(entry.tags.length > 0 ? { tags: [...entry.tags] } : {}),
|
|
104
|
+
updated_at: entry.updatedAt,
|
|
105
|
+
},
|
|
106
|
+
`${entry.body.trimEnd()}\n`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readEntry(storage: BlobStore, id: string): Promise<Entry | null> {
|
|
111
|
+
const bytes = await storage.get(entryKey(id));
|
|
112
|
+
if (bytes === null) return null;
|
|
113
|
+
|
|
114
|
+
return parseEntry(id, new TextDecoder().decode(bytes), new Date(0).toISOString());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* How many entries are read at once.
|
|
119
|
+
*
|
|
120
|
+
* A bound rather than `Promise.all` over the whole listing: against a bucket
|
|
121
|
+
* each read is an HTTPS request, and firing four hundred at once trades a slow
|
|
122
|
+
* search for a rate-limited one. Against a local directory the cap costs
|
|
123
|
+
* nothing measurable.
|
|
124
|
+
*/
|
|
125
|
+
const READ_CONCURRENCY = 16;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Every entry, newest first.
|
|
129
|
+
*
|
|
130
|
+
* **This reads every entry, and is honest about being one pass over all of
|
|
131
|
+
* them.** That is the cost of one file per entry: the metadata a listing needs
|
|
132
|
+
* is inside the document, so there is nothing cheaper to consult. At owner
|
|
133
|
+
* scale — hundreds of entries, one process, a local directory — it is fine, and
|
|
134
|
+
* the previous index row bought its speed by being a second copy that could
|
|
135
|
+
* disagree with the file it described.
|
|
136
|
+
*
|
|
137
|
+
* The reads are concurrent, which is not a micro-optimisation: one pass over
|
|
138
|
+
* four hundred entries is four hundred serial round trips once the store is a
|
|
139
|
+
* bucket rather than a directory, and serial round trips are the whole of the
|
|
140
|
+
* difference between a search that feels instant and one that times out.
|
|
141
|
+
*
|
|
142
|
+
* If this ever has to serve tens of thousands of entries, the fix is a derived
|
|
143
|
+
* cache that can be rebuilt from the files, never a second source of truth.
|
|
144
|
+
*/
|
|
145
|
+
async function allEntries(storage: BlobStore): Promise<Entry[]> {
|
|
146
|
+
const blobs = (await storage.list(ENTRY_PREFIX)).flatMap((blob) => {
|
|
147
|
+
const id = idFromKey(blob.key);
|
|
148
|
+
return id === null ? [] : [{ blob, id }];
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const entries: Entry[] = [];
|
|
152
|
+
|
|
153
|
+
for (let start = 0; start < blobs.length; start += READ_CONCURRENCY) {
|
|
154
|
+
const batch = await Promise.all(
|
|
155
|
+
blobs.slice(start, start + READ_CONCURRENCY).map(async ({ blob, id }) => {
|
|
156
|
+
const bytes = await storage.get(blob.key);
|
|
157
|
+
return bytes === null
|
|
158
|
+
? null
|
|
159
|
+
: parseEntry(id, new TextDecoder().decode(bytes), blob.modifiedAt.toISOString());
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
for (const entry of batch) if (entry) entries.push(entry);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return entries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** A stable id from a title, so writing does not demand one be invented. */
|
|
169
|
+
function slugify(title: string): string {
|
|
170
|
+
const slug = title
|
|
171
|
+
.toLowerCase()
|
|
172
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
173
|
+
.replace(/^-+|-+$/g, '')
|
|
174
|
+
.slice(0, 60);
|
|
175
|
+
|
|
176
|
+
return slug.length > 0 ? slug : `entry-${title.length}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const memoryProvider: ProviderDefinition = defineLocalProvider({
|
|
180
|
+
id: 'memory',
|
|
181
|
+
name: 'Memory',
|
|
182
|
+
version: '1.0.0',
|
|
183
|
+
description:
|
|
184
|
+
"The owner's accumulated knowledge. Entries are addressed by id as resources and found by content with memory.search. Writing is a separate capability from reading.",
|
|
185
|
+
|
|
186
|
+
configSchema: z.object({}),
|
|
187
|
+
connectionSchema: z.object({}),
|
|
188
|
+
|
|
189
|
+
bundles: [
|
|
190
|
+
{
|
|
191
|
+
name: 'read',
|
|
192
|
+
description: 'Retrieve and search entries.',
|
|
193
|
+
oauth_scopes: [],
|
|
194
|
+
capabilities: ['entry', 'search', 'get'],
|
|
195
|
+
default: true,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
// Not in the default bundle, and worth denying explicitly for any agent
|
|
199
|
+
// that does not need it — see the provider docstring.
|
|
200
|
+
name: 'write',
|
|
201
|
+
description: 'Create, replace, and delete entries.',
|
|
202
|
+
oauth_scopes: [],
|
|
203
|
+
capabilities: ['write', 'forget'],
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
|
|
207
|
+
capabilities: [
|
|
208
|
+
/**
|
|
209
|
+
* Retrieval by address — a resource, not a tool (ADR-006).
|
|
210
|
+
*
|
|
211
|
+
* A memory entry is read-oriented context with a stable identifier, which is
|
|
212
|
+
* the definition of the primitive. It is the case ADR-006 said the
|
|
213
|
+
* distinction would matter for.
|
|
214
|
+
*/
|
|
215
|
+
{
|
|
216
|
+
kind: 'resource',
|
|
217
|
+
name: 'entry',
|
|
218
|
+
title: 'Memory entry',
|
|
219
|
+
description: 'One stored memory entry, addressed by its id.',
|
|
220
|
+
uriTemplate: 'memory://entry/{id}',
|
|
221
|
+
mimeType: 'text/markdown',
|
|
222
|
+
// The address is worth recording and the content is not — the same trade
|
|
223
|
+
// `gmail.get_message` makes with a message id.
|
|
224
|
+
redact: keepKeys('uri'),
|
|
225
|
+
|
|
226
|
+
async list(context) {
|
|
227
|
+
return (await allEntries(context.storage)).map((entry) => ({
|
|
228
|
+
uri: `memory://entry/${encodeURIComponent(entry.id)}`,
|
|
229
|
+
name: entry.title,
|
|
230
|
+
}));
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
async read(uri, params, context) {
|
|
234
|
+
const raw = params['id'];
|
|
235
|
+
if (!raw) throw new Error(`Malformed memory URI: ${uri}`);
|
|
236
|
+
|
|
237
|
+
const id = decodeURIComponent(raw);
|
|
238
|
+
const entry = await readEntry(context.storage, id);
|
|
239
|
+
if (entry === null) throw new Error(`No memory entry "${id}" on ${context.connection.key}`);
|
|
240
|
+
|
|
241
|
+
return { uri, mimeType: 'text/markdown', text: entry.body };
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
{
|
|
246
|
+
kind: 'tool',
|
|
247
|
+
name: 'get',
|
|
248
|
+
title: 'Read a memory entry',
|
|
249
|
+
description:
|
|
250
|
+
'Return one entry by id. The resource memory://entry/{id} is the same content; this exists for clients that do not read resources.',
|
|
251
|
+
inputSchema: z.object({
|
|
252
|
+
id: z.string().min(1).describe('Entry id'),
|
|
253
|
+
}),
|
|
254
|
+
redact: keepKeys('id'),
|
|
255
|
+
async handler({ id }, context) {
|
|
256
|
+
const entry = await readEntry(context.storage, id);
|
|
257
|
+
|
|
258
|
+
if (entry === null) {
|
|
259
|
+
return {
|
|
260
|
+
content: [{ type: 'text', text: `No memory entry "${id}" on ${context.connection.key}.` }],
|
|
261
|
+
isError: true,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { content: [{ type: 'text', text: entry.body }] };
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
{
|
|
270
|
+
kind: 'tool',
|
|
271
|
+
name: 'search',
|
|
272
|
+
title: 'Search memory',
|
|
273
|
+
description:
|
|
274
|
+
'Find entries whose title, tags, or body contain the query. Case-insensitive substring matching, not ranked relevance.',
|
|
275
|
+
inputSchema: z.object({
|
|
276
|
+
query: z.string().min(1).describe('Text to look for'),
|
|
277
|
+
tag: z.string().optional().describe('Restrict to entries carrying this tag'),
|
|
278
|
+
limit: z.number().int().min(1).max(50).optional().describe(`Maximum results (default ${DEFAULT_LIMIT})`),
|
|
279
|
+
}),
|
|
280
|
+
// Nothing kept: a memory query is as revealing as a Gmail search query,
|
|
281
|
+
// and frequently more so — it is the owner's own material being asked for.
|
|
282
|
+
async handler({ query, tag, limit }, context) {
|
|
283
|
+
const needle = query.toLowerCase();
|
|
284
|
+
const entries = await allEntries(context.storage);
|
|
285
|
+
const candidates = tag ? entries.filter((entry) => entry.tags.includes(tag)) : entries;
|
|
286
|
+
|
|
287
|
+
const matches: Array<{ entry: Entry; snippet: string }> = [];
|
|
288
|
+
|
|
289
|
+
for (const entry of candidates) {
|
|
290
|
+
if (matches.length >= (limit ?? DEFAULT_LIMIT)) break;
|
|
291
|
+
|
|
292
|
+
const inHeader =
|
|
293
|
+
entry.title.toLowerCase().includes(needle) ||
|
|
294
|
+
entry.tags.some((value) => value.toLowerCase().includes(needle));
|
|
295
|
+
|
|
296
|
+
const at = entry.body.toLowerCase().indexOf(needle);
|
|
297
|
+
if (!inHeader && at === -1) continue;
|
|
298
|
+
|
|
299
|
+
matches.push({ entry, snippet: at === -1 ? entry.title : excerpt(entry.body, at) });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
context.audit.annotate({ scanned: candidates.length, matched: matches.length });
|
|
303
|
+
|
|
304
|
+
if (matches.length === 0) {
|
|
305
|
+
return { content: [{ type: 'text', text: `No memory entry matches on ${context.connection.key}.` }] };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// A `resource_link` rather than a URI spelled into the text: core routes
|
|
309
|
+
// the link to the profile and connection this call was made on, and a
|
|
310
|
+
// provider must not learn either. Written as text it would name an
|
|
311
|
+
// address no client could read.
|
|
312
|
+
return {
|
|
313
|
+
content: matches.flatMap(({ entry, snippet }) => [
|
|
314
|
+
{
|
|
315
|
+
type: 'resource_link' as const,
|
|
316
|
+
uri: `memory://entry/${encodeURIComponent(entry.id)}`,
|
|
317
|
+
name: entry.title,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
type: 'text' as const,
|
|
321
|
+
text: `${entry.title}${entry.tags.length > 0 ? ` [${entry.tags.join(', ')}]` : ''}\n${snippet}`,
|
|
322
|
+
},
|
|
323
|
+
]),
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
{
|
|
329
|
+
kind: 'tool',
|
|
330
|
+
name: 'write',
|
|
331
|
+
title: 'Store a memory entry',
|
|
332
|
+
description:
|
|
333
|
+
'Create or replace an entry. Deliberately a separate capability from reading: an instruction written here is served to every later session, so an agent that only needs to recall should not be granted this.',
|
|
334
|
+
inputSchema: z.object({
|
|
335
|
+
title: z.string().min(1).describe('Short human-readable title'),
|
|
336
|
+
text: z.string().min(1).describe('The entry body, as Markdown'),
|
|
337
|
+
id: z.string().optional().describe('Entry id. Derived from the title when omitted; naming an existing one replaces it.'),
|
|
338
|
+
tags: z.array(z.string()).optional().describe('Labels for filtering searches'),
|
|
339
|
+
}),
|
|
340
|
+
// The title and the id are addresses; the text is the content itself.
|
|
341
|
+
redact: keepKeys('id', 'title', 'tags'),
|
|
342
|
+
async handler({ title, text, id, tags }, context) {
|
|
343
|
+
const entryId = id ?? slugify(title);
|
|
344
|
+
assertEntryId(entryId);
|
|
345
|
+
|
|
346
|
+
const document = serialiseEntry({
|
|
347
|
+
title,
|
|
348
|
+
tags: tags ?? [],
|
|
349
|
+
updatedAt: new Date().toISOString(),
|
|
350
|
+
body: text,
|
|
351
|
+
});
|
|
352
|
+
const bytes = new TextEncoder().encode(document);
|
|
353
|
+
await context.storage.put(entryKey(entryId), bytes, { contentType: 'text/markdown' });
|
|
354
|
+
|
|
355
|
+
context.audit.annotate({ entry: entryId, bytes: bytes.byteLength });
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
content: [
|
|
359
|
+
{ type: 'text', text: `Stored memory entry "${entryId}" on ${context.connection.key}.` },
|
|
360
|
+
{ type: 'resource_link', uri: `memory://entry/${entryId}`, name: title },
|
|
361
|
+
],
|
|
362
|
+
};
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
{
|
|
367
|
+
kind: 'tool',
|
|
368
|
+
name: 'forget',
|
|
369
|
+
title: 'Delete a memory entry',
|
|
370
|
+
description: 'Remove an entry and its body.',
|
|
371
|
+
inputSchema: z.object({
|
|
372
|
+
id: z.string().min(1).describe('Entry id'),
|
|
373
|
+
}),
|
|
374
|
+
redact: keepKeys('id'),
|
|
375
|
+
async handler({ id }, context) {
|
|
376
|
+
const existed = await context.storage.has(entryKey(id));
|
|
377
|
+
await context.storage.delete(entryKey(id));
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
content: [
|
|
381
|
+
{
|
|
382
|
+
type: 'text',
|
|
383
|
+
text: existed
|
|
384
|
+
? `Forgot memory entry "${id}" on ${context.connection.key}.`
|
|
385
|
+
: `No memory entry "${id}" on ${context.connection.key}.`,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
...(existed ? {} : { isError: true }),
|
|
389
|
+
};
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
export function assertEntryId(id: string): void {
|
|
396
|
+
if (!ENTRY_ID.test(id)) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
`Memory entry id ${JSON.stringify(id)} must be lowercase letters, digits, "_" or "-".`,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** A window around a match, so a result says why it matched. */
|
|
404
|
+
function excerpt(body: string, at: number, width = 160): string {
|
|
405
|
+
const start = Math.max(0, at - width / 4);
|
|
406
|
+
const text = body.slice(start, start + width).replace(/\s+/g, ' ').trim();
|
|
407
|
+
return `${start > 0 ? '…' : ''}${text}${start + width < body.length ? '…' : ''}`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export default memoryProvider;
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* The pieces `lanes link memory` needs to reach the same bytes this provider does.
|
|
414
|
+
*
|
|
415
|
+
* Exported rather than reimplemented in the CLI: two spellings of one storage
|
|
416
|
+
* layout is exactly how a control plane and its data plane drift apart.
|
|
417
|
+
*/
|
|
418
|
+
export const memoryStorage = {
|
|
419
|
+
key: entryKey,
|
|
420
|
+
idFromKey,
|
|
421
|
+
parse: parseEntry,
|
|
422
|
+
serialise: serialiseEntry,
|
|
423
|
+
read: readEntry,
|
|
424
|
+
all: allEntries,
|
|
425
|
+
prefix: ENTRY_PREFIX,
|
|
426
|
+
slugify,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
export type MemoryEntry = Entry;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Notion supports Dynamic Client Registration, so there is genuinely nothing
|
|
5
|
+
* for an operator to do: we register ourselves with their authorization server
|
|
6
|
+
* at connect time. Browser, approve, done.
|
|
7
|
+
*/
|
|
8
|
+
export const notion = defineProvider({
|
|
9
|
+
id: 'notion',
|
|
10
|
+
name: 'Notion',
|
|
11
|
+
description: 'Pages, databases, comments, and workspace search, via Notion\'s official MCP server.',
|
|
12
|
+
connector: { kind: 'mcp', endpoint: 'https://mcp.notion.com/mcp' },
|
|
13
|
+
auth: { kind: 'oauth', registration: 'dynamic' },
|
|
14
|
+
// No identity block on purpose. Notion exposes no "who am I" tool:
|
|
15
|
+
// `get-users` leads with integration bots rather than the person, and
|
|
16
|
+
// `get-teams` returns teamspaces, which are not the workspace and may be in
|
|
17
|
+
// the trash. Both would produce a confident wrong label, so `connect` asks
|
|
18
|
+
// instead — the fallback exists for exactly this.
|
|
19
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The owner layer — memory, skills, vault.
|
|
3
|
+
*
|
|
4
|
+
* Three providers that hold no third-party account: no OAuth, no vendor API, no
|
|
5
|
+
* rate limit anyone else imposes. They are ordinary `defineLocalProvider`
|
|
6
|
+
* registrations, configured by the same `lanes link connect <provider>`, scoped by the
|
|
7
|
+
* same profiles, and gated by the same policy evaluation as everything else —
|
|
8
|
+
* which was the claim `docs/detailed/init.md` made and called the real test of the
|
|
9
|
+
* architecture.
|
|
10
|
+
*
|
|
11
|
+
* They live in `./memory/`, `./skills/` and `./vault/`, beside `google/` and
|
|
12
|
+
* `icloud/`, because that claim is only true if they are providers in the
|
|
13
|
+
* layout as well as in the prose. This file is the one thing they share: a
|
|
14
|
+
* barrel, because each is *constructed* with a store rather than declared as
|
|
15
|
+
* data, so `#profile`'s registry builder needs all three together.
|
|
16
|
+
*
|
|
17
|
+
* `setup` joined them later and is the same shape: it holds no account either,
|
|
18
|
+
* and it describes the other three rather than reaching anything. It is
|
|
19
|
+
* read-only by construction — see ADR-019 for why describing setup is not one
|
|
20
|
+
* of ADR-007's control-plane exclusions.
|
|
21
|
+
*
|
|
22
|
+
* The ids `memory`, `skills`, `vault`, and `setup` are reserved (`RESERVED_PROVIDER_IDS`)
|
|
23
|
+
* and still refused by default — the registry has to be built with
|
|
24
|
+
* `allowReserved` to hold them, so a third-party provider cannot claim a
|
|
25
|
+
* namespace whose policy rules would then mean something else.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export { memoryProvider, memoryStorage, assertEntryId, type MemoryEntry } from './memory/provider.ts';
|
|
29
|
+
export { createSkillsProvider, type SkillsProviderOptions } from './skills/provider.ts';
|
|
30
|
+
export { createVaultProvider, type VaultProviderOptions } from './vault/provider.ts';
|
|
31
|
+
export { createSetupProvider, type SetupProviderOptions } from './setup/provider.ts';
|
|
32
|
+
export { planAll, planFor, type PlanContext, type ProviderPlan } from './setup/plan.ts';
|
|
33
|
+
// The vault's *store* is not here: it is `#secrets`, beside the system
|
|
34
|
+
// credential store it must never become. What lives in `./vault/` is the
|
|
35
|
+
// provider — the capabilities, and the rule that each item is its own.
|
|
36
|
+
export {
|
|
37
|
+
assertItemId,
|
|
38
|
+
createBlobVaultStore,
|
|
39
|
+
createFileVaultStore,
|
|
40
|
+
createMemoryVaultStore,
|
|
41
|
+
createSecretVaultStore,
|
|
42
|
+
generateVaultKey,
|
|
43
|
+
VAULT_ITEM_ID,
|
|
44
|
+
type BlobVaultStoreOptions,
|
|
45
|
+
type SecretVaultStoreOptions,
|
|
46
|
+
type FileVaultStoreOptions,
|
|
47
|
+
type VaultItem,
|
|
48
|
+
type VaultStore,
|
|
49
|
+
} from '#secrets';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { GOOGLE_SCOPE_MEANINGS } from './google/shared/scopes.ts';
|
|
2
|
+
import { LINEAR_SCOPE_MEANINGS } from './linear/scopes.ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What a scope actually permits, contributed by the provider that requests it.
|
|
6
|
+
*
|
|
7
|
+
* Least privilege is the goal, and a vendor's *advertised* scope list is not the
|
|
8
|
+
* same as its required one. Where a grant does have to be broad it must not
|
|
9
|
+
* happen quietly: an over-broad grant is invisible after the fact, and a consent
|
|
10
|
+
* screen listing five Google-worded scopes is not where someone discovers that
|
|
11
|
+
* "Gmail" meant permanent delete.
|
|
12
|
+
*
|
|
13
|
+
* The table is here rather than in the CLI so that a provider owns its own
|
|
14
|
+
* vocabulary. The CLI renders it and never learns a vendor's words.
|
|
15
|
+
*/
|
|
16
|
+
export interface ScopeMeaning {
|
|
17
|
+
/** Plain-English meaning. */
|
|
18
|
+
readonly meaning: string;
|
|
19
|
+
/** Unrestricted access to the service, or close enough to warrant a stop. */
|
|
20
|
+
readonly broad?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const SCOPE_MEANINGS: Record<string, ScopeMeaning> = {
|
|
24
|
+
...GOOGLE_SCOPE_MEANINGS,
|
|
25
|
+
...LINEAR_SCOPE_MEANINGS,
|
|
26
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { ProviderManifest } from '#connectivity';
|
|
2
|
+
import { setupRequirements, type SetupRequirement } from '#connectivity';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What connecting a provider involves, assembled from its manifest.
|
|
6
|
+
*
|
|
7
|
+
* One implementation, two consumers: `lanes link setup plan` renders it for a
|
|
8
|
+
* terminal, and the `setup.provider` capability renders it for a model. Both
|
|
9
|
+
* must emit the *same* command, because the model's job is to hand a person
|
|
10
|
+
* something to paste — a tool that suggested a command the CLI would reject is
|
|
11
|
+
* worse than one that suggested nothing.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here reads the credential store. What a provider *requires* is a
|
|
14
|
+
* property of shipped code; whether it is *satisfied* needs a store, and only
|
|
15
|
+
* the CLI asks that — ADR-007.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface ProviderPlan {
|
|
19
|
+
readonly id: string;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly description: string;
|
|
22
|
+
/** Connection keys of this provider already configured, e.g. `gmail.main`. */
|
|
23
|
+
readonly connected: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* Whether this provider holds accounts at all, and so can hold another.
|
|
26
|
+
*
|
|
27
|
+
* `auth.kind === 'none'` means there is no credential to key on a connection
|
|
28
|
+
* id, so a second row would address the same thing the first does and "connect
|
|
29
|
+
* another" is meaningless. Everything else derives `<provider>/<id>` (or the
|
|
30
|
+
* `app` it shares), so a second account is a second `connect`.
|
|
31
|
+
*
|
|
32
|
+
* Read that as the credential test it is, not as "the owner layer". The owner
|
|
33
|
+
* layer never reaches here — `catalogue` is `PROVIDER_MANIFESTS`, the shipped
|
|
34
|
+
* third-party providers, and `memory`, `skills`, `vault` and `setup` are
|
|
35
|
+
* registered separately and are not in it. The one manifest this actually
|
|
36
|
+
* excludes is `icloud_drive`, which is `auth: none` because Apple exposes a
|
|
37
|
+
* synced folder rather than a protocol: its root is a manifest field, so a
|
|
38
|
+
* second connection would read the same files off the same Mac. Correct, and
|
|
39
|
+
* for a reason worth stating, because the next `auth: none` provider added
|
|
40
|
+
* with a per-connection identity would be excluded here silently and wrongly.
|
|
41
|
+
*
|
|
42
|
+
* This exists because the overview used to drop a provider entirely once it
|
|
43
|
+
* had one connection, which made "connect another Gmail account" unanswerable
|
|
44
|
+
* from the surface built to answer it.
|
|
45
|
+
*/
|
|
46
|
+
readonly multiAccount: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Finishing this needs a person at a browser.
|
|
49
|
+
*
|
|
50
|
+
* The one fact that decides whether an agent can do it. Everything else is a
|
|
51
|
+
* value that can be written to the store and then used.
|
|
52
|
+
*/
|
|
53
|
+
readonly browser: boolean;
|
|
54
|
+
readonly summary?: string;
|
|
55
|
+
readonly docsUrl?: string;
|
|
56
|
+
readonly steps: readonly string[];
|
|
57
|
+
readonly requires: readonly SetupRequirement[];
|
|
58
|
+
/** True when the command below needs `--id` filling in first. */
|
|
59
|
+
readonly needsId: boolean;
|
|
60
|
+
/** The one line that connects it. */
|
|
61
|
+
readonly command: string;
|
|
62
|
+
/**
|
|
63
|
+
* The OAuth client is operated by somebody else, so `requires` is empty
|
|
64
|
+
* because there is nothing to register — not because setup is trivial.
|
|
65
|
+
*/
|
|
66
|
+
readonly brokered: boolean;
|
|
67
|
+
/** Who operates that client, for the sentence shown before consent. */
|
|
68
|
+
readonly clientOperator?: string;
|
|
69
|
+
/** The line that opts out of it and registers one of your own instead. */
|
|
70
|
+
readonly ownClientCommand?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface PlanContext {
|
|
74
|
+
readonly profile: string;
|
|
75
|
+
/** Every configured connection this caller may see, as `provider.id`. */
|
|
76
|
+
readonly connections: readonly string[];
|
|
77
|
+
/**
|
|
78
|
+
* `oauth_apps` entries this profile declares.
|
|
79
|
+
*
|
|
80
|
+
* Which clients are the operator's own, so a profile that has registered one
|
|
81
|
+
* is described as needing it rather than as needing nothing.
|
|
82
|
+
*/
|
|
83
|
+
readonly ownClients?: readonly string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function planFor(
|
|
87
|
+
manifest: ProviderManifest,
|
|
88
|
+
context: PlanContext,
|
|
89
|
+
connectionId?: string,
|
|
90
|
+
): ProviderPlan {
|
|
91
|
+
const { requirements, needsId, brokered } = setupRequirements(
|
|
92
|
+
manifest,
|
|
93
|
+
connectionId,
|
|
94
|
+
context.profile,
|
|
95
|
+
{ ...(context.ownClients ? { ownClients: context.ownClients } : {}) },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const connected = context.connections.filter((key) => key.startsWith(`${manifest.id}.`));
|
|
99
|
+
|
|
100
|
+
// `--profile` always, never conditionally. One endpoint serves every profile,
|
|
101
|
+
// and the shell this command is pasted into may default to a different one —
|
|
102
|
+
// which is exactly what `resolveSelection`'s "never a silent pick" rule
|
|
103
|
+
// exists to prevent.
|
|
104
|
+
const command =
|
|
105
|
+
`lanes link connect ${manifest.id} --profile ${context.profile}` +
|
|
106
|
+
(needsId ? ' --id <name>' : connectionId ? ` --id ${connectionId}` : '');
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
id: manifest.id,
|
|
110
|
+
name: manifest.name,
|
|
111
|
+
description: manifest.description,
|
|
112
|
+
connected,
|
|
113
|
+
multiAccount: manifest.auth.kind !== 'none',
|
|
114
|
+
browser: manifest.auth.kind === 'oauth',
|
|
115
|
+
...(manifest.setup?.summary ? { summary: manifest.setup.summary } : {}),
|
|
116
|
+
...(manifest.setup?.docs_url ? { docsUrl: manifest.setup.docs_url } : {}),
|
|
117
|
+
steps: manifest.setup?.steps ?? [],
|
|
118
|
+
requires: requirements,
|
|
119
|
+
needsId,
|
|
120
|
+
command,
|
|
121
|
+
brokered,
|
|
122
|
+
...(brokered && manifest.auth.kind === 'oauth' && manifest.auth.broker
|
|
123
|
+
? {
|
|
124
|
+
clientOperator: manifest.auth.broker.operator,
|
|
125
|
+
// The steps stay in `steps` either way. A renderer decides whether to
|
|
126
|
+
// show a console walkthrough for a path nobody has asked for; the
|
|
127
|
+
// plan's job is to say the path exists and what opens it.
|
|
128
|
+
ownClientCommand: `${command} --own-client`,
|
|
129
|
+
}
|
|
130
|
+
: {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function planAll(
|
|
135
|
+
manifests: readonly ProviderManifest[],
|
|
136
|
+
context: PlanContext,
|
|
137
|
+
): ProviderPlan[] {
|
|
138
|
+
return [...manifests]
|
|
139
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
140
|
+
.map((manifest) => planFor(manifest, context));
|
|
141
|
+
}
|