agent-trellis 0.7.0 → 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 (104) 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 +6 -1
  10. package/dist/adapters/mcpPlan.js +106 -8
  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 +121 -21
  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 +16 -5
  32. package/dist/commands/migrate.js +90 -17
  33. package/dist/commands/onboard.d.ts +74 -2
  34. package/dist/commands/onboard.js +458 -71
  35. package/dist/commands/rollback.js +4 -1
  36. package/dist/commands/skill.d.ts +17 -0
  37. package/dist/commands/skill.js +47 -2
  38. package/dist/commands/sync.js +2 -0
  39. package/dist/core/canonical.d.ts +15 -7
  40. package/dist/core/canonical.js +101 -13
  41. package/dist/core/capabilitySelection.d.ts +45 -0
  42. package/dist/core/capabilitySelection.js +124 -0
  43. package/dist/core/types.d.ts +25 -2
  44. package/dist/core/types.js +7 -0
  45. package/dist/lib/backup.d.ts +2 -0
  46. package/dist/lib/backup.js +4 -0
  47. package/dist/lib/builtinSkills.d.ts +2 -0
  48. package/dist/lib/builtinSkills.js +4 -0
  49. package/dist/lib/cliMetadata.d.ts +1 -0
  50. package/dist/lib/cliMetadata.js +14 -0
  51. package/dist/lib/gatewayBackend.d.ts +28 -0
  52. package/dist/lib/gatewayBackend.js +72 -1
  53. package/dist/lib/installAgent.d.ts +7 -5
  54. package/dist/lib/installAgent.js +4 -3
  55. package/dist/lib/mcpConnect.js +5 -1
  56. package/dist/lib/mcpMigrateRead.d.ts +1 -0
  57. package/dist/lib/mcpMigrateRead.js +3 -0
  58. package/dist/lib/mcpRuntime.d.ts +46 -0
  59. package/dist/lib/mcpRuntime.js +185 -0
  60. package/dist/lib/mcpStatusProvider.d.ts +10 -0
  61. package/dist/lib/mcpStatusProvider.js +25 -0
  62. package/dist/lib/mcpToolRegistry.d.ts +18 -0
  63. package/dist/lib/mcpToolRegistry.js +107 -14
  64. package/dist/lib/memoryGraph.d.ts +1 -1
  65. package/dist/lib/memoryGraph.js +9 -3
  66. package/dist/lib/memoryProvider.d.ts +58 -0
  67. package/dist/lib/memoryProvider.js +175 -0
  68. package/dist/lib/memoryStatus.d.ts +17 -0
  69. package/dist/lib/memoryStatus.js +88 -0
  70. package/dist/lib/nativeMemory.d.ts +19 -0
  71. package/dist/lib/nativeMemory.js +129 -0
  72. package/dist/lib/runtimeControlProvider.d.ts +16 -0
  73. package/dist/lib/runtimeControlProvider.js +168 -0
  74. package/dist/lib/secretEnv.d.ts +2 -1
  75. package/dist/lib/secretEnv.js +6 -2
  76. package/dist/lib/skillProvider.d.ts +13 -0
  77. package/dist/lib/skillProvider.js +210 -0
  78. package/dist/lib/taskProvider.d.ts +7 -0
  79. package/dist/lib/taskProvider.js +43 -0
  80. package/dist/lib/taskStore.d.ts +46 -0
  81. package/dist/lib/taskStore.js +156 -0
  82. package/dist/lib/terminalPicker.d.ts +23 -49
  83. package/dist/lib/terminalPicker.js +53 -218
  84. package/dist/pi-bridge/bundle.js +475 -37
  85. package/dist/pi-bridge/index.js +42 -21
  86. package/dist/pi-bridge/schemaTranslate.d.ts +4 -8
  87. package/dist/pi-bridge/schemaTranslate.js +4 -8
  88. package/dist/probes/kimi-code.d.ts +12 -0
  89. package/dist/probes/kimi-code.js +55 -0
  90. package/dist/sdk.d.ts +1 -1
  91. package/docs/architecture.md +224 -26
  92. package/docs/assets/agent-trellis-architecture-horizontal.svg +80 -0
  93. package/docs/assets/agent-trellis-architecture-vertical.svg +82 -0
  94. package/docs/assets/trellis-mark.svg +12 -0
  95. package/docs/getting-started.md +328 -35
  96. package/docs/overview.md +131 -0
  97. package/docs/overview.zh-CN.md +100 -0
  98. package/docs/research.md +59 -0
  99. package/docs/roadmap.md +9 -5
  100. package/package.json +3 -2
  101. package/schema/builtin-skills/trellis-runtime/SKILL.md +117 -0
  102. package/schema/capability-selection.example.yaml +23 -0
  103. package/schema/scope.example.yaml +1 -1
  104. package/schema/servers.example.yaml +15 -1
@@ -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
@@ -32,4 +33,4 @@ export type WriteLocalSecretOutcome = "created" | "already-present" | "conflict"
32
33
  * would be exactly the kind of value-clobbering this whole feature
33
34
  * exists to prevent, just relocated to a new file).
34
35
  */
35
- export declare function writeLocalSecretValue(path: string, name: string, value: string): WriteLocalSecretOutcome;
36
+ export declare function writeLocalSecretValue(path: string, name: string, value: string, backup?: BackupSession): WriteLocalSecretOutcome;
@@ -59,7 +59,7 @@ export function resolveSecretEnv(names, policy) {
59
59
  * would be exactly the kind of value-clobbering this whole feature
60
60
  * exists to prevent, just relocated to a new file).
61
61
  */
62
- export function writeLocalSecretValue(path, name, value) {
62
+ export function writeLocalSecretValue(path, name, value, backup) {
63
63
  const existingContent = existsSync(path) ? readFileSync(path, "utf-8") : "";
64
64
  const existing = parseDotenv(existingContent);
65
65
  if (Object.hasOwn(existing, name)) {
@@ -67,6 +67,10 @@ export function writeLocalSecretValue(path, name, value) {
67
67
  }
68
68
  mkdirSync(dirname(path), { recursive: true });
69
69
  const separator = existingContent.length > 0 && !existingContent.endsWith("\n") ? "\n" : "";
70
- appendFileSync(path, `${separator}${name}=${value}\n`);
70
+ const next = `${existingContent}${separator}${name}=${value}\n`;
71
+ if (backup)
72
+ backup.writeFile(path, next);
73
+ else
74
+ appendFileSync(path, `${separator}${name}=${value}\n`);
71
75
  return "created";
72
76
  }
@@ -0,0 +1,13 @@
1
+ import type { Resource, ReadResourceResult, Tool, CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
3
+ export declare class SkillProvider implements TrellisProvider {
4
+ readonly id = "skills";
5
+ listTools(): Tool[];
6
+ callTool(name: string, args: unknown, context: RuntimeContext): Promise<CallToolResult>;
7
+ listResources(context: RuntimeContext): Resource[];
8
+ readResource(uri: URL, context: RuntimeContext): ReadResourceResult;
9
+ private search;
10
+ private readSkill;
11
+ private readFile;
12
+ private readBoundedFile;
13
+ }
@@ -0,0 +1,210 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
3
+ import { resolve, relative, sep } from "node:path";
4
+ import { parse as parseYaml } from "yaml";
5
+ import { isInScope } from "../core/adapter.js";
6
+ import { loadCanonicalSource } from "../core/canonical.js";
7
+ import { resolveScope } from "../core/types.js";
8
+ const MAX_FILE_BYTES = 256 * 1024;
9
+ const SKILL_RESOURCE_PREFIX = "trellis://skills/";
10
+ const SEARCH_TOOL = {
11
+ name: "trellis.skills.search",
12
+ description: "Search in-scope Trellis skills by name or description, then read one selected skill on demand.",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: { query: { type: "string" }, limit: { type: "number", minimum: 1, maximum: 50 } },
16
+ required: ["query"],
17
+ additionalProperties: false,
18
+ },
19
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
20
+ };
21
+ const READ_TOOL = {
22
+ name: "trellis.skills.read",
23
+ description: "Read the SKILL.md instructions for one in-scope Trellis skill.",
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: { name: { type: "string" } },
27
+ required: ["name"],
28
+ additionalProperties: false,
29
+ },
30
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
31
+ };
32
+ const READ_FILE_TOOL = {
33
+ name: "trellis.skills.read_file",
34
+ description: "Read one bounded, non-secret supporting file under an in-scope skill directory.",
35
+ inputSchema: {
36
+ type: "object",
37
+ properties: { name: { type: "string" }, relativePath: { type: "string" } },
38
+ required: ["name", "relativePath"],
39
+ additionalProperties: false,
40
+ },
41
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
42
+ };
43
+ function textResult(value) {
44
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], structuredContent: value };
45
+ }
46
+ function errorResult(message) {
47
+ return { content: [{ type: "text", text: message }], isError: true };
48
+ }
49
+ function frontmatter(content) {
50
+ if (!content.startsWith("---"))
51
+ return {};
52
+ const end = content.indexOf("\n---", 3);
53
+ if (end < 0)
54
+ return {};
55
+ try {
56
+ const parsed = parseYaml(content.slice(3, end));
57
+ return {
58
+ name: typeof parsed?.name === "string" ? parsed.name : undefined,
59
+ description: typeof parsed?.description === "string" ? parsed.description : undefined,
60
+ };
61
+ }
62
+ catch {
63
+ return {};
64
+ }
65
+ }
66
+ function fingerprint(content) {
67
+ return createHash("sha256").update(content).digest("hex");
68
+ }
69
+ function isSecretLikePath(path) {
70
+ const normalized = path.toLowerCase();
71
+ return normalized.includes(".env") || normalized.includes("secret") || normalized.endsWith("servers.local.env");
72
+ }
73
+ function skillFor(canonical, agentId, name) {
74
+ const skill = canonical.skills.find((candidate) => candidate.name === name);
75
+ if (!skill || !hasSkillDocument(skill) || !isInScope(agentId, skill.scope, canonical.managedAgents))
76
+ return undefined;
77
+ return skill;
78
+ }
79
+ function hasSkillDocument(skill) {
80
+ try {
81
+ return statSync(resolve(skill.dir, "SKILL.md")).isFile();
82
+ }
83
+ catch {
84
+ return false;
85
+ }
86
+ }
87
+ function skillUri(name) {
88
+ return `${SKILL_RESOURCE_PREFIX}${encodeURIComponent(name)}/SKILL.md`;
89
+ }
90
+ function skillMetadata(skill, canonical, agentId, content) {
91
+ const metadata = frontmatter(content);
92
+ return {
93
+ name: skill.name,
94
+ description: metadata.description ?? "",
95
+ fingerprint: fingerprint(content),
96
+ uri: skillUri(skill.name),
97
+ scope: resolveScope(skill.scope, canonical.managedAgents).includes(agentId) ? "in-scope" : "out-of-scope",
98
+ source: "canonical",
99
+ };
100
+ }
101
+ export class SkillProvider {
102
+ id = "skills";
103
+ listTools() {
104
+ return [SEARCH_TOOL, READ_TOOL, READ_FILE_TOOL];
105
+ }
106
+ async callTool(name, args, context) {
107
+ const canonical = loadCanonicalSource(context.homeDir);
108
+ const input = (args ?? {});
109
+ if (name === SEARCH_TOOL.name)
110
+ return this.search(canonical, context.agentId, input);
111
+ if (name === READ_TOOL.name)
112
+ return this.readSkill(canonical, context.agentId, input);
113
+ if (name === READ_FILE_TOOL.name)
114
+ return this.readFile(canonical, context.agentId, input);
115
+ return errorResult(`unknown skill provider tool "${name}"`);
116
+ }
117
+ listResources(context) {
118
+ const canonical = loadCanonicalSource(context.homeDir);
119
+ return canonical.skills
120
+ .filter((skill) => hasSkillDocument(skill) && isInScope(context.agentId, skill.scope, canonical.managedAgents))
121
+ .map((skill) => ({
122
+ uri: skillUri(skill.name),
123
+ name: `${skill.name}/SKILL.md`,
124
+ description: `Instructions for the ${skill.name} skill`,
125
+ mimeType: "text/markdown",
126
+ }));
127
+ }
128
+ readResource(uri, context) {
129
+ if (uri.protocol !== "trellis:" || uri.hostname !== "skills") {
130
+ throw new Error(`unsupported skill resource URI "${uri.toString()}"`);
131
+ }
132
+ const segments = uri.pathname.split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
133
+ if (segments.length !== 2 || segments[1] !== "SKILL.md") {
134
+ throw new Error(`unsupported skill resource URI "${uri.toString()}"`);
135
+ }
136
+ const canonical = loadCanonicalSource(context.homeDir);
137
+ const skill = skillFor(canonical, context.agentId, segments[0]);
138
+ if (!skill)
139
+ throw new Error(`skill "${segments[0]}" is not available to ${context.agentId}`);
140
+ const content = this.readBoundedFile(resolve(skill.dir, "SKILL.md"), skill.dir);
141
+ return { contents: [{ uri: uri.toString(), mimeType: "text/markdown", text: content }] };
142
+ }
143
+ search(canonical, agentId, input) {
144
+ const query = typeof input.query === "string" ? input.query.trim().toLowerCase() : "";
145
+ const limit = typeof input.limit === "number" ? Math.min(50, Math.max(1, Math.floor(input.limit))) : 20;
146
+ const results = canonical.skills
147
+ .filter((skill) => isInScope(agentId, skill.scope, canonical.managedAgents))
148
+ .map((skill) => {
149
+ let content;
150
+ try {
151
+ content = this.readBoundedFile(resolve(skill.dir, "SKILL.md"), skill.dir);
152
+ }
153
+ catch {
154
+ return undefined;
155
+ }
156
+ return { ...skillMetadata(skill, canonical, agentId, content), dir: skill.dir };
157
+ })
158
+ .filter((skill) => skill !== undefined)
159
+ .filter((skill) => !query || `${skill.name} ${skill.description}`.toLowerCase().includes(query))
160
+ .slice(0, limit)
161
+ .map(({ dir: _dir, ...result }) => result);
162
+ return textResult({ skills: results });
163
+ }
164
+ readSkill(canonical, agentId, input) {
165
+ if (typeof input.name !== "string" || input.name.length === 0)
166
+ return errorResult('"name" is required');
167
+ const skill = skillFor(canonical, agentId, input.name);
168
+ if (!skill)
169
+ return errorResult(`skill "${input.name}" is not available to ${agentId}`);
170
+ const path = resolve(skill.dir, "SKILL.md");
171
+ const content = this.readBoundedFile(path, skill.dir);
172
+ return textResult({ ...skillMetadata(skill, canonical, agentId, content), content });
173
+ }
174
+ readFile(canonical, agentId, input) {
175
+ if (typeof input.name !== "string" || typeof input.relativePath !== "string")
176
+ return errorResult('"name" and "relativePath" are required');
177
+ const skill = skillFor(canonical, agentId, input.name);
178
+ if (!skill)
179
+ return errorResult(`skill "${input.name}" is not available to ${agentId}`);
180
+ if (input.relativePath.length === 0 || input.relativePath.includes("\0") || isSecretLikePath(input.relativePath)) {
181
+ return errorResult("supporting file path is not allowed");
182
+ }
183
+ try {
184
+ const path = resolve(skill.dir, input.relativePath);
185
+ const root = realpathSync(skill.dir);
186
+ const realPath = realpathSync(path);
187
+ const rel = relative(root, realPath);
188
+ if (rel.startsWith(`..${sep}`) || rel === ".." || !existsSync(realPath) || !statSync(realPath).isFile()) {
189
+ return errorResult("supporting file is outside the skill root");
190
+ }
191
+ return textResult({ name: skill.name, relativePath: rel, source: "canonical-skill-supporting-file", content: this.readBoundedFile(realPath, root) });
192
+ }
193
+ catch (err) {
194
+ return errorResult(`could not read supporting file: ${err instanceof Error ? err.message : String(err)}`);
195
+ }
196
+ }
197
+ readBoundedFile(path, root) {
198
+ if (isSecretLikePath(path))
199
+ throw new Error("secret-like files are not readable through the skill provider");
200
+ const rootPath = realpathSync(root);
201
+ const realPath = realpathSync(path);
202
+ const rel = relative(rootPath, realPath);
203
+ if (rel.startsWith(`..${sep}`) || rel === "..")
204
+ throw new Error("file is outside the skill root");
205
+ const stat = statSync(realPath);
206
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES)
207
+ throw new Error(`file exceeds the ${MAX_FILE_BYTES}-byte provider limit`);
208
+ return readFileSync(realPath, "utf8");
209
+ }
210
+ }
@@ -0,0 +1,7 @@
1
+ import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { RuntimeContext, TrellisProvider } from "./mcpRuntime.js";
3
+ export declare class TaskProvider implements TrellisProvider {
4
+ readonly id = "tasks";
5
+ listTools(): Tool[];
6
+ callTool(name: string, args: unknown, context: RuntimeContext): Promise<CallToolResult>;
7
+ }
@@ -0,0 +1,43 @@
1
+ import { claimTask, createTask, handoffTask, listTasks, readTask, updateTask } from "./taskStore.js";
2
+ const LIST = { name: "trellis.tasks.list", description: "List safe Trellis handoff tasks.", inputSchema: { type: "object", properties: { status: { type: "string" }, toAgent: { type: "string" } }, additionalProperties: false }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false } };
3
+ const READ = { name: "trellis.tasks.read", description: "Read one Trellis handoff task and its audit history.", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"], additionalProperties: false }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false } };
4
+ const CREATE = { name: "trellis.tasks.create", description: "Create a safe durable handoff task. Requires explicit confirm=true and never launches another Agent.", inputSchema: { type: "object", properties: { objective: { type: "string" }, toAgent: { type: "string" }, context: { type: "object" }, constraints: { type: "array", items: { type: "string" } }, confirm: { type: "boolean" } }, required: ["objective", "confirm"], additionalProperties: false }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false } };
5
+ const CLAIM = { name: "trellis.tasks.claim", description: "Claim one pending handoff task with an exclusive lease. Requires explicit confirm=true.", inputSchema: { type: "object", properties: { id: { type: "string" }, leaseMs: { type: "number" }, confirm: { type: "boolean" } }, required: ["id", "confirm"], additionalProperties: false }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false } };
6
+ const UPDATE = { name: "trellis.tasks.update", description: "Update a claimed task status without launching another Agent. Requires explicit confirm=true.", inputSchema: { type: "object", properties: { id: { type: "string" }, status: { type: "string" }, note: { type: "string" }, result: { type: "string" }, confirm: { type: "boolean" } }, required: ["id", "status", "confirm"], additionalProperties: false }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false } };
7
+ const HANDOFF = { name: "trellis.tasks.handoff", description: "Hand a task to another managed Agent as data only. Requires explicit confirm=true.", inputSchema: { type: "object", properties: { id: { type: "string" }, toAgent: { type: "string" }, note: { type: "string" }, confirm: { type: "boolean" } }, required: ["id", "toAgent", "confirm"], additionalProperties: false }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false } };
8
+ function result(value) {
9
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], structuredContent: value };
10
+ }
11
+ function failure(error) {
12
+ return { content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }], isError: true };
13
+ }
14
+ export class TaskProvider {
15
+ id = "tasks";
16
+ listTools() { return [LIST, READ, CREATE, CLAIM, UPDATE, HANDOFF]; }
17
+ async callTool(name, args, context) {
18
+ const input = (args ?? {});
19
+ try {
20
+ if (name === LIST.name) {
21
+ const status = input.status;
22
+ const toAgent = input.toAgent;
23
+ return result({ tasks: listTasks(context.homeDir, { status, toAgent }) });
24
+ }
25
+ if (name === READ.name)
26
+ return result(readTask(context.homeDir, String(input.id ?? "")));
27
+ if (input.confirm !== true)
28
+ return failure(`task mutation "${name}" requires confirm=true; no Agent is launched automatically`);
29
+ if (name === CREATE.name)
30
+ return result(createTask(context.homeDir, { objective: String(input.objective ?? ""), fromAgent: context.agentId, ...(input.toAgent ? { toAgent: input.toAgent } : {}), ...(input.context ? { context: input.context } : {}), ...(input.constraints ? { constraints: input.constraints } : {}) }));
31
+ if (name === CLAIM.name)
32
+ return result(claimTask(context.homeDir, String(input.id ?? ""), context.agentId, input.leaseMs));
33
+ if (name === UPDATE.name)
34
+ return result(updateTask(context.homeDir, String(input.id ?? ""), context.agentId, input.status, input.note, input.result));
35
+ if (name === HANDOFF.name)
36
+ return result(handoffTask(context.homeDir, String(input.id ?? ""), context.agentId, input.toAgent, input.note));
37
+ return failure(`unknown task tool "${name}"`);
38
+ }
39
+ catch (error) {
40
+ return failure(error);
41
+ }
42
+ }
43
+ }