agent-trellis 0.7.0 → 0.8.2

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 (302) hide show
  1. package/README.md +48 -153
  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/adapters/zcode.d.ts +21 -0
  15. package/dist/adapters/zcode.js +234 -0
  16. package/dist/cli.js +145 -29
  17. package/dist/commands/doctor.d.ts +7 -2
  18. package/dist/commands/doctor.js +40 -2
  19. package/dist/commands/init.d.ts +4 -2
  20. package/dist/commands/init.js +29 -13
  21. package/dist/commands/kimi.d.ts +16 -0
  22. package/dist/commands/kimi.js +64 -0
  23. package/dist/commands/manage.d.ts +39 -0
  24. package/dist/commands/manage.js +126 -0
  25. package/dist/commands/mcp.d.ts +77 -14
  26. package/dist/commands/mcp.js +169 -29
  27. package/dist/commands/mcpGateway.d.ts +6 -0
  28. package/dist/commands/mcpGateway.js +35 -14
  29. package/dist/commands/mcpImport.d.ts +46 -0
  30. package/dist/commands/mcpImport.js +359 -0
  31. package/dist/commands/memory.d.ts +17 -3
  32. package/dist/commands/memory.js +27 -8
  33. package/dist/commands/migrate.d.ts +16 -5
  34. package/dist/commands/migrate.js +93 -17
  35. package/dist/commands/onboard.d.ts +74 -2
  36. package/dist/commands/onboard.js +469 -71
  37. package/dist/commands/rollback.js +28 -2
  38. package/dist/commands/secretsAudit.js +4 -1
  39. package/dist/commands/skill.d.ts +52 -6
  40. package/dist/commands/skill.js +114 -15
  41. package/dist/commands/sync.js +5 -1
  42. package/dist/core/adapter.d.ts +11 -1
  43. package/dist/core/canonical.d.ts +15 -7
  44. package/dist/core/canonical.js +101 -13
  45. package/dist/core/capabilitySelection.d.ts +45 -0
  46. package/dist/core/capabilitySelection.js +124 -0
  47. package/dist/core/types.d.ts +25 -2
  48. package/dist/core/types.js +8 -0
  49. package/dist/lib/agentBridge.d.ts +169 -0
  50. package/dist/lib/agentBridge.js +367 -0
  51. package/dist/lib/agentBridgeProvider.d.ts +14 -0
  52. package/dist/lib/agentBridgeProvider.js +95 -0
  53. package/dist/lib/backup.d.ts +32 -0
  54. package/dist/lib/backup.js +20 -1
  55. package/dist/lib/builtinSkills.d.ts +2 -0
  56. package/dist/lib/builtinSkills.js +4 -0
  57. package/dist/lib/chatAgents.d.ts +42 -0
  58. package/dist/lib/chatAgents.js +59 -0
  59. package/dist/lib/cliMetadata.d.ts +1 -0
  60. package/dist/lib/cliMetadata.js +14 -0
  61. package/dist/lib/envVarNames.d.ts +3 -0
  62. package/dist/lib/envVarNames.js +19 -0
  63. package/dist/lib/gatewayBackend.d.ts +28 -0
  64. package/dist/lib/gatewayBackend.js +72 -1
  65. package/dist/lib/installAgent.d.ts +7 -5
  66. package/dist/lib/installAgent.js +4 -3
  67. package/dist/lib/mcpConnect.js +5 -1
  68. package/dist/lib/mcpMigrateRead.d.ts +2 -0
  69. package/dist/lib/mcpMigrateRead.js +22 -1
  70. package/dist/lib/mcpRuntime.d.ts +46 -0
  71. package/dist/lib/mcpRuntime.js +185 -0
  72. package/dist/lib/mcpStatusProvider.d.ts +10 -0
  73. package/dist/lib/mcpStatusProvider.js +25 -0
  74. package/dist/lib/mcpToolRegistry.d.ts +18 -0
  75. package/dist/lib/mcpToolRegistry.js +107 -14
  76. package/dist/lib/memoryGraph.d.ts +1 -1
  77. package/dist/lib/memoryGraph.js +9 -3
  78. package/dist/lib/memoryProvider.d.ts +58 -0
  79. package/dist/lib/memoryProvider.js +175 -0
  80. package/dist/lib/memoryStatus.d.ts +17 -0
  81. package/dist/lib/memoryStatus.js +88 -0
  82. package/dist/lib/nativeMemory.d.ts +19 -0
  83. package/dist/lib/nativeMemory.js +129 -0
  84. package/dist/lib/realHomeSnapshot.js +5 -0
  85. package/dist/lib/runtimeControlProvider.d.ts +16 -0
  86. package/dist/lib/runtimeControlProvider.js +170 -0
  87. package/dist/lib/secretEnv.d.ts +2 -1
  88. package/dist/lib/secretEnv.js +6 -2
  89. package/dist/lib/skillProvider.d.ts +13 -0
  90. package/dist/lib/skillProvider.js +210 -0
  91. package/dist/lib/taskProvider.d.ts +7 -0
  92. package/dist/lib/taskProvider.js +43 -0
  93. package/dist/lib/taskStore.d.ts +46 -0
  94. package/dist/lib/taskStore.js +156 -0
  95. package/dist/lib/terminalPicker.d.ts +23 -49
  96. package/dist/lib/terminalPicker.js +80 -218
  97. package/dist/lib/zcodeOwnership.d.ts +17 -0
  98. package/dist/lib/zcodeOwnership.js +37 -0
  99. package/dist/pi-bridge/bundle.js +476 -37
  100. package/dist/pi-bridge/index.js +42 -21
  101. package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
  102. package/dist/pi-bridge/schemaTranslate.js +4 -8
  103. package/dist/probes/codex.d.ts +7 -0
  104. package/dist/probes/codex.js +37 -5
  105. package/dist/probes/kimi-code.d.ts +12 -0
  106. package/dist/probes/kimi-code.js +55 -0
  107. package/dist/probes/zcode.d.ts +32 -0
  108. package/dist/probes/zcode.js +140 -0
  109. package/dist/sdk.d.ts +1 -1
  110. package/docs/architecture.md +230 -28
  111. package/docs/assets/brand-v3/trellis-mark-v3-a.png +0 -0
  112. package/docs/assets/brand-v3/trellis-mark-v3-a.svg +10 -0
  113. package/docs/assets/poster-series-v3/README.md +60 -0
  114. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.png +0 -0
  115. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.svg +51 -0
  116. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.png +0 -0
  117. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.svg +51 -0
  118. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.png +0 -0
  119. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.svg +48 -0
  120. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.png +0 -0
  121. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.svg +48 -0
  122. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.png +0 -0
  123. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.svg +56 -0
  124. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.png +0 -0
  125. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.svg +56 -0
  126. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.png +0 -0
  127. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.svg +47 -0
  128. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.png +0 -0
  129. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.svg +47 -0
  130. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.png +0 -0
  131. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.svg +51 -0
  132. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.png +0 -0
  133. package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.svg +51 -0
  134. package/docs/getting-started.md +496 -42
  135. package/docs/overview.md +134 -0
  136. package/docs/overview.zh-CN.md +102 -0
  137. package/docs/research.md +59 -0
  138. package/docs/roadmap.md +11 -6
  139. package/docs/video/README.md +76 -0
  140. package/docs/video/build/audio/01-opening.aiff +0 -0
  141. package/docs/video/build/audio/01-opening.mp3 +0 -0
  142. package/docs/video/build/audio/02-pain.aiff +0 -0
  143. package/docs/video/build/audio/02-pain.mp3 +0 -0
  144. package/docs/video/build/audio/03-solution.aiff +0 -0
  145. package/docs/video/build/audio/03-solution.mp3 +0 -0
  146. package/docs/video/build/audio/04-install.aiff +0 -0
  147. package/docs/video/build/audio/04-install.mp3 +0 -0
  148. package/docs/video/build/audio/04-setup.mp3 +0 -0
  149. package/docs/video/build/audio/05-dryrun.mp3 +0 -0
  150. package/docs/video/build/audio/05-init.aiff +0 -0
  151. package/docs/video/build/audio/05-init.mp3 +0 -0
  152. package/docs/video/build/audio/05-interactive.mp3 +0 -0
  153. package/docs/video/build/audio/06-dryrun.aiff +0 -0
  154. package/docs/video/build/audio/06-dryrun.mp3 +0 -0
  155. package/docs/video/build/audio/06-onboard.mp3 +0 -0
  156. package/docs/video/build/audio/06-run.mp3 +0 -0
  157. package/docs/video/build/audio/07-onboard.aiff +0 -0
  158. package/docs/video/build/audio/07-onboard.mp3 +0 -0
  159. package/docs/video/build/audio/07-verify.mp3 +0 -0
  160. package/docs/video/build/audio/08-closing.mp3 +0 -0
  161. package/docs/video/build/audio/08-doctor.aiff +0 -0
  162. package/docs/video/build/audio/08-doctor.mp3 +0 -0
  163. package/docs/video/build/audio/09-rollback.aiff +0 -0
  164. package/docs/video/build/audio/09-rollback.mp3 +0 -0
  165. package/docs/video/build/audio/10-principles.aiff +0 -0
  166. package/docs/video/build/audio/10-principles.mp3 +0 -0
  167. package/docs/video/build/audio/11-closing.aiff +0 -0
  168. package/docs/video/build/audio/11-closing.mp3 +0 -0
  169. package/docs/video/build/frames/01-opening-0.png +0 -0
  170. package/docs/video/build/frames/02-pain-1.png +0 -0
  171. package/docs/video/build/frames/02-pain-2.png +0 -0
  172. package/docs/video/build/frames/03-solution-3.png +0 -0
  173. package/docs/video/build/frames/04-install-4.png +0 -0
  174. package/docs/video/build/frames/04-setup-4.png +0 -0
  175. package/docs/video/build/frames/04-setup-5.png +0 -0
  176. package/docs/video/build/frames/05-dryrun-6.png +0 -0
  177. package/docs/video/build/frames/05-init-5.png +0 -0
  178. package/docs/video/build/frames/05-interactive-5.png +0 -0
  179. package/docs/video/build/frames/05-interactive-6.png +0 -0
  180. package/docs/video/build/frames/06-dryrun-6.png +0 -0
  181. package/docs/video/build/frames/06-dryrun-7.png +0 -0
  182. package/docs/video/build/frames/06-onboard-7.png +0 -0
  183. package/docs/video/build/frames/06-onboard-8.png +0 -0
  184. package/docs/video/build/frames/06-run-7.png +0 -0
  185. package/docs/video/build/frames/06-run-8.png +0 -0
  186. package/docs/video/build/frames/07-onboard-10.png +0 -0
  187. package/docs/video/build/frames/07-onboard-7.png +0 -0
  188. package/docs/video/build/frames/07-onboard-8.png +0 -0
  189. package/docs/video/build/frames/07-onboard-9.png +0 -0
  190. package/docs/video/build/frames/07-verify-10.png +0 -0
  191. package/docs/video/build/frames/07-verify-8.png +0 -0
  192. package/docs/video/build/frames/07-verify-9.png +0 -0
  193. package/docs/video/build/frames/08-closing-10.png +0 -0
  194. package/docs/video/build/frames/08-closing-11.png +0 -0
  195. package/docs/video/build/frames/08-doctor-11.png +0 -0
  196. package/docs/video/build/frames/08-doctor-9.png +0 -0
  197. package/docs/video/build/frames/09-rollback-10.png +0 -0
  198. package/docs/video/build/frames/09-rollback-12.png +0 -0
  199. package/docs/video/build/frames/10-principles-11.png +0 -0
  200. package/docs/video/build/frames/10-principles-13.png +0 -0
  201. package/docs/video/build/frames/11-closing-12.png +0 -0
  202. package/docs/video/build/frames/11-closing-14.png +0 -0
  203. package/docs/video/build/segments/01-opening.mp4 +0 -0
  204. package/docs/video/build/segments/01-opening.txt +3 -0
  205. package/docs/video/build/segments/02-pain.mp4 +0 -0
  206. package/docs/video/build/segments/02-pain.txt +5 -0
  207. package/docs/video/build/segments/03-solution.mp4 +0 -0
  208. package/docs/video/build/segments/03-solution.txt +3 -0
  209. package/docs/video/build/segments/04-install.mp4 +0 -0
  210. package/docs/video/build/segments/04-install.txt +3 -0
  211. package/docs/video/build/segments/04-setup.mp4 +0 -0
  212. package/docs/video/build/segments/04-setup.txt +3 -0
  213. package/docs/video/build/segments/05-dryrun.mp4 +0 -0
  214. package/docs/video/build/segments/05-dryrun.txt +3 -0
  215. package/docs/video/build/segments/05-init.mp4 +0 -0
  216. package/docs/video/build/segments/05-init.txt +3 -0
  217. package/docs/video/build/segments/05-interactive.mp4 +0 -0
  218. package/docs/video/build/segments/05-interactive.txt +5 -0
  219. package/docs/video/build/segments/06-dryrun.mp4 +0 -0
  220. package/docs/video/build/segments/06-dryrun.txt +3 -0
  221. package/docs/video/build/segments/06-onboard.mp4 +0 -0
  222. package/docs/video/build/segments/06-onboard.txt +5 -0
  223. package/docs/video/build/segments/06-run.mp4 +0 -0
  224. package/docs/video/build/segments/06-run.txt +3 -0
  225. package/docs/video/build/segments/07-onboard.mp4 +0 -0
  226. package/docs/video/build/segments/07-onboard.txt +5 -0
  227. package/docs/video/build/segments/07-verify.mp4 +0 -0
  228. package/docs/video/build/segments/07-verify.txt +5 -0
  229. package/docs/video/build/segments/08-closing.mp4 +0 -0
  230. package/docs/video/build/segments/08-closing.txt +3 -0
  231. package/docs/video/build/segments/08-doctor.mp4 +0 -0
  232. package/docs/video/build/segments/08-doctor.txt +3 -0
  233. package/docs/video/build/segments/09-rollback.mp4 +0 -0
  234. package/docs/video/build/segments/09-rollback.txt +3 -0
  235. package/docs/video/build/segments/10-principles.mp4 +0 -0
  236. package/docs/video/build/segments/10-principles.txt +3 -0
  237. package/docs/video/build/segments/11-closing.mp4 +0 -0
  238. package/docs/video/build/segments/11-closing.txt +3 -0
  239. package/docs/video/build/segments/final.txt +8 -0
  240. package/docs/video/build-ai/audio/01-hook.mp3 +0 -0
  241. package/docs/video/build-ai/audio/02-pain.mp3 +0 -0
  242. package/docs/video/build-ai/audio/03-solution.mp3 +0 -0
  243. package/docs/video/build-ai/audio/04-how.mp3 +0 -0
  244. package/docs/video/build-ai/audio/05-safe.mp3 +0 -0
  245. package/docs/video/build-ai/audio/06-closing.mp3 +0 -0
  246. package/docs/video/build-ai/clips/01-hook.mp4 +0 -0
  247. package/docs/video/build-ai/clips/02-pain.mp4 +0 -0
  248. package/docs/video/build-ai/clips/03-solution.mp4 +0 -0
  249. package/docs/video/build-ai/clips/04-how.mp4 +0 -0
  250. package/docs/video/build-ai/clips/05-safe.mp4 +0 -0
  251. package/docs/video/build-ai/clips/06-closing.mp4 +0 -0
  252. package/docs/video/build-ai/first-frame-02-pain.png +0 -0
  253. package/docs/video/build-ai/first-frame-03-solution.png +0 -0
  254. package/docs/video/build-ai/first-frame-04-how.png +0 -0
  255. package/docs/video/build-ai/first-frame-05-safe.png +0 -0
  256. package/docs/video/build-ai/segments/01-hook.caption.txt +2 -0
  257. package/docs/video/build-ai/segments/01-hook.mp4 +0 -0
  258. package/docs/video/build-ai/segments/02-pain.mp4 +0 -0
  259. package/docs/video/build-ai/segments/03-solution.mp4 +0 -0
  260. package/docs/video/build-ai/segments/04-how.mp4 +0 -0
  261. package/docs/video/build-ai/segments/05-safe.mp4 +0 -0
  262. package/docs/video/build-ai/segments/06-closing.mp4 +0 -0
  263. package/docs/video/build-ai/segments/cap-01-hook.png +0 -0
  264. package/docs/video/build-ai/segments/cap-02-pain.png +0 -0
  265. package/docs/video/build-ai/segments/cap-03-solution.png +0 -0
  266. package/docs/video/build-ai/segments/cap-04-how.png +0 -0
  267. package/docs/video/build-ai/segments/cap-05-safe.png +0 -0
  268. package/docs/video/build-ai/segments/cap-06-closing.png +0 -0
  269. package/docs/video/build-ai/segments/final.txt +6 -0
  270. package/docs/video/build-posters/audio/01-hook.mp3 +0 -0
  271. package/docs/video/build-posters/audio/02-pain.mp3 +0 -0
  272. package/docs/video/build-posters/audio/03-solution.mp3 +0 -0
  273. package/docs/video/build-posters/audio/04-how.mp3 +0 -0
  274. package/docs/video/build-posters/audio/05-safe.mp3 +0 -0
  275. package/docs/video/build-posters/audio/06-closing.mp3 +0 -0
  276. package/docs/video/build-posters/cap-01-hook.png +0 -0
  277. package/docs/video/build-posters/cap-02-pain.png +0 -0
  278. package/docs/video/build-posters/cap-03-solution.png +0 -0
  279. package/docs/video/build-posters/cap-04-how.png +0 -0
  280. package/docs/video/build-posters/cap-05-safe.png +0 -0
  281. package/docs/video/build-posters/cap-06-closing.png +0 -0
  282. package/docs/video/build-posters/card-01-hook.png +0 -0
  283. package/docs/video/build-posters/card-06-closing.png +0 -0
  284. package/docs/video/build-posters/segments/01-hook.mp4 +0 -0
  285. package/docs/video/build-posters/segments/02-pain.mp4 +0 -0
  286. package/docs/video/build-posters/segments/03-solution.mp4 +0 -0
  287. package/docs/video/build-posters/segments/04-how.mp4 +0 -0
  288. package/docs/video/build-posters/segments/05-safe.mp4 +0 -0
  289. package/docs/video/build-posters/segments/06-closing.mp4 +0 -0
  290. package/docs/video/build-posters/segments/final.txt +6 -0
  291. package/docs/video/generate-video-ai.mjs +334 -0
  292. package/docs/video/generate-video-posters.mjs +204 -0
  293. package/docs/video/generate-video.mjs +426 -0
  294. package/docs/video/outline.zh-CN.md +102 -0
  295. package/docs/video/trellis-demo-video-ai.mp4 +0 -0
  296. package/docs/video/trellis-demo-video.mp4 +0 -0
  297. package/docs/video/voiceover.zh-CN.md +49 -0
  298. package/package.json +6 -2
  299. package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
  300. package/schema/capability-selection.example.yaml +23 -0
  301. package/schema/scope.example.yaml +1 -1
  302. package/schema/servers.example.yaml +15 -1
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Explicit lifecycle management for `~/.trellis/managed.yaml`.
3
+ *
4
+ * Onboarding stays additive: omission must never silently revoke Trellis's
5
+ * write authorization. Exact replacement and subtraction live here so they
6
+ * are deliberate, visible in shell history, dry-runnable, and recoverable.
7
+ */
8
+ import type { AgentId } from "../core/types.js";
9
+ export type ManageOperation = "list" | "set" | "add" | "remove";
10
+ export interface ManagePlan {
11
+ operation: ManageOperation;
12
+ current: AgentId[];
13
+ desired: AgentId[];
14
+ added: AgentId[];
15
+ removed: AgentId[];
16
+ changed: boolean;
17
+ }
18
+ export interface ParsedManageArgs {
19
+ operation: ManageOperation;
20
+ ids?: string;
21
+ json: boolean;
22
+ dryRun: boolean;
23
+ }
24
+ export type ManagePlanResult = {
25
+ plan: ManagePlan;
26
+ } | {
27
+ error: string;
28
+ };
29
+ export declare function parseManageArgs(argv: readonly string[]): ParsedManageArgs | {
30
+ error: string;
31
+ };
32
+ export declare function collectManagePlan(operation: ManageOperation, rawIds: string | undefined, homeDir?: string): ManagePlanResult;
33
+ export declare function applyManagePlan(plan: ManagePlan, homeDir?: string): void;
34
+ export declare function runManage(parsed: ParsedManageArgs, opts?: {
35
+ homeDir?: string;
36
+ }): {
37
+ exitCode: number;
38
+ plan?: ManagePlan;
39
+ };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Explicit lifecycle management for `~/.trellis/managed.yaml`.
3
+ *
4
+ * Onboarding stays additive: omission must never silently revoke Trellis's
5
+ * write authorization. Exact replacement and subtraction live here so they
6
+ * are deliberate, visible in shell history, dry-runnable, and recoverable.
7
+ */
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { loadCanonicalSource } from "../core/canonical.js";
11
+ import { ALL_AGENTS } from "../core/types.js";
12
+ import { openBackupSession } from "../lib/backup.js";
13
+ function stableAgents(ids) {
14
+ const selected = new Set(ids);
15
+ return ALL_AGENTS.filter((id) => selected.has(id));
16
+ }
17
+ function parseAgentList(raw, allowNone) {
18
+ if (raw === undefined || raw.trim() === "") {
19
+ return { error: `agent list is required — use a comma-separated subset of ${ALL_AGENTS.join(", ")}${allowNone ? ", or none" : ""}` };
20
+ }
21
+ if (raw.trim().toLowerCase() === "none") {
22
+ return allowNone ? { agents: [] } : { error: '"none" is valid only for `trellis manage set none`' };
23
+ }
24
+ const tokens = raw.split(",").map((token) => token.trim()).filter(Boolean);
25
+ const invalid = tokens.filter((token) => !ALL_AGENTS.includes(token));
26
+ if (invalid.length > 0) {
27
+ return { error: `unknown agent id${invalid.length === 1 ? "" : "s"}: ${invalid.join(", ")} — expected ${ALL_AGENTS.join(", ")}` };
28
+ }
29
+ return { agents: stableAgents(tokens) };
30
+ }
31
+ export function parseManageArgs(argv) {
32
+ const [rawOperation, ...rest] = argv;
33
+ if (rawOperation !== "list" && rawOperation !== "set" && rawOperation !== "add" && rawOperation !== "remove") {
34
+ return { error: `unknown manage operation "${rawOperation ?? "(none)"}" — expected list, set, add, or remove` };
35
+ }
36
+ const unknownFlags = rest.filter((arg) => arg.startsWith("--") && arg !== "--json" && arg !== "--dry-run");
37
+ if (unknownFlags.length > 0)
38
+ return { error: `unknown manage flag "${unknownFlags[0]}"` };
39
+ const positional = rest.filter((arg) => !arg.startsWith("--"));
40
+ if (rawOperation === "list") {
41
+ if (positional.length > 0)
42
+ return { error: "manage list accepts no agent list" };
43
+ }
44
+ else if (positional.length !== 1) {
45
+ return { error: `manage ${rawOperation} requires exactly one comma-separated agent list` };
46
+ }
47
+ return {
48
+ operation: rawOperation,
49
+ ids: positional[0],
50
+ json: rest.includes("--json"),
51
+ dryRun: rest.includes("--dry-run"),
52
+ };
53
+ }
54
+ export function collectManagePlan(operation, rawIds, homeDir = homedir()) {
55
+ const current = stableAgents(loadCanonicalSource(homeDir).managedAgents);
56
+ if (operation === "list") {
57
+ return { plan: { operation, current, desired: current, added: [], removed: [], changed: false } };
58
+ }
59
+ const parsed = parseAgentList(rawIds, operation === "set");
60
+ if ("error" in parsed)
61
+ return parsed;
62
+ const requested = new Set(parsed.agents);
63
+ const desired = operation === "set"
64
+ ? stableAgents(requested)
65
+ : operation === "add"
66
+ ? stableAgents([...current, ...requested])
67
+ : stableAgents(current.filter((id) => !requested.has(id)));
68
+ const currentSet = new Set(current);
69
+ const desiredSet = new Set(desired);
70
+ const added = desired.filter((id) => !currentSet.has(id));
71
+ const removed = current.filter((id) => !desiredSet.has(id));
72
+ return {
73
+ plan: {
74
+ operation,
75
+ current,
76
+ desired,
77
+ added,
78
+ removed,
79
+ changed: added.length > 0 || removed.length > 0,
80
+ },
81
+ };
82
+ }
83
+ export function applyManagePlan(plan, homeDir = homedir()) {
84
+ if (!plan.changed || plan.operation === "list")
85
+ return;
86
+ const session = openBackupSession(homeDir, `manage-${plan.operation}`);
87
+ session.writeFile(join(homeDir, ".trellis", "managed.yaml"), `agents: [${plan.desired.join(", ")}]\n`);
88
+ session.finalize();
89
+ }
90
+ function displayAgents(agents) {
91
+ return agents.length > 0 ? agents.join(", ") : "(none)";
92
+ }
93
+ export function runManage(parsed, opts = {}) {
94
+ const homeDir = opts.homeDir ?? homedir();
95
+ let result;
96
+ try {
97
+ result = collectManagePlan(parsed.operation, parsed.ids, homeDir);
98
+ }
99
+ catch (err) {
100
+ console.error(err instanceof Error ? err.message : String(err));
101
+ return { exitCode: 1 };
102
+ }
103
+ if ("error" in result) {
104
+ console.error(result.error);
105
+ return { exitCode: 1 };
106
+ }
107
+ const { plan } = result;
108
+ if (!parsed.dryRun)
109
+ applyManagePlan(plan, homeDir);
110
+ if (parsed.json) {
111
+ console.log(JSON.stringify(plan, null, 2));
112
+ }
113
+ else if (parsed.operation === "list") {
114
+ console.log(`managed agents: ${displayAgents(plan.current)}`);
115
+ }
116
+ else {
117
+ console.log(`${parsed.dryRun ? "[dry run] " : ""}managed agents: ${displayAgents(plan.current)} -> ${displayAgents(plan.desired)}`);
118
+ if (!plan.changed)
119
+ console.log(" no change");
120
+ if (plan.added.length > 0)
121
+ console.log(` added: ${plan.added.join(", ")}`);
122
+ if (plan.removed.length > 0)
123
+ console.log(` detached: ${plan.removed.join(", ")} (native state preserved)`);
124
+ }
125
+ return { exitCode: 0, plan };
126
+ }
@@ -5,14 +5,16 @@
5
5
  * gated — remove), and reports what happened. Kept as a separate
6
6
  * command from bare `trellis sync` (skills/instructions) since the two
7
7
  * have different write mechanisms entirely, not because MCP removal is
8
- * unsafe (see `src/lib/mcpOwnership.ts`). `list`/`add`/`remove`
9
- * (trellis-canonical-cli-crud) are canonical-side only — an alternative
10
- * to hand-editing `~/.trellis/mcp/servers.yaml`, never touching any
11
- * agent's native config (that stays `sync`'s job).
8
+ * unsafe (see `src/lib/mcpOwnership.ts`). `add`/`remove` mutate the
9
+ * canonical source and then run the same sync `mcp sync` performs, so
10
+ * one command both edits the list and maps it onto the managed agents
11
+ * (removal propagates through the ownership ledger). `list` is
12
+ * read-only. All three are an alternative to hand-editing
13
+ * `~/.trellis/mcp/servers.yaml`.
12
14
  */
13
15
  import { type ServersYamlWriteResult } from "../core/canonical.js";
14
16
  import type { AdapterPlanItem } from "../core/adapter.js";
15
- import type { AgentId, McpServerDef, Transport } from "../core/types.js";
17
+ import type { AgentId, McpAuthMode, McpConfig, McpServerDef, Transport } from "../core/types.js";
16
18
  import { type BackupSession } from "../lib/backup.js";
17
19
  export interface RunMcpSyncOptions {
18
20
  json?: boolean;
@@ -24,6 +26,9 @@ export interface RunMcpSyncOptions {
24
26
  /** Onboard-only seam — see RunSyncOptions.managedAgents. Never a CLI
25
27
  * flag. */
26
28
  managedAgents?: readonly AgentId[];
29
+ /** Onboard-only canonical MCP override used to preview a route selection
30
+ * before writing it to servers.yaml. */
31
+ mcp?: McpConfig;
27
32
  /** Onboard-only seam — see RunSyncOptions.backupSession. Never a CLI
28
33
  * flag. */
29
34
  backupSession?: BackupSession;
@@ -47,6 +52,7 @@ export declare function printReport(report: McpSyncReport, dryRun: boolean): voi
47
52
  export interface McpListEntry {
48
53
  name: string;
49
54
  transport: Transport;
55
+ auth?: McpAuthMode;
50
56
  enabled: boolean;
51
57
  agents: readonly AgentId[];
52
58
  command?: string;
@@ -75,6 +81,7 @@ export declare function runMcpList(opts?: {
75
81
  * command's own inline flag parsing in src/cli.ts. */
76
82
  export interface McpAddRawArgs {
77
83
  transport?: string;
84
+ auth?: string;
78
85
  command?: string;
79
86
  /** Comma-separated. */
80
87
  args?: string;
@@ -107,29 +114,85 @@ export interface McpAddPlan {
107
114
  * always refuses, ever (design.md D4).
108
115
  */
109
116
  export declare function collectMcpAddPlan(name: string | undefined, raw: McpAddRawArgs, homeDir?: string): McpAddPlan;
110
- export declare function applyMcpAddPlan(plan: McpAddPlan, homeDir?: string): ServersYamlWriteResult;
111
- export declare function runMcpAdd(name: string | undefined, raw: McpAddRawArgs, opts?: {
117
+ export type McpSetAuthAction = "updated" | "already-set" | "not-found" | "invalid-input";
118
+ export interface McpSetAuthPlan {
119
+ name: string;
120
+ auth: "oauth" | "none";
121
+ action: McpSetAuthAction;
122
+ detail: string;
123
+ def?: McpServerDef;
124
+ }
125
+ export declare function collectMcpSetAuthPlan(name: string | undefined, auth: string | undefined, homeDir?: string): McpSetAuthPlan;
126
+ export declare function runMcpSetAuth(name: string | undefined, auth: string | undefined, opts?: {
112
127
  homeDir?: string;
113
128
  json?: boolean;
114
129
  dryRun?: boolean;
115
130
  }): {
116
131
  exitCode: number;
117
132
  };
133
+ export declare function applyMcpAddPlan(plan: McpAddPlan, homeDir?: string, backup?: BackupSession): ServersYamlWriteResult;
134
+ export interface McpAddOutcome {
135
+ plan: McpAddPlan;
136
+ writeError?: string;
137
+ sync?: McpSyncReport;
138
+ }
139
+ /**
140
+ * The apply-and-cascade-sync orchestration `runMcpAdd` used to do inline
141
+ * and only expose via a console.log'd payload — split out so a
142
+ * non-CLI caller (trellis-gui's sidecar) can get the same structured
143
+ * result a JSON-mode CLI run prints, without scraping stdout or
144
+ * re-deriving the same sequencing itself (trellis-gui design.md
145
+ * Decision 1). Behavior-preserving: `runMcpAdd` below is now a thin
146
+ * wrapper over this plus its own printing, same as every other command
147
+ * in this file already splits compute from CLI I/O.
148
+ *
149
+ * Opens its own backup session around the direct `servers.yaml` write
150
+ * when the caller doesn't already have one open — this canonical write
151
+ * used to be the one path in this file that bypassed backup/rollback
152
+ * entirely (trellis-gui tasks.md 3.5 found this empirically: a real `mcp
153
+ * add` left zero trace in `trellis rollback --list`). Mirrors the
154
+ * `ownSession` pattern `collectMcpSyncReport` below already uses for
155
+ * exactly the same reason.
156
+ */
157
+ export declare function applyMcpAddWithSync(plan: McpAddPlan, opts?: {
158
+ homeDir?: string;
159
+ dryRun?: boolean;
160
+ backupSession?: BackupSession;
161
+ }): Promise<McpAddOutcome>;
162
+ export declare function runMcpAdd(name: string | undefined, raw: McpAddRawArgs, opts?: {
163
+ homeDir?: string;
164
+ json?: boolean;
165
+ dryRun?: boolean;
166
+ }): Promise<{
167
+ exitCode: number;
168
+ }>;
118
169
  export type McpRemoveAction = "removed" | "not-found";
119
170
  export interface McpRemovePlan {
120
171
  name: string;
121
172
  action: McpRemoveAction;
122
173
  }
123
- /** Canonical-side only — never touches an agent's native config. Making
124
- * an already-synced agent forget it is `mcp sync`'s own removal-
125
- * propagation gap, tracked separately (roadmap.md P14), not this
126
- * command's job. */
174
+ /** Canonical plan only — `runMcpRemove` follows a real removal with the
175
+ * same sync `mcp sync` performs, which propagates the deletion to every
176
+ * agent whose entry the ownership ledger proves Trellis wrote
177
+ * (trellis-mcp-lifecycle-parity, roadmap P14). */
127
178
  export declare function collectMcpRemovePlan(name: string, homeDir?: string): McpRemovePlan;
128
- export declare function applyMcpRemovePlan(plan: McpRemovePlan, homeDir?: string): ServersYamlWriteResult;
179
+ export declare function applyMcpRemovePlan(plan: McpRemovePlan, homeDir?: string, backup?: BackupSession): ServersYamlWriteResult;
180
+ export interface McpRemoveOutcome {
181
+ plan: McpRemovePlan;
182
+ writeError?: string;
183
+ sync?: McpSyncReport;
184
+ }
185
+ /** See `applyMcpAddWithSync`'s doc comment — same split, same reason, same
186
+ * own-backup-session fix. */
187
+ export declare function applyMcpRemoveWithSync(plan: McpRemovePlan, opts?: {
188
+ homeDir?: string;
189
+ dryRun?: boolean;
190
+ backupSession?: BackupSession;
191
+ }): Promise<McpRemoveOutcome>;
129
192
  export declare function runMcpRemove(name: string, opts?: {
130
193
  homeDir?: string;
131
194
  json?: boolean;
132
195
  dryRun?: boolean;
133
- }): {
196
+ }): Promise<{
134
197
  exitCode: number;
135
- };
198
+ }>;
@@ -5,12 +5,15 @@
5
5
  * gated — remove), and reports what happened. Kept as a separate
6
6
  * command from bare `trellis sync` (skills/instructions) since the two
7
7
  * have different write mechanisms entirely, not because MCP removal is
8
- * unsafe (see `src/lib/mcpOwnership.ts`). `list`/`add`/`remove`
9
- * (trellis-canonical-cli-crud) are canonical-side only — an alternative
10
- * to hand-editing `~/.trellis/mcp/servers.yaml`, never touching any
11
- * agent's native config (that stays `sync`'s job).
8
+ * unsafe (see `src/lib/mcpOwnership.ts`). `add`/`remove` mutate the
9
+ * canonical source and then run the same sync `mcp sync` performs, so
10
+ * one command both edits the list and maps it onto the managed agents
11
+ * (removal propagates through the ownership ledger). `list` is
12
+ * read-only. All three are an alternative to hand-editing
13
+ * `~/.trellis/mcp/servers.yaml`.
12
14
  */
13
15
  import { homedir } from "node:os";
16
+ import { existsSync } from "node:fs";
14
17
  import { join } from "node:path";
15
18
  import { loadCanonicalSource, upsertServerYaml, removeServerYaml } from "../core/canonical.js";
16
19
  import { ALL_AGENTS, resolveScope } from "../core/types.js";
@@ -18,12 +21,16 @@ import { ClaudeCodeAdapter } from "../adapters/claude-code.js";
18
21
  import { CodexAdapter } from "../adapters/codex.js";
19
22
  import { KiroAdapter } from "../adapters/kiro.js";
20
23
  import { PiAdapter } from "../adapters/pi.js";
24
+ import { KimiCodeAdapter } from "../adapters/kimi-code.js";
25
+ import { ZcodeAdapter } from "../adapters/zcode.js";
21
26
  import { openBackupSession } from "../lib/backup.js";
22
27
  const ADAPTER_FACTORY = {
23
28
  "claude-code": (homeDir) => new ClaudeCodeAdapter(homeDir),
24
29
  codex: (homeDir) => new CodexAdapter(homeDir),
25
30
  kiro: (homeDir) => new KiroAdapter(homeDir),
26
31
  pi: (homeDir) => new PiAdapter(homeDir),
32
+ "kimi-code": (homeDir) => new KimiCodeAdapter(homeDir),
33
+ zcode: (homeDir) => new ZcodeAdapter(homeDir),
27
34
  };
28
35
  /** Only agents in `canonical.managedAgents` — see src/commands/sync.ts's
29
36
  * own copy of this same restriction (trellis-managed-agents). */
@@ -33,7 +40,11 @@ function buildAdapters(homeDir, managedAgents) {
33
40
  export async function collectMcpSyncReport(opts = {}) {
34
41
  const homeDir = opts.homeDir ?? homedir();
35
42
  const loaded = loadCanonicalSource(homeDir);
36
- const canonical = opts.managedAgents ? { ...loaded, managedAgents: opts.managedAgents } : loaded;
43
+ const canonical = {
44
+ ...loaded,
45
+ ...(opts.managedAgents ? { managedAgents: opts.managedAgents } : {}),
46
+ ...(opts.mcp ? { mcp: opts.mcp } : {}),
47
+ };
37
48
  const reports = [];
38
49
  const ownSession = !opts.dryRun && !opts.backupSession ? openBackupSession(homeDir, "mcp-sync") : undefined;
39
50
  const backup = opts.backupSession ?? ownSession;
@@ -107,6 +118,7 @@ export function collectMcpListPlan(homeDir = homedir()) {
107
118
  return Object.entries(canonical.mcp.servers).map(([name, def]) => ({
108
119
  name,
109
120
  transport: def.transport,
121
+ auth: def.auth,
110
122
  enabled: def.enabled ?? true,
111
123
  agents: resolveScope(def.agents, canonical.managedAgents),
112
124
  command: def.command,
@@ -136,7 +148,7 @@ export function runMcpList(opts = {}) {
136
148
  else {
137
149
  for (const e of entries) {
138
150
  const scope = e.agents.length > 0 ? e.agents.join(", ") : "(no managed agent reaches it)";
139
- console.log(`${e.name} (${e.transport})${e.enabled ? "" : " [disabled]"} — ${scope}`);
151
+ console.log(`${e.name} (${e.transport}${e.auth === "oauth" ? ", oauth" : ""})${e.enabled ? "" : " [disabled]"} — ${scope}`);
140
152
  if (e.env && e.env.length > 0)
141
153
  console.log(` env: ${e.env.join(", ")} (values never read/printed)`);
142
154
  if (e.staticEnv)
@@ -152,6 +164,7 @@ export function parseMcpAddArgs(argv) {
152
164
  };
153
165
  return {
154
166
  transport: flag("--transport"),
167
+ auth: flag("--auth"),
155
168
  command: flag("--command"),
156
169
  args: flag("--args"),
157
170
  url: flag("--url"),
@@ -202,6 +215,12 @@ export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
202
215
  if ((transport === "http" || transport === "sse") && !raw.url) {
203
216
  return { name, action: "invalid-input", detail: `--transport ${transport} requires --url` };
204
217
  }
218
+ if (raw.auth !== undefined && raw.auth !== "oauth") {
219
+ return { name, action: "invalid-input", detail: `--auth must be "oauth" when provided (got ${raw.auth})` };
220
+ }
221
+ if (raw.auth === "oauth" && transport === "stdio") {
222
+ return { name, action: "invalid-input", detail: "--auth oauth requires an http or sse MCP server" };
223
+ }
205
224
  const agentsList = parseCsv(raw.agents);
206
225
  if (agentsList) {
207
226
  for (const id of agentsList) {
@@ -215,6 +234,7 @@ export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
215
234
  }
216
235
  const def = {
217
236
  transport,
237
+ ...(raw.auth === "oauth" ? { auth: "oauth" } : {}),
218
238
  ...(raw.command ? { command: raw.command } : {}),
219
239
  ...(parseCsv(raw.args) ? { args: parseCsv(raw.args) } : {}),
220
240
  ...(raw.url ? { url: raw.url } : {}),
@@ -234,52 +254,164 @@ export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
234
254
  }
235
255
  return { name, action: "create", detail: "will add to servers.yaml", def };
236
256
  }
237
- export function applyMcpAddPlan(plan, homeDir = homedir()) {
238
- if (plan.action !== "create" || !plan.def)
239
- return { ok: true };
240
- return upsertServerYaml(serversYamlPath(homeDir), plan.name, plan.def);
257
+ export function collectMcpSetAuthPlan(name, auth, homeDir = homedir()) {
258
+ const mode = auth === "oauth" || auth === "none" ? auth : undefined;
259
+ if (!name)
260
+ return { name: "(none)", auth: (mode ?? "none"), action: "invalid-input", detail: "usage: trellis mcp set <name> --auth oauth|none" };
261
+ if (!mode)
262
+ return { name, auth: "none", action: "invalid-input", detail: `--auth must be "oauth" or "none" (got ${auth ?? "(missing)"})` };
263
+ const canonical = loadCanonicalSource(homeDir);
264
+ const current = canonical.mcp.servers[name];
265
+ if (!current)
266
+ return { name, auth: mode, action: "not-found", detail: `no canonical MCP server named "${name}"` };
267
+ if (mode === "oauth" && !current.url) {
268
+ return { name, auth: mode, action: "invalid-input", detail: "auth: oauth requires an http or sse MCP server" };
269
+ }
270
+ const next = mode === "oauth" ? { ...current, auth: "oauth" } : (() => {
271
+ const { auth: _auth, ...withoutAuth } = current;
272
+ return withoutAuth;
273
+ })();
274
+ if (JSON.stringify(current) === JSON.stringify(next))
275
+ return { name, auth: mode, action: "already-set", detail: `auth is already ${mode}`, def: next };
276
+ return { name, auth: mode, action: "updated", detail: `set auth to ${mode}`, def: next };
241
277
  }
242
- export function runMcpAdd(name, raw, opts = {}) {
278
+ export function runMcpSetAuth(name, auth, opts = {}) {
243
279
  const homeDir = opts.homeDir ?? homedir();
244
280
  let plan;
245
281
  try {
246
- plan = collectMcpAddPlan(name, raw, homeDir);
282
+ plan = collectMcpSetAuthPlan(name, auth, homeDir);
247
283
  }
248
284
  catch (err) {
249
285
  console.error(err instanceof Error ? err.message : String(err));
250
286
  return { exitCode: 1 };
251
287
  }
252
288
  let writeError;
253
- if (!opts.dryRun && plan.action === "create") {
254
- const result = applyMcpAddPlan(plan, homeDir);
289
+ if (!opts.dryRun && plan.action === "updated" && plan.def) {
290
+ const result = upsertServerYaml(serversYamlPath(homeDir), plan.name, plan.def);
255
291
  if (!result.ok)
256
292
  writeError = result.error;
257
293
  }
258
294
  if (opts.json) {
259
295
  console.log(JSON.stringify(writeError ? { ...plan, writeError } : plan, null, 2));
260
296
  }
297
+ else {
298
+ console.log(`${opts.dryRun ? "[dry run] " : ""}mcp set ${plan.name}`);
299
+ console.log(` [${plan.action}] ${plan.detail}`);
300
+ if (writeError)
301
+ console.error(` write failed: ${writeError}`);
302
+ }
303
+ return { exitCode: ["invalid-input", "not-found"].includes(plan.action) || Boolean(writeError) ? 1 : 0 };
304
+ }
305
+ export function applyMcpAddPlan(plan, homeDir = homedir(), backup) {
306
+ if (plan.action !== "create" || !plan.def)
307
+ return { ok: true };
308
+ return upsertServerYaml(serversYamlPath(homeDir), plan.name, plan.def, backup);
309
+ }
310
+ /**
311
+ * The apply-and-cascade-sync orchestration `runMcpAdd` used to do inline
312
+ * and only expose via a console.log'd payload — split out so a
313
+ * non-CLI caller (trellis-gui's sidecar) can get the same structured
314
+ * result a JSON-mode CLI run prints, without scraping stdout or
315
+ * re-deriving the same sequencing itself (trellis-gui design.md
316
+ * Decision 1). Behavior-preserving: `runMcpAdd` below is now a thin
317
+ * wrapper over this plus its own printing, same as every other command
318
+ * in this file already splits compute from CLI I/O.
319
+ *
320
+ * Opens its own backup session around the direct `servers.yaml` write
321
+ * when the caller doesn't already have one open — this canonical write
322
+ * used to be the one path in this file that bypassed backup/rollback
323
+ * entirely (trellis-gui tasks.md 3.5 found this empirically: a real `mcp
324
+ * add` left zero trace in `trellis rollback --list`). Mirrors the
325
+ * `ownSession` pattern `collectMcpSyncReport` below already uses for
326
+ * exactly the same reason.
327
+ */
328
+ export async function applyMcpAddWithSync(plan, opts = {}) {
329
+ const homeDir = opts.homeDir ?? homedir();
330
+ const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "create" ? openBackupSession(homeDir, "mcp-add") : undefined;
331
+ const session = opts.backupSession ?? ownSession;
332
+ let writeError;
333
+ if (!opts.dryRun && plan.action === "create") {
334
+ const result = applyMcpAddPlan(plan, homeDir, session);
335
+ if (!result.ok)
336
+ writeError = result.error;
337
+ }
338
+ ownSession?.finalize();
339
+ const planFailed = plan.action === "conflict" || plan.action === "invalid-input" || Boolean(writeError);
340
+ let syncReport;
341
+ if (!planFailed && plan.action === "create" && existsSync(join(homeDir, ".trellis"))) {
342
+ syncReport = await collectMcpSyncReport({ homeDir, dryRun: opts.dryRun });
343
+ }
344
+ return { plan, ...(writeError ? { writeError } : {}), ...(syncReport ? { sync: syncReport } : {}) };
345
+ }
346
+ export async function runMcpAdd(name, raw, opts = {}) {
347
+ const homeDir = opts.homeDir ?? homedir();
348
+ let plan;
349
+ try {
350
+ plan = collectMcpAddPlan(name, raw, homeDir);
351
+ }
352
+ catch (err) {
353
+ console.error(err instanceof Error ? err.message : String(err));
354
+ return { exitCode: 1 };
355
+ }
356
+ let outcome;
357
+ try {
358
+ outcome = await applyMcpAddWithSync(plan, { homeDir, dryRun: opts.dryRun });
359
+ }
360
+ catch (err) {
361
+ console.error(err instanceof Error ? err.message : String(err));
362
+ return { exitCode: 1 };
363
+ }
364
+ const { writeError, sync: syncReport } = outcome;
365
+ if (opts.json) {
366
+ const payload = writeError ? { ...plan, writeError } : plan;
367
+ console.log(JSON.stringify(syncReport ? { ...payload, sync: syncReport } : payload, null, 2));
368
+ }
261
369
  else {
262
370
  console.log(`${opts.dryRun ? "[dry run] " : ""}mcp add ${plan.name}`);
263
371
  console.log(` [${plan.action}] ${plan.detail}`);
264
372
  if (writeError)
265
373
  console.error(` write failed: ${writeError}`);
374
+ if (syncReport)
375
+ printReport(syncReport, opts.dryRun ?? false);
266
376
  }
267
- return { exitCode: plan.action === "conflict" || plan.action === "invalid-input" || writeError ? 1 : 0 };
377
+ const planFailed = plan.action === "conflict" || plan.action === "invalid-input" || Boolean(writeError);
378
+ const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
379
+ return { exitCode: planFailed || syncConflict ? 1 : 0 };
268
380
  }
269
- /** Canonical-side only — never touches an agent's native config. Making
270
- * an already-synced agent forget it is `mcp sync`'s own removal-
271
- * propagation gap, tracked separately (roadmap.md P14), not this
272
- * command's job. */
381
+ /** Canonical plan only — `runMcpRemove` follows a real removal with the
382
+ * same sync `mcp sync` performs, which propagates the deletion to every
383
+ * agent whose entry the ownership ledger proves Trellis wrote
384
+ * (trellis-mcp-lifecycle-parity, roadmap P14). */
273
385
  export function collectMcpRemovePlan(name, homeDir = homedir()) {
274
386
  const canonical = loadCanonicalSource(homeDir);
275
387
  return { name, action: canonical.mcp.servers[name] ? "removed" : "not-found" };
276
388
  }
277
- export function applyMcpRemovePlan(plan, homeDir = homedir()) {
389
+ export function applyMcpRemovePlan(plan, homeDir = homedir(), backup) {
278
390
  if (plan.action !== "removed")
279
391
  return { ok: true };
280
- return removeServerYaml(serversYamlPath(homeDir), plan.name);
392
+ return removeServerYaml(serversYamlPath(homeDir), plan.name, backup);
393
+ }
394
+ /** See `applyMcpAddWithSync`'s doc comment — same split, same reason, same
395
+ * own-backup-session fix. */
396
+ export async function applyMcpRemoveWithSync(plan, opts = {}) {
397
+ const homeDir = opts.homeDir ?? homedir();
398
+ const ownSession = !opts.dryRun && !opts.backupSession && plan.action === "removed" ? openBackupSession(homeDir, "mcp-remove") : undefined;
399
+ const session = opts.backupSession ?? ownSession;
400
+ let writeError;
401
+ if (!opts.dryRun && plan.action === "removed") {
402
+ const result = applyMcpRemovePlan(plan, homeDir, session);
403
+ if (!result.ok)
404
+ writeError = result.error;
405
+ }
406
+ ownSession?.finalize();
407
+ const planFailed = plan.action === "not-found" || Boolean(writeError);
408
+ let syncReport;
409
+ if (!planFailed && existsSync(join(homeDir, ".trellis"))) {
410
+ syncReport = await collectMcpSyncReport({ homeDir, dryRun: opts.dryRun });
411
+ }
412
+ return { plan, ...(writeError ? { writeError } : {}), ...(syncReport ? { sync: syncReport } : {}) };
281
413
  }
282
- export function runMcpRemove(name, opts = {}) {
414
+ export async function runMcpRemove(name, opts = {}) {
283
415
  const homeDir = opts.homeDir ?? homedir();
284
416
  let plan;
285
417
  try {
@@ -289,14 +421,19 @@ export function runMcpRemove(name, opts = {}) {
289
421
  console.error(err instanceof Error ? err.message : String(err));
290
422
  return { exitCode: 1 };
291
423
  }
292
- let writeError;
293
- if (!opts.dryRun && plan.action === "removed") {
294
- const result = applyMcpRemovePlan(plan, homeDir);
295
- if (!result.ok)
296
- writeError = result.error;
424
+ let outcome;
425
+ try {
426
+ outcome = await applyMcpRemoveWithSync(plan, { homeDir, dryRun: opts.dryRun });
427
+ }
428
+ catch (err) {
429
+ console.error(err instanceof Error ? err.message : String(err));
430
+ return { exitCode: 1 };
297
431
  }
432
+ const { writeError, sync: syncReport } = outcome;
433
+ const planFailed = plan.action === "not-found" || Boolean(writeError);
298
434
  if (opts.json) {
299
- console.log(JSON.stringify(writeError ? { ...plan, writeError } : plan, null, 2));
435
+ const payload = writeError ? { ...plan, writeError } : plan;
436
+ console.log(JSON.stringify(syncReport ? { ...payload, sync: syncReport } : payload, null, 2));
300
437
  }
301
438
  else if (plan.action === "not-found") {
302
439
  console.error(`"${name}" is not a canonical MCP server — nothing to remove.`);
@@ -306,6 +443,9 @@ export function runMcpRemove(name, opts = {}) {
306
443
  }
307
444
  else {
308
445
  console.log(`${opts.dryRun ? "[dry run] " : ""}removed MCP server "${name}" from canonical source.`);
446
+ if (syncReport)
447
+ printReport(syncReport, opts.dryRun ?? false);
309
448
  }
310
- return { exitCode: plan.action === "not-found" || writeError ? 1 : 0 };
449
+ const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
450
+ return { exitCode: planFailed || syncConflict ? 1 : 0 };
311
451
  }
@@ -51,3 +51,9 @@ export declare function resolveGatewayUpstreams(agentId: AgentId, canonical: Pic
51
51
  export declare function runMcpGateway(opts: RunMcpGatewayOptions): Promise<{
52
52
  exitCode: number;
53
53
  }>;
54
+ /** The first-class runtime entrypoint. mcp-gateway remains a compatibility
55
+ * alias for existing native agent config, but both edges use the same
56
+ * provider registry and backend construction path. */
57
+ export declare function runMcpRuntime(opts: RunMcpGatewayOptions): Promise<{
58
+ exitCode: number;
59
+ }>;