agent-trellis 0.7.0 → 0.8.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/README.md +97 -38
- package/dist/adapters/claude-code.js +3 -2
- package/dist/adapters/codex.js +3 -2
- package/dist/adapters/jsonMcp.d.ts +3 -1
- package/dist/adapters/jsonMcp.js +18 -4
- package/dist/adapters/kimi-code.d.ts +21 -0
- package/dist/adapters/kimi-code.js +107 -0
- package/dist/adapters/kiro.js +3 -2
- package/dist/adapters/mcpPlan.d.ts +6 -1
- package/dist/adapters/mcpPlan.js +106 -8
- package/dist/adapters/pi.js +22 -3
- package/dist/adapters/symlinkPlan.d.ts +7 -0
- package/dist/adapters/symlinkPlan.js +16 -7
- package/dist/cli.js +121 -21
- package/dist/commands/doctor.d.ts +7 -2
- package/dist/commands/doctor.js +38 -2
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.js +26 -13
- package/dist/commands/kimi.d.ts +16 -0
- package/dist/commands/kimi.js +64 -0
- package/dist/commands/manage.d.ts +39 -0
- package/dist/commands/manage.js +126 -0
- package/dist/commands/mcp.d.ts +22 -1
- package/dist/commands/mcp.js +65 -2
- package/dist/commands/mcpGateway.d.ts +6 -0
- package/dist/commands/mcpGateway.js +34 -14
- package/dist/commands/mcpImport.d.ts +27 -0
- package/dist/commands/mcpImport.js +336 -0
- package/dist/commands/memory.d.ts +5 -3
- package/dist/commands/memory.js +12 -8
- package/dist/commands/migrate.d.ts +16 -5
- package/dist/commands/migrate.js +90 -17
- package/dist/commands/onboard.d.ts +74 -2
- package/dist/commands/onboard.js +458 -71
- package/dist/commands/rollback.js +4 -1
- package/dist/commands/skill.d.ts +17 -0
- package/dist/commands/skill.js +47 -2
- package/dist/commands/sync.js +2 -0
- package/dist/core/canonical.d.ts +15 -7
- package/dist/core/canonical.js +101 -13
- package/dist/core/capabilitySelection.d.ts +45 -0
- package/dist/core/capabilitySelection.js +124 -0
- package/dist/core/types.d.ts +25 -2
- package/dist/core/types.js +7 -0
- package/dist/lib/backup.d.ts +2 -0
- package/dist/lib/backup.js +4 -0
- package/dist/lib/builtinSkills.d.ts +2 -0
- package/dist/lib/builtinSkills.js +4 -0
- package/dist/lib/cliMetadata.d.ts +1 -0
- package/dist/lib/cliMetadata.js +14 -0
- package/dist/lib/gatewayBackend.d.ts +28 -0
- package/dist/lib/gatewayBackend.js +72 -1
- package/dist/lib/installAgent.d.ts +7 -5
- package/dist/lib/installAgent.js +4 -3
- package/dist/lib/mcpConnect.js +5 -1
- package/dist/lib/mcpMigrateRead.d.ts +1 -0
- package/dist/lib/mcpMigrateRead.js +3 -0
- package/dist/lib/mcpRuntime.d.ts +46 -0
- package/dist/lib/mcpRuntime.js +185 -0
- package/dist/lib/mcpStatusProvider.d.ts +10 -0
- package/dist/lib/mcpStatusProvider.js +25 -0
- package/dist/lib/mcpToolRegistry.d.ts +18 -0
- package/dist/lib/mcpToolRegistry.js +107 -14
- package/dist/lib/memoryGraph.d.ts +1 -1
- package/dist/lib/memoryGraph.js +9 -3
- package/dist/lib/memoryProvider.d.ts +58 -0
- package/dist/lib/memoryProvider.js +175 -0
- package/dist/lib/memoryStatus.d.ts +17 -0
- package/dist/lib/memoryStatus.js +88 -0
- package/dist/lib/nativeMemory.d.ts +19 -0
- package/dist/lib/nativeMemory.js +129 -0
- package/dist/lib/runtimeControlProvider.d.ts +16 -0
- package/dist/lib/runtimeControlProvider.js +168 -0
- package/dist/lib/secretEnv.d.ts +2 -1
- package/dist/lib/secretEnv.js +6 -2
- package/dist/lib/skillProvider.d.ts +13 -0
- package/dist/lib/skillProvider.js +210 -0
- package/dist/lib/taskProvider.d.ts +7 -0
- package/dist/lib/taskProvider.js +43 -0
- package/dist/lib/taskStore.d.ts +46 -0
- package/dist/lib/taskStore.js +156 -0
- package/dist/lib/terminalPicker.d.ts +23 -49
- package/dist/lib/terminalPicker.js +53 -218
- package/dist/pi-bridge/bundle.js +475 -37
- package/dist/pi-bridge/index.js +42 -21
- package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
- package/dist/pi-bridge/schemaTranslate.js +4 -8
- package/dist/probes/kimi-code.d.ts +12 -0
- package/dist/probes/kimi-code.js +55 -0
- package/dist/sdk.d.ts +1 -1
- package/docs/architecture.md +224 -26
- package/docs/assets/agent-trellis-architecture-horizontal.svg +80 -0
- package/docs/assets/agent-trellis-architecture-vertical.svg +82 -0
- package/docs/assets/trellis-mark.svg +12 -0
- package/docs/getting-started.md +328 -35
- package/docs/overview.md +131 -0
- package/docs/overview.zh-CN.md +100 -0
- package/docs/research.md +59 -0
- package/docs/roadmap.md +9 -5
- package/package.json +3 -2
- package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
- package/schema/capability-selection.example.yaml +23 -0
- package/schema/scope.example.yaml +1 -1
- package/schema/servers.example.yaml +15 -1
package/dist/adapters/mcpPlan.js
CHANGED
|
@@ -17,21 +17,32 @@
|
|
|
17
17
|
* ever returns create/repair + refuse.
|
|
18
18
|
*/
|
|
19
19
|
import { isInScope } from "../core/adapter.js";
|
|
20
|
+
import { capabilityDeliveryForAgent } from "../core/types.js";
|
|
20
21
|
import { codexBearerTokenEnvVar } from "../lib/tomlSection.js";
|
|
21
22
|
import { resolveSecretEnv } from "../lib/secretEnv.js";
|
|
22
23
|
export const HUB_ENTRY_NAME = "trellis-hub";
|
|
23
|
-
export const GATEWAY_ENTRY_NAME = "trellis
|
|
24
|
+
export const GATEWAY_ENTRY_NAME = "trellis";
|
|
25
|
+
export const LEGACY_GATEWAY_ENTRY_NAME = "trellis-gateway";
|
|
26
|
+
export const RUNTIME_ENTRY_NAME = "trellis";
|
|
27
|
+
export const LEGACY_RUNTIME_ENTRY_NAME = "trellis-runtime";
|
|
24
28
|
/** The command an agent spawns in gateway mode. Bare `trellis` rather than
|
|
25
29
|
* an absolute path: the entry has to keep working across reinstalls and
|
|
26
30
|
* version bumps, and every agent resolves it through the same PATH the
|
|
27
31
|
* user installed the CLI onto. */
|
|
28
32
|
export const GATEWAY_COMMAND = "trellis";
|
|
33
|
+
export const RUNTIME_COMMAND = "trellis";
|
|
34
|
+
export function isOAuthMcpServer(def) {
|
|
35
|
+
return def.auth === "oauth";
|
|
36
|
+
}
|
|
29
37
|
/**
|
|
30
38
|
* Gateway mode applies to every managed agent when `agents` is omitted —
|
|
31
39
|
* the expected normal case (design.md D14). An explicit list narrows it,
|
|
32
40
|
* letting some agents stay in direct or hub mode.
|
|
33
41
|
*/
|
|
34
42
|
export function isGatewayAgent(agentId, mcp, managedAgents) {
|
|
43
|
+
const route = mcp.routes?.[agentId];
|
|
44
|
+
if (route)
|
|
45
|
+
return route.mode === "gateway";
|
|
35
46
|
const gateway = mcp.gateway;
|
|
36
47
|
if (!gateway?.enabled)
|
|
37
48
|
return false;
|
|
@@ -39,6 +50,16 @@ export function isGatewayAgent(agentId, mcp, managedAgents) {
|
|
|
39
50
|
return gateway.agents.includes(agentId);
|
|
40
51
|
return managedAgents.includes(agentId);
|
|
41
52
|
}
|
|
53
|
+
function routeForAgent(agentId, mcp, managedAgents) {
|
|
54
|
+
const explicit = mcp.routes?.[agentId];
|
|
55
|
+
if (explicit)
|
|
56
|
+
return explicit;
|
|
57
|
+
if (isGatewayAgent(agentId, mcp, managedAgents))
|
|
58
|
+
return { mode: "gateway" };
|
|
59
|
+
if (mcp.hub)
|
|
60
|
+
return { mode: "hub" };
|
|
61
|
+
return { mode: "direct" };
|
|
62
|
+
}
|
|
42
63
|
const CODEX_STDIO_URL_CRASH_NOTE = ' On Codex specifically, this crashes the entire process at startup ("url is not supported for stdio"), not just this one server — see docs/research.md.';
|
|
43
64
|
function collisionMessage(name, agentId) {
|
|
44
65
|
return `refusing to write MCP server "${name}": also appears in known_host_injected.${agentId === "codex" ? CODEX_STDIO_URL_CRASH_NOTE : ""}`;
|
|
@@ -116,45 +137,122 @@ function findUnresolvedEnvName(def, policy) {
|
|
|
116
137
|
return names.find((name) => !resolved[name]);
|
|
117
138
|
}
|
|
118
139
|
export function resolveMcpPlan(agentId, mcp, managedAgents, policy) {
|
|
140
|
+
const route = routeForAgent(agentId, mcp, managedAgents);
|
|
141
|
+
const runtimeEnabled = capabilityDeliveryForAgent(agentId, mcp) !== "native";
|
|
119
142
|
// Checked before `hub`: only one entry can be "the" MCP entry for an
|
|
120
143
|
// agent, and gateway is the newer, purpose-built path (design.md D6).
|
|
121
144
|
// `hub` stays available, unchanged, for anyone pointing an agent at an
|
|
122
145
|
// externally-operated endpoint — including a different agent in the
|
|
123
146
|
// same canonical source.
|
|
124
|
-
if (
|
|
125
|
-
|
|
147
|
+
if (route.mode === "gateway") {
|
|
148
|
+
const entryName = runtimeEnabled ? RUNTIME_ENTRY_NAME : GATEWAY_ENTRY_NAME;
|
|
149
|
+
if (mcp.knownHostInjected.includes(entryName)) {
|
|
126
150
|
return {
|
|
127
151
|
desired: [],
|
|
128
|
-
conflicts: [{ name:
|
|
152
|
+
conflicts: [{ name: entryName, message: collisionMessage(entryName, agentId), remediation: collisionRemediation(entryName) }],
|
|
129
153
|
};
|
|
130
154
|
}
|
|
155
|
+
const oauth = planDirectServers(agentId, mcp, managedAgents, policy, route, isOAuthMcpServer);
|
|
156
|
+
// Every ordinary per-server check (enabled, scope, literal secrets,
|
|
157
|
+
// unresolved env names, Codex's header shape) still runs at gateway
|
|
158
|
+
// startup. OAuth servers are the deliberate exception: they are written
|
|
159
|
+
// as direct entries so the gateway never owns their client connection.
|
|
131
160
|
// Every per-server check (enabled, scope, literal secrets, unresolved
|
|
132
161
|
// env names, Codex's header shape) still runs — but at gateway
|
|
133
162
|
// *startup*, inside the subcommand, since no adapter sees an
|
|
134
163
|
// individual server in this mode (design.md D6).
|
|
135
164
|
return {
|
|
136
|
-
desired: [{
|
|
137
|
-
|
|
165
|
+
desired: [{
|
|
166
|
+
name: entryName,
|
|
167
|
+
def: {
|
|
168
|
+
transport: "stdio",
|
|
169
|
+
command: runtimeEnabled ? RUNTIME_COMMAND : GATEWAY_COMMAND,
|
|
170
|
+
args: [runtimeEnabled ? "mcp-runtime" : "mcp-gateway", "--agent", agentId],
|
|
171
|
+
},
|
|
172
|
+
}, ...oauth.desired],
|
|
173
|
+
conflicts: oauth.conflicts,
|
|
138
174
|
};
|
|
139
175
|
}
|
|
140
|
-
if (
|
|
176
|
+
if (route.mode === "hub") {
|
|
177
|
+
if (!mcp.hub) {
|
|
178
|
+
return {
|
|
179
|
+
desired: [],
|
|
180
|
+
conflicts: [{
|
|
181
|
+
name: HUB_ENTRY_NAME,
|
|
182
|
+
message: `cannot resolve hub route "${agentId}": canonical mcp.hub.url is not configured`,
|
|
183
|
+
remediation: `configure mcp.hub.url or choose direct/gateway mode for ${agentId}`,
|
|
184
|
+
}],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
141
187
|
if (mcp.knownHostInjected.includes(HUB_ENTRY_NAME)) {
|
|
142
188
|
return {
|
|
143
189
|
desired: [],
|
|
144
190
|
conflicts: [{ name: HUB_ENTRY_NAME, message: collisionMessage(HUB_ENTRY_NAME, agentId), remediation: collisionRemediation(HUB_ENTRY_NAME) }],
|
|
145
191
|
};
|
|
146
192
|
}
|
|
147
|
-
|
|
193
|
+
if (route.servers) {
|
|
194
|
+
return {
|
|
195
|
+
desired: [],
|
|
196
|
+
conflicts: [{
|
|
197
|
+
name: HUB_ENTRY_NAME,
|
|
198
|
+
message: `cannot restrict external hub route "${agentId}" to individual MCP servers: the hub owns its upstream set`,
|
|
199
|
+
remediation: `remove the servers list from the ${agentId} hub route, or use direct/gateway mode for per-server selection`,
|
|
200
|
+
}],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const desired = [{ name: HUB_ENTRY_NAME, def: { transport: "http", url: mcp.hub.url } }];
|
|
204
|
+
if (runtimeEnabled) {
|
|
205
|
+
if (mcp.knownHostInjected.includes(RUNTIME_ENTRY_NAME)) {
|
|
206
|
+
return {
|
|
207
|
+
desired: [],
|
|
208
|
+
conflicts: [{ name: RUNTIME_ENTRY_NAME, message: collisionMessage(RUNTIME_ENTRY_NAME, agentId), remediation: collisionRemediation(RUNTIME_ENTRY_NAME) }],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
// The runtime provides Trellis-owned capabilities here. The external
|
|
212
|
+
// hub remains the upstream MCP edge; the runtime does not connect to
|
|
213
|
+
// canonical upstreams a second time for this route.
|
|
214
|
+
desired.push({
|
|
215
|
+
name: RUNTIME_ENTRY_NAME,
|
|
216
|
+
def: { transport: "stdio", command: RUNTIME_COMMAND, args: ["mcp-runtime", "--agent", agentId] },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return { desired, conflicts: [] };
|
|
220
|
+
}
|
|
221
|
+
if (runtimeEnabled) {
|
|
222
|
+
if (mcp.knownHostInjected.includes(RUNTIME_ENTRY_NAME)) {
|
|
223
|
+
return {
|
|
224
|
+
desired: [],
|
|
225
|
+
conflicts: [{ name: RUNTIME_ENTRY_NAME, message: collisionMessage(RUNTIME_ENTRY_NAME, agentId), remediation: collisionRemediation(RUNTIME_ENTRY_NAME) }],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// Runtime mode owns the agent-facing MCP edge. It resolves the same
|
|
229
|
+
// direct/gateway-compatible upstream route at startup, so native
|
|
230
|
+
// per-server entries are not duplicated here.
|
|
231
|
+
return {
|
|
232
|
+
desired: [{
|
|
233
|
+
name: RUNTIME_ENTRY_NAME,
|
|
234
|
+
def: { transport: "stdio", command: RUNTIME_COMMAND, args: ["mcp-runtime", "--agent", agentId] },
|
|
235
|
+
}],
|
|
236
|
+
conflicts: [],
|
|
237
|
+
};
|
|
148
238
|
}
|
|
239
|
+
return planDirectServers(agentId, mcp, managedAgents, policy, route);
|
|
240
|
+
}
|
|
241
|
+
function planDirectServers(agentId, mcp, managedAgents, policy, route, filter) {
|
|
149
242
|
const desired = [];
|
|
150
243
|
const conflicts = [];
|
|
151
244
|
for (const [name, def] of Object.entries(mcp.servers)) {
|
|
152
245
|
if (def.enabled === false) {
|
|
153
246
|
continue;
|
|
154
247
|
}
|
|
248
|
+
if (filter && !filter(def))
|
|
249
|
+
continue;
|
|
155
250
|
if (!isInScope(agentId, def.agents, managedAgents)) {
|
|
156
251
|
continue;
|
|
157
252
|
}
|
|
253
|
+
if (route.servers && !route.servers.includes(name)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
158
256
|
if (mcp.knownHostInjected.includes(name)) {
|
|
159
257
|
conflicts.push({ name, message: collisionMessage(name, agentId), remediation: collisionRemediation(name) });
|
|
160
258
|
continue;
|
package/dist/adapters/pi.js
CHANGED
|
@@ -9,13 +9,31 @@
|
|
|
9
9
|
* mechanism from every other agent's config-generation adapter, but the
|
|
10
10
|
* same symlink create/repair/remove machinery as skills/instructions.
|
|
11
11
|
*/
|
|
12
|
-
import { dirname, join } from "node:path";
|
|
12
|
+
import { dirname, join, sep } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
15
16
|
import { isInScope } from "../core/adapter.js";
|
|
17
|
+
import { usesNativeCapabilityDelivery } from "../core/types.js";
|
|
16
18
|
import * as piProbe from "../probes/pi.js";
|
|
17
19
|
import { applySymlinkPlan, planSymlinks } from "./symlinkPlan.js";
|
|
18
20
|
const BRIDGE_SYMLINK_NAME = "trellis-mcp-bridge.js";
|
|
21
|
+
/**
|
|
22
|
+
* A source-checkout bridge can survive a switch to the packaged CLI. Adopt it
|
|
23
|
+
* only when it has the Trellis bridge artifact shape and is byte-identical
|
|
24
|
+
* to the bundle this installation would write. This is intentionally not a
|
|
25
|
+
* general symlink ownership rule.
|
|
26
|
+
*/
|
|
27
|
+
function isEquivalentTrellisBridge(existingTarget, desiredTarget) {
|
|
28
|
+
if (!existingTarget.endsWith(`${sep}${join("dist", "pi-bridge", "bundle.js")}`))
|
|
29
|
+
return false;
|
|
30
|
+
try {
|
|
31
|
+
return readFileSync(existingTarget).equals(readFileSync(desiredTarget));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
19
37
|
/**
|
|
20
38
|
* The bundled bridge file's location — always `<repo-root>/dist/pi-bridge/
|
|
21
39
|
* bundle.js`, never `~/.trellis/` (it's Trellis's own packaged code, not a
|
|
@@ -56,7 +74,7 @@ export class PiAdapter {
|
|
|
56
74
|
const canonicalRoot = dirname(canonical.instructionsFile);
|
|
57
75
|
const agentDir = join(this.homeDir, ".pi", "agent");
|
|
58
76
|
const desiredSkills = canonical.skills
|
|
59
|
-
.filter((skill) => isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
77
|
+
.filter((skill) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, skill.scope, canonical.managedAgents))
|
|
60
78
|
.map((skill) => ({ name: skill.name, target: skill.dir }));
|
|
61
79
|
const skillItems = planSymlinks({
|
|
62
80
|
rootDir: join(agentDir, "skills"),
|
|
@@ -79,6 +97,7 @@ export class PiAdapter {
|
|
|
79
97
|
// (design.md D2), same role `canonicalRoot` plays for skills.
|
|
80
98
|
canonicalRoot: dirname(bridgeFile),
|
|
81
99
|
kind: "extension",
|
|
100
|
+
adoptExistingSymlink: isEquivalentTrellisBridge,
|
|
82
101
|
});
|
|
83
102
|
return [...skillItems, ...instructionsItems, ...extensionItems];
|
|
84
103
|
}
|
|
@@ -91,7 +110,7 @@ export class PiAdapter {
|
|
|
91
110
|
return { ok: false, mismatches: ["pi is not present on this machine"] };
|
|
92
111
|
}
|
|
93
112
|
const mismatches = [];
|
|
94
|
-
const desiredNames = new Set(canonical.skills.filter((s) => isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
113
|
+
const desiredNames = new Set(canonical.skills.filter((s) => usesNativeCapabilityDelivery(this.id, canonical.mcp) && isInScope(this.id, s.scope, canonical.managedAgents)).map((s) => s.name));
|
|
95
114
|
const actualSkills = new Set(snapshot.skillRoots.flatMap((root) => root.skills).map((s) => s.name));
|
|
96
115
|
for (const name of desiredNames) {
|
|
97
116
|
if (!actualSkills.has(name)) {
|
|
@@ -12,6 +12,12 @@ export interface DesiredSymlink {
|
|
|
12
12
|
/** Absolute path the symlink should point at (inside `canonicalRoot`). */
|
|
13
13
|
target: string;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* A narrowly-scoped escape hatch for artifacts that Trellis can identify
|
|
17
|
+
* across installation roots. The default ownership rule remains path-based;
|
|
18
|
+
* callers must opt in explicitly for a particular artifact kind.
|
|
19
|
+
*/
|
|
20
|
+
export type ExistingSymlinkAdoption = (existingTarget: string, desiredTarget: string) => boolean;
|
|
15
21
|
/**
|
|
16
22
|
* `rootDir` is the directory entries are named directly under (e.g.
|
|
17
23
|
* `~/.claude/skills`, or the instructions file's own parent directory when
|
|
@@ -28,6 +34,7 @@ export declare function planSymlinks(opts: {
|
|
|
28
34
|
desired: DesiredSymlink[];
|
|
29
35
|
canonicalRoot: string;
|
|
30
36
|
kind: "skill" | "instructions" | "extension";
|
|
37
|
+
adoptExistingSymlink?: ExistingSymlinkAdoption;
|
|
31
38
|
}): AdapterPlanItem[];
|
|
32
39
|
/** Executes a `planSymlinks` result through the run's backup session
|
|
33
40
|
* (trellis-backup-rollback) — every create/repair/remove is recorded
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* trellis-sync-p1/specs/skill-instructions-sync/spec.md) — built once,
|
|
5
5
|
* reused by every adapter rather than reimplemented per agent.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { lstatSync, readdirSync, readlinkSync } from "node:fs";
|
|
8
8
|
import { mkdir } from "node:fs/promises";
|
|
9
|
-
import { join, resolve, sep } from "node:path";
|
|
9
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
10
10
|
import { isSymlinkTo } from "../lib/fsIdentity.js";
|
|
11
11
|
import { currentLinkTarget } from "../lib/backup.js";
|
|
12
12
|
function isUnderRoot(path, root) {
|
|
@@ -29,7 +29,7 @@ function isUnderRoot(path, root) {
|
|
|
29
29
|
* case this removal path exists to catch.
|
|
30
30
|
*/
|
|
31
31
|
export function planSymlinks(opts) {
|
|
32
|
-
const { rootDir, desired, kind } = opts;
|
|
32
|
+
const { rootDir, desired, kind, adoptExistingSymlink } = opts;
|
|
33
33
|
const items = [];
|
|
34
34
|
const desiredNames = new Set(desired.map((d) => d.name));
|
|
35
35
|
const canonicalRootResolved = resolve(opts.canonicalRoot);
|
|
@@ -38,8 +38,15 @@ export function planSymlinks(opts) {
|
|
|
38
38
|
if (isSymlinkTo(path, target)) {
|
|
39
39
|
continue; // already correct — no-op
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// lstat sees dangling links too; existsSync would treat them as absent
|
|
42
|
+
// and bypass the foreign-link protection below.
|
|
43
|
+
const existing = lstatSync(path, { throwIfNoEntry: false });
|
|
44
|
+
if (existing) {
|
|
45
|
+
const isSymlink = existing.isSymbolicLink();
|
|
46
|
+
const rawExistingTarget = isSymlink ? readlinkSync(path) : undefined;
|
|
47
|
+
const resolvedExistingTarget = rawExistingTarget === undefined
|
|
48
|
+
? undefined
|
|
49
|
+
: resolve(dirname(path), rawExistingTarget);
|
|
43
50
|
// A symlink whose stored target resolves outside canonicalRoot is
|
|
44
51
|
// owned by something else (e.g. a user's own dotfile-management
|
|
45
52
|
// setup) — repairing it as if it were a stale Trellis entry would
|
|
@@ -47,14 +54,16 @@ export function planSymlinks(opts) {
|
|
|
47
54
|
// symlink Trellis itself left pointing at a since-removed canonical
|
|
48
55
|
// entry is broken by construction and must still be treated as
|
|
49
56
|
// ours to repair, not thrown out to a conflict by a realpath error.
|
|
50
|
-
const isForeign =
|
|
57
|
+
const isForeign = resolvedExistingTarget !== undefined
|
|
58
|
+
&& !isUnderRoot(resolvedExistingTarget, canonicalRootResolved)
|
|
59
|
+
&& adoptExistingSymlink?.(resolvedExistingTarget, target) !== true;
|
|
51
60
|
if (!isSymlink || isForeign) {
|
|
52
61
|
items.push({
|
|
53
62
|
action: "conflict",
|
|
54
63
|
kind,
|
|
55
64
|
target: path,
|
|
56
65
|
description: isForeign
|
|
57
|
-
? `${path} exists as a symlink to ${
|
|
66
|
+
? `${path} exists as a symlink to ${rawExistingTarget}, not owned by Trellis — left untouched`
|
|
58
67
|
: `${path} exists and is not a Trellis-managed symlink — left untouched`,
|
|
59
68
|
remediation: isForeign
|
|
60
69
|
? `remove the existing symlink at ${path} if you want Trellis to manage it, then re-run sync — otherwise leave it, Trellis will not touch it`
|
package/dist/cli.js
CHANGED
|
@@ -9,22 +9,33 @@ import { runInit } from "./commands/init.js";
|
|
|
9
9
|
import { runMigrate } from "./commands/migrate.js";
|
|
10
10
|
import { runOnboard } from "./commands/onboard.js";
|
|
11
11
|
import { runSync } from "./commands/sync.js";
|
|
12
|
-
import { runMcpSync, runMcpList, runMcpAdd, runMcpRemove, parseMcpAddArgs } from "./commands/mcp.js";
|
|
13
|
-
import {
|
|
12
|
+
import { runMcpSync, runMcpList, runMcpAdd, runMcpRemove, runMcpSetAuth, parseMcpAddArgs } from "./commands/mcp.js";
|
|
13
|
+
import { runMcpImport } from "./commands/mcpImport.js";
|
|
14
|
+
import { parseMcpGatewayArgs, runMcpGateway, runMcpRuntime } from "./commands/mcpGateway.js";
|
|
14
15
|
import { runMcpAuth } from "./commands/mcpAuth.js";
|
|
15
16
|
import { runSecretsAudit } from "./commands/secretsAudit.js";
|
|
16
17
|
import { runRollback } from "./commands/rollback.js";
|
|
17
|
-
import { runSkillList, runSkillAdd, runSkillRemove } from "./commands/skill.js";
|
|
18
|
+
import { runSkillList, runSkillAdd, runSkillRemove, runSkillUpdateBuiltin } from "./commands/skill.js";
|
|
18
19
|
import { runMemoryExtraction, runMemorySync } from "./commands/memory.js";
|
|
20
|
+
import { parseManageArgs, runManage } from "./commands/manage.js";
|
|
21
|
+
import { runKimi } from "./commands/kimi.js";
|
|
22
|
+
import { TRELLIS_VERSION } from "./lib/cliMetadata.js";
|
|
19
23
|
import { parseSyncArgs } from "./lib/syncArgs.js";
|
|
20
|
-
const KNOWN_COMMANDS = ["onboard", "init", "migrate", "doctor", "sync", "mcp", "mcp-gateway", "skill", "memory", "secrets", "rollback"];
|
|
24
|
+
const KNOWN_COMMANDS = ["onboard", "init", "migrate", "doctor", "sync", "mcp", "mcp-gateway", "mcp-runtime", "skill", "memory", "manage", "kimi", "secrets", "rollback"];
|
|
21
25
|
function printUsage() {
|
|
22
26
|
console.log(`trellis - a single source of capability for every coding agent
|
|
23
27
|
|
|
24
28
|
Usage:
|
|
25
29
|
trellis <command>
|
|
30
|
+
trellis --version
|
|
31
|
+
trellis --help
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
-v, --version print the installed Trellis version
|
|
35
|
+
-h, --help show this help
|
|
26
36
|
|
|
27
37
|
Commands:
|
|
38
|
+
help Show this help
|
|
28
39
|
onboard Guided flow: init -> detect agents -> pick a migration source ->
|
|
29
40
|
pick which agents to manage -> migrate -> sync -> mcp sync ->
|
|
30
41
|
secrets audit, in one command. Source (read from) and managed
|
|
@@ -39,15 +50,21 @@ Commands:
|
|
|
39
50
|
(required with no terminal to prompt
|
|
40
51
|
in, e.g. --json)
|
|
41
52
|
--mcp-mode <direct|hub|gateway>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
--hub-url <url> required with --mcp-mode hub
|
|
53
|
+
direct = Agent 分别直连;gateway =
|
|
54
|
+
Trellis 本机统一托管(推荐);hub =
|
|
55
|
+
连接已有的外部 MCP Hub
|
|
56
|
+
--hub-url <url> required with --mcp-mode hub; Trellis
|
|
57
|
+
does not deploy an external Hub
|
|
46
58
|
--gateway-agents <ids>
|
|
47
59
|
optional with --mcp-mode gateway;
|
|
48
60
|
omit for every managed agent
|
|
49
61
|
--memory <on|off> enable/disable the shared memory MCP
|
|
50
62
|
server; omit to leave it untouched
|
|
63
|
+
--memory-migrate <on|off>
|
|
64
|
+
import supported native Agent memory into
|
|
65
|
+
canonical; omit to leave it untouched
|
|
66
|
+
--selection <file> item-level YAML/JSON selection for skills,
|
|
67
|
+
MCP servers, memories, and per-agent routes
|
|
51
68
|
--dry-run preview the whole flow, write nothing
|
|
52
69
|
--json machine-readable output, no report text
|
|
53
70
|
init Create ~/.trellis/ with a minimal valid skeleton if missing
|
|
@@ -55,12 +72,13 @@ Commands:
|
|
|
55
72
|
missing) and prints which agents are present
|
|
56
73
|
--json machine-readable output, no report text
|
|
57
74
|
migrate --from <agent>
|
|
58
|
-
Import an existing agent's real skills/instructions/MCP
|
|
75
|
+
Import an existing agent's real skills/instructions/MCP/memory
|
|
59
76
|
servers into canonical source (claude-code | codex | kiro |
|
|
60
77
|
pi — pi has no static MCP config, mcp migrate-in is a no-op
|
|
61
78
|
for it). Never overwrites differing content — reports a
|
|
62
79
|
conflict instead.
|
|
63
|
-
|
|
80
|
+
--only skills|instructions|mcp|memory
|
|
81
|
+
restrict to one category
|
|
64
82
|
--dry-run preview the plan, write nothing
|
|
65
83
|
--json machine-readable output, no report text
|
|
66
84
|
doctor Scan Claude Code / Codex / Kiro / pi for drift
|
|
@@ -83,12 +101,22 @@ Commands:
|
|
|
83
101
|
--json machine-readable output, no report text
|
|
84
102
|
mcp add <name> --transport stdio|http|sse ...
|
|
85
103
|
Add a canonical MCP server (refuses on an existing name,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
no overwrite): --command <cmd> [--args a,b] (stdio) or
|
|
105
|
+
--url <url> (http/sse); [--headers k=v,...]
|
|
106
|
+
[--auth oauth] [--env NAME,...] [--static-env k=v,...] [--agents id,...]
|
|
107
|
+
[--enabled true|false]
|
|
108
|
+
--dry-run preview the plan, write nothing
|
|
109
|
+
--json machine-readable output, no report text
|
|
110
|
+
mcp import <json-file>
|
|
111
|
+
Import a standard mcpServers JSON export without changing the
|
|
112
|
+
source; extracts credential env values into the ignored local
|
|
113
|
+
secret file, de-duplicates, and reports conflicts
|
|
114
|
+
--dry-run preview without writing
|
|
115
|
+
--json machine-readable output, no report text
|
|
116
|
+
mcp set <name> --auth oauth|none
|
|
117
|
+
Set or clear explicit OAuth classification; canonical only
|
|
118
|
+
--dry-run preview the plan, write nothing
|
|
119
|
+
--json machine-readable output, no report text
|
|
92
120
|
mcp auth <server-name>
|
|
93
121
|
Authorize a remote (http/sse) MCP server that uses OAuth —
|
|
94
122
|
opens a browser once, stores the result 0600 under
|
|
@@ -97,6 +125,10 @@ Commands:
|
|
|
97
125
|
refreshes silently on its own and never prompts.
|
|
98
126
|
--force re-authorize even if the stored token is valid
|
|
99
127
|
--json machine-readable output, no report text
|
|
128
|
+
mcp-runtime --agent <agent>
|
|
129
|
+
Internal MCP edge exposing Trellis providers and selected
|
|
130
|
+
upstream capabilities; existing mcp-gateway configs remain valid
|
|
131
|
+
as a compatibility alias
|
|
100
132
|
mcp remove <name>
|
|
101
133
|
Remove a canonical MCP server (canonical-side only — does
|
|
102
134
|
not touch any agent's already-synced native config)
|
|
@@ -111,6 +143,11 @@ Commands:
|
|
|
111
143
|
--dry-run preview the plan, write nothing
|
|
112
144
|
--json machine-readable output, no report text
|
|
113
145
|
skill remove <name>
|
|
146
|
+
skill update-builtin
|
|
147
|
+
Refresh the package-owned trellis-runtime Skill in canonical
|
|
148
|
+
source; existing native symlinks see the update
|
|
149
|
+
--dry-run preview without writing
|
|
150
|
+
--json machine-readable output, no report text
|
|
114
151
|
Remove a canonical skill (the next sync auto-removes the
|
|
115
152
|
now-stale symlink on every managed agent)
|
|
116
153
|
--dry-run preview the plan, write nothing
|
|
@@ -132,6 +169,20 @@ Commands:
|
|
|
132
169
|
round-trip byte-for-byte back through memory sync.
|
|
133
170
|
--dry-run preview the plan, write nothing
|
|
134
171
|
--json machine-readable output, no report text
|
|
172
|
+
manage list
|
|
173
|
+
Show the canonical managed-agent write boundary.
|
|
174
|
+
manage set <ids|none>
|
|
175
|
+
manage add <ids>
|
|
176
|
+
manage remove <ids>
|
|
177
|
+
Explicitly replace, extend, or narrow the managed set. Removing
|
|
178
|
+
an agent preserves its existing native state; future Trellis
|
|
179
|
+
writes stop reaching it. Mutations are backup/rollback-enabled.
|
|
180
|
+
--dry-run preview current -> desired, write nothing
|
|
181
|
+
--json machine-readable output
|
|
182
|
+
kimi [args...]
|
|
183
|
+
Launch Kimi Code. When kimi-code Runtime delivery is \`mcp\`,
|
|
184
|
+
starts Kimi with an empty --skills-dir so Skills come from
|
|
185
|
+
trellis-runtime instead of native discovery.
|
|
135
186
|
secrets audit
|
|
136
187
|
Scan each present agent's real MCP config for leaked
|
|
137
188
|
credentials and unexpected env var names
|
|
@@ -148,12 +199,23 @@ Commands:
|
|
|
148
199
|
|
|
149
200
|
See docs/roadmap.md for what's built vs. planned.`);
|
|
150
201
|
}
|
|
202
|
+
function printVersion() {
|
|
203
|
+
console.log(TRELLIS_VERSION);
|
|
204
|
+
}
|
|
151
205
|
async function main(argv) {
|
|
152
206
|
const [command, ...rest] = argv;
|
|
153
207
|
if (!command || command === "--help" || command === "-h") {
|
|
154
208
|
printUsage();
|
|
155
209
|
return;
|
|
156
210
|
}
|
|
211
|
+
if (command === "--version" || command === "-v") {
|
|
212
|
+
printVersion();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (command === "help") {
|
|
216
|
+
printUsage();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
157
219
|
if (!KNOWN_COMMANDS.includes(command)) {
|
|
158
220
|
console.error(`Unknown command: ${command}\n`);
|
|
159
221
|
printUsage();
|
|
@@ -165,7 +227,7 @@ async function main(argv) {
|
|
|
165
227
|
// sync --help`) must never fall through to actually running the command
|
|
166
228
|
// it was asking about (real incident: `--help` isn't a flag any branch
|
|
167
229
|
// recognizes, so it silently ran a real, non-dry-run `mcp sync`).
|
|
168
|
-
if (rest.includes("--help") || rest.includes("-h")) {
|
|
230
|
+
if (command !== "kimi" && (rest.includes("--help") || rest.includes("-h"))) {
|
|
169
231
|
printUsage();
|
|
170
232
|
return;
|
|
171
233
|
}
|
|
@@ -182,6 +244,10 @@ async function main(argv) {
|
|
|
182
244
|
const gatewayAgents = gatewayAgentsIndex >= 0 ? rest[gatewayAgentsIndex + 1] : undefined;
|
|
183
245
|
const memoryIndex = rest.indexOf("--memory");
|
|
184
246
|
const memory = memoryIndex >= 0 ? rest[memoryIndex + 1] : undefined;
|
|
247
|
+
const memoryMigrateIndex = rest.indexOf("--memory-migrate");
|
|
248
|
+
const memoryMigrate = memoryMigrateIndex >= 0 ? rest[memoryMigrateIndex + 1] : undefined;
|
|
249
|
+
const selectionIndex = rest.indexOf("--selection");
|
|
250
|
+
const selectionFile = selectionIndex >= 0 ? rest[selectionIndex + 1] : undefined;
|
|
185
251
|
const { exitCode } = await runOnboard({
|
|
186
252
|
agent,
|
|
187
253
|
manage,
|
|
@@ -189,6 +255,8 @@ async function main(argv) {
|
|
|
189
255
|
hubUrl,
|
|
190
256
|
gatewayAgents,
|
|
191
257
|
memory,
|
|
258
|
+
memoryMigrate,
|
|
259
|
+
selectionFile,
|
|
192
260
|
dryRun: rest.includes("--dry-run"),
|
|
193
261
|
json: rest.includes("--json"),
|
|
194
262
|
});
|
|
@@ -217,14 +285,16 @@ async function main(argv) {
|
|
|
217
285
|
// Spawned by an agent, never typed by a person: stdout is the MCP
|
|
218
286
|
// protocol stream for the rest of this process's life, so nothing here
|
|
219
287
|
// may print to it (trellis-mcp-gateway-hosting design.md D2).
|
|
220
|
-
if (command === "mcp-gateway") {
|
|
288
|
+
if (command === "mcp-gateway" || command === "mcp-runtime") {
|
|
221
289
|
const parsed = parseMcpGatewayArgs(rest);
|
|
222
290
|
if ("error" in parsed) {
|
|
223
291
|
console.error(parsed.error);
|
|
224
292
|
process.exitCode = 1;
|
|
225
293
|
return;
|
|
226
294
|
}
|
|
227
|
-
const { exitCode } =
|
|
295
|
+
const { exitCode } = command === "mcp-runtime"
|
|
296
|
+
? await runMcpRuntime({ agentId: parsed.agentId })
|
|
297
|
+
: await runMcpGateway({ agentId: parsed.agentId });
|
|
228
298
|
process.exitCode = exitCode;
|
|
229
299
|
return;
|
|
230
300
|
}
|
|
@@ -258,6 +328,18 @@ async function main(argv) {
|
|
|
258
328
|
process.exitCode = runMcpAdd(name, parseMcpAddArgs(mcpRest), { json, dryRun }).exitCode;
|
|
259
329
|
return;
|
|
260
330
|
}
|
|
331
|
+
if (subcommand === "import") {
|
|
332
|
+
const [file] = mcpRest;
|
|
333
|
+
process.exitCode = runMcpImport(file, { json, dryRun }).exitCode;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (subcommand === "set") {
|
|
337
|
+
const [name] = mcpRest;
|
|
338
|
+
const authIndex = mcpRest.indexOf("--auth");
|
|
339
|
+
const auth = authIndex >= 0 ? mcpRest[authIndex + 1] : undefined;
|
|
340
|
+
process.exitCode = runMcpSetAuth(name, auth, { json, dryRun }).exitCode;
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
261
343
|
if (subcommand === "remove") {
|
|
262
344
|
const [name] = mcpRest;
|
|
263
345
|
if (!name) {
|
|
@@ -279,7 +361,7 @@ async function main(argv) {
|
|
|
279
361
|
process.exitCode = exitCode;
|
|
280
362
|
return;
|
|
281
363
|
}
|
|
282
|
-
console.error(`Unknown mcp subcommand: ${subcommand ?? "(none)"}\nUsage: trellis mcp sync|list|add <name>|remove <name>|auth <name>\n`);
|
|
364
|
+
console.error(`Unknown mcp subcommand: ${subcommand ?? "(none)"}\nUsage: trellis mcp sync|list|add <name>|set <name>|remove <name>|auth <name>\n`);
|
|
283
365
|
process.exitCode = 1;
|
|
284
366
|
return;
|
|
285
367
|
}
|
|
@@ -313,7 +395,11 @@ async function main(argv) {
|
|
|
313
395
|
process.exitCode = runSkillRemove(name, { json, dryRun }).exitCode;
|
|
314
396
|
return;
|
|
315
397
|
}
|
|
316
|
-
|
|
398
|
+
if (subcommand === "update-builtin") {
|
|
399
|
+
process.exitCode = runSkillUpdateBuiltin({ json, dryRun }).exitCode;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
console.error(`Unknown skill subcommand: ${subcommand ?? "(none)"}\nUsage: trellis skill list|add <name> --from <path>|remove <name>|update-builtin\n`);
|
|
317
403
|
process.exitCode = 1;
|
|
318
404
|
return;
|
|
319
405
|
}
|
|
@@ -333,6 +419,20 @@ async function main(argv) {
|
|
|
333
419
|
process.exitCode = 1;
|
|
334
420
|
return;
|
|
335
421
|
}
|
|
422
|
+
if (command === "manage") {
|
|
423
|
+
const parsed = parseManageArgs(rest);
|
|
424
|
+
if ("error" in parsed) {
|
|
425
|
+
console.error(`${parsed.error}\nUsage: trellis manage list|set <ids|none>|add <ids>|remove <ids> [--dry-run] [--json]\n`);
|
|
426
|
+
process.exitCode = 1;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
process.exitCode = runManage(parsed).exitCode;
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (command === "kimi") {
|
|
433
|
+
process.exitCode = (await runKimi(rest)).exitCode;
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
336
436
|
if (command === "secrets") {
|
|
337
437
|
const [subcommand] = rest;
|
|
338
438
|
if (subcommand !== "audit") {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `trellis doctor` — read-only cross-agent scan. No `.trellis/` canonical
|
|
3
|
-
* source required (docs/roadmap.md): this compares the
|
|
3
|
+
* source required (docs/roadmap.md): this compares the five agents' own
|
|
4
4
|
* current state against each other, the same manual process used
|
|
5
5
|
* throughout docs/research.md's investigation, now formalized.
|
|
6
6
|
*/
|
|
7
|
+
import { loadCanonicalSource } from "../core/canonical.js";
|
|
7
8
|
import type { AgentId, AgentSnapshot } from "../core/types.js";
|
|
8
9
|
/**
|
|
9
10
|
* Interim default until P1 wires this from `.trellis/mcp/servers.yaml`'s
|
|
@@ -13,7 +14,7 @@ import type { AgentId, AgentSnapshot } from "../core/types.js";
|
|
|
13
14
|
* machine.
|
|
14
15
|
*/
|
|
15
16
|
export declare const DEFAULT_KNOWN_HOST_INJECTED: readonly string[];
|
|
16
|
-
export type FindingKind = "probe-failed" | "duplication" | "collision" | "drift" | "case-mismatch" | "parse-diagnostic" | "mcp-unreachable";
|
|
17
|
+
export type FindingKind = "probe-failed" | "duplication" | "collision" | "drift" | "case-mismatch" | "parse-diagnostic" | "runtime-drift" | "mcp-unreachable";
|
|
17
18
|
export interface Finding {
|
|
18
19
|
kind: FindingKind;
|
|
19
20
|
/** Omitted for a finding that names more than one agent (drift). */
|
|
@@ -92,6 +93,10 @@ export declare function detectCrossAgentDrift(snapshots: AgentSnapshot[]): Findi
|
|
|
92
93
|
export declare function detectCaseMismatches(snapshots: AgentSnapshot[]): Finding[];
|
|
93
94
|
export declare function detectParseDiagnostics(snapshots: AgentSnapshot[]): Finding[];
|
|
94
95
|
export declare function detectMcpUnreachable(snapshots: AgentSnapshot[]): Finding[];
|
|
96
|
+
/** Compares runtime entries against the same canonical MCP plan that sync
|
|
97
|
+
* uses. Only missing Trellis-owned runtime edges are reported; extra
|
|
98
|
+
* user-authored MCP entries remain outside doctor ownership. */
|
|
99
|
+
export declare function detectRuntimeDrift(snapshots: AgentSnapshot[], canonical: Pick<ReturnType<typeof loadCanonicalSource>, "mcp" | "managedAgents" | "secretsPolicy">): Finding[];
|
|
95
100
|
/** Exported so `trellis onboard` can reuse it verbatim for its own final
|
|
96
101
|
* stage (trellis-onboard-closed-loop) rather than a second copy of this
|
|
97
102
|
* formatting. */
|