agent-pool-mcp 1.0.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.
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/index.js +38 -0
- package/package.json +44 -0
- package/policies/read-only.yaml +13 -0
- package/policies/safe-edit.yaml +7 -0
- package/skills/code-reviewer.md +37 -0
- package/skills/doc-fixer.md +21 -0
- package/skills/orchestrator.md +77 -0
- package/skills/test-writer.md +25 -0
- package/src/cli.js +298 -0
- package/src/runner/config.js +92 -0
- package/src/runner/gemini-runner.js +273 -0
- package/src/runner/process-manager.js +136 -0
- package/src/runner/ssh.js +72 -0
- package/src/server.js +350 -0
- package/src/tool-definitions.js +176 -0
- package/src/tools/consult.js +99 -0
- package/src/tools/results.js +416 -0
- package/src/tools/skills.js +281 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process Manager — tracks spawned Gemini CLI processes and ensures cleanup.
|
|
3
|
+
*
|
|
4
|
+
* Fixes zombie process bug: spawns with detached=true, kills with
|
|
5
|
+
* process.kill(-pid) to terminate the entire process group.
|
|
6
|
+
*
|
|
7
|
+
* @module agent-pool/runner/process-manager
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
|
|
12
|
+
/** @type {Map<number, {taskId: string, startTime: number, label: string}>} */
|
|
13
|
+
const children = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Register a spawned child process for tracking.
|
|
17
|
+
*
|
|
18
|
+
* @param {number} pid - Child process PID
|
|
19
|
+
* @param {string} taskId - Associated task ID
|
|
20
|
+
* @param {string} [label] - Human-readable label
|
|
21
|
+
*/
|
|
22
|
+
export function trackChild(pid, taskId, label = '') {
|
|
23
|
+
children.set(pid, { taskId, startTime: Date.now(), label });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Kill an entire process group by PID.
|
|
28
|
+
* Uses negative PID to kill the process group (only works with detached processes).
|
|
29
|
+
*
|
|
30
|
+
* @param {number} pid - Process PID
|
|
31
|
+
* @returns {boolean} Whether kill signal was sent
|
|
32
|
+
*/
|
|
33
|
+
export function killGroup(pid) {
|
|
34
|
+
try {
|
|
35
|
+
process.kill(-pid, 'SIGTERM');
|
|
36
|
+
children.delete(pid);
|
|
37
|
+
return true;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// ESRCH = process not found (already dead)
|
|
40
|
+
if (err.code === 'ESRCH') {
|
|
41
|
+
children.delete(pid);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// EPERM = no permission — try simple kill
|
|
45
|
+
try {
|
|
46
|
+
process.kill(pid, 'SIGTERM');
|
|
47
|
+
children.delete(pid);
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
children.delete(pid);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Untrack a child process (called on normal exit).
|
|
58
|
+
*
|
|
59
|
+
* @param {number} pid
|
|
60
|
+
*/
|
|
61
|
+
export function untrackChild(pid) {
|
|
62
|
+
children.delete(pid);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Kill all tracked child processes.
|
|
67
|
+
* Called on SIGTERM/SIGINT for graceful shutdown.
|
|
68
|
+
*
|
|
69
|
+
* @returns {number} Number of processes killed
|
|
70
|
+
*/
|
|
71
|
+
export function killAll() {
|
|
72
|
+
let killed = 0;
|
|
73
|
+
for (const pid of children.keys()) {
|
|
74
|
+
if (killGroup(pid)) killed++;
|
|
75
|
+
}
|
|
76
|
+
return killed;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get list of currently tracked processes.
|
|
81
|
+
*
|
|
82
|
+
* @returns {Array<{pid: number, taskId: string, startTime: number, label: string}>}
|
|
83
|
+
*/
|
|
84
|
+
export function listChildren() {
|
|
85
|
+
return [...children.entries()].map(([pid, info]) => ({ pid, ...info }));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get system-wide Gemini process load.
|
|
90
|
+
* Counts all `gemini` processes on the system, separating ours from external.
|
|
91
|
+
*
|
|
92
|
+
* @returns {{total: number, ours: number, external: number, warning: string|null}}
|
|
93
|
+
*/
|
|
94
|
+
export function getSystemLoad() {
|
|
95
|
+
let total = 0;
|
|
96
|
+
try {
|
|
97
|
+
// Use the exact gemini binary path to avoid false positives
|
|
98
|
+
// (e.g. Chrome processes using .gemini/ profile directory)
|
|
99
|
+
const geminiPath = execSync('which gemini 2>/dev/null || true', { encoding: 'utf-8' }).trim();
|
|
100
|
+
if (geminiPath) {
|
|
101
|
+
const out = execSync(`pgrep -f "${geminiPath}" 2>/dev/null || true`, { encoding: 'utf-8' }).trim();
|
|
102
|
+
if (out) {
|
|
103
|
+
total = out.split('\n').filter(Boolean).length;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// pgrep not available or failed
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const ours = children.size;
|
|
111
|
+
const external = Math.max(0, total - ours);
|
|
112
|
+
|
|
113
|
+
let warning = null;
|
|
114
|
+
if (external > 0) {
|
|
115
|
+
warning = `⚠️ System load: ${external} other Gemini process${external > 1 ? 'es' : ''} running — responses may be slower.`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { total, ours, external, warning };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Cleanup on exit signals
|
|
122
|
+
process.on('SIGTERM', () => {
|
|
123
|
+
const count = killAll();
|
|
124
|
+
if (count > 0) {
|
|
125
|
+
console.error(`[agent-pool] SIGTERM: killed ${count} child process(es)`);
|
|
126
|
+
}
|
|
127
|
+
process.exit(0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
process.on('SIGINT', () => {
|
|
131
|
+
const count = killAll();
|
|
132
|
+
if (count > 0) {
|
|
133
|
+
console.error(`[agent-pool] SIGINT: killed ${count} child process(es)`);
|
|
134
|
+
}
|
|
135
|
+
process.exit(0);
|
|
136
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSH utilities — shell escaping and remote command building.
|
|
3
|
+
*
|
|
4
|
+
* @module agent-pool/runner/ssh
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Escape a string for safe use in a remote shell command.
|
|
9
|
+
* Wraps in single quotes, escaping any embedded single quotes.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} arg - Argument to escape
|
|
12
|
+
* @returns {string} Shell-safe argument
|
|
13
|
+
*/
|
|
14
|
+
export function escapeShellArg(arg) {
|
|
15
|
+
// Replace single quotes: ' → '\''
|
|
16
|
+
// Then wrap entire string in single quotes
|
|
17
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Build spawn arguments for SSH remote execution.
|
|
22
|
+
* Uses `echo REMOTE_PID:$$` to capture remote PID for cleanup.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} runner - Runner config
|
|
25
|
+
* @param {string} runner.host - SSH host
|
|
26
|
+
* @param {string} [runner.cwd] - Remote working directory
|
|
27
|
+
* @param {string[]} geminiArgs - Gemini CLI arguments
|
|
28
|
+
* @param {string} localCwd - Local cwd (fallback for remote)
|
|
29
|
+
* @returns {{command: string, args: string[]}}
|
|
30
|
+
*/
|
|
31
|
+
export function buildSshSpawn(runner, geminiArgs, localCwd) {
|
|
32
|
+
const remoteCwd = runner.cwd ?? localCwd;
|
|
33
|
+
|
|
34
|
+
// Build safe remote command with PID echo
|
|
35
|
+
const safeArgs = geminiArgs.map(escapeShellArg).join(' ');
|
|
36
|
+
const remoteCmd = `cd ${escapeShellArg(remoteCwd)} && echo "REMOTE_PID:$$" && exec gemini ${safeArgs}`;
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
command: 'ssh',
|
|
40
|
+
args: [runner.host, remoteCmd],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse REMOTE_PID from the first line of stdout.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} line - stdout line
|
|
48
|
+
* @returns {number|null} Remote PID or null
|
|
49
|
+
*/
|
|
50
|
+
export function parseRemotePid(line) {
|
|
51
|
+
const match = line.match(/^REMOTE_PID:(\d+)$/);
|
|
52
|
+
return match ? parseInt(match[1]) : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Kill a remote process group via SSH.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} host - SSH host
|
|
59
|
+
* @param {number} remotePid - Remote PID to kill
|
|
60
|
+
* @returns {Promise<boolean>} Whether kill was attempted
|
|
61
|
+
*/
|
|
62
|
+
export function killRemoteProcess(host, remotePid) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
import('node:child_process').then(({ execFile }) => {
|
|
65
|
+
execFile('ssh', [host, `kill -TERM -${remotePid} 2>/dev/null; kill -TERM ${remotePid} 2>/dev/null`], {
|
|
66
|
+
timeout: 5000,
|
|
67
|
+
}, () => {
|
|
68
|
+
resolve(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
package/src/server.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server setup — tool registry and call handler.
|
|
3
|
+
* Connects tool definitions to their implementations.
|
|
4
|
+
*
|
|
5
|
+
* @module agent-pool/server
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
9
|
+
import {
|
|
10
|
+
ListToolsRequestSchema,
|
|
11
|
+
CallToolRequestSchema,
|
|
12
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
|
|
15
|
+
import { runGeminiStreaming, listGeminiSessions, DEFAULT_TIMEOUT_SEC, DEFAULT_APPROVAL_MODE } from './runner/gemini-runner.js';
|
|
16
|
+
import { getSystemLoad } from './runner/process-manager.js';
|
|
17
|
+
import { createTask, completeTask, failTask, formatTaskResult, getActiveTasks, cancelTask } from './tools/results.js';
|
|
18
|
+
import { listSkills, createSkill, deleteSkill, installSkill, provisionSkill } from './tools/skills.js';
|
|
19
|
+
import { consultPeer } from './tools/consult.js';
|
|
20
|
+
|
|
21
|
+
import { TOOL_DEFINITIONS } from './tool-definitions.js';
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
import { execFileSync } from 'node:child_process';
|
|
27
|
+
|
|
28
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
|
|
30
|
+
const defaultCwd = process.cwd();
|
|
31
|
+
|
|
32
|
+
// ─── Prerequisite check (cached at startup) ──────────────────
|
|
33
|
+
|
|
34
|
+
let geminiAvailable = null;
|
|
35
|
+
|
|
36
|
+
function checkGemini() {
|
|
37
|
+
if (geminiAvailable !== null) return geminiAvailable;
|
|
38
|
+
try {
|
|
39
|
+
execFileSync('which', ['gemini'], { encoding: 'utf-8', timeout: 2000 });
|
|
40
|
+
geminiAvailable = true;
|
|
41
|
+
} catch {
|
|
42
|
+
geminiAvailable = false;
|
|
43
|
+
}
|
|
44
|
+
return geminiAvailable;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const GEMINI_REQUIRED_ERROR = {
|
|
48
|
+
content: [{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: `❌ Gemini CLI is not installed or not in PATH.
|
|
51
|
+
|
|
52
|
+
**To fix:**
|
|
53
|
+
1. Install: \`npm install -g @google/gemini-cli\`
|
|
54
|
+
2. Authenticate: run \`gemini\` (opens browser for OAuth)
|
|
55
|
+
3. Verify: \`gemini --version\`
|
|
56
|
+
4. Restart your IDE to reload the MCP server.
|
|
57
|
+
|
|
58
|
+
Docs: https://github.com/google-gemini/gemini-cli`
|
|
59
|
+
}],
|
|
60
|
+
isError: true,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Tools that require Gemini CLI to be installed */
|
|
64
|
+
const GEMINI_TOOLS = new Set([
|
|
65
|
+
'delegate_task', 'delegate_task_readonly', 'consult_peer', 'list_sessions',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
// ─── Depth tracking (for nested orchestration) ──────────────
|
|
69
|
+
|
|
70
|
+
const CURRENT_DEPTH = parseInt(process.env.AGENT_POOL_DEPTH ?? '0');
|
|
71
|
+
const MAX_DEPTH = process.env.AGENT_POOL_MAX_DEPTH
|
|
72
|
+
? parseInt(process.env.AGENT_POOL_MAX_DEPTH)
|
|
73
|
+
: null; // null = no limit (disabled by default)
|
|
74
|
+
|
|
75
|
+
function isDepthExceeded() {
|
|
76
|
+
return MAX_DEPTH !== null && CURRENT_DEPTH >= MAX_DEPTH;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const DEPTH_EXCEEDED_ERROR = {
|
|
80
|
+
content: [{
|
|
81
|
+
type: 'text',
|
|
82
|
+
text: `⚠️ Orchestration depth limit reached (depth=${CURRENT_DEPTH}, max=${MAX_DEPTH}).
|
|
83
|
+
|
|
84
|
+
This agent-pool instance is running inside a nested Gemini CLI worker.
|
|
85
|
+
Delegation is disabled at this depth to prevent runaway process spawning.
|
|
86
|
+
|
|
87
|
+
Execute the task directly instead of delegating it.
|
|
88
|
+
|
|
89
|
+
To increase the limit, set AGENT_POOL_MAX_DEPTH to a higher value.`
|
|
90
|
+
}],
|
|
91
|
+
isError: true,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create and configure the MCP server.
|
|
96
|
+
*
|
|
97
|
+
* @returns {Server}
|
|
98
|
+
*/
|
|
99
|
+
export function createServer() {
|
|
100
|
+
// Check gemini once at server creation
|
|
101
|
+
checkGemini();
|
|
102
|
+
|
|
103
|
+
if (CURRENT_DEPTH > 0) {
|
|
104
|
+
console.error(`[agent-pool] Nested orchestration: depth=${CURRENT_DEPTH}${MAX_DEPTH !== null ? `, max=${MAX_DEPTH}` : ''}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const server = new Server(
|
|
108
|
+
{ name: 'agent-pool', version: '1.0.0' },
|
|
109
|
+
{ capabilities: { tools: {} } },
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
113
|
+
tools: TOOL_DEFINITIONS,
|
|
114
|
+
}));
|
|
115
|
+
|
|
116
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
117
|
+
const { name, arguments: args } = request.params;
|
|
118
|
+
|
|
119
|
+
// Guard: tools that need gemini
|
|
120
|
+
if (GEMINI_TOOLS.has(name)) {
|
|
121
|
+
if (!checkGemini()) return GEMINI_REQUIRED_ERROR;
|
|
122
|
+
if (isDepthExceeded()) return DEPTH_EXCEEDED_ERROR;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let response;
|
|
126
|
+
try {
|
|
127
|
+
switch (name) {
|
|
128
|
+
case 'delegate_task':
|
|
129
|
+
response = handleDelegateTask(args); break;
|
|
130
|
+
case 'delegate_task_readonly':
|
|
131
|
+
response = handleDelegateReadonly(args); break;
|
|
132
|
+
case 'get_task_result':
|
|
133
|
+
response = formatTaskResult(args.task_id); break;
|
|
134
|
+
case 'cancel_task':
|
|
135
|
+
response = cancelTask(args.task_id); break;
|
|
136
|
+
case 'consult_peer':
|
|
137
|
+
response = consultPeer(args, defaultCwd); break;
|
|
138
|
+
case 'list_sessions':
|
|
139
|
+
response = await handleListSessions(args); break;
|
|
140
|
+
case 'list_skills':
|
|
141
|
+
response = handleListSkills(args); break;
|
|
142
|
+
case 'create_skill':
|
|
143
|
+
response = handleCreateSkill(args); break;
|
|
144
|
+
case 'delete_skill':
|
|
145
|
+
response = handleDeleteSkill(args); break;
|
|
146
|
+
case 'install_skill':
|
|
147
|
+
response = handleInstallSkill(args); break;
|
|
148
|
+
default:
|
|
149
|
+
response = { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true };
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
response = { content: [{ type: 'text', text: `Gemini CLI Error: ${error.message}` }], isError: true };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Append active tasks footer to every response
|
|
156
|
+
const footer = getActiveTasks();
|
|
157
|
+
if (footer && response.content?.[0]?.text) {
|
|
158
|
+
response.content[0].text += footer;
|
|
159
|
+
}
|
|
160
|
+
return response;
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return server;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Tool Handlers ──────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Shared handler for delegate_task and delegate_task_readonly.
|
|
170
|
+
*
|
|
171
|
+
* @param {object} args - Tool arguments
|
|
172
|
+
* @param {object} defaults - Override defaults for the mode
|
|
173
|
+
* @param {string} defaults.approvalMode - Approval mode
|
|
174
|
+
* @param {string} defaults.emoji - Status emoji
|
|
175
|
+
* @param {string} defaults.label - Status label
|
|
176
|
+
* @returns {{content: Array<{type: string, text: string}>}}
|
|
177
|
+
*/
|
|
178
|
+
function handleDelegate(args, { approvalMode, emoji, label }) {
|
|
179
|
+
const taskId = randomUUID();
|
|
180
|
+
const cwd = args.cwd ?? defaultCwd;
|
|
181
|
+
let prompt = args.prompt;
|
|
182
|
+
|
|
183
|
+
// Hybrid skill activation: provision to project, then instruct native activation
|
|
184
|
+
if (args.skill) {
|
|
185
|
+
const provisioned = provisionSkill(cwd, args.skill);
|
|
186
|
+
if (provisioned) {
|
|
187
|
+
prompt = `IMPORTANT: Before starting the task, activate the skill "${provisioned.name}" using the activate_skill tool. Then proceed with the task.\n\n${prompt}`;
|
|
188
|
+
} else {
|
|
189
|
+
prompt = `NOTE: Skill '${args.skill}' was requested but not found in any tier (project, global, built-in). Proceed with the task.\n\n${prompt}`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Resolve policy path (built-in templates or absolute path)
|
|
194
|
+
let policyPath = args.policy ?? null;
|
|
195
|
+
if (policyPath && !path.isAbsolute(policyPath)) {
|
|
196
|
+
const policiesDir = path.resolve(__dirname, '..', 'policies');
|
|
197
|
+
let builtinPolicy = path.resolve(policiesDir, policyPath);
|
|
198
|
+
if (!fs.existsSync(builtinPolicy) && fs.existsSync(builtinPolicy + '.yaml')) {
|
|
199
|
+
builtinPolicy = builtinPolicy + '.yaml';
|
|
200
|
+
}
|
|
201
|
+
// Path traversal protection: ensure resolved path stays within policies/
|
|
202
|
+
if (builtinPolicy.startsWith(policiesDir + path.sep) && fs.existsSync(builtinPolicy)) {
|
|
203
|
+
policyPath = builtinPolicy;
|
|
204
|
+
} else {
|
|
205
|
+
policyPath = null; // Invalid or traversal attempt — ignore
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Inject role awareness into the agent's prompt
|
|
210
|
+
const roleDescriptions = {
|
|
211
|
+
yolo: 'FULL ACCESS — you can read/write files, run shell commands, and make any changes.',
|
|
212
|
+
auto_edit: 'AUTO-EDIT — you can read files and edit code, but shell commands require approval.',
|
|
213
|
+
plan: 'READ-ONLY — you can only read files and analyze code. You CANNOT write files or run destructive commands.',
|
|
214
|
+
};
|
|
215
|
+
const resolvedMode = args.approval_mode ?? approvalMode;
|
|
216
|
+
const modeNotice = roleDescriptions[resolvedMode] ?? `Mode: ${resolvedMode}`;
|
|
217
|
+
|
|
218
|
+
// Build workspace scope awareness — tell the agent its sandbox boundaries upfront
|
|
219
|
+
const workspaceDirs = [cwd];
|
|
220
|
+
if (args.include_dirs?.length > 0) {
|
|
221
|
+
workspaceDirs.push(...args.include_dirs);
|
|
222
|
+
}
|
|
223
|
+
const scopeNotice = `[Workspace Scope] You have access to these directories:\n${workspaceDirs.map((d) => ` - ${d}`).join('\n')}\nIf you need files outside these paths, use shell commands (cat, find, ls) instead of file tools. Do NOT attempt list_directory or read_file on paths outside your workspace — they will be rejected by the sandbox.`;
|
|
224
|
+
|
|
225
|
+
prompt = `[Agent Mode: ${resolvedMode.toUpperCase()}] ${modeNotice}\n\n${scopeNotice}\n\n${prompt}`;
|
|
226
|
+
|
|
227
|
+
const taskOpts = {
|
|
228
|
+
prompt,
|
|
229
|
+
cwd,
|
|
230
|
+
model: args.model,
|
|
231
|
+
approvalMode: resolvedMode,
|
|
232
|
+
timeout: args.timeout ?? DEFAULT_TIMEOUT_SEC,
|
|
233
|
+
sessionId: args.session_id,
|
|
234
|
+
taskId,
|
|
235
|
+
runner: args.runner,
|
|
236
|
+
policy: policyPath,
|
|
237
|
+
includeDirs: args.include_dirs,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
createTask(taskId, args.prompt, args.on_wait_hint, resolvedMode);
|
|
241
|
+
|
|
242
|
+
runGeminiStreaming(taskOpts)
|
|
243
|
+
.then((result) => completeTask(taskId, result))
|
|
244
|
+
.catch((err) => failTask(taskId, err.message));
|
|
245
|
+
|
|
246
|
+
const mode = args.approval_mode ?? approvalMode;
|
|
247
|
+
const runnerInfo = args.runner ? `\n- **Runner**: ${args.runner}` : '';
|
|
248
|
+
const skillInfo = args.skill ? `\n- **Skill**: ${args.skill}` : '';
|
|
249
|
+
const policyInfo = policyPath ? `\n- **Policy**: ${policyPath}` : '';
|
|
250
|
+
|
|
251
|
+
// System load awareness
|
|
252
|
+
const load = getSystemLoad();
|
|
253
|
+
const loadInfo = load.warning ? `\n\n${load.warning}` : '';
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
content: [{
|
|
257
|
+
type: 'text',
|
|
258
|
+
text: `${emoji} ${label}\n\n- **Task ID**: \`${taskId}\`\n- **Mode**: ${mode}${runnerInfo}${skillInfo}${policyInfo}\n- **Prompt**: ${args.prompt.substring(0, 100)}...${loadInfo}\n\nUse \`get_task_result\` with this task_id to check status.`,
|
|
259
|
+
}],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function handleDelegateTask(args) {
|
|
264
|
+
return handleDelegate(args, {
|
|
265
|
+
approvalMode: DEFAULT_APPROVAL_MODE,
|
|
266
|
+
emoji: '🚀',
|
|
267
|
+
label: 'Task delegated.',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function handleDelegateReadonly(args) {
|
|
272
|
+
return handleDelegate(args, {
|
|
273
|
+
approvalMode: DEFAULT_APPROVAL_MODE,
|
|
274
|
+
emoji: '🔍',
|
|
275
|
+
label: 'Analysis task delegated (full access).',
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @param {object} args
|
|
281
|
+
*/
|
|
282
|
+
async function handleListSessions(args) {
|
|
283
|
+
const sessions = await listGeminiSessions(args.cwd ?? defaultCwd);
|
|
284
|
+
if (sessions.length === 0) {
|
|
285
|
+
return { content: [{ type: 'text', text: 'No sessions found for this project.' }] };
|
|
286
|
+
}
|
|
287
|
+
const lines = sessions.map(
|
|
288
|
+
(s) => `- **${s.index}**. ${s.preview} (${s.timeAgo}) \`${s.sessionId}\``,
|
|
289
|
+
);
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: 'text', text: `## Available Sessions (${sessions.length})\n\n${lines.join('\n')}` }],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** @param {object} args */
|
|
296
|
+
function handleListSkills(args) {
|
|
297
|
+
const skills = listSkills(args.cwd ?? defaultCwd);
|
|
298
|
+
if (skills.length === 0) {
|
|
299
|
+
return { content: [{ type: 'text', text: 'No skills found. Use create_skill to create one.' }] };
|
|
300
|
+
}
|
|
301
|
+
const lines = skills.map(
|
|
302
|
+
(s) => `- **${s.name}** — ${s.description} (\`${s.fileName}\`) [${s.tier}]`,
|
|
303
|
+
);
|
|
304
|
+
return {
|
|
305
|
+
content: [{ type: 'text', text: `## Available Skills (${skills.length})\n\n${lines.join('\n')}` }],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** @param {object} args */
|
|
310
|
+
function handleCreateSkill(args) {
|
|
311
|
+
const scope = args.scope ?? 'project';
|
|
312
|
+
const filePath = createSkill(args.cwd ?? defaultCwd, args.skill_name, args.description, args.instructions, scope);
|
|
313
|
+
return {
|
|
314
|
+
content: [{
|
|
315
|
+
type: 'text',
|
|
316
|
+
text: `✅ Skill created [${scope}]: \`${args.skill_name}\`\nPath: \`${filePath}\`\n\nUse with delegate_task: \`skill: "${args.skill_name}"\``,
|
|
317
|
+
}],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** @param {object} args */
|
|
322
|
+
function handleDeleteSkill(args) {
|
|
323
|
+
const scope = args.scope ?? 'project';
|
|
324
|
+
const deleted = deleteSkill(args.cwd ?? defaultCwd, args.skill_name, scope);
|
|
325
|
+
return {
|
|
326
|
+
content: [{
|
|
327
|
+
type: 'text',
|
|
328
|
+
text: deleted ? `✅ Skill deleted [${scope}]: \`${args.skill_name}\`` : `❌ Skill not found [${scope}]: \`${args.skill_name}\``,
|
|
329
|
+
}],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** @param {object} args */
|
|
334
|
+
function handleInstallSkill(args) {
|
|
335
|
+
const result = installSkill(args.cwd ?? defaultCwd, args.skill_name);
|
|
336
|
+
if (!result) {
|
|
337
|
+
return {
|
|
338
|
+
content: [{
|
|
339
|
+
type: 'text',
|
|
340
|
+
text: `❌ Skill \`${args.skill_name}\` not found in global or built-in tiers. Use \`list_skills\` to see available skills.`,
|
|
341
|
+
}],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
content: [{
|
|
346
|
+
type: 'text',
|
|
347
|
+
text: `✅ Skill installed into project:\n- **From**: \`${result.from}\` [${result.tier}]\n- **To**: \`${result.to}\`\n\nThe skill is now a local copy — you can customize it for this project.`,
|
|
348
|
+
}],
|
|
349
|
+
};
|
|
350
|
+
}
|