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.
- package/README.md +69 -17
- package/dist/adapters/claude-code.d.ts +5 -3
- package/dist/adapters/claude-code.js +27 -14
- package/dist/adapters/codex.d.ts +8 -4
- package/dist/adapters/codex.js +47 -16
- package/dist/adapters/jsonMcp.d.ts +16 -5
- package/dist/adapters/jsonMcp.js +38 -29
- package/dist/adapters/kiro.d.ts +5 -3
- package/dist/adapters/kiro.js +29 -16
- package/dist/adapters/mcpPlan.d.ts +11 -6
- package/dist/adapters/mcpPlan.js +40 -7
- package/dist/adapters/pi.d.ts +2 -1
- package/dist/adapters/pi.js +4 -4
- package/dist/adapters/symlinkPlan.d.ts +7 -3
- package/dist/adapters/symlinkPlan.js +42 -16
- package/dist/cli.js +161 -18
- package/dist/commands/init.js +11 -0
- package/dist/commands/mcp.d.ts +114 -7
- package/dist/commands/mcp.js +258 -17
- package/dist/commands/memory.d.ts +39 -0
- package/dist/commands/memory.js +78 -0
- package/dist/commands/migrate.d.ts +30 -4
- package/dist/commands/migrate.js +83 -16
- package/dist/commands/onboard.d.ts +52 -7
- package/dist/commands/onboard.js +318 -35
- package/dist/commands/rollback.d.ts +44 -0
- package/dist/commands/rollback.js +201 -0
- package/dist/commands/secretsAudit.d.ts +7 -0
- package/dist/commands/secretsAudit.js +14 -7
- package/dist/commands/skill.d.ts +51 -0
- package/dist/commands/skill.js +104 -0
- package/dist/commands/sync.d.ts +13 -0
- package/dist/commands/sync.js +31 -5
- package/dist/core/adapter.d.ts +28 -11
- package/dist/core/adapter.js +2 -2
- package/dist/core/canonical.d.ts +26 -1
- package/dist/core/canonical.js +103 -3
- package/dist/core/types.d.ts +29 -1
- package/dist/core/types.js +11 -2
- package/dist/lib/backup.d.ts +56 -0
- package/dist/lib/backup.js +98 -0
- package/dist/lib/deepEqual.d.ts +8 -0
- package/dist/lib/deepEqual.js +26 -0
- package/dist/lib/dirEquals.d.ts +9 -0
- package/dist/lib/dirEquals.js +15 -1
- package/dist/lib/installAgent.d.ts +26 -0
- package/dist/lib/installAgent.js +46 -0
- package/dist/lib/mcpMigrateRead.d.ts +69 -0
- package/dist/lib/mcpMigrateRead.js +188 -0
- package/dist/lib/mcpOwnership.d.ts +25 -0
- package/dist/lib/mcpOwnership.js +50 -0
- package/dist/lib/memoryGraph.d.ts +60 -0
- package/dist/lib/memoryGraph.js +101 -0
- package/dist/lib/realHomeSnapshot.d.ts +26 -0
- package/dist/lib/realHomeSnapshot.js +77 -0
- package/dist/lib/terminalPicker.d.ts +45 -0
- package/dist/lib/terminalPicker.js +193 -0
- package/dist/lib/tomlSection.d.ts +20 -6
- package/dist/lib/tomlSection.js +78 -12
- package/dist/pi-bridge/bundle.js +100 -51
- package/dist/pi-bridge/index.js +14 -2
- package/dist/probes/codex.js +10 -2
- package/docs/architecture.md +7 -4
- package/docs/getting-started.md +267 -33
- package/docs/roadmap.md +444 -0
- package/package.json +1 -1
- package/schema/servers.example.yaml +39 -2
package/dist/commands/mcp.d.ts
CHANGED
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `trellis mcp sync` — loads canonical once, runs
|
|
3
|
-
* adapter, applies
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
+
};
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,34 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `trellis mcp sync` — loads canonical once, runs
|
|
3
|
-
* adapter, applies
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
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
|
-
|
|
16
|
-
|
|
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
|
|
35
|
+
const loaded = loadCanonicalSource(homeDir);
|
|
36
|
+
const canonical = opts.managedAgents ? { ...loaded, managedAgents: opts.managedAgents } : loaded;
|
|
21
37
|
const reports = [];
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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. */
|