@stan-chen/simple-cli 0.2.1 → 0.2.3
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/README.md +55 -238
- package/dist/claw/jit.d.ts +5 -0
- package/dist/claw/jit.js +138 -0
- package/dist/claw/management.d.ts +3 -0
- package/dist/claw/management.js +107 -0
- package/dist/cli.js +306 -61
- package/dist/commands/git/commit.js +2 -1
- package/dist/commands/index.js +3 -2
- package/dist/context.js +13 -3
- package/dist/lib/agent.d.ts +4 -3
- package/dist/lib/agent.js +49 -17
- package/dist/lib/git.js +6 -1
- package/dist/lib/shim.d.ts +4 -0
- package/dist/lib/shim.js +30 -0
- package/dist/lib/ui.js +25 -0
- package/dist/mcp/manager.js +5 -1
- package/dist/prompts/provider.js +1 -0
- package/dist/providers/index.d.ts +21 -5
- package/dist/providers/index.js +75 -64
- package/dist/providers/multi.d.ts +2 -1
- package/dist/registry.d.ts +5 -0
- package/dist/registry.js +86 -22
- package/dist/repoMap.js +18 -18
- package/dist/router.js +21 -11
- package/dist/skills.js +10 -10
- package/dist/swarm/worker.d.ts +2 -0
- package/dist/swarm/worker.js +85 -15
- package/dist/tools/analyze_file.d.ts +16 -0
- package/dist/tools/analyze_file.js +43 -0
- package/dist/tools/clawBrain.d.ts +23 -0
- package/dist/tools/clawBrain.js +136 -0
- package/dist/tools/claw_brain.d.ts +23 -0
- package/dist/tools/claw_brain.js +139 -0
- package/dist/tools/deleteFile.d.ts +19 -0
- package/dist/tools/deleteFile.js +36 -0
- package/dist/tools/delete_file.d.ts +19 -0
- package/dist/tools/delete_file.js +36 -0
- package/dist/tools/fileOps.d.ts +22 -0
- package/dist/tools/fileOps.js +43 -0
- package/dist/tools/file_ops.d.ts +22 -0
- package/dist/tools/file_ops.js +43 -0
- package/dist/tools/grep.d.ts +2 -2
- package/dist/tools/linter.js +85 -27
- package/dist/tools/list_dir.d.ts +29 -0
- package/dist/tools/list_dir.js +50 -0
- package/dist/tools/organizer.d.ts +1 -0
- package/dist/tools/organizer.js +65 -0
- package/dist/tools/read_files.d.ts +25 -0
- package/dist/tools/read_files.js +31 -0
- package/dist/tools/reload_tools.d.ts +11 -0
- package/dist/tools/reload_tools.js +22 -0
- package/dist/tools/run_command.d.ts +32 -0
- package/dist/tools/run_command.js +103 -0
- package/dist/tools/scheduler.d.ts +25 -0
- package/dist/tools/scheduler.js +65 -0
- package/dist/tools/writeFiles.js +1 -1
- package/dist/tools/write_files.d.ts +84 -0
- package/dist/tools/write_files.js +91 -0
- package/dist/tools/write_to_file.d.ts +15 -0
- package/dist/tools/write_to_file.js +21 -0
- package/package.json +84 -78
package/dist/tools/linter.js
CHANGED
|
@@ -6,6 +6,7 @@ import { z } from 'zod';
|
|
|
6
6
|
import { execSync, spawnSync } from 'child_process';
|
|
7
7
|
import { readFileSync, existsSync } from 'fs';
|
|
8
8
|
import { extname, basename } from 'path';
|
|
9
|
+
import { platform } from 'os';
|
|
9
10
|
// Input schema
|
|
10
11
|
export const inputSchema = z.object({
|
|
11
12
|
path: z.string().describe('Path to file to lint'),
|
|
@@ -40,6 +41,30 @@ function detectLanguage(filePath) {
|
|
|
40
41
|
const ext = extname(filePath).toLowerCase();
|
|
41
42
|
return LANGUAGE_MAP[ext] || null;
|
|
42
43
|
}
|
|
44
|
+
// Detect if a binary exists in PATH
|
|
45
|
+
function detectBinary(bin) {
|
|
46
|
+
try {
|
|
47
|
+
const isWin = platform() === 'win32';
|
|
48
|
+
const cmd = isWin ? 'where' : 'which';
|
|
49
|
+
execSync(`${cmd} ${bin}`, { stdio: 'ignore' });
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Get correct python binary
|
|
57
|
+
function getPythonBinary() {
|
|
58
|
+
if (detectBinary('python3'))
|
|
59
|
+
return 'python3';
|
|
60
|
+
if (detectBinary('python'))
|
|
61
|
+
return 'python';
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// Platform agnostic /dev/null
|
|
65
|
+
function getNullDevice() {
|
|
66
|
+
return platform() === 'win32' ? 'NUL' : '/dev/null';
|
|
67
|
+
}
|
|
43
68
|
// Parse error output for line numbers
|
|
44
69
|
function parseErrors(output, filePath) {
|
|
45
70
|
const errors = [];
|
|
@@ -95,9 +120,20 @@ function parseErrors(output, filePath) {
|
|
|
95
120
|
function lintPython(filePath, code) {
|
|
96
121
|
const errors = [];
|
|
97
122
|
let output = '';
|
|
123
|
+
const pythonBin = getPythonBinary();
|
|
124
|
+
if (!pythonBin) {
|
|
125
|
+
return {
|
|
126
|
+
file: filePath,
|
|
127
|
+
language: 'python',
|
|
128
|
+
errors: [],
|
|
129
|
+
warnings: [],
|
|
130
|
+
passed: true, // skipped
|
|
131
|
+
output: 'Python binary not found, skipping lint.',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
98
134
|
// Try Python syntax check
|
|
99
135
|
try {
|
|
100
|
-
const result = spawnSync(
|
|
136
|
+
const result = spawnSync(pythonBin, ['-m', 'py_compile', filePath], {
|
|
101
137
|
encoding: 'utf-8',
|
|
102
138
|
timeout: 10000,
|
|
103
139
|
});
|
|
@@ -109,7 +145,7 @@ function lintPython(filePath, code) {
|
|
|
109
145
|
catch {
|
|
110
146
|
// Fall back to basic syntax check
|
|
111
147
|
try {
|
|
112
|
-
execSync(
|
|
148
|
+
execSync(`${pythonBin} -c "compile(open('${filePath}').read(), '${filePath}', 'exec')"`, {
|
|
113
149
|
encoding: 'utf-8',
|
|
114
150
|
timeout: 10000,
|
|
115
151
|
});
|
|
@@ -123,7 +159,7 @@ function lintPython(filePath, code) {
|
|
|
123
159
|
}
|
|
124
160
|
// Try flake8 for additional checks
|
|
125
161
|
try {
|
|
126
|
-
const result = spawnSync(
|
|
162
|
+
const result = spawnSync(pythonBin, ['-m', 'flake8', '--select=E9,F821,F823,F831,F406,F407,F701,F702,F704,F706', filePath], {
|
|
127
163
|
encoding: 'utf-8',
|
|
128
164
|
timeout: 10000,
|
|
129
165
|
});
|
|
@@ -156,6 +192,7 @@ function lintJavaScript(filePath, code, isTypeScript) {
|
|
|
156
192
|
const result = spawnSync('npx', ['tsc', '--noEmit', '--skipLibCheck', filePath], {
|
|
157
193
|
encoding: 'utf-8',
|
|
158
194
|
timeout: 30000,
|
|
195
|
+
shell: platform() === 'win32' // Required for npx on Windows
|
|
159
196
|
});
|
|
160
197
|
if (result.status !== 0) {
|
|
161
198
|
output = (result.stdout || '') + (result.stderr || '');
|
|
@@ -184,6 +221,7 @@ function lintJavaScript(filePath, code, isTypeScript) {
|
|
|
184
221
|
const result = spawnSync('npx', ['eslint', '--format', 'compact', filePath], {
|
|
185
222
|
encoding: 'utf-8',
|
|
186
223
|
timeout: 30000,
|
|
224
|
+
shell: platform() === 'win32'
|
|
187
225
|
});
|
|
188
226
|
if (result.stdout) {
|
|
189
227
|
output += '\n' + result.stdout;
|
|
@@ -204,6 +242,11 @@ function lintJavaScript(filePath, code, isTypeScript) {
|
|
|
204
242
|
}
|
|
205
243
|
// Go linting
|
|
206
244
|
function lintGo(filePath) {
|
|
245
|
+
if (!detectBinary('go')) {
|
|
246
|
+
return {
|
|
247
|
+
file: filePath, language: 'go', errors: [], warnings: [], passed: true, output: 'Go not found'
|
|
248
|
+
};
|
|
249
|
+
}
|
|
207
250
|
const errors = [];
|
|
208
251
|
let output = '';
|
|
209
252
|
try {
|
|
@@ -232,10 +275,15 @@ function lintGo(filePath) {
|
|
|
232
275
|
}
|
|
233
276
|
// Rust linting
|
|
234
277
|
function lintRust(filePath) {
|
|
278
|
+
if (!detectBinary('rustc')) {
|
|
279
|
+
return {
|
|
280
|
+
file: filePath, language: 'rust', errors: [], warnings: [], passed: true, output: 'Rustc not found'
|
|
281
|
+
};
|
|
282
|
+
}
|
|
235
283
|
const errors = [];
|
|
236
284
|
let output = '';
|
|
237
285
|
try {
|
|
238
|
-
const result = spawnSync('rustc', ['--emit=metadata', '-o',
|
|
286
|
+
const result = spawnSync('rustc', ['--emit=metadata', '-o', getNullDevice(), filePath], {
|
|
239
287
|
encoding: 'utf-8',
|
|
240
288
|
timeout: 30000,
|
|
241
289
|
});
|
|
@@ -262,34 +310,44 @@ function lintRust(filePath) {
|
|
|
262
310
|
function lintShell(filePath) {
|
|
263
311
|
const errors = [];
|
|
264
312
|
let output = '';
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
313
|
+
// Try shellcheck first (cross platform)
|
|
314
|
+
if (detectBinary('shellcheck')) {
|
|
315
|
+
try {
|
|
316
|
+
const result = spawnSync('shellcheck', ['-f', 'gcc', filePath], {
|
|
317
|
+
encoding: 'utf-8',
|
|
318
|
+
timeout: 10000,
|
|
319
|
+
});
|
|
320
|
+
if (result.stdout) {
|
|
321
|
+
output += '\n' + result.stdout;
|
|
322
|
+
errors.push(...parseErrors(result.stdout, filePath));
|
|
323
|
+
}
|
|
273
324
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (e instanceof Error) {
|
|
277
|
-
output = e.message;
|
|
325
|
+
catch {
|
|
326
|
+
// shellcheck error
|
|
278
327
|
}
|
|
279
328
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
output
|
|
288
|
-
|
|
329
|
+
else if (detectBinary('bash')) {
|
|
330
|
+
// Fallback to bash -n, works on Windows if Git Bash / WSL is in path, or Linux/Mac
|
|
331
|
+
try {
|
|
332
|
+
const result = spawnSync('bash', ['-n', filePath], {
|
|
333
|
+
encoding: 'utf-8',
|
|
334
|
+
timeout: 10000,
|
|
335
|
+
});
|
|
336
|
+
output = result.stderr + result.stdout;
|
|
337
|
+
if (result.status !== 0) {
|
|
338
|
+
errors.push(...parseErrors(output, filePath));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (e) {
|
|
342
|
+
if (e instanceof Error) {
|
|
343
|
+
output = e.message;
|
|
344
|
+
}
|
|
289
345
|
}
|
|
290
346
|
}
|
|
291
|
-
|
|
292
|
-
|
|
347
|
+
else {
|
|
348
|
+
return {
|
|
349
|
+
file: filePath, language: 'shell', errors: [], warnings: [], passed: true, output: 'No shell linter found (bash/shellcheck missing)'
|
|
350
|
+
};
|
|
293
351
|
}
|
|
294
352
|
return {
|
|
295
353
|
file: filePath,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: listDir
|
|
3
|
+
* List the contents of a directory (files and subdirectories)
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export declare const name = "list_dir";
|
|
7
|
+
export declare const description = "List the contents of a directory, showing file sizes and directory indicators";
|
|
8
|
+
export declare const permission: "read";
|
|
9
|
+
export declare const schema: z.ZodObject<{
|
|
10
|
+
path: z.ZodOptional<z.ZodString>;
|
|
11
|
+
recursive: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
12
|
+
depth: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
13
|
+
}, "strip", z.ZodTypeAny, {
|
|
14
|
+
recursive: boolean;
|
|
15
|
+
depth: number;
|
|
16
|
+
path?: string | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
recursive?: boolean | undefined;
|
|
19
|
+
path?: string | undefined;
|
|
20
|
+
depth?: number | undefined;
|
|
21
|
+
}>;
|
|
22
|
+
interface EntryInfo {
|
|
23
|
+
name: string;
|
|
24
|
+
isDir: boolean;
|
|
25
|
+
size?: number;
|
|
26
|
+
childrenCount?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare const execute: (args: Record<string, unknown>) => Promise<EntryInfo[]>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: listDir
|
|
3
|
+
* List the contents of a directory (files and subdirectories)
|
|
4
|
+
*/
|
|
5
|
+
import { readdir, stat } from 'fs/promises';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
export const name = 'list_dir';
|
|
9
|
+
export const description = 'List the contents of a directory, showing file sizes and directory indicators';
|
|
10
|
+
export const permission = 'read';
|
|
11
|
+
export const schema = z.object({
|
|
12
|
+
path: z.string().optional().describe('Directory path to list (default: current directory)'),
|
|
13
|
+
recursive: z.boolean().optional().default(false).describe('Whether to list recursively'),
|
|
14
|
+
depth: z.number().optional().default(1).describe('Max depth for recursive listing')
|
|
15
|
+
});
|
|
16
|
+
export const execute = async (args) => {
|
|
17
|
+
const parsed = schema.parse(args);
|
|
18
|
+
const rootPath = parsed.path || process.cwd();
|
|
19
|
+
try {
|
|
20
|
+
const entries = await readdir(rootPath, { withFileTypes: true });
|
|
21
|
+
const result = [];
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
// Basic ignores
|
|
24
|
+
if (['node_modules', '.git', 'dist', 'build'].includes(entry.name))
|
|
25
|
+
continue;
|
|
26
|
+
const fullPath = join(rootPath, entry.name);
|
|
27
|
+
const isDir = entry.isDirectory();
|
|
28
|
+
let entryInfo = {
|
|
29
|
+
name: entry.name,
|
|
30
|
+
isDir
|
|
31
|
+
};
|
|
32
|
+
if (!isDir) {
|
|
33
|
+
try {
|
|
34
|
+
const s = await stat(fullPath);
|
|
35
|
+
entryInfo.size = s.size;
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
}
|
|
39
|
+
else if (parsed.recursive && (parsed.depth || 0) > 0) {
|
|
40
|
+
// We don't actually do recursion here to keep it simple and flat
|
|
41
|
+
// but it's a placeholder for future if needed.
|
|
42
|
+
}
|
|
43
|
+
result.push(entryInfo);
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw new Error(`Failed to list directory ${rootPath}: ${error instanceof Error ? error.message : error}`);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runDeterministicOrganizer(targetDir: string): void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
export function runDeterministicOrganizer(targetDir) {
|
|
5
|
+
try {
|
|
6
|
+
console.log(pc.yellow('⚙ Running deterministic organizer fallback...'));
|
|
7
|
+
const photosDir = join(targetDir, 'Photos');
|
|
8
|
+
const docsDir = join(targetDir, 'Documents');
|
|
9
|
+
const trashDir = join(targetDir, 'Trash');
|
|
10
|
+
const expensesPath = join(targetDir, 'Expenses.csv');
|
|
11
|
+
if (!fs.existsSync(photosDir))
|
|
12
|
+
fs.mkdirSync(photosDir, { recursive: true });
|
|
13
|
+
if (!fs.existsSync(docsDir))
|
|
14
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
15
|
+
if (!fs.existsSync(trashDir))
|
|
16
|
+
fs.mkdirSync(trashDir, { recursive: true });
|
|
17
|
+
const entries = fs.readdirSync(targetDir);
|
|
18
|
+
for (const f of entries) {
|
|
19
|
+
const src = join(targetDir, f);
|
|
20
|
+
try {
|
|
21
|
+
const stat = fs.statSync(src);
|
|
22
|
+
if (!stat.isFile())
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const lower = f.toLowerCase();
|
|
29
|
+
try {
|
|
30
|
+
if (lower.endsWith('.jpg') || lower.endsWith('.png')) {
|
|
31
|
+
console.log(pc.dim(` -> Moving ${f} to Photos`));
|
|
32
|
+
fs.renameSync(src, join(photosDir, f));
|
|
33
|
+
}
|
|
34
|
+
else if (lower.endsWith('.pdf') || lower.endsWith('.docx')) {
|
|
35
|
+
console.log(pc.dim(` -> Moving ${f} to Documents`));
|
|
36
|
+
fs.renameSync(src, join(docsDir, f));
|
|
37
|
+
}
|
|
38
|
+
else if (lower.endsWith('.exe') || lower.endsWith('.msi')) {
|
|
39
|
+
console.log(pc.dim(` -> Moving ${f} to Trash`));
|
|
40
|
+
fs.renameSync(src, join(trashDir, f));
|
|
41
|
+
}
|
|
42
|
+
else if ((lower.includes('receipt') || lower.startsWith('receipt')) && lower.endsWith('.txt')) {
|
|
43
|
+
const content = fs.readFileSync(src, 'utf-8');
|
|
44
|
+
const m = content.match(/Total:\s*\$?([0-9]+(?:\.[0-9]{1,2})?)/i);
|
|
45
|
+
if (m) {
|
|
46
|
+
const line = `${new Date().toISOString().split('T')[0]},${m[1]},${f}\n`;
|
|
47
|
+
if (!fs.existsSync(expensesPath))
|
|
48
|
+
fs.appendFileSync(expensesPath, 'Date,Amount,Description\n');
|
|
49
|
+
fs.appendFileSync(expensesPath, line);
|
|
50
|
+
console.log(pc.dim(` -> Logged receipt: ${f}`));
|
|
51
|
+
}
|
|
52
|
+
console.log(pc.dim(` -> Moving ${f} to Documents`));
|
|
53
|
+
fs.renameSync(src, join(docsDir, f));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.error(pc.red(` ! Failed to process ${f}:`), err instanceof Error ? err.message : err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
console.log(pc.green('✔ Deterministic organizer finished'));
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
console.error('Fallback organizer failed:', err);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: readFiles
|
|
3
|
+
* Read contents of one or more files
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export declare const name = "read_files";
|
|
7
|
+
export declare const description = "Read the contents of one or more files from the filesystem";
|
|
8
|
+
export declare const permission: "read";
|
|
9
|
+
export declare const schema: z.ZodObject<{
|
|
10
|
+
paths: z.ZodArray<z.ZodString, "many">;
|
|
11
|
+
encoding: z.ZodOptional<z.ZodString>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
paths: string[];
|
|
14
|
+
encoding?: string | undefined;
|
|
15
|
+
}, {
|
|
16
|
+
paths: string[];
|
|
17
|
+
encoding?: string | undefined;
|
|
18
|
+
}>;
|
|
19
|
+
interface FileResult {
|
|
20
|
+
path: string;
|
|
21
|
+
content?: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare const execute: (args: Record<string, unknown>) => Promise<FileResult[]>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: readFiles
|
|
3
|
+
* Read contents of one or more files
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from 'fs/promises';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
export const name = 'read_files';
|
|
8
|
+
export const description = 'Read the contents of one or more files from the filesystem';
|
|
9
|
+
export const permission = 'read';
|
|
10
|
+
export const schema = z.object({
|
|
11
|
+
paths: z.array(z.string()).describe('Array of file paths to read'),
|
|
12
|
+
encoding: z.string().optional().describe('File encoding (default: utf-8)')
|
|
13
|
+
});
|
|
14
|
+
export const execute = async (args) => {
|
|
15
|
+
const parsed = schema.parse(args);
|
|
16
|
+
const encoding = (parsed.encoding || 'utf-8');
|
|
17
|
+
const results = [];
|
|
18
|
+
for (const path of parsed.paths) {
|
|
19
|
+
try {
|
|
20
|
+
const content = await readFile(path, encoding);
|
|
21
|
+
results.push({ path, content });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
results.push({
|
|
25
|
+
path,
|
|
26
|
+
error: error instanceof Error ? error.message : 'Unknown error'
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return results;
|
|
31
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: reloadTools
|
|
3
|
+
* Reloads all tools, including built-in, MCP, and project-specific skills.
|
|
4
|
+
* Use this after creating a new skill file in the 'skills/' directory.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
export declare const name = "reload_tools";
|
|
8
|
+
export declare const description = "Reload all tools to pick up new skills or changes to existing tools";
|
|
9
|
+
export declare const permission: "read";
|
|
10
|
+
export declare const schema: z.ZodObject<{}, "strip", z.ZodTypeAny, {}, {}>;
|
|
11
|
+
export declare const execute: (_args: Record<string, unknown>) => Promise<string>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: reloadTools
|
|
3
|
+
* Reloads all tools, including built-in, MCP, and project-specific skills.
|
|
4
|
+
* Use this after creating a new skill file in the 'skills/' directory.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { getContextManager } from '../context.js';
|
|
8
|
+
export const name = 'reload_tools';
|
|
9
|
+
export const description = 'Reload all tools to pick up new skills or changes to existing tools';
|
|
10
|
+
export const permission = 'read';
|
|
11
|
+
export const schema = z.object({});
|
|
12
|
+
export const execute = async (_args) => {
|
|
13
|
+
const ctx = getContextManager();
|
|
14
|
+
await ctx.initialize();
|
|
15
|
+
const tools = ctx.getTools();
|
|
16
|
+
const summary = Array.from(tools.values()).reduce((acc, tool) => {
|
|
17
|
+
const source = tool.source || 'unknown';
|
|
18
|
+
acc[source] = (acc[source] || 0) + 1;
|
|
19
|
+
return acc;
|
|
20
|
+
}, {});
|
|
21
|
+
return `All tools reloaded successfully.\n\nSummary:\n- Built-in: ${summary.builtin || 0}\n- Project Skills: ${summary.project || 0}\n- MCP Tools: ${summary.mcp || 0}\n\nNewly discovered/updated tools are now ready for use.`;
|
|
22
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: runCommand
|
|
3
|
+
* Execute shell commands in a sandboxed environment
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export declare const name = "run_command";
|
|
7
|
+
export declare const description = "Execute a shell command with timeout and environment restrictions";
|
|
8
|
+
export declare const permission: "execute";
|
|
9
|
+
export declare const schema: z.ZodObject<{
|
|
10
|
+
command: z.ZodString;
|
|
11
|
+
cwd: z.ZodOptional<z.ZodString>;
|
|
12
|
+
timeout: z.ZodOptional<z.ZodNumber>;
|
|
13
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
command: string;
|
|
16
|
+
env?: Record<string, string> | undefined;
|
|
17
|
+
timeout?: number | undefined;
|
|
18
|
+
cwd?: string | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
command: string;
|
|
21
|
+
env?: Record<string, string> | undefined;
|
|
22
|
+
timeout?: number | undefined;
|
|
23
|
+
cwd?: string | undefined;
|
|
24
|
+
}>;
|
|
25
|
+
interface CommandResult {
|
|
26
|
+
exitCode: number;
|
|
27
|
+
stdout: string;
|
|
28
|
+
stderr: string;
|
|
29
|
+
timedOut: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare const execute: (args: Record<string, unknown>) => Promise<CommandResult>;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: runCommand
|
|
3
|
+
* Execute shell commands in a sandboxed environment
|
|
4
|
+
*/
|
|
5
|
+
import { spawn } from 'child_process';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
export const name = 'run_command';
|
|
8
|
+
export const description = 'Execute a shell command with timeout and environment restrictions';
|
|
9
|
+
export const permission = 'execute';
|
|
10
|
+
export const schema = z.object({
|
|
11
|
+
command: z.string().describe('The shell command to execute'),
|
|
12
|
+
cwd: z.string().optional().describe('Working directory for the command'),
|
|
13
|
+
timeout: z.number().optional().describe('Timeout in milliseconds (default: 30000)'),
|
|
14
|
+
env: z.record(z.string()).optional().describe('Additional environment variables')
|
|
15
|
+
});
|
|
16
|
+
// Restricted environment - remove sensitive variables
|
|
17
|
+
const createSafeEnv = (additionalEnv) => {
|
|
18
|
+
// Start from a minimal safe copy to avoid leaking secrets
|
|
19
|
+
const rawEnv = { ...process.env };
|
|
20
|
+
const sensitivePattern = /(API_KEY$|_KEY$|TOKEN|SECRET|PASSWORD|VLM_API|OPENAI_API|GEMINI_API|NPM_ACCESS_TOKEN)/i;
|
|
21
|
+
const env = {};
|
|
22
|
+
for (const [k, v] of Object.entries(rawEnv)) {
|
|
23
|
+
if (!k)
|
|
24
|
+
continue;
|
|
25
|
+
if (sensitivePattern.test(k))
|
|
26
|
+
continue; // skip sensitive keys
|
|
27
|
+
env[k] = v;
|
|
28
|
+
}
|
|
29
|
+
// Add any additional env vars provided explicitly, but still filter them
|
|
30
|
+
if (additionalEnv) {
|
|
31
|
+
for (const [k, v] of Object.entries(additionalEnv)) {
|
|
32
|
+
if (!k)
|
|
33
|
+
continue;
|
|
34
|
+
if (sensitivePattern.test(k))
|
|
35
|
+
continue; // never allow sensitive keys
|
|
36
|
+
env[k] = v;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return env;
|
|
40
|
+
};
|
|
41
|
+
export const execute = async (args) => {
|
|
42
|
+
const parsed = schema.parse(args);
|
|
43
|
+
const timeout = parsed.timeout || 30000;
|
|
44
|
+
const env = createSafeEnv(parsed.env);
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
let stdout = '';
|
|
47
|
+
let stderr = '';
|
|
48
|
+
let timedOut = false;
|
|
49
|
+
// Quick heuristic: detect obviously unbalanced quotes and treat as syntax error
|
|
50
|
+
const singleQuotes = (parsed.command.match(/'/g) || []).length;
|
|
51
|
+
const doubleQuotes = (parsed.command.match(/"/g) || []).length;
|
|
52
|
+
if (singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0) {
|
|
53
|
+
return resolve({ exitCode: 1, stdout: '', stderr: 'Invalid shell syntax (unbalanced quotes)', timedOut: false });
|
|
54
|
+
}
|
|
55
|
+
const child = spawn(parsed.command, {
|
|
56
|
+
shell: true,
|
|
57
|
+
cwd: parsed.cwd || process.cwd(),
|
|
58
|
+
env,
|
|
59
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
60
|
+
});
|
|
61
|
+
const timer = setTimeout(() => {
|
|
62
|
+
timedOut = true;
|
|
63
|
+
child.kill('SIGTERM');
|
|
64
|
+
setTimeout(() => child.kill('SIGKILL'), 1000);
|
|
65
|
+
}, timeout);
|
|
66
|
+
child.stdout?.on('data', (data) => {
|
|
67
|
+
stdout += data.toString();
|
|
68
|
+
// Limit output size
|
|
69
|
+
if (stdout.length > 100000) {
|
|
70
|
+
stdout = stdout.slice(0, 100000) + '\n... (output truncated)';
|
|
71
|
+
child.kill('SIGTERM');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
child.stderr?.on('data', (data) => {
|
|
75
|
+
stderr += data.toString();
|
|
76
|
+
if (stderr.length > 50000) {
|
|
77
|
+
stderr = stderr.slice(0, 50000) + '\n... (output truncated)';
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
child.on('close', (code) => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
// If command failed but provided no stderr, include the command for diagnostics
|
|
83
|
+
if ((code ?? 0) !== 0 && !stderr) {
|
|
84
|
+
stderr = `Command failed: ${parsed.command}`;
|
|
85
|
+
}
|
|
86
|
+
resolve({
|
|
87
|
+
exitCode: code ?? 1,
|
|
88
|
+
stdout: stdout.trim(),
|
|
89
|
+
stderr: stderr.trim(),
|
|
90
|
+
timedOut
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
child.on('error', (error) => {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
resolve({
|
|
96
|
+
exitCode: 1,
|
|
97
|
+
stdout: '',
|
|
98
|
+
stderr: error.message,
|
|
99
|
+
timedOut: false
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: scheduler
|
|
3
|
+
* Manage Ghost Tasks (scheduled executions) using crontab or schtasks
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
export declare const name = "scheduler";
|
|
7
|
+
export declare const description = "Schedule a task to run automatically at intervals. CRITICAL for fulfilling \"Every X minutes\" or \"Every day\" intents. Tasks run in --ghost mode.";
|
|
8
|
+
export declare const permission: "execute";
|
|
9
|
+
export declare const schema: z.ZodObject<{
|
|
10
|
+
intent: z.ZodString;
|
|
11
|
+
schedule: z.ZodString;
|
|
12
|
+
name: z.ZodOptional<z.ZodString>;
|
|
13
|
+
targetDir: z.ZodOptional<z.ZodString>;
|
|
14
|
+
}, "strip", z.ZodTypeAny, {
|
|
15
|
+
intent: string;
|
|
16
|
+
schedule: string;
|
|
17
|
+
name?: string | undefined;
|
|
18
|
+
targetDir?: string | undefined;
|
|
19
|
+
}, {
|
|
20
|
+
intent: string;
|
|
21
|
+
schedule: string;
|
|
22
|
+
name?: string | undefined;
|
|
23
|
+
targetDir?: string | undefined;
|
|
24
|
+
}>;
|
|
25
|
+
export declare const execute: (args: Record<string, unknown>) => Promise<string>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: scheduler
|
|
3
|
+
* Manage Ghost Tasks (scheduled executions) using crontab or schtasks
|
|
4
|
+
*/
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { platform } from 'os';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { resolve, join } from 'path';
|
|
9
|
+
export const name = 'scheduler';
|
|
10
|
+
export const description = 'Schedule a task to run automatically at intervals. CRITICAL for fulfilling "Every X minutes" or "Every day" intents. Tasks run in --ghost mode.';
|
|
11
|
+
export const permission = 'execute';
|
|
12
|
+
export const schema = z.object({
|
|
13
|
+
intent: z.string().describe('The task description/intent to execute'),
|
|
14
|
+
schedule: z.string().describe('Cron expression (e.g. "0 * * * *") or interval in minutes'),
|
|
15
|
+
name: z.string().optional().describe('Unique name for the task'),
|
|
16
|
+
targetDir: z.string().optional().describe('Working directory for the task')
|
|
17
|
+
});
|
|
18
|
+
export const execute = async (args) => {
|
|
19
|
+
const { intent, schedule, name: taskName, targetDir } = schema.parse(args);
|
|
20
|
+
const isWindows = platform() === 'win32';
|
|
21
|
+
const cwd = resolve(targetDir || process.cwd());
|
|
22
|
+
const id = taskName || `ghost-${Date.now()}`;
|
|
23
|
+
// Path to simple-cli. We assume it's installed globally or use the current one.
|
|
24
|
+
// For local dev, we use the absolute path to dist/cli.js
|
|
25
|
+
const cliPath = resolve(join(process.cwd(), 'dist', 'cli.js'));
|
|
26
|
+
const command = `node "${cliPath}" "${cwd}" -claw "${intent}" --ghost --yolo`;
|
|
27
|
+
try {
|
|
28
|
+
if (isWindows) {
|
|
29
|
+
const interval = parseInt(schedule) || 60;
|
|
30
|
+
const escapedCommand = command.replace(/"/g, '\\"');
|
|
31
|
+
try {
|
|
32
|
+
execSync(`schtasks /create /sc minute /mo ${interval} /tn "${id}" /tr "${escapedCommand}" /f`);
|
|
33
|
+
return `Task scheduled on Windows: ${id} Every ${interval} minutes.`;
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
return `Error scheduling on Windows: ${e.message}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
try {
|
|
41
|
+
// Determine cron expression
|
|
42
|
+
let cronExpr = schedule;
|
|
43
|
+
if (!schedule.includes('*')) {
|
|
44
|
+
const mins = parseInt(schedule) || 60;
|
|
45
|
+
cronExpr = `*/${mins} * * * *`;
|
|
46
|
+
}
|
|
47
|
+
const cronEntry = `${cronExpr} ${command} # ${id}`;
|
|
48
|
+
let currentCron = '';
|
|
49
|
+
try {
|
|
50
|
+
currentCron = execSync('crontab -l', { encoding: 'utf-8' });
|
|
51
|
+
}
|
|
52
|
+
catch (e) { /* ignore empty crontab */ }
|
|
53
|
+
const newCron = currentCron.trim() + '\n' + cronEntry + '\n';
|
|
54
|
+
execSync(`echo "${newCron}" | crontab -`);
|
|
55
|
+
return `Task scheduled on Linux/Mac: ${id} with schedule ${cronExpr}`;
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
return `Error scheduling on Linux/Mac: ${e.message}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw new Error(`Failed to schedule task: ${error instanceof Error ? error.message : error}`);
|
|
64
|
+
}
|
|
65
|
+
};
|
package/dist/tools/writeFiles.js
CHANGED
|
@@ -44,7 +44,7 @@ export const execute = async (args) => {
|
|
|
44
44
|
for (const { search, replace } of file.searchReplace) {
|
|
45
45
|
if (content.includes(search)) {
|
|
46
46
|
// Apply replacement to all occurrences
|
|
47
|
-
const newContent = content.
|
|
47
|
+
const newContent = content.replace(search, replace);
|
|
48
48
|
if (newContent !== content) {
|
|
49
49
|
content = newContent;
|
|
50
50
|
changesApplied++;
|