codeep 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 +201 -0
- package/README.md +576 -0
- package/dist/api/index.d.ts +8 -0
- package/dist/api/index.js +421 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.js +1406 -0
- package/dist/components/AgentProgress.d.ts +33 -0
- package/dist/components/AgentProgress.js +97 -0
- package/dist/components/Export.d.ts +8 -0
- package/dist/components/Export.js +27 -0
- package/dist/components/Help.d.ts +2 -0
- package/dist/components/Help.js +3 -0
- package/dist/components/Input.d.ts +9 -0
- package/dist/components/Input.js +89 -0
- package/dist/components/Loading.d.ts +9 -0
- package/dist/components/Loading.js +31 -0
- package/dist/components/Login.d.ts +7 -0
- package/dist/components/Login.js +77 -0
- package/dist/components/Logo.d.ts +8 -0
- package/dist/components/Logo.js +89 -0
- package/dist/components/LogoutPicker.d.ts +8 -0
- package/dist/components/LogoutPicker.js +61 -0
- package/dist/components/Message.d.ts +10 -0
- package/dist/components/Message.js +234 -0
- package/dist/components/MessageList.d.ts +10 -0
- package/dist/components/MessageList.js +8 -0
- package/dist/components/ProjectPermission.d.ts +7 -0
- package/dist/components/ProjectPermission.js +52 -0
- package/dist/components/Search.d.ts +10 -0
- package/dist/components/Search.js +30 -0
- package/dist/components/SessionPicker.d.ts +9 -0
- package/dist/components/SessionPicker.js +88 -0
- package/dist/components/Sessions.d.ts +12 -0
- package/dist/components/Sessions.js +102 -0
- package/dist/components/Settings.d.ts +7 -0
- package/dist/components/Settings.js +162 -0
- package/dist/components/Status.d.ts +2 -0
- package/dist/components/Status.js +12 -0
- package/dist/config/config.test.d.ts +1 -0
- package/dist/config/config.test.js +157 -0
- package/dist/config/index.d.ts +121 -0
- package/dist/config/index.js +555 -0
- package/dist/config/providers.d.ts +43 -0
- package/dist/config/providers.js +82 -0
- package/dist/config/providers.test.d.ts +1 -0
- package/dist/config/providers.test.js +132 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +38 -0
- package/dist/utils/agent.d.ts +37 -0
- package/dist/utils/agent.js +627 -0
- package/dist/utils/codeReview.d.ts +36 -0
- package/dist/utils/codeReview.js +390 -0
- package/dist/utils/context.d.ts +49 -0
- package/dist/utils/context.js +216 -0
- package/dist/utils/diffPreview.d.ts +57 -0
- package/dist/utils/diffPreview.js +335 -0
- package/dist/utils/export.d.ts +19 -0
- package/dist/utils/export.js +94 -0
- package/dist/utils/git.d.ts +85 -0
- package/dist/utils/git.js +399 -0
- package/dist/utils/git.test.d.ts +1 -0
- package/dist/utils/git.test.js +193 -0
- package/dist/utils/history.d.ts +93 -0
- package/dist/utils/history.js +348 -0
- package/dist/utils/interactive.d.ts +34 -0
- package/dist/utils/interactive.js +206 -0
- package/dist/utils/keychain.d.ts +17 -0
- package/dist/utils/keychain.js +160 -0
- package/dist/utils/learning.d.ts +89 -0
- package/dist/utils/learning.js +330 -0
- package/dist/utils/logger.d.ts +33 -0
- package/dist/utils/logger.js +130 -0
- package/dist/utils/project.d.ts +86 -0
- package/dist/utils/project.js +415 -0
- package/dist/utils/project.test.d.ts +1 -0
- package/dist/utils/project.test.js +212 -0
- package/dist/utils/ratelimit.d.ts +26 -0
- package/dist/utils/ratelimit.js +132 -0
- package/dist/utils/ratelimit.test.d.ts +1 -0
- package/dist/utils/ratelimit.test.js +131 -0
- package/dist/utils/retry.d.ts +28 -0
- package/dist/utils/retry.js +109 -0
- package/dist/utils/retry.test.d.ts +1 -0
- package/dist/utils/retry.test.js +163 -0
- package/dist/utils/search.d.ts +11 -0
- package/dist/utils/search.js +29 -0
- package/dist/utils/shell.d.ts +45 -0
- package/dist/utils/shell.js +242 -0
- package/dist/utils/skills.d.ts +144 -0
- package/dist/utils/skills.js +1137 -0
- package/dist/utils/smartContext.d.ts +29 -0
- package/dist/utils/smartContext.js +441 -0
- package/dist/utils/tools.d.ts +224 -0
- package/dist/utils/tools.js +731 -0
- package/dist/utils/update.d.ts +22 -0
- package/dist/utils/update.js +128 -0
- package/dist/utils/validation.d.ts +28 -0
- package/dist/utils/validation.js +141 -0
- package/dist/utils/validation.test.d.ts +1 -0
- package/dist/utils/validation.test.js +164 -0
- package/dist/utils/verify.d.ts +78 -0
- package/dist/utils/verify.js +464 -0
- package/package.json +68 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { withRetry, isNetworkError, isTimeoutError, fetchWithTimeout, } from './retry';
|
|
3
|
+
describe('retry utilities', () => {
|
|
4
|
+
describe('isNetworkError', () => {
|
|
5
|
+
it('should detect fetch TypeError', () => {
|
|
6
|
+
const error = new TypeError('Failed to fetch');
|
|
7
|
+
expect(isNetworkError(error)).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
it('should detect network TypeError', () => {
|
|
10
|
+
const error = new TypeError('Network request failed');
|
|
11
|
+
expect(isNetworkError(error)).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
it('should detect ECONNREFUSED', () => {
|
|
14
|
+
const error = { code: 'ECONNREFUSED' };
|
|
15
|
+
expect(isNetworkError(error)).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
it('should detect ENOTFOUND', () => {
|
|
18
|
+
const error = { code: 'ENOTFOUND' };
|
|
19
|
+
expect(isNetworkError(error)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
it('should detect ETIMEDOUT', () => {
|
|
22
|
+
const error = { code: 'ETIMEDOUT' };
|
|
23
|
+
expect(isNetworkError(error)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
it('should detect ENETUNREACH', () => {
|
|
26
|
+
const error = { code: 'ENETUNREACH' };
|
|
27
|
+
expect(isNetworkError(error)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
it('should detect ECONNRESET', () => {
|
|
30
|
+
const error = { code: 'ECONNRESET' };
|
|
31
|
+
expect(isNetworkError(error)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
it('should return false for non-network errors', () => {
|
|
34
|
+
expect(isNetworkError(new Error('Some other error'))).toBe(false);
|
|
35
|
+
expect(isNetworkError({ status: 400 })).toBe(false);
|
|
36
|
+
expect(isNetworkError({ code: 'EPERM' })).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('isTimeoutError', () => {
|
|
40
|
+
it('should detect AbortError', () => {
|
|
41
|
+
const error = new Error('Aborted');
|
|
42
|
+
error.name = 'AbortError';
|
|
43
|
+
expect(isTimeoutError(error)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
it('should detect ETIMEDOUT', () => {
|
|
46
|
+
const error = { code: 'ETIMEDOUT' };
|
|
47
|
+
expect(isTimeoutError(error)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it('should return false for non-timeout errors', () => {
|
|
50
|
+
expect(isTimeoutError(new Error('Some error'))).toBe(false);
|
|
51
|
+
expect(isTimeoutError({ code: 'ECONNREFUSED' })).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('withRetry', () => {
|
|
55
|
+
it('should return result on first success', async () => {
|
|
56
|
+
const fn = vi.fn().mockResolvedValue('success');
|
|
57
|
+
const result = await withRetry(fn);
|
|
58
|
+
expect(result).toBe('success');
|
|
59
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
60
|
+
});
|
|
61
|
+
it('should retry on failure and succeed', async () => {
|
|
62
|
+
const fn = vi.fn()
|
|
63
|
+
.mockRejectedValueOnce(new Error('fail 1'))
|
|
64
|
+
.mockRejectedValueOnce(new Error('fail 2'))
|
|
65
|
+
.mockResolvedValue('success');
|
|
66
|
+
const result = await withRetry(fn, { baseDelay: 10 });
|
|
67
|
+
expect(result).toBe('success');
|
|
68
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
69
|
+
});
|
|
70
|
+
it('should throw after max attempts', async () => {
|
|
71
|
+
const fn = vi.fn().mockRejectedValue(new Error('always fails'));
|
|
72
|
+
await expect(withRetry(fn, { maxAttempts: 3, baseDelay: 10 }))
|
|
73
|
+
.rejects.toThrow('always fails');
|
|
74
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
75
|
+
});
|
|
76
|
+
it('should not retry on AbortError', async () => {
|
|
77
|
+
const abortError = new Error('Aborted');
|
|
78
|
+
abortError.name = 'AbortError';
|
|
79
|
+
const fn = vi.fn().mockRejectedValue(abortError);
|
|
80
|
+
await expect(withRetry(fn, { maxAttempts: 3, baseDelay: 10 }))
|
|
81
|
+
.rejects.toThrow('Aborted');
|
|
82
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
83
|
+
});
|
|
84
|
+
it('should call onRetry callback', async () => {
|
|
85
|
+
const onRetry = vi.fn();
|
|
86
|
+
const fn = vi.fn()
|
|
87
|
+
.mockRejectedValueOnce(new Error('fail'))
|
|
88
|
+
.mockResolvedValue('success');
|
|
89
|
+
await withRetry(fn, { baseDelay: 10, onRetry });
|
|
90
|
+
expect(onRetry).toHaveBeenCalledTimes(1);
|
|
91
|
+
expect(onRetry).toHaveBeenCalledWith(1, expect.any(Error), expect.any(Number));
|
|
92
|
+
});
|
|
93
|
+
it('should respect shouldRetry option', async () => {
|
|
94
|
+
const shouldRetry = vi.fn().mockReturnValue(false);
|
|
95
|
+
const fn = vi.fn().mockRejectedValue(new Error('fail'));
|
|
96
|
+
await expect(withRetry(fn, { shouldRetry, maxAttempts: 3, baseDelay: 10 }))
|
|
97
|
+
.rejects.toThrow('fail');
|
|
98
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
99
|
+
});
|
|
100
|
+
it('should not retry on 4xx errors by default', async () => {
|
|
101
|
+
const error = { status: 400, message: 'Bad Request' };
|
|
102
|
+
const fn = vi.fn().mockRejectedValue(error);
|
|
103
|
+
await expect(withRetry(fn, { maxAttempts: 3, baseDelay: 10 }))
|
|
104
|
+
.rejects.toEqual(error);
|
|
105
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
it('should retry on 5xx errors by default', async () => {
|
|
108
|
+
const error = { status: 500, message: 'Server Error' };
|
|
109
|
+
const fn = vi.fn()
|
|
110
|
+
.mockRejectedValueOnce(error)
|
|
111
|
+
.mockResolvedValue('success');
|
|
112
|
+
const result = await withRetry(fn, { baseDelay: 10 });
|
|
113
|
+
expect(result).toBe('success');
|
|
114
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
115
|
+
});
|
|
116
|
+
it('should respect maxDelay', async () => {
|
|
117
|
+
const onRetry = vi.fn();
|
|
118
|
+
const fn = vi.fn()
|
|
119
|
+
.mockRejectedValueOnce(new Error('fail 1'))
|
|
120
|
+
.mockRejectedValueOnce(new Error('fail 2'))
|
|
121
|
+
.mockResolvedValue('success');
|
|
122
|
+
await withRetry(fn, { baseDelay: 1000, maxDelay: 100, onRetry });
|
|
123
|
+
// All delays should be capped at maxDelay
|
|
124
|
+
for (const call of onRetry.mock.calls) {
|
|
125
|
+
expect(call[2]).toBeLessThanOrEqual(100);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe('fetchWithTimeout', () => {
|
|
130
|
+
it('should make fetch request', async () => {
|
|
131
|
+
const mockResponse = new Response('ok', { status: 200 });
|
|
132
|
+
global.fetch = vi.fn().mockResolvedValue(mockResponse);
|
|
133
|
+
const response = await fetchWithTimeout('https://example.com');
|
|
134
|
+
expect(response.status).toBe(200);
|
|
135
|
+
expect(global.fetch).toHaveBeenCalledWith('https://example.com', expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
|
136
|
+
});
|
|
137
|
+
it('should abort on timeout', async () => {
|
|
138
|
+
global.fetch = vi.fn().mockImplementation(() => new Promise((_, reject) => {
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
const error = new Error('Aborted');
|
|
141
|
+
error.name = 'AbortError';
|
|
142
|
+
reject(error);
|
|
143
|
+
}, 100);
|
|
144
|
+
}));
|
|
145
|
+
await expect(fetchWithTimeout('https://example.com', { timeout: 50 }))
|
|
146
|
+
.rejects.toThrow();
|
|
147
|
+
});
|
|
148
|
+
it('should pass through fetch options', async () => {
|
|
149
|
+
const mockResponse = new Response('ok');
|
|
150
|
+
global.fetch = vi.fn().mockResolvedValue(mockResponse);
|
|
151
|
+
await fetchWithTimeout('https://example.com', {
|
|
152
|
+
method: 'POST',
|
|
153
|
+
headers: { 'Content-Type': 'application/json' },
|
|
154
|
+
body: JSON.stringify({ test: true }),
|
|
155
|
+
});
|
|
156
|
+
expect(global.fetch).toHaveBeenCalledWith('https://example.com', expect.objectContaining({
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: { 'Content-Type': 'application/json' },
|
|
159
|
+
body: JSON.stringify({ test: true }),
|
|
160
|
+
}));
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Message } from '../config/index';
|
|
2
|
+
export interface SearchResult {
|
|
3
|
+
messageIndex: number;
|
|
4
|
+
role: 'user' | 'assistant';
|
|
5
|
+
content: string;
|
|
6
|
+
matchedText: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Search through chat history for a term
|
|
10
|
+
*/
|
|
11
|
+
export declare function searchMessages(messages: Message[], searchTerm: string): SearchResult[];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search through chat history for a term
|
|
3
|
+
*/
|
|
4
|
+
export function searchMessages(messages, searchTerm) {
|
|
5
|
+
const results = [];
|
|
6
|
+
const term = searchTerm.toLowerCase();
|
|
7
|
+
messages.forEach((message, index) => {
|
|
8
|
+
const content = message.content.toLowerCase();
|
|
9
|
+
if (content.includes(term)) {
|
|
10
|
+
// Find the matching snippet with context
|
|
11
|
+
const matchIndex = content.indexOf(term);
|
|
12
|
+
const start = Math.max(0, matchIndex - 50);
|
|
13
|
+
const end = Math.min(content.length, matchIndex + term.length + 50);
|
|
14
|
+
let snippet = message.content.substring(start, end);
|
|
15
|
+
// Add ellipsis if truncated
|
|
16
|
+
if (start > 0)
|
|
17
|
+
snippet = '...' + snippet;
|
|
18
|
+
if (end < content.length)
|
|
19
|
+
snippet = snippet + '...';
|
|
20
|
+
results.push({
|
|
21
|
+
messageIndex: index,
|
|
22
|
+
role: message.role,
|
|
23
|
+
content: message.content,
|
|
24
|
+
matchedText: snippet,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
return results;
|
|
29
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell command execution utilities with safety checks
|
|
3
|
+
*/
|
|
4
|
+
export interface CommandResult {
|
|
5
|
+
success: boolean;
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
exitCode: number;
|
|
9
|
+
duration: number;
|
|
10
|
+
command: string;
|
|
11
|
+
args: string[];
|
|
12
|
+
}
|
|
13
|
+
export interface CommandOptions {
|
|
14
|
+
cwd?: string;
|
|
15
|
+
timeout?: number;
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
projectRoot?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validate if a command is safe to execute
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateCommand(command: string, args: string[], options?: CommandOptions): {
|
|
23
|
+
valid: boolean;
|
|
24
|
+
reason?: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Execute a shell command with safety checks
|
|
28
|
+
*/
|
|
29
|
+
export declare function executeCommand(command: string, args?: string[], options?: CommandOptions): CommandResult;
|
|
30
|
+
/**
|
|
31
|
+
* Execute a command and return only stdout if successful
|
|
32
|
+
*/
|
|
33
|
+
export declare function execSimple(command: string, args?: string[], options?: CommandOptions): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Check if a command exists in PATH
|
|
36
|
+
*/
|
|
37
|
+
export declare function commandExists(command: string): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Get list of allowed commands
|
|
40
|
+
*/
|
|
41
|
+
export declare function getAllowedCommands(): string[];
|
|
42
|
+
/**
|
|
43
|
+
* Format command result for display
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatCommandResult(result: CommandResult): string;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell command execution utilities with safety checks
|
|
3
|
+
*/
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { resolve, relative, isAbsolute } from 'path';
|
|
6
|
+
import { existsSync } from 'fs';
|
|
7
|
+
// Dangerous command patterns that should never be executed
|
|
8
|
+
const BLOCKED_COMMANDS = new Set([
|
|
9
|
+
'sudo',
|
|
10
|
+
'su',
|
|
11
|
+
'chmod',
|
|
12
|
+
'chown',
|
|
13
|
+
'mkfs',
|
|
14
|
+
'fdisk',
|
|
15
|
+
'dd',
|
|
16
|
+
'mount',
|
|
17
|
+
'umount',
|
|
18
|
+
'systemctl',
|
|
19
|
+
'service',
|
|
20
|
+
'shutdown',
|
|
21
|
+
'reboot',
|
|
22
|
+
'init',
|
|
23
|
+
'kill',
|
|
24
|
+
'killall',
|
|
25
|
+
'pkill',
|
|
26
|
+
]);
|
|
27
|
+
// Dangerous argument patterns
|
|
28
|
+
const BLOCKED_PATTERNS = [
|
|
29
|
+
/rm\s+(-[rf]+\s+)*\/(?![\w])/, // rm -rf / (root)
|
|
30
|
+
/rm\s+(-[rf]+\s+)*~/, // rm home directory
|
|
31
|
+
/>\s*\/etc\//, // redirect to /etc
|
|
32
|
+
/>\s*\/usr\//, // redirect to /usr
|
|
33
|
+
/>\s*\/var\//, // redirect to /var
|
|
34
|
+
/>\s*\/bin\//, // redirect to /bin
|
|
35
|
+
/>\s*\/sbin\//, // redirect to /sbin
|
|
36
|
+
/curl.*\|\s*(ba)?sh/, // curl pipe to shell
|
|
37
|
+
/wget.*\|\s*(ba)?sh/, // wget pipe to shell
|
|
38
|
+
/eval\s+/, // eval command
|
|
39
|
+
/`.*`/, // command substitution in backticks
|
|
40
|
+
/\$\(.*\)/, // command substitution
|
|
41
|
+
];
|
|
42
|
+
// Allowed commands for agent mode (whitelist approach for extra safety)
|
|
43
|
+
const ALLOWED_COMMANDS = new Set([
|
|
44
|
+
// Package managers
|
|
45
|
+
'npm', 'npx', 'yarn', 'pnpm', 'bun',
|
|
46
|
+
'pip', 'pip3', 'poetry', 'pipenv',
|
|
47
|
+
'cargo', 'rustup',
|
|
48
|
+
'go',
|
|
49
|
+
'composer',
|
|
50
|
+
'gem', 'bundle',
|
|
51
|
+
'brew',
|
|
52
|
+
// Build tools
|
|
53
|
+
'make', 'cmake', 'gradle', 'mvn',
|
|
54
|
+
'tsc', 'esbuild', 'vite', 'webpack', 'rollup',
|
|
55
|
+
// Version control
|
|
56
|
+
'git',
|
|
57
|
+
// File operations (safe ones)
|
|
58
|
+
'ls', 'cat', 'head', 'tail', 'grep', 'find', 'wc',
|
|
59
|
+
'mkdir', 'touch', 'cp', 'mv', 'rm', 'rmdir',
|
|
60
|
+
// Node.js
|
|
61
|
+
'node', 'deno',
|
|
62
|
+
// Python
|
|
63
|
+
'python', 'python3',
|
|
64
|
+
// PHP
|
|
65
|
+
'php', 'composer', 'phpunit', 'artisan',
|
|
66
|
+
// Testing
|
|
67
|
+
'jest', 'vitest', 'pytest', 'mocha',
|
|
68
|
+
// Linting/Formatting
|
|
69
|
+
'eslint', 'prettier', 'black', 'rustfmt',
|
|
70
|
+
// Other common tools
|
|
71
|
+
'echo', 'pwd', 'which', 'env', 'date',
|
|
72
|
+
'curl', 'wget', // allowed but patterns checked
|
|
73
|
+
'tar', 'unzip', 'zip',
|
|
74
|
+
// HTTP tools
|
|
75
|
+
'http', 'https',
|
|
76
|
+
]);
|
|
77
|
+
/**
|
|
78
|
+
* Validate if a command is safe to execute
|
|
79
|
+
*/
|
|
80
|
+
export function validateCommand(command, args, options) {
|
|
81
|
+
// Check if command is in blocked list
|
|
82
|
+
if (BLOCKED_COMMANDS.has(command)) {
|
|
83
|
+
return { valid: false, reason: `Command '${command}' is not allowed for security reasons` };
|
|
84
|
+
}
|
|
85
|
+
// Check if command is in allowed list (whitelist mode)
|
|
86
|
+
if (!ALLOWED_COMMANDS.has(command)) {
|
|
87
|
+
return { valid: false, reason: `Command '${command}' is not in the allowed list` };
|
|
88
|
+
}
|
|
89
|
+
// Check full command string against dangerous patterns
|
|
90
|
+
const fullCommand = `${command} ${args.join(' ')}`;
|
|
91
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
92
|
+
if (pattern.test(fullCommand)) {
|
|
93
|
+
return { valid: false, reason: `Command contains blocked pattern: ${pattern}` };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Validate paths in arguments stay within project
|
|
97
|
+
if (options?.projectRoot) {
|
|
98
|
+
for (const arg of args) {
|
|
99
|
+
// Skip flags
|
|
100
|
+
if (arg.startsWith('-'))
|
|
101
|
+
continue;
|
|
102
|
+
// Check if argument looks like a path
|
|
103
|
+
if (arg.includes('/') || arg.includes('\\')) {
|
|
104
|
+
const absolutePath = isAbsolute(arg) ? arg : resolve(options.cwd || options.projectRoot, arg);
|
|
105
|
+
const relativePath = relative(options.projectRoot, absolutePath);
|
|
106
|
+
// Path escapes project root
|
|
107
|
+
if (relativePath.startsWith('..')) {
|
|
108
|
+
return { valid: false, reason: `Path '${arg}' is outside project directory` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Special validation for rm command
|
|
114
|
+
if (command === 'rm') {
|
|
115
|
+
const hasRecursive = args.some(a => a.includes('r'));
|
|
116
|
+
const hasForce = args.some(a => a.includes('f'));
|
|
117
|
+
if (hasRecursive && hasForce) {
|
|
118
|
+
// rm -rf requires extra validation
|
|
119
|
+
const paths = args.filter(a => !a.startsWith('-'));
|
|
120
|
+
if (paths.length === 0) {
|
|
121
|
+
return { valid: false, reason: 'rm -rf without specific paths is not allowed' };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { valid: true };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Execute a shell command with safety checks
|
|
129
|
+
*/
|
|
130
|
+
export function executeCommand(command, args = [], options) {
|
|
131
|
+
const startTime = Date.now();
|
|
132
|
+
const cwd = options?.cwd || process.cwd();
|
|
133
|
+
const timeout = options?.timeout || 60000; // Default 1 minute
|
|
134
|
+
// Validate command first
|
|
135
|
+
const validation = validateCommand(command, args, options);
|
|
136
|
+
if (!validation.valid) {
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
stdout: '',
|
|
140
|
+
stderr: validation.reason || 'Command validation failed',
|
|
141
|
+
exitCode: -1,
|
|
142
|
+
duration: 0,
|
|
143
|
+
command,
|
|
144
|
+
args,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
// Ensure cwd exists
|
|
148
|
+
if (!existsSync(cwd)) {
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
stdout: '',
|
|
152
|
+
stderr: `Working directory does not exist: ${cwd}`,
|
|
153
|
+
exitCode: -1,
|
|
154
|
+
duration: 0,
|
|
155
|
+
command,
|
|
156
|
+
args,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const spawnOptions = {
|
|
160
|
+
cwd,
|
|
161
|
+
timeout,
|
|
162
|
+
encoding: 'utf-8',
|
|
163
|
+
env: {
|
|
164
|
+
...process.env,
|
|
165
|
+
...options?.env,
|
|
166
|
+
},
|
|
167
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
const result = spawnSync(command, args, spawnOptions);
|
|
171
|
+
const duration = Date.now() - startTime;
|
|
172
|
+
// Handle timeout
|
|
173
|
+
if (result.signal === 'SIGTERM') {
|
|
174
|
+
return {
|
|
175
|
+
success: false,
|
|
176
|
+
stdout: result.stdout?.toString() || '',
|
|
177
|
+
stderr: `Command timed out after ${timeout}ms`,
|
|
178
|
+
exitCode: -1,
|
|
179
|
+
duration,
|
|
180
|
+
command,
|
|
181
|
+
args,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
success: result.status === 0,
|
|
186
|
+
stdout: result.stdout?.toString() || '',
|
|
187
|
+
stderr: result.stderr?.toString() || '',
|
|
188
|
+
exitCode: result.status ?? -1,
|
|
189
|
+
duration,
|
|
190
|
+
command,
|
|
191
|
+
args,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const duration = Date.now() - startTime;
|
|
196
|
+
const err = error;
|
|
197
|
+
return {
|
|
198
|
+
success: false,
|
|
199
|
+
stdout: '',
|
|
200
|
+
stderr: err.message || 'Unknown error executing command',
|
|
201
|
+
exitCode: -1,
|
|
202
|
+
duration,
|
|
203
|
+
command,
|
|
204
|
+
args,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Execute a command and return only stdout if successful
|
|
210
|
+
*/
|
|
211
|
+
export function execSimple(command, args = [], options) {
|
|
212
|
+
const result = executeCommand(command, args, options);
|
|
213
|
+
return result.success ? result.stdout.trim() : null;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Check if a command exists in PATH
|
|
217
|
+
*/
|
|
218
|
+
export function commandExists(command) {
|
|
219
|
+
const result = spawnSync('which', [command], { encoding: 'utf-8' });
|
|
220
|
+
return result.status === 0;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Get list of allowed commands
|
|
224
|
+
*/
|
|
225
|
+
export function getAllowedCommands() {
|
|
226
|
+
return Array.from(ALLOWED_COMMANDS).sort();
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Format command result for display
|
|
230
|
+
*/
|
|
231
|
+
export function formatCommandResult(result) {
|
|
232
|
+
const status = result.success ? '✓' : '✗';
|
|
233
|
+
const cmd = `${result.command} ${result.args.join(' ')}`.trim();
|
|
234
|
+
let output = `${status} ${cmd} (${result.duration}ms, exit ${result.exitCode})`;
|
|
235
|
+
if (result.stdout) {
|
|
236
|
+
output += `\n stdout: ${result.stdout.slice(0, 500)}${result.stdout.length > 500 ? '...' : ''}`;
|
|
237
|
+
}
|
|
238
|
+
if (result.stderr && !result.success) {
|
|
239
|
+
output += `\n stderr: ${result.stderr.slice(0, 500)}${result.stderr.length > 500 ? '...' : ''}`;
|
|
240
|
+
}
|
|
241
|
+
return output;
|
|
242
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills System - predefined workflows and commands
|
|
3
|
+
*/
|
|
4
|
+
import { ProjectContext } from './project';
|
|
5
|
+
export interface SkillStep {
|
|
6
|
+
type: 'prompt' | 'command' | 'confirm' | 'notify' | 'agent';
|
|
7
|
+
content: string;
|
|
8
|
+
optional?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface SkillParameter {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
default?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Skill {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
shortcut?: string;
|
|
20
|
+
category: SkillCategory;
|
|
21
|
+
steps: SkillStep[];
|
|
22
|
+
parameters?: SkillParameter[];
|
|
23
|
+
requiresWriteAccess?: boolean;
|
|
24
|
+
requiresGit?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export type SkillCategory = 'git' | 'testing' | 'documentation' | 'refactoring' | 'debugging' | 'deployment' | 'generation' | 'devops' | 'custom';
|
|
27
|
+
export interface SkillExecutionResult {
|
|
28
|
+
success: boolean;
|
|
29
|
+
output: string;
|
|
30
|
+
steps: {
|
|
31
|
+
step: SkillStep;
|
|
32
|
+
result: string;
|
|
33
|
+
success: boolean;
|
|
34
|
+
}[];
|
|
35
|
+
}
|
|
36
|
+
export interface SkillChain {
|
|
37
|
+
skills: string[];
|
|
38
|
+
stopOnError: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get all built-in skills
|
|
42
|
+
*/
|
|
43
|
+
export declare function getBuiltInSkills(): Skill[];
|
|
44
|
+
/**
|
|
45
|
+
* Load custom skills from disk
|
|
46
|
+
*/
|
|
47
|
+
export declare function loadCustomSkills(): Skill[];
|
|
48
|
+
/**
|
|
49
|
+
* Get all skills (built-in + custom)
|
|
50
|
+
*/
|
|
51
|
+
export declare function getAllSkills(): Skill[];
|
|
52
|
+
/**
|
|
53
|
+
* Find skill by name or shortcut
|
|
54
|
+
*/
|
|
55
|
+
export declare function findSkill(nameOrShortcut: string): Skill | null;
|
|
56
|
+
/**
|
|
57
|
+
* Parse skill chain (e.g., "commit+push" → ["commit", "push"])
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseSkillChain(input: string): SkillChain | null;
|
|
60
|
+
/**
|
|
61
|
+
* Parse skill parameters from args string
|
|
62
|
+
* Supports: "value" for first param, key=value, key="value with spaces"
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseSkillArgs(args: string, skill: Skill): Record<string, string>;
|
|
65
|
+
/**
|
|
66
|
+
* Interpolate parameters into skill step content
|
|
67
|
+
*/
|
|
68
|
+
export declare function interpolateParams(content: string, params: Record<string, string>): string;
|
|
69
|
+
/**
|
|
70
|
+
* Save a custom skill
|
|
71
|
+
*/
|
|
72
|
+
export declare function saveCustomSkill(skill: Skill): void;
|
|
73
|
+
/**
|
|
74
|
+
* Delete a custom skill
|
|
75
|
+
*/
|
|
76
|
+
export declare function deleteCustomSkill(name: string): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Generate prompt for a skill with parameters
|
|
79
|
+
*/
|
|
80
|
+
export declare function generateSkillPrompt(skill: Skill, context: ProjectContext, additionalContext?: string, params?: Record<string, string>): string;
|
|
81
|
+
/**
|
|
82
|
+
* Get skill steps that need execution
|
|
83
|
+
*/
|
|
84
|
+
export declare function getExecutableSteps(skill: Skill): SkillStep[];
|
|
85
|
+
/**
|
|
86
|
+
* Format skills list for display
|
|
87
|
+
*/
|
|
88
|
+
export declare function formatSkillsList(skills: Skill[]): string;
|
|
89
|
+
/**
|
|
90
|
+
* Format skill help
|
|
91
|
+
*/
|
|
92
|
+
export declare function formatSkillHelp(skill: Skill): string;
|
|
93
|
+
/**
|
|
94
|
+
* Create a custom skill from template
|
|
95
|
+
*/
|
|
96
|
+
export declare function createSkillTemplate(name: string): Skill;
|
|
97
|
+
/**
|
|
98
|
+
* Wizard step for creating custom skills
|
|
99
|
+
*/
|
|
100
|
+
export interface WizardStep {
|
|
101
|
+
field: 'name' | 'description' | 'shortcut' | 'step_type' | 'step_content' | 'add_another' | 'done';
|
|
102
|
+
prompt: string;
|
|
103
|
+
validate?: (input: string) => string | null;
|
|
104
|
+
}
|
|
105
|
+
export declare const WIZARD_STEPS: WizardStep[];
|
|
106
|
+
/**
|
|
107
|
+
* Parse skill definition from YAML-like string
|
|
108
|
+
*/
|
|
109
|
+
export declare function parseSkillDefinition(content: string): Skill | null;
|
|
110
|
+
/**
|
|
111
|
+
* Get skill categories summary
|
|
112
|
+
*/
|
|
113
|
+
export declare function getSkillsSummary(): Record<SkillCategory, number>;
|
|
114
|
+
/**
|
|
115
|
+
* Search skills by keyword
|
|
116
|
+
*/
|
|
117
|
+
export declare function searchSkills(query: string): Skill[];
|
|
118
|
+
/**
|
|
119
|
+
* Track skill usage
|
|
120
|
+
*/
|
|
121
|
+
export declare function trackSkillUsage(skillName: string, success?: boolean): void;
|
|
122
|
+
/**
|
|
123
|
+
* Get recently used skills
|
|
124
|
+
*/
|
|
125
|
+
export declare function getRecentSkills(limit?: number): string[];
|
|
126
|
+
/**
|
|
127
|
+
* Get most frequently used skills
|
|
128
|
+
*/
|
|
129
|
+
export declare function getMostUsedSkills(limit?: number): Array<{
|
|
130
|
+
name: string;
|
|
131
|
+
count: number;
|
|
132
|
+
}>;
|
|
133
|
+
/**
|
|
134
|
+
* Get skill usage statistics
|
|
135
|
+
*/
|
|
136
|
+
export declare function getSkillStats(): {
|
|
137
|
+
totalUsage: number;
|
|
138
|
+
uniqueSkills: number;
|
|
139
|
+
successRate: number;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Clear skill usage history
|
|
143
|
+
*/
|
|
144
|
+
export declare function clearSkillHistory(): void;
|