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.
Files changed (104) hide show
  1. package/README.md +97 -38
  2. package/dist/adapters/claude-code.js +3 -2
  3. package/dist/adapters/codex.js +3 -2
  4. package/dist/adapters/jsonMcp.d.ts +3 -1
  5. package/dist/adapters/jsonMcp.js +18 -4
  6. package/dist/adapters/kimi-code.d.ts +21 -0
  7. package/dist/adapters/kimi-code.js +107 -0
  8. package/dist/adapters/kiro.js +3 -2
  9. package/dist/adapters/mcpPlan.d.ts +6 -1
  10. package/dist/adapters/mcpPlan.js +106 -8
  11. package/dist/adapters/pi.js +22 -3
  12. package/dist/adapters/symlinkPlan.d.ts +7 -0
  13. package/dist/adapters/symlinkPlan.js +16 -7
  14. package/dist/cli.js +121 -21
  15. package/dist/commands/doctor.d.ts +7 -2
  16. package/dist/commands/doctor.js +38 -2
  17. package/dist/commands/init.d.ts +4 -2
  18. package/dist/commands/init.js +26 -13
  19. package/dist/commands/kimi.d.ts +16 -0
  20. package/dist/commands/kimi.js +64 -0
  21. package/dist/commands/manage.d.ts +39 -0
  22. package/dist/commands/manage.js +126 -0
  23. package/dist/commands/mcp.d.ts +22 -1
  24. package/dist/commands/mcp.js +65 -2
  25. package/dist/commands/mcpGateway.d.ts +6 -0
  26. package/dist/commands/mcpGateway.js +34 -14
  27. package/dist/commands/mcpImport.d.ts +27 -0
  28. package/dist/commands/mcpImport.js +336 -0
  29. package/dist/commands/memory.d.ts +5 -3
  30. package/dist/commands/memory.js +12 -8
  31. package/dist/commands/migrate.d.ts +16 -5
  32. package/dist/commands/migrate.js +90 -17
  33. package/dist/commands/onboard.d.ts +74 -2
  34. package/dist/commands/onboard.js +458 -71
  35. package/dist/commands/rollback.js +4 -1
  36. package/dist/commands/skill.d.ts +17 -0
  37. package/dist/commands/skill.js +47 -2
  38. package/dist/commands/sync.js +2 -0
  39. package/dist/core/canonical.d.ts +15 -7
  40. package/dist/core/canonical.js +101 -13
  41. package/dist/core/capabilitySelection.d.ts +45 -0
  42. package/dist/core/capabilitySelection.js +124 -0
  43. package/dist/core/types.d.ts +25 -2
  44. package/dist/core/types.js +7 -0
  45. package/dist/lib/backup.d.ts +2 -0
  46. package/dist/lib/backup.js +4 -0
  47. package/dist/lib/builtinSkills.d.ts +2 -0
  48. package/dist/lib/builtinSkills.js +4 -0
  49. package/dist/lib/cliMetadata.d.ts +1 -0
  50. package/dist/lib/cliMetadata.js +14 -0
  51. package/dist/lib/gatewayBackend.d.ts +28 -0
  52. package/dist/lib/gatewayBackend.js +72 -1
  53. package/dist/lib/installAgent.d.ts +7 -5
  54. package/dist/lib/installAgent.js +4 -3
  55. package/dist/lib/mcpConnect.js +5 -1
  56. package/dist/lib/mcpMigrateRead.d.ts +1 -0
  57. package/dist/lib/mcpMigrateRead.js +3 -0
  58. package/dist/lib/mcpRuntime.d.ts +46 -0
  59. package/dist/lib/mcpRuntime.js +185 -0
  60. package/dist/lib/mcpStatusProvider.d.ts +10 -0
  61. package/dist/lib/mcpStatusProvider.js +25 -0
  62. package/dist/lib/mcpToolRegistry.d.ts +18 -0
  63. package/dist/lib/mcpToolRegistry.js +107 -14
  64. package/dist/lib/memoryGraph.d.ts +1 -1
  65. package/dist/lib/memoryGraph.js +9 -3
  66. package/dist/lib/memoryProvider.d.ts +58 -0
  67. package/dist/lib/memoryProvider.js +175 -0
  68. package/dist/lib/memoryStatus.d.ts +17 -0
  69. package/dist/lib/memoryStatus.js +88 -0
  70. package/dist/lib/nativeMemory.d.ts +19 -0
  71. package/dist/lib/nativeMemory.js +129 -0
  72. package/dist/lib/runtimeControlProvider.d.ts +16 -0
  73. package/dist/lib/runtimeControlProvider.js +168 -0
  74. package/dist/lib/secretEnv.d.ts +2 -1
  75. package/dist/lib/secretEnv.js +6 -2
  76. package/dist/lib/skillProvider.d.ts +13 -0
  77. package/dist/lib/skillProvider.js +210 -0
  78. package/dist/lib/taskProvider.d.ts +7 -0
  79. package/dist/lib/taskProvider.js +43 -0
  80. package/dist/lib/taskStore.d.ts +46 -0
  81. package/dist/lib/taskStore.js +156 -0
  82. package/dist/lib/terminalPicker.d.ts +23 -49
  83. package/dist/lib/terminalPicker.js +53 -218
  84. package/dist/pi-bridge/bundle.js +475 -37
  85. package/dist/pi-bridge/index.js +42 -21
  86. package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
  87. package/dist/pi-bridge/schemaTranslate.js +4 -8
  88. package/dist/probes/kimi-code.d.ts +12 -0
  89. package/dist/probes/kimi-code.js +55 -0
  90. package/dist/sdk.d.ts +1 -1
  91. package/docs/architecture.md +224 -26
  92. package/docs/assets/agent-trellis-architecture-horizontal.svg +80 -0
  93. package/docs/assets/agent-trellis-architecture-vertical.svg +82 -0
  94. package/docs/assets/trellis-mark.svg +12 -0
  95. package/docs/getting-started.md +328 -35
  96. package/docs/overview.md +131 -0
  97. package/docs/overview.zh-CN.md +100 -0
  98. package/docs/research.md +59 -0
  99. package/docs/roadmap.md +9 -5
  100. package/package.json +3 -2
  101. package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
  102. package/schema/capability-selection.example.yaml +23 -0
  103. package/schema/scope.example.yaml +1 -1
  104. package/schema/servers.example.yaml +15 -1
@@ -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-gateway";
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 (isGatewayAgent(agentId, mcp, managedAgents)) {
125
- if (mcp.knownHostInjected.includes(GATEWAY_ENTRY_NAME)) {
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: GATEWAY_ENTRY_NAME, message: collisionMessage(GATEWAY_ENTRY_NAME, agentId), remediation: collisionRemediation(GATEWAY_ENTRY_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: [{ name: GATEWAY_ENTRY_NAME, def: { transport: "stdio", command: GATEWAY_COMMAND, args: ["mcp-gateway", "--agent", agentId] } }],
137
- conflicts: [],
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 (mcp.hub) {
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
- return { desired: [{ name: HUB_ENTRY_NAME, def: { transport: "http", url: mcp.hub.url } }], conflicts: [] };
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;
@@ -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 { existsSync, lstatSync, readdirSync, readlinkSync } from "node:fs";
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
- if (existsSync(path)) {
42
- const isSymlink = lstatSync(path).isSymbolicLink();
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 = isSymlink && !isUnderRoot(readlinkSync(path), canonicalRootResolved);
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 ${readlinkSync(path)}, not owned by Trellis — left untouched`
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 { parseMcpGatewayArgs, runMcpGateway } from "./commands/mcpGateway.js";
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
- change the MCP mode; omit to leave
43
- whatever's already configured
44
- untouched (never prompted)
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
- --only skills|instructions|mcp restrict to one category
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
- no overwrite): --command <cmd> [--args a,b] (stdio) or
87
- --url <url> (http/sse); [--headers k=v,...]
88
- [--env NAME,...] [--static-env k=v,...] [--agents id,...]
89
- [--enabled true|false]
90
- --dry-run preview the plan, write nothing
91
- --json machine-readable output, no report text
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 } = await runMcpGateway({ agentId: parsed.agentId });
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
- console.error(`Unknown skill subcommand: ${subcommand ?? "(none)"}\nUsage: trellis skill list|add <name> --from <path>|remove <name>\n`);
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 four agents' own
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. */