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.
- package/README.md +48 -153
- package/dist/adapters/claude-code.js +3 -2
- package/dist/adapters/codex.js +3 -2
- package/dist/adapters/jsonMcp.d.ts +3 -1
- package/dist/adapters/jsonMcp.js +18 -4
- package/dist/adapters/kimi-code.d.ts +21 -0
- package/dist/adapters/kimi-code.js +107 -0
- package/dist/adapters/kiro.js +3 -2
- package/dist/adapters/mcpPlan.d.ts +6 -1
- package/dist/adapters/mcpPlan.js +106 -8
- package/dist/adapters/pi.js +22 -3
- package/dist/adapters/symlinkPlan.d.ts +7 -0
- package/dist/adapters/symlinkPlan.js +16 -7
- package/dist/adapters/zcode.d.ts +21 -0
- package/dist/adapters/zcode.js +234 -0
- package/dist/cli.js +145 -29
- package/dist/commands/doctor.d.ts +7 -2
- package/dist/commands/doctor.js +40 -2
- package/dist/commands/init.d.ts +4 -2
- package/dist/commands/init.js +29 -13
- package/dist/commands/kimi.d.ts +16 -0
- package/dist/commands/kimi.js +64 -0
- package/dist/commands/manage.d.ts +39 -0
- package/dist/commands/manage.js +126 -0
- package/dist/commands/mcp.d.ts +77 -14
- package/dist/commands/mcp.js +169 -29
- package/dist/commands/mcpGateway.d.ts +6 -0
- package/dist/commands/mcpGateway.js +35 -14
- package/dist/commands/mcpImport.d.ts +46 -0
- package/dist/commands/mcpImport.js +359 -0
- package/dist/commands/memory.d.ts +17 -3
- package/dist/commands/memory.js +27 -8
- package/dist/commands/migrate.d.ts +16 -5
- package/dist/commands/migrate.js +93 -17
- package/dist/commands/onboard.d.ts +74 -2
- package/dist/commands/onboard.js +469 -71
- package/dist/commands/rollback.js +28 -2
- package/dist/commands/secretsAudit.js +4 -1
- package/dist/commands/skill.d.ts +52 -6
- package/dist/commands/skill.js +114 -15
- package/dist/commands/sync.js +5 -1
- package/dist/core/adapter.d.ts +11 -1
- package/dist/core/canonical.d.ts +15 -7
- package/dist/core/canonical.js +101 -13
- package/dist/core/capabilitySelection.d.ts +45 -0
- package/dist/core/capabilitySelection.js +124 -0
- package/dist/core/types.d.ts +25 -2
- package/dist/core/types.js +8 -0
- package/dist/lib/agentBridge.d.ts +169 -0
- package/dist/lib/agentBridge.js +367 -0
- package/dist/lib/agentBridgeProvider.d.ts +14 -0
- package/dist/lib/agentBridgeProvider.js +95 -0
- package/dist/lib/backup.d.ts +32 -0
- package/dist/lib/backup.js +20 -1
- package/dist/lib/builtinSkills.d.ts +2 -0
- package/dist/lib/builtinSkills.js +4 -0
- package/dist/lib/chatAgents.d.ts +42 -0
- package/dist/lib/chatAgents.js +59 -0
- package/dist/lib/cliMetadata.d.ts +1 -0
- package/dist/lib/cliMetadata.js +14 -0
- package/dist/lib/envVarNames.d.ts +3 -0
- package/dist/lib/envVarNames.js +19 -0
- package/dist/lib/gatewayBackend.d.ts +28 -0
- package/dist/lib/gatewayBackend.js +72 -1
- package/dist/lib/installAgent.d.ts +7 -5
- package/dist/lib/installAgent.js +4 -3
- package/dist/lib/mcpConnect.js +5 -1
- package/dist/lib/mcpMigrateRead.d.ts +2 -0
- package/dist/lib/mcpMigrateRead.js +22 -1
- package/dist/lib/mcpRuntime.d.ts +46 -0
- package/dist/lib/mcpRuntime.js +185 -0
- package/dist/lib/mcpStatusProvider.d.ts +10 -0
- package/dist/lib/mcpStatusProvider.js +25 -0
- package/dist/lib/mcpToolRegistry.d.ts +18 -0
- package/dist/lib/mcpToolRegistry.js +107 -14
- package/dist/lib/memoryGraph.d.ts +1 -1
- package/dist/lib/memoryGraph.js +9 -3
- package/dist/lib/memoryProvider.d.ts +58 -0
- package/dist/lib/memoryProvider.js +175 -0
- package/dist/lib/memoryStatus.d.ts +17 -0
- package/dist/lib/memoryStatus.js +88 -0
- package/dist/lib/nativeMemory.d.ts +19 -0
- package/dist/lib/nativeMemory.js +129 -0
- package/dist/lib/realHomeSnapshot.js +5 -0
- package/dist/lib/runtimeControlProvider.d.ts +16 -0
- package/dist/lib/runtimeControlProvider.js +170 -0
- package/dist/lib/secretEnv.d.ts +2 -1
- package/dist/lib/secretEnv.js +6 -2
- package/dist/lib/skillProvider.d.ts +13 -0
- package/dist/lib/skillProvider.js +210 -0
- package/dist/lib/taskProvider.d.ts +7 -0
- package/dist/lib/taskProvider.js +43 -0
- package/dist/lib/taskStore.d.ts +46 -0
- package/dist/lib/taskStore.js +156 -0
- package/dist/lib/terminalPicker.d.ts +23 -49
- package/dist/lib/terminalPicker.js +80 -218
- package/dist/lib/zcodeOwnership.d.ts +17 -0
- package/dist/lib/zcodeOwnership.js +37 -0
- package/dist/pi-bridge/bundle.js +476 -37
- package/dist/pi-bridge/index.js +42 -21
- package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
- package/dist/pi-bridge/schemaTranslate.js +4 -8
- package/dist/probes/codex.d.ts +7 -0
- package/dist/probes/codex.js +37 -5
- package/dist/probes/kimi-code.d.ts +12 -0
- package/dist/probes/kimi-code.js +55 -0
- package/dist/probes/zcode.d.ts +32 -0
- package/dist/probes/zcode.js +140 -0
- package/dist/sdk.d.ts +1 -1
- package/docs/architecture.md +230 -28
- package/docs/assets/brand-v3/trellis-mark-v3-a.png +0 -0
- package/docs/assets/brand-v3/trellis-mark-v3-a.svg +10 -0
- package/docs/assets/poster-series-v3/README.md +60 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-en.svg +51 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-horizontal-zh.svg +51 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-en.svg +48 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-memory-zh.svg +48 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-en.svg +56 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-migrate-zh.svg +56 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-en.svg +47 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-switch-zh.svg +47 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-en.svg +51 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.png +0 -0
- package/docs/assets/poster-series-v3/agent-trellis-poster-v3-unified-zh.svg +51 -0
- package/docs/getting-started.md +496 -42
- package/docs/overview.md +134 -0
- package/docs/overview.zh-CN.md +102 -0
- package/docs/research.md +59 -0
- package/docs/roadmap.md +11 -6
- package/docs/video/README.md +76 -0
- package/docs/video/build/audio/01-opening.aiff +0 -0
- package/docs/video/build/audio/01-opening.mp3 +0 -0
- package/docs/video/build/audio/02-pain.aiff +0 -0
- package/docs/video/build/audio/02-pain.mp3 +0 -0
- package/docs/video/build/audio/03-solution.aiff +0 -0
- package/docs/video/build/audio/03-solution.mp3 +0 -0
- package/docs/video/build/audio/04-install.aiff +0 -0
- package/docs/video/build/audio/04-install.mp3 +0 -0
- package/docs/video/build/audio/04-setup.mp3 +0 -0
- package/docs/video/build/audio/05-dryrun.mp3 +0 -0
- package/docs/video/build/audio/05-init.aiff +0 -0
- package/docs/video/build/audio/05-init.mp3 +0 -0
- package/docs/video/build/audio/05-interactive.mp3 +0 -0
- package/docs/video/build/audio/06-dryrun.aiff +0 -0
- package/docs/video/build/audio/06-dryrun.mp3 +0 -0
- package/docs/video/build/audio/06-onboard.mp3 +0 -0
- package/docs/video/build/audio/06-run.mp3 +0 -0
- package/docs/video/build/audio/07-onboard.aiff +0 -0
- package/docs/video/build/audio/07-onboard.mp3 +0 -0
- package/docs/video/build/audio/07-verify.mp3 +0 -0
- package/docs/video/build/audio/08-closing.mp3 +0 -0
- package/docs/video/build/audio/08-doctor.aiff +0 -0
- package/docs/video/build/audio/08-doctor.mp3 +0 -0
- package/docs/video/build/audio/09-rollback.aiff +0 -0
- package/docs/video/build/audio/09-rollback.mp3 +0 -0
- package/docs/video/build/audio/10-principles.aiff +0 -0
- package/docs/video/build/audio/10-principles.mp3 +0 -0
- package/docs/video/build/audio/11-closing.aiff +0 -0
- package/docs/video/build/audio/11-closing.mp3 +0 -0
- package/docs/video/build/frames/01-opening-0.png +0 -0
- package/docs/video/build/frames/02-pain-1.png +0 -0
- package/docs/video/build/frames/02-pain-2.png +0 -0
- package/docs/video/build/frames/03-solution-3.png +0 -0
- package/docs/video/build/frames/04-install-4.png +0 -0
- package/docs/video/build/frames/04-setup-4.png +0 -0
- package/docs/video/build/frames/04-setup-5.png +0 -0
- package/docs/video/build/frames/05-dryrun-6.png +0 -0
- package/docs/video/build/frames/05-init-5.png +0 -0
- package/docs/video/build/frames/05-interactive-5.png +0 -0
- package/docs/video/build/frames/05-interactive-6.png +0 -0
- package/docs/video/build/frames/06-dryrun-6.png +0 -0
- package/docs/video/build/frames/06-dryrun-7.png +0 -0
- package/docs/video/build/frames/06-onboard-7.png +0 -0
- package/docs/video/build/frames/06-onboard-8.png +0 -0
- package/docs/video/build/frames/06-run-7.png +0 -0
- package/docs/video/build/frames/06-run-8.png +0 -0
- package/docs/video/build/frames/07-onboard-10.png +0 -0
- package/docs/video/build/frames/07-onboard-7.png +0 -0
- package/docs/video/build/frames/07-onboard-8.png +0 -0
- package/docs/video/build/frames/07-onboard-9.png +0 -0
- package/docs/video/build/frames/07-verify-10.png +0 -0
- package/docs/video/build/frames/07-verify-8.png +0 -0
- package/docs/video/build/frames/07-verify-9.png +0 -0
- package/docs/video/build/frames/08-closing-10.png +0 -0
- package/docs/video/build/frames/08-closing-11.png +0 -0
- package/docs/video/build/frames/08-doctor-11.png +0 -0
- package/docs/video/build/frames/08-doctor-9.png +0 -0
- package/docs/video/build/frames/09-rollback-10.png +0 -0
- package/docs/video/build/frames/09-rollback-12.png +0 -0
- package/docs/video/build/frames/10-principles-11.png +0 -0
- package/docs/video/build/frames/10-principles-13.png +0 -0
- package/docs/video/build/frames/11-closing-12.png +0 -0
- package/docs/video/build/frames/11-closing-14.png +0 -0
- package/docs/video/build/segments/01-opening.mp4 +0 -0
- package/docs/video/build/segments/01-opening.txt +3 -0
- package/docs/video/build/segments/02-pain.mp4 +0 -0
- package/docs/video/build/segments/02-pain.txt +5 -0
- package/docs/video/build/segments/03-solution.mp4 +0 -0
- package/docs/video/build/segments/03-solution.txt +3 -0
- package/docs/video/build/segments/04-install.mp4 +0 -0
- package/docs/video/build/segments/04-install.txt +3 -0
- package/docs/video/build/segments/04-setup.mp4 +0 -0
- package/docs/video/build/segments/04-setup.txt +3 -0
- package/docs/video/build/segments/05-dryrun.mp4 +0 -0
- package/docs/video/build/segments/05-dryrun.txt +3 -0
- package/docs/video/build/segments/05-init.mp4 +0 -0
- package/docs/video/build/segments/05-init.txt +3 -0
- package/docs/video/build/segments/05-interactive.mp4 +0 -0
- package/docs/video/build/segments/05-interactive.txt +5 -0
- package/docs/video/build/segments/06-dryrun.mp4 +0 -0
- package/docs/video/build/segments/06-dryrun.txt +3 -0
- package/docs/video/build/segments/06-onboard.mp4 +0 -0
- package/docs/video/build/segments/06-onboard.txt +5 -0
- package/docs/video/build/segments/06-run.mp4 +0 -0
- package/docs/video/build/segments/06-run.txt +3 -0
- package/docs/video/build/segments/07-onboard.mp4 +0 -0
- package/docs/video/build/segments/07-onboard.txt +5 -0
- package/docs/video/build/segments/07-verify.mp4 +0 -0
- package/docs/video/build/segments/07-verify.txt +5 -0
- package/docs/video/build/segments/08-closing.mp4 +0 -0
- package/docs/video/build/segments/08-closing.txt +3 -0
- package/docs/video/build/segments/08-doctor.mp4 +0 -0
- package/docs/video/build/segments/08-doctor.txt +3 -0
- package/docs/video/build/segments/09-rollback.mp4 +0 -0
- package/docs/video/build/segments/09-rollback.txt +3 -0
- package/docs/video/build/segments/10-principles.mp4 +0 -0
- package/docs/video/build/segments/10-principles.txt +3 -0
- package/docs/video/build/segments/11-closing.mp4 +0 -0
- package/docs/video/build/segments/11-closing.txt +3 -0
- package/docs/video/build/segments/final.txt +8 -0
- package/docs/video/build-ai/audio/01-hook.mp3 +0 -0
- package/docs/video/build-ai/audio/02-pain.mp3 +0 -0
- package/docs/video/build-ai/audio/03-solution.mp3 +0 -0
- package/docs/video/build-ai/audio/04-how.mp3 +0 -0
- package/docs/video/build-ai/audio/05-safe.mp3 +0 -0
- package/docs/video/build-ai/audio/06-closing.mp3 +0 -0
- package/docs/video/build-ai/clips/01-hook.mp4 +0 -0
- package/docs/video/build-ai/clips/02-pain.mp4 +0 -0
- package/docs/video/build-ai/clips/03-solution.mp4 +0 -0
- package/docs/video/build-ai/clips/04-how.mp4 +0 -0
- package/docs/video/build-ai/clips/05-safe.mp4 +0 -0
- package/docs/video/build-ai/clips/06-closing.mp4 +0 -0
- package/docs/video/build-ai/first-frame-02-pain.png +0 -0
- package/docs/video/build-ai/first-frame-03-solution.png +0 -0
- package/docs/video/build-ai/first-frame-04-how.png +0 -0
- package/docs/video/build-ai/first-frame-05-safe.png +0 -0
- package/docs/video/build-ai/segments/01-hook.caption.txt +2 -0
- package/docs/video/build-ai/segments/01-hook.mp4 +0 -0
- package/docs/video/build-ai/segments/02-pain.mp4 +0 -0
- package/docs/video/build-ai/segments/03-solution.mp4 +0 -0
- package/docs/video/build-ai/segments/04-how.mp4 +0 -0
- package/docs/video/build-ai/segments/05-safe.mp4 +0 -0
- package/docs/video/build-ai/segments/06-closing.mp4 +0 -0
- package/docs/video/build-ai/segments/cap-01-hook.png +0 -0
- package/docs/video/build-ai/segments/cap-02-pain.png +0 -0
- package/docs/video/build-ai/segments/cap-03-solution.png +0 -0
- package/docs/video/build-ai/segments/cap-04-how.png +0 -0
- package/docs/video/build-ai/segments/cap-05-safe.png +0 -0
- package/docs/video/build-ai/segments/cap-06-closing.png +0 -0
- package/docs/video/build-ai/segments/final.txt +6 -0
- package/docs/video/build-posters/audio/01-hook.mp3 +0 -0
- package/docs/video/build-posters/audio/02-pain.mp3 +0 -0
- package/docs/video/build-posters/audio/03-solution.mp3 +0 -0
- package/docs/video/build-posters/audio/04-how.mp3 +0 -0
- package/docs/video/build-posters/audio/05-safe.mp3 +0 -0
- package/docs/video/build-posters/audio/06-closing.mp3 +0 -0
- package/docs/video/build-posters/cap-01-hook.png +0 -0
- package/docs/video/build-posters/cap-02-pain.png +0 -0
- package/docs/video/build-posters/cap-03-solution.png +0 -0
- package/docs/video/build-posters/cap-04-how.png +0 -0
- package/docs/video/build-posters/cap-05-safe.png +0 -0
- package/docs/video/build-posters/cap-06-closing.png +0 -0
- package/docs/video/build-posters/card-01-hook.png +0 -0
- package/docs/video/build-posters/card-06-closing.png +0 -0
- package/docs/video/build-posters/segments/01-hook.mp4 +0 -0
- package/docs/video/build-posters/segments/02-pain.mp4 +0 -0
- package/docs/video/build-posters/segments/03-solution.mp4 +0 -0
- package/docs/video/build-posters/segments/04-how.mp4 +0 -0
- package/docs/video/build-posters/segments/05-safe.mp4 +0 -0
- package/docs/video/build-posters/segments/06-closing.mp4 +0 -0
- package/docs/video/build-posters/segments/final.txt +6 -0
- package/docs/video/generate-video-ai.mjs +334 -0
- package/docs/video/generate-video-posters.mjs +204 -0
- package/docs/video/generate-video.mjs +426 -0
- package/docs/video/outline.zh-CN.md +102 -0
- package/docs/video/trellis-demo-video-ai.mp4 +0 -0
- package/docs/video/trellis-demo-video.mp4 +0 -0
- package/docs/video/voiceover.zh-CN.md +49 -0
- package/package.json +6 -2
- package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
- package/schema/capability-selection.example.yaml +23 -0
- package/schema/scope.example.yaml +1 -1
- package/schema/servers.example.yaml +15 -1
|
@@ -14,17 +14,24 @@
|
|
|
14
14
|
* an in-process backend without this file, or any agent's config,
|
|
15
15
|
* changing.
|
|
16
16
|
*/
|
|
17
|
-
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
18
17
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
20
18
|
import { homedir } from "node:os";
|
|
21
19
|
import { loadCanonicalSource } from "../core/canonical.js";
|
|
22
|
-
import { resolveMcpPlan } from "../adapters/mcpPlan.js";
|
|
20
|
+
import { isGatewayAgent, isOAuthMcpServer, resolveMcpPlan } from "../adapters/mcpPlan.js";
|
|
23
21
|
import { LocalBackend } from "../lib/gatewayBackend.js";
|
|
24
22
|
import { DEFAULT_CONNECT_TIMEOUT_MS } from "../lib/mcpConnect.js";
|
|
23
|
+
import { BuiltinRegistry, createRuntimeServer, UpstreamProvider } from "../lib/mcpRuntime.js";
|
|
24
|
+
import { McpStatusProvider } from "../lib/mcpStatusProvider.js";
|
|
25
|
+
import { RuntimeControlProvider } from "../lib/runtimeControlProvider.js";
|
|
26
|
+
import { TaskProvider } from "../lib/taskProvider.js";
|
|
27
|
+
import { AgentBridgeProvider } from "../lib/agentBridgeProvider.js";
|
|
28
|
+
import { RuntimeMemoryProvider } from "../lib/memoryProvider.js";
|
|
29
|
+
import { SkillProvider } from "../lib/skillProvider.js";
|
|
25
30
|
import { ALL_AGENTS } from "../core/types.js";
|
|
26
31
|
const CLIENT_INFO = { name: "trellis-mcp-gateway", version: "0.0.0" };
|
|
27
32
|
const SERVER_INFO = { name: "trellis-mcp-gateway", version: "0.0.0" };
|
|
33
|
+
const RUNTIME_CLIENT_INFO = { name: "trellis-mcp-runtime", version: "0.0.0" };
|
|
34
|
+
const RUNTIME_SERVER_INFO = { name: "trellis-mcp-runtime", version: "0.0.0" };
|
|
28
35
|
export function parseMcpGatewayArgs(argv) {
|
|
29
36
|
const index = argv.indexOf("--agent");
|
|
30
37
|
if (index === -1 || index === argv.length - 1) {
|
|
@@ -50,7 +57,16 @@ export function parseMcpGatewayArgs(argv) {
|
|
|
50
57
|
* canonical gives the set gateway mode replaced.
|
|
51
58
|
*/
|
|
52
59
|
export function resolveGatewayUpstreams(agentId, canonical) {
|
|
53
|
-
const
|
|
60
|
+
const route = canonical.mcp.routes?.[agentId];
|
|
61
|
+
const hubActive = route ? route.mode === "hub" : Boolean(canonical.mcp.hub) && !isGatewayAgent(agentId, canonical.mcp, canonical.managedAgents);
|
|
62
|
+
if (hubActive) {
|
|
63
|
+
return { upstreams: [], conflicts: [] };
|
|
64
|
+
}
|
|
65
|
+
const directRoutes = canonical.mcp.routes && route
|
|
66
|
+
? { ...canonical.mcp.routes, [agentId]: { mode: "direct", ...(route.servers ? { servers: route.servers } : {}) } }
|
|
67
|
+
: canonical.mcp.routes;
|
|
68
|
+
const ordinaryServers = Object.fromEntries(Object.entries(canonical.mcp.servers).filter(([, def]) => !isOAuthMcpServer(def)));
|
|
69
|
+
const direct = { ...canonical.mcp, servers: ordinaryServers, gateway: undefined, hub: undefined, routes: directRoutes, runtime: undefined };
|
|
54
70
|
// The managed set is passed through unchanged, so an unmanaged agent
|
|
55
71
|
// reaches nothing. Being spawned is NOT treated as consent here, which
|
|
56
72
|
// is a deliberate divergence from `src/pi-bridge/index.ts` (which passes
|
|
@@ -69,6 +85,15 @@ export function resolveGatewayUpstreams(agentId, canonical) {
|
|
|
69
85
|
};
|
|
70
86
|
}
|
|
71
87
|
export async function runMcpGateway(opts) {
|
|
88
|
+
return runMcpEdge(opts, CLIENT_INFO, SERVER_INFO);
|
|
89
|
+
}
|
|
90
|
+
/** The first-class runtime entrypoint. mcp-gateway remains a compatibility
|
|
91
|
+
* alias for existing native agent config, but both edges use the same
|
|
92
|
+
* provider registry and backend construction path. */
|
|
93
|
+
export async function runMcpRuntime(opts) {
|
|
94
|
+
return runMcpEdge(opts, RUNTIME_CLIENT_INFO, RUNTIME_SERVER_INFO);
|
|
95
|
+
}
|
|
96
|
+
async function runMcpEdge(opts, clientInfo, serverInfo) {
|
|
72
97
|
const homeDir = opts.homeDir ?? homedir();
|
|
73
98
|
const connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
74
99
|
let canonical;
|
|
@@ -90,22 +115,18 @@ export async function runMcpGateway(opts) {
|
|
|
90
115
|
? await opts.backendFactory(upstreams, homeDir)
|
|
91
116
|
: await LocalBackend.connect(upstreams, {
|
|
92
117
|
secretsPolicy: canonical.secretsPolicy,
|
|
93
|
-
clientInfo
|
|
118
|
+
clientInfo,
|
|
94
119
|
connectTimeoutMs,
|
|
95
120
|
// Enables silent OAuth refresh-before-connect. Never an
|
|
96
121
|
// interactive grant: that is `trellis mcp auth`'s job, and this
|
|
97
122
|
// process has no terminal to run one in (design.md D9).
|
|
98
123
|
homeDir,
|
|
99
124
|
});
|
|
100
|
-
const
|
|
101
|
-
server
|
|
102
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
103
|
-
const result = await backend.callTool(request.params.name, request.params.arguments);
|
|
104
|
-
return result;
|
|
105
|
-
});
|
|
125
|
+
const registry = new BuiltinRegistry([new SkillProvider(), new RuntimeMemoryProvider(), new RuntimeControlProvider(backend), new McpStatusProvider(backend), new TaskProvider(), new AgentBridgeProvider(), new UpstreamProvider(backend)]);
|
|
126
|
+
const server = createRuntimeServer(serverInfo, { agentId: opts.agentId, homeDir }, registry);
|
|
106
127
|
const transport = new StdioServerTransport();
|
|
107
128
|
await server.connect(transport);
|
|
108
|
-
await waitForShutdown(
|
|
129
|
+
await waitForShutdown(registry, server);
|
|
109
130
|
return { exitCode: 0 };
|
|
110
131
|
}
|
|
111
132
|
/**
|
|
@@ -118,7 +139,7 @@ export async function runMcpGateway(opts) {
|
|
|
118
139
|
* Without this, each agent session would permanently leak a gateway plus
|
|
119
140
|
* its entire upstream process set (design.md D13).
|
|
120
141
|
*/
|
|
121
|
-
function waitForShutdown(
|
|
142
|
+
function waitForShutdown(registry, server) {
|
|
122
143
|
return new Promise((resolve) => {
|
|
123
144
|
let settled = false;
|
|
124
145
|
const shutdown = async () => {
|
|
@@ -126,7 +147,7 @@ function waitForShutdown(backend, server) {
|
|
|
126
147
|
return;
|
|
127
148
|
settled = true;
|
|
128
149
|
try {
|
|
129
|
-
await
|
|
150
|
+
await registry.close();
|
|
130
151
|
}
|
|
131
152
|
catch (err) {
|
|
132
153
|
console.error(`trellis-mcp-gateway: failed to close backend: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Safe import of standard JSON `mcpServers` exports.
|
|
2
|
+
*
|
|
3
|
+
* This is deliberately separate from `mcp add`: an export may contain real
|
|
4
|
+
* credentials, multiple differently-named copies of one server, and shapes
|
|
5
|
+
* that cannot be expressed safely for every native Agent. The source file is
|
|
6
|
+
* read-only; plans never carry secret values.
|
|
7
|
+
*/
|
|
8
|
+
import { type BackupSession } from "../lib/backup.js";
|
|
9
|
+
import type { McpSyncReport } from "./mcp.js";
|
|
10
|
+
export type McpImportAction = "create" | "already-present" | "duplicate" | "skipped" | "conflict" | "invalid-input";
|
|
11
|
+
export interface McpImportItem {
|
|
12
|
+
name: string;
|
|
13
|
+
action: McpImportAction;
|
|
14
|
+
detail: string;
|
|
15
|
+
}
|
|
16
|
+
export interface McpImportPlan {
|
|
17
|
+
source: string;
|
|
18
|
+
items: McpImportItem[];
|
|
19
|
+
}
|
|
20
|
+
export declare function collectMcpImportPlan(path: string, homeDir?: string): McpImportPlan;
|
|
21
|
+
export declare function applyMcpImportPlan(path: string, homeDir?: string, backup?: BackupSession): McpImportPlan;
|
|
22
|
+
export interface McpImportOutcome {
|
|
23
|
+
plan: McpImportPlan;
|
|
24
|
+
sync?: McpSyncReport;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* See `applyMcpAddWithSync`'s doc comment (mcp.ts) — same split, same
|
|
28
|
+
* reason: a non-CLI caller (trellis-gui's sidecar) gets the same
|
|
29
|
+
* structured result a JSON-mode CLI run prints, without scraping
|
|
30
|
+
* stdout or re-deriving the plan-then-apply-then-cascade-sync
|
|
31
|
+
* sequencing itself. Unlike `mcp add`/`mcp remove`, `applyMcpImportPlan`
|
|
32
|
+
* already opens its own backup session unconditionally (it always has,
|
|
33
|
+
* per design.md's original survey of `openBackupSession` call sites) —
|
|
34
|
+
* no tasks.md 3.5-style fix needed here.
|
|
35
|
+
*/
|
|
36
|
+
export declare function applyMcpImportWithSync(path: string, opts?: {
|
|
37
|
+
homeDir?: string;
|
|
38
|
+
dryRun?: boolean;
|
|
39
|
+
}): Promise<McpImportOutcome>;
|
|
40
|
+
export declare function runMcpImport(path: string | undefined, opts?: {
|
|
41
|
+
homeDir?: string;
|
|
42
|
+
json?: boolean;
|
|
43
|
+
dryRun?: boolean;
|
|
44
|
+
}): Promise<{
|
|
45
|
+
exitCode: number;
|
|
46
|
+
}>;
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/** Safe import of standard JSON `mcpServers` exports.
|
|
2
|
+
*
|
|
3
|
+
* This is deliberately separate from `mcp add`: an export may contain real
|
|
4
|
+
* credentials, multiple differently-named copies of one server, and shapes
|
|
5
|
+
* that cannot be expressed safely for every native Agent. The source file is
|
|
6
|
+
* read-only; plans never carry secret values.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { loadCanonicalSource, ensureGitignoreEntry, ensureShellEnvSource, upsertServerYaml, writeSecretsPolicyExtraction } from "../core/canonical.js";
|
|
12
|
+
import { openBackupSession } from "../lib/backup.js";
|
|
13
|
+
import { parseDotenv, writeLocalSecretValue } from "../lib/secretEnv.js";
|
|
14
|
+
import { collectMcpSyncReport, printReport as printMcpSyncReport } from "./mcp.js";
|
|
15
|
+
const DEFAULT_ENV_FILE_TILDE = "~/.trellis/mcp/servers.local.env";
|
|
16
|
+
class SkipImport extends Error {
|
|
17
|
+
}
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
function stringValue(value, field) {
|
|
22
|
+
if (value === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
if (typeof value !== "string" || value.length === 0)
|
|
25
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function stringArray(value, field) {
|
|
29
|
+
if (value === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string"))
|
|
32
|
+
throw new Error(`${field} must be an array of strings`);
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function looksLikeCredentialKey(key) {
|
|
36
|
+
return /(token|secret|password|authorization|api.?key|bearer|credential|private.?key|headers?)/i.test(key);
|
|
37
|
+
}
|
|
38
|
+
function looksLikeCredentialValue(value) {
|
|
39
|
+
return /(mcpr_|ghp_|glpat-|sk-[A-Za-z0-9]{8,}|basic\s+[A-Za-z0-9+/=]{12,}|bearer\s+\S+)/i.test(value);
|
|
40
|
+
}
|
|
41
|
+
function exactReference(value) {
|
|
42
|
+
const match = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(value);
|
|
43
|
+
return match?.[1];
|
|
44
|
+
}
|
|
45
|
+
function sanitizeEnvPart(value) {
|
|
46
|
+
return value.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "X";
|
|
47
|
+
}
|
|
48
|
+
function generatedSecretName(server, key) {
|
|
49
|
+
return `TRELLIS_${sanitizeEnvPart(server)}_${sanitizeEnvPart(key)}`;
|
|
50
|
+
}
|
|
51
|
+
function defaultShellRcPath(homeDir) {
|
|
52
|
+
const shell = process.env.SHELL ?? "";
|
|
53
|
+
if (shell.includes("zsh"))
|
|
54
|
+
return join(homeDir, ".zshrc");
|
|
55
|
+
if (shell.includes("bash"))
|
|
56
|
+
return join(homeDir, ".bash_profile");
|
|
57
|
+
return join(homeDir, ".profile");
|
|
58
|
+
}
|
|
59
|
+
function inferTransport(raw) {
|
|
60
|
+
const value = raw.transport ?? raw.type;
|
|
61
|
+
if (value === "http" || value === "sse" || value === "stdio")
|
|
62
|
+
return value;
|
|
63
|
+
if (raw.url !== undefined)
|
|
64
|
+
return "http";
|
|
65
|
+
return "stdio";
|
|
66
|
+
}
|
|
67
|
+
function sorted(values) {
|
|
68
|
+
return [...values].sort();
|
|
69
|
+
}
|
|
70
|
+
function identityArgs(def) {
|
|
71
|
+
if (!def.args)
|
|
72
|
+
return undefined;
|
|
73
|
+
if (def.command !== "npx")
|
|
74
|
+
return def.args;
|
|
75
|
+
// MCP exports often omit npx's non-interactive confirmation flag while the
|
|
76
|
+
// canonical adapter adds it. It changes prompting, not server identity.
|
|
77
|
+
const normalized = def.args.filter((arg) => arg !== "-y" && arg !== "--yes");
|
|
78
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
79
|
+
}
|
|
80
|
+
function normalizeMcpRemote(serverName, args, secrets) {
|
|
81
|
+
const remoteIndex = args.indexOf("mcp-remote");
|
|
82
|
+
if (remoteIndex < 0)
|
|
83
|
+
return undefined;
|
|
84
|
+
const url = args[remoteIndex + 1];
|
|
85
|
+
if (!url || !/^https?:\/\//.test(url))
|
|
86
|
+
throw new Error(`MCP server "${serverName}" has an invalid mcp-remote URL`);
|
|
87
|
+
const headerIndex = args.indexOf("--header", remoteIndex + 2);
|
|
88
|
+
const allowed = new Set(["-y", "--yes", "mcp-remote", url, "--header", ...(headerIndex >= 0 && args[headerIndex + 1] ? [args[headerIndex + 1]] : [])]);
|
|
89
|
+
if (args.some((arg) => !allowed.has(arg)))
|
|
90
|
+
throw new Error(`MCP server "${serverName}" has unsupported mcp-remote arguments; import it manually after reviewing the shape`);
|
|
91
|
+
if (headerIndex < 0)
|
|
92
|
+
return { transport: "http", url };
|
|
93
|
+
const header = args[headerIndex + 1];
|
|
94
|
+
const match = /^Authorization:\s*Basic\s+(.+)$/i.exec(header ?? "");
|
|
95
|
+
if (!match)
|
|
96
|
+
throw new Error(`MCP server "${serverName}" has an unsupported mcp-remote header; only Basic Authorization can be migrated safely`);
|
|
97
|
+
const rawCredential = match[1];
|
|
98
|
+
const reference = exactReference(rawCredential);
|
|
99
|
+
const sourceName = generatedSecretName(serverName, "AUTHORIZATION");
|
|
100
|
+
if (reference) {
|
|
101
|
+
return { transport: "http", url, headers: { Authorization: `Basic \${${reference}}` } };
|
|
102
|
+
}
|
|
103
|
+
secrets.push({ name: sourceName, value: rawCredential });
|
|
104
|
+
return { transport: "http", url, headers: { Authorization: `Basic \${${sourceName}}` } };
|
|
105
|
+
}
|
|
106
|
+
/** Identity deliberately ignores secret values and generated source names but
|
|
107
|
+
* retains target env/header keys, so notion and notionApi do not collapse. */
|
|
108
|
+
function semanticIdentity(def) {
|
|
109
|
+
return JSON.stringify({
|
|
110
|
+
transport: def.transport,
|
|
111
|
+
auth: def.auth,
|
|
112
|
+
command: def.command,
|
|
113
|
+
args: identityArgs(def),
|
|
114
|
+
url: def.url,
|
|
115
|
+
enabled: def.enabled,
|
|
116
|
+
agents: def.agents ? sorted(def.agents) : undefined,
|
|
117
|
+
staticEnv: def.staticEnv,
|
|
118
|
+
envKeys: sorted([...(def.env ?? []), ...Object.keys(def.envAliases ?? {})]),
|
|
119
|
+
headerKeys: sorted(Object.keys(def.headers ?? {})),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function normalizeEntry(serverName, raw, secrets) {
|
|
123
|
+
if (!isRecord(raw))
|
|
124
|
+
throw new Error(`MCP server "${serverName}" must be an object`);
|
|
125
|
+
const transport = inferTransport(raw);
|
|
126
|
+
const command = stringValue(raw.command, `${serverName}.command`);
|
|
127
|
+
const parsedArgs = stringArray(raw.args, `${serverName}.args`);
|
|
128
|
+
const args = parsedArgs && parsedArgs.length > 0 ? parsedArgs : undefined;
|
|
129
|
+
const url = stringValue(raw.url, `${serverName}.url`);
|
|
130
|
+
if (transport === "stdio" && !command)
|
|
131
|
+
throw new Error(`MCP server "${serverName}" is stdio but has no command`);
|
|
132
|
+
if ((transport === "http" || transport === "sse") && !url)
|
|
133
|
+
throw new Error(`MCP server "${serverName}" is ${transport} but has no url`);
|
|
134
|
+
if (command === "npx" && args) {
|
|
135
|
+
const remote = normalizeMcpRemote(serverName, args, secrets);
|
|
136
|
+
if (remote) {
|
|
137
|
+
if (raw.env !== undefined || raw.headers !== undefined) {
|
|
138
|
+
throw new Error(`MCP server "${serverName}" combines mcp-remote arguments with env/headers fields; import it manually after reviewing the merge`);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
...remote,
|
|
142
|
+
...(raw.auth === "oauth" ? { auth: "oauth" } : {}),
|
|
143
|
+
...(raw.disabled === true || raw.enabled === false ? { enabled: false } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const env = [];
|
|
148
|
+
const envAliases = {};
|
|
149
|
+
const staticEnv = {};
|
|
150
|
+
if (raw.env !== undefined) {
|
|
151
|
+
if (!isRecord(raw.env))
|
|
152
|
+
throw new Error(`${serverName}.env must be an object`);
|
|
153
|
+
for (const [targetKey, rawValue] of Object.entries(raw.env)) {
|
|
154
|
+
const value = stringValue(rawValue, `${serverName}.env.${targetKey}`);
|
|
155
|
+
const reference = exactReference(value);
|
|
156
|
+
if (reference) {
|
|
157
|
+
if (reference === targetKey)
|
|
158
|
+
env.push(reference);
|
|
159
|
+
else
|
|
160
|
+
envAliases[targetKey] = reference;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (looksLikeCredentialKey(targetKey) || looksLikeCredentialValue(value)) {
|
|
164
|
+
const sourceName = generatedSecretName(serverName, targetKey);
|
|
165
|
+
envAliases[targetKey] = sourceName;
|
|
166
|
+
secrets.push({ name: sourceName, value });
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
staticEnv[targetKey] = value;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const headers = {};
|
|
174
|
+
if (raw.headers !== undefined) {
|
|
175
|
+
if (!isRecord(raw.headers))
|
|
176
|
+
throw new Error(`${serverName}.headers must be an object`);
|
|
177
|
+
for (const [key, rawValue] of Object.entries(raw.headers)) {
|
|
178
|
+
const value = stringValue(rawValue, `${serverName}.headers.${key}`);
|
|
179
|
+
const reference = exactReference(value);
|
|
180
|
+
if (reference) {
|
|
181
|
+
headers[key] = `\${${reference}}`;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
throw new Error(`MCP server "${serverName}" has a literal header "${key}"; move it to an environment variable before importing`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const inlineText = [command, ...(args ?? []), url].filter(Boolean).join(" ");
|
|
188
|
+
if (looksLikeCredentialValue(inlineText)) {
|
|
189
|
+
throw new Error(`MCP server "${serverName}" has a credential embedded in command, args, or url; importer will not store it in canonical`);
|
|
190
|
+
}
|
|
191
|
+
for (const arg of args ?? []) {
|
|
192
|
+
if (/^(\/Users\/|\/home\/|[A-Za-z]:[\\/])/.test(arg) && !existsSync(arg)) {
|
|
193
|
+
throw new SkipImport(`MCP server "${serverName}" references missing absolute path "${arg}"; skipped as unavailable on this machine`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const disabled = raw.disabled === true || raw.enabled === false;
|
|
197
|
+
const auth = raw.auth === "oauth" ? "oauth" : undefined;
|
|
198
|
+
return {
|
|
199
|
+
transport,
|
|
200
|
+
...(auth ? { auth } : {}),
|
|
201
|
+
...(command ? { command } : {}),
|
|
202
|
+
...(args ? { args } : {}),
|
|
203
|
+
...(url ? { url } : {}),
|
|
204
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
205
|
+
...(env.length > 0 ? { env } : {}),
|
|
206
|
+
...(Object.keys(envAliases).length > 0 ? { envAliases } : {}),
|
|
207
|
+
...(Object.keys(staticEnv).length > 0 ? { staticEnv } : {}),
|
|
208
|
+
...(disabled ? { enabled: false } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function readExport(path) {
|
|
212
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
213
|
+
if (!isRecord(parsed.mcpServers))
|
|
214
|
+
throw new Error(`MCP import requires a top-level "mcpServers" object in ${path}`);
|
|
215
|
+
return parsed.mcpServers;
|
|
216
|
+
}
|
|
217
|
+
function publicPlan(plan) {
|
|
218
|
+
return { source: plan.source, items: plan.items };
|
|
219
|
+
}
|
|
220
|
+
export function collectMcpImportPlan(path, homeDir = homedir()) {
|
|
221
|
+
return publicPlan(collectInternalPlan(path, homeDir));
|
|
222
|
+
}
|
|
223
|
+
function collectInternalPlan(path, homeDir) {
|
|
224
|
+
if (!existsSync(path))
|
|
225
|
+
throw new Error(`MCP import file does not exist: ${path}`);
|
|
226
|
+
const source = readExport(path);
|
|
227
|
+
const canonical = loadCanonicalSource(homeDir);
|
|
228
|
+
const existingByIdentity = new Map(Object.entries(canonical.mcp.servers).map(([name, def]) => [semanticIdentity(def), name]));
|
|
229
|
+
const additions = [];
|
|
230
|
+
const secrets = [];
|
|
231
|
+
const items = [];
|
|
232
|
+
const envFilePath = canonical.secretsPolicy.envFile ?? join(homeDir, ".trellis", "mcp", "servers.local.env");
|
|
233
|
+
const existingSecrets = existsSync(envFilePath) ? parseDotenv(readFileSync(envFilePath, "utf8")) : {};
|
|
234
|
+
for (const [name, raw] of Object.entries(source)) {
|
|
235
|
+
let def;
|
|
236
|
+
const beforeSecretCount = secrets.length;
|
|
237
|
+
try {
|
|
238
|
+
def = normalizeEntry(name, raw, secrets);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
secrets.splice(beforeSecretCount);
|
|
242
|
+
if (err instanceof SkipImport) {
|
|
243
|
+
items.push({ name, action: "skipped", detail: err.message });
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
items.push({ name, action: "conflict", detail: err instanceof Error ? err.message : String(err) });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const identity = semanticIdentity(def);
|
|
250
|
+
const existing = canonical.mcp.servers[name];
|
|
251
|
+
if (existing) {
|
|
252
|
+
secrets.splice(beforeSecretCount);
|
|
253
|
+
items.push(existing && semanticIdentity(existing) === identity
|
|
254
|
+
? { name, action: "already-present", detail: "canonical entry is already semantically identical; existing credentials are preserved" }
|
|
255
|
+
: { name, action: "conflict", detail: `servers.yaml already has "${name}" with different non-secret settings — left unchanged` });
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const duplicate = existingByIdentity.get(identity) ?? additions.find((item) => semanticIdentity(item.def) === identity)?.name;
|
|
259
|
+
if (duplicate) {
|
|
260
|
+
secrets.splice(beforeSecretCount);
|
|
261
|
+
items.push({ name, action: "duplicate", detail: `same non-secret MCP definition is already represented by "${duplicate}" — not added` });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const newSecrets = secrets.slice(beforeSecretCount);
|
|
265
|
+
const secretConflict = newSecrets.find((secret) => Object.hasOwn(existingSecrets, secret.name) && existingSecrets[secret.name] !== secret.value);
|
|
266
|
+
if (secretConflict) {
|
|
267
|
+
secrets.splice(beforeSecretCount);
|
|
268
|
+
items.push({ name, action: "conflict", detail: `local secret variable "${secretConflict.name}" already has a different value — not overwritten` });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
additions.push({ name, def });
|
|
272
|
+
items.push({ name, action: "create", detail: `will add to canonical and extract ${newSecrets.length} credential value(s) to the ignored local secrets file` });
|
|
273
|
+
}
|
|
274
|
+
return { source: path, items, additions, secrets, envFilePath, envFileIsDefault: !canonical.secretsPolicy.envFile };
|
|
275
|
+
}
|
|
276
|
+
function applySecrets(plan, homeDir, backup) {
|
|
277
|
+
if (plan.secrets.length === 0)
|
|
278
|
+
return;
|
|
279
|
+
if (plan.envFileIsDefault)
|
|
280
|
+
ensureGitignoreEntry(join(homeDir, ".trellis", ".gitignore"), "mcp/servers.local.env", backup);
|
|
281
|
+
for (const secret of plan.secrets) {
|
|
282
|
+
const secretResult = writeLocalSecretValue(plan.envFilePath, secret.name, secret.value, backup);
|
|
283
|
+
if (secretResult === "conflict")
|
|
284
|
+
throw new Error(`local secret variable "${secret.name}" changed during import — refusing to overwrite it`);
|
|
285
|
+
const policyResult = writeSecretsPolicyExtraction(join(homeDir, ".trellis", "secrets.policy.yaml"), { varName: secret.name, envFilePath: DEFAULT_ENV_FILE_TILDE }, backup);
|
|
286
|
+
if (!policyResult.ok)
|
|
287
|
+
throw new Error(policyResult.error);
|
|
288
|
+
}
|
|
289
|
+
ensureShellEnvSource(defaultShellRcPath(homeDir), plan.envFilePath, backup);
|
|
290
|
+
}
|
|
291
|
+
export function applyMcpImportPlan(path, homeDir = homedir(), backup) {
|
|
292
|
+
const plan = collectInternalPlan(path, homeDir);
|
|
293
|
+
const ownSession = backup ?? openBackupSession(homeDir, "mcp-import");
|
|
294
|
+
try {
|
|
295
|
+
for (const addition of plan.additions) {
|
|
296
|
+
const result = upsertServerYaml(join(homeDir, ".trellis", "mcp", "servers.yaml"), addition.name, addition.def, ownSession);
|
|
297
|
+
if (!result.ok)
|
|
298
|
+
throw new Error(result.error);
|
|
299
|
+
}
|
|
300
|
+
applySecrets(plan, homeDir, ownSession);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
// Keep the manifest even on failure so the caller can use the normal
|
|
304
|
+
// `trellis rollback` path to undo writes that happened before the error.
|
|
305
|
+
if (!backup)
|
|
306
|
+
ownSession.finalize();
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
if (!backup)
|
|
310
|
+
ownSession.finalize();
|
|
311
|
+
return publicPlan(plan);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* See `applyMcpAddWithSync`'s doc comment (mcp.ts) — same split, same
|
|
315
|
+
* reason: a non-CLI caller (trellis-gui's sidecar) gets the same
|
|
316
|
+
* structured result a JSON-mode CLI run prints, without scraping
|
|
317
|
+
* stdout or re-deriving the plan-then-apply-then-cascade-sync
|
|
318
|
+
* sequencing itself. Unlike `mcp add`/`mcp remove`, `applyMcpImportPlan`
|
|
319
|
+
* already opens its own backup session unconditionally (it always has,
|
|
320
|
+
* per design.md's original survey of `openBackupSession` call sites) —
|
|
321
|
+
* no tasks.md 3.5-style fix needed here.
|
|
322
|
+
*/
|
|
323
|
+
export async function applyMcpImportWithSync(path, opts = {}) {
|
|
324
|
+
const homeDir = opts.homeDir ?? homedir();
|
|
325
|
+
const plan = opts.dryRun ? collectMcpImportPlan(path, homeDir) : applyMcpImportPlan(path, homeDir);
|
|
326
|
+
const failed = plan.items.some((item) => item.action === "conflict" || item.action === "invalid-input");
|
|
327
|
+
const changed = plan.items.some((item) => item.action === "create");
|
|
328
|
+
let syncReport;
|
|
329
|
+
if (!failed && changed && existsSync(join(homeDir, ".trellis"))) {
|
|
330
|
+
syncReport = await collectMcpSyncReport({ homeDir, dryRun: opts.dryRun });
|
|
331
|
+
}
|
|
332
|
+
return { plan, ...(syncReport ? { sync: syncReport } : {}) };
|
|
333
|
+
}
|
|
334
|
+
export async function runMcpImport(path, opts = {}) {
|
|
335
|
+
const homeDir = opts.homeDir ?? homedir();
|
|
336
|
+
if (!path) {
|
|
337
|
+
console.error("Usage: trellis mcp import <json-file> [--dry-run]");
|
|
338
|
+
return { exitCode: 1 };
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const { plan: result, sync: syncReport } = await applyMcpImportWithSync(path, { homeDir, dryRun: opts.dryRun });
|
|
342
|
+
const failed = result.items.some((item) => item.action === "conflict" || item.action === "invalid-input");
|
|
343
|
+
if (opts.json)
|
|
344
|
+
console.log(JSON.stringify(syncReport ? { ...result, sync: syncReport } : result, null, 2));
|
|
345
|
+
else {
|
|
346
|
+
console.log(`${opts.dryRun ? "[dry run] " : ""}mcp import ${path}`);
|
|
347
|
+
for (const item of result.items)
|
|
348
|
+
console.log(` [${item.action}] ${item.name} — ${item.detail}`);
|
|
349
|
+
if (syncReport)
|
|
350
|
+
printMcpSyncReport(syncReport, opts.dryRun ?? false);
|
|
351
|
+
}
|
|
352
|
+
const syncConflict = syncReport?.reports.some((r) => r.items.some((i) => i.action === "conflict")) ?? false;
|
|
353
|
+
return { exitCode: failed || syncConflict ? 1 : 0 };
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
357
|
+
return { exitCode: 1 };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
@@ -12,8 +12,21 @@
|
|
|
12
12
|
* server process — both are plain file operations, same posture as
|
|
13
13
|
* every other Trellis write.
|
|
14
14
|
*/
|
|
15
|
-
import type { McpServerDef } from "../core/types.js";
|
|
15
|
+
import type { AgentId, McpConfig, McpServerDef } from "../core/types.js";
|
|
16
|
+
import type { BackupSession } from "../lib/backup.js";
|
|
16
17
|
import { type MemoryExtractionPlan, type MemorySyncPlan } from "../lib/memoryGraph.js";
|
|
18
|
+
export interface MemoryListEntry {
|
|
19
|
+
name: string;
|
|
20
|
+
scope: readonly AgentId[];
|
|
21
|
+
}
|
|
22
|
+
/** Canonical `memories/*.md` profile files — the list `trellis-gui`'s
|
|
23
|
+
* Memory view reads (tasks.md 6.2), mirroring `collectSkillList`'s
|
|
24
|
+
* shape exactly (skill.ts) since both are "canonical files with a
|
|
25
|
+
* resolved managed-agent scope" lists. Distinct from this file's own
|
|
26
|
+
* `MEMORY_SERVER_NAME` sync/extraction pipeline below, which is about
|
|
27
|
+
* the separate shared-Memory-MCP-server knowledge graph, not these
|
|
28
|
+
* per-topic markdown files. */
|
|
29
|
+
export declare function collectMemoryList(homeDir?: string): MemoryListEntry[];
|
|
17
30
|
/** Exported so `onboard` (`trellis onboard --memory on|off`,
|
|
18
31
|
* trellis-onboard-mcp-mode) can look up and write this same entry
|
|
19
32
|
* without hardcoding the name a second time. */
|
|
@@ -32,6 +45,7 @@ export interface RunMemorySyncOptions {
|
|
|
32
45
|
homeDir?: string;
|
|
33
46
|
json?: boolean;
|
|
34
47
|
dryRun?: boolean;
|
|
48
|
+
selectedMemoryNames?: readonly string[];
|
|
35
49
|
}
|
|
36
50
|
export type MemorySyncResult = {
|
|
37
51
|
configured: true;
|
|
@@ -41,8 +55,8 @@ export type MemorySyncResult = {
|
|
|
41
55
|
configured: false;
|
|
42
56
|
reason: string;
|
|
43
57
|
};
|
|
44
|
-
export declare function collectMemorySyncResult(homeDir?: string): MemorySyncResult;
|
|
45
|
-
export declare function applyMemorySync(result: MemorySyncResult): void;
|
|
58
|
+
export declare function collectMemorySyncResult(homeDir?: string, selectedMemoryNames?: readonly string[], mcpOverride?: McpConfig): MemorySyncResult;
|
|
59
|
+
export declare function applyMemorySync(result: MemorySyncResult, backup?: BackupSession): void;
|
|
46
60
|
/**
|
|
47
61
|
* Extracted from `runMemorySync` (trellis-onboard-mcp-memory) so
|
|
48
62
|
* `onboard`'s own chained output can print this stage in the exact same
|
package/dist/commands/memory.js
CHANGED
|
@@ -16,7 +16,22 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
16
16
|
import { homedir } from "node:os";
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
18
|
import { loadCanonicalSource } from "../core/canonical.js";
|
|
19
|
+
import { resolveScope } from "../core/types.js";
|
|
19
20
|
import { parseMemoryGraph, planMemoryExtraction, planMemorySync, renderMemoryGraph } from "../lib/memoryGraph.js";
|
|
21
|
+
/** Canonical `memories/*.md` profile files — the list `trellis-gui`'s
|
|
22
|
+
* Memory view reads (tasks.md 6.2), mirroring `collectSkillList`'s
|
|
23
|
+
* shape exactly (skill.ts) since both are "canonical files with a
|
|
24
|
+
* resolved managed-agent scope" lists. Distinct from this file's own
|
|
25
|
+
* `MEMORY_SERVER_NAME` sync/extraction pipeline below, which is about
|
|
26
|
+
* the separate shared-Memory-MCP-server knowledge graph, not these
|
|
27
|
+
* per-topic markdown files. */
|
|
28
|
+
export function collectMemoryList(homeDir = homedir()) {
|
|
29
|
+
const canonical = loadCanonicalSource(homeDir);
|
|
30
|
+
return canonical.memories.map((memory) => ({
|
|
31
|
+
name: memory.name,
|
|
32
|
+
scope: resolveScope(memory.scope, canonical.managedAgents),
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
20
35
|
/** Exported so `onboard` (`trellis onboard --memory on|off`,
|
|
21
36
|
* trellis-onboard-mcp-mode) can look up and write this same entry
|
|
22
37
|
* without hardcoding the name a second time. */
|
|
@@ -50,9 +65,9 @@ export const DEFAULT_MEMORY_SERVER_DEF = {
|
|
|
50
65
|
* about which graph file they're reading/writing
|
|
51
66
|
* (trellis-memory-extraction).
|
|
52
67
|
*/
|
|
53
|
-
function resolveMemoryServerGraphPath(homeDir) {
|
|
68
|
+
function resolveMemoryServerGraphPath(homeDir, mcpOverride) {
|
|
54
69
|
const canonical = loadCanonicalSource(homeDir);
|
|
55
|
-
const server = canonical.mcp.servers[MEMORY_SERVER_NAME];
|
|
70
|
+
const server = (mcpOverride ?? canonical.mcp).servers[MEMORY_SERVER_NAME];
|
|
56
71
|
const rawPath = server?.staticEnv?.MEMORY_FILE_PATH;
|
|
57
72
|
if (!server || !rawPath) {
|
|
58
73
|
return {
|
|
@@ -62,20 +77,24 @@ function resolveMemoryServerGraphPath(homeDir) {
|
|
|
62
77
|
}
|
|
63
78
|
return { configured: true, graphPath: rawPath.replace(/^~(?=$|\/)/, homeDir) };
|
|
64
79
|
}
|
|
65
|
-
export function collectMemorySyncResult(homeDir = homedir()) {
|
|
66
|
-
const lookup = resolveMemoryServerGraphPath(homeDir);
|
|
80
|
+
export function collectMemorySyncResult(homeDir = homedir(), selectedMemoryNames, mcpOverride) {
|
|
81
|
+
const lookup = resolveMemoryServerGraphPath(homeDir, mcpOverride);
|
|
67
82
|
if (!lookup.configured)
|
|
68
83
|
return lookup;
|
|
69
84
|
const canonical = loadCanonicalSource(homeDir);
|
|
70
85
|
const currentContent = existsSync(lookup.graphPath) ? readFileSync(lookup.graphPath, "utf-8") : undefined;
|
|
71
|
-
const plan = planMemorySync(canonical, currentContent);
|
|
86
|
+
const plan = planMemorySync(canonical, currentContent, selectedMemoryNames);
|
|
72
87
|
return { configured: true, graphPath: lookup.graphPath, plan };
|
|
73
88
|
}
|
|
74
|
-
export function applyMemorySync(result) {
|
|
89
|
+
export function applyMemorySync(result, backup) {
|
|
75
90
|
if (!result.configured || !result.plan.nextGraph)
|
|
76
91
|
return;
|
|
77
92
|
mkdirSync(dirname(result.graphPath), { recursive: true });
|
|
78
|
-
|
|
93
|
+
const content = renderMemoryGraph(result.plan.nextGraph);
|
|
94
|
+
if (backup)
|
|
95
|
+
backup.writeFile(result.graphPath, content);
|
|
96
|
+
else
|
|
97
|
+
writeFileSync(result.graphPath, content);
|
|
79
98
|
}
|
|
80
99
|
/**
|
|
81
100
|
* Extracted from `runMemorySync` (trellis-onboard-mcp-memory) so
|
|
@@ -101,7 +120,7 @@ export function runMemorySync(opts = {}) {
|
|
|
101
120
|
const homeDir = opts.homeDir ?? homedir();
|
|
102
121
|
let result;
|
|
103
122
|
try {
|
|
104
|
-
result = collectMemorySyncResult(homeDir);
|
|
123
|
+
result = collectMemorySyncResult(homeDir, opts.selectedMemoryNames);
|
|
105
124
|
}
|
|
106
125
|
catch (err) {
|
|
107
126
|
console.error(err instanceof Error ? err.message : String(err));
|