gentle-agent-extensions 1.0.1 → 1.2.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.
@@ -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