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
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import type { GatewayBackend } from "./gatewayBackend.js";
|
|
3
|
+
import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
|
|
4
|
+
export declare class McpStatusProvider implements TrellisProvider {
|
|
5
|
+
private readonly backend;
|
|
6
|
+
readonly id = "mcp";
|
|
7
|
+
constructor(backend: GatewayBackend);
|
|
8
|
+
listTools(): Tool[];
|
|
9
|
+
callTool(name: string, _args: unknown, _context: RuntimeContext): Promise<CallToolResult>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const STATUS_TOOL = {
|
|
2
|
+
name: "trellis.mcp.status",
|
|
3
|
+
description: "Show MCP upstream availability and the safe next action for authentication or configuration failures.",
|
|
4
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
5
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
6
|
+
};
|
|
7
|
+
export class McpStatusProvider {
|
|
8
|
+
backend;
|
|
9
|
+
id = "mcp";
|
|
10
|
+
constructor(backend) {
|
|
11
|
+
this.backend = backend;
|
|
12
|
+
}
|
|
13
|
+
listTools() {
|
|
14
|
+
return [STATUS_TOOL];
|
|
15
|
+
}
|
|
16
|
+
async callTool(name, _args, _context) {
|
|
17
|
+
if (name !== STATUS_TOOL.name)
|
|
18
|
+
return { content: [{ type: "text", text: `unknown MCP status tool "${name}"` }], isError: true };
|
|
19
|
+
const statuses = this.backend.listStatus?.() ?? [];
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text", text: JSON.stringify({ servers: statuses }, null, 2) }],
|
|
22
|
+
structuredContent: { servers: statuses },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -33,6 +33,11 @@ export interface UpstreamTool {
|
|
|
33
33
|
[key: string]: unknown;
|
|
34
34
|
}
|
|
35
35
|
export type AggregatedTool = UpstreamTool;
|
|
36
|
+
/** The strictest Agent-facing tool-name contract currently exercised by the
|
|
37
|
+
* local stack (notably the Codex-compatible model tool surface). */
|
|
38
|
+
export declare const MAX_EXPOSED_TOOL_NAME_LENGTH = 64;
|
|
39
|
+
export declare const SHORT_EXPOSED_TOOL_NAME_LENGTH = 12;
|
|
40
|
+
export declare const EXPOSED_TOOL_NAME_PATTERN: RegExp;
|
|
36
41
|
/**
|
|
37
42
|
* `${serverName}__${toolName}`, the separator every consumer of this
|
|
38
43
|
* registry shares. Two underscores rather than one because single
|
|
@@ -41,7 +46,17 @@ export type AggregatedTool = UpstreamTool;
|
|
|
41
46
|
* gateway matching it keeps one naming convention across both consumers.
|
|
42
47
|
*/
|
|
43
48
|
export declare const TOOL_NAME_SEPARATOR = "__";
|
|
49
|
+
/** Normalize one raw tool name without adding a server prefix. */
|
|
50
|
+
export declare function normalizedToolName(toolName: string): string;
|
|
44
51
|
export declare function prefixedToolName(serverName: string, toolName: string): string;
|
|
52
|
+
export interface ToolNameCandidate {
|
|
53
|
+
serverName: string;
|
|
54
|
+
toolName: string;
|
|
55
|
+
}
|
|
56
|
+
/** Allocate all exposed names together so normalization collisions are
|
|
57
|
+
* deterministic and no upstream tool is silently lost. The returned array
|
|
58
|
+
* is indexed like `candidates`. */
|
|
59
|
+
export declare function allocateExposedToolNames(candidates: readonly ToolNameCandidate[]): string[];
|
|
45
60
|
export interface AddResult {
|
|
46
61
|
/** Prefixed names actually registered by this call. */
|
|
47
62
|
added: string[];
|
|
@@ -58,6 +73,8 @@ export interface AddResult {
|
|
|
58
73
|
export declare class McpToolRegistry {
|
|
59
74
|
private readonly entries;
|
|
60
75
|
private readonly order;
|
|
76
|
+
private readonly candidates;
|
|
77
|
+
private rebuild;
|
|
61
78
|
/**
|
|
62
79
|
* Register one connected upstream's tools. Callers add each upstream
|
|
63
80
|
* independently, so an upstream that failed to connect or to list its
|
|
@@ -65,6 +82,7 @@ export declare class McpToolRegistry {
|
|
|
65
82
|
* aggregated, with no special-casing here.
|
|
66
83
|
*/
|
|
67
84
|
add(serverName: string, upstream: ToolUpstream, tools: readonly AggregatedTool[]): AddResult;
|
|
85
|
+
private exposedNameFor;
|
|
68
86
|
/** Every registered tool, under its prefixed name, in registration
|
|
69
87
|
* order. Each tool's own `description`/`inputSchema` and any additional
|
|
70
88
|
* fields the upstream sent are preserved verbatim; only `name` changes. */
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
* forwarding client to a shared gateway service, both fit without this
|
|
14
14
|
* module changing (design.md D11).
|
|
15
15
|
*/
|
|
16
|
+
/** The strictest Agent-facing tool-name contract currently exercised by the
|
|
17
|
+
* local stack (notably the Codex-compatible model tool surface). */
|
|
18
|
+
export const MAX_EXPOSED_TOOL_NAME_LENGTH = 64;
|
|
19
|
+
export const SHORT_EXPOSED_TOOL_NAME_LENGTH = 12;
|
|
20
|
+
export const EXPOSED_TOOL_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
16
21
|
/**
|
|
17
22
|
* `${serverName}__${toolName}`, the separator every consumer of this
|
|
18
23
|
* registry shares. Two underscores rather than one because single
|
|
@@ -21,12 +26,94 @@
|
|
|
21
26
|
* gateway matching it keeps one naming convention across both consumers.
|
|
22
27
|
*/
|
|
23
28
|
export const TOOL_NAME_SEPARATOR = "__";
|
|
29
|
+
function stableHash(value) {
|
|
30
|
+
// FNV-1a is deliberately small and deterministic; this is only a bounded
|
|
31
|
+
// display-name disambiguator, never a security or persistence primitive.
|
|
32
|
+
let hash = 0x811c9dc5;
|
|
33
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
34
|
+
hash ^= value.charCodeAt(index);
|
|
35
|
+
hash = Math.imul(hash, 0x01000193);
|
|
36
|
+
}
|
|
37
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
38
|
+
}
|
|
39
|
+
function safeSegment(value, fallback) {
|
|
40
|
+
const normalized = value.replace(/[^a-zA-Z0-9_-]+/g, "_");
|
|
41
|
+
return normalized || fallback;
|
|
42
|
+
}
|
|
43
|
+
function compactSourceSegment(value) {
|
|
44
|
+
const normalized = safeSegment(value, "server");
|
|
45
|
+
return safeSegment(normalized.replace(/^mcp(?:_|-)+/i, ""), "server");
|
|
46
|
+
}
|
|
47
|
+
function boundedName(value, identity) {
|
|
48
|
+
if (value.length <= MAX_EXPOSED_TOOL_NAME_LENGTH && EXPOSED_TOOL_NAME_PATTERN.test(value))
|
|
49
|
+
return value;
|
|
50
|
+
const suffix = `__${stableHash(identity)}`;
|
|
51
|
+
const prefixLength = Math.max(1, MAX_EXPOSED_TOOL_NAME_LENGTH - suffix.length);
|
|
52
|
+
return `${value.slice(0, prefixLength)}${suffix}`;
|
|
53
|
+
}
|
|
54
|
+
/** Normalize one raw tool name without adding a server prefix. */
|
|
55
|
+
export function normalizedToolName(toolName) {
|
|
56
|
+
return boundedName(safeSegment(toolName, "tool"), `tool\0${toolName}`);
|
|
57
|
+
}
|
|
24
58
|
export function prefixedToolName(serverName, toolName) {
|
|
25
|
-
|
|
59
|
+
const server = compactSourceSegment(serverName);
|
|
60
|
+
const tool = safeSegment(toolName, "tool");
|
|
61
|
+
return boundedName(`${server}${TOOL_NAME_SEPARATOR}${tool}`, `prefixed\0${serverName}\0${toolName}`);
|
|
62
|
+
}
|
|
63
|
+
/** Allocate all exposed names together so normalization collisions are
|
|
64
|
+
* deterministic and no upstream tool is silently lost. The returned array
|
|
65
|
+
* is indexed like `candidates`. */
|
|
66
|
+
export function allocateExposedToolNames(candidates) {
|
|
67
|
+
const counts = new Map();
|
|
68
|
+
for (const candidate of candidates)
|
|
69
|
+
counts.set(candidate.toolName, (counts.get(candidate.toolName) ?? 0) + 1);
|
|
70
|
+
const ordered = candidates
|
|
71
|
+
.map((candidate, index) => ({ candidate, index }))
|
|
72
|
+
.sort((left, right) => left.candidate.serverName.localeCompare(right.candidate.serverName)
|
|
73
|
+
|| left.candidate.toolName.localeCompare(right.candidate.toolName)
|
|
74
|
+
|| left.index - right.index);
|
|
75
|
+
const allocated = new Array(candidates.length);
|
|
76
|
+
const used = new Set();
|
|
77
|
+
for (const { candidate, index } of ordered) {
|
|
78
|
+
const normalized = normalizedToolName(candidate.toolName);
|
|
79
|
+
const source = compactSourceSegment(candidate.serverName);
|
|
80
|
+
// A `__` segment is already a source-qualified name from an upstream
|
|
81
|
+
// registry (for example `fixture__env` reaching the Runtime provider).
|
|
82
|
+
// Do not add a second provider prefix around it.
|
|
83
|
+
const sourceQualified = normalized.includes("__") || normalized === source || normalized.startsWith(`${source}_`) || normalized.startsWith(`${source}__`);
|
|
84
|
+
const unique = (counts.get(candidate.toolName) ?? 0) === 1;
|
|
85
|
+
const base = unique && normalized.length > SHORT_EXPOSED_TOOL_NAME_LENGTH
|
|
86
|
+
? normalized
|
|
87
|
+
: unique && sourceQualified
|
|
88
|
+
? normalized
|
|
89
|
+
: prefixedToolName(candidate.serverName, candidate.toolName);
|
|
90
|
+
let exposed = base;
|
|
91
|
+
let suffix = 2;
|
|
92
|
+
while (used.has(exposed)) {
|
|
93
|
+
exposed = boundedName(`${base}${TOOL_NAME_SEPARATOR}${suffix}`, `collision\0${candidate.serverName}\0${candidate.toolName}\0${suffix}`);
|
|
94
|
+
suffix += 1;
|
|
95
|
+
}
|
|
96
|
+
used.add(exposed);
|
|
97
|
+
allocated[index] = exposed;
|
|
98
|
+
}
|
|
99
|
+
return allocated;
|
|
26
100
|
}
|
|
27
101
|
export class McpToolRegistry {
|
|
28
102
|
entries = new Map();
|
|
29
103
|
order = [];
|
|
104
|
+
candidates = [];
|
|
105
|
+
rebuild() {
|
|
106
|
+
this.entries.clear();
|
|
107
|
+
this.order.length = 0;
|
|
108
|
+
const allocated = allocateExposedToolNames(this.candidates);
|
|
109
|
+
const ordered = this.candidates
|
|
110
|
+
.map((candidate, index) => ({ candidate, index, exposed: allocated[index] }))
|
|
111
|
+
.sort((left, right) => left.candidate.serverName.localeCompare(right.candidate.serverName) || left.candidate.toolName.localeCompare(right.candidate.toolName) || left.index - right.index);
|
|
112
|
+
for (const { candidate, exposed } of ordered) {
|
|
113
|
+
this.entries.set(exposed, candidate);
|
|
114
|
+
this.order.push(exposed);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
30
117
|
/**
|
|
31
118
|
* Register one connected upstream's tools. Callers add each upstream
|
|
32
119
|
* independently, so an upstream that failed to connect or to list its
|
|
@@ -34,32 +121,36 @@ export class McpToolRegistry {
|
|
|
34
121
|
* aggregated, with no special-casing here.
|
|
35
122
|
*/
|
|
36
123
|
add(serverName, upstream, tools) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
added.push(name);
|
|
124
|
+
for (const tool of tools)
|
|
125
|
+
this.candidates.push({ serverName, toolName: tool.name, upstream, tool });
|
|
126
|
+
this.rebuild();
|
|
127
|
+
const added = tools.map((tool) => this.exposedNameFor(serverName, tool.name)).filter((name) => name !== undefined);
|
|
128
|
+
return { added, skipped: [] };
|
|
129
|
+
}
|
|
130
|
+
exposedNameFor(serverName, toolName) {
|
|
131
|
+
for (const [name, entry] of this.entries) {
|
|
132
|
+
if (entry.serverName === serverName && entry.toolName === toolName)
|
|
133
|
+
return name;
|
|
48
134
|
}
|
|
49
|
-
return
|
|
135
|
+
return undefined;
|
|
50
136
|
}
|
|
51
137
|
/** Every registered tool, under its prefixed name, in registration
|
|
52
138
|
* order. Each tool's own `description`/`inputSchema` and any additional
|
|
53
139
|
* fields the upstream sent are preserved verbatim; only `name` changes. */
|
|
54
140
|
listTools() {
|
|
141
|
+
this.rebuild();
|
|
55
142
|
return this.order.map((name) => {
|
|
56
143
|
const entry = this.entries.get(name);
|
|
57
|
-
|
|
144
|
+
const title = name === entry.toolName || entry.tool.title !== undefined
|
|
145
|
+
? entry.tool.title
|
|
146
|
+
: `${entry.serverName} / ${entry.toolName}`;
|
|
147
|
+
return { ...entry.tool, name, ...(title === undefined ? {} : { title }) };
|
|
58
148
|
});
|
|
59
149
|
}
|
|
60
150
|
/** The owning upstream and original tool name for a prefixed name, or
|
|
61
151
|
* `undefined` if nothing owns it. */
|
|
62
152
|
resolve(prefixedName) {
|
|
153
|
+
this.rebuild();
|
|
63
154
|
const entry = this.entries.get(prefixedName);
|
|
64
155
|
if (!entry)
|
|
65
156
|
return undefined;
|
|
@@ -80,6 +171,7 @@ export class McpToolRegistry {
|
|
|
80
171
|
/** Distinct server names with at least one registered tool, in
|
|
81
172
|
* registration order. */
|
|
82
173
|
servers() {
|
|
174
|
+
this.rebuild();
|
|
83
175
|
const seen = [];
|
|
84
176
|
for (const name of this.order) {
|
|
85
177
|
const { serverName } = this.entries.get(name);
|
|
@@ -89,6 +181,7 @@ export class McpToolRegistry {
|
|
|
89
181
|
return seen;
|
|
90
182
|
}
|
|
91
183
|
get size() {
|
|
184
|
+
this.rebuild();
|
|
92
185
|
return this.entries.size;
|
|
93
186
|
}
|
|
94
187
|
}
|
|
@@ -57,7 +57,7 @@ export interface MemorySyncPlan {
|
|
|
57
57
|
* entity and every relation passes through completely untouched,
|
|
58
58
|
* regardless of what canonical wants.
|
|
59
59
|
*/
|
|
60
|
-
export declare function planMemorySync(canonical: Pick<CanonicalSource, "memories">, currentGraphContent: string | undefined): MemorySyncPlan;
|
|
60
|
+
export declare function planMemorySync(canonical: Pick<CanonicalSource, "memories">, currentGraphContent: string | undefined, selectedMemoryNames?: readonly string[]): MemorySyncPlan;
|
|
61
61
|
/**
|
|
62
62
|
* Renders one extraction candidate as readable markdown — the entity's
|
|
63
63
|
* real (unslugged) name as a heading, its type, its observations as a
|
package/dist/lib/memoryGraph.js
CHANGED
|
@@ -56,16 +56,20 @@ export function renderMemoryGraph(lines) {
|
|
|
56
56
|
* entity and every relation passes through completely untouched,
|
|
57
57
|
* regardless of what canonical wants.
|
|
58
58
|
*/
|
|
59
|
-
export function planMemorySync(canonical, currentGraphContent) {
|
|
59
|
+
export function planMemorySync(canonical, currentGraphContent, selectedMemoryNames) {
|
|
60
60
|
const existingLines = currentGraphContent !== undefined ? parseMemoryGraph(currentGraphContent) : [];
|
|
61
61
|
const untouchedLines = existingLines.filter((line) => !(line.type === "entity" && line.entityType === TRELLIS_MEMORY_ENTITY_TYPE));
|
|
62
62
|
const existingTrellisEntities = new Map(existingLines.filter((line) => line.type === "entity" && line.entityType === TRELLIS_MEMORY_ENTITY_TYPE).map((e) => [e.name, e]));
|
|
63
63
|
const existingOtherEntityNames = new Set(existingLines.filter((line) => line.type === "entity" && line.entityType !== TRELLIS_MEMORY_ENTITY_TYPE).map((e) => e.name));
|
|
64
|
+
const selected = selectedMemoryNames !== undefined ? new Set(selectedMemoryNames) : undefined;
|
|
65
|
+
const selectedMemories = selected ? canonical.memories.filter((memory) => selected.has(memory.name)) : canonical.memories;
|
|
64
66
|
const items = [];
|
|
65
|
-
const nextTrellisEntities =
|
|
67
|
+
const nextTrellisEntities = selected
|
|
68
|
+
? [...existingTrellisEntities.values()].filter((entity) => !selected.has(entity.name))
|
|
69
|
+
: [];
|
|
66
70
|
let changed = false;
|
|
67
71
|
const canonicalNames = new Set(canonical.memories.map((m) => m.name));
|
|
68
|
-
for (const memory of
|
|
72
|
+
for (const memory of selectedMemories) {
|
|
69
73
|
if (existingOtherEntityNames.has(memory.name)) {
|
|
70
74
|
items.push({ name: memory.name, action: "conflict", detail: `an entity named "${memory.name}" already exists in the graph and wasn't created by Trellis — resolve by hand` });
|
|
71
75
|
continue;
|
|
@@ -90,6 +94,8 @@ export function planMemorySync(canonical, currentGraphContent) {
|
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
for (const [name] of existingTrellisEntities) {
|
|
97
|
+
if (selected)
|
|
98
|
+
continue;
|
|
93
99
|
if (canonicalNames.has(name))
|
|
94
100
|
continue; // still wanted, handled above
|
|
95
101
|
items.push({ name, action: "remove", detail: "no longer in canonical — will be removed from the graph" });
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { CallToolResult, ReadResourceResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import type { Scope } from "../core/types.js";
|
|
3
|
+
import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
|
|
4
|
+
/**
|
|
5
|
+
* The storage-independent boundary for memory providers. A future local
|
|
6
|
+
* graph or OpenViking adapter can implement this contract without changing
|
|
7
|
+
* the MCP edge or the agent-facing tool names.
|
|
8
|
+
*
|
|
9
|
+
* Providers are deliberately read-only here. Mutation needs a separate
|
|
10
|
+
* reviewed capability with explicit audit and approval semantics.
|
|
11
|
+
*/
|
|
12
|
+
export interface MemoryProvider {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
list(context: RuntimeContext): readonly MemoryRef[] | Promise<readonly MemoryRef[]>;
|
|
15
|
+
read(name: string, context: RuntimeContext): MemorySourceDocument | undefined | Promise<MemorySourceDocument | undefined>;
|
|
16
|
+
}
|
|
17
|
+
export interface MemoryRef {
|
|
18
|
+
name: string;
|
|
19
|
+
scope?: Scope;
|
|
20
|
+
}
|
|
21
|
+
export interface MemorySourceDocument {
|
|
22
|
+
content: string;
|
|
23
|
+
}
|
|
24
|
+
export interface MemoryDocument {
|
|
25
|
+
name: string;
|
|
26
|
+
content: string;
|
|
27
|
+
fingerprint: string;
|
|
28
|
+
uri: string;
|
|
29
|
+
scope: "in-scope";
|
|
30
|
+
source: string;
|
|
31
|
+
}
|
|
32
|
+
export interface MemorySearchHit {
|
|
33
|
+
name: string;
|
|
34
|
+
fingerprint: string;
|
|
35
|
+
uri: string;
|
|
36
|
+
scope: "in-scope";
|
|
37
|
+
source: string;
|
|
38
|
+
}
|
|
39
|
+
/** The default provider reads only canonical Markdown memory files. */
|
|
40
|
+
export declare class CanonicalMemoryProvider implements MemoryProvider {
|
|
41
|
+
readonly id = "canonical";
|
|
42
|
+
list(context: RuntimeContext): readonly MemoryRef[];
|
|
43
|
+
read(name: string, context: RuntimeContext): MemorySourceDocument | undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Adapts any read-only MemoryProvider to Trellis's MCP provider registry. */
|
|
46
|
+
export declare class RuntimeMemoryProvider implements TrellisProvider {
|
|
47
|
+
private readonly source;
|
|
48
|
+
readonly id = "memory";
|
|
49
|
+
constructor(source?: MemoryProvider);
|
|
50
|
+
listTools(): Tool[];
|
|
51
|
+
callTool(name: string, args: unknown, context: RuntimeContext): Promise<CallToolResult>;
|
|
52
|
+
listResources(context: RuntimeContext): Promise<Resource[]>;
|
|
53
|
+
readResource(uri: URL, context: RuntimeContext): Promise<ReadResourceResult>;
|
|
54
|
+
private inScopeRefs;
|
|
55
|
+
private readDocument;
|
|
56
|
+
private search;
|
|
57
|
+
private readTool;
|
|
58
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { relative, resolve, sep } from "node:path";
|
|
4
|
+
import { isInScope } from "../core/adapter.js";
|
|
5
|
+
import { loadCanonicalSource } from "../core/canonical.js";
|
|
6
|
+
const MAX_MEMORY_BYTES = 256 * 1024;
|
|
7
|
+
const MEMORY_RESOURCE_PREFIX = "trellis://memories/";
|
|
8
|
+
const SEARCH_TOOL = {
|
|
9
|
+
name: "trellis.memory.search",
|
|
10
|
+
description: "Search in-scope Trellis memories by name or content, then read one selected memory on demand.",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: { query: { type: "string" }, limit: { type: "number", minimum: 1, maximum: 50 } },
|
|
14
|
+
required: ["query"],
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
},
|
|
17
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
18
|
+
};
|
|
19
|
+
const READ_TOOL = {
|
|
20
|
+
name: "trellis.memory.read",
|
|
21
|
+
description: "Read one in-scope canonical Trellis memory as Markdown.",
|
|
22
|
+
inputSchema: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: { name: { type: "string" } },
|
|
25
|
+
required: ["name"],
|
|
26
|
+
additionalProperties: false,
|
|
27
|
+
},
|
|
28
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
29
|
+
};
|
|
30
|
+
/** The default provider reads only canonical Markdown memory files. */
|
|
31
|
+
export class CanonicalMemoryProvider {
|
|
32
|
+
id = "canonical";
|
|
33
|
+
list(context) {
|
|
34
|
+
return loadCanonicalSource(context.homeDir).memories.map(({ name, scope }) => ({ name, scope }));
|
|
35
|
+
}
|
|
36
|
+
read(name, context) {
|
|
37
|
+
const canonical = loadCanonicalSource(context.homeDir);
|
|
38
|
+
const memory = canonical.memories.find((candidate) => candidate.name === name);
|
|
39
|
+
if (!memory)
|
|
40
|
+
return undefined;
|
|
41
|
+
const memoryRoot = resolve(context.homeDir, ".trellis", "memories");
|
|
42
|
+
const root = realpathSync(memoryRoot);
|
|
43
|
+
const file = realpathSync(memory.file);
|
|
44
|
+
const relativePath = relative(root, file);
|
|
45
|
+
if (relativePath.startsWith(`..${sep}`) || relativePath === "..") {
|
|
46
|
+
throw new Error("memory file is outside the canonical memories directory");
|
|
47
|
+
}
|
|
48
|
+
const stat = statSync(file);
|
|
49
|
+
if (!stat.isFile())
|
|
50
|
+
throw new Error("memory path is not a file");
|
|
51
|
+
if (stat.size > MAX_MEMORY_BYTES)
|
|
52
|
+
throw new Error(`memory exceeds the ${MAX_MEMORY_BYTES}-byte provider limit`);
|
|
53
|
+
return { content: readFileSync(file, "utf8") };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function textResult(value) {
|
|
57
|
+
return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], structuredContent: value };
|
|
58
|
+
}
|
|
59
|
+
function errorResult(message) {
|
|
60
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
61
|
+
}
|
|
62
|
+
function fingerprint(content) {
|
|
63
|
+
return createHash("sha256").update(content).digest("hex");
|
|
64
|
+
}
|
|
65
|
+
function memoryUri(name) {
|
|
66
|
+
return `${MEMORY_RESOURCE_PREFIX}${encodeURIComponent(name)}.md`;
|
|
67
|
+
}
|
|
68
|
+
function memoryNameFromUri(uri) {
|
|
69
|
+
if (uri.protocol !== "trellis:" || uri.hostname !== "memories") {
|
|
70
|
+
throw new Error(`unsupported memory resource URI "${uri.toString()}"`);
|
|
71
|
+
}
|
|
72
|
+
const segments = uri.pathname.split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
|
|
73
|
+
if (segments.length !== 1 || !segments[0].endsWith(".md")) {
|
|
74
|
+
throw new Error(`unsupported memory resource URI "${uri.toString()}"`);
|
|
75
|
+
}
|
|
76
|
+
return segments[0].slice(0, -3);
|
|
77
|
+
}
|
|
78
|
+
/** Adapts any read-only MemoryProvider to Trellis's MCP provider registry. */
|
|
79
|
+
export class RuntimeMemoryProvider {
|
|
80
|
+
source;
|
|
81
|
+
id = "memory";
|
|
82
|
+
constructor(source = new CanonicalMemoryProvider()) {
|
|
83
|
+
this.source = source;
|
|
84
|
+
}
|
|
85
|
+
listTools() {
|
|
86
|
+
return [SEARCH_TOOL, READ_TOOL];
|
|
87
|
+
}
|
|
88
|
+
async callTool(name, args, context) {
|
|
89
|
+
const input = (args ?? {});
|
|
90
|
+
if (name === SEARCH_TOOL.name)
|
|
91
|
+
return this.search(context, input);
|
|
92
|
+
if (name === READ_TOOL.name)
|
|
93
|
+
return this.readTool(context, input);
|
|
94
|
+
return errorResult(`unknown memory provider tool "${name}"`);
|
|
95
|
+
}
|
|
96
|
+
async listResources(context) {
|
|
97
|
+
const refs = await this.inScopeRefs(context);
|
|
98
|
+
return refs.map((ref) => ({
|
|
99
|
+
uri: memoryUri(ref.name),
|
|
100
|
+
name: `${ref.name}.md`,
|
|
101
|
+
description: `Canonical memory ${ref.name}`,
|
|
102
|
+
mimeType: "text/markdown",
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
async readResource(uri, context) {
|
|
106
|
+
const name = memoryNameFromUri(uri);
|
|
107
|
+
const document = await this.readDocument(name, context);
|
|
108
|
+
if (!document)
|
|
109
|
+
throw new Error(`memory "${name}" is not available to ${context.agentId}`);
|
|
110
|
+
return { contents: [{ uri: uri.toString(), mimeType: "text/markdown", text: document.content }] };
|
|
111
|
+
}
|
|
112
|
+
async inScopeRefs(context) {
|
|
113
|
+
const canonical = loadCanonicalSource(context.homeDir);
|
|
114
|
+
const refs = await this.source.list(context);
|
|
115
|
+
return refs.filter((ref) => isInScope(context.agentId, ref.scope, canonical.managedAgents));
|
|
116
|
+
}
|
|
117
|
+
async readDocument(name, context) {
|
|
118
|
+
const ref = (await this.inScopeRefs(context)).find((candidate) => candidate.name === name);
|
|
119
|
+
if (!ref)
|
|
120
|
+
return undefined;
|
|
121
|
+
const document = await this.source.read(name, context);
|
|
122
|
+
if (!document)
|
|
123
|
+
return undefined;
|
|
124
|
+
return {
|
|
125
|
+
name,
|
|
126
|
+
content: document.content,
|
|
127
|
+
fingerprint: fingerprint(document.content),
|
|
128
|
+
uri: memoryUri(name),
|
|
129
|
+
scope: "in-scope",
|
|
130
|
+
source: this.source.id,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async search(context, input) {
|
|
134
|
+
const query = typeof input.query === "string" ? input.query.trim().toLowerCase() : "";
|
|
135
|
+
const limit = typeof input.limit === "number" ? Math.min(50, Math.max(1, Math.floor(input.limit))) : 20;
|
|
136
|
+
const hits = [];
|
|
137
|
+
for (const ref of await this.inScopeRefs(context)) {
|
|
138
|
+
let document;
|
|
139
|
+
try {
|
|
140
|
+
document = await this.readDocument(ref.name, context);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// One unreadable memory must not hide every other memory from search.
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!document)
|
|
147
|
+
continue;
|
|
148
|
+
if (query && !`${document.name}\n${document.content}`.toLowerCase().includes(query))
|
|
149
|
+
continue;
|
|
150
|
+
hits.push({
|
|
151
|
+
name: document.name,
|
|
152
|
+
fingerprint: document.fingerprint,
|
|
153
|
+
uri: document.uri,
|
|
154
|
+
scope: document.scope,
|
|
155
|
+
source: document.source,
|
|
156
|
+
});
|
|
157
|
+
if (hits.length >= limit)
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
return textResult({ memories: hits });
|
|
161
|
+
}
|
|
162
|
+
async readTool(context, input) {
|
|
163
|
+
if (typeof input.name !== "string" || input.name.length === 0)
|
|
164
|
+
return errorResult('"name" is required');
|
|
165
|
+
try {
|
|
166
|
+
const document = await this.readDocument(input.name, context);
|
|
167
|
+
if (!document)
|
|
168
|
+
return errorResult(`memory "${input.name}" is not available to ${context.agentId}`);
|
|
169
|
+
return textResult(document);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
return errorResult(`could not read memory "${input.name}": ${err instanceof Error ? err.message : String(err)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AgentId, CanonicalSource } from "../core/types.js";
|
|
2
|
+
import type { UpstreamStatus } from "./gatewayBackend.js";
|
|
3
|
+
export interface MemoryReadiness {
|
|
4
|
+
backendConfigured: boolean;
|
|
5
|
+
graphPath?: string;
|
|
6
|
+
graphExists: boolean;
|
|
7
|
+
graphReadable: boolean;
|
|
8
|
+
graphWritable: boolean;
|
|
9
|
+
graphReady: boolean;
|
|
10
|
+
canonicalCount: number;
|
|
11
|
+
deliveredAgents: AgentId[];
|
|
12
|
+
writePath: "mcp" | "unavailable";
|
|
13
|
+
extractionCommand: "trellis memory extract";
|
|
14
|
+
nativeImportSupported: false;
|
|
15
|
+
upstream?: Pick<UpstreamStatus, "status" | "detail" | "remediation">;
|
|
16
|
+
}
|
|
17
|
+
export declare function inspectMemoryReadiness(canonical: CanonicalSource, homeDir: string, upstream?: readonly UpstreamStatus[]): MemoryReadiness;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { accessSync, constants, existsSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { isInScope } from "../core/adapter.js";
|
|
4
|
+
import { isGatewayAgent } from "../adapters/mcpPlan.js";
|
|
5
|
+
function routeFor(agent, canonical) {
|
|
6
|
+
const explicit = canonical.mcp.routes?.[agent];
|
|
7
|
+
if (explicit)
|
|
8
|
+
return explicit.mode;
|
|
9
|
+
if (isGatewayAgent(agent, canonical.mcp, canonical.managedAgents))
|
|
10
|
+
return "gateway";
|
|
11
|
+
if (canonical.mcp.hub)
|
|
12
|
+
return "hub";
|
|
13
|
+
return "direct";
|
|
14
|
+
}
|
|
15
|
+
function memoryDeliveredTo(agent, canonical) {
|
|
16
|
+
const server = canonical.mcp.servers.memory;
|
|
17
|
+
if (!server || server.enabled === false || !isInScope(agent, server.agents, canonical.managedAgents))
|
|
18
|
+
return false;
|
|
19
|
+
const route = canonical.mcp.routes?.[agent];
|
|
20
|
+
const mode = routeFor(agent, canonical);
|
|
21
|
+
if (mode === "hub")
|
|
22
|
+
return false;
|
|
23
|
+
if (mode === "gateway" && route?.servers && !route.servers.includes("memory"))
|
|
24
|
+
return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function displayPath(raw, homeDir) {
|
|
28
|
+
if (raw === "~" || raw.startsWith("~/"))
|
|
29
|
+
return raw;
|
|
30
|
+
if (raw === homeDir || raw.startsWith(`${homeDir}/`))
|
|
31
|
+
return `~${raw.slice(homeDir.length)}`;
|
|
32
|
+
return "custom-path";
|
|
33
|
+
}
|
|
34
|
+
function fileState(path) {
|
|
35
|
+
const graphExists = existsSync(path);
|
|
36
|
+
const graphReadable = graphExists && (() => {
|
|
37
|
+
try {
|
|
38
|
+
accessSync(path, constants.R_OK);
|
|
39
|
+
return statSync(path).isFile();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
const graphWritable = (() => {
|
|
46
|
+
try {
|
|
47
|
+
let parent = graphExists ? path : dirname(path);
|
|
48
|
+
while (!existsSync(parent)) {
|
|
49
|
+
const next = dirname(parent);
|
|
50
|
+
if (next === parent)
|
|
51
|
+
return false;
|
|
52
|
+
parent = next;
|
|
53
|
+
}
|
|
54
|
+
accessSync(parent, constants.W_OK);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
return { graphExists, graphReadable, graphWritable, graphReady: graphWritable };
|
|
62
|
+
}
|
|
63
|
+
export function inspectMemoryReadiness(canonical, homeDir, upstream = []) {
|
|
64
|
+
const server = canonical.mcp.servers.memory;
|
|
65
|
+
const rawPath = server?.staticEnv?.MEMORY_FILE_PATH;
|
|
66
|
+
const backendConfigured = Boolean(server && server.enabled !== false && rawPath);
|
|
67
|
+
const graph = rawPath ? fileState(rawPath.replace(/^~(?=$|\/)/, homeDir)) : {
|
|
68
|
+
graphExists: false,
|
|
69
|
+
graphReadable: false,
|
|
70
|
+
graphWritable: false,
|
|
71
|
+
graphReady: false,
|
|
72
|
+
};
|
|
73
|
+
const memoryUpstream = upstream.find((status) => status.name === "memory");
|
|
74
|
+
const deliveredAgents = backendConfigured
|
|
75
|
+
? canonical.managedAgents.filter((agent) => memoryDeliveredTo(agent, canonical))
|
|
76
|
+
: [];
|
|
77
|
+
return {
|
|
78
|
+
backendConfigured,
|
|
79
|
+
...(rawPath ? { graphPath: displayPath(rawPath, homeDir) } : {}),
|
|
80
|
+
...graph,
|
|
81
|
+
canonicalCount: canonical.memories.length,
|
|
82
|
+
deliveredAgents,
|
|
83
|
+
writePath: backendConfigured ? "mcp" : "unavailable",
|
|
84
|
+
extractionCommand: "trellis memory extract",
|
|
85
|
+
nativeImportSupported: false,
|
|
86
|
+
...(memoryUpstream ? { upstream: { status: memoryUpstream.status, ...(memoryUpstream.detail ? { detail: memoryUpstream.detail } : {}), ...(memoryUpstream.remediation ? { remediation: memoryUpstream.remediation } : {}) } } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AgentId } from "../core/types.js";
|
|
2
|
+
export declare const MAX_NATIVE_MEMORY_BYTES: number;
|
|
3
|
+
export type NativeMemoryStatus = "supported" | "empty" | "unsupported";
|
|
4
|
+
export interface NativeMemoryCandidate {
|
|
5
|
+
sourceAgent: AgentId;
|
|
6
|
+
sourceId: string;
|
|
7
|
+
sourcePath: string;
|
|
8
|
+
displayName: string;
|
|
9
|
+
targetName: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
export interface NativeMemoryDiscovery {
|
|
13
|
+
adapter: string;
|
|
14
|
+
status: NativeMemoryStatus;
|
|
15
|
+
detail: string;
|
|
16
|
+
candidates: NativeMemoryCandidate[];
|
|
17
|
+
}
|
|
18
|
+
export declare function discoverNativeMemory(agent: AgentId, homeDir?: string, workspaceDir?: string, env?: NodeJS.ProcessEnv): NativeMemoryDiscovery;
|
|
19
|
+
export declare function renderNativeMemory(candidate: Pick<NativeMemoryCandidate, "sourceAgent" | "sourceId" | "content">): string;
|