@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,283 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import {
|
|
3
|
+
loadProfileSkills,
|
|
4
|
+
readSkill,
|
|
5
|
+
removeSkill,
|
|
6
|
+
renderSkill,
|
|
7
|
+
writeSkill,
|
|
8
|
+
type LoadedSkill,
|
|
9
|
+
} from './store.ts';
|
|
10
|
+
import {
|
|
11
|
+
defineLocalProvider,
|
|
12
|
+
keepKeys,
|
|
13
|
+
type BlobStore,
|
|
14
|
+
type Capability,
|
|
15
|
+
type ProviderDefinition,
|
|
16
|
+
} from '#connectivity';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `skills` — reusable procedures, on the MCP prompts primitive.
|
|
20
|
+
*
|
|
21
|
+
* **Skills are invoked, not read** (ADR-012 §1). The discriminator is whether
|
|
22
|
+
* the answer depends on arguments: a resource is a function of its URI alone,
|
|
23
|
+
* and "review this diff" is a function of the diff. Against a tool: a tool
|
|
24
|
+
* result returns to the model as data it reasons about, while a prompt returns
|
|
25
|
+
* as messages that *become* the conversation — and a procedure wants the
|
|
26
|
+
* second. This is the case ADR-006 reserved the primitive for. That part is
|
|
27
|
+
* unchanged.
|
|
28
|
+
*
|
|
29
|
+
* **What changed is that a skill can now be written (ADR-014).** ADR-012 §1
|
|
30
|
+
* refused the write path outright: a skill is instructions, so an agent able to
|
|
31
|
+
* author one could persist its own future behaviour. The argument was right and
|
|
32
|
+
* the conclusion was too strong. It is the same risk `memory.write` carries —
|
|
33
|
+
* text an agent authors, stored once, re-served to every later session — and
|
|
34
|
+
* memory answered it by making writing a separate capability in a non-default
|
|
35
|
+
* bundle rather than by having no write path at all. Skills now answer it the
|
|
36
|
+
* same way, and the asymmetry between the two goes away.
|
|
37
|
+
*
|
|
38
|
+
* Two things preserve the narrower half of ADR-012 §1:
|
|
39
|
+
*
|
|
40
|
+
* - **Authoring is not in the default bundle.** `lanes link connect skills` grants
|
|
41
|
+
* `skills.*`, so narrowing it is a `deny: [skills.manage.*]` line — the
|
|
42
|
+
* same one-line narrowing memory documents, and stated as plainly.
|
|
43
|
+
* - **`skills.manage.get` is in the write bundle, not the read one.** A
|
|
44
|
+
* read-only agent still cannot read a skill body and so cannot select its
|
|
45
|
+
* own instructions from one; the prompt primitive's user-selected framing
|
|
46
|
+
* survives for every agent that has not been granted authoring.
|
|
47
|
+
*
|
|
48
|
+
* The management tools live under `manage.` rather than at the top level
|
|
49
|
+
* because a skill named `write` would otherwise be the capability
|
|
50
|
+
* `skills.write` twice over. Skill names cannot contain a dot, so
|
|
51
|
+
* `skills.manage.*` can only ever mean these four.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
export interface SkillsProviderOptions {
|
|
55
|
+
/**
|
|
56
|
+
* The skills currently in the store. Read by the caller because loading them
|
|
57
|
+
* is asynchronous and building a provider is not, and because the runtime is
|
|
58
|
+
* the right place to decide when the store is touched.
|
|
59
|
+
*/
|
|
60
|
+
readonly skills: readonly LoadedSkill[];
|
|
61
|
+
/**
|
|
62
|
+
* Where skills live. Omitted for a read-only provider — one built without a
|
|
63
|
+
* store has no authoring capabilities at all, rather than four that fail.
|
|
64
|
+
*/
|
|
65
|
+
readonly store?: BlobStore;
|
|
66
|
+
/**
|
|
67
|
+
* Called after a write or a removal, so the registry can pick the change up.
|
|
68
|
+
*
|
|
69
|
+
* Each skill is its own capability, so a newly written one is invisible until
|
|
70
|
+
* the provider is rebuilt. The provider cannot rebuild itself and should not
|
|
71
|
+
* know how; it says that something changed, and the runtime decides what that
|
|
72
|
+
* means.
|
|
73
|
+
*/
|
|
74
|
+
readonly onChange?: () => Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function createSkillsProvider(options: SkillsProviderOptions): ProviderDefinition {
|
|
78
|
+
const seen = new Set<string>();
|
|
79
|
+
const capabilities: Capability[] = [];
|
|
80
|
+
|
|
81
|
+
for (const skill of options.skills) {
|
|
82
|
+
if (seen.has(skill.name)) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Two skills are both named "${skill.name}" (${skill.path}). A skill name becomes the capability id "skills.${skill.name}", which must be unique.`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
seen.add(skill.name);
|
|
88
|
+
|
|
89
|
+
capabilities.push({
|
|
90
|
+
kind: 'prompt',
|
|
91
|
+
name: skill.name,
|
|
92
|
+
...(skill.title ? { title: skill.title } : {}),
|
|
93
|
+
description: skill.description,
|
|
94
|
+
arguments: skill.arguments.map((argument) => ({
|
|
95
|
+
name: argument.name,
|
|
96
|
+
description: argument.description,
|
|
97
|
+
...(argument.required ? { required: true } : {}),
|
|
98
|
+
})),
|
|
99
|
+
// The arguments are the procedure's parameters and are recorded by name
|
|
100
|
+
// and type only — the default. A skill argument routinely carries the very
|
|
101
|
+
// material the skill is about.
|
|
102
|
+
async render(args) {
|
|
103
|
+
return {
|
|
104
|
+
messages: [{ role: 'user', text: renderSkill(skill.body, args) }],
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const manage = options.store ? managementCapabilities(options.store, options.onChange) : [];
|
|
111
|
+
|
|
112
|
+
return defineLocalProvider({
|
|
113
|
+
id: 'skills',
|
|
114
|
+
name: 'Skills',
|
|
115
|
+
version: '1.0.0',
|
|
116
|
+
description:
|
|
117
|
+
"Reusable procedures the owner has written, offered as MCP prompts. Authoring them is a separate capability from invoking them, and is not granted by default.",
|
|
118
|
+
|
|
119
|
+
configSchema: z.object({}),
|
|
120
|
+
connectionSchema: z.object({}),
|
|
121
|
+
|
|
122
|
+
bundles: [
|
|
123
|
+
{
|
|
124
|
+
name: 'read',
|
|
125
|
+
description: 'Invoke the owner-authored skills.',
|
|
126
|
+
oauth_scopes: [],
|
|
127
|
+
// Every skill, and nothing that reads or writes a skill's source.
|
|
128
|
+
// Per-skill control is a policy line — `deny: [skills.review_diff]` —
|
|
129
|
+
// rather than a bundle.
|
|
130
|
+
capabilities: [...seen],
|
|
131
|
+
default: true,
|
|
132
|
+
},
|
|
133
|
+
...(manage.length > 0
|
|
134
|
+
? [
|
|
135
|
+
{
|
|
136
|
+
// Not in the default bundle, for the reason in the docstring: a
|
|
137
|
+
// skill an agent writes is instructions it will be handed later.
|
|
138
|
+
name: 'author',
|
|
139
|
+
description: 'Read, create, and delete the skills themselves.',
|
|
140
|
+
oauth_scopes: [],
|
|
141
|
+
capabilities: manage.map((capability) => capability.name),
|
|
142
|
+
},
|
|
143
|
+
]
|
|
144
|
+
: []),
|
|
145
|
+
],
|
|
146
|
+
|
|
147
|
+
capabilities: [...capabilities, ...manage],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function managementCapabilities(
|
|
152
|
+
store: BlobStore,
|
|
153
|
+
onChange: (() => Promise<void>) | undefined,
|
|
154
|
+
): Capability[] {
|
|
155
|
+
const changed = async (): Promise<void> => {
|
|
156
|
+
await onChange?.();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return [
|
|
160
|
+
{
|
|
161
|
+
kind: 'tool',
|
|
162
|
+
name: 'manage.list',
|
|
163
|
+
title: 'List skills',
|
|
164
|
+
description:
|
|
165
|
+
'Every skill that exists, with its description and arguments. The prompt list shows only the ones policy permits; this shows what is stored.',
|
|
166
|
+
inputSchema: z.object({}),
|
|
167
|
+
async handler(_input, context) {
|
|
168
|
+
const skills = await loadProfileSkills(store);
|
|
169
|
+
|
|
170
|
+
if (skills.length === 0) {
|
|
171
|
+
return { content: [{ type: 'text', text: `No skills on ${context.connection.key}.` }] };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: 'text',
|
|
178
|
+
text: skills
|
|
179
|
+
.map((skill) => {
|
|
180
|
+
const args = skill.arguments
|
|
181
|
+
.map((argument) => (argument.required ? argument.name : `${argument.name}?`))
|
|
182
|
+
.join(', ');
|
|
183
|
+
return `${skill.name}${args ? `(${args})` : ''} — ${skill.description}`;
|
|
184
|
+
})
|
|
185
|
+
.join('\n'),
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
{
|
|
193
|
+
kind: 'tool',
|
|
194
|
+
name: 'manage.get',
|
|
195
|
+
title: 'Read a skill',
|
|
196
|
+
description:
|
|
197
|
+
'Return a skill exactly as stored, frontmatter included — what to edit before writing it back.',
|
|
198
|
+
inputSchema: z.object({
|
|
199
|
+
name: z.string().min(1).describe('Skill name'),
|
|
200
|
+
}),
|
|
201
|
+
redact: keepKeys('name'),
|
|
202
|
+
async handler({ name }, context) {
|
|
203
|
+
const skill = await readSkill(store, name);
|
|
204
|
+
|
|
205
|
+
if (skill === null) {
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: 'text', text: `No skill "${name}" on ${context.connection.key}.` }],
|
|
208
|
+
isError: true,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// The stored document, not the parsed body: an edit-then-write round
|
|
213
|
+
// trip has to carry the frontmatter with it.
|
|
214
|
+
const bytes = await store.get(skill.path);
|
|
215
|
+
return {
|
|
216
|
+
content: [
|
|
217
|
+
{ type: 'text', text: bytes ? new TextDecoder().decode(bytes) : skill.body },
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
{
|
|
224
|
+
kind: 'tool',
|
|
225
|
+
name: 'manage.write',
|
|
226
|
+
title: 'Write a skill',
|
|
227
|
+
description:
|
|
228
|
+
'Create or replace a skill. The text is a whole Markdown document: YAML frontmatter carrying "description" and optional "arguments", then the procedure body, where {{argument}} is substituted at invocation. A skill written here is instructions an agent is later handed as its own turn — this is deliberately a separate capability from invoking one.',
|
|
229
|
+
inputSchema: z.object({
|
|
230
|
+
name: z
|
|
231
|
+
.string()
|
|
232
|
+
.min(1)
|
|
233
|
+
.describe('Skill name: lowercase letters, digits, "_" or "-". Becomes skills.<name>.'),
|
|
234
|
+
text: z.string().min(1).describe('The whole document, frontmatter included'),
|
|
235
|
+
}),
|
|
236
|
+
// The name is an address and is worth recording; the text is the
|
|
237
|
+
// instructions themselves, which are the content rather than the subject.
|
|
238
|
+
redact: keepKeys('name'),
|
|
239
|
+
async handler({ name, text }, context) {
|
|
240
|
+
const skill = await writeSkill(store, name, text);
|
|
241
|
+
await changed();
|
|
242
|
+
|
|
243
|
+
context.audit.annotate({ skill: name, bytes: new TextEncoder().encode(text).byteLength });
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
content: [
|
|
247
|
+
{
|
|
248
|
+
type: 'text',
|
|
249
|
+
text: `Stored skill "${skill.name}" on ${context.connection.key}. It is available as the prompt "skills_${skill.name}" where policy allows it.`,
|
|
250
|
+
},
|
|
251
|
+
],
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
{
|
|
257
|
+
kind: 'tool',
|
|
258
|
+
name: 'manage.remove',
|
|
259
|
+
title: 'Delete a skill',
|
|
260
|
+
description: 'Remove a skill and the prompt it provided.',
|
|
261
|
+
inputSchema: z.object({
|
|
262
|
+
name: z.string().min(1).describe('Skill name'),
|
|
263
|
+
}),
|
|
264
|
+
redact: keepKeys('name'),
|
|
265
|
+
async handler({ name }, context) {
|
|
266
|
+
const removed = await removeSkill(store, name);
|
|
267
|
+
if (removed) await changed();
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
content: [
|
|
271
|
+
{
|
|
272
|
+
type: 'text',
|
|
273
|
+
text: removed
|
|
274
|
+
? `Removed skill "${name}" from ${context.connection.key}.`
|
|
275
|
+
: `No skill "${name}" on ${context.connection.key}.`,
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
...(removed ? {} : { isError: true }),
|
|
279
|
+
};
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
];
|
|
283
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { BlobStore } from '#stores/blobs';
|
|
2
|
+
import { splitFrontmatter } from '#providers/shared/frontmatter.ts';
|
|
3
|
+
import { ConfigError } from '#profile';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Skills the owner has written.
|
|
7
|
+
*
|
|
8
|
+
* A skill is a **document in the owner layer's store**, which locally is a file
|
|
9
|
+
* in `<workspace>/data/<profile>/skills.d/`, in the same format it has always
|
|
10
|
+
* had. Going through `BlobStore` rather than `node:fs` is what lets a deployed
|
|
11
|
+
* instance have skills at all: a filesystem path is baked into a container
|
|
12
|
+
* image at build time, and an S3 key is not.
|
|
13
|
+
*
|
|
14
|
+
* **One profile's skills, not the workspace's.** Nothing in this file knows
|
|
15
|
+
* that — the store it is handed is already rooted at one profile's directory,
|
|
16
|
+
* which is why moving skills under the profile (ADR-030) changed where the
|
|
17
|
+
* store is opened and nothing about how it is read.
|
|
18
|
+
*
|
|
19
|
+
* Two layouts, because both are conventional and neither is worth refusing:
|
|
20
|
+
*
|
|
21
|
+
* skills.d/review-diff.md
|
|
22
|
+
* skills.d/review-diff/SKILL.md
|
|
23
|
+
*
|
|
24
|
+
* Frontmatter is YAML between `---` fences, matching every other tool that
|
|
25
|
+
* reads a skill file. `description` is the only required key; the body after
|
|
26
|
+
* the fence is the prompt.
|
|
27
|
+
*
|
|
28
|
+
* **A skill can now be written, which ADR-012 §1 refused and ADR-014 reverses.**
|
|
29
|
+
* The reversal is about the *write path existing*, not about who may use it:
|
|
30
|
+
* authoring stays out of the default bundle, so an agent reaches it only where
|
|
31
|
+
* policy says so, and the control plane reaches it always.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** The nested layout's filename, kept so a rewrite lands on the file it read. */
|
|
35
|
+
const NESTED = 'SKILL.md';
|
|
36
|
+
|
|
37
|
+
export interface SkillArgument {
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly description: string;
|
|
40
|
+
readonly required?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface LoadedSkill {
|
|
44
|
+
/** Becomes the capability name, so `skills.<name>` is what policy grants. */
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly title?: string;
|
|
47
|
+
readonly description: string;
|
|
48
|
+
readonly arguments: readonly SkillArgument[];
|
|
49
|
+
readonly body: string;
|
|
50
|
+
/** The store key it was read from — `review-diff.md` or `review-diff/SKILL.md`. */
|
|
51
|
+
readonly path: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Capability names reach policy rules and MCP wire names, and both are stricter
|
|
56
|
+
* than a filename. Refused rather than slugified: a skill silently renamed is a
|
|
57
|
+
* policy rule that silently stops matching.
|
|
58
|
+
*/
|
|
59
|
+
const SKILL_NAME = /^[a-z0-9][a-z0-9_-]*$/;
|
|
60
|
+
|
|
61
|
+
export function assertSkillName(name: string, source = 'skill'): void {
|
|
62
|
+
if (!SKILL_NAME.test(name)) {
|
|
63
|
+
throw new ConfigError(
|
|
64
|
+
`${source}: skill name ${JSON.stringify(name)} must be lowercase letters, digits, "_" or "-" — ` +
|
|
65
|
+
'it becomes the capability id "skills.<name>", which policy rules and MCP names both constrain.',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Every skill in the store.
|
|
72
|
+
*
|
|
73
|
+
* A key that is not a skill is skipped rather than refused — a skill directory
|
|
74
|
+
* legitimately holds references and scripts beside its `SKILL.md`, and one
|
|
75
|
+
* unparseable file must not hide every other skill behind it. A file that *is*
|
|
76
|
+
* a skill and is malformed still throws, because that one is a mistake the
|
|
77
|
+
* owner wants to hear about.
|
|
78
|
+
*/
|
|
79
|
+
export async function loadProfileSkills(store: BlobStore): Promise<LoadedSkill[]> {
|
|
80
|
+
const keys = (await store.list()).map((blob) => blob.key).sort();
|
|
81
|
+
const loaded: LoadedSkill[] = [];
|
|
82
|
+
|
|
83
|
+
for (const key of keys) {
|
|
84
|
+
const name = skillNameFor(key);
|
|
85
|
+
if (name === null) continue;
|
|
86
|
+
|
|
87
|
+
const bytes = await store.get(key);
|
|
88
|
+
if (bytes === null) continue; // Listed then deleted; not worth failing over.
|
|
89
|
+
|
|
90
|
+
loaded.push(parseSkill(new TextDecoder().decode(bytes), key, name));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return loaded;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Read one skill by name, or null when there is none. */
|
|
97
|
+
export async function readSkill(store: BlobStore, name: string): Promise<LoadedSkill | null> {
|
|
98
|
+
const key = await skillKey(store, name);
|
|
99
|
+
if (key === null) return null;
|
|
100
|
+
|
|
101
|
+
const bytes = await store.get(key);
|
|
102
|
+
if (bytes === null) return null;
|
|
103
|
+
|
|
104
|
+
return parseSkill(new TextDecoder().decode(bytes), key, name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Create or replace a skill, validating it before it is stored.
|
|
109
|
+
*
|
|
110
|
+
* Parsed first so a malformed document is refused rather than persisted: a
|
|
111
|
+
* skill that fails to load is invisible until the next start, at which point
|
|
112
|
+
* the reason it broke is a long way from the write that broke it.
|
|
113
|
+
*
|
|
114
|
+
* An existing skill is rewritten **in the layout it already has**, so editing a
|
|
115
|
+
* `review-diff/SKILL.md` does not silently leave a second `review-diff.md`
|
|
116
|
+
* beside it — two files claiming one capability id, which the provider refuses
|
|
117
|
+
* to build at all.
|
|
118
|
+
*
|
|
119
|
+
* A *new* one is written as `<name>/SKILL.md`, which is the shape `~/.claude`
|
|
120
|
+
* uses and the shape the skill bundled with this repository already has. The
|
|
121
|
+
* flat `<name>.md` still loads and is still rewritten in place; the directory is
|
|
122
|
+
* the better default because a skill that grows a reference or a script has
|
|
123
|
+
* somewhere to put it without moving first.
|
|
124
|
+
*/
|
|
125
|
+
export async function writeSkill(
|
|
126
|
+
store: BlobStore,
|
|
127
|
+
name: string,
|
|
128
|
+
text: string,
|
|
129
|
+
): Promise<LoadedSkill> {
|
|
130
|
+
assertSkillName(name);
|
|
131
|
+
|
|
132
|
+
const key = (await skillKey(store, name)) ?? `${name}/${NESTED}`;
|
|
133
|
+
const skill = parseSkill(text, key, name);
|
|
134
|
+
|
|
135
|
+
if (skill.name !== name) {
|
|
136
|
+
throw new ConfigError(
|
|
137
|
+
`${key}: the frontmatter names this skill "${skill.name}", but it is being written as "${name}". ` +
|
|
138
|
+
'Remove the "name" key to take it from the filename, or write it under the name it declares.',
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// No contentType: on the filesystem adapter that writes a `<key>.meta`
|
|
143
|
+
// sidecar, and `skills.d/` is a directory the owner opens and edits by hand.
|
|
144
|
+
// Nothing reads a skill's stored content type.
|
|
145
|
+
await store.put(key, new TextEncoder().encode(text));
|
|
146
|
+
return skill;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Delete a skill in whichever layout holds it. Returns false when absent. */
|
|
150
|
+
export async function removeSkill(store: BlobStore, name: string): Promise<boolean> {
|
|
151
|
+
const key = await skillKey(store, name);
|
|
152
|
+
if (key === null) return false;
|
|
153
|
+
|
|
154
|
+
await store.delete(key);
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Where a skill of this name lives today, in either layout. */
|
|
159
|
+
async function skillKey(store: BlobStore, name: string): Promise<string | null> {
|
|
160
|
+
assertSkillName(name);
|
|
161
|
+
|
|
162
|
+
const flat = `${name}.md`;
|
|
163
|
+
if (await store.has(flat)) return flat;
|
|
164
|
+
|
|
165
|
+
const nested = `${name}/${NESTED}`;
|
|
166
|
+
return (await store.has(nested)) ? nested : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The skill name a store key implies, or null when the key is not a skill.
|
|
171
|
+
*
|
|
172
|
+
* `review-diff.md` and `review-diff/SKILL.md` are skills. A file nested any
|
|
173
|
+
* deeper, or named anything else inside a skill's own directory, belongs to
|
|
174
|
+
* that skill and is not one itself.
|
|
175
|
+
*/
|
|
176
|
+
function skillNameFor(key: string): string | null {
|
|
177
|
+
const segments = key.split('/');
|
|
178
|
+
|
|
179
|
+
if (segments.length === 1) {
|
|
180
|
+
const [file] = segments as [string];
|
|
181
|
+
return file.endsWith('.md') ? file.slice(0, -'.md'.length) : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (segments.length === 2 && segments[1] === NESTED) return segments[0]!;
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function parseSkill(text: string, source: string, fallbackName: string): LoadedSkill {
|
|
189
|
+
const { frontmatter, body } = splitFrontmatter(text, source);
|
|
190
|
+
|
|
191
|
+
const name = typeof frontmatter['name'] === 'string' ? frontmatter['name'] : fallbackName;
|
|
192
|
+
assertSkillName(name, source);
|
|
193
|
+
|
|
194
|
+
const description = frontmatter['description'];
|
|
195
|
+
if (typeof description !== 'string' || description.trim().length === 0) {
|
|
196
|
+
throw new ConfigError(
|
|
197
|
+
`${source}: a skill needs a "description" in its frontmatter. It is the only thing an agent ` +
|
|
198
|
+
'sees when deciding whether this skill applies.',
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (body.trim().length === 0) {
|
|
203
|
+
throw new ConfigError(`${source}: the skill has no body, so there is no procedure to render.`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
name,
|
|
208
|
+
...(typeof frontmatter['title'] === 'string' ? { title: frontmatter['title'] } : {}),
|
|
209
|
+
description,
|
|
210
|
+
arguments: parseArguments(frontmatter['arguments'], source),
|
|
211
|
+
body,
|
|
212
|
+
path: source,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function parseArguments(raw: unknown, source: string): SkillArgument[] {
|
|
217
|
+
if (raw === undefined || raw === null) return [];
|
|
218
|
+
if (!Array.isArray(raw)) {
|
|
219
|
+
throw new ConfigError(`${source}: "arguments" must be a list.`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return raw.map((entry, index) => {
|
|
223
|
+
const record = (entry ?? {}) as Record<string, unknown>;
|
|
224
|
+
const name = record['name'];
|
|
225
|
+
|
|
226
|
+
if (typeof name !== 'string' || !SKILL_NAME.test(name)) {
|
|
227
|
+
throw new ConfigError(
|
|
228
|
+
`${source}: arguments[${index}] needs a lowercase "name" — it becomes a prompt argument.`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
name,
|
|
234
|
+
description: typeof record['description'] === 'string' ? record['description'] : name,
|
|
235
|
+
...(record['required'] === true ? { required: true } : {}),
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Substitute `{{argument}}` in a skill body.
|
|
242
|
+
*
|
|
243
|
+
* An argument the caller omitted leaves its placeholder in place rather than
|
|
244
|
+
* becoming an empty string: a procedure that reads "review the diff below" with
|
|
245
|
+
* nothing below it is a worse failure than one that visibly names what is
|
|
246
|
+
* missing.
|
|
247
|
+
*/
|
|
248
|
+
export function renderSkill(body: string, args: Readonly<Record<string, string>>): string {
|
|
249
|
+
return body.replace(/\{\{\s*([a-z0-9_-]+)\s*\}\}/gi, (placeholder, name: string) =>
|
|
250
|
+
typeof args[name] === 'string' ? args[name]! : placeholder,
|
|
251
|
+
);
|
|
252
|
+
}
|