agent-trellis 0.14.1 → 0.15.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/claude-code.js +4 -3
- package/dist/adapters/codex.js +4 -3
- package/dist/adapters/kimi-code.js +4 -3
- package/dist/adapters/kiro.js +5 -4
- package/dist/adapters/pi.js +4 -3
- package/dist/adapters/zcode.d.ts +12 -1
- package/dist/adapters/zcode.js +7 -6
- package/dist/cli.js +38 -5
- package/dist/commands/doctor.d.ts +9 -1
- package/dist/commands/doctor.js +43 -6
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.js +24 -0
- package/dist/commands/mcp.d.ts +44 -1
- package/dist/commands/mcp.js +145 -33
- package/dist/commands/migrate.d.ts +12 -0
- package/dist/commands/migrate.js +8 -4
- package/dist/commands/migrateWorkspace.d.ts +40 -0
- package/dist/commands/migrateWorkspace.js +197 -0
- package/dist/commands/rollback.js +65 -35
- package/dist/commands/skill.d.ts +14 -0
- package/dist/commands/skill.js +71 -0
- package/dist/commands/sync.d.ts +16 -1
- package/dist/commands/sync.js +48 -15
- package/dist/core/adapter.d.ts +14 -0
- package/dist/core/canonical.d.ts +20 -34
- package/dist/core/canonical.js +18 -92
- package/dist/core/mcpServerDef.d.ts +39 -0
- package/dist/core/mcpServerDef.js +93 -0
- package/dist/core/types.d.ts +39 -4
- package/dist/lib/mcpMigrateRead.d.ts +25 -0
- package/dist/lib/mcpMigrateRead.js +9 -2
- package/dist/lib/skillProvider.js +18 -5
- package/dist/lib/workspace.d.ts +39 -0
- package/dist/lib/workspace.js +158 -0
- package/dist/lib/workspaceConfig.d.ts +45 -0
- package/dist/lib/workspaceConfig.js +98 -0
- package/dist/lib/workspaceDoctor.d.ts +44 -0
- package/dist/lib/workspaceDoctor.js +94 -0
- package/dist/lib/workspaceFiles.d.ts +38 -0
- package/dist/lib/workspaceFiles.js +264 -0
- package/dist/lib/workspaceMcp.d.ts +57 -0
- package/dist/lib/workspaceMcp.js +277 -0
- package/dist/lib/workspaceMerge.d.ts +37 -0
- package/dist/lib/workspaceMerge.js +98 -0
- package/dist/lib/workspaceMigrate.d.ts +118 -0
- package/dist/lib/workspaceMigrate.js +423 -0
- package/dist/pi-bridge/bundle.js +258 -96
- package/docs/architecture.md +111 -49
- package/docs/getting-started.md +86 -14
- package/docs/mcp-oauth-matrix.md +12 -1
- package/docs/overview.zh-CN.md +17 -0
- package/docs/research.md +28 -6
- package/docs/roadmap.md +17 -5
- package/package.json +1 -1
- package/docs/implementation-plan.md +0 -341
|
@@ -18,6 +18,7 @@ import * as claudeCodeProbe from "../probes/claude-code.js";
|
|
|
18
18
|
import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
|
|
19
19
|
import { applyJsonMcp, planJsonMcp, renderJsonServerEntry } from "./jsonMcp.js";
|
|
20
20
|
import { loadMcpOwnership, ownedByAgent, recordOwned, forgetOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
21
|
+
import { globalLayerMcp, globalLayerSkills } from "../lib/workspaceMerge.js";
|
|
21
22
|
export class ClaudeCodeAdapter {
|
|
22
23
|
homeDir;
|
|
23
24
|
name = "Claude Code";
|
|
@@ -32,7 +33,7 @@ export class ClaudeCodeAdapter {
|
|
|
32
33
|
async plan(canonical) {
|
|
33
34
|
const canonicalRoot = dirname(canonical.instructionsFile);
|
|
34
35
|
const skillsRoot = join(this.homeDir, ".claude", "skills");
|
|
35
|
-
const desiredSkills = canonical
|
|
36
|
+
const desiredSkills = globalLayerSkills(canonical)
|
|
36
37
|
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
37
38
|
.map((skill) => ({ name: skill.name, target: skill.dir }));
|
|
38
39
|
const skillItems = planSymlinks({
|
|
@@ -54,7 +55,7 @@ export class ClaudeCodeAdapter {
|
|
|
54
55
|
const configPath = join(this.homeDir, ".claude.json");
|
|
55
56
|
const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
|
|
56
57
|
const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
|
|
57
|
-
return planJsonMcp({ configPath, parsed, mcp: canonical
|
|
58
|
+
return planJsonMcp({ configPath, parsed, mcp: globalLayerMcp(canonical), agentId: this.id, managedAgents: canonical.managedAgents, policy: canonical.secretsPolicy, ownership });
|
|
58
59
|
}
|
|
59
60
|
async apply(plan, backup) {
|
|
60
61
|
await applySymlinkPlan(plan.filter((item) => item.kind !== "mcp"), backup);
|
|
@@ -83,7 +84,7 @@ export class ClaudeCodeAdapter {
|
|
|
83
84
|
return { ok: false, mismatches: ["claude-code is not present on this machine"] };
|
|
84
85
|
}
|
|
85
86
|
const mismatches = [];
|
|
86
|
-
const desiredNames = new Set(canonical.
|
|
87
|
+
const desiredNames = new Set(globalLayerSkills(canonical).filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
87
88
|
const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
|
|
88
89
|
for (const name of desiredNames) {
|
|
89
90
|
if (!actualSkills.has(name)) {
|
package/dist/adapters/codex.js
CHANGED
|
@@ -26,6 +26,7 @@ import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
|
|
|
26
26
|
import { resolveMcpPlan } from "./mcpPlan.js";
|
|
27
27
|
import { currentServerSectionText, removeSection, renderServerSection, upsertSection } from "../lib/tomlSection.js";
|
|
28
28
|
import { loadMcpOwnership, ownedByAgent, recordOwned, forgetOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
29
|
+
import { globalLayerMcp, globalLayerSkills } from "../lib/workspaceMerge.js";
|
|
29
30
|
export class CodexAdapter {
|
|
30
31
|
homeDir;
|
|
31
32
|
name = "Codex";
|
|
@@ -40,7 +41,7 @@ export class CodexAdapter {
|
|
|
40
41
|
async plan(canonical) {
|
|
41
42
|
const canonicalRoot = dirname(canonical.instructionsFile);
|
|
42
43
|
const skillsRoot = join(this.homeDir, ".agents", "skills");
|
|
43
|
-
const desiredSkills = canonical
|
|
44
|
+
const desiredSkills = globalLayerSkills(canonical)
|
|
44
45
|
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
45
46
|
.map((skill) => ({ name: skill.name, target: skill.dir }));
|
|
46
47
|
const skillItems = planSymlinks({
|
|
@@ -67,7 +68,7 @@ export class CodexAdapter {
|
|
|
67
68
|
planMcp(canonical) {
|
|
68
69
|
const configTomlPath = join(this.homeDir, ".codex", "config.toml");
|
|
69
70
|
const content = existsSync(configTomlPath) ? readFileSync(configTomlPath, "utf-8") : "";
|
|
70
|
-
const { desired, conflicts } = resolveMcpPlan(this.id, canonical
|
|
71
|
+
const { desired, conflicts } = resolveMcpPlan(this.id, globalLayerMcp(canonical), canonical.managedAgents, canonical.secretsPolicy);
|
|
71
72
|
const items = [];
|
|
72
73
|
for (const { name, def } of desired) {
|
|
73
74
|
const current = currentServerSectionText(content, name);
|
|
@@ -137,7 +138,7 @@ export class CodexAdapter {
|
|
|
137
138
|
return { ok: false, mismatches: ["codex is not present on this machine"] };
|
|
138
139
|
}
|
|
139
140
|
const mismatches = [];
|
|
140
|
-
const desiredNames = new Set(canonical.
|
|
141
|
+
const desiredNames = new Set(globalLayerSkills(canonical).filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
141
142
|
const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
|
|
142
143
|
for (const name of desiredNames) {
|
|
143
144
|
if (!actualSkills.has(name)) {
|
|
@@ -16,6 +16,7 @@ import { applyJsonMcp, planJsonMcp, renderJsonServerEntry } from "./jsonMcp.js";
|
|
|
16
16
|
import { RUNTIME_COMMAND, RUNTIME_ENTRY_NAME } from "./mcpPlan.js";
|
|
17
17
|
import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
|
|
18
18
|
import { loadMcpOwnership, ownedByAgent, recordOwned, forgetOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
19
|
+
import { globalLayerMcp, globalLayerSkills } from "../lib/workspaceMerge.js";
|
|
19
20
|
const KIMI_RUNTIME_STARTUP_TIMEOUT_MS = 30_000;
|
|
20
21
|
function renderKimiServerEntry(def, name) {
|
|
21
22
|
const entry = renderJsonServerEntry(def);
|
|
@@ -46,7 +47,7 @@ export class KimiCodeAdapter {
|
|
|
46
47
|
items.push(...planSymlinks({
|
|
47
48
|
rootDir: join(root, "skills"),
|
|
48
49
|
desired: native
|
|
49
|
-
? canonical
|
|
50
|
+
? globalLayerSkills(canonical)
|
|
50
51
|
.filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
51
52
|
.map((skill) => ({ name: skill.name, target: skill.dir }))
|
|
52
53
|
: [],
|
|
@@ -65,7 +66,7 @@ export class KimiCodeAdapter {
|
|
|
65
66
|
items.push(...planJsonMcp({
|
|
66
67
|
configPath,
|
|
67
68
|
parsed,
|
|
68
|
-
mcp: canonical
|
|
69
|
+
mcp: globalLayerMcp(canonical),
|
|
69
70
|
agentId: this.id,
|
|
70
71
|
managedAgents: canonical.managedAgents,
|
|
71
72
|
policy: canonical.secretsPolicy,
|
|
@@ -96,7 +97,7 @@ export class KimiCodeAdapter {
|
|
|
96
97
|
const snapshot = await kimiProbe.probe(this.homeDir);
|
|
97
98
|
if (!snapshot.present)
|
|
98
99
|
return { ok: false, mismatches: ["kimi-code is not present on this machine"] };
|
|
99
|
-
const desired = new Set(canonical.
|
|
100
|
+
const desired = new Set(globalLayerSkills(canonical).filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents)).map((skill) => skill.name));
|
|
100
101
|
const actual = new Set(snapshot.skillRoots.flatMap((root) => root.skills).filter((skill) => skill.dir.includes("/.kimi-code/skills/")).map((skill) => skill.name));
|
|
101
102
|
const mismatches = [
|
|
102
103
|
...[...desired].filter((name) => !actual.has(name)).map((name) => `skill "${name}" is missing on Kimi Code`),
|
package/dist/adapters/kiro.js
CHANGED
|
@@ -26,6 +26,7 @@ import { applyJsonMcp, planJsonMcp, renderJsonServerEntry } from "./jsonMcp.js";
|
|
|
26
26
|
import { resolveMcpPlan } from "./mcpPlan.js";
|
|
27
27
|
import { declaredEnvNames } from "../lib/envVarNames.js";
|
|
28
28
|
import { loadMcpOwnership, ownedByAgent, recordOwned, forgetOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
29
|
+
import { globalLayerMcp, globalLayerSkills } from "../lib/workspaceMerge.js";
|
|
29
30
|
/** VS-Code-family global settings path. macOS only — see
|
|
30
31
|
* trellis-kiro-approved-env-vars proposal.md Non-Goals: Linux/Windows
|
|
31
32
|
* equivalents are the well-known convention but unverified against a
|
|
@@ -48,7 +49,7 @@ export class KiroAdapter {
|
|
|
48
49
|
async plan(canonical) {
|
|
49
50
|
const canonicalRoot = dirname(canonical.instructionsFile);
|
|
50
51
|
const skillsRoot = join(this.homeDir, ".kiro", "skills");
|
|
51
|
-
const desiredSkills = canonical
|
|
52
|
+
const desiredSkills = globalLayerSkills(canonical)
|
|
52
53
|
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
53
54
|
.map((skill) => ({ name: skill.name, target: skill.dir }));
|
|
54
55
|
const skillItems = planSymlinks({
|
|
@@ -71,13 +72,13 @@ export class KiroAdapter {
|
|
|
71
72
|
const configPath = join(this.homeDir, ".kiro", "settings", "mcp.json");
|
|
72
73
|
const parsed = existsSync(configPath) ? JSON.parse(readFileSync(configPath, "utf-8")) : undefined;
|
|
73
74
|
const ownership = ownedByAgent(loadMcpOwnership(this.homeDir), this.id);
|
|
74
|
-
return planJsonMcp({ configPath, parsed, mcp: canonical
|
|
75
|
+
return planJsonMcp({ configPath, parsed, mcp: globalLayerMcp(canonical), agentId: this.id, managedAgents: canonical.managedAgents, policy: canonical.secretsPolicy, ownership });
|
|
75
76
|
}
|
|
76
77
|
/** Names come from `resolveMcpPlan`, not a raw scan of `canonical.mcp.servers`
|
|
77
78
|
* (design.md D2) — a server scoped away from Kiro, or refused for a
|
|
78
79
|
* known_host_injected collision, never contributes a name here either. */
|
|
79
80
|
desiredApprovedEnvVars(canonical) {
|
|
80
|
-
const { desired } = resolveMcpPlan(this.id, canonical
|
|
81
|
+
const { desired } = resolveMcpPlan(this.id, globalLayerMcp(canonical), canonical.managedAgents, canonical.secretsPolicy);
|
|
81
82
|
const names = new Set();
|
|
82
83
|
for (const { def } of desired) {
|
|
83
84
|
for (const name of declaredEnvNames(def))
|
|
@@ -162,7 +163,7 @@ export class KiroAdapter {
|
|
|
162
163
|
return { ok: false, mismatches: ["kiro is not present on this machine"] };
|
|
163
164
|
}
|
|
164
165
|
const mismatches = [];
|
|
165
|
-
const desiredNames = new Set(canonical.
|
|
166
|
+
const desiredNames = new Set(globalLayerSkills(canonical).filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
166
167
|
const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
|
|
167
168
|
for (const name of desiredNames) {
|
|
168
169
|
if (!actualSkills.has(name)) {
|
package/dist/adapters/pi.js
CHANGED
|
@@ -36,6 +36,7 @@ import { applyJsonMcp, planJsonMcp } from "./jsonMcp.js";
|
|
|
36
36
|
import { piRenderer, supportsNativeMcp } from "./piMcp.js";
|
|
37
37
|
import { loadMcpOwnership, ownedByAgent, recordOwned, forgetOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
38
38
|
import { currentLinkTarget } from "../lib/backup.js";
|
|
39
|
+
import { globalLayerMcp, globalLayerSkills } from "../lib/workspaceMerge.js";
|
|
39
40
|
const BRIDGE_SYMLINK_NAME = "trellis-mcp-bridge.js";
|
|
40
41
|
/**
|
|
41
42
|
* A source-checkout bridge can survive a switch to the packaged CLI. Adopt it
|
|
@@ -97,7 +98,7 @@ export class PiAdapter {
|
|
|
97
98
|
async plan(canonical) {
|
|
98
99
|
const canonicalRoot = dirname(canonical.instructionsFile);
|
|
99
100
|
const agentDir = join(this.homeDir, ".pi", "agent");
|
|
100
|
-
const desiredSkills = canonical
|
|
101
|
+
const desiredSkills = globalLayerSkills(canonical)
|
|
101
102
|
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
102
103
|
.map((skill) => ({ name: skill.name, target: skill.dir }));
|
|
103
104
|
const skillItems = planSymlinks({
|
|
@@ -146,7 +147,7 @@ export class PiAdapter {
|
|
|
146
147
|
const items = planJsonMcp({
|
|
147
148
|
configPath,
|
|
148
149
|
parsed,
|
|
149
|
-
mcp: canonical
|
|
150
|
+
mcp: globalLayerMcp(canonical),
|
|
150
151
|
agentId: this.id,
|
|
151
152
|
managedAgents: canonical.managedAgents,
|
|
152
153
|
policy: canonical.secretsPolicy,
|
|
@@ -233,7 +234,7 @@ export class PiAdapter {
|
|
|
233
234
|
return { ok: false, mismatches: ["pi is not present on this machine"] };
|
|
234
235
|
}
|
|
235
236
|
const mismatches = [];
|
|
236
|
-
const desiredNames = new Set(canonical.
|
|
237
|
+
const desiredNames = new Set(globalLayerSkills(canonical).filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
237
238
|
const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
|
|
238
239
|
for (const name of desiredNames) {
|
|
239
240
|
if (!actualSkills.has(name)) {
|
package/dist/adapters/zcode.d.ts
CHANGED
|
@@ -6,8 +6,18 @@
|
|
|
6
6
|
* native Skill switches through a ledger-backed, reversible config change.
|
|
7
7
|
*/
|
|
8
8
|
import type { AdapterPlanItem, AdapterProbeResult, AdapterVerifyResult, TrellisAdapter } from "../core/adapter.js";
|
|
9
|
-
import type { CanonicalSource } from "../core/types.js";
|
|
9
|
+
import type { CanonicalSource, McpConfig, SecretsPolicy } from "../core/types.js";
|
|
10
10
|
import type { BackupSession } from "../lib/backup.js";
|
|
11
|
+
type JsonRecord = Record<string, unknown>;
|
|
12
|
+
export declare function planZcodeMcp(opts: {
|
|
13
|
+
configPath: string;
|
|
14
|
+
parsed: JsonRecord | undefined;
|
|
15
|
+
mcp: McpConfig;
|
|
16
|
+
managedAgents: readonly import("../core/types.js").AgentId[];
|
|
17
|
+
policy: SecretsPolicy;
|
|
18
|
+
ownership: JsonRecord;
|
|
19
|
+
}): AdapterPlanItem[];
|
|
20
|
+
export declare function applyZcodeMcp(parsed: JsonRecord | undefined, items: readonly AdapterPlanItem[]): JsonRecord;
|
|
11
21
|
export declare class ZcodeAdapter implements TrellisAdapter {
|
|
12
22
|
private readonly homeDir;
|
|
13
23
|
private readonly env;
|
|
@@ -19,3 +29,4 @@ export declare class ZcodeAdapter implements TrellisAdapter {
|
|
|
19
29
|
apply(plan: AdapterPlanItem[], backup: BackupSession): Promise<void>;
|
|
20
30
|
verify(canonical: CanonicalSource): Promise<AdapterVerifyResult>;
|
|
21
31
|
}
|
|
32
|
+
export {};
|
package/dist/adapters/zcode.js
CHANGED
|
@@ -17,6 +17,7 @@ import { deepEqual } from "../lib/deepEqual.js";
|
|
|
17
17
|
import { forgetOwned, loadMcpOwnership, ownedByAgent, recordOwned, saveMcpOwnership } from "../lib/mcpOwnership.js";
|
|
18
18
|
import { forgetZcodeSettingsOwnership, loadZcodeSettingsOwnership, saveZcodeSettingsOwnership } from "../lib/zcodeOwnership.js";
|
|
19
19
|
import * as zcodeProbe from "../probes/zcode.js";
|
|
20
|
+
import { globalLayerMcp, globalLayerSkills } from "../lib/workspaceMerge.js";
|
|
20
21
|
function readConfig(path) {
|
|
21
22
|
if (!existsSync(path))
|
|
22
23
|
return undefined;
|
|
@@ -28,7 +29,7 @@ function record(value) {
|
|
|
28
29
|
function zcodeServers(parsed) {
|
|
29
30
|
return record(record(parsed?.mcp).servers);
|
|
30
31
|
}
|
|
31
|
-
function planZcodeMcp(opts) {
|
|
32
|
+
export function planZcodeMcp(opts) {
|
|
32
33
|
const { desired, conflicts } = resolveMcpPlan("zcode", opts.mcp, opts.managedAgents, opts.policy);
|
|
33
34
|
const existing = zcodeServers(opts.parsed);
|
|
34
35
|
const items = [];
|
|
@@ -65,7 +66,7 @@ function planZcodeMcp(opts) {
|
|
|
65
66
|
}
|
|
66
67
|
return items;
|
|
67
68
|
}
|
|
68
|
-
function applyZcodeMcp(parsed, items) {
|
|
69
|
+
export function applyZcodeMcp(parsed, items) {
|
|
69
70
|
const base = parsed ?? {};
|
|
70
71
|
const mcp = record(base.mcp);
|
|
71
72
|
const servers = { ...record(mcp.servers) };
|
|
@@ -124,7 +125,7 @@ export class ZcodeAdapter {
|
|
|
124
125
|
items.push(...planSymlinks({
|
|
125
126
|
rootDir: join(this.homeDir, ".zcode", "skills"),
|
|
126
127
|
desired: usesNativeCapabilityDelivery(this.id, canonical.mcp)
|
|
127
|
-
? canonical.
|
|
128
|
+
? globalLayerSkills(canonical).filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents)).map((skill) => ({ name: skill.name, target: skill.dir }))
|
|
128
129
|
: [],
|
|
129
130
|
canonicalRoot: join(canonicalRoot, "skills"),
|
|
130
131
|
kind: "skill",
|
|
@@ -138,7 +139,7 @@ export class ZcodeAdapter {
|
|
|
138
139
|
items.push(...planZcodeMcp({
|
|
139
140
|
configPath: profile.configPath,
|
|
140
141
|
parsed,
|
|
141
|
-
mcp: canonical
|
|
142
|
+
mcp: globalLayerMcp(canonical),
|
|
142
143
|
managedAgents: canonical.managedAgents,
|
|
143
144
|
policy: canonical.secretsPolicy,
|
|
144
145
|
ownership: ownedByAgent(loadMcpOwnership(this.homeDir), this.id),
|
|
@@ -206,7 +207,7 @@ export class ZcodeAdapter {
|
|
|
206
207
|
if (!profile)
|
|
207
208
|
return { ok: false, mismatches: ["zcode profile cannot be selected"] };
|
|
208
209
|
const mismatches = [];
|
|
209
|
-
const desiredNativeSkills = canonical
|
|
210
|
+
const desiredNativeSkills = globalLayerSkills(canonical)
|
|
210
211
|
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
211
212
|
.map((skill) => skill.name);
|
|
212
213
|
const actualNativeSkills = new Set(snapshot.skillRoots.filter((root) => root.path === join(this.homeDir, ".zcode", "skills")).flatMap((root) => root.skills.map((skill) => skill.name)));
|
|
@@ -216,7 +217,7 @@ export class ZcodeAdapter {
|
|
|
216
217
|
const expectedMcp = planZcodeMcp({
|
|
217
218
|
configPath: profile.configPath,
|
|
218
219
|
parsed: readConfig(profile.configPath),
|
|
219
|
-
mcp: canonical
|
|
220
|
+
mcp: globalLayerMcp(canonical),
|
|
220
221
|
managedAgents: canonical.managedAgents,
|
|
221
222
|
policy: canonical.secretsPolicy,
|
|
222
223
|
ownership: ownedByAgent(loadMcpOwnership(this.homeDir), this.id),
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { runDoctor } from "./commands/doctor.js";
|
|
8
8
|
import { runInit } from "./commands/init.js";
|
|
9
9
|
import { runMigrate } from "./commands/migrate.js";
|
|
10
|
+
import { runMigrateWorkspace } from "./commands/migrateWorkspace.js";
|
|
10
11
|
import { runOnboard } from "./commands/onboard.js";
|
|
11
12
|
import { runSync } from "./commands/sync.js";
|
|
12
13
|
import { runMcpSync, runMcpList, runMcpAdd, runMcpRemove, runMcpSetAuth, runMcpScope, parseMcpAddArgs, parseMcpSetArgs } from "./commands/mcp.js";
|
|
@@ -74,6 +75,7 @@ Commands:
|
|
|
74
75
|
(never overwrites an existing file — fills in only what's
|
|
75
76
|
missing) and prints which agents are present
|
|
76
77
|
--json machine-readable output, no report text
|
|
78
|
+
--workspace scaffold .agents/ in the current project instead
|
|
77
79
|
migrate --from <agent>
|
|
78
80
|
Import an existing agent's real skills/instructions/MCP/memory
|
|
79
81
|
servers into canonical source (claude-code | codex | kiro |
|
|
@@ -84,6 +86,18 @@ Commands:
|
|
|
84
86
|
restrict to one category
|
|
85
87
|
--dry-run preview the plan, write nothing
|
|
86
88
|
--json machine-readable output, no report text
|
|
89
|
+
migrate --workspace
|
|
90
|
+
Absorb the repo's existing project-level agent config (all six
|
|
91
|
+
agents' MCP files + root AGENTS.md) into .agents/, then
|
|
92
|
+
re-render. Plaintext secrets are intercepted into the global
|
|
93
|
+
secret store as \${VAR} references, never written under
|
|
94
|
+
.agents/; *.local.* files are listed, never read.
|
|
95
|
+
--choose name=agentId pick a side for a cross-agent conflict
|
|
96
|
+
--dedup drop|override identical-to-global entries: skip
|
|
97
|
+
(default) or import as an intentional
|
|
98
|
+
shadow
|
|
99
|
+
--dry-run preview the plan, write nothing
|
|
100
|
+
--json machine-readable output, no report text
|
|
87
101
|
doctor Scan Claude Code / Codex / Kiro / pi for drift
|
|
88
102
|
--json machine-readable output, no table text
|
|
89
103
|
--probe-mcp also handshake every configured MCP server
|
|
@@ -123,6 +137,7 @@ Commands:
|
|
|
123
137
|
no overwrite): --command <cmd> [--args a,b] (stdio) or
|
|
124
138
|
--url <url> (http/sse); [--headers k=v,...]
|
|
125
139
|
[--auth oauth] [--env NAME,...] [--static-env k=v,...] [--agents id,...]
|
|
140
|
+
[--workspace] write to .agents/config.json instead of servers.yaml
|
|
126
141
|
[--enabled true|false]
|
|
127
142
|
--dry-run preview the plan, write nothing
|
|
128
143
|
--json machine-readable output, no report text
|
|
@@ -186,6 +201,7 @@ Commands:
|
|
|
186
201
|
skill add <name> --from <path>
|
|
187
202
|
Import a real skill directory into canonical (refuses on
|
|
188
203
|
an existing name with different content, no overwrite)
|
|
204
|
+
--workspace copy into .agents/skills/ instead of canonical
|
|
189
205
|
--dry-run preview the plan, write nothing
|
|
190
206
|
--json machine-readable output, no report text
|
|
191
207
|
skill remove <name>
|
|
@@ -341,11 +357,28 @@ async function main(argv) {
|
|
|
341
357
|
return;
|
|
342
358
|
}
|
|
343
359
|
if (command === "init") {
|
|
344
|
-
const { exitCode } = await runInit({ json: rest.includes("--json") });
|
|
360
|
+
const { exitCode } = await runInit({ json: rest.includes("--json"), workspace: rest.includes("--workspace") });
|
|
345
361
|
process.exitCode = exitCode;
|
|
346
362
|
return;
|
|
347
363
|
}
|
|
348
364
|
if (command === "migrate") {
|
|
365
|
+
if (rest.includes("--workspace")) {
|
|
366
|
+
// Repeated `--choose name=agentId` flags collect into an array.
|
|
367
|
+
const choose = [];
|
|
368
|
+
for (let i = 0; i < rest.length; i++) {
|
|
369
|
+
if (rest[i] === "--choose" && rest[i + 1] !== undefined)
|
|
370
|
+
choose.push(rest[i + 1]);
|
|
371
|
+
}
|
|
372
|
+
const dedupIndex = rest.indexOf("--dedup");
|
|
373
|
+
const { exitCode } = await runMigrateWorkspace({
|
|
374
|
+
choose,
|
|
375
|
+
dedup: dedupIndex >= 0 ? rest[dedupIndex + 1] : undefined,
|
|
376
|
+
dryRun: rest.includes("--dry-run"),
|
|
377
|
+
json: rest.includes("--json"),
|
|
378
|
+
});
|
|
379
|
+
process.exitCode = exitCode;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
349
382
|
const fromIndex = rest.indexOf("--from");
|
|
350
383
|
const from = fromIndex >= 0 ? rest[fromIndex + 1] : undefined;
|
|
351
384
|
const onlyIndex = rest.indexOf("--only");
|
|
@@ -383,7 +416,7 @@ async function main(argv) {
|
|
|
383
416
|
process.exitCode = 1;
|
|
384
417
|
return;
|
|
385
418
|
}
|
|
386
|
-
const { exitCode } = await runSync({ target, json: rest.includes("--json"), dryRun: rest.includes("--dry-run") });
|
|
419
|
+
const { exitCode } = await runSync({ target, json: rest.includes("--json"), dryRun: rest.includes("--dry-run"), check: rest.includes("--check"), globalOnly: rest.includes("--global-only") });
|
|
387
420
|
process.exitCode = exitCode;
|
|
388
421
|
return;
|
|
389
422
|
}
|
|
@@ -443,7 +476,7 @@ async function main(argv) {
|
|
|
443
476
|
const json = mcpRest.includes("--json");
|
|
444
477
|
const dryRun = mcpRest.includes("--dry-run");
|
|
445
478
|
if (subcommand === "sync") {
|
|
446
|
-
const { exitCode } = await runMcpSync({ json, dryRun });
|
|
479
|
+
const { exitCode } = await runMcpSync({ json, dryRun, check: mcpRest.includes("--check"), globalOnly: mcpRest.includes("--global-only") });
|
|
447
480
|
process.exitCode = exitCode;
|
|
448
481
|
return;
|
|
449
482
|
}
|
|
@@ -453,7 +486,7 @@ async function main(argv) {
|
|
|
453
486
|
}
|
|
454
487
|
if (subcommand === "add") {
|
|
455
488
|
const [name] = mcpRest;
|
|
456
|
-
process.exitCode = (await runMcpAdd(name, parseMcpAddArgs(mcpRest), { json, dryRun })).exitCode;
|
|
489
|
+
process.exitCode = (await runMcpAdd(name, parseMcpAddArgs(mcpRest), { json, dryRun, workspace: mcpRest.includes("--workspace") })).exitCode;
|
|
457
490
|
return;
|
|
458
491
|
}
|
|
459
492
|
if (subcommand === "import") {
|
|
@@ -518,7 +551,7 @@ async function main(argv) {
|
|
|
518
551
|
process.exitCode = 1;
|
|
519
552
|
return;
|
|
520
553
|
}
|
|
521
|
-
process.exitCode = (await runSkillAdd(name, from, { json, dryRun })).exitCode;
|
|
554
|
+
process.exitCode = (await runSkillAdd(name, from, { json, dryRun, workspace: skillRest.includes("--workspace") })).exitCode;
|
|
522
555
|
return;
|
|
523
556
|
}
|
|
524
557
|
if (subcommand === "remove") {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { loadCanonicalSource } from "../core/canonical.js";
|
|
8
8
|
import type { AgentId, AgentSnapshot, CanonicalSource } from "../core/types.js";
|
|
9
|
+
import { type WorkspaceDoctorSection } from "../lib/workspaceDoctor.js";
|
|
9
10
|
/**
|
|
10
11
|
* Interim default until P1 wires this from `.trellis/mcp/servers.yaml`'s
|
|
11
12
|
* real `known_host_injected` field (P0 has no canonical source at all —
|
|
@@ -47,6 +48,10 @@ export interface RunDoctorOptions {
|
|
|
47
48
|
/** Defaults to the real `~`; overridable for tests only — same seam
|
|
48
49
|
* every other Trellis entry point uses. */
|
|
49
50
|
homeDir?: string;
|
|
51
|
+
/** Explicit workspace root for layer attribution — the test/sandbox
|
|
52
|
+
* seam. CLI discovery walks up from cwd via `findWorkspaceRoot`;
|
|
53
|
+
* null = no workspace layer. */
|
|
54
|
+
workspaceDir?: string | null;
|
|
50
55
|
}
|
|
51
56
|
export interface OAuthHealthEntry {
|
|
52
57
|
server: string;
|
|
@@ -61,6 +66,9 @@ export interface DoctorReport {
|
|
|
61
66
|
* server is OAuth-classified. Read-only by construction
|
|
62
67
|
* (trellis-mcp-oauth-renew design.md D4). */
|
|
63
68
|
oauth?: OAuthHealthEntry[];
|
|
69
|
+
/** Per-entry layer attribution when a `.agents/` workspace layer
|
|
70
|
+
* participated (trellis-workspace-scope task 3.5). */
|
|
71
|
+
workspace?: WorkspaceDoctorSection;
|
|
64
72
|
}
|
|
65
73
|
/** Exported so tests can assert on this resolution directly, without
|
|
66
74
|
* routing through real per-agent probes `runDoctor` also runs. */
|
|
@@ -76,7 +84,7 @@ export declare function runDoctor(opts?: RunDoctorOptions): Promise<{
|
|
|
76
84
|
* existing call site (which now passes `opts.homeDir`, itself normally
|
|
77
85
|
* `undefined`) is unaffected.
|
|
78
86
|
*/
|
|
79
|
-
export declare function collectDoctorReport(homeDir?: string, knownHostInjected?: readonly string[], probeMcp?: boolean): Promise<DoctorReport>;
|
|
87
|
+
export declare function collectDoctorReport(homeDir?: string, knownHostInjected?: readonly string[], probeMcp?: boolean, workspaceDir?: string | null): Promise<DoctorReport>;
|
|
80
88
|
/**
|
|
81
89
|
* Read-only OAuth credential health (trellis-mcp-oauth-renew design.md
|
|
82
90
|
* D4): classified from the stored token file alone — no token endpoint,
|
package/dist/commands/doctor.js
CHANGED
|
@@ -16,6 +16,8 @@ import { GATEWAY_ENTRY_NAME, RUNTIME_ENTRY_NAME, resolveMcpPlan } from "../adapt
|
|
|
16
16
|
import { ALL_AGENTS, isOAuthAuth, oauthOwner } from "../core/types.js";
|
|
17
17
|
import { classifyStoredToken, remediationFor } from "../lib/oauth/renew.js";
|
|
18
18
|
import { readToken } from "../lib/oauth/store.js";
|
|
19
|
+
import { findWorkspaceRoot } from "../lib/workspace.js";
|
|
20
|
+
import { collectWorkspaceDoctorSection } from "../lib/workspaceDoctor.js";
|
|
19
21
|
/**
|
|
20
22
|
* Interim default until P1 wires this from `.trellis/mcp/servers.yaml`'s
|
|
21
23
|
* real `known_host_injected` field (P0 has no canonical source at all —
|
|
@@ -58,7 +60,8 @@ export function resolveKnownHostInjected(opts) {
|
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
export async function runDoctor(opts = {}) {
|
|
61
|
-
const
|
|
63
|
+
const workspaceDir = opts.workspaceDir !== undefined ? opts.workspaceDir : findWorkspaceRoot(process.cwd());
|
|
64
|
+
const report = await collectDoctorReport(opts.homeDir, resolveKnownHostInjected(opts), opts.probeMcp, workspaceDir);
|
|
62
65
|
const exitCode = report.findings.some((f) => EXIT_NONZERO_KINDS.has(f.kind)) ? 1 : 0;
|
|
63
66
|
if (opts.json) {
|
|
64
67
|
console.log(JSON.stringify(report, null, 2));
|
|
@@ -76,7 +79,7 @@ export async function runDoctor(opts = {}) {
|
|
|
76
79
|
* existing call site (which now passes `opts.homeDir`, itself normally
|
|
77
80
|
* `undefined`) is unaffected.
|
|
78
81
|
*/
|
|
79
|
-
export async function collectDoctorReport(homeDir = homedir(), knownHostInjected = DEFAULT_KNOWN_HOST_INJECTED, probeMcp = false) {
|
|
82
|
+
export async function collectDoctorReport(homeDir = homedir(), knownHostInjected = DEFAULT_KNOWN_HOST_INJECTED, probeMcp = false, workspaceDir) {
|
|
80
83
|
const probeList = [
|
|
81
84
|
{ agent: "claude-code", run: () => claudeCodeProbe.probe(homeDir, { probeMcp }) },
|
|
82
85
|
{ agent: "codex", run: () => codexProbe.probe(homeDir, { probeMcp }) },
|
|
@@ -103,15 +106,24 @@ export async function collectDoctorReport(homeDir = homedir(), knownHostInjected
|
|
|
103
106
|
});
|
|
104
107
|
let canonical;
|
|
105
108
|
try {
|
|
106
|
-
|
|
109
|
+
// Workspace-merged when a layer is in play — doctor's attribution is
|
|
110
|
+
// the merged view, and a broken workspace config degrades doctor to
|
|
111
|
+
// the global-only view rather than failing the whole scan.
|
|
112
|
+
canonical = loadCanonicalSource({ homeDir, workspaceDir: workspaceDir ?? null });
|
|
107
113
|
}
|
|
108
114
|
catch {
|
|
109
|
-
|
|
110
|
-
|
|
115
|
+
try {
|
|
116
|
+
canonical = loadCanonicalSource(homeDir);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Doctor remains useful as a standalone cross-agent scanner when
|
|
120
|
+
// canonical has not been initialized yet.
|
|
121
|
+
}
|
|
111
122
|
}
|
|
123
|
+
const workspace = canonical?.workspace ? collectWorkspaceDoctorSection(canonical) : undefined;
|
|
112
124
|
const oauth = canonical ? collectOAuthHealth(homeDir, canonical) : [];
|
|
113
125
|
findings.push(...detectDuplication(snapshots), ...detectCollisions(snapshots, knownHostInjected), ...detectCrossAgentDrift(snapshots), ...detectCaseMismatches(snapshots), ...detectParseDiagnostics(snapshots), ...(canonical ? detectRuntimeDrift(snapshots, canonical) : []), ...detectMcpUnreachable(snapshots), ...oauth.flatMap((entry) => (entry.status === "valid" ? [] : [oauthFinding(entry)])));
|
|
114
|
-
return { snapshots, findings, ...(oauth.length > 0 ? { oauth } : {}) };
|
|
126
|
+
return { snapshots, findings, ...(oauth.length > 0 ? { oauth } : {}), ...(workspace ? { workspace } : {}) };
|
|
115
127
|
}
|
|
116
128
|
/**
|
|
117
129
|
* Read-only OAuth credential health (trellis-mcp-oauth-renew design.md
|
|
@@ -362,4 +374,29 @@ export function printReport(report) {
|
|
|
362
374
|
console.log(`⚠️ ${finding.message}`);
|
|
363
375
|
}
|
|
364
376
|
}
|
|
377
|
+
if (report.workspace) {
|
|
378
|
+
const ws = report.workspace;
|
|
379
|
+
console.log(`\nWorkspace layer: ${ws.root} (.agents/)`);
|
|
380
|
+
for (const entry of ws.mcp) {
|
|
381
|
+
console.log(` mcp ${entry.name} — ${entry.layer}`);
|
|
382
|
+
}
|
|
383
|
+
for (const entry of ws.skills) {
|
|
384
|
+
console.log(` skill ${entry.name} — ${entry.layer}`);
|
|
385
|
+
}
|
|
386
|
+
if (ws.shadowed.length > 0) {
|
|
387
|
+
console.log(` shadowed by workspace: ${ws.shadowed.join(", ")}`);
|
|
388
|
+
}
|
|
389
|
+
if (ws.tombstoned.length > 0) {
|
|
390
|
+
console.log(` tombstoned (disabled for this project): ${ws.tombstoned.join(", ")}`);
|
|
391
|
+
}
|
|
392
|
+
for (const ref of ws.unresolvedVars) {
|
|
393
|
+
console.log(` ⚠ ${ref.server} ${ref.field}: \${${ref.name}} does not resolve under the current secrets policy`);
|
|
394
|
+
}
|
|
395
|
+
for (const hint of ws.unmigrated) {
|
|
396
|
+
console.log(` ⚠ ${hint.path} holds ${hint.entries.length} entr${hint.entries.length === 1 ? "y" : "ies"} Trellis didn't render (${hint.entries.join(", ")}) — run \`trellis migrate --workspace\``);
|
|
397
|
+
}
|
|
398
|
+
for (const warning of ws.precedenceWarnings) {
|
|
399
|
+
console.log(` ⚠ ${warning}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
365
402
|
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export interface RunInitOptions {
|
|
|
12
12
|
/** Defaults to the real `~`; overridable for tests only — same seam
|
|
13
13
|
* every other Trellis entry point uses. */
|
|
14
14
|
homeDir?: string;
|
|
15
|
+
/** `--workspace`: scaffold `.agents/` in the project instead of the
|
|
16
|
+
* global `~/.trellis/` (trellis-workspace-scope task 3.3). */
|
|
17
|
+
workspace?: boolean;
|
|
18
|
+
/** Workspace scaffold target — the test/sandbox seam. CLI discovery is
|
|
19
|
+
* simply `process.cwd()`. */
|
|
20
|
+
workspaceDir?: string;
|
|
15
21
|
}
|
|
16
22
|
export interface InitFileResult {
|
|
17
23
|
path: string;
|
package/dist/commands/init.js
CHANGED
|
@@ -18,6 +18,7 @@ import * as piProbe from "../probes/pi.js";
|
|
|
18
18
|
import * as kimiCodeProbe from "../probes/kimi-code.js";
|
|
19
19
|
import * as zcodeProbe from "../probes/zcode.js";
|
|
20
20
|
import { ALL_AGENTS } from "../core/types.js";
|
|
21
|
+
import { scaffoldWorkspace } from "../lib/workspaceConfig.js";
|
|
21
22
|
/** Exported so `migrate` can byte-compare canonical `agents.md` against
|
|
22
23
|
* this exact string (design.md D3, trellis-cli-migrate) rather than
|
|
23
24
|
* retyping an equivalent one that could silently drift from this. */
|
|
@@ -148,6 +149,29 @@ export async function collectInitReport(homeDir = homedir(), options = {}) {
|
|
|
148
149
|
return { files, agents };
|
|
149
150
|
}
|
|
150
151
|
export async function runInit(opts = {}) {
|
|
152
|
+
if (opts.workspace) {
|
|
153
|
+
const dir = opts.workspaceDir ?? process.cwd();
|
|
154
|
+
const files = scaffoldWorkspace(dir);
|
|
155
|
+
if (opts.json) {
|
|
156
|
+
console.log(JSON.stringify({ workspace: dir, files }, null, 2));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const created = files.filter((f) => f.action === "create");
|
|
160
|
+
if (created.length === 0) {
|
|
161
|
+
console.log(`${dir}/.agents already fully initialized — nothing to create.`);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
console.log(`Created ${created.length} file(s) under ${dir}/.agents:`);
|
|
165
|
+
for (const f of created)
|
|
166
|
+
console.log(` - ${f.path}`);
|
|
167
|
+
}
|
|
168
|
+
for (const f of files.filter((f) => f.action === "already-present")) {
|
|
169
|
+
console.log(` - ${f.path} (already present, left untouched)`);
|
|
170
|
+
}
|
|
171
|
+
console.log("\nNext: commit .agents/ to share it, then `trellis sync` renders per-agent project files.");
|
|
172
|
+
}
|
|
173
|
+
return { exitCode: 0 };
|
|
174
|
+
}
|
|
151
175
|
const homeDir = opts.homeDir ?? homedir();
|
|
152
176
|
const report = await collectInitReport(homeDir);
|
|
153
177
|
if (opts.json) {
|
package/dist/commands/mcp.d.ts
CHANGED
|
@@ -33,14 +33,23 @@ export interface RunMcpSyncOptions {
|
|
|
33
33
|
/** Onboard-only seam — see RunSyncOptions.backupSession. Never a CLI
|
|
34
34
|
* flag. */
|
|
35
35
|
backupSession?: BackupSession;
|
|
36
|
+
/** Force global-only behavior even inside a workspace (`--global-only`). */
|
|
37
|
+
globalOnly?: boolean;
|
|
38
|
+
/** Explicit workspace root — the test/sandbox seam (see RunSyncOptions). */
|
|
39
|
+
workspaceDir?: string | null;
|
|
40
|
+
/** `--check`: plan only, write nothing, exit non-zero on any stale or
|
|
41
|
+
* missing rendered artifact (trellis-workspace-scope design D7). */
|
|
42
|
+
check?: boolean;
|
|
36
43
|
}
|
|
37
44
|
export interface AgentMcpSyncReport {
|
|
38
45
|
agent: AgentId;
|
|
39
46
|
present: boolean;
|
|
40
47
|
items: AdapterPlanItem[];
|
|
48
|
+
warnings?: string[];
|
|
41
49
|
}
|
|
42
50
|
export interface McpSyncReport {
|
|
43
51
|
reports: AgentMcpSyncReport[];
|
|
52
|
+
workspaceRoot?: string;
|
|
44
53
|
}
|
|
45
54
|
export declare function collectMcpSyncReport(opts?: RunMcpSyncOptions): Promise<McpSyncReport>;
|
|
46
55
|
export declare function runMcpSync(opts?: RunMcpSyncOptions): Promise<{
|
|
@@ -49,7 +58,7 @@ export declare function runMcpSync(opts?: RunMcpSyncOptions): Promise<{
|
|
|
49
58
|
/** Exported so `onboard` prints an mcp-sync report identically to running
|
|
50
59
|
* `mcp sync` standalone, instead of a second, easily-drifting copy of this
|
|
51
60
|
* formatting. */
|
|
52
|
-
export declare function printReport(report: McpSyncReport, dryRun: boolean): void;
|
|
61
|
+
export declare function printReport(report: McpSyncReport, dryRun: boolean, check?: boolean): void;
|
|
53
62
|
export interface McpListEntry {
|
|
54
63
|
name: string;
|
|
55
64
|
transport: Transport;
|
|
@@ -147,8 +156,30 @@ export interface McpAddPlan {
|
|
|
147
156
|
* write failure — design.md's open question, resolved in favor of
|
|
148
157
|
* refusing early with a specific reason. No `--force`: an existing name
|
|
149
158
|
* always refuses, ever (design.md D4).
|
|
159
|
+
*
|
|
160
|
+
* Split into two steps so `mcp add --workspace` reuses the identical
|
|
161
|
+
* flag validation/def construction and only swaps which file's existing
|
|
162
|
+
* entries it conflicts against (trellis-workspace-scope task 3.4):
|
|
163
|
+
* `buildMcpAddDef` is the pure part, `collectMcpAddPlan` adds the
|
|
164
|
+
* global-canonical existence check.
|
|
150
165
|
*/
|
|
166
|
+
export declare function buildMcpAddDef(name: string | undefined, raw: McpAddRawArgs): {
|
|
167
|
+
ok: true;
|
|
168
|
+
name: string;
|
|
169
|
+
def: McpServerDef;
|
|
170
|
+
} | {
|
|
171
|
+
ok: false;
|
|
172
|
+
plan: McpAddPlan;
|
|
173
|
+
};
|
|
151
174
|
export declare function collectMcpAddPlan(name: string | undefined, raw: McpAddRawArgs, homeDir?: string): McpAddPlan;
|
|
175
|
+
/** The workspace twin of `collectMcpAddPlan` (trellis-workspace-scope
|
|
176
|
+
* task 3.4): identical validation, but the existence check runs against
|
|
177
|
+
* `.agents/config.json` — compared through the same snake_case
|
|
178
|
+
* serialization the file stores, so a re-add of the same definition is
|
|
179
|
+
* a clean "already-present" no-op. A global same-name entry is NOT a
|
|
180
|
+
* conflict here (workspace shadowing a global entry is a legal,
|
|
181
|
+
* documented merge outcome), just noted in the detail. */
|
|
182
|
+
export declare function collectMcpAddWorkspacePlan(name: string | undefined, raw: McpAddRawArgs, workspaceRoot: string, homeDir?: string): McpAddPlan;
|
|
152
183
|
export type McpSetAuthAction = "updated" | "already-set" | "not-found" | "invalid-input";
|
|
153
184
|
export interface McpSetAuthPlan {
|
|
154
185
|
name: string;
|
|
@@ -269,10 +300,22 @@ export declare function applyMcpAddWithSync(plan: McpAddPlan, opts?: {
|
|
|
269
300
|
dryRun?: boolean;
|
|
270
301
|
backupSession?: BackupSession;
|
|
271
302
|
}): Promise<McpAddOutcome>;
|
|
303
|
+
/** Workspace twin of `applyMcpAddWithSync` (trellis-workspace-scope task
|
|
304
|
+
* 3.4): writes `.agents/config.json` instead of `servers.yaml`, then
|
|
305
|
+
* cascades the same mcp sync — which, run inside the workspace, renders
|
|
306
|
+
* only the project-level targets for the workspace layer (plus the usual
|
|
307
|
+
* global-layer targets; the agent does its own layering). */
|
|
308
|
+
export declare function applyMcpAddWorkspaceWithSync(plan: McpAddPlan, workspaceRoot: string, opts?: {
|
|
309
|
+
homeDir?: string;
|
|
310
|
+
dryRun?: boolean;
|
|
311
|
+
backupSession?: BackupSession;
|
|
312
|
+
}): Promise<McpAddOutcome>;
|
|
272
313
|
export declare function runMcpAdd(name: string | undefined, raw: McpAddRawArgs, opts?: {
|
|
273
314
|
homeDir?: string;
|
|
274
315
|
json?: boolean;
|
|
275
316
|
dryRun?: boolean;
|
|
317
|
+
workspace?: boolean;
|
|
318
|
+
workspaceDir?: string;
|
|
276
319
|
}): Promise<{
|
|
277
320
|
exitCode: number;
|
|
278
321
|
}>;
|