agent-trellis 0.1.0 → 0.3.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 (67) hide show
  1. package/README.md +69 -17
  2. package/dist/adapters/claude-code.d.ts +5 -3
  3. package/dist/adapters/claude-code.js +27 -14
  4. package/dist/adapters/codex.d.ts +8 -4
  5. package/dist/adapters/codex.js +47 -16
  6. package/dist/adapters/jsonMcp.d.ts +16 -5
  7. package/dist/adapters/jsonMcp.js +38 -29
  8. package/dist/adapters/kiro.d.ts +5 -3
  9. package/dist/adapters/kiro.js +29 -16
  10. package/dist/adapters/mcpPlan.d.ts +11 -6
  11. package/dist/adapters/mcpPlan.js +40 -7
  12. package/dist/adapters/pi.d.ts +2 -1
  13. package/dist/adapters/pi.js +4 -4
  14. package/dist/adapters/symlinkPlan.d.ts +7 -3
  15. package/dist/adapters/symlinkPlan.js +42 -16
  16. package/dist/cli.js +161 -18
  17. package/dist/commands/init.js +11 -0
  18. package/dist/commands/mcp.d.ts +114 -7
  19. package/dist/commands/mcp.js +258 -17
  20. package/dist/commands/memory.d.ts +39 -0
  21. package/dist/commands/memory.js +78 -0
  22. package/dist/commands/migrate.d.ts +30 -4
  23. package/dist/commands/migrate.js +83 -16
  24. package/dist/commands/onboard.d.ts +52 -7
  25. package/dist/commands/onboard.js +318 -35
  26. package/dist/commands/rollback.d.ts +44 -0
  27. package/dist/commands/rollback.js +201 -0
  28. package/dist/commands/secretsAudit.d.ts +7 -0
  29. package/dist/commands/secretsAudit.js +14 -7
  30. package/dist/commands/skill.d.ts +51 -0
  31. package/dist/commands/skill.js +104 -0
  32. package/dist/commands/sync.d.ts +13 -0
  33. package/dist/commands/sync.js +31 -5
  34. package/dist/core/adapter.d.ts +28 -11
  35. package/dist/core/adapter.js +2 -2
  36. package/dist/core/canonical.d.ts +26 -1
  37. package/dist/core/canonical.js +103 -3
  38. package/dist/core/types.d.ts +29 -1
  39. package/dist/core/types.js +11 -2
  40. package/dist/lib/backup.d.ts +56 -0
  41. package/dist/lib/backup.js +98 -0
  42. package/dist/lib/deepEqual.d.ts +8 -0
  43. package/dist/lib/deepEqual.js +26 -0
  44. package/dist/lib/dirEquals.d.ts +9 -0
  45. package/dist/lib/dirEquals.js +15 -1
  46. package/dist/lib/installAgent.d.ts +26 -0
  47. package/dist/lib/installAgent.js +46 -0
  48. package/dist/lib/mcpMigrateRead.d.ts +69 -0
  49. package/dist/lib/mcpMigrateRead.js +188 -0
  50. package/dist/lib/mcpOwnership.d.ts +25 -0
  51. package/dist/lib/mcpOwnership.js +50 -0
  52. package/dist/lib/memoryGraph.d.ts +60 -0
  53. package/dist/lib/memoryGraph.js +101 -0
  54. package/dist/lib/realHomeSnapshot.d.ts +26 -0
  55. package/dist/lib/realHomeSnapshot.js +77 -0
  56. package/dist/lib/terminalPicker.d.ts +45 -0
  57. package/dist/lib/terminalPicker.js +193 -0
  58. package/dist/lib/tomlSection.d.ts +20 -6
  59. package/dist/lib/tomlSection.js +78 -12
  60. package/dist/pi-bridge/bundle.js +100 -51
  61. package/dist/pi-bridge/index.js +14 -2
  62. package/dist/probes/codex.js +10 -2
  63. package/docs/architecture.md +7 -4
  64. package/docs/getting-started.md +267 -33
  65. package/docs/roadmap.md +444 -0
  66. package/package.json +1 -1
  67. package/schema/servers.example.yaml +39 -2
@@ -1,18 +1,32 @@
1
1
  /**
2
- * `trellis mcp sync` — loads canonical once, runs every present agent's
3
- * adapter, applies only the "mcp" slice of its plan, and reports what
4
- * happened. Same create/repair/refuse shape as `src/commands/sync.ts`,
5
- * kept as a separate command because MCP has no ownership marker for
6
- * automatic removal yet (trellis-mcp-sync-p2 design.md D7) folding it
7
- * into bare `trellis sync` would blur that distinction.
2
+ * `trellis mcp sync|list|add|remove` — `sync` loads canonical once, runs
3
+ * every present agent's adapter, applies its full "mcp" plan (create,
4
+ * repair, and since trellis-mcp-lifecycle-parity, ownership-ledger-
5
+ * gated remove), and reports what happened. Kept as a separate
6
+ * command from bare `trellis sync` (skills/instructions) since the two
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
12
  */
13
+ import { type ServersYamlWriteResult } from "../core/canonical.js";
9
14
  import type { AdapterPlanItem } from "../core/adapter.js";
10
- import type { AgentId } from "../core/types.js";
15
+ import type { AgentId, McpServerDef, Transport } from "../core/types.js";
16
+ import { type BackupSession } from "../lib/backup.js";
11
17
  export interface RunMcpSyncOptions {
12
18
  json?: boolean;
13
19
  /** Same test/sandbox-only seam as `RunSyncOptions.homeDir` — never a CLI
14
20
  * flag. See docs/architecture.md's testing philosophy. */
15
21
  homeDir?: string;
22
+ /** Compute and report the plan without calling adapter.apply(). */
23
+ dryRun?: boolean;
24
+ /** Onboard-only seam — see RunSyncOptions.managedAgents. Never a CLI
25
+ * flag. */
26
+ managedAgents?: readonly AgentId[];
27
+ /** Onboard-only seam — see RunSyncOptions.backupSession. Never a CLI
28
+ * flag. */
29
+ backupSession?: BackupSession;
16
30
  }
17
31
  export interface AgentMcpSyncReport {
18
32
  agent: AgentId;
@@ -26,3 +40,96 @@ export declare function collectMcpSyncReport(opts?: RunMcpSyncOptions): Promise<
26
40
  export declare function runMcpSync(opts?: RunMcpSyncOptions): Promise<{
27
41
  exitCode: number;
28
42
  }>;
43
+ /** Exported so `onboard` prints an mcp-sync report identically to running
44
+ * `mcp sync` standalone, instead of a second, easily-drifting copy of this
45
+ * formatting. */
46
+ export declare function printReport(report: McpSyncReport, dryRun: boolean): void;
47
+ export interface McpListEntry {
48
+ name: string;
49
+ transport: Transport;
50
+ enabled: boolean;
51
+ agents: readonly AgentId[];
52
+ command?: string;
53
+ args?: string[];
54
+ url?: string;
55
+ /** Reference strings (e.g. `${VAR}`), not resolved secret values — see
56
+ * design.md D6. */
57
+ headers?: Record<string, string>;
58
+ /** Names only — `McpServerDef.env` never stores values in the first
59
+ * place, so there is nothing here to redact. */
60
+ env?: string[];
61
+ /** Non-secret by `McpServerDef`'s own contract (design.md D6) —
62
+ * printed in full. */
63
+ staticEnv?: Record<string, string>;
64
+ }
65
+ export declare function collectMcpListPlan(homeDir?: string): McpListEntry[];
66
+ export declare function runMcpList(opts?: {
67
+ homeDir?: string;
68
+ json?: boolean;
69
+ }): {
70
+ exitCode: number;
71
+ };
72
+ /** Raw, unvalidated CLI flag values — `collectMcpAddPlan` does the actual
73
+ * parsing/validation. Kept as plain strings here so `parseMcpAddArgs`
74
+ * stays a dumb `--flag value` reader, same posture as every other
75
+ * command's own inline flag parsing in src/cli.ts. */
76
+ export interface McpAddRawArgs {
77
+ transport?: string;
78
+ command?: string;
79
+ /** Comma-separated. */
80
+ args?: string;
81
+ url?: string;
82
+ /** Comma-separated `k=v` pairs. */
83
+ headers?: string;
84
+ /** Comma-separated names. */
85
+ env?: string;
86
+ /** Comma-separated `k=v` pairs. */
87
+ staticEnv?: string;
88
+ /** Comma-separated agent ids. */
89
+ agents?: string;
90
+ /** `"true"` or `"false"`. */
91
+ enabled?: string;
92
+ }
93
+ export declare function parseMcpAddArgs(argv: readonly string[]): McpAddRawArgs;
94
+ export type McpAddAction = "create" | "already-present" | "conflict" | "invalid-input";
95
+ export interface McpAddPlan {
96
+ name: string;
97
+ action: McpAddAction;
98
+ detail: string;
99
+ /** Only set when action === "create"; consumed by applyMcpAddPlan. */
100
+ def?: McpServerDef;
101
+ }
102
+ /**
103
+ * Validates transport-specific required flags at this layer (stdio needs
104
+ * `--command`, http/sse need `--url`) rather than deferring to a later
105
+ * write failure — design.md's open question, resolved in favor of
106
+ * refusing early with a specific reason. No `--force`: an existing name
107
+ * always refuses, ever (design.md D4).
108
+ */
109
+ 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?: {
112
+ homeDir?: string;
113
+ json?: boolean;
114
+ dryRun?: boolean;
115
+ }): {
116
+ exitCode: number;
117
+ };
118
+ export type McpRemoveAction = "removed" | "not-found";
119
+ export interface McpRemovePlan {
120
+ name: string;
121
+ action: McpRemoveAction;
122
+ }
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. */
127
+ export declare function collectMcpRemovePlan(name: string, homeDir?: string): McpRemovePlan;
128
+ export declare function applyMcpRemovePlan(plan: McpRemovePlan, homeDir?: string): ServersYamlWriteResult;
129
+ export declare function runMcpRemove(name: string, opts?: {
130
+ homeDir?: string;
131
+ json?: boolean;
132
+ dryRun?: boolean;
133
+ }): {
134
+ exitCode: number;
135
+ };
@@ -1,34 +1,55 @@
1
1
  /**
2
- * `trellis mcp sync` — loads canonical once, runs every present agent's
3
- * adapter, applies only the "mcp" slice of its plan, and reports what
4
- * happened. Same create/repair/refuse shape as `src/commands/sync.ts`,
5
- * kept as a separate command because MCP has no ownership marker for
6
- * automatic removal yet (trellis-mcp-sync-p2 design.md D7) folding it
7
- * into bare `trellis sync` would blur that distinction.
2
+ * `trellis mcp sync|list|add|remove` — `sync` loads canonical once, runs
3
+ * every present agent's adapter, applies its full "mcp" plan (create,
4
+ * repair, and since trellis-mcp-lifecycle-parity, ownership-ledger-
5
+ * gated remove), and reports what happened. Kept as a separate
6
+ * command from bare `trellis sync` (skills/instructions) since the two
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
12
  */
9
13
  import { homedir } from "node:os";
10
- import { loadCanonicalSource } from "../core/canonical.js";
14
+ import { join } from "node:path";
15
+ import { loadCanonicalSource, upsertServerYaml, removeServerYaml } from "../core/canonical.js";
16
+ import { ALL_AGENTS, resolveScope } from "../core/types.js";
11
17
  import { ClaudeCodeAdapter } from "../adapters/claude-code.js";
12
18
  import { CodexAdapter } from "../adapters/codex.js";
13
19
  import { KiroAdapter } from "../adapters/kiro.js";
14
20
  import { PiAdapter } from "../adapters/pi.js";
15
- function buildAdapters(homeDir) {
16
- return [new ClaudeCodeAdapter(homeDir), new CodexAdapter(homeDir), new KiroAdapter(homeDir), new PiAdapter(homeDir)];
21
+ import { openBackupSession } from "../lib/backup.js";
22
+ const ADAPTER_FACTORY = {
23
+ "claude-code": (homeDir) => new ClaudeCodeAdapter(homeDir),
24
+ codex: (homeDir) => new CodexAdapter(homeDir),
25
+ kiro: (homeDir) => new KiroAdapter(homeDir),
26
+ pi: (homeDir) => new PiAdapter(homeDir),
27
+ };
28
+ /** Only agents in `canonical.managedAgents` — see src/commands/sync.ts's
29
+ * own copy of this same restriction (trellis-managed-agents). */
30
+ function buildAdapters(homeDir, managedAgents) {
31
+ return managedAgents.map((id) => ADAPTER_FACTORY[id](homeDir));
17
32
  }
18
33
  export async function collectMcpSyncReport(opts = {}) {
19
34
  const homeDir = opts.homeDir ?? homedir();
20
- const canonical = loadCanonicalSource(homeDir);
35
+ const loaded = loadCanonicalSource(homeDir);
36
+ const canonical = opts.managedAgents ? { ...loaded, managedAgents: opts.managedAgents } : loaded;
21
37
  const reports = [];
22
- for (const adapter of buildAdapters(homeDir)) {
38
+ const ownSession = !opts.dryRun && !opts.backupSession ? openBackupSession(homeDir, "mcp-sync") : undefined;
39
+ const backup = opts.backupSession ?? ownSession;
40
+ for (const adapter of buildAdapters(homeDir, canonical.managedAgents)) {
23
41
  const probeResult = await adapter.probe();
24
42
  if (!probeResult.present) {
25
43
  reports.push({ agent: adapter.id, present: false, items: [] });
26
44
  continue;
27
45
  }
28
46
  const items = (await adapter.plan(canonical)).filter((item) => item.kind === "mcp");
29
- await adapter.apply(items);
47
+ if (!opts.dryRun) {
48
+ await adapter.apply(items, backup);
49
+ }
30
50
  reports.push({ agent: adapter.id, present: true, items });
31
51
  }
52
+ ownSession?.finalize();
32
53
  return { reports };
33
54
  }
34
55
  export async function runMcpSync(opts = {}) {
@@ -44,27 +65,247 @@ export async function runMcpSync(opts = {}) {
44
65
  console.log(JSON.stringify(report, null, 2));
45
66
  }
46
67
  else {
47
- printReport(report);
68
+ printReport(report, opts.dryRun ?? false);
48
69
  }
49
70
  const hasConflict = report.reports.some((r) => r.items.some((i) => i.action === "conflict"));
50
71
  return { exitCode: hasConflict ? 1 : 0 };
51
72
  }
52
- function printReport(report) {
73
+ /** Exported so `onboard` prints an mcp-sync report identically to running
74
+ * `mcp sync` standalone, instead of a second, easily-drifting copy of this
75
+ * formatting. */
76
+ export function printReport(report, dryRun) {
77
+ if (dryRun)
78
+ console.log("[dry run]");
79
+ if (report.reports.length === 0) {
80
+ console.log("No managed agents yet — run `trellis onboard` or list agent ids in ~/.trellis/managed.yaml.");
81
+ return;
82
+ }
53
83
  for (const { agent, present, items } of report.reports) {
54
84
  if (!present) {
55
85
  console.log(`— ${agent} (not installed)`);
56
86
  continue;
57
87
  }
58
88
  const created = items.filter((i) => i.action === "create");
89
+ const removed = items.filter((i) => i.action === "remove");
59
90
  const conflicts = items.filter((i) => i.action === "conflict");
60
- if (created.length === 0 && conflicts.length === 0) {
91
+ if (created.length === 0 && removed.length === 0 && conflicts.length === 0) {
61
92
  console.log(`✅ ${agent} — already in sync`);
62
93
  continue;
63
94
  }
64
95
  const icon = conflicts.length > 0 ? "⚠️ " : "✅";
65
- console.log(`${icon} ${agent} — ${created.length} created/updated, ${conflicts.length} conflict(s)`);
66
- for (const item of [...created, ...conflicts]) {
96
+ console.log(`${icon} ${agent} — ${created.length} created/updated, ${removed.length} removed, ${conflicts.length} conflict(s)`);
97
+ for (const item of [...created, ...removed, ...conflicts]) {
67
98
  console.log(` - [${item.action}] ${item.description}`);
68
99
  }
69
100
  }
70
101
  }
102
+ function serversYamlPath(homeDir) {
103
+ return join(homeDir, ".trellis", "mcp", "servers.yaml");
104
+ }
105
+ export function collectMcpListPlan(homeDir = homedir()) {
106
+ const canonical = loadCanonicalSource(homeDir);
107
+ return Object.entries(canonical.mcp.servers).map(([name, def]) => ({
108
+ name,
109
+ transport: def.transport,
110
+ enabled: def.enabled ?? true,
111
+ agents: resolveScope(def.agents, canonical.managedAgents),
112
+ command: def.command,
113
+ args: def.args,
114
+ url: def.url,
115
+ headers: def.headers,
116
+ env: def.env,
117
+ staticEnv: def.staticEnv,
118
+ }));
119
+ }
120
+ export function runMcpList(opts = {}) {
121
+ const homeDir = opts.homeDir ?? homedir();
122
+ let entries;
123
+ try {
124
+ entries = collectMcpListPlan(homeDir);
125
+ }
126
+ catch (err) {
127
+ console.error(err instanceof Error ? err.message : String(err));
128
+ return { exitCode: 1 };
129
+ }
130
+ if (opts.json) {
131
+ console.log(JSON.stringify(entries, null, 2));
132
+ }
133
+ else if (entries.length === 0) {
134
+ console.log("No MCP servers in canonical source yet.");
135
+ }
136
+ else {
137
+ for (const e of entries) {
138
+ 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}`);
140
+ if (e.env && e.env.length > 0)
141
+ console.log(` env: ${e.env.join(", ")} (values never read/printed)`);
142
+ if (e.staticEnv)
143
+ console.log(` static_env: ${Object.entries(e.staticEnv).map(([k, v]) => `${k}=${v}`).join(", ")}`);
144
+ }
145
+ }
146
+ return { exitCode: 0 };
147
+ }
148
+ export function parseMcpAddArgs(argv) {
149
+ const flag = (name) => {
150
+ const i = argv.indexOf(name);
151
+ return i >= 0 ? argv[i + 1] : undefined;
152
+ };
153
+ return {
154
+ transport: flag("--transport"),
155
+ command: flag("--command"),
156
+ args: flag("--args"),
157
+ url: flag("--url"),
158
+ headers: flag("--headers"),
159
+ env: flag("--env"),
160
+ staticEnv: flag("--static-env"),
161
+ agents: flag("--agents"),
162
+ enabled: flag("--enabled"),
163
+ };
164
+ }
165
+ function parseCsv(raw) {
166
+ if (!raw)
167
+ return undefined;
168
+ const items = raw.split(",").map((s) => s.trim()).filter(Boolean);
169
+ return items.length > 0 ? items : undefined;
170
+ }
171
+ function parseKvList(raw) {
172
+ if (!raw)
173
+ return undefined;
174
+ const out = {};
175
+ for (const pair of raw.split(",")) {
176
+ const eq = pair.indexOf("=");
177
+ if (eq < 0)
178
+ continue;
179
+ out[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
180
+ }
181
+ return Object.keys(out).length > 0 ? out : undefined;
182
+ }
183
+ const TRANSPORTS = ["stdio", "http", "sse"];
184
+ /**
185
+ * Validates transport-specific required flags at this layer (stdio needs
186
+ * `--command`, http/sse need `--url`) rather than deferring to a later
187
+ * write failure — design.md's open question, resolved in favor of
188
+ * refusing early with a specific reason. No `--force`: an existing name
189
+ * always refuses, ever (design.md D4).
190
+ */
191
+ export function collectMcpAddPlan(name, raw, homeDir = homedir()) {
192
+ if (!name) {
193
+ return { name: "(none)", action: "invalid-input", detail: "a name is required: trellis mcp add <name> --transport ..." };
194
+ }
195
+ if (!raw.transport || !TRANSPORTS.includes(raw.transport)) {
196
+ return { name, action: "invalid-input", detail: `--transport must be one of: ${TRANSPORTS.join(", ")}` };
197
+ }
198
+ const transport = raw.transport;
199
+ if (transport === "stdio" && !raw.command) {
200
+ return { name, action: "invalid-input", detail: "--transport stdio requires --command" };
201
+ }
202
+ if ((transport === "http" || transport === "sse") && !raw.url) {
203
+ return { name, action: "invalid-input", detail: `--transport ${transport} requires --url` };
204
+ }
205
+ const agentsList = parseCsv(raw.agents);
206
+ if (agentsList) {
207
+ for (const id of agentsList) {
208
+ if (!ALL_AGENTS.includes(id)) {
209
+ return { name, action: "invalid-input", detail: `--agents "${id}" is not a recognized agent id (${ALL_AGENTS.join(", ")})` };
210
+ }
211
+ }
212
+ }
213
+ if (raw.enabled !== undefined && raw.enabled !== "true" && raw.enabled !== "false") {
214
+ return { name, action: "invalid-input", detail: `--enabled must be "true" or "false" (got ${raw.enabled})` };
215
+ }
216
+ const def = {
217
+ transport,
218
+ ...(raw.command ? { command: raw.command } : {}),
219
+ ...(parseCsv(raw.args) ? { args: parseCsv(raw.args) } : {}),
220
+ ...(raw.url ? { url: raw.url } : {}),
221
+ ...(parseKvList(raw.headers) ? { headers: parseKvList(raw.headers) } : {}),
222
+ ...(parseCsv(raw.env) ? { env: parseCsv(raw.env) } : {}),
223
+ ...(parseKvList(raw.staticEnv) ? { staticEnv: parseKvList(raw.staticEnv) } : {}),
224
+ ...(raw.enabled !== undefined ? { enabled: raw.enabled === "true" } : {}),
225
+ ...(agentsList ? { agents: agentsList } : {}),
226
+ };
227
+ const canonical = loadCanonicalSource(homeDir);
228
+ const existing = canonical.mcp.servers[name];
229
+ if (existing) {
230
+ if (JSON.stringify(existing) === JSON.stringify(def)) {
231
+ return { name, action: "already-present", detail: "canonical entry is already identical" };
232
+ }
233
+ return { name, action: "conflict", detail: `servers.yaml already has "${name}" with different settings — resolve by hand (no --force, design.md D4)` };
234
+ }
235
+ return { name, action: "create", detail: "will add to servers.yaml", def };
236
+ }
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);
241
+ }
242
+ export function runMcpAdd(name, raw, opts = {}) {
243
+ const homeDir = opts.homeDir ?? homedir();
244
+ let plan;
245
+ try {
246
+ plan = collectMcpAddPlan(name, raw, homeDir);
247
+ }
248
+ catch (err) {
249
+ console.error(err instanceof Error ? err.message : String(err));
250
+ return { exitCode: 1 };
251
+ }
252
+ let writeError;
253
+ if (!opts.dryRun && plan.action === "create") {
254
+ const result = applyMcpAddPlan(plan, homeDir);
255
+ if (!result.ok)
256
+ writeError = result.error;
257
+ }
258
+ if (opts.json) {
259
+ console.log(JSON.stringify(writeError ? { ...plan, writeError } : plan, null, 2));
260
+ }
261
+ else {
262
+ console.log(`${opts.dryRun ? "[dry run] " : ""}mcp add ${plan.name}`);
263
+ console.log(` [${plan.action}] ${plan.detail}`);
264
+ if (writeError)
265
+ console.error(` write failed: ${writeError}`);
266
+ }
267
+ return { exitCode: plan.action === "conflict" || plan.action === "invalid-input" || writeError ? 1 : 0 };
268
+ }
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. */
273
+ export function collectMcpRemovePlan(name, homeDir = homedir()) {
274
+ const canonical = loadCanonicalSource(homeDir);
275
+ return { name, action: canonical.mcp.servers[name] ? "removed" : "not-found" };
276
+ }
277
+ export function applyMcpRemovePlan(plan, homeDir = homedir()) {
278
+ if (plan.action !== "removed")
279
+ return { ok: true };
280
+ return removeServerYaml(serversYamlPath(homeDir), plan.name);
281
+ }
282
+ export function runMcpRemove(name, opts = {}) {
283
+ const homeDir = opts.homeDir ?? homedir();
284
+ let plan;
285
+ try {
286
+ plan = collectMcpRemovePlan(name, homeDir);
287
+ }
288
+ catch (err) {
289
+ console.error(err instanceof Error ? err.message : String(err));
290
+ return { exitCode: 1 };
291
+ }
292
+ let writeError;
293
+ if (!opts.dryRun && plan.action === "removed") {
294
+ const result = applyMcpRemovePlan(plan, homeDir);
295
+ if (!result.ok)
296
+ writeError = result.error;
297
+ }
298
+ if (opts.json) {
299
+ console.log(JSON.stringify(writeError ? { ...plan, writeError } : plan, null, 2));
300
+ }
301
+ else if (plan.action === "not-found") {
302
+ console.error(`"${name}" is not a canonical MCP server — nothing to remove.`);
303
+ }
304
+ else if (writeError) {
305
+ console.error(` write failed: ${writeError}`);
306
+ }
307
+ else {
308
+ console.log(`${opts.dryRun ? "[dry run] " : ""}removed MCP server "${name}" from canonical source.`);
309
+ }
310
+ return { exitCode: plan.action === "not-found" || writeError ? 1 : 0 };
311
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `trellis memory sync` — ingests canonical `memories/*.md` into the
3
+ * on-disk knowledge-graph file `@modelcontextprotocol/server-memory`
4
+ * itself reads at startup, closing P6's explicitly-left-open "auto-
5
+ * ingesting memories/*.md content into the running memory server's
6
+ * store" gap (trellis-memory-sync). Never spawns or talks to a running
7
+ * server process — this is a plain file write, same posture as every
8
+ * other Trellis write.
9
+ */
10
+ import { type MemorySyncPlan } from "../lib/memoryGraph.js";
11
+ export interface RunMemorySyncOptions {
12
+ homeDir?: string;
13
+ json?: boolean;
14
+ dryRun?: boolean;
15
+ }
16
+ export type MemorySyncResult = {
17
+ configured: true;
18
+ graphPath: string;
19
+ plan: MemorySyncPlan;
20
+ } | {
21
+ configured: false;
22
+ reason: string;
23
+ };
24
+ /**
25
+ * Looks up `mcp.servers["memory"]` specifically (matching `schema/
26
+ * servers.example.yaml`'s own naming convention for the default shared-
27
+ * memory backend) — not a heuristic scan for any server whose command
28
+ * happens to mention `@modelcontextprotocol/server-memory`, since a
29
+ * user could reasonably name or configure it differently and this
30
+ * would then silently miss it. `static_env.MEMORY_FILE_PATH` must be
31
+ * set explicitly: the server's own unset-env default resolves relative
32
+ * to wherever `npx` cached the package, a location Trellis has no
33
+ * reliable way to predict.
34
+ */
35
+ export declare function collectMemorySyncResult(homeDir?: string): MemorySyncResult;
36
+ export declare function applyMemorySync(result: MemorySyncResult): void;
37
+ export declare function runMemorySync(opts?: RunMemorySyncOptions): {
38
+ exitCode: number;
39
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `trellis memory sync` — ingests canonical `memories/*.md` into the
3
+ * on-disk knowledge-graph file `@modelcontextprotocol/server-memory`
4
+ * itself reads at startup, closing P6's explicitly-left-open "auto-
5
+ * ingesting memories/*.md content into the running memory server's
6
+ * store" gap (trellis-memory-sync). Never spawns or talks to a running
7
+ * server process — this is a plain file write, same posture as every
8
+ * other Trellis write.
9
+ */
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { dirname } from "node:path";
13
+ import { loadCanonicalSource } from "../core/canonical.js";
14
+ import { planMemorySync, renderMemoryGraph } from "../lib/memoryGraph.js";
15
+ const MEMORY_SERVER_NAME = "memory";
16
+ /**
17
+ * Looks up `mcp.servers["memory"]` specifically (matching `schema/
18
+ * servers.example.yaml`'s own naming convention for the default shared-
19
+ * memory backend) — not a heuristic scan for any server whose command
20
+ * happens to mention `@modelcontextprotocol/server-memory`, since a
21
+ * user could reasonably name or configure it differently and this
22
+ * would then silently miss it. `static_env.MEMORY_FILE_PATH` must be
23
+ * set explicitly: the server's own unset-env default resolves relative
24
+ * to wherever `npx` cached the package, a location Trellis has no
25
+ * reliable way to predict.
26
+ */
27
+ export function collectMemorySyncResult(homeDir = homedir()) {
28
+ const canonical = loadCanonicalSource(homeDir);
29
+ const server = canonical.mcp.servers[MEMORY_SERVER_NAME];
30
+ const rawPath = server?.staticEnv?.MEMORY_FILE_PATH;
31
+ if (!server || !rawPath) {
32
+ return {
33
+ configured: false,
34
+ reason: `no "${MEMORY_SERVER_NAME}" MCP server with static_env.MEMORY_FILE_PATH configured in servers.yaml — see schema/servers.example.yaml`,
35
+ };
36
+ }
37
+ const graphPath = rawPath.replace(/^~(?=$|\/)/, homeDir);
38
+ const currentContent = existsSync(graphPath) ? readFileSync(graphPath, "utf-8") : undefined;
39
+ const plan = planMemorySync(canonical, currentContent);
40
+ return { configured: true, graphPath, plan };
41
+ }
42
+ export function applyMemorySync(result) {
43
+ if (!result.configured || !result.plan.nextGraph)
44
+ return;
45
+ mkdirSync(dirname(result.graphPath), { recursive: true });
46
+ writeFileSync(result.graphPath, renderMemoryGraph(result.plan.nextGraph));
47
+ }
48
+ export function runMemorySync(opts = {}) {
49
+ const homeDir = opts.homeDir ?? homedir();
50
+ let result;
51
+ try {
52
+ result = collectMemorySyncResult(homeDir);
53
+ }
54
+ catch (err) {
55
+ console.error(err instanceof Error ? err.message : String(err));
56
+ return { exitCode: 1 };
57
+ }
58
+ if (!opts.dryRun) {
59
+ applyMemorySync(result);
60
+ }
61
+ if (opts.json) {
62
+ console.log(JSON.stringify(result, null, 2));
63
+ }
64
+ else if (!result.configured) {
65
+ console.log(result.reason);
66
+ }
67
+ else {
68
+ console.log(`${opts.dryRun ? "[dry run] " : ""}memory sync — ${result.graphPath}`);
69
+ if (result.plan.items.length === 0) {
70
+ console.log(" nothing in canonical memories/ yet");
71
+ }
72
+ for (const item of result.plan.items) {
73
+ console.log(` [${item.action}] "${item.name}" — ${item.detail}`);
74
+ }
75
+ }
76
+ const hasConflict = result.configured && result.plan.items.some((i) => i.action === "conflict");
77
+ return { exitCode: hasConflict ? 1 : 0 };
78
+ }
@@ -4,26 +4,52 @@
4
4
  * Pure `collectMigratePlan` / effectful `applyMigratePlan`, same
5
5
  * plan-then-apply split every adapter already uses.
6
6
  */
7
- import type { AgentId } from "../core/types.js";
8
- export type MigrateAction = "create" | "skip-symlink" | "skip-case-broken" | "already-migrated" | "conflict";
7
+ import type { AgentId, McpServerDef } from "../core/types.js";
8
+ export type MigrateAction = "create" | "skip-symlink" | "skip-case-broken" | "skip-unsupported" | "already-migrated" | "conflict";
9
+ /** Internal kind naming, unchanged since before `--only` existed
10
+ * (trellis-migrate-category-selection design.md D2) — the CLI-facing
11
+ * flag value is the plural `"skills"`, mapped to this singular `"skill"`
12
+ * in `runMigrate`, not renamed here to avoid touching every existing
13
+ * `MigratePlanItem.kind` comparison for no functional reason. */
14
+ export type MigrateKind = "skill" | "instructions" | "mcp";
9
15
  export interface MigratePlanItem {
10
- kind: "skill" | "instructions";
16
+ kind: MigrateKind;
11
17
  name: string;
12
18
  action: MigrateAction;
13
19
  detail: string;
14
20
  /** Only set when action === "create"; consumed by applyMigratePlan. */
15
21
  sourceDir?: string;
16
22
  sourceContent?: string;
23
+ /** Only set when kind === "mcp" && action === "create". */
24
+ mcpDef?: McpServerDef;
17
25
  }
18
26
  export interface MigratePlan {
19
27
  agent: AgentId;
20
28
  present: boolean;
21
29
  items: MigratePlanItem[];
22
30
  }
23
- export declare function collectMigratePlan(agent: AgentId, homeDir?: string): Promise<MigratePlan>;
31
+ /**
32
+ * `only` restricts which kind(s) are even considered — not a post-hoc
33
+ * filter on a fully-computed plan (trellis-migrate-category-selection
34
+ * design.md D3): the excluded kind's canonical path is never read for
35
+ * comparison and never appears in the plan, not even as a suppressed
36
+ * conflict. Omitting `only` (or passing both kinds) is exactly today's
37
+ * behavior.
38
+ */
39
+ export declare function collectMigratePlan(agent: AgentId, homeDir?: string, only?: readonly MigrateKind[]): Promise<MigratePlan>;
24
40
  export declare function applyMigratePlan(plan: MigratePlan, homeDir?: string): void;
41
+ /** CLI-facing spelling: `"skills"` (plural — a run usually touches more
42
+ * than one), `"instructions"` (already singular-shaped), or `"mcp"`
43
+ * (already the CLI's own convention, matching `trellis mcp`'s own
44
+ * command name). Mapped to `MigrateKind` in `runMigrate`, the one place
45
+ * this translation lives. */
46
+ export type MigrateOnlyValue = "skills" | "instructions" | "mcp";
25
47
  export interface RunMigrateOptions {
26
48
  from?: string;
49
+ /** Restricts the run to one category (`"skills"`, `"instructions"`, or
50
+ * `"mcp"`, raw and unvalidated same as `from` — `runMigrate` checks
51
+ * it). `undefined` means all three, exactly as before `"mcp"` existed. */
52
+ only?: string;
27
53
  dryRun?: boolean;
28
54
  json?: boolean;
29
55
  /** Defaults to the real `~`; overridable for tests only. */