agent-trellis 0.6.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +97 -38
  2. package/dist/adapters/claude-code.js +3 -2
  3. package/dist/adapters/codex.js +3 -2
  4. package/dist/adapters/jsonMcp.d.ts +3 -1
  5. package/dist/adapters/jsonMcp.js +18 -4
  6. package/dist/adapters/kimi-code.d.ts +21 -0
  7. package/dist/adapters/kimi-code.js +107 -0
  8. package/dist/adapters/kiro.js +3 -2
  9. package/dist/adapters/mcpPlan.d.ts +26 -2
  10. package/dist/adapters/mcpPlan.js +135 -15
  11. package/dist/adapters/pi.js +22 -3
  12. package/dist/adapters/symlinkPlan.d.ts +7 -0
  13. package/dist/adapters/symlinkPlan.js +16 -7
  14. package/dist/cli.js +129 -20
  15. package/dist/commands/doctor.d.ts +7 -2
  16. package/dist/commands/doctor.js +38 -2
  17. package/dist/commands/init.d.ts +4 -2
  18. package/dist/commands/init.js +26 -13
  19. package/dist/commands/kimi.d.ts +16 -0
  20. package/dist/commands/kimi.js +64 -0
  21. package/dist/commands/manage.d.ts +39 -0
  22. package/dist/commands/manage.js +126 -0
  23. package/dist/commands/mcp.d.ts +22 -1
  24. package/dist/commands/mcp.js +65 -2
  25. package/dist/commands/mcpGateway.d.ts +6 -0
  26. package/dist/commands/mcpGateway.js +34 -14
  27. package/dist/commands/mcpImport.d.ts +27 -0
  28. package/dist/commands/mcpImport.js +336 -0
  29. package/dist/commands/memory.d.ts +5 -3
  30. package/dist/commands/memory.js +12 -8
  31. package/dist/commands/migrate.d.ts +37 -7
  32. package/dist/commands/migrate.js +253 -27
  33. package/dist/commands/onboard.d.ts +74 -2
  34. package/dist/commands/onboard.js +463 -72
  35. package/dist/commands/onboardVerdict.js +20 -7
  36. package/dist/commands/rollback.js +4 -1
  37. package/dist/commands/secretsAudit.d.ts +21 -12
  38. package/dist/commands/secretsAudit.js +39 -10
  39. package/dist/commands/skill.d.ts +17 -0
  40. package/dist/commands/skill.js +47 -2
  41. package/dist/commands/sync.js +2 -0
  42. package/dist/core/canonical.d.ts +58 -4
  43. package/dist/core/canonical.js +183 -11
  44. package/dist/core/capabilitySelection.d.ts +45 -0
  45. package/dist/core/capabilitySelection.js +124 -0
  46. package/dist/core/types.d.ts +25 -2
  47. package/dist/core/types.js +7 -0
  48. package/dist/lib/backup.d.ts +2 -0
  49. package/dist/lib/backup.js +4 -0
  50. package/dist/lib/builtinSkills.d.ts +2 -0
  51. package/dist/lib/builtinSkills.js +4 -0
  52. package/dist/lib/cliMetadata.d.ts +1 -0
  53. package/dist/lib/cliMetadata.js +14 -0
  54. package/dist/lib/gatewayBackend.d.ts +28 -0
  55. package/dist/lib/gatewayBackend.js +72 -1
  56. package/dist/lib/installAgent.d.ts +7 -5
  57. package/dist/lib/installAgent.js +4 -3
  58. package/dist/lib/mcpConnect.js +5 -1
  59. package/dist/lib/mcpMigrateRead.d.ts +1 -0
  60. package/dist/lib/mcpMigrateRead.js +3 -0
  61. package/dist/lib/mcpRuntime.d.ts +46 -0
  62. package/dist/lib/mcpRuntime.js +185 -0
  63. package/dist/lib/mcpStatusProvider.d.ts +10 -0
  64. package/dist/lib/mcpStatusProvider.js +25 -0
  65. package/dist/lib/mcpToolRegistry.d.ts +18 -0
  66. package/dist/lib/mcpToolRegistry.js +107 -14
  67. package/dist/lib/memoryGraph.d.ts +1 -1
  68. package/dist/lib/memoryGraph.js +9 -3
  69. package/dist/lib/memoryProvider.d.ts +58 -0
  70. package/dist/lib/memoryProvider.js +175 -0
  71. package/dist/lib/memoryStatus.d.ts +17 -0
  72. package/dist/lib/memoryStatus.js +88 -0
  73. package/dist/lib/nativeMemory.d.ts +19 -0
  74. package/dist/lib/nativeMemory.js +129 -0
  75. package/dist/lib/runtimeControlProvider.d.ts +16 -0
  76. package/dist/lib/runtimeControlProvider.js +168 -0
  77. package/dist/lib/secretEnv.d.ts +17 -0
  78. package/dist/lib/secretEnv.js +31 -1
  79. package/dist/lib/skillProvider.d.ts +13 -0
  80. package/dist/lib/skillProvider.js +210 -0
  81. package/dist/lib/taskProvider.d.ts +7 -0
  82. package/dist/lib/taskProvider.js +43 -0
  83. package/dist/lib/taskStore.d.ts +46 -0
  84. package/dist/lib/taskStore.js +156 -0
  85. package/dist/lib/terminalPicker.d.ts +23 -28
  86. package/dist/lib/terminalPicker.js +54 -164
  87. package/dist/pi-bridge/bundle.js +493 -51
  88. package/dist/pi-bridge/index.js +42 -21
  89. package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
  90. package/dist/pi-bridge/schemaTranslate.js +4 -8
  91. package/dist/probes/kimi-code.d.ts +12 -0
  92. package/dist/probes/kimi-code.js +55 -0
  93. package/dist/sdk.d.ts +1 -1
  94. package/docs/architecture.md +224 -26
  95. package/docs/assets/agent-trellis-architecture-horizontal.svg +80 -0
  96. package/docs/assets/agent-trellis-architecture-vertical.svg +82 -0
  97. package/docs/assets/trellis-mark.svg +12 -0
  98. package/docs/getting-started.md +403 -35
  99. package/docs/overview.md +131 -0
  100. package/docs/overview.zh-CN.md +100 -0
  101. package/docs/research.md +59 -0
  102. package/docs/roadmap.md +10 -5
  103. package/package.json +5 -3
  104. package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
  105. package/schema/capability-selection.example.yaml +23 -0
  106. package/schema/scope.example.yaml +1 -1
  107. package/schema/servers.example.yaml +15 -1
@@ -0,0 +1,185 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3
+ import { allocateExposedToolNames } from "./mcpToolRegistry.js";
4
+ function compactBuiltinToolName(name) {
5
+ return name.startsWith("trellis.") ? name.slice("trellis.".length).replaceAll(".", "_") : name;
6
+ }
7
+ function providerError(provider, error) {
8
+ return `trellis-mcp-runtime: provider "${provider}" failed: ${error instanceof Error ? error.message : String(error)}`;
9
+ }
10
+ export class BuiltinRegistry {
11
+ providers;
12
+ warn;
13
+ toolOwners = new Map();
14
+ originalToolOwners = new Map();
15
+ exposedToOriginal = new Map();
16
+ resourceOwners = new Map();
17
+ promptOwners = new Map();
18
+ constructor(providers, warn = console.error) {
19
+ this.providers = providers;
20
+ this.warn = warn;
21
+ }
22
+ async listTools(context) {
23
+ const candidates = [];
24
+ this.toolOwners.clear();
25
+ this.originalToolOwners.clear();
26
+ this.exposedToOriginal.clear();
27
+ for (const provider of this.providers) {
28
+ if (!provider.listTools)
29
+ continue;
30
+ try {
31
+ for (const tool of await provider.listTools(context)) {
32
+ candidates.push({ provider, tool });
33
+ }
34
+ }
35
+ catch (err) {
36
+ this.warn(providerError(provider.id, err));
37
+ }
38
+ }
39
+ const exposedNames = allocateExposedToolNames(candidates.map(({ provider, tool }) => ({
40
+ serverName: provider.id,
41
+ toolName: provider.id === "upstream" ? tool.name : compactBuiltinToolName(tool.name),
42
+ })));
43
+ return candidates.map(({ provider, tool }, index) => {
44
+ const exposedName = exposedNames[index];
45
+ this.toolOwners.set(exposedName, provider);
46
+ this.exposedToOriginal.set(exposedName, tool.name);
47
+ if (!this.originalToolOwners.has(tool.name))
48
+ this.originalToolOwners.set(tool.name, provider);
49
+ const title = exposedName === tool.name || tool.title !== undefined
50
+ ? tool.title
51
+ : `${provider.id} / ${tool.name}`;
52
+ return { ...tool, name: exposedName, ...(title === undefined ? {} : { title }) };
53
+ });
54
+ }
55
+ async callTool(name, args, context) {
56
+ let provider = this.toolOwners.get(name);
57
+ let originalName = this.exposedToOriginal.get(name) ?? name;
58
+ if (!provider) {
59
+ await this.listTools(context);
60
+ provider = this.toolOwners.get(name);
61
+ originalName = this.exposedToOriginal.get(name) ?? name;
62
+ // Keep direct programmatic calls using the logical pre-normalization
63
+ // name working, while never advertising an invalid alias to an Agent.
64
+ if (!provider)
65
+ provider = this.originalToolOwners.get(name);
66
+ }
67
+ if (!provider)
68
+ return { content: [{ type: "text", text: `unknown Trellis tool "${name}"` }], isError: true };
69
+ try {
70
+ return await provider.callTool(originalName, args, context);
71
+ }
72
+ catch (err) {
73
+ return { content: [{ type: "text", text: providerError(provider.id, err) }], isError: true };
74
+ }
75
+ }
76
+ async listResources(context) {
77
+ const resources = [];
78
+ this.resourceOwners.clear();
79
+ for (const provider of this.providers) {
80
+ if (!provider.listResources)
81
+ continue;
82
+ try {
83
+ for (const resource of await provider.listResources(context)) {
84
+ if (this.resourceOwners.has(resource.uri)) {
85
+ this.warn(`trellis-mcp-runtime: duplicate resource "${resource.uri}" from provider "${provider.id}"`);
86
+ continue;
87
+ }
88
+ this.resourceOwners.set(resource.uri, provider);
89
+ resources.push(resource);
90
+ }
91
+ }
92
+ catch (err) {
93
+ this.warn(providerError(provider.id, err));
94
+ }
95
+ }
96
+ return resources;
97
+ }
98
+ async readResource(uri, context) {
99
+ const key = uri.toString();
100
+ let provider = this.resourceOwners.get(key);
101
+ if (!provider) {
102
+ await this.listResources(context);
103
+ provider = this.resourceOwners.get(key);
104
+ }
105
+ if (!provider?.readResource)
106
+ throw new Error(`unknown Trellis resource "${key}"`);
107
+ return provider.readResource(uri, context);
108
+ }
109
+ async listPrompts(context) {
110
+ const prompts = [];
111
+ this.promptOwners.clear();
112
+ for (const provider of this.providers) {
113
+ if (!provider.listPrompts)
114
+ continue;
115
+ try {
116
+ for (const prompt of await provider.listPrompts(context)) {
117
+ if (this.promptOwners.has(prompt.name)) {
118
+ this.warn(`trellis-mcp-runtime: duplicate prompt "${prompt.name}" from provider "${provider.id}"`);
119
+ continue;
120
+ }
121
+ this.promptOwners.set(prompt.name, provider);
122
+ prompts.push(prompt);
123
+ }
124
+ }
125
+ catch (err) {
126
+ this.warn(providerError(provider.id, err));
127
+ }
128
+ }
129
+ return prompts;
130
+ }
131
+ async getPrompt(name, args, context) {
132
+ let provider = this.promptOwners.get(name);
133
+ if (!provider) {
134
+ await this.listPrompts(context);
135
+ provider = this.promptOwners.get(name);
136
+ }
137
+ if (!provider?.getPrompt)
138
+ throw new Error(`unknown Trellis prompt "${name}"`);
139
+ return provider.getPrompt(name, args, context);
140
+ }
141
+ async close() {
142
+ await Promise.all(this.providers.map(async (provider) => {
143
+ try {
144
+ await provider.close?.();
145
+ }
146
+ catch (err) {
147
+ this.warn(providerError(provider.id, err));
148
+ }
149
+ }));
150
+ }
151
+ }
152
+ /** Adapts the existing gateway tool backend without duplicating its routing
153
+ * or upstream connection logic. */
154
+ export class UpstreamProvider {
155
+ backend;
156
+ id = "upstream";
157
+ constructor(backend) {
158
+ this.backend = backend;
159
+ }
160
+ async listTools() {
161
+ return (await this.backend.listTools());
162
+ }
163
+ async callTool(name, args) {
164
+ return (await this.backend.callTool(name, args));
165
+ }
166
+ async close() {
167
+ await this.backend.close();
168
+ }
169
+ }
170
+ export function createRuntimeServer(serverInfo, context, registry) {
171
+ const server = new Server(serverInfo, {
172
+ capabilities: {
173
+ tools: { listChanged: true },
174
+ resources: { listChanged: true },
175
+ prompts: { listChanged: true },
176
+ },
177
+ });
178
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: await registry.listTools(context) }));
179
+ server.setRequestHandler(CallToolRequestSchema, async (request) => registry.callTool(request.params.name, request.params.arguments, context));
180
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: await registry.listResources(context) }));
181
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => registry.readResource(new URL(request.params.uri), context));
182
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: await registry.listPrompts(context) }));
183
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => registry.getPrompt(request.params.name, request.params.arguments, context));
184
+ return server;
185
+ }
@@ -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
- return `${serverName}${TOOL_NAME_SEPARATOR}${toolName}`;
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 added = [];
38
- const skipped = [];
39
- for (const tool of tools) {
40
- const name = prefixedToolName(serverName, tool.name);
41
- if (this.entries.has(name)) {
42
- skipped.push(name);
43
- continue;
44
- }
45
- this.entries.set(name, { serverName, toolName: tool.name, upstream, tool });
46
- this.order.push(name);
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 { added, skipped };
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
- return { ...entry.tool, name };
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
@@ -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 canonical.memories) {
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
+ }