gentle-agent-extensions 1.0.1 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -0
- package/README.md +177 -287
- package/package.json +23 -15
- package/resources/agents/architecture-council.md +158 -0
- package/resources/overlays/orchestrator.overlay.md +162 -0
- package/resources/routing/architecture-council.md +65 -0
- package/resources/skills/architecture-quest/SKILL.md +238 -0
- package/resources/skills/gentle-agent-extension/SKILL.md +329 -0
- package/src/extension.ts +544 -0
- package/.manifest.json +0 -15
- package/agents/.gitkeep +0 -0
- package/assets/.gitkeep +0 -0
- package/docs/conventions.md +0 -278
- package/extensions/gentle-agent-extensions.ts +0 -547
- package/mcp/.gitkeep +0 -31
- package/overlays/.gitkeep +0 -0
- package/routing/.gitkeep +0 -0
- package/scripts/doctor.sh +0 -509
- package/scripts/install.sh +0 -206
- package/scripts/sync.sh +0 -513
- package/skills/gentle-agent-extension/SKILL.md +0 -360
- package/tests/gentle-extensions.test.ts +0 -653
|
@@ -1,547 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
|
-
import { join, resolve, dirname } from "node:path";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
|
|
6
|
-
const EXTENSION_NAME = "gentle-agent-extensions";
|
|
7
|
-
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
-
const AGENTS_DIR = join(PROJECT_ROOT, "agents");
|
|
9
|
-
const OVERLAYS_DIR = join(PROJECT_ROOT, "overlays");
|
|
10
|
-
const ROUTING_DIR = join(PROJECT_ROOT, "routing");
|
|
11
|
-
const SKILLS_DIR = join(PROJECT_ROOT, "skills");
|
|
12
|
-
const MCP_DIR = join(PROJECT_ROOT, "mcp");
|
|
13
|
-
const MANIFEST_PATH = join(PROJECT_ROOT, ".manifest.json");
|
|
14
|
-
|
|
15
|
-
interface OverlayDefinition {
|
|
16
|
-
target: string;
|
|
17
|
-
priority: number;
|
|
18
|
-
mode: "append";
|
|
19
|
-
instructions: string;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface McpServerDefinition {
|
|
23
|
-
name: string;
|
|
24
|
-
description: string;
|
|
25
|
-
transport: "stdio" | "http";
|
|
26
|
-
command?: string;
|
|
27
|
-
args?: string[];
|
|
28
|
-
url?: string;
|
|
29
|
-
env?: Record<string, string>;
|
|
30
|
-
exposure?: "codemode" | "direct" | "deferred" | "hidden";
|
|
31
|
-
toolExposure?: Record<string, string>;
|
|
32
|
-
enabled?: boolean;
|
|
33
|
-
timeout?: number;
|
|
34
|
-
install?: {
|
|
35
|
-
package: string;
|
|
36
|
-
version: string;
|
|
37
|
-
manager: string;
|
|
38
|
-
};
|
|
39
|
-
auth?: {
|
|
40
|
-
type: "oauth" | "bearer" | "none";
|
|
41
|
-
envVar?: string;
|
|
42
|
-
description?: string;
|
|
43
|
-
};
|
|
44
|
-
capabilities?: string[];
|
|
45
|
-
agentDependencies?: string[];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
interface ProjectState {
|
|
49
|
-
overlays: Map<string, OverlayDefinition[]>;
|
|
50
|
-
agents: Set<string>;
|
|
51
|
-
mcpServers: Map<string, McpServerDefinition>;
|
|
52
|
-
initialized: boolean;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const projectState: ProjectState = {
|
|
56
|
-
overlays: new Map(),
|
|
57
|
-
agents: new Set(),
|
|
58
|
-
mcpServers: new Map(),
|
|
59
|
-
initialized: false,
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
function loadManifest(): any {
|
|
63
|
-
if (!existsSync(MANIFEST_PATH)) return { managedPaths: {} };
|
|
64
|
-
try {
|
|
65
|
-
return JSON.parse(readFileSync(MANIFEST_PATH, "utf8"));
|
|
66
|
-
} catch {
|
|
67
|
-
return { managedPaths: {} };
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function saveManifest(manifest: any): void {
|
|
72
|
-
writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function discoverProjectAgents(): string[] {
|
|
76
|
-
if (!existsSync(AGENTS_DIR)) return [];
|
|
77
|
-
return readdirSync(AGENTS_DIR)
|
|
78
|
-
.filter((f) => f.endsWith(".md"))
|
|
79
|
-
.map((f) => f.slice(0, -3));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function loadOverlays(): Map<string, OverlayDefinition[]> {
|
|
83
|
-
const overlays = new Map<string, OverlayDefinition[]>();
|
|
84
|
-
|
|
85
|
-
if (!existsSync(OVERLAYS_DIR)) return overlays;
|
|
86
|
-
|
|
87
|
-
for (const file of readdirSync(OVERLAYS_DIR)) {
|
|
88
|
-
if (!file.endsWith(".overlay.md")) continue;
|
|
89
|
-
|
|
90
|
-
const filePath = join(OVERLAYS_DIR, file);
|
|
91
|
-
const content = readFileSync(filePath, "utf8");
|
|
92
|
-
|
|
93
|
-
const frontmatterMatch = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(content);
|
|
94
|
-
if (!frontmatterMatch) continue;
|
|
95
|
-
|
|
96
|
-
const [, fm, body] = frontmatterMatch;
|
|
97
|
-
const targetMatch = /target:\s*([^\n]+)/.exec(fm);
|
|
98
|
-
const priorityMatch = /priority:\s*(\d+)/.exec(fm);
|
|
99
|
-
const modeMatch = /mode:\s*(\w+)/.exec(fm);
|
|
100
|
-
|
|
101
|
-
if (!targetMatch) continue;
|
|
102
|
-
|
|
103
|
-
const target = targetMatch[1].trim();
|
|
104
|
-
const priority = priorityMatch ? parseInt(priorityMatch[1], 10) : 100;
|
|
105
|
-
const mode = (modeMatch ? modeMatch[1].trim() : "append") as "append";
|
|
106
|
-
|
|
107
|
-
const overlay: OverlayDefinition = {
|
|
108
|
-
target,
|
|
109
|
-
priority,
|
|
110
|
-
mode,
|
|
111
|
-
instructions: body.trim(),
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const existing = overlays.get(target) || [];
|
|
115
|
-
existing.push(overlay);
|
|
116
|
-
existing.sort((a, b) => a.priority - b.priority);
|
|
117
|
-
overlays.set(target, existing);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return overlays;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function loadMcpServers(): Map<string, McpServerDefinition> {
|
|
124
|
-
const servers = new Map<string, McpServerDefinition>();
|
|
125
|
-
|
|
126
|
-
if (!existsSync(MCP_DIR)) return servers;
|
|
127
|
-
|
|
128
|
-
for (const file of readdirSync(MCP_DIR)) {
|
|
129
|
-
if (!file.endsWith(".mcp.json")) continue;
|
|
130
|
-
|
|
131
|
-
const filePath = join(MCP_DIR, file);
|
|
132
|
-
const content = readFileSync(filePath, "utf8");
|
|
133
|
-
|
|
134
|
-
let server: McpServerDefinition;
|
|
135
|
-
try {
|
|
136
|
-
server = JSON.parse(content);
|
|
137
|
-
} catch (error) {
|
|
138
|
-
console.error(`[${EXTENSION_NAME}] Failed to parse MCP definition ${file}:`, error);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Validate required fields
|
|
143
|
-
if (!server.name || !server.description || !server.transport) {
|
|
144
|
-
console.warn(`[${EXTENSION_NAME}] MCP definition ${file} missing required fields (name, description, transport)`);
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (server.transport === "stdio" && !server.command) {
|
|
149
|
-
console.warn(`[${EXTENSION_NAME}] MCP definition ${file}: stdio transport requires 'command'`);
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (server.transport === "http" && !server.url) {
|
|
154
|
-
console.warn(`[${EXTENSION_NAME}] MCP definition ${file}: http transport requires 'url'`);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Validate name format (letters, digits, _, - only)
|
|
159
|
-
if (!/^[A-Za-z0-9_-]+$/.test(server.name)) {
|
|
160
|
-
console.warn(`[${EXTENSION_NAME}] MCP definition ${file}: invalid name format (only letters, digits, _, - allowed)`);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
servers.set(server.name, server);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return servers;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function getAgentHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
171
|
-
if (env.GENTLE_PI_AGENT_HOME) return resolve(env.GENTLE_PI_AGENT_HOME);
|
|
172
|
-
if (env.PI_CODING_AGENT_DIR) return resolve(env.PI_CODING_AGENT_DIR);
|
|
173
|
-
return join(homedir(), ".pi", "agent");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function getProjectAgentDirs(cwd: string, agentHome: string): Array<{ dir: string; scope: string }> {
|
|
177
|
-
return [
|
|
178
|
-
{ dir: join(agentHome, "agents"), scope: "global" },
|
|
179
|
-
{ dir: join(agentHome, "subagents"), scope: "global" },
|
|
180
|
-
{ dir: join(cwd, ".pi", "agents"), scope: "project" },
|
|
181
|
-
{ dir: join(cwd, ".pi", "subagents"), scope: "project" },
|
|
182
|
-
];
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function resolveProjectAgentDir(cwd: string, agentHome: string): string {
|
|
186
|
-
const dirs = getProjectAgentDirs(cwd, agentHome);
|
|
187
|
-
for (const { dir, scope } of dirs) {
|
|
188
|
-
if (scope === "project") return dir;
|
|
189
|
-
}
|
|
190
|
-
return join(cwd, ".pi", "agents");
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function ensureDir(dir: string): void {
|
|
194
|
-
if (!existsSync(dir)) {
|
|
195
|
-
mkdirSync(dir, { recursive: true });
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function appendSystemPromptOnce(options: { appendSystemPrompt?: string } | null | undefined, text: string): void {
|
|
200
|
-
const normalized = text.replace(/^\n+/, "");
|
|
201
|
-
if (!options || !normalized) return;
|
|
202
|
-
const current = options.appendSystemPrompt ?? "";
|
|
203
|
-
if (current.includes(normalized)) return;
|
|
204
|
-
options.appendSystemPrompt = current ? `${current}\n\n${normalized}` : normalized;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function buildMcpOrchestratorInstructions(): string {
|
|
208
|
-
const servers = Array.from(projectState.mcpServers.values());
|
|
209
|
-
if (servers.length === 0) return "";
|
|
210
|
-
|
|
211
|
-
let instructions = "\n\n<!-- gentle-agent-extensions: MCP servers available -->\n";
|
|
212
|
-
instructions += "## Available MCP Servers\n\n";
|
|
213
|
-
instructions += "The following MCP servers are registered and available for use:\n\n";
|
|
214
|
-
|
|
215
|
-
for (const server of servers) {
|
|
216
|
-
if (server.enabled === false) continue;
|
|
217
|
-
|
|
218
|
-
instructions += `### ${server.name}\n`;
|
|
219
|
-
instructions += `- **Description**: ${server.description}\n`;
|
|
220
|
-
instructions += `- **Transport**: ${server.transport}\n`;
|
|
221
|
-
if (server.transport === "stdio") {
|
|
222
|
-
instructions += `- **Command**: ${server.command} ${server.args?.join(" ") || ""}\n`;
|
|
223
|
-
} else {
|
|
224
|
-
instructions += `- **URL**: ${server.url}\n`;
|
|
225
|
-
}
|
|
226
|
-
if (server.exposure) {
|
|
227
|
-
instructions += `- **Exposure**: ${server.exposure}\n`;
|
|
228
|
-
}
|
|
229
|
-
if (server.capabilities && server.capabilities.length > 0) {
|
|
230
|
-
instructions += `- **Capabilities**: ${server.capabilities.join(", ")}\n`;
|
|
231
|
-
}
|
|
232
|
-
if (server.agentDependencies && server.agentDependencies.length > 0) {
|
|
233
|
-
instructions += `- **Recommended for agents**: ${server.agentDependencies.join(", ")}\n`;
|
|
234
|
-
}
|
|
235
|
-
instructions += "\n";
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
instructions += "When a task requires capabilities from these servers, consider delegating to agents that declare those MCP dependencies, or use the MCP tools directly via `codemode` or `tool_search`.\n";
|
|
239
|
-
|
|
240
|
-
return instructions;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function initializeProjectState(cwd: string): void {
|
|
244
|
-
if (projectState.initialized) return;
|
|
245
|
-
|
|
246
|
-
projectState.agents = new Set(discoverProjectAgents());
|
|
247
|
-
projectState.overlays = loadOverlays();
|
|
248
|
-
projectState.mcpServers = loadMcpServers();
|
|
249
|
-
projectState.initialized = true;
|
|
250
|
-
|
|
251
|
-
// Log discovered agents
|
|
252
|
-
const agents = Array.from(projectState.agents);
|
|
253
|
-
if (agents.length > 0) {
|
|
254
|
-
console.log(`[${EXTENSION_NAME}] Discovered ${agents.length} custom agent(s): ${agents.join(", ")}`);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const overlayTargets = Array.from(projectState.overlays.keys());
|
|
258
|
-
if (overlayTargets.length > 0) {
|
|
259
|
-
console.log(`[${EXTENSION_NAME}] Loaded overlays for: ${overlayTargets.join(", ")}`);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const mcpServerNames = Array.from(projectState.mcpServers.keys());
|
|
263
|
-
if (mcpServerNames.length > 0) {
|
|
264
|
-
console.log(`[${EXTENSION_NAME}] Discovered ${mcpServerNames.length} MCP server(s): ${mcpServerNames.join(", ")}`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export default function gentleAgentExtensions(pi: ExtensionAPI): void {
|
|
269
|
-
pi.on("session_start", async (_event, ctx: ExtensionContext) => {
|
|
270
|
-
initializeProjectState(ctx.cwd);
|
|
271
|
-
|
|
272
|
-
// Register project skills directory with skill registry if available
|
|
273
|
-
if (existsSync(SKILLS_DIR)) {
|
|
274
|
-
// The skill registry extension scans projectSkillDirs which includes <cwd>/skills
|
|
275
|
-
// So our skills are automatically discovered. We just ensure the directory exists.
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Register MCP servers via Pi's native API
|
|
279
|
-
for (const [name, server] of projectState.mcpServers) {
|
|
280
|
-
if (server.enabled === false) {
|
|
281
|
-
console.log(`[${EXTENSION_NAME}] MCP server ${name} disabled, skipping registration`);
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const config: any = {
|
|
286
|
-
description: server.description,
|
|
287
|
-
exposure: server.exposure || "codemode",
|
|
288
|
-
enabled: server.enabled !== false,
|
|
289
|
-
timeout: server.timeout || 60,
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
if (server.transport === "stdio") {
|
|
293
|
-
config.command = server.command;
|
|
294
|
-
config.args = server.args || [];
|
|
295
|
-
if (server.env) config.env = server.env;
|
|
296
|
-
if (server.cwd) config.cwd = server.cwd;
|
|
297
|
-
} else {
|
|
298
|
-
config.url = server.url;
|
|
299
|
-
if (server.headers) config.headers = server.headers;
|
|
300
|
-
if (server.oauth) config.oauth = server.oauth;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (server.toolExposure) config.toolExposure = server.toolExposure;
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
pi.registerMcpServer(name, config);
|
|
307
|
-
console.log(`[${EXTENSION_NAME}] Registered MCP server: ${name}`);
|
|
308
|
-
} catch (error) {
|
|
309
|
-
console.error(`[${EXTENSION_NAME}] Failed to register MCP server ${name}:`, error);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Ensure project agent directory exists
|
|
314
|
-
const agentHome = getAgentHome(ctx.env);
|
|
315
|
-
const projectAgentDir = resolveProjectAgentDir(ctx.cwd, agentHome);
|
|
316
|
-
ensureDir(projectAgentDir);
|
|
317
|
-
|
|
318
|
-
// Sync agents from project to project agent directory
|
|
319
|
-
await syncAgents(ctx.cwd, agentHome);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
pi.on("before_agent_start", (event, ctx: ExtensionContext) => {
|
|
323
|
-
if (!projectState.initialized) {
|
|
324
|
-
initializeProjectState(ctx.cwd);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const agentName = (event as { agentName?: string }).agentName;
|
|
328
|
-
const options = event.systemPromptOptions;
|
|
329
|
-
|
|
330
|
-
// Apply orchestrator overlays (when no specific agent, or primary session)
|
|
331
|
-
if (!agentName || agentName === "orchestrator") {
|
|
332
|
-
const orchestratorOverlays = projectState.overlays.get("orchestrator") || [];
|
|
333
|
-
for (const overlay of orchestratorOverlays) {
|
|
334
|
-
appendSystemPromptOnce(options, `\n\n<!-- gentle-agent-extensions: overlay for orchestrator -->\n${overlay.instructions}`);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Add MCP server information to orchestrator context
|
|
338
|
-
const mcpInstructions = buildMcpOrchestratorInstructions();
|
|
339
|
-
if (mcpInstructions) {
|
|
340
|
-
appendSystemPromptOnce(options, mcpInstructions);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Apply agent-specific overlays
|
|
345
|
-
if (agentName) {
|
|
346
|
-
const agentOverlays = projectState.overlays.get(agentName) || [];
|
|
347
|
-
for (const overlay of agentOverlays) {
|
|
348
|
-
appendSystemPromptOnce(options, `\n\n<!-- gentle-agent-extensions: overlay for ${agentName} -->\n${overlay.instructions}`);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return undefined;
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
pi.registerCommand("gentle-extensions:sync", {
|
|
356
|
-
description: "Synchronize custom agents, overlays, skills, and MCP servers with the active Pi profile",
|
|
357
|
-
handler: async (_args, ctx) => {
|
|
358
|
-
const agentHome = getAgentHome(ctx.env);
|
|
359
|
-
await syncAgents(ctx.cwd, agentHome);
|
|
360
|
-
ctx.ui.notify("gentle-agent-extensions: sync completed", "info");
|
|
361
|
-
},
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
pi.registerCommand("gentle-extensions:doctor", {
|
|
365
|
-
description: "Check gentle-agent-extensions installation and configuration",
|
|
366
|
-
handler: async (_args, ctx) => {
|
|
367
|
-
await runDoctor(ctx.cwd, ctx.env);
|
|
368
|
-
},
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
pi.registerCommand("gentle-extensions:status", {
|
|
372
|
-
description: "Show current gentle-agent-extensions status",
|
|
373
|
-
handler: async (_args, ctx) => {
|
|
374
|
-
if (!projectState.initialized) initializeProjectState(ctx.cwd);
|
|
375
|
-
|
|
376
|
-
const agents = Array.from(projectState.agents);
|
|
377
|
-
const overlayTargets = Array.from(projectState.overlays.keys());
|
|
378
|
-
const mcpServers = Array.from(projectState.mcpServers.keys());
|
|
379
|
-
|
|
380
|
-
let msg = `gentle-agent-extensions status:\n`;
|
|
381
|
-
msg += ` Project root: ${PROJECT_ROOT}\n`;
|
|
382
|
-
msg += ` Custom agents: ${agents.length > 0 ? agents.join(", ") : "none"}\n`;
|
|
383
|
-
msg += ` Overlays: ${overlayTargets.length > 0 ? overlayTargets.join(", ") : "none"}\n`;
|
|
384
|
-
msg += ` MCP servers: ${mcpServers.length > 0 ? mcpServers.join(", ") : "none"}\n`;
|
|
385
|
-
msg += ` Skills dir: ${existsSync(SKILLS_DIR) ? "present" : "missing"}\n`;
|
|
386
|
-
|
|
387
|
-
ctx.ui.notify(msg, "info");
|
|
388
|
-
},
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
async function syncAgents(cwd: string, agentHome: string): Promise<void> {
|
|
393
|
-
if (!existsSync(AGENTS_DIR)) return;
|
|
394
|
-
|
|
395
|
-
const projectAgentDir = resolveProjectAgentDir(cwd, agentHome);
|
|
396
|
-
ensureDir(projectAgentDir);
|
|
397
|
-
|
|
398
|
-
const manifest = loadManifest();
|
|
399
|
-
const managedAgents: string[] = manifest.managedPaths?.agents || [];
|
|
400
|
-
|
|
401
|
-
for (const agentName of projectState.agents) {
|
|
402
|
-
const sourcePath = join(AGENTS_DIR, `${agentName}.md`);
|
|
403
|
-
const targetPath = join(projectAgentDir, `${agentName}.md`);
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
const sourceContent = readFileSync(sourcePath, "utf8");
|
|
407
|
-
let shouldWrite = true;
|
|
408
|
-
|
|
409
|
-
if (existsSync(targetPath)) {
|
|
410
|
-
const targetContent = readFileSync(targetPath, "utf8");
|
|
411
|
-
if (targetContent === sourceContent) {
|
|
412
|
-
shouldWrite = false;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (shouldWrite) {
|
|
417
|
-
writeFileSync(targetPath, sourceContent);
|
|
418
|
-
console.log(`[${EXTENSION_NAME}] Installed agent: ${agentName} -> ${targetPath}`);
|
|
419
|
-
|
|
420
|
-
if (!managedAgents.includes(targetPath)) {
|
|
421
|
-
managedAgents.push(targetPath);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
} catch (error) {
|
|
425
|
-
console.error(`[${EXTENSION_NAME}] Failed to sync agent ${agentName}:`, error);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// Update manifest
|
|
430
|
-
manifest.managedPaths = manifest.managedPaths || {};
|
|
431
|
-
manifest.managedPaths.agents = managedAgents;
|
|
432
|
-
saveManifest(manifest);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
async function runDoctor(cwd: string, env: NodeJS.ProcessEnv): Promise<void> {
|
|
436
|
-
const issues: string[] = [];
|
|
437
|
-
const warnings: string[] = [];
|
|
438
|
-
|
|
439
|
-
// Check project structure
|
|
440
|
-
if (!existsSync(AGENTS_DIR)) warnings.push("agents/ directory missing");
|
|
441
|
-
if (!existsSync(OVERLAYS_DIR)) warnings.push("overlays/ directory missing");
|
|
442
|
-
if (!existsSync(ROUTING_DIR)) warnings.push("routing/ directory missing");
|
|
443
|
-
if (!existsSync(SKILLS_DIR)) warnings.push("skills/ directory missing");
|
|
444
|
-
if (!existsSync(MCP_DIR)) warnings.push("mcp/ directory missing");
|
|
445
|
-
|
|
446
|
-
// Check agent home
|
|
447
|
-
const agentHome = getAgentHome(env);
|
|
448
|
-
if (!existsSync(agentHome)) {
|
|
449
|
-
issues.push(`Agent home not found: ${agentHome}`);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Check project agent directory
|
|
453
|
-
const projectAgentDir = resolveProjectAgentDir(cwd, agentHome);
|
|
454
|
-
if (!existsSync(projectAgentDir)) {
|
|
455
|
-
warnings.push(`Project agent directory does not exist yet: ${projectAgentDir} (will be created on sync)`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Check manifest
|
|
459
|
-
if (!existsSync(MANIFEST_PATH)) {
|
|
460
|
-
warnings.push("Manifest file missing (.manifest.json)");
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Check for duplicate agent names in global scope
|
|
464
|
-
const globalAgentDirs = [
|
|
465
|
-
join(agentHome, "agents"),
|
|
466
|
-
join(agentHome, "subagents"),
|
|
467
|
-
];
|
|
468
|
-
|
|
469
|
-
const globalAgents = new Map<string, string>();
|
|
470
|
-
for (const dir of globalAgentDirs) {
|
|
471
|
-
if (!existsSync(dir)) continue;
|
|
472
|
-
for (const file of readdirSync(dir)) {
|
|
473
|
-
if (!file.endsWith(".md")) continue;
|
|
474
|
-
const name = file.slice(0, -3);
|
|
475
|
-
if (globalAgents.has(name)) {
|
|
476
|
-
warnings.push(`Duplicate agent name in global scope: ${name} (in ${globalAgents.get(name)} and ${dir})`);
|
|
477
|
-
} else {
|
|
478
|
-
globalAgents.set(name, dir);
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// Check for conflicts between project agents and global agents
|
|
484
|
-
for (const agentName of projectState.agents) {
|
|
485
|
-
if (globalAgents.has(agentName)) {
|
|
486
|
-
warnings.push(`Project agent "${agentName}" shadows global agent in ${globalAgents.get(agentName)}`);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Check overlays reference existing targets
|
|
491
|
-
for (const [target] of projectState.overlays) {
|
|
492
|
-
if (target !== "orchestrator") {
|
|
493
|
-
// Check if target agent exists (global or project)
|
|
494
|
-
const targetExists = globalAgents.has(target) || projectState.agents.has(target);
|
|
495
|
-
if (!targetExists) {
|
|
496
|
-
warnings.push(`Overlay targets unknown agent: ${target}`);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// Check MCP server definitions
|
|
502
|
-
if (existsSync(MCP_DIR)) {
|
|
503
|
-
for (const [name, server] of projectState.mcpServers) {
|
|
504
|
-
// Validate server config
|
|
505
|
-
if (server.transport === "stdio" && !server.command) {
|
|
506
|
-
warnings.push(`MCP server ${name}: stdio transport missing 'command'`);
|
|
507
|
-
}
|
|
508
|
-
if (server.transport === "http" && !server.url) {
|
|
509
|
-
warnings.push(`MCP server ${name}: http transport missing 'url'`);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// Check for env var references that might be secrets
|
|
513
|
-
if (server.env) {
|
|
514
|
-
for (const [key, value] of Object.entries(server.env)) {
|
|
515
|
-
if (value && !value.startsWith("${") && (key.toLowerCase().includes("secret") || key.toLowerCase().includes("token") || key.toLowerCase().includes("key"))) {
|
|
516
|
-
warnings.push(`MCP server ${name}: env ${key} may contain hardcoded secret (use \${VAR} syntax)`);
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Check for duplicate names in mcp.json
|
|
522
|
-
// Note: We can't easily check user/project mcp.json here without ctx
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Report results
|
|
527
|
-
let output = "gentle-agent-extensions doctor:\n\n";
|
|
528
|
-
|
|
529
|
-
if (issues.length > 0) {
|
|
530
|
-
output += "❌ Issues:\n";
|
|
531
|
-
for (const issue of issues) output += ` - ${issue}\n`;
|
|
532
|
-
output += "\n";
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (warnings.length > 0) {
|
|
536
|
-
output += "⚠️ Warnings:\n";
|
|
537
|
-
for (const warning of warnings) output += ` - ${warning}\n`;
|
|
538
|
-
output += "\n";
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
if (issues.length === 0 && warnings.length === 0) {
|
|
542
|
-
output += "✅ All checks passed\n";
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// Use console.log since we don't have ctx here
|
|
546
|
-
console.log(output);
|
|
547
|
-
}
|
package/mcp/.gitkeep
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# MCP Server Definitions
|
|
2
|
-
#
|
|
3
|
-
# Place MCP server definition files (*.mcp.json) in this directory.
|
|
4
|
-
# Each file defines one MCP server that can be registered or configured.
|
|
5
|
-
#
|
|
6
|
-
# Format:
|
|
7
|
-
# {
|
|
8
|
-
# "name": "server-name",
|
|
9
|
-
# "description": "What this server provides",
|
|
10
|
-
# "transport": "stdio|http",
|
|
11
|
-
# "command": "npx", # for stdio
|
|
12
|
-
# "args": ["-y", "pkg"], # for stdio
|
|
13
|
-
# "url": "https://...", # for http
|
|
14
|
-
# "env": {"KEY": "${ENV_VAR}"}, # environment variables (no secrets)
|
|
15
|
-
# "exposure": "codemode", # optional: codemode|direct|deferred|hidden
|
|
16
|
-
# "toolExposure": {}, # optional: per-tool exposure overrides
|
|
17
|
-
# "enabled": true, # optional: default true
|
|
18
|
-
# "timeout": 60, # optional: seconds
|
|
19
|
-
# "install": { # optional: installation metadata
|
|
20
|
-
# "package": "@pkg/server",
|
|
21
|
-
# "version": "1.0.0",
|
|
22
|
-
# "manager": "npx"
|
|
23
|
-
# },
|
|
24
|
-
# "auth": { # optional: authentication requirements
|
|
25
|
-
# "type": "oauth|bearer|none",
|
|
26
|
-
# "envVar": "TOKEN_ENV_VAR",
|
|
27
|
-
# "description": "OAuth flow or bearer token"
|
|
28
|
-
# },
|
|
29
|
-
# "capabilities": ["tools", "resources"],
|
|
30
|
-
# "agentDependencies": ["agent-name"]
|
|
31
|
-
# }
|
package/overlays/.gitkeep
DELETED
|
File without changes
|
package/routing/.gitkeep
DELETED
|
File without changes
|