@hanhnd/agent-kit 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.
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Agent Delegation Tools - Invoke external agent CLIs (Gemini, Claude)
3
+ * Supports handoff file injection and automatic fallback
4
+ */
5
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ /**
7
+ * Register agent delegation tools with MCP server
8
+ */
9
+ export declare function registerAgentTools(server: McpServer): void;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Agent Delegation Tools - Invoke external agent CLIs (Gemini, Claude)
3
+ * Supports handoff file injection and automatic fallback
4
+ */
5
+ import { spawn } from 'child_process';
6
+ import { randomUUID } from 'crypto';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import { z } from 'zod';
10
+ import { getWorkspaceRoot } from '../utils.js';
11
+ import { commandExists, validatePath } from './security.js';
12
+ // Configurable timeout via environment variable (default: 5 minutes)
13
+ const AGENT_TIMEOUT = parseInt(process.env.KIT_AGENT_TIMEOUT || '300000', 10);
14
+ const jobRegistry = new Map();
15
+ /**
16
+ * Generate a URL-safe slug from a task string (first ~6 words, max 48 chars)
17
+ */
18
+ function taskSlug(task) {
19
+ return task
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9\s]/g, '')
22
+ .trim()
23
+ .split(/\s+/)
24
+ .slice(0, 6)
25
+ .join('-')
26
+ .slice(0, 48);
27
+ }
28
+ /**
29
+ * Initialize a live-streaming log file for an agent job.
30
+ * Writes the header synchronously so the file exists immediately,
31
+ * then opens an append-mode WriteStream for real-time chunk writing.
32
+ */
33
+ function initAgentLog(workspaceRoot, slug, agent) {
34
+ const logsDir = path.join(workspaceRoot, '.agent-kit', 'logs', 'agents');
35
+ fs.mkdirSync(logsDir, { recursive: true });
36
+ const logFile = path.join(logsDir, `delegate-${slug}.log`);
37
+ const header = `Agent: ${agent}\nDate: ${new Date().toISOString()}\n${'─'.repeat(60)}\n\n`;
38
+ fs.writeFileSync(logFile, header, 'utf-8');
39
+ const logStream = fs.createWriteStream(logFile, { flags: 'a' });
40
+ logStream.on('error', () => { });
41
+ return { logPath: path.relative(workspaceRoot, logFile), logStream };
42
+ }
43
+ /**
44
+ * Register agent delegation tools with MCP server
45
+ */
46
+ export function registerAgentTools(server) {
47
+ // ═══════════════════════════════════════════════════════════════
48
+ // TOOL: TRIGGER AGENT
49
+ // Delegates a task to an external agent CLI (gemini or claude).
50
+ // Auto-detects if task is a file path and reads it as context.
51
+ // Falls back to the alternate agent if requested one is missing.
52
+ // ═══════════════════════════════════════════════════════════════
53
+ server.tool('kit_trigger_agent', 'Delegate a task to an external agent CLI (gemini or claude). The task can be a direct message or a path to a handoff file (.agent-kit/handoffs/plans/plan-xyz.md). Falls back to the other agent CLI if the requested one is not installed.', {
54
+ agent: z.enum(['gemini', 'claude']).describe('The agent CLI to invoke: "gemini" or "claude"'),
55
+ task: z
56
+ .string()
57
+ .describe('Task message or path to a handoff file (e.g., ".agent-kit/handoffs/plans/plan-xyz.md")'),
58
+ }, async ({ agent, task }) => {
59
+ try {
60
+ const workspaceRoot = getWorkspaceRoot();
61
+ // Detect if task is a file path — resolve and read if it exists
62
+ let prompt = task;
63
+ const resolvedTask = path.resolve(workspaceRoot, task);
64
+ if (fs.existsSync(resolvedTask) && fs.statSync(resolvedTask).isFile()) {
65
+ try {
66
+ validatePath(task, workspaceRoot);
67
+ prompt = fs.readFileSync(resolvedTask, 'utf-8');
68
+ }
69
+ catch (err) {
70
+ return {
71
+ content: [{ type: 'text', text: `Error reading task file: ${err}` }],
72
+ };
73
+ }
74
+ }
75
+ // Determine which agent to use — fallback if requested not available
76
+ let usedAgent = agent;
77
+ let fallbackReason;
78
+ if (!commandExists(agent)) {
79
+ const fallback = agent === 'gemini' ? 'claude' : 'gemini';
80
+ if (commandExists(fallback)) {
81
+ usedAgent = fallback;
82
+ fallbackReason = `${agent} CLI not installed, fell back to ${fallback}`;
83
+ }
84
+ else {
85
+ const installHint = agent === 'gemini'
86
+ ? 'Install Gemini CLI: https://github.com/google-gemini/gemini-cli'
87
+ : 'Install Claude CLI: npm install -g @anthropic-ai/claude-code';
88
+ return {
89
+ content: [
90
+ {
91
+ type: 'text',
92
+ text: JSON.stringify({
93
+ agent,
94
+ status: 'error',
95
+ output: '',
96
+ error: `Neither ${agent} nor the fallback agent CLI is installed. ${installHint}`,
97
+ }, null, 2),
98
+ },
99
+ ],
100
+ };
101
+ }
102
+ }
103
+ // Execute the agent CLI with the prompt
104
+ // Gemini: -y (--yolo) for auto-accept, -p for headless prompt
105
+ // Claude: --dangerously-skip-permissions for auto-accept, -p/--print for headless
106
+ const agentArgs = usedAgent === 'gemini'
107
+ ? ['-y', '-p', prompt]
108
+ : ['--dangerously-skip-permissions', '-p', prompt];
109
+ const slug = taskSlug(task);
110
+ const { logPath, logStream } = initAgentLog(workspaceRoot, slug, usedAgent);
111
+ const jobId = randomUUID();
112
+ const child = spawn(usedAgent, agentArgs, {
113
+ cwd: workspaceRoot,
114
+ env: { ...process.env, GEMINI_WORKSPACE: workspaceRoot },
115
+ });
116
+ const job = {
117
+ id: jobId,
118
+ agent: usedAgent,
119
+ process: child,
120
+ startedAt: new Date(),
121
+ chunks: [],
122
+ logStream,
123
+ timeoutHandle: setTimeout(() => {
124
+ child.kill('SIGTERM');
125
+ }, AGENT_TIMEOUT),
126
+ };
127
+ jobRegistry.set(jobId, job);
128
+ try {
129
+ await new Promise((resolve, reject) => {
130
+ child.stdout?.on('data', (data) => {
131
+ job.chunks.push(data.toString());
132
+ job.logStream.write(data);
133
+ });
134
+ child.stderr?.on('data', (data) => {
135
+ job.chunks.push(data.toString());
136
+ job.logStream.write(data);
137
+ });
138
+ child.on('close', (code) => {
139
+ if (code === 0)
140
+ resolve();
141
+ else
142
+ reject(new Error(`Process exited with code ${code}`));
143
+ });
144
+ child.on('error', reject);
145
+ });
146
+ const output = job.chunks.join('');
147
+ return {
148
+ content: [
149
+ {
150
+ type: 'text',
151
+ text: JSON.stringify({
152
+ agent: usedAgent,
153
+ status: fallbackReason ? 'fallback' : 'success',
154
+ log: logPath,
155
+ output,
156
+ ...(fallbackReason && { fallback_reason: fallbackReason }),
157
+ }, null, 2),
158
+ },
159
+ ],
160
+ };
161
+ }
162
+ catch (error) {
163
+ throw error;
164
+ }
165
+ finally {
166
+ job.logStream.end();
167
+ clearTimeout(job.timeoutHandle);
168
+ jobRegistry.delete(jobId);
169
+ }
170
+ }
171
+ catch (error) {
172
+ const msg = error instanceof Error ? error.message : String(error);
173
+ return {
174
+ content: [
175
+ {
176
+ type: 'text',
177
+ text: JSON.stringify({
178
+ agent,
179
+ status: 'error',
180
+ output: '',
181
+ error: msg,
182
+ }, null, 2),
183
+ },
184
+ ],
185
+ };
186
+ }
187
+ });
188
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Configuration utilities
3
+ * Shared configuration constants and loaders
4
+ */
5
+ /**
6
+ * Default file extensions - expanded to support more languages
7
+ * Used by kit_get_project_context and other tools
8
+ */
9
+ export declare const DEFAULT_EXTENSIONS: string[];
10
+ /**
11
+ * Project settings interface
12
+ */
13
+ export interface ProjectSettings {
14
+ fileExtensions?: string[];
15
+ [key: string]: unknown;
16
+ }
17
+ /**
18
+ * Get file extensions - from settings.json or defaults
19
+ * Allows project-specific customization
20
+ */
21
+ export declare function getFileExtensions(projectDir: string): string[];
22
+ /**
23
+ * Load project settings from .gemini/settings.json
24
+ */
25
+ export declare function loadProjectSettings(projectDir: string): ProjectSettings;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Configuration utilities
3
+ * Shared configuration constants and loaders
4
+ */
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
7
+ /**
8
+ * Default file extensions - expanded to support more languages
9
+ * Used by kit_get_project_context and other tools
10
+ */
11
+ export const DEFAULT_EXTENSIONS = [
12
+ '.ts',
13
+ '.js',
14
+ '.tsx',
15
+ '.jsx', // JavaScript/TypeScript
16
+ '.py', // Python
17
+ '.go', // Go
18
+ '.rs', // Rust
19
+ '.java',
20
+ '.kt', // Java/Kotlin
21
+ '.cpp',
22
+ '.c',
23
+ '.h',
24
+ '.hpp', // C/C++
25
+ '.php', // PHP
26
+ '.rb', // Ruby
27
+ '.swift', // Swift
28
+ '.vue',
29
+ '.svelte', // Frontend frameworks
30
+ '.json',
31
+ '.yaml',
32
+ '.yml', // Config files
33
+ '.md', // Documentation
34
+ ];
35
+ /**
36
+ * Get file extensions - from settings.json or defaults
37
+ * Allows project-specific customization
38
+ */
39
+ export function getFileExtensions(projectDir) {
40
+ const settings = loadProjectSettings(projectDir);
41
+ if (settings.fileExtensions && Array.isArray(settings.fileExtensions)) {
42
+ return settings.fileExtensions;
43
+ }
44
+ return DEFAULT_EXTENSIONS;
45
+ }
46
+ /**
47
+ * Load project settings from .gemini/settings.json
48
+ */
49
+ export function loadProjectSettings(projectDir) {
50
+ const settingsPath = path.join(projectDir, '.agent-kit', 'settings.json');
51
+ if (fs.existsSync(settingsPath)) {
52
+ try {
53
+ return JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
54
+ }
55
+ catch (error) {
56
+ console.error(`[agent-kit] Warning: Failed to parse ${settingsPath}:`, error);
57
+ }
58
+ }
59
+ return {};
60
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Core Tools - Extension info and handoff persistence
3
+ */
4
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { DEFAULT_EXTENSIONS } from './config.js';
6
+ /**
7
+ * Register core tools with MCP server
8
+ */
9
+ export declare function registerCoreTools(server: McpServer): void;
10
+ export { DEFAULT_EXTENSIONS };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Core Tools - Extension info and handoff persistence
3
+ */
4
+ import * as fs from 'fs';
5
+ import * as path from 'path';
6
+ import { z } from 'zod';
7
+ import { getExtensionRoot, getWorkspaceRoot } from '../utils.js';
8
+ import { DEFAULT_EXTENSIONS } from './config.js';
9
+ /**
10
+ * Register core tools with MCP server
11
+ */
12
+ export function registerCoreTools(server) {
13
+ // ═══════════════════════════════════════════════════════════════
14
+ // TOOL: GET EXTENSION INFO
15
+ // Returns absolute paths so agents can use native Read tool
16
+ // for loading agent personas and skill files
17
+ // ═══════════════════════════════════════════════════════════════
18
+ server.tool('kit_get_extension_info', 'Get absolute paths to the agent-kit extension directories (agents, skills, commands, scripts). Use the returned paths with the native Read tool to load agent personas and skill modules.', {}, async () => {
19
+ try {
20
+ const root = getExtensionRoot();
21
+ return {
22
+ content: [
23
+ {
24
+ type: 'text',
25
+ text: JSON.stringify({
26
+ extensionRoot: root,
27
+ agentsDir: path.join(root, 'agents'),
28
+ skillsDir: path.join(root, 'skills'),
29
+ commandsDir: path.join(root, 'commands'),
30
+ scriptsDir: path.join(root, 'scripts'),
31
+ }, null, 2),
32
+ },
33
+ ],
34
+ };
35
+ }
36
+ catch (error) {
37
+ return {
38
+ content: [{ type: 'text', text: `Error getting extension info: ${error}` }],
39
+ };
40
+ }
41
+ });
42
+ // ═══════════════════════════════════════════════════════════════
43
+ // TOOL: SAVE HANDOFF
44
+ // Writes brainstorm/plan/ticket artifacts to the workspace
45
+ // Returns the saved file path for use in next-step instructions
46
+ // ═══════════════════════════════════════════════════════════════
47
+ server.tool('kit_save_handoff', 'Save a brainstorm, plan, or ticket handoff artifact to .agent-kit/handoffs/. Returns the saved file path to use in next-step instructions.', {
48
+ type: z.enum(['brainstorm', 'plan', 'ticket']).describe('Handoff type'),
49
+ content: z.string().describe('Full markdown content to save'),
50
+ slug: z
51
+ .string()
52
+ .describe('Short identifier for the filename, e.g. "user-auth" or "PROJ-123"'),
53
+ }, async ({ type, content, slug }) => {
54
+ try {
55
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
56
+ const safeSlug = slug.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
57
+ const filename = `${type}-${timestamp}-${safeSlug}.md`;
58
+ const handoffDir = path.join(getWorkspaceRoot(), '.agent-kit', 'handoffs', `${type}s`);
59
+ if (!fs.existsSync(handoffDir)) {
60
+ fs.mkdirSync(handoffDir, { recursive: true });
61
+ }
62
+ const filePath = path.join(handoffDir, filename);
63
+ fs.writeFileSync(filePath, content, 'utf8');
64
+ return {
65
+ content: [{ type: 'text', text: `✅ Saved to: ${filePath}` }],
66
+ };
67
+ }
68
+ catch (error) {
69
+ return {
70
+ content: [{ type: 'text', text: `Error saving handoff: ${error}` }],
71
+ };
72
+ }
73
+ });
74
+ }
75
+ // Export DEFAULT_EXTENSIONS for backward compatibility
76
+ export { DEFAULT_EXTENSIONS };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Agent Delegation Tools - Invoke external agent CLIs (Gemini, Claude)
3
+ * Supports handoff file injection and automatic fallback
4
+ */
5
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ /**
7
+ * Register agent delegation tools with MCP server
8
+ */
9
+ export declare function registerGeminiTools(server: McpServer): void;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Agent Delegation Tools - Invoke external agent CLIs (Gemini, Claude)
3
+ * Supports handoff file injection and automatic fallback
4
+ */
5
+ import { execFileSync } from 'child_process';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import { z } from 'zod';
9
+ import { getWorkspaceRoot } from '../utils.js';
10
+ import { commandExists, validatePath } from './security.js';
11
+ // Configurable timeout via environment variable (default: 5 minutes)
12
+ const AGENT_TIMEOUT = parseInt(process.env.KIT_AGENT_TIMEOUT || '300000', 10);
13
+ /**
14
+ * Register agent delegation tools with MCP server
15
+ */
16
+ export function registerGeminiTools(server) {
17
+ // ═══════════════════════════════════════════════════════════════
18
+ // TOOL: TRIGGER AGENT
19
+ // Delegates a task to an external agent CLI (gemini or claude).
20
+ // Auto-detects if task is a file path and reads it as context.
21
+ // Falls back to the alternate agent if requested one is missing.
22
+ // ═══════════════════════════════════════════════════════════════
23
+ server.tool('kit_trigger_agent', 'Delegate a task to an external agent CLI (gemini or claude). The task can be a direct message or a path to a handoff file (.agent-kit/handoffs/plans/plan-xyz.md). Falls back to the other agent CLI if the requested one is not installed.', {
24
+ agent: z.enum(['gemini', 'claude']).describe('The agent CLI to invoke: "gemini" or "claude"'),
25
+ task: z
26
+ .string()
27
+ .describe('Task message or path to a handoff file (e.g., ".agent-kit/handoffs/plans/plan-xyz.md")'),
28
+ }, async ({ agent, task }) => {
29
+ try {
30
+ const workspaceRoot = getWorkspaceRoot();
31
+ // Detect if task is a file path — resolve and read if it exists
32
+ let prompt = task;
33
+ const resolvedTask = path.resolve(workspaceRoot, task);
34
+ if (fs.existsSync(resolvedTask) && fs.statSync(resolvedTask).isFile()) {
35
+ try {
36
+ validatePath(task, workspaceRoot);
37
+ prompt = fs.readFileSync(resolvedTask, 'utf-8');
38
+ }
39
+ catch (err) {
40
+ return {
41
+ content: [{ type: 'text', text: `Error reading task file: ${err}` }],
42
+ };
43
+ }
44
+ }
45
+ // Determine which agent to use — fallback if requested not available
46
+ let usedAgent = agent;
47
+ let fallbackReason;
48
+ if (!commandExists(agent)) {
49
+ const fallback = agent === 'gemini' ? 'claude' : 'gemini';
50
+ if (commandExists(fallback)) {
51
+ usedAgent = fallback;
52
+ fallbackReason = `${agent} CLI not installed, fell back to ${fallback}`;
53
+ }
54
+ else {
55
+ const installHint = agent === 'gemini'
56
+ ? 'Install Gemini CLI: https://github.com/google-gemini/gemini-cli'
57
+ : 'Install Claude CLI: npm install -g @anthropic-ai/claude-code';
58
+ return {
59
+ content: [
60
+ {
61
+ type: 'text',
62
+ text: JSON.stringify({
63
+ agent,
64
+ status: 'error',
65
+ output: '',
66
+ error: `Neither ${agent} nor the fallback agent CLI is installed. ${installHint}`,
67
+ }, null, 2),
68
+ },
69
+ ],
70
+ };
71
+ }
72
+ }
73
+ // Execute the agent CLI with the prompt
74
+ const output = execFileSync(usedAgent, ['-p', prompt], {
75
+ encoding: 'utf8',
76
+ timeout: AGENT_TIMEOUT,
77
+ maxBuffer: 10 * 1024 * 1024, // 10MB
78
+ cwd: workspaceRoot,
79
+ });
80
+ return {
81
+ content: [
82
+ {
83
+ type: 'text',
84
+ text: JSON.stringify({
85
+ agent: usedAgent,
86
+ status: fallbackReason ? 'fallback' : 'success',
87
+ output,
88
+ ...(fallbackReason && { fallback_reason: fallbackReason }),
89
+ }, null, 2),
90
+ },
91
+ ],
92
+ };
93
+ }
94
+ catch (error) {
95
+ const msg = error instanceof Error ? error.message : String(error);
96
+ return {
97
+ content: [
98
+ {
99
+ type: 'text',
100
+ text: JSON.stringify({
101
+ agent,
102
+ status: 'error',
103
+ output: '',
104
+ error: msg,
105
+ }, null, 2),
106
+ },
107
+ ],
108
+ };
109
+ }
110
+ });
111
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Integration Tools - Bitbucket, Jira
3
+ * Tools: kit_get_bitbucket_pr, kit_jira_get_ticket
4
+ */
5
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ export declare function registerIntegrationTools(server: McpServer): void;