agent-trellis 0.21.0 → 0.22.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/dist/adapters/mcpPlan.d.ts +3 -1
- package/dist/adapters/mcpPlan.js +43 -16
- package/dist/cli.js +9 -6
- package/dist/commands/mcpGateway.js +1 -1
- package/dist/commands/migrate.js +11 -1
- package/dist/commands/remoteSkill.js +18 -1
- package/dist/commands/secretsAudit.js +17 -4
- package/dist/commands/skill.d.ts +11 -6
- package/dist/commands/skill.js +85 -32
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +3 -1
- package/dist/core/canonical.d.ts +1 -2
- package/dist/core/canonical.js +16 -9
- package/dist/core/types.d.ts +8 -7
- package/dist/lib/envVarNames.d.ts +5 -1
- package/dist/lib/envVarNames.js +15 -2
- package/dist/lib/mcpConnect.js +18 -4
- package/dist/lib/secretEnv.d.ts +8 -4
- package/dist/lib/secretEnv.js +44 -15
- package/dist/lib/skillScope.d.ts +14 -0
- package/dist/lib/skillScope.js +40 -0
- package/dist/lib/workspace.js +7 -2
- package/dist/pi-bridge/bundle.js +164 -58
- package/docs/architecture.md +27 -7
- package/docs/getting-started.md +29 -9
- package/docs/research.md +15 -2
- package/package.json +1 -1
- package/schema/scope.example.yaml +15 -3
- package/schema/servers.example.yaml +6 -0
|
@@ -91,4 +91,6 @@ export interface LiteralSecretMatch {
|
|
|
91
91
|
* as before this function reported a field at all.
|
|
92
92
|
*/
|
|
93
93
|
export declare function findLiteralSecret(def: McpServerDef): LiteralSecretMatch | undefined;
|
|
94
|
-
export declare function resolveMcpPlan(agentId: AgentId, mcp: McpConfig, managedAgents: readonly AgentId[], policy: SecretsPolicy
|
|
94
|
+
export declare function resolveMcpPlan(agentId: AgentId, mcp: McpConfig, managedAgents: readonly AgentId[], policy: SecretsPolicy, options?: {
|
|
95
|
+
hosted?: boolean;
|
|
96
|
+
}): Promise<McpPlanResult>;
|
package/dist/adapters/mcpPlan.js
CHANGED
|
@@ -20,7 +20,8 @@ import { isInScope } from "../core/adapter.js";
|
|
|
20
20
|
import { capabilityDeliveryForAgent } from "../core/types.js";
|
|
21
21
|
import { isOAuthAuth, oauthOwner } from "../core/types.js";
|
|
22
22
|
import { codexBearerTokenEnvVar } from "../lib/tomlSection.js";
|
|
23
|
-
import { parseEnvEntry, resolveSecretEnv } from "../lib/secretEnv.js";
|
|
23
|
+
import { deliveredName, isValidSecretVarName, parseEnvEntry, resolveSecretEntries, resolveSecretEnv, SecretReferenceError } from "../lib/secretEnv.js";
|
|
24
|
+
import { extractTemplateVarNames, invalidHeaderTemplate } from "../lib/envVarNames.js";
|
|
24
25
|
export const HUB_ENTRY_NAME = "trellis-hub";
|
|
25
26
|
export const GATEWAY_ENTRY_NAME = "trellis";
|
|
26
27
|
export const LEGACY_GATEWAY_ENTRY_NAME = "trellis-gateway";
|
|
@@ -139,7 +140,10 @@ export function findLiteralSecret(def) {
|
|
|
139
140
|
* (trellis-migrate-env-var-alias).
|
|
140
141
|
*/
|
|
141
142
|
async function findUnresolvedEnvName(def, policy) {
|
|
142
|
-
const
|
|
143
|
+
const headerRefs = Object.values(def.headers ?? {}).flatMap(extractTemplateVarNames)
|
|
144
|
+
.filter((entry) => !isValidSecretVarName(entry));
|
|
145
|
+
const envNames = [...(def.env ?? []), ...Object.values(def.envAliases ?? {})];
|
|
146
|
+
const names = [...envNames, ...headerRefs];
|
|
143
147
|
if (names.length === 0)
|
|
144
148
|
return undefined;
|
|
145
149
|
// A malformed `mxenv://` entry conflicts here too — it is rejected input,
|
|
@@ -147,16 +151,26 @@ async function findUnresolvedEnvName(def, policy) {
|
|
|
147
151
|
// returned text is the DECLARED entry; resolution checks use delivered
|
|
148
152
|
// names (the `KEY` segment for a reference).
|
|
149
153
|
const parsed = names.map(parseEnvEntry);
|
|
150
|
-
const invalid =
|
|
151
|
-
if (invalid)
|
|
152
|
-
return invalid
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
const invalid = names.find((entry, i) => parsed[i].kind === "invalid" || (headerRefs.includes(entry) && parsed[i].kind === "name"));
|
|
155
|
+
if (invalid !== undefined)
|
|
156
|
+
return invalid;
|
|
157
|
+
try {
|
|
158
|
+
// Retain env/envAliases' existing delivered-name collision behavior.
|
|
159
|
+
// Header references instead remain source-bound, with no bare-name fallback.
|
|
160
|
+
const env = await resolveSecretEnv(envNames, policy);
|
|
161
|
+
const missingEnv = envNames.find((entry) => !env[deliveredName(entry)]);
|
|
162
|
+
if (missingEnv !== undefined)
|
|
163
|
+
return missingEnv;
|
|
164
|
+
const headers = await resolveSecretEntries(headerRefs, policy);
|
|
165
|
+
return headerRefs.find((entry) => !headers[entry]);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error instanceof SecretReferenceError)
|
|
169
|
+
return error.entry;
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
158
172
|
}
|
|
159
|
-
export async function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
173
|
+
export async function resolveMcpPlan(agentId, mcp, managedAgents, policy, options = {}) {
|
|
160
174
|
const route = routeForAgent(agentId, mcp, managedAgents);
|
|
161
175
|
const runtimeEnabled = capabilityDeliveryForAgent(agentId, mcp) !== "native";
|
|
162
176
|
// Checked before `hub`: only one entry can be "the" MCP entry for an
|
|
@@ -257,14 +271,14 @@ export async function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
|
257
271
|
conflicts: oauth.conflicts,
|
|
258
272
|
};
|
|
259
273
|
}
|
|
260
|
-
const native = await planDirectServers(agentId, mcp, managedAgents, policy, route);
|
|
274
|
+
const native = await planDirectServers(agentId, mcp, managedAgents, policy, route, undefined, options.hosted || agentId === "pi");
|
|
261
275
|
const hosted = native.desired.filter(({ def }) => isOAuthMcpServer(def) && oauthOwner(def.auth) === "trellis");
|
|
262
276
|
return {
|
|
263
277
|
desired: native.desired.filter(({ def }) => !isOAuthMcpServer(def) || oauthOwner(def.auth) === "agent"),
|
|
264
278
|
conflicts: [...native.conflicts, ...hosted.map(({ name }) => ({ name, message: `Trellis-owned OAuth server "${name}" requires gateway or runtime delivery for ${agentId}`, remediation: "enable gateway/runtime delivery or set auth.owner to agent" }))],
|
|
265
279
|
};
|
|
266
280
|
}
|
|
267
|
-
async function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
|
|
281
|
+
async function planDirectServers(agentId, mcp, managedAgents, policy, route, filter, hosted = false) {
|
|
268
282
|
const desired = [];
|
|
269
283
|
const conflicts = [];
|
|
270
284
|
for (const [name, def] of Object.entries(mcp.servers)) {
|
|
@@ -304,12 +318,17 @@ async function planDirectServers(agentId, mcp, managedAgents, policy, route, fil
|
|
|
304
318
|
});
|
|
305
319
|
continue;
|
|
306
320
|
}
|
|
321
|
+
const invalidHeader = invalidHeaderTemplate(def.headers);
|
|
322
|
+
if (invalidHeader !== undefined) {
|
|
323
|
+
conflicts.push({ name, message: `refusing to write MCP server "${name}": invalid reference template in header ${invalidHeader}`, remediation: "use ${VAR} or ${mxenv://<profile>/<KEY>} in headers" });
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
307
326
|
// Codex has no generic headers concept — only the single
|
|
308
327
|
// Authorization-bearer-token shape is expressible there
|
|
309
328
|
// (trellis-mcp-transport-auth design.md D4). Any other shape is
|
|
310
329
|
// refused for Codex specifically; every other in-scope agent for
|
|
311
330
|
// the same server is unaffected (this loop runs once per agentId).
|
|
312
|
-
if (agentId === "codex" && def.headers && Object.keys(def.headers).length > 0 && !codexBearerTokenEnvVar(def)) {
|
|
331
|
+
if (!hosted && agentId === "codex" && def.headers && Object.keys(def.headers).length > 0 && !codexBearerTokenEnvVar(def)) {
|
|
313
332
|
conflicts.push({
|
|
314
333
|
name,
|
|
315
334
|
message: `refusing to write MCP server "${name}" for codex: its "headers" field isn't the single { Authorization: "Bearer \${VAR}" } shape Codex's own config format can express — Codex has no generic headers concept, only \`bearer_token_env_var\`. Still written normally for every other in-scope agent.`,
|
|
@@ -318,9 +337,9 @@ async function planDirectServers(agentId, mcp, managedAgents, policy, route, fil
|
|
|
318
337
|
continue;
|
|
319
338
|
}
|
|
320
339
|
const unresolvedName = await findUnresolvedEnvName(def, policy);
|
|
321
|
-
if (unresolvedName) {
|
|
340
|
+
if (unresolvedName !== undefined) {
|
|
322
341
|
const entry = parseEnvEntry(unresolvedName);
|
|
323
|
-
if (entry.kind === "invalid") {
|
|
342
|
+
if (entry.kind === "invalid" || !isValidSecretVarName(unresolvedName) && entry.kind === "name") {
|
|
324
343
|
conflicts.push({
|
|
325
344
|
name,
|
|
326
345
|
message: `refusing to write MCP server "${name}": its declared env entry "${unresolvedName}" is not a valid mxenv://<profile>/<KEY> reference (mx-env-secret-source)`,
|
|
@@ -339,6 +358,14 @@ async function planDirectServers(agentId, mcp, managedAgents, policy, route, fil
|
|
|
339
358
|
});
|
|
340
359
|
continue;
|
|
341
360
|
}
|
|
361
|
+
if (!hosted && Object.values(def.headers ?? {}).flatMap(extractTemplateVarNames).some((entry) => parseEnvEntry(entry).kind === "ref")) {
|
|
362
|
+
conflicts.push({
|
|
363
|
+
name,
|
|
364
|
+
message: `refusing to write MCP server "${name}": mxenv header references require Trellis gateway/runtime delivery; native clients cannot resolve them`,
|
|
365
|
+
remediation: `enable gateway/runtime delivery for ${agentId}, keeping transport and headers in servers.yaml`,
|
|
366
|
+
});
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
342
369
|
desired.push({ name, def });
|
|
343
370
|
}
|
|
344
371
|
return { desired, conflicts };
|
package/dist/cli.js
CHANGED
|
@@ -118,7 +118,7 @@ Commands:
|
|
|
118
118
|
--skill, -s <name> required unless --list
|
|
119
119
|
--branch <ref> Git branch, tag, or ref; default is
|
|
120
120
|
the source repository's default branch
|
|
121
|
-
--agent, -a <ids
|
|
121
|
+
--agent, -a <ids|*|none> choose applicable managed Agents
|
|
122
122
|
--list show discovered Skills without import
|
|
123
123
|
--global, --yes accepted skills-CLI compatibility flags
|
|
124
124
|
--dry-run fetch and preview; write nothing
|
|
@@ -205,19 +205,22 @@ Commands:
|
|
|
205
205
|
--dry-run preview the plan, write nothing
|
|
206
206
|
--json machine-readable output, no report text
|
|
207
207
|
skill list
|
|
208
|
-
List canonical skills
|
|
208
|
+
List canonical skills; unregistered skills are paused
|
|
209
|
+
--agent <id> filter by agent and show actual delivery status
|
|
209
210
|
--json machine-readable output, no report text
|
|
210
211
|
skill add <name> --from <path>
|
|
211
212
|
Import a real skill directory into canonical (refuses on
|
|
212
213
|
an existing name with different content, no overwrite)
|
|
213
214
|
--workspace copy into .agents/skills/ instead of canonical
|
|
215
|
+
--agents <ids> | --all | --none
|
|
216
|
+
required without an interactive terminal
|
|
214
217
|
--dry-run preview the plan, write nothing
|
|
215
218
|
--json machine-readable output, no report text
|
|
216
219
|
skill remove <name>
|
|
217
220
|
skill scope <name> --agents <ids> | --all | --none
|
|
218
221
|
Choose which managed agents receive a canonical skill, then
|
|
219
|
-
sync it.
|
|
220
|
-
scope is not editable
|
|
222
|
+
sync it. --all records today's managed set; newly managed
|
|
223
|
+
agents need explicit registration. Built-in scope is not editable
|
|
221
224
|
--dry-run preview the plan, write nothing
|
|
222
225
|
--json machine-readable output, no report text
|
|
223
226
|
skill update-builtin
|
|
@@ -527,7 +530,7 @@ async function main(argv) {
|
|
|
527
530
|
const json = skillRest.includes("--json");
|
|
528
531
|
const dryRun = skillRest.includes("--dry-run");
|
|
529
532
|
if (subcommand === "list") {
|
|
530
|
-
process.exitCode = runSkillList({ json }).exitCode;
|
|
533
|
+
process.exitCode = runSkillList({ json, agent: flagValue(skillRest, "--agent") }).exitCode;
|
|
531
534
|
return;
|
|
532
535
|
}
|
|
533
536
|
if (subcommand === "add") {
|
|
@@ -538,7 +541,7 @@ async function main(argv) {
|
|
|
538
541
|
process.exitCode = 1;
|
|
539
542
|
return;
|
|
540
543
|
}
|
|
541
|
-
process.exitCode = (await runSkillAdd(name, from, { json, dryRun, workspace: skillRest.includes("--workspace") })).exitCode;
|
|
544
|
+
process.exitCode = (await runSkillAdd(name, from, { json, dryRun, workspace: skillRest.includes("--workspace"), selection: parseScopeArgs(skillRest) })).exitCode;
|
|
542
545
|
return;
|
|
543
546
|
}
|
|
544
547
|
if (subcommand === "remove") {
|
|
@@ -90,7 +90,7 @@ export async function resolveGatewayUpstreams(agentId, canonical) {
|
|
|
90
90
|
// ownership-gated removal clears it. Serving that stale entry would hand
|
|
91
91
|
// every server, and every secret resolved into it, to the one agent the
|
|
92
92
|
// user just said to stop involving.
|
|
93
|
-
const { desired, conflicts } = await resolveMcpPlan(agentId, direct, canonical.managedAgents, canonical.secretsPolicy);
|
|
93
|
+
const { desired, conflicts } = await resolveMcpPlan(agentId, direct, canonical.managedAgents, canonical.secretsPolicy, { hosted: true });
|
|
94
94
|
// Agent-owned OAuth servers bypass the gateway as direct entries, so no
|
|
95
95
|
// connection state exists for them here — but Trellis owns the token
|
|
96
96
|
// store, so `trellis.mcp.status` reports their credential health for
|
package/dist/commands/migrate.js
CHANGED
|
@@ -19,7 +19,7 @@ import { deepEqual } from "../lib/deepEqual.js";
|
|
|
19
19
|
import { readClaudeCodeMcpDefs, readCodexMcpDefs, readKiroMcpDefs, readKimiCodeMcpDefs, readZcodeMcpDefs, resolvedEnvTextMap } from "../lib/mcpMigrateRead.js";
|
|
20
20
|
import { parseDotenv, writeLocalSecretValue } from "../lib/secretEnv.js";
|
|
21
21
|
import { findLiteralSecret } from "../adapters/mcpPlan.js";
|
|
22
|
-
import { ensureGitignoreEntry, ensureShellEnvSource, loadCanonicalSource, upsertServerYaml, writeSecretsPolicyExtraction } from "../core/canonical.js";
|
|
22
|
+
import { ensureGitignoreEntry, ensureShellEnvSource, loadCanonicalSource, upsertServerYaml, validateSkillScopeYaml, writeSecretsPolicyExtraction, writeSkillScopeYaml } from "../core/canonical.js";
|
|
23
23
|
import { ALL_AGENTS } from "../core/types.js";
|
|
24
24
|
import { discoverNativeMemory, MAX_NATIVE_MEMORY_BYTES, renderNativeMemory } from "../lib/nativeMemory.js";
|
|
25
25
|
/** Where a `staticEnv` literal secret's real value goes when
|
|
@@ -390,6 +390,11 @@ function copyDirectoryThroughBackup(sourceDir, destDir, backup) {
|
|
|
390
390
|
}
|
|
391
391
|
export function applyMigratePlan(plan, homeDir = homedir(), backup) {
|
|
392
392
|
const canonicalRoot = join(homeDir, ".trellis");
|
|
393
|
+
if (plan.items.some((item) => item.kind === "skill" && item.action === "create")) {
|
|
394
|
+
const validation = validateSkillScopeYaml(join(canonicalRoot, "scope.yaml"));
|
|
395
|
+
if (!validation.ok)
|
|
396
|
+
throw new Error(validation.error);
|
|
397
|
+
}
|
|
393
398
|
for (const item of plan.items) {
|
|
394
399
|
if (item.action !== "create" && item.action !== "reclassify" && item.action !== "extract-secret")
|
|
395
400
|
continue;
|
|
@@ -400,6 +405,11 @@ export function applyMigratePlan(plan, homeDir = homedir(), backup) {
|
|
|
400
405
|
copyDirectoryThroughBackup(item.sourceDir, dest, backup);
|
|
401
406
|
else
|
|
402
407
|
cpSync(item.sourceDir, dest, { recursive: true });
|
|
408
|
+
// Migration proves applicability to its source agent only. Sharing
|
|
409
|
+
// with other agents is a separate, explicit compatibility decision.
|
|
410
|
+
const scope = writeSkillScopeYaml(join(canonicalRoot, "scope.yaml"), item.name, [plan.agent], backup);
|
|
411
|
+
if (!scope.ok)
|
|
412
|
+
throw new Error(scope.error);
|
|
403
413
|
}
|
|
404
414
|
else if (item.kind === "instructions" && item.sourceContent !== undefined) {
|
|
405
415
|
mkdirSync(canonicalRoot, { recursive: true });
|
|
@@ -15,6 +15,7 @@ import { isBuiltinSkillName } from "../lib/builtinSkills.js";
|
|
|
15
15
|
import { emptyRemoteSkillLock, readRemoteSkillLock, writeRemoteSkillLock } from "../lib/remoteSkillLock.js";
|
|
16
16
|
import { fetchRemoteRepository, normalizeGitHubRepository } from "../lib/remoteSkillSource.js";
|
|
17
17
|
import { collectSyncReport, printReport as printSyncReport } from "./sync.js";
|
|
18
|
+
import { requestSkillScope } from "../lib/skillScope.js";
|
|
18
19
|
function safeSkillName(name) {
|
|
19
20
|
return typeof name === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(name) && name !== "." && name !== "..";
|
|
20
21
|
}
|
|
@@ -41,7 +42,9 @@ function scopeForRequest(homeDir, agents) {
|
|
|
41
42
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
42
43
|
}
|
|
43
44
|
if (!agents || agents.length === 0)
|
|
44
|
-
return {
|
|
45
|
+
return { error: 'choose applicable agents with --agent <ids> or --agent "*"' };
|
|
46
|
+
if (agents.length === 1 && agents[0] === "none")
|
|
47
|
+
return { scope: [] };
|
|
45
48
|
const values = agents.flatMap((value) => value.split(",").map((id) => id.trim()).filter(Boolean));
|
|
46
49
|
if (values.length === 0)
|
|
47
50
|
return { error: "--agent needs at least one Agent id" };
|
|
@@ -164,6 +167,20 @@ export async function runRemoteSkillAdd(sourceInput, name, opts = {}) {
|
|
|
164
167
|
console.error(plan.detail);
|
|
165
168
|
return { exitCode: 1 };
|
|
166
169
|
}
|
|
170
|
+
if (!opts.agents) {
|
|
171
|
+
const home = opts.homeDir ?? homedir();
|
|
172
|
+
const managed = existsSync(join(home, ".trellis")) ? loadCanonicalSource(home).managedAgents : [];
|
|
173
|
+
const selected = await requestSkillScope({}, managed, !opts.json);
|
|
174
|
+
if ("error" in selected) {
|
|
175
|
+
const detail = `${selected.error}; remote import uses --agent <ids>, --agent "*" or --agent none`;
|
|
176
|
+
if (opts.json)
|
|
177
|
+
console.log(JSON.stringify({ action: "invalid-options", detail }));
|
|
178
|
+
else
|
|
179
|
+
console.error(detail);
|
|
180
|
+
return { exitCode: 1 };
|
|
181
|
+
}
|
|
182
|
+
opts = { ...opts, agents: selected.scope.length ? selected.scope : ["none"] };
|
|
183
|
+
}
|
|
167
184
|
let fetched;
|
|
168
185
|
try {
|
|
169
186
|
fetched = fetchRemoteRepository(repository, { branch: opts.branch });
|
|
@@ -29,7 +29,7 @@ import { KiroAdapter } from "../adapters/kiro.js";
|
|
|
29
29
|
import { ZcodeAdapter } from "../adapters/zcode.js";
|
|
30
30
|
import { declaredEnvNames, extractJsonEnvVarNames, extractTomlEnvVarNames, extractZcodeEnvVarNames } from "../lib/envVarNames.js";
|
|
31
31
|
import { resolveZcodeProfile } from "../probes/zcode.js";
|
|
32
|
-
import { parseEnvEntry,
|
|
32
|
+
import { parseEnvEntry, resolveSecretEntries, SecretReferenceError } from "../lib/secretEnv.js";
|
|
33
33
|
/** Only agents in `managedAgents` — a present-but-unmanaged agent's config
|
|
34
34
|
* file is never even read (trellis-managed-agents), same restriction as
|
|
35
35
|
* `sync`/`mcp sync`. pi has no static, generated MCP config file to audit
|
|
@@ -76,16 +76,29 @@ async function findMissingEnvValues(servers, policy) {
|
|
|
76
76
|
if (resolvable.length === 0)
|
|
77
77
|
return findings;
|
|
78
78
|
// Findings name the DELIVERED name (the KEY segment for a reference).
|
|
79
|
-
const
|
|
79
|
+
const pending = [...resolvable];
|
|
80
|
+
let resolved = {};
|
|
81
|
+
// One broken profile must be a finding, not abort the entire audit.
|
|
82
|
+
while (pending.length) {
|
|
83
|
+
try {
|
|
84
|
+
resolved = await resolveSecretEntries(pending, policy);
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (!(error instanceof SecretReferenceError))
|
|
89
|
+
throw error;
|
|
90
|
+
pending.splice(pending.indexOf(error.entry), 1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
80
93
|
for (const entry of resolvable) {
|
|
81
94
|
const p = parseEnvEntry(entry);
|
|
82
95
|
const delivered = p.kind === "invalid" ? entry : p.delivered;
|
|
83
|
-
if (!resolved[
|
|
96
|
+
if (!resolved[entry]) {
|
|
84
97
|
findings.push({
|
|
85
98
|
agent: "environment",
|
|
86
99
|
file: p.kind === "ref" ? `the mxenv composition (profile "${p.profile}")` : (policy.envFile ?? "process environment"),
|
|
87
100
|
kind: "missing-env-value",
|
|
88
|
-
detail: `"${delivered}" is declared by a canonical MCP server's env but has no resolvable value`,
|
|
101
|
+
detail: `"${delivered}" is declared by a canonical MCP server's env or headers but has no resolvable value`,
|
|
89
102
|
});
|
|
90
103
|
}
|
|
91
104
|
}
|
package/dist/commands/skill.d.ts
CHANGED
|
@@ -31,12 +31,18 @@ export declare function runSkillUpdateBuiltin(opts?: {
|
|
|
31
31
|
export interface SkillListEntry {
|
|
32
32
|
name: string;
|
|
33
33
|
scope: readonly AgentId[];
|
|
34
|
+
registered?: boolean;
|
|
35
|
+
delivery?: {
|
|
36
|
+
state: "linked" | "missing" | "conflict" | "runtime";
|
|
37
|
+
path?: string;
|
|
38
|
+
};
|
|
34
39
|
remote?: Pick<RemoteSkillLockEntry, "source" | "requestedRef" | "commit" | "subdirectory" | "digest">;
|
|
35
40
|
}
|
|
36
|
-
export declare function collectSkillList(homeDir?: string): SkillListEntry[];
|
|
41
|
+
export declare function collectSkillList(homeDir?: string, agent?: AgentId): SkillListEntry[];
|
|
37
42
|
export declare function runSkillList(opts?: {
|
|
38
43
|
homeDir?: string;
|
|
39
44
|
json?: boolean;
|
|
45
|
+
agent?: string;
|
|
40
46
|
}): {
|
|
41
47
|
exitCode: number;
|
|
42
48
|
};
|
|
@@ -46,6 +52,7 @@ export interface SkillAddPlan {
|
|
|
46
52
|
action: SkillAddAction;
|
|
47
53
|
detail: string;
|
|
48
54
|
sourceDir?: string;
|
|
55
|
+
scope?: AgentId[];
|
|
49
56
|
}
|
|
50
57
|
export declare function collectSkillAddPlan(name: string, fromPath: string, homeDir?: string): SkillAddPlan;
|
|
51
58
|
export declare function applySkillAddPlan(plan: SkillAddPlan, homeDir?: string, backup?: BackupSession): void;
|
|
@@ -84,6 +91,7 @@ export declare function runSkillAdd(name: string, fromPath: string, opts?: {
|
|
|
84
91
|
dryRun?: boolean;
|
|
85
92
|
workspace?: boolean;
|
|
86
93
|
workspaceDir?: string;
|
|
94
|
+
selection?: ScopeSelectorRaw;
|
|
87
95
|
}): Promise<{
|
|
88
96
|
exitCode: number;
|
|
89
97
|
}>;
|
|
@@ -117,13 +125,10 @@ export interface SkillScopePlan {
|
|
|
117
125
|
name: string;
|
|
118
126
|
action: SkillScopeAction;
|
|
119
127
|
detail: string;
|
|
120
|
-
/**
|
|
121
|
-
* scope so the skill reaches every managed agent. Set when the selection was
|
|
122
|
-
* valid, whatever the action. */
|
|
128
|
+
/** Applicable agents recorded in scope.yaml. Even --all is a fixed list. */
|
|
123
129
|
scope?: AgentId[];
|
|
124
130
|
mode?: "all" | "none" | "explicit";
|
|
125
|
-
/**
|
|
126
|
-
* (trellis-scope-editing-and-auth-status design.md D3). */
|
|
131
|
+
/** Retained for API compatibility; skill lists are never normalized away. */
|
|
127
132
|
normalizedFromFull?: boolean;
|
|
128
133
|
/** The agents that will actually receive the skill afterwards. */
|
|
129
134
|
effective?: readonly AgentId[];
|
package/dist/commands/skill.js
CHANGED
|
@@ -14,14 +14,17 @@ import { dirname } from "node:path";
|
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import { decideDirImport } from "../lib/dirEquals.js";
|
|
16
16
|
import { findSkillFile } from "../lib/skillFile.js";
|
|
17
|
-
import { loadCanonicalSource, writeSkillScopeYaml } from "../core/canonical.js";
|
|
17
|
+
import { loadCanonicalSource, validateSkillScopeYaml, writeSkillScopeYaml } from "../core/canonical.js";
|
|
18
18
|
import { resolveScope } from "../core/types.js";
|
|
19
19
|
import { isBuiltinSkillName } from "../lib/builtinSkills.js";
|
|
20
20
|
import { openBackupSession } from "../lib/backup.js";
|
|
21
21
|
import { collectSyncReport, printReport as printSyncReport } from "./sync.js";
|
|
22
22
|
import { remoteSkillProvenance } from "./remoteSkill.js";
|
|
23
|
-
import {
|
|
23
|
+
import { sameScope } from "../lib/scopeSelection.js";
|
|
24
24
|
import { findWorkspaceRoot } from "../lib/workspace.js";
|
|
25
|
+
import { readSkillScopes, requestSkillScope, selectSkillScope } from "../lib/skillScope.js";
|
|
26
|
+
import { isSymlinkTo } from "../lib/fsIdentity.js";
|
|
27
|
+
import { usesNativeCapabilityDelivery, ALL_AGENTS } from "../core/types.js";
|
|
25
28
|
function builtinSkillTemplate() {
|
|
26
29
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
27
30
|
return readFileSync(join(here, "..", "..", "schema", "builtin-skills", "trellis-runtime", "SKILL.md"), "utf8");
|
|
@@ -58,17 +61,34 @@ export function runSkillUpdateBuiltin(opts = {}) {
|
|
|
58
61
|
console.log(`${opts.dryRun ? "[dry run] " : ""}skill update-builtin\n [${plan.action}] ${plan.detail}`);
|
|
59
62
|
return { exitCode: 0 };
|
|
60
63
|
}
|
|
61
|
-
export function collectSkillList(homeDir = homedir()) {
|
|
64
|
+
export function collectSkillList(homeDir = homedir(), agent) {
|
|
62
65
|
const canonical = loadCanonicalSource(homeDir);
|
|
63
66
|
const provenance = remoteSkillProvenance(homeDir);
|
|
64
|
-
|
|
67
|
+
const registry = readSkillScopes(join(homeDir, ".trellis", "scope.yaml"));
|
|
68
|
+
const roots = {
|
|
69
|
+
"claude-code": join(homeDir, ".claude", "skills"), codex: join(homeDir, ".agents", "skills"),
|
|
70
|
+
kiro: join(homeDir, ".kiro", "skills"), pi: join(homeDir, ".pi", "agent", "skills"),
|
|
71
|
+
"kimi-code": join(homeDir, ".kimi-code", "skills"), zcode: "",
|
|
72
|
+
};
|
|
73
|
+
return canonical.skills.filter((skill) => agent ? resolveScope(skill.scope, canonical.managedAgents).includes(agent) : !isBuiltinSkillName(skill.name)).map((skill) => ({
|
|
65
74
|
name: skill.name,
|
|
66
75
|
scope: resolveScope(skill.scope, canonical.managedAgents),
|
|
76
|
+
...(agent ? {
|
|
77
|
+
registered: Object.hasOwn(registry, skill.name) || isBuiltinSkillName(skill.name),
|
|
78
|
+
delivery: !usesNativeCapabilityDelivery(agent, canonical.mcp) || agent === "zcode"
|
|
79
|
+
? { state: "runtime" }
|
|
80
|
+
: { state: isSymlinkTo(join(roots[agent], skill.name), skill.dir) ? "linked"
|
|
81
|
+
: existsSync(join(roots[agent], skill.name)) ? "conflict" : "missing", path: join(roots[agent], skill.name) },
|
|
82
|
+
} : {}),
|
|
67
83
|
...(provenance[skill.name] ? { remote: provenance[skill.name] } : {}),
|
|
68
84
|
}));
|
|
69
85
|
}
|
|
70
86
|
export function runSkillList(opts = {}) {
|
|
71
|
-
|
|
87
|
+
if (opts.agent !== undefined && !ALL_AGENTS.includes(opts.agent)) {
|
|
88
|
+
console.error(`unknown agent: ${opts.agent}; choose ${ALL_AGENTS.join(", ")}`);
|
|
89
|
+
return { exitCode: 1 };
|
|
90
|
+
}
|
|
91
|
+
const entries = collectSkillList(opts.homeDir ?? homedir(), opts.agent);
|
|
72
92
|
if (opts.json) {
|
|
73
93
|
console.log(JSON.stringify(entries, null, 2));
|
|
74
94
|
}
|
|
@@ -76,9 +96,9 @@ export function runSkillList(opts = {}) {
|
|
|
76
96
|
console.log("No skills in canonical source yet.");
|
|
77
97
|
}
|
|
78
98
|
else {
|
|
79
|
-
for (const { name, scope, remote } of entries) {
|
|
99
|
+
for (const { name, scope, remote, delivery } of entries) {
|
|
80
100
|
const provenance = remote ? ` — remote: ${remote.source}@${remote.commit.slice(0, 12)}` : "";
|
|
81
|
-
console.log(`${name} — ${scope.length > 0 ? scope.join(", ") : "(no managed agent
|
|
101
|
+
console.log(`${name} — ${scope.length > 0 ? scope.join(", ") : "(paused: no applicable managed agent)"}${delivery ? ` [${delivery.state}]` : ""}${provenance}`);
|
|
82
102
|
}
|
|
83
103
|
}
|
|
84
104
|
return { exitCode: 0 };
|
|
@@ -105,15 +125,26 @@ export function collectSkillAddPlan(name, fromPath, homeDir = homedir()) {
|
|
|
105
125
|
}
|
|
106
126
|
}
|
|
107
127
|
export function applySkillAddPlan(plan, homeDir = homedir(), backup) {
|
|
108
|
-
if (plan.action !== "create"
|
|
128
|
+
if (plan.action !== "create" && !(plan.action === "already-present" && plan.scope !== undefined))
|
|
109
129
|
return;
|
|
110
|
-
|
|
111
|
-
if (backup) {
|
|
112
|
-
backup.createDirFromSource(dest, plan.sourceDir);
|
|
130
|
+
if (plan.action === "create" && !plan.sourceDir)
|
|
113
131
|
return;
|
|
132
|
+
const scopePath = join(homeDir, ".trellis", "scope.yaml");
|
|
133
|
+
const validation = validateSkillScopeYaml(scopePath);
|
|
134
|
+
if (!validation.ok)
|
|
135
|
+
throw new Error(validation.error);
|
|
136
|
+
const dest = join(homeDir, ".trellis", "skills", plan.name);
|
|
137
|
+
if (plan.action === "create") {
|
|
138
|
+
if (backup)
|
|
139
|
+
backup.createDirFromSource(dest, plan.sourceDir);
|
|
140
|
+
else {
|
|
141
|
+
mkdirSync(dest, { recursive: true });
|
|
142
|
+
cpSync(plan.sourceDir, dest, { recursive: true });
|
|
143
|
+
}
|
|
114
144
|
}
|
|
115
|
-
|
|
116
|
-
|
|
145
|
+
const written = writeSkillScopeYaml(scopePath, plan.name, plan.scope ?? [], backup);
|
|
146
|
+
if (!written.ok)
|
|
147
|
+
throw new Error(written.error);
|
|
117
148
|
}
|
|
118
149
|
/** The workspace twin of `collectSkillAddPlan` (trellis-workspace-scope
|
|
119
150
|
* task 3.4): same source validation, destination is `.agents/skills/`
|
|
@@ -137,29 +168,40 @@ export function collectSkillAddWorkspacePlan(name, fromPath, workspaceRoot) {
|
|
|
137
168
|
}
|
|
138
169
|
}
|
|
139
170
|
export function applySkillAddWorkspacePlan(plan, workspaceRoot, backup) {
|
|
140
|
-
if (plan.action !== "create"
|
|
171
|
+
if (plan.action !== "create" && !(plan.action === "already-present" && plan.scope !== undefined))
|
|
141
172
|
return;
|
|
142
|
-
|
|
143
|
-
if (backup) {
|
|
144
|
-
backup.createDirFromSource(dest, plan.sourceDir);
|
|
173
|
+
if (plan.action === "create" && !plan.sourceDir)
|
|
145
174
|
return;
|
|
175
|
+
const scopePath = join(workspaceRoot, ".agents", "scope.yaml");
|
|
176
|
+
const validation = validateSkillScopeYaml(scopePath);
|
|
177
|
+
if (!validation.ok)
|
|
178
|
+
throw new Error(validation.error);
|
|
179
|
+
const dest = join(workspaceRoot, ".agents", "skills", plan.name);
|
|
180
|
+
if (plan.action === "create") {
|
|
181
|
+
if (backup)
|
|
182
|
+
backup.createDirFromSource(dest, plan.sourceDir);
|
|
183
|
+
else {
|
|
184
|
+
mkdirSync(dest, { recursive: true });
|
|
185
|
+
cpSync(plan.sourceDir, dest, { recursive: true });
|
|
186
|
+
}
|
|
146
187
|
}
|
|
147
|
-
|
|
148
|
-
|
|
188
|
+
const written = writeSkillScopeYaml(scopePath, plan.name, plan.scope ?? [], backup);
|
|
189
|
+
if (!written.ok)
|
|
190
|
+
throw new Error(written.error);
|
|
149
191
|
}
|
|
150
192
|
/** Same own-backup-session + cascade-sync posture as
|
|
151
193
|
* `applySkillAddWithSync`, scoped to the workspace layer. */
|
|
152
194
|
export async function applySkillAddWorkspaceWithSync(plan, workspaceRoot, opts = {}) {
|
|
153
195
|
const homeDir = opts.homeDir ?? homedir();
|
|
154
|
-
const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "create" ? openBackupSession(homeDir, "skill-add") : undefined;
|
|
196
|
+
const ownSession = !opts.dryRun && !opts.backupSession && (plan.action === "create" || plan.action === "already-present" && plan.scope !== undefined) ? openBackupSession(homeDir, "skill-add") : undefined;
|
|
155
197
|
const session = opts.backupSession ?? ownSession;
|
|
156
|
-
if (!opts.dryRun && plan.action === "create") {
|
|
198
|
+
if (!opts.dryRun && (plan.action === "create" || plan.action === "already-present")) {
|
|
157
199
|
applySkillAddWorkspacePlan(plan, workspaceRoot, session);
|
|
158
200
|
}
|
|
159
201
|
ownSession?.finalize();
|
|
160
202
|
const failed = plan.action === "conflict" || plan.action === "invalid-source";
|
|
161
203
|
let syncReport;
|
|
162
|
-
if (!failed && plan.action === "create" && existsSync(join(homeDir, ".trellis"))) {
|
|
204
|
+
if (!failed && (plan.action === "create" || plan.scope !== undefined) && existsSync(join(homeDir, ".trellis"))) {
|
|
163
205
|
syncReport = await collectSyncReport({ target: "skills", homeDir, workspaceDir: workspaceRoot, dryRun: opts.dryRun });
|
|
164
206
|
}
|
|
165
207
|
return { plan, ...(syncReport ? { sync: syncReport } : {}) };
|
|
@@ -174,21 +216,30 @@ export async function applySkillAddWorkspaceWithSync(plan, workspaceRoot, opts =
|
|
|
174
216
|
*/
|
|
175
217
|
export async function applySkillAddWithSync(plan, opts = {}) {
|
|
176
218
|
const homeDir = opts.homeDir ?? homedir();
|
|
177
|
-
const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "create" ? openBackupSession(homeDir, "skill-add") : undefined;
|
|
219
|
+
const ownSession = !opts.dryRun && !opts.backupSession && (plan.action === "create" || plan.action === "already-present" && plan.scope !== undefined) ? openBackupSession(homeDir, "skill-add") : undefined;
|
|
178
220
|
const session = opts.backupSession ?? ownSession;
|
|
179
|
-
if (!opts.dryRun && plan.action === "create") {
|
|
221
|
+
if (!opts.dryRun && (plan.action === "create" || plan.action === "already-present")) {
|
|
180
222
|
applySkillAddPlan(plan, homeDir, session);
|
|
181
223
|
}
|
|
182
224
|
ownSession?.finalize();
|
|
183
225
|
const failed = plan.action === "conflict" || plan.action === "invalid-source";
|
|
184
226
|
let syncReport;
|
|
185
|
-
if (!failed && plan.action === "create" && existsSync(join(homeDir, ".trellis"))) {
|
|
227
|
+
if (!failed && (plan.action === "create" || plan.scope !== undefined) && existsSync(join(homeDir, ".trellis"))) {
|
|
186
228
|
syncReport = await collectSyncReport({ target: "skills", homeDir, dryRun: opts.dryRun });
|
|
187
229
|
}
|
|
188
230
|
return { plan, ...(syncReport ? { sync: syncReport } : {}) };
|
|
189
231
|
}
|
|
190
232
|
export async function runSkillAdd(name, fromPath, opts = {}) {
|
|
191
233
|
const homeDir = opts.homeDir ?? homedir();
|
|
234
|
+
const managed = existsSync(join(homeDir, ".trellis")) ? loadCanonicalSource(homeDir).managedAgents : [];
|
|
235
|
+
const selected = await requestSkillScope(opts.selection ?? {}, managed, !opts.json);
|
|
236
|
+
if ("error" in selected) {
|
|
237
|
+
if (opts.json)
|
|
238
|
+
console.log(JSON.stringify({ name, action: "invalid-source", detail: selected.error }));
|
|
239
|
+
else
|
|
240
|
+
console.error(selected.error);
|
|
241
|
+
return { exitCode: 1 };
|
|
242
|
+
}
|
|
192
243
|
if (opts.workspace) {
|
|
193
244
|
const workspaceRoot = opts.workspaceDir ?? findWorkspaceRoot(process.cwd());
|
|
194
245
|
if (!workspaceRoot) {
|
|
@@ -196,6 +247,7 @@ export async function runSkillAdd(name, fromPath, opts = {}) {
|
|
|
196
247
|
return { exitCode: 1 };
|
|
197
248
|
}
|
|
198
249
|
const plan = collectSkillAddWorkspacePlan(name, fromPath, workspaceRoot);
|
|
250
|
+
plan.scope = selected.scope;
|
|
199
251
|
const { sync: syncReport } = await applySkillAddWorkspaceWithSync(plan, workspaceRoot, { homeDir, dryRun: opts.dryRun });
|
|
200
252
|
const failed = plan.action === "conflict" || plan.action === "invalid-source";
|
|
201
253
|
if (opts.json) {
|
|
@@ -211,6 +263,7 @@ export async function runSkillAdd(name, fromPath, opts = {}) {
|
|
|
211
263
|
return { exitCode: failed || syncConflict ? 1 : 0 };
|
|
212
264
|
}
|
|
213
265
|
const plan = collectSkillAddPlan(name, fromPath, homeDir);
|
|
266
|
+
plan.scope = selected.scope;
|
|
214
267
|
const { sync: syncReport } = await applySkillAddWithSync(plan, { homeDir, dryRun: opts.dryRun });
|
|
215
268
|
const failed = plan.action === "conflict" || plan.action === "invalid-source";
|
|
216
269
|
if (opts.json) {
|
|
@@ -289,9 +342,10 @@ export function collectSkillScopePlan(name, raw, homeDir = homedir()) {
|
|
|
289
342
|
return { name, action: "invalid-input", detail: `"${name}" is a Trellis package-owned Skill — its scope is not editable` };
|
|
290
343
|
}
|
|
291
344
|
const canonical = loadCanonicalSource(homeDir);
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
return { name, action: "invalid-input", detail:
|
|
345
|
+
const chosen = selectSkillScope(raw, canonical.managedAgents);
|
|
346
|
+
if ("error" in chosen)
|
|
347
|
+
return { name, action: "invalid-input", detail: chosen.error };
|
|
348
|
+
const selection = { scope: chosen.scope, mode: raw.all ? "all" : raw.none ? "none" : "explicit", normalizedFromFull: false };
|
|
295
349
|
const skill = canonical.skills.find((candidate) => candidate.name === name);
|
|
296
350
|
if (!skill)
|
|
297
351
|
return { name, action: "not-found", detail: `no canonical skill named "${name}"` };
|
|
@@ -302,12 +356,11 @@ export function collectSkillScopePlan(name, raw, homeDir = homedir()) {
|
|
|
302
356
|
normalizedFromFull: selection.normalizedFromFull,
|
|
303
357
|
effective: resolveScope(selection.scope, canonical.managedAgents),
|
|
304
358
|
};
|
|
305
|
-
const summary =
|
|
306
|
-
|
|
307
|
-
if (sameScope(skill.scope, selection.scope)) {
|
|
359
|
+
const summary = selection.scope.length ? selection.scope.join(", ") : "no agent (paused)";
|
|
360
|
+
if (sameScope(skill.scope, selection.scope) && Object.hasOwn(readSkillScopes(join(homeDir, ".trellis", "scope.yaml")), name)) {
|
|
308
361
|
return { ...base, action: "already-set", detail: `scope is already ${summary}` };
|
|
309
362
|
}
|
|
310
|
-
return { ...base, action: "updated", detail: `set scope to ${summary}
|
|
363
|
+
return { ...base, action: "updated", detail: `set scope to ${summary}` };
|
|
311
364
|
}
|
|
312
365
|
/** Write inside a backup session so `trellis rollback` covers it, then the
|
|
313
366
|
* same skills sync `add` / `remove` run — narrowing is converged by
|
package/dist/commands/sync.d.ts
CHANGED
package/dist/commands/sync.js
CHANGED
|
@@ -83,7 +83,7 @@ export async function collectSyncReport(opts = {}) {
|
|
|
83
83
|
reports.push({ agent: adapter.id, present: true, items: allItems, ...(warnings.length > 0 ? { warnings } : {}) });
|
|
84
84
|
}
|
|
85
85
|
ownSession?.finalize();
|
|
86
|
-
return { reports, ...(canonical.workspace ? { workspaceRoot: canonical.workspace.root } : {}) };
|
|
86
|
+
return { reports, ...(canonical.diagnostics.length ? { diagnostics: canonical.diagnostics } : {}), ...(canonical.workspace ? { workspaceRoot: canonical.workspace.root } : {}) };
|
|
87
87
|
}
|
|
88
88
|
export async function runSync(opts = {}) {
|
|
89
89
|
let report;
|
|
@@ -118,6 +118,8 @@ export function printReport(report, dryRun, check = false) {
|
|
|
118
118
|
console.log("[dry run]");
|
|
119
119
|
if (check)
|
|
120
120
|
console.log("[check]");
|
|
121
|
+
for (const diagnostic of report.diagnostics ?? [])
|
|
122
|
+
console.log(` ${diagnostic}`);
|
|
121
123
|
if (report.workspaceRoot) {
|
|
122
124
|
console.log(`Workspace layer: ${report.workspaceRoot} (.agents/)`);
|
|
123
125
|
}
|
package/dist/core/canonical.d.ts
CHANGED
|
@@ -27,8 +27,7 @@ export type ScopeYamlWriteResult = {
|
|
|
27
27
|
export declare function validateSkillScopeYaml(path: string): ScopeYamlWriteResult;
|
|
28
28
|
/**
|
|
29
29
|
* Sets one Skill's canonical scope without rebuilding unrelated hand-authored
|
|
30
|
-
* scope entries.
|
|
31
|
-
* therefore restores the normal all-managed default.
|
|
30
|
+
* scope entries. `undefined` removes registration, pausing distribution.
|
|
32
31
|
*/
|
|
33
32
|
export declare function writeSkillScopeYaml(path: string, name: string, scope: Scope, backup?: BackupSession): ScopeYamlWriteResult;
|
|
34
33
|
export declare function upsertServerYaml(path: string, name: string, def: McpServerDef, backup?: BackupSession): ServersYamlWriteResult;
|