pelulu-cli 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.
Files changed (56) hide show
  1. package/README.md +273 -0
  2. package/config.example.json +24 -0
  3. package/package.json +44 -0
  4. package/src/core/auto-format.js +47 -0
  5. package/src/core/completion.js +65 -0
  6. package/src/core/config.js +68 -0
  7. package/src/core/confirm.js +51 -0
  8. package/src/core/context.js +69 -0
  9. package/src/core/conversation.js +57 -0
  10. package/src/core/diff.js +50 -0
  11. package/src/core/doctor.js +60 -0
  12. package/src/core/error-handler.js +39 -0
  13. package/src/core/event-bus.js +43 -0
  14. package/src/core/file-tracker.js +44 -0
  15. package/src/core/formatter.js +83 -0
  16. package/src/core/intent.js +81 -0
  17. package/src/core/keybindings.js +29 -0
  18. package/src/core/logger.js +67 -0
  19. package/src/core/model-info.js +26 -0
  20. package/src/core/retry.js +26 -0
  21. package/src/core/sandbox.js +54 -0
  22. package/src/core/session.js +46 -0
  23. package/src/core/spinner.js +60 -0
  24. package/src/core/stats.js +73 -0
  25. package/src/core/system-prompt.js +42 -0
  26. package/src/core/thinking.js +39 -0
  27. package/src/core/tool-help.js +117 -0
  28. package/src/core/tool-registry.js +82 -0
  29. package/src/core/wizard.js +55 -0
  30. package/src/core/workspace.js +96 -0
  31. package/src/index.js +150 -0
  32. package/src/mcp/activation.js +54 -0
  33. package/src/mcp/mcp-handler.js +92 -0
  34. package/src/mcp/message-sender.js +147 -0
  35. package/src/mcp/mqtt-client.js +160 -0
  36. package/src/mcp/wss-endpoint.js +133 -0
  37. package/src/plugins/manager.js +72 -0
  38. package/src/repl-commands.js +91 -0
  39. package/src/repl.js +115 -0
  40. package/src/tools/ai.js +128 -0
  41. package/src/tools/config.js +87 -0
  42. package/src/tools/diff.js +118 -0
  43. package/src/tools/env.js +58 -0
  44. package/src/tools/file.js +177 -0
  45. package/src/tools/git.js +166 -0
  46. package/src/tools/history.js +64 -0
  47. package/src/tools/network.js +87 -0
  48. package/src/tools/process.js +69 -0
  49. package/src/tools/project.js +152 -0
  50. package/src/tools/search.js +100 -0
  51. package/src/tools/shell.js +91 -0
  52. package/src/tools/snippet.js +95 -0
  53. package/src/tools/template.js +113 -0
  54. package/src/tools/watch.js +87 -0
  55. package/src/tui/renderer.js +119 -0
  56. package/src/tui/status-bar.js +34 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Process Tool — process management (1 MCP tool, 4 actions)
3
+ */
4
+ import { exec } from 'child_process';
5
+ import { log } from '../core/logger.js';
6
+
7
+ function run(cmd) {
8
+ return new Promise((resolve) => {
9
+ exec(cmd, { timeout: 10000, maxBuffer: 256 * 1024 }, (_, stdout, stderr) => {
10
+ resolve({ stdout: stdout?.trim() || '', stderr: stderr?.trim() || '' });
11
+ });
12
+ });
13
+ }
14
+
15
+ const ACTIONS = {
16
+ async list({ filter }) {
17
+ const cmd = filter
18
+ ? `ps aux | grep -i "${filter}" | grep -v grep`
19
+ : 'ps aux --sort=-%mem | head -20';
20
+ const r = await run(cmd);
21
+ return { processes: r.stdout };
22
+ },
23
+
24
+ async info({ pid }) {
25
+ if (!pid) throw new Error('pid required');
26
+ const r = await run(`ps -p ${pid} -o pid,ppid,user,%cpu,%mem,vsz,rss,etime,cmd --no-headers`);
27
+ if (!r.stdout) throw new Error(`Process ${pid} not found`);
28
+ const parts = r.stdout.trim().split(/\s+/);
29
+ return { pid: parts[0], ppid: parts[1], user: parts[2], cpu: parts[3], mem: parts[4], vsz: parts[5], rss: parts[6], time: parts[7], cmd: parts.slice(8).join(' ') };
30
+ },
31
+
32
+ async kill({ pid, signal }) {
33
+ if (!pid) throw new Error('pid required');
34
+ const sig = signal || 'SIGTERM';
35
+ try {
36
+ process.kill(pid, sig);
37
+ return { pid, signal, killed: true };
38
+ } catch (e) {
39
+ throw new Error(`Cannot kill ${pid}: ${e.message}`);
40
+ }
41
+ },
42
+
43
+ async top({ limit }) {
44
+ const n = limit || 15;
45
+ const r = await run(`ps aux --sort=-%cpu | head -${n + 1}`);
46
+ return { top: r.stdout };
47
+ },
48
+ };
49
+
50
+ export default {
51
+ name: 'process',
52
+ description: 'Process management: list, info, kill, top',
53
+ actions: Object.keys(ACTIONS).map(name => ({ name })),
54
+ inputSchema: {
55
+ type: 'object',
56
+ properties: {
57
+ action: { type: 'string', enum: Object.keys(ACTIONS) },
58
+ pid: { type: 'number', description: 'Process ID' },
59
+ signal: { type: 'string', description: 'Signal (default SIGTERM)' },
60
+ filter: { type: 'string', description: 'Filter pattern' },
61
+ limit: { type: 'number', description: 'Number of processes' },
62
+ },
63
+ required: ['action'],
64
+ },
65
+ async handler({ action, ...params }) {
66
+ if (!ACTIONS[action]) throw new Error(`Unknown action: ${action}. Use: ${Object.keys(ACTIONS).join(', ')}`);
67
+ return ACTIONS[action](params);
68
+ },
69
+ };
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Project Tool — project lifecycle operations (1 MCP tool, 6 actions)
3
+ * Actions: init, build, test, lint, deps, info
4
+ */
5
+ import { exec } from 'child_process';
6
+ import { readFile } from 'fs/promises';
7
+ import { existsSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { log } from '../core/logger.js';
10
+ import { getConfig } from '../core/config.js';
11
+
12
+ function run(cmd, cwd, timeout = 120000) {
13
+ return new Promise((resolve, reject) => {
14
+ exec(cmd, { cwd, timeout, maxBuffer: 512 * 1024 }, (err, stdout, stderr) => {
15
+ if (err?.killed) return reject(new Error(`Timed out: ${cmd}`));
16
+ resolve({ stdout: stdout?.trim() || '', stderr: stderr?.trim() || '', code: err?.code ?? 0 });
17
+ });
18
+ });
19
+ }
20
+
21
+ function cwd(p) { return p || getConfig().agent?.workspace || process.cwd(); }
22
+
23
+ async function detectProject(dir) {
24
+ const checks = [
25
+ ['package.json', 'node'], ['requirements.txt', 'python'], ['pyproject.toml', 'python'],
26
+ ['Cargo.toml', 'rust'], ['go.mod', 'go'], ['CMakeLists.txt', 'cmake'], ['Makefile', 'make'],
27
+ ['pom.xml', 'java'], ['build.gradle', 'gradle'],
28
+ ];
29
+ for (const [file, type] of checks) {
30
+ if (existsSync(join(dir, file))) return type;
31
+ }
32
+ return 'unknown';
33
+ }
34
+
35
+ const BUILD_CMD = { node: 'npm run build', python: 'python -m build', go: 'go build ./...', rust: 'cargo build', make: 'make', cmake: 'cmake --build build', java: 'mvn package', gradle: 'gradle build' };
36
+ const TEST_CMD = { node: 'npm test', python: 'pytest', go: 'go test ./...', rust: 'cargo test', make: 'make test', java: 'mvn test', gradle: 'gradle test' };
37
+ const LINT_CMD = { node: 'npx eslint . 2>&1 || true', python: 'ruff check . 2>&1 || true', go: 'golangci-lint run 2>&1 || true', rust: 'cargo clippy 2>&1 || true' };
38
+ const INSTALL_CMD = { node: 'npm install', python: 'pip install -r requirements.txt', go: 'go mod download', rust: 'cargo fetch', java: 'mvn dependency:resolve', gradle: 'gradle dependencies' };
39
+
40
+ const ACTIONS = {
41
+ init: {
42
+ required: [],
43
+ handler: async (params) => {
44
+ const dir = cwd(params.path);
45
+ const type = params.template || await detectProject(dir);
46
+ const cmds = { node: 'npm init -y', python: 'python -m venv .venv', go: 'go mod init project', rust: 'cargo init' };
47
+ if (!cmds[type]) throw new Error(`No init for ${type}`);
48
+ await run(cmds[type], dir);
49
+ return { initialized: true, type, path: dir };
50
+ },
51
+ },
52
+
53
+ build: {
54
+ required: [],
55
+ handler: async (params) => {
56
+ const dir = cwd(params.path);
57
+ const type = await detectProject(dir);
58
+ const cmd = BUILD_CMD[type];
59
+ if (!cmd) throw new Error(`No build for ${type}`);
60
+ log('project', `🔨 Building (${type})...`);
61
+ const r = await run(cmd, dir);
62
+ return { success: r.code === 0, type, exitCode: r.code, output: r.stdout.slice(-500) || r.stderr.slice(-500) };
63
+ },
64
+ },
65
+
66
+ test: {
67
+ required: [],
68
+ handler: async (params) => {
69
+ const dir = cwd(params.path);
70
+ const type = await detectProject(dir);
71
+ const cmd = TEST_CMD[type];
72
+ if (!cmd) throw new Error(`No test for ${type}`);
73
+ log('project', `🧪 Testing (${type})...`);
74
+ const r = await run(cmd, dir);
75
+ return { passed: r.code === 0, type, exitCode: r.code, output: r.stdout.slice(-1000) || r.stderr.slice(-1000) };
76
+ },
77
+ },
78
+
79
+ lint: {
80
+ required: [],
81
+ handler: async (params) => {
82
+ const dir = cwd(params.path);
83
+ const type = await detectProject(dir);
84
+ const cmd = LINT_CMD[type];
85
+ if (!cmd) throw new Error(`No lint for ${type}`);
86
+ const r = await run(cmd, dir);
87
+ return { type, issues: r.stdout.slice(-2000) };
88
+ },
89
+ },
90
+
91
+ deps: {
92
+ required: [],
93
+ handler: async (params) => {
94
+ const dir = cwd(params.path);
95
+ const type = await detectProject(dir);
96
+ if (params.install) {
97
+ const cmd = INSTALL_CMD[type];
98
+ if (!cmd) throw new Error(`No install for ${type}`);
99
+ await run(cmd, dir, 180000);
100
+ return { installed: true, type };
101
+ }
102
+ try {
103
+ if (type === 'node') {
104
+ const pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf-8'));
105
+ return { type, dependencies: pkg.dependencies || {}, devDependencies: pkg.devDependencies || {} };
106
+ }
107
+ return { type, message: 'Set install=true to install deps' };
108
+ } catch { return { type, error: 'Could not read project config' }; }
109
+ },
110
+ },
111
+
112
+ info: {
113
+ required: [],
114
+ handler: async (params) => {
115
+ const dir = cwd(params.path);
116
+ const type = await detectProject(dir);
117
+ const info = { type, path: dir };
118
+ try {
119
+ if (type === 'node') {
120
+ const pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf-8'));
121
+ info.name = pkg.name; info.version = pkg.version;
122
+ info.scripts = Object.keys(pkg.scripts || {});
123
+ info.deps = Object.keys(pkg.dependencies || {});
124
+ }
125
+ } catch {}
126
+ return info;
127
+ },
128
+ },
129
+ };
130
+
131
+ const actionNames = Object.keys(ACTIONS);
132
+
133
+ export default {
134
+ name: 'project',
135
+ description: 'Project lifecycle: init, build, test, lint, deps, info (auto-detects type)',
136
+ actions: actionNames.map(name => ({ name, required: ACTIONS[name].required })),
137
+ inputSchema: {
138
+ type: 'object',
139
+ properties: {
140
+ action: { type: 'string', enum: actionNames },
141
+ path: { type: 'string', description: 'Project directory' },
142
+ template: { type: 'string', description: 'Project type for init' },
143
+ install: { type: 'boolean', description: 'Install deps' },
144
+ },
145
+ required: ['action'],
146
+ },
147
+ async handler({ action, ...params }) {
148
+ const a = ACTIONS[action];
149
+ if (!a) throw new Error(`Unknown action: ${action}. Use: ${actionNames.join(', ')}`);
150
+ return a.handler(params);
151
+ },
152
+ };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Search Tool — consolidated search operations (1 MCP tool, 3 actions)
3
+ * Actions: grep, find, web
4
+ */
5
+ import { exec } from 'child_process';
6
+ import https from 'https';
7
+ import http from 'http';
8
+ import { log } from '../core/logger.js';
9
+
10
+ function runCmd(cmd, timeout = 10000) {
11
+ return new Promise((resolve) => {
12
+ exec(cmd, { timeout, maxBuffer: 256 * 1024 }, (_, stdout, stderr) => {
13
+ resolve({ stdout: stdout?.trim() || '', stderr: stderr?.trim() || '' });
14
+ });
15
+ });
16
+ }
17
+
18
+ function httpGet(url, maxChars = 8000) {
19
+ return new Promise((resolve, reject) => {
20
+ const client = url.startsWith('https') ? https : http;
21
+ client.get(url, { timeout: 15000, headers: { 'User-Agent': 'coding-agent/1.0' } }, (res) => {
22
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
23
+ return httpGet(res.headers.location, maxChars).then(resolve, reject);
24
+ }
25
+ let d = '';
26
+ res.on('data', c => { if (d.length < maxChars) d += c; });
27
+ res.on('end', () => resolve({ status: res.statusCode, body: d }));
28
+ }).on('error', reject);
29
+ });
30
+ }
31
+
32
+ const ACTIONS = {
33
+ grep: {
34
+ required: ['pattern'],
35
+ handler: async ({ pattern, path, ignoreCase, include, context }) => {
36
+ if (!pattern) throw new Error('pattern required');
37
+ log('search', `🔍 grep: ${pattern}`);
38
+ const flags = ignoreCase ? '-rn' : '-rn';
39
+ const ic = ignoreCase ? 'i' : '';
40
+ const inc = include ? `--include="*.${include}"` : '';
41
+ const ctx = context ? `-C ${context}` : '';
42
+ const cmd = `grep ${flags}${ic} ${inc} ${ctx} "${pattern}" "${path || '.'}" 2>/dev/null | head -100`;
43
+ const r = await runCmd(cmd);
44
+ const lines = r.stdout.split('\n').filter(Boolean);
45
+ return { pattern, matches: lines.length, results: lines };
46
+ },
47
+ },
48
+
49
+ find: {
50
+ required: ['name'],
51
+ handler: async ({ name, path, type }) => {
52
+ log('search', `🔍 find: ${name}`);
53
+ const t = type ? `-type ${type}` : '';
54
+ const cmd = `find "${path || '.'}" ${t} -name "${name}" 2>/dev/null | head -50`;
55
+ const r = await runCmd(cmd);
56
+ return { pattern: name, matches: r.stdout.split('\n').filter(Boolean).length, files: r.stdout.split('\n').filter(Boolean) };
57
+ },
58
+ },
59
+
60
+ web: {
61
+ required: ['url'],
62
+ handler: async ({ url, maxChars }) => {
63
+ log('search', `🌐 fetch: ${url}`);
64
+ const r = await httpGet(url, maxChars || 8000);
65
+ return { url, status: r.status, body: r.body.slice(0, maxChars || 8000), length: r.body.length };
66
+ },
67
+ },
68
+ };
69
+
70
+ const actionNames = Object.keys(ACTIONS);
71
+
72
+ export default {
73
+ name: 'search',
74
+ description: 'Search: grep (text in files), find (files by name), web (fetch URL content)',
75
+ actions: actionNames.map(name => ({ name, required: ACTIONS[name].required })),
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ action: { type: 'string', enum: actionNames },
80
+ pattern: { type: 'string', description: 'Search pattern (for grep/find)' },
81
+ path: { type: 'string', description: 'Directory or URL' },
82
+ name: { type: 'string', description: 'Filename glob (for find)' },
83
+ type: { type: 'string', description: 'File type: f=file, d=dir' },
84
+ ignoreCase: { type: 'boolean' },
85
+ include: { type: 'string', description: 'Extension filter for grep' },
86
+ context: { type: 'number', description: 'Context lines for grep' },
87
+ url: { type: 'string', description: 'URL to fetch' },
88
+ maxChars: { type: 'number', description: 'Max response chars' },
89
+ },
90
+ required: ['action'],
91
+ },
92
+ async handler({ action, ...params }) {
93
+ const a = ACTIONS[action];
94
+ if (!a) throw new Error(`Unknown action: ${action}. Use: ${actionNames.join(', ')}`);
95
+ for (const field of a.required) {
96
+ if (params[field] === undefined) throw new Error(`Missing required: ${field}`);
97
+ }
98
+ return a.handler(params);
99
+ },
100
+ };
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Shell Tool — consolidated shell operations (1 MCP tool, 4 actions)
3
+ * Actions: exec, bg, ps, kill
4
+ */
5
+ import { exec, spawn } from 'child_process';
6
+ import { log } from '../core/logger.js';
7
+ import { getConfig } from '../core/config.js';
8
+
9
+ const BLOCKED = [/rm\s+-rf\s+\//, /mkfs/, /dd\s+if=.*of=\/dev/, /chmod\s+777\s+\//];
10
+
11
+ function isBlocked(cmd) { return BLOCKED.some(re => re.test(cmd)); }
12
+
13
+ function run(cmd, timeout) {
14
+ return new Promise((resolve, reject) => {
15
+ if (isBlocked(cmd)) return reject(new Error('Blocked: dangerous command'));
16
+ const cfg = getConfig();
17
+ const t = timeout || cfg.tools?.shell_timeout || 30000;
18
+ exec(cmd, { timeout: t, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
19
+ if (err?.killed) return reject(new Error(`Timed out after ${t}ms`));
20
+ resolve({ stdout: stdout?.trim() || '', stderr: stderr?.trim() || '', code: err?.code ?? 0 });
21
+ });
22
+ });
23
+ }
24
+
25
+ const ACTIONS = {
26
+ exec: {
27
+ required: ['command'],
28
+ handler: async ({ command, timeout }) => {
29
+ log('shell', `⚙️ $ ${command}`);
30
+ const result = await run(command, timeout);
31
+ const maxLen = getConfig().tools?.max_output || 10000;
32
+ return { stdout: result.stdout.slice(0, maxLen), stderr: result.stderr.slice(0, 2000), exitCode: result.code };
33
+ },
34
+ },
35
+
36
+ bg: {
37
+ required: ['command'],
38
+ handler: async ({ command }) => {
39
+ log('shell', `⚙️ [bg] $ ${command}`);
40
+ const child = spawn('bash', ['-c', command], { detached: true, stdio: 'ignore' });
41
+ child.unref();
42
+ return { pid: child.pid, background: true, command };
43
+ },
44
+ },
45
+
46
+ ps: {
47
+ required: [],
48
+ handler: async ({ filter }) => {
49
+ const cmd = filter ? `ps aux | grep -i "${filter}" | grep -v grep` : 'ps aux --sort=-%cpu | head -20';
50
+ const result = await run(cmd);
51
+ return { processes: result.stdout };
52
+ },
53
+ },
54
+
55
+ kill: {
56
+ required: ['pid'],
57
+ handler: async ({ pid, signal }) => {
58
+ const sig = signal || 'SIGTERM';
59
+ process.kill(pid, sig);
60
+ return { pid, signal: sig, killed: true };
61
+ },
62
+ },
63
+ };
64
+
65
+ const actionNames = Object.keys(ACTIONS);
66
+
67
+ export default {
68
+ name: 'shell',
69
+ description: 'Shell operations: exec (run command), bg (background process), ps (list), kill',
70
+ actions: actionNames.map(name => ({ name, required: ACTIONS[name].required })),
71
+ inputSchema: {
72
+ type: 'object',
73
+ properties: {
74
+ action: { type: 'string', enum: actionNames },
75
+ command: { type: 'string', description: 'Shell command' },
76
+ timeout: { type: 'number', description: 'Timeout ms' },
77
+ filter: { type: 'string', description: 'Process filter' },
78
+ pid: { type: 'number', description: 'Process ID' },
79
+ signal: { type: 'string', description: 'Signal (default SIGTERM)' },
80
+ },
81
+ required: ['action'],
82
+ },
83
+ async handler({ action, ...params }) {
84
+ const a = ACTIONS[action];
85
+ if (!a) throw new Error(`Unknown action: ${action}. Use: ${actionNames.join(', ')}`);
86
+ for (const field of a.required) {
87
+ if (params[field] === undefined) throw new Error(`Missing required: ${field}`);
88
+ }
89
+ return a.handler(params);
90
+ },
91
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Snippet Tool — save and reuse code snippets (1 MCP tool, 4 actions)
3
+ * Actions: save, load, list, delete
4
+ */
5
+ import { readFile, writeFile, mkdir, readdir, unlink } from 'fs/promises';
6
+ import { existsSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { getConfig } from '../core/config.js';
9
+ import { log } from '../core/logger.js';
10
+
11
+ function snippetDir() {
12
+ return join(getConfig()._root, 'snippets');
13
+ }
14
+
15
+ function snippetPath(name) {
16
+ return join(snippetDir(), `${name}.json`);
17
+ }
18
+
19
+ const ACTIONS = {
20
+ save: {
21
+ required: ['name', 'code'],
22
+ handler: async ({ name, code, language, description }) => {
23
+ const dir = snippetDir();
24
+ await mkdir(dir, { recursive: true });
25
+ const data = { name, code, language: language || 'unknown', description: description || '', savedAt: Date.now() };
26
+ await writeFile(snippetPath(name), JSON.stringify(data, null, 2));
27
+ log('snippet', `💾 Saved: ${name}`);
28
+ return { saved: true, name, path: snippetPath(name) };
29
+ },
30
+ },
31
+
32
+ load: {
33
+ required: ['name'],
34
+ handler: async ({ name }) => {
35
+ const path = snippetPath(name);
36
+ if (!existsSync(path)) throw new Error(`Snippet not found: ${name}`);
37
+ const data = JSON.parse(await readFile(path, 'utf-8'));
38
+ return data;
39
+ },
40
+ },
41
+
42
+ list: {
43
+ required: [],
44
+ handler: async () => {
45
+ const dir = snippetDir();
46
+ if (!existsSync(dir)) return { snippets: [] };
47
+ const files = await readdir(dir);
48
+ const snippets = [];
49
+ for (const f of files.filter(f => f.endsWith('.json'))) {
50
+ try {
51
+ const data = JSON.parse(await readFile(join(dir, f), 'utf-8'));
52
+ snippets.push({ name: data.name, language: data.language, description: data.description });
53
+ } catch {}
54
+ }
55
+ return { count: snippets.length, snippets };
56
+ },
57
+ },
58
+
59
+ delete: {
60
+ required: ['name'],
61
+ handler: async ({ name }) => {
62
+ const path = snippetPath(name);
63
+ if (!existsSync(path)) throw new Error(`Snippet not found: ${name}`);
64
+ await unlink(path);
65
+ return { deleted: true, name };
66
+ },
67
+ },
68
+ };
69
+
70
+ const actionNames = Object.keys(ACTIONS);
71
+
72
+ export default {
73
+ name: 'snippet',
74
+ description: 'Code snippets: save, load, list, delete',
75
+ actions: actionNames.map(name => ({ name, required: ACTIONS[name].required })),
76
+ inputSchema: {
77
+ type: 'object',
78
+ properties: {
79
+ action: { type: 'string', enum: actionNames },
80
+ name: { type: 'string', description: 'Snippet name' },
81
+ code: { type: 'string', description: 'Code content' },
82
+ language: { type: 'string', description: 'Programming language' },
83
+ description: { type: 'string', description: 'Snippet description' },
84
+ },
85
+ required: ['action'],
86
+ },
87
+ async handler({ action, ...params }) {
88
+ const a = ACTIONS[action];
89
+ if (!a) throw new Error(`Unknown action: ${action}`);
90
+ for (const f of a.required) {
91
+ if (params[f] === undefined) throw new Error(`Missing required: ${f}`);
92
+ }
93
+ return a.handler(params);
94
+ },
95
+ };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Template Tool — project scaffolding templates (1 MCP tool, 3 actions)
3
+ * Actions: list, create, info
4
+ */
5
+ import { writeFile, mkdir, readdir, readFile } from 'fs/promises';
6
+ import { existsSync } from 'fs';
7
+ import { join } from 'path';
8
+ import { getConfig } from '../core/config.js';
9
+ import { log } from '../core/logger.js';
10
+
11
+ const TEMPLATES = {
12
+ 'node-basic': {
13
+ name: 'Node.js Basic',
14
+ description: 'Basic Node.js project with package.json',
15
+ files: {
16
+ 'package.json': '{\n "name": "{{name}}",\n "version": "1.0.0",\n "type": "module",\n "main": "index.js",\n "scripts": {\n "start": "node index.js",\n "test": "echo \\"No tests\\""\n }\n}',
17
+ 'index.js': '// {{name}}\nconsole.log("Hello!");\n',
18
+ '.gitignore': 'node_modules/\n.env\n*.log\n',
19
+ 'README.md': '# {{name}}\n\nA new project.\n',
20
+ },
21
+ },
22
+ 'python-basic': {
23
+ name: 'Python Basic',
24
+ description: 'Basic Python project with venv',
25
+ files: {
26
+ 'main.py': '# {{name}}\n\ndef main():\n print("Hello!")\n\nif __name__ == "__main__":\n main()\n',
27
+ 'requirements.txt': '',
28
+ '.gitignore': '__pycache__/\n.venv/\n*.pyc\n.env\n',
29
+ 'README.md': '# {{name}}\n\nA new Python project.\n',
30
+ },
31
+ },
32
+ 'go-basic': {
33
+ name: 'Go Basic',
34
+ description: 'Basic Go project',
35
+ files: {
36
+ 'main.go': 'package main\n\nimport "fmt"\n\nfunc main() {\n\tfmt.Println("Hello!")\n}\n',
37
+ 'go.mod': 'module {{name}}\n\ngo 1.21\n',
38
+ '.gitignore': '{{name}}\n*.exe\n',
39
+ 'README.md': '# {{name}}\n\nA new Go project.\n',
40
+ },
41
+ },
42
+ };
43
+
44
+ function applyTemplate(template, vars) {
45
+ let content = TEMPLATES[template].files;
46
+ const result = {};
47
+ for (const [file, tpl] of Object.entries(content)) {
48
+ result[file] = tpl.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] || key);
49
+ }
50
+ return result;
51
+ }
52
+
53
+ const ACTIONS = {
54
+ list: {
55
+ required: [],
56
+ handler: async () => {
57
+ const templates = Object.entries(TEMPLATES).map(([id, t]) => ({
58
+ id, name: t.name, description: t.description, files: Object.keys(t.files).length,
59
+ }));
60
+ return { count: templates.length, templates };
61
+ },
62
+ },
63
+
64
+ create: {
65
+ required: ['template', 'name'],
66
+ handler: async ({ template, name, path }) => {
67
+ if (!TEMPLATES[template]) throw new Error(`Unknown template: ${template}. Use: ${Object.keys(TEMPLATES).join(', ')}`);
68
+ const dir = path || join(getConfig().agent?.workspace || process.cwd(), name);
69
+ await mkdir(dir, { recursive: true });
70
+ const files = applyTemplate(template, { name });
71
+ for (const [file, content] of Object.entries(files)) {
72
+ await writeFile(join(dir, file), content);
73
+ }
74
+ log('template', `📁 Created ${template}: ${dir}`);
75
+ return { created: true, template, path: dir, files: Object.keys(files) };
76
+ },
77
+ },
78
+
79
+ info: {
80
+ required: ['template'],
81
+ handler: async ({ template }) => {
82
+ if (!TEMPLATES[template]) throw new Error(`Unknown template: ${template}`);
83
+ const t = TEMPLATES[template];
84
+ return { id: template, name: t.name, description: t.description, files: Object.keys(t.files) };
85
+ },
86
+ },
87
+ };
88
+
89
+ const actionNames = Object.keys(ACTIONS);
90
+
91
+ export default {
92
+ name: 'template',
93
+ description: 'Project templates: list, create, info',
94
+ actions: actionNames.map(name => ({ name, required: ACTIONS[name].required })),
95
+ inputSchema: {
96
+ type: 'object',
97
+ properties: {
98
+ action: { type: 'string', enum: actionNames },
99
+ template: { type: 'string', description: 'Template ID' },
100
+ name: { type: 'string', description: 'Project name' },
101
+ path: { type: 'string', description: 'Target directory' },
102
+ },
103
+ required: ['action'],
104
+ },
105
+ async handler({ action, ...params }) {
106
+ const a = ACTIONS[action];
107
+ if (!a) throw new Error(`Unknown action: ${action}`);
108
+ for (const f of a.required) {
109
+ if (params[f] === undefined) throw new Error(`Missing required: ${f}`);
110
+ }
111
+ return a.handler(params);
112
+ },
113
+ };