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,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;
@@ -0,0 +1,129 @@
1
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { basename, join, relative, resolve, sep } from "node:path";
4
+ import { slugify } from "./slug.js";
5
+ export const MAX_NATIVE_MEMORY_BYTES = 256 * 1024;
6
+ function claudeProjectKey(workspaceDir) {
7
+ const absolute = resolve(workspaceDir);
8
+ const normalized = absolute
9
+ .replace(/^[\\/]+/, "")
10
+ .replace(/[\\/]+/g, "-")
11
+ .replace(/[^A-Za-z0-9._-]/g, "-");
12
+ return `-${normalized}`;
13
+ }
14
+ function safeMarkdownFiles(root) {
15
+ if (!existsSync(root))
16
+ return [];
17
+ let realRoot;
18
+ try {
19
+ realRoot = realpathSync(root);
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ return readdirSync(root)
25
+ .filter((name) => name.endsWith(".md"))
26
+ .map((name) => join(root, name))
27
+ .filter((file) => {
28
+ try {
29
+ const realFile = realpathSync(file);
30
+ const rel = relative(realRoot, realFile);
31
+ return !rel.startsWith(`..${sep}`) && rel !== ".." && statSync(realFile).isFile();
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ })
37
+ .sort();
38
+ }
39
+ export function discoverNativeMemory(agent, homeDir = homedir(), workspaceDir = process.cwd(), env = process.env) {
40
+ if (agent === "codex") {
41
+ const codexHome = env.CODEX_HOME ? resolve(env.CODEX_HOME) : join(homeDir, ".codex");
42
+ const memoryRoot = join(codexHome, "memories");
43
+ const files = safeMarkdownFiles(memoryRoot);
44
+ if (files.length === 0) {
45
+ return {
46
+ adapter: "codex-local-memory",
47
+ status: "empty",
48
+ detail: `no Markdown memory files found under ${memoryRoot}; session indexes and non-Markdown state were not inspected`,
49
+ candidates: [],
50
+ };
51
+ }
52
+ const candidates = [];
53
+ for (const file of files) {
54
+ const stat = statSync(file);
55
+ if (stat.size > MAX_NATIVE_MEMORY_BYTES)
56
+ continue;
57
+ const content = readFileSync(file, "utf8");
58
+ const displayName = basename(file, ".md");
59
+ const targetName = `codex-${slugify(displayName)}`;
60
+ if (!targetName || targetName === "codex-")
61
+ continue;
62
+ candidates.push({
63
+ sourceAgent: agent,
64
+ sourceId: `codex/${displayName}`,
65
+ sourcePath: file,
66
+ displayName,
67
+ targetName,
68
+ content,
69
+ });
70
+ }
71
+ return {
72
+ adapter: "codex-local-memory",
73
+ status: candidates.length > 0 ? "supported" : "empty",
74
+ detail: candidates.length > 0
75
+ ? `found ${candidates.length} bounded Markdown memory file(s) under ${memoryRoot}`
76
+ : "Codex memory files were empty, oversized, or unsafe to read",
77
+ candidates,
78
+ };
79
+ }
80
+ if (agent !== "claude-code") {
81
+ return {
82
+ adapter: "none",
83
+ status: "unsupported",
84
+ detail: `${agent} native memory is unsupported: sessions or private stores do not have a supported Trellis adapter; no session, cache, SQLite, or credential data was read`,
85
+ candidates: [],
86
+ };
87
+ }
88
+ const projectKey = claudeProjectKey(workspaceDir);
89
+ const memoryRoot = join(homeDir, ".claude", "projects", projectKey, "memory");
90
+ const files = safeMarkdownFiles(memoryRoot);
91
+ if (files.length === 0) {
92
+ return {
93
+ adapter: "claude-code-project-memory",
94
+ status: "empty",
95
+ detail: `no Markdown memory files found for the exact current workspace project key "${projectKey}"`,
96
+ candidates: [],
97
+ };
98
+ }
99
+ const candidates = [];
100
+ for (const file of files) {
101
+ const stat = statSync(file);
102
+ if (stat.size > MAX_NATIVE_MEMORY_BYTES)
103
+ continue;
104
+ const content = readFileSync(file, "utf8");
105
+ const displayName = basename(file, ".md");
106
+ const targetName = `claude-${slugify(projectKey)}-${slugify(displayName)}`;
107
+ if (!targetName || targetName === "claude-")
108
+ continue;
109
+ candidates.push({
110
+ sourceAgent: agent,
111
+ sourceId: `claude-code/${projectKey}/${displayName}`,
112
+ sourcePath: file,
113
+ displayName,
114
+ targetName,
115
+ content,
116
+ });
117
+ }
118
+ return {
119
+ adapter: "claude-code-project-memory",
120
+ status: candidates.length > 0 ? "supported" : "empty",
121
+ detail: candidates.length > 0
122
+ ? `found ${candidates.length} bounded Markdown memory file(s) for the exact current workspace project key "${projectKey}"`
123
+ : "memory files were empty, oversized, or unsafe to read",
124
+ candidates,
125
+ };
126
+ }
127
+ export function renderNativeMemory(candidate) {
128
+ return `<!-- Trellis import: ${candidate.sourceAgent} / ${candidate.sourceId} -->\n\n${candidate.content.replace(/^\s+|\s+$/g, "")}\n`;
129
+ }
@@ -0,0 +1,16 @@
1
+ import type { CallToolResult, ReadResourceResult, Resource, 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 RuntimeControlProvider implements TrellisProvider {
5
+ private readonly backend;
6
+ private readonly trellisVersion;
7
+ readonly id = "runtime";
8
+ constructor(backend: GatewayBackend, trellisVersion?: string);
9
+ listTools(): Tool[];
10
+ callTool(name: string, _args: unknown, context: RuntimeContext): Promise<CallToolResult>;
11
+ listResources(): Resource[];
12
+ readResource(uri: URL, context: RuntimeContext): ReadResourceResult;
13
+ private agents;
14
+ private status;
15
+ private instructions;
16
+ }
@@ -0,0 +1,168 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
3
+ import { join, relative, sep } from "node:path";
4
+ import { ALL_AGENTS, capabilityDeliveryForAgent } from "../core/types.js";
5
+ import { loadCanonicalSource } from "../core/canonical.js";
6
+ import { isGatewayAgent } from "../adapters/mcpPlan.js";
7
+ import * as claudeCodeProbe from "../probes/claude-code.js";
8
+ import * as codexProbe from "../probes/codex.js";
9
+ import * as kiroProbe from "../probes/kiro.js";
10
+ import * as piProbe from "../probes/pi.js";
11
+ import * as kimiCodeProbe from "../probes/kimi-code.js";
12
+ import { TRELLIS_VERSION } from "./cliMetadata.js";
13
+ import { inspectMemoryReadiness } from "./memoryStatus.js";
14
+ const MAX_INSTRUCTIONS_BYTES = 256 * 1024;
15
+ const INSTRUCTIONS_URI = "trellis://instructions/agents.md";
16
+ const STATUS_TOOL = {
17
+ name: "trellis.runtime.status",
18
+ description: "Describe the current Agent's Trellis Runtime, managed boundary, providers, and safe MCP health summary.",
19
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
20
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
21
+ };
22
+ const AGENTS_TOOL = {
23
+ name: "trellis.agents.list",
24
+ description: "List installed and Trellis-managed Agents with high-level capability health; never returns credentials or private sessions.",
25
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
26
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
27
+ };
28
+ const INSTRUCTIONS_TOOL = {
29
+ name: "trellis.instructions.read",
30
+ description: "Read the canonical global Agent instructions as bounded Markdown.",
31
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
32
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
33
+ };
34
+ const PROBES = {
35
+ "claude-code": (homeDir) => claudeCodeProbe.probe(homeDir),
36
+ codex: (homeDir) => codexProbe.probe(homeDir),
37
+ kiro: (homeDir) => kiroProbe.probe(homeDir),
38
+ pi: (homeDir) => piProbe.probe(homeDir),
39
+ "kimi-code": (homeDir) => kimiCodeProbe.probe(homeDir),
40
+ };
41
+ function textResult(value) {
42
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], structuredContent: value };
43
+ }
44
+ function errorResult(message) {
45
+ return { content: [{ type: "text", text: message }], isError: true };
46
+ }
47
+ function routeFor(agent, canonical) {
48
+ const explicit = canonical.mcp.routes?.[agent];
49
+ if (explicit)
50
+ return explicit.mode;
51
+ if (isGatewayAgent(agent, canonical.mcp, canonical.managedAgents))
52
+ return "gateway";
53
+ if (canonical.mcp.hub)
54
+ return "hub";
55
+ return "direct";
56
+ }
57
+ function healthFor(snapshot, managed) {
58
+ if (!snapshot.present)
59
+ return "absent";
60
+ if (!managed)
61
+ return "unmanaged";
62
+ if (snapshot.diagnostics.length > 0)
63
+ return "degraded";
64
+ return "healthy";
65
+ }
66
+ function snapshotSummary(snapshot, canonical) {
67
+ const managed = canonical.managedAgents.includes(snapshot.agent);
68
+ return {
69
+ id: snapshot.agent,
70
+ present: snapshot.present,
71
+ managed,
72
+ health: healthFor(snapshot, managed),
73
+ ...(snapshot.version ? { version: snapshot.version } : {}),
74
+ runtimeDelivery: capabilityDeliveryForAgent(snapshot.agent, canonical.mcp),
75
+ mcpRoute: routeFor(snapshot.agent, canonical),
76
+ skillCount: snapshot.skillRoots.reduce((count, root) => count + root.skills.length, 0),
77
+ mcpServerCount: snapshot.mcpServers.length,
78
+ instructions: { present: snapshot.instructionsFile !== undefined, native: true },
79
+ diagnostics: snapshot.diagnostics,
80
+ };
81
+ }
82
+ async function probeAgents(homeDir) {
83
+ const results = await Promise.allSettled(ALL_AGENTS.map((agent) => PROBES[agent](homeDir)));
84
+ return results.map((result, index) => result.status === "fulfilled"
85
+ ? result.value
86
+ : { agent: ALL_AGENTS[index], present: false, skillRoots: [], mcpServers: [], diagnostics: [`probe failed: ${result.reason instanceof Error ? result.reason.message : String(result.reason)}`] });
87
+ }
88
+ function safeInstructions(homeDir, file) {
89
+ const canonicalRoot = realpathSync(join(homeDir, ".trellis"));
90
+ const realFile = realpathSync(file);
91
+ const rel = relative(canonicalRoot, realFile);
92
+ if (rel.startsWith(`..${sep}`) || rel === "..")
93
+ throw new Error("canonical instructions file is outside ~/.trellis");
94
+ const stat = statSync(realFile);
95
+ if (!stat.isFile() || stat.size > MAX_INSTRUCTIONS_BYTES)
96
+ throw new Error(`instructions exceed the ${MAX_INSTRUCTIONS_BYTES}-byte provider limit`);
97
+ return readFileSync(realFile, "utf8");
98
+ }
99
+ export class RuntimeControlProvider {
100
+ backend;
101
+ trellisVersion;
102
+ id = "runtime";
103
+ constructor(backend, trellisVersion = TRELLIS_VERSION) {
104
+ this.backend = backend;
105
+ this.trellisVersion = trellisVersion;
106
+ }
107
+ listTools() {
108
+ return [STATUS_TOOL, AGENTS_TOOL, INSTRUCTIONS_TOOL];
109
+ }
110
+ async callTool(name, _args, context) {
111
+ if (name === STATUS_TOOL.name)
112
+ return textResult(await this.status(context));
113
+ if (name === AGENTS_TOOL.name)
114
+ return textResult(await this.agents(context));
115
+ if (name === INSTRUCTIONS_TOOL.name) {
116
+ try {
117
+ return textResult(this.instructions(context));
118
+ }
119
+ catch (err) {
120
+ return errorResult(`could not read canonical instructions: ${err instanceof Error ? err.message : String(err)}`);
121
+ }
122
+ }
123
+ return errorResult(`unknown control-plane tool "${name}"`);
124
+ }
125
+ listResources() {
126
+ return [{ uri: INSTRUCTIONS_URI, name: "agents.md", description: "Canonical global Agent instructions", mimeType: "text/markdown" }];
127
+ }
128
+ readResource(uri, context) {
129
+ if (uri.toString() !== INSTRUCTIONS_URI)
130
+ throw new Error(`unsupported instructions resource URI "${uri.toString()}"`);
131
+ const value = this.instructions(context);
132
+ return { contents: [{ uri: INSTRUCTIONS_URI, mimeType: "text/markdown", text: value.content }] };
133
+ }
134
+ async agents(context) {
135
+ const canonical = loadCanonicalSource(context.homeDir);
136
+ const snapshots = await probeAgents(context.homeDir);
137
+ return { agents: snapshots.map((snapshot) => snapshotSummary(snapshot, canonical)), source: "trellis" };
138
+ }
139
+ async status(context) {
140
+ const canonical = loadCanonicalSource(context.homeDir);
141
+ const snapshots = await probeAgents(context.homeDir);
142
+ const current = snapshots.find((snapshot) => snapshot.agent === context.agentId);
143
+ const currentSummary = current ? snapshotSummary(current, canonical) : { id: context.agentId, present: false };
144
+ const route = routeFor(context.agentId, canonical);
145
+ const upstream = this.backend.listStatus?.() ?? [];
146
+ const unhealthy = upstream.filter((status) => status.status !== "ready");
147
+ const memory = inspectMemoryReadiness(canonical, context.homeDir, upstream);
148
+ return {
149
+ agent: currentSummary,
150
+ runtime: { trellisVersion: this.trellisVersion, edge: "gateway", route },
151
+ providers: {
152
+ skills: { available: true, canonicalCount: canonical.skills.length },
153
+ memory: { available: true, ...memory },
154
+ instructions: { available: existsSync(canonical.instructionsFile), native: true, runtimeReadable: true },
155
+ mcp: { available: true, upstreamCount: upstream.length, unhealthyCount: unhealthy.length },
156
+ tasks: { available: true, mutationRequiresConfirmation: true, launchesAgents: false },
157
+ },
158
+ upstream,
159
+ agents: snapshots.map((snapshot) => snapshotSummary(snapshot, canonical)),
160
+ source: "trellis",
161
+ };
162
+ }
163
+ instructions(context) {
164
+ const canonical = loadCanonicalSource(context.homeDir);
165
+ const content = safeInstructions(context.homeDir, canonical.instructionsFile);
166
+ return { name: "agents.md", content, fingerprint: createHash("sha256").update(content).digest("hex"), source: "canonical" };
167
+ }
168
+ }
@@ -9,6 +9,7 @@
9
9
  * this narrow need requires (design.md D4).
10
10
  */
11
11
  import type { SecretsPolicy } from "../core/types.js";
12
+ import type { BackupSession } from "./backup.js";
12
13
  export declare function parseDotenv(content: string): Record<string, string>;
13
14
  /**
14
15
  * `policy.envFile` set: it is the SOLE source — a name absent from it
@@ -17,3 +18,19 @@ export declare function parseDotenv(content: string): Record<string, string>;
17
18
  * reads `process.env` directly, identical to every pre-existing caller.
18
19
  */
19
20
  export declare function resolveSecretEnv(names: string[], policy: SecretsPolicy): Record<string, string | undefined>;
21
+ export type WriteLocalSecretOutcome = "created" | "already-present" | "conflict";
22
+ /**
23
+ * `trellis migrate`'s static-env secret extraction
24
+ * (trellis-migrate-extract-static-env-secrets design.md D4) — appends
25
+ * one `NAME=value` line to a dotenv-format file in exactly the shape
26
+ * `parseDotenv` above already reads, creating the file (and its parent
27
+ * directory) if neither exists yet. Idempotent, the same three-way
28
+ * split every other Trellis writer uses: the name is absent (create),
29
+ * already present with the identical value (no-op — re-running migrate
30
+ * must not duplicate the line), or already present with a *different*
31
+ * value (conflict, left untouched — the file may hold a value the user
32
+ * already rotated by hand since the last run; silently overwriting it
33
+ * would be exactly the kind of value-clobbering this whole feature
34
+ * exists to prevent, just relocated to a new file).
35
+ */
36
+ export declare function writeLocalSecretValue(path: string, name: string, value: string, backup?: BackupSession): WriteLocalSecretOutcome;