@compilr-dev/agents 0.0.1
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 +1277 -0
- package/dist/agent.d.ts +1272 -0
- package/dist/agent.js +1912 -0
- package/dist/anchors/builtin.d.ts +24 -0
- package/dist/anchors/builtin.js +61 -0
- package/dist/anchors/index.d.ts +6 -0
- package/dist/anchors/index.js +5 -0
- package/dist/anchors/manager.d.ts +115 -0
- package/dist/anchors/manager.js +412 -0
- package/dist/anchors/types.d.ts +168 -0
- package/dist/anchors/types.js +10 -0
- package/dist/context/index.d.ts +12 -0
- package/dist/context/index.js +10 -0
- package/dist/context/manager.d.ts +224 -0
- package/dist/context/manager.js +770 -0
- package/dist/context/types.d.ts +377 -0
- package/dist/context/types.js +7 -0
- package/dist/costs/index.d.ts +8 -0
- package/dist/costs/index.js +7 -0
- package/dist/costs/tracker.d.ts +121 -0
- package/dist/costs/tracker.js +295 -0
- package/dist/costs/types.d.ts +157 -0
- package/dist/costs/types.js +8 -0
- package/dist/errors.d.ts +178 -0
- package/dist/errors.js +249 -0
- package/dist/guardrails/builtin.d.ts +27 -0
- package/dist/guardrails/builtin.js +223 -0
- package/dist/guardrails/index.d.ts +6 -0
- package/dist/guardrails/index.js +5 -0
- package/dist/guardrails/manager.d.ts +117 -0
- package/dist/guardrails/manager.js +288 -0
- package/dist/guardrails/types.d.ts +159 -0
- package/dist/guardrails/types.js +7 -0
- package/dist/hooks/index.d.ts +31 -0
- package/dist/hooks/index.js +29 -0
- package/dist/hooks/manager.d.ts +147 -0
- package/dist/hooks/manager.js +600 -0
- package/dist/hooks/types.d.ts +368 -0
- package/dist/hooks/types.js +12 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +73 -0
- package/dist/mcp/client.d.ts +93 -0
- package/dist/mcp/client.js +287 -0
- package/dist/mcp/errors.d.ts +60 -0
- package/dist/mcp/errors.js +78 -0
- package/dist/mcp/index.d.ts +43 -0
- package/dist/mcp/index.js +45 -0
- package/dist/mcp/manager.d.ts +120 -0
- package/dist/mcp/manager.js +276 -0
- package/dist/mcp/tools.d.ts +54 -0
- package/dist/mcp/tools.js +99 -0
- package/dist/mcp/types.d.ts +150 -0
- package/dist/mcp/types.js +40 -0
- package/dist/memory/index.d.ts +8 -0
- package/dist/memory/index.js +7 -0
- package/dist/memory/loader.d.ts +114 -0
- package/dist/memory/loader.js +463 -0
- package/dist/memory/types.d.ts +182 -0
- package/dist/memory/types.js +8 -0
- package/dist/messages/index.d.ts +82 -0
- package/dist/messages/index.js +155 -0
- package/dist/permissions/index.d.ts +5 -0
- package/dist/permissions/index.js +4 -0
- package/dist/permissions/manager.d.ts +125 -0
- package/dist/permissions/manager.js +379 -0
- package/dist/permissions/types.d.ts +162 -0
- package/dist/permissions/types.js +7 -0
- package/dist/providers/claude.d.ts +90 -0
- package/dist/providers/claude.js +348 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +11 -0
- package/dist/providers/mock.d.ts +133 -0
- package/dist/providers/mock.js +204 -0
- package/dist/providers/types.d.ts +168 -0
- package/dist/providers/types.js +4 -0
- package/dist/rate-limit/index.d.ts +45 -0
- package/dist/rate-limit/index.js +47 -0
- package/dist/rate-limit/limiter.d.ts +104 -0
- package/dist/rate-limit/limiter.js +326 -0
- package/dist/rate-limit/provider-wrapper.d.ts +112 -0
- package/dist/rate-limit/provider-wrapper.js +201 -0
- package/dist/rate-limit/retry.d.ts +108 -0
- package/dist/rate-limit/retry.js +287 -0
- package/dist/rate-limit/types.d.ts +181 -0
- package/dist/rate-limit/types.js +22 -0
- package/dist/rehearsal/file-analyzer.d.ts +22 -0
- package/dist/rehearsal/file-analyzer.js +351 -0
- package/dist/rehearsal/git-analyzer.d.ts +22 -0
- package/dist/rehearsal/git-analyzer.js +472 -0
- package/dist/rehearsal/index.d.ts +35 -0
- package/dist/rehearsal/index.js +36 -0
- package/dist/rehearsal/manager.d.ts +100 -0
- package/dist/rehearsal/manager.js +290 -0
- package/dist/rehearsal/types.d.ts +235 -0
- package/dist/rehearsal/types.js +8 -0
- package/dist/skills/index.d.ts +160 -0
- package/dist/skills/index.js +282 -0
- package/dist/state/agent-state.d.ts +41 -0
- package/dist/state/agent-state.js +88 -0
- package/dist/state/checkpointer.d.ts +110 -0
- package/dist/state/checkpointer.js +362 -0
- package/dist/state/errors.d.ts +66 -0
- package/dist/state/errors.js +88 -0
- package/dist/state/index.d.ts +35 -0
- package/dist/state/index.js +37 -0
- package/dist/state/serializer.d.ts +55 -0
- package/dist/state/serializer.js +172 -0
- package/dist/state/types.d.ts +312 -0
- package/dist/state/types.js +14 -0
- package/dist/tools/builtin/bash-output.d.ts +61 -0
- package/dist/tools/builtin/bash-output.js +90 -0
- package/dist/tools/builtin/bash.d.ts +150 -0
- package/dist/tools/builtin/bash.js +354 -0
- package/dist/tools/builtin/edit.d.ts +50 -0
- package/dist/tools/builtin/edit.js +215 -0
- package/dist/tools/builtin/glob.d.ts +62 -0
- package/dist/tools/builtin/glob.js +244 -0
- package/dist/tools/builtin/grep.d.ts +74 -0
- package/dist/tools/builtin/grep.js +363 -0
- package/dist/tools/builtin/index.d.ts +44 -0
- package/dist/tools/builtin/index.js +69 -0
- package/dist/tools/builtin/kill-shell.d.ts +44 -0
- package/dist/tools/builtin/kill-shell.js +80 -0
- package/dist/tools/builtin/read-file.d.ts +57 -0
- package/dist/tools/builtin/read-file.js +184 -0
- package/dist/tools/builtin/shell-manager.d.ts +176 -0
- package/dist/tools/builtin/shell-manager.js +337 -0
- package/dist/tools/builtin/task.d.ts +202 -0
- package/dist/tools/builtin/task.js +350 -0
- package/dist/tools/builtin/todo.d.ts +207 -0
- package/dist/tools/builtin/todo.js +453 -0
- package/dist/tools/builtin/utils.d.ts +27 -0
- package/dist/tools/builtin/utils.js +70 -0
- package/dist/tools/builtin/web-fetch.d.ts +96 -0
- package/dist/tools/builtin/web-fetch.js +290 -0
- package/dist/tools/builtin/write-file.d.ts +54 -0
- package/dist/tools/builtin/write-file.js +147 -0
- package/dist/tools/define.d.ts +60 -0
- package/dist/tools/define.js +65 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +37 -0
- package/dist/tools/registry.d.ts +79 -0
- package/dist/tools/registry.js +151 -0
- package/dist/tools/types.d.ts +59 -0
- package/dist/tools/types.js +4 -0
- package/dist/tracing/hooks.d.ts +58 -0
- package/dist/tracing/hooks.js +377 -0
- package/dist/tracing/index.d.ts +51 -0
- package/dist/tracing/index.js +55 -0
- package/dist/tracing/logging.d.ts +78 -0
- package/dist/tracing/logging.js +310 -0
- package/dist/tracing/manager.d.ts +160 -0
- package/dist/tracing/manager.js +468 -0
- package/dist/tracing/otel.d.ts +102 -0
- package/dist/tracing/otel.js +246 -0
- package/dist/tracing/types.d.ts +346 -0
- package/dist/tracing/types.js +38 -0
- package/dist/utils/index.d.ts +23 -0
- package/dist/utils/index.js +44 -0
- package/package.json +79 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read File Tool - Read contents of a file from the filesystem
|
|
3
|
+
*/
|
|
4
|
+
import { readFile as fsReadFile, stat } from 'node:fs/promises';
|
|
5
|
+
import { defineTool, createSuccessResult, createErrorResult } from '../define.js';
|
|
6
|
+
import { isNodeError, isExtensionAllowed } from './utils.js';
|
|
7
|
+
/**
|
|
8
|
+
* Default maximum file size (10MB)
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
11
|
+
/**
|
|
12
|
+
* Default maximum content size returned to agent (100KB)
|
|
13
|
+
* Files larger than this are truncated to prevent memory bloat
|
|
14
|
+
*/
|
|
15
|
+
const DEFAULT_MAX_CONTENT_SIZE = 100 * 1024;
|
|
16
|
+
/**
|
|
17
|
+
* Read file tool definition
|
|
18
|
+
*/
|
|
19
|
+
export const readFileTool = defineTool({
|
|
20
|
+
name: 'read_file',
|
|
21
|
+
description: 'Read the contents of a file. Returns the file content as text. ' +
|
|
22
|
+
'Use maxLines and startLine to read specific portions of large files.',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
path: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Absolute or relative path to the file',
|
|
29
|
+
},
|
|
30
|
+
encoding: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
description: 'File encoding (default: utf-8)',
|
|
33
|
+
},
|
|
34
|
+
maxLines: {
|
|
35
|
+
type: 'number',
|
|
36
|
+
description: 'Maximum number of lines to read',
|
|
37
|
+
},
|
|
38
|
+
startLine: {
|
|
39
|
+
type: 'number',
|
|
40
|
+
description: 'Line number to start reading from (1-indexed)',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
required: ['path'],
|
|
44
|
+
},
|
|
45
|
+
execute: executeReadFile,
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* Execute the read_file tool
|
|
49
|
+
*/
|
|
50
|
+
async function executeReadFile(input, options) {
|
|
51
|
+
const maxFileSize = options?.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
52
|
+
const maxContentSize = options?.maxContentSize ?? DEFAULT_MAX_CONTENT_SIZE;
|
|
53
|
+
const truncateIfLarge = options?.truncateIfLarge ?? true;
|
|
54
|
+
try {
|
|
55
|
+
// Pre-check: Get file size before reading
|
|
56
|
+
const stats = await stat(input.path);
|
|
57
|
+
const fileSize = stats.size;
|
|
58
|
+
// Check if file is too large to read at all
|
|
59
|
+
if (fileSize > maxFileSize) {
|
|
60
|
+
return createErrorResult(`File too large to read: ${formatSize(fileSize)} (max: ${formatSize(maxFileSize)}). ` +
|
|
61
|
+
`Consider using maxLines/startLine to read portions.`);
|
|
62
|
+
}
|
|
63
|
+
const encoding = input.encoding ?? 'utf-8';
|
|
64
|
+
let content = await fsReadFile(input.path, { encoding });
|
|
65
|
+
let truncated = false;
|
|
66
|
+
let totalLines;
|
|
67
|
+
let linesReturned;
|
|
68
|
+
// Handle line-based reading
|
|
69
|
+
if (input.maxLines !== undefined || input.startLine !== undefined) {
|
|
70
|
+
const lines = content.split('\n');
|
|
71
|
+
totalLines = lines.length;
|
|
72
|
+
// Validate and convert startLine to 0-indexed (minimum 1)
|
|
73
|
+
const startLine = Math.max(1, input.startLine ?? 1);
|
|
74
|
+
const startIdx = startLine - 1;
|
|
75
|
+
const endIdx = input.maxLines !== undefined ? startIdx + input.maxLines : lines.length;
|
|
76
|
+
content = lines.slice(startIdx, endIdx).join('\n');
|
|
77
|
+
linesReturned = Math.min(endIdx, lines.length) - startIdx;
|
|
78
|
+
}
|
|
79
|
+
// Truncate content if too large for agent context
|
|
80
|
+
if (content.length > maxContentSize) {
|
|
81
|
+
if (truncateIfLarge) {
|
|
82
|
+
truncated = true;
|
|
83
|
+
const lines = content.split('\n');
|
|
84
|
+
totalLines = totalLines ?? lines.length;
|
|
85
|
+
// Keep first and last portions
|
|
86
|
+
const headSize = Math.floor(maxContentSize * 0.7);
|
|
87
|
+
const tailSize = Math.floor(maxContentSize * 0.2);
|
|
88
|
+
const head = content.slice(0, headSize);
|
|
89
|
+
const tail = content.slice(-tailSize);
|
|
90
|
+
const omitted = content.length - headSize - tailSize;
|
|
91
|
+
content = `${head}\n\n... [${formatSize(omitted)} truncated] ...\n\n${tail}`;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
return createErrorResult(`File content too large: ${formatSize(content.length)} (max: ${formatSize(maxContentSize)}). ` +
|
|
95
|
+
`Use maxLines/startLine to read portions.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Return result with metadata
|
|
99
|
+
return createSuccessResult({
|
|
100
|
+
content,
|
|
101
|
+
path: input.path,
|
|
102
|
+
size: fileSize,
|
|
103
|
+
truncated,
|
|
104
|
+
...(totalLines !== undefined && { totalLines }),
|
|
105
|
+
...(linesReturned !== undefined && { linesReturned }),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (isNodeError(error)) {
|
|
110
|
+
switch (error.code) {
|
|
111
|
+
case 'ENOENT':
|
|
112
|
+
return createErrorResult(`File not found: ${input.path}`);
|
|
113
|
+
case 'EACCES':
|
|
114
|
+
return createErrorResult(`Permission denied: ${input.path}`);
|
|
115
|
+
case 'EISDIR':
|
|
116
|
+
return createErrorResult(`Path is a directory: ${input.path}`);
|
|
117
|
+
default:
|
|
118
|
+
return createErrorResult(`Failed to read file: ${error.message}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return createErrorResult(error instanceof Error ? error.message : String(error));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Format byte size for human readability
|
|
126
|
+
*/
|
|
127
|
+
function formatSize(bytes) {
|
|
128
|
+
if (bytes < 1024)
|
|
129
|
+
return `${String(bytes)} bytes`;
|
|
130
|
+
if (bytes < 1024 * 1024)
|
|
131
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
132
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Factory function to create a read_file tool with custom options
|
|
136
|
+
*/
|
|
137
|
+
export function createReadFileTool(options) {
|
|
138
|
+
return defineTool({
|
|
139
|
+
name: 'read_file',
|
|
140
|
+
description: 'Read the contents of a file. Returns the file content as text. ' +
|
|
141
|
+
'Use maxLines and startLine to read specific portions of large files.',
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
path: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
description: 'Absolute or relative path to the file',
|
|
148
|
+
},
|
|
149
|
+
encoding: {
|
|
150
|
+
type: 'string',
|
|
151
|
+
description: 'File encoding (default: utf-8)',
|
|
152
|
+
},
|
|
153
|
+
maxLines: {
|
|
154
|
+
type: 'number',
|
|
155
|
+
description: 'Maximum number of lines to read',
|
|
156
|
+
},
|
|
157
|
+
startLine: {
|
|
158
|
+
type: 'number',
|
|
159
|
+
description: 'Line number to start reading from (1-indexed)',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
required: ['path'],
|
|
163
|
+
},
|
|
164
|
+
execute: async (input) => {
|
|
165
|
+
const { baseDir, allowedExtensions, maxFileSize, maxContentSize, truncateIfLarge } = options ?? {};
|
|
166
|
+
let filePath = input.path;
|
|
167
|
+
// Resolve relative paths
|
|
168
|
+
if (baseDir && !filePath.startsWith('/')) {
|
|
169
|
+
filePath = `${baseDir}/${filePath}`;
|
|
170
|
+
}
|
|
171
|
+
// Check extension if restricted
|
|
172
|
+
if (allowedExtensions && allowedExtensions.length > 0) {
|
|
173
|
+
if (!isExtensionAllowed(filePath, allowedExtensions)) {
|
|
174
|
+
return createErrorResult(`File extension not allowed. Allowed: ${allowedExtensions.join(', ')}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return executeReadFile({ ...input, path: filePath }, {
|
|
178
|
+
maxFileSize,
|
|
179
|
+
maxContentSize,
|
|
180
|
+
truncateIfLarge,
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell Manager - Track and manage background shell processes
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Status of a background shell
|
|
6
|
+
*/
|
|
7
|
+
export type ShellStatus = 'running' | 'completed' | 'failed' | 'killed';
|
|
8
|
+
/**
|
|
9
|
+
* Information about a background shell
|
|
10
|
+
*/
|
|
11
|
+
export interface BackgroundShell {
|
|
12
|
+
/**
|
|
13
|
+
* Unique identifier for this shell
|
|
14
|
+
*/
|
|
15
|
+
id: string;
|
|
16
|
+
/**
|
|
17
|
+
* The command being executed
|
|
18
|
+
*/
|
|
19
|
+
command: string;
|
|
20
|
+
/**
|
|
21
|
+
* Current status
|
|
22
|
+
*/
|
|
23
|
+
status: ShellStatus;
|
|
24
|
+
/**
|
|
25
|
+
* When the shell was started
|
|
26
|
+
*/
|
|
27
|
+
startTime: Date;
|
|
28
|
+
/**
|
|
29
|
+
* When the shell finished (if completed/failed/killed)
|
|
30
|
+
*/
|
|
31
|
+
endTime?: Date;
|
|
32
|
+
/**
|
|
33
|
+
* Exit code (if completed or failed)
|
|
34
|
+
*/
|
|
35
|
+
exitCode?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Working directory
|
|
38
|
+
*/
|
|
39
|
+
cwd?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Output from a background shell
|
|
43
|
+
*/
|
|
44
|
+
export interface ShellOutput {
|
|
45
|
+
/**
|
|
46
|
+
* Shell ID
|
|
47
|
+
*/
|
|
48
|
+
id: string;
|
|
49
|
+
/**
|
|
50
|
+
* Current status
|
|
51
|
+
*/
|
|
52
|
+
status: ShellStatus;
|
|
53
|
+
/**
|
|
54
|
+
* Standard output (new since last read)
|
|
55
|
+
*/
|
|
56
|
+
stdout: string;
|
|
57
|
+
/**
|
|
58
|
+
* Standard error (new since last read)
|
|
59
|
+
*/
|
|
60
|
+
stderr: string;
|
|
61
|
+
/**
|
|
62
|
+
* Whether there is more output buffered
|
|
63
|
+
*/
|
|
64
|
+
hasMore: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Exit code (if completed)
|
|
67
|
+
*/
|
|
68
|
+
exitCode?: number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Options for ShellManager
|
|
72
|
+
*/
|
|
73
|
+
export interface ShellManagerOptions {
|
|
74
|
+
/**
|
|
75
|
+
* Maximum buffer size per stream in bytes
|
|
76
|
+
*/
|
|
77
|
+
maxBufferSize?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Maximum number of concurrent shells
|
|
80
|
+
*/
|
|
81
|
+
maxShells?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Shell to use (default: /bin/bash)
|
|
84
|
+
*/
|
|
85
|
+
shell?: string;
|
|
86
|
+
/**
|
|
87
|
+
* Auto-cleanup completed shells after this many milliseconds
|
|
88
|
+
* Set to 0 to disable auto-cleanup
|
|
89
|
+
*/
|
|
90
|
+
autoCleanupMs?: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Manages background shell processes
|
|
94
|
+
*/
|
|
95
|
+
export declare class ShellManager {
|
|
96
|
+
private readonly shells;
|
|
97
|
+
private readonly maxBufferSize;
|
|
98
|
+
private readonly maxShells;
|
|
99
|
+
private readonly shell;
|
|
100
|
+
private readonly autoCleanupMs;
|
|
101
|
+
private readonly cleanupTimers;
|
|
102
|
+
constructor(options?: ShellManagerOptions);
|
|
103
|
+
/**
|
|
104
|
+
* Spawn a new background shell
|
|
105
|
+
*/
|
|
106
|
+
spawn(command: string, options?: {
|
|
107
|
+
cwd?: string;
|
|
108
|
+
env?: Record<string, string>;
|
|
109
|
+
}): string;
|
|
110
|
+
/**
|
|
111
|
+
* Get output from a shell (only new output since last read)
|
|
112
|
+
*/
|
|
113
|
+
getOutput(id: string, filter?: RegExp): ShellOutput | null;
|
|
114
|
+
/**
|
|
115
|
+
* Get all output from a shell (including previously read)
|
|
116
|
+
*/
|
|
117
|
+
getAllOutput(id: string): ShellOutput | null;
|
|
118
|
+
/**
|
|
119
|
+
* Kill a background shell
|
|
120
|
+
*/
|
|
121
|
+
kill(id: string): boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Verify and sync shell status with actual process state.
|
|
124
|
+
* Call this to ensure status reflects reality after potential race conditions.
|
|
125
|
+
*/
|
|
126
|
+
verifyStatus(id: string): ShellStatus | null;
|
|
127
|
+
/**
|
|
128
|
+
* Verify all shells and sync their status.
|
|
129
|
+
* Returns number of shells whose status was corrected.
|
|
130
|
+
*/
|
|
131
|
+
verifyAllStatus(): number;
|
|
132
|
+
/**
|
|
133
|
+
* List all shells
|
|
134
|
+
*/
|
|
135
|
+
list(): BackgroundShell[];
|
|
136
|
+
/**
|
|
137
|
+
* List running shells only
|
|
138
|
+
*/
|
|
139
|
+
listRunning(): BackgroundShell[];
|
|
140
|
+
/**
|
|
141
|
+
* Get shell info
|
|
142
|
+
*/
|
|
143
|
+
get(id: string): BackgroundShell | null;
|
|
144
|
+
/**
|
|
145
|
+
* Remove a shell from tracking (only if not running)
|
|
146
|
+
*/
|
|
147
|
+
remove(id: string): boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Clear all completed/failed/killed shells
|
|
150
|
+
*/
|
|
151
|
+
clearCompleted(): number;
|
|
152
|
+
/**
|
|
153
|
+
* Kill all running shells
|
|
154
|
+
*/
|
|
155
|
+
killAll(): number;
|
|
156
|
+
/**
|
|
157
|
+
* Cleanup - kill all shells and clear state
|
|
158
|
+
*/
|
|
159
|
+
dispose(): void;
|
|
160
|
+
/**
|
|
161
|
+
* Append data to buffer with size limit
|
|
162
|
+
*/
|
|
163
|
+
private appendToBuffer;
|
|
164
|
+
/**
|
|
165
|
+
* Schedule auto-cleanup for a completed shell
|
|
166
|
+
*/
|
|
167
|
+
private scheduleCleanup;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get or create the default shell manager
|
|
171
|
+
*/
|
|
172
|
+
export declare function getDefaultShellManager(): ShellManager;
|
|
173
|
+
/**
|
|
174
|
+
* Set a custom default shell manager
|
|
175
|
+
*/
|
|
176
|
+
export declare function setDefaultShellManager(manager: ShellManager): void;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell Manager - Track and manage background shell processes
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
/**
|
|
7
|
+
* Default maximum buffer size per stream (100KB)
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_MAX_BUFFER_SIZE = 100 * 1024;
|
|
10
|
+
/**
|
|
11
|
+
* Default maximum number of concurrent shells
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_MAX_SHELLS = 10;
|
|
14
|
+
/**
|
|
15
|
+
* Manages background shell processes
|
|
16
|
+
*/
|
|
17
|
+
export class ShellManager {
|
|
18
|
+
shells = new Map();
|
|
19
|
+
maxBufferSize;
|
|
20
|
+
maxShells;
|
|
21
|
+
shell;
|
|
22
|
+
autoCleanupMs;
|
|
23
|
+
cleanupTimers = new Map();
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.maxBufferSize = options?.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
|
|
26
|
+
this.maxShells = options?.maxShells ?? DEFAULT_MAX_SHELLS;
|
|
27
|
+
this.shell = options?.shell ?? '/bin/bash';
|
|
28
|
+
this.autoCleanupMs = options?.autoCleanupMs ?? 5 * 60 * 1000; // 5 minutes default
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Spawn a new background shell
|
|
32
|
+
*/
|
|
33
|
+
spawn(command, options) {
|
|
34
|
+
// Check limit
|
|
35
|
+
const runningCount = Array.from(this.shells.values()).filter((s) => s.info.status === 'running').length;
|
|
36
|
+
if (runningCount >= this.maxShells) {
|
|
37
|
+
throw new Error(`Maximum concurrent shells (${String(this.maxShells)}) reached. ` +
|
|
38
|
+
`Kill some shells before starting new ones.`);
|
|
39
|
+
}
|
|
40
|
+
const id = randomUUID().slice(0, 8);
|
|
41
|
+
const { cwd, env } = options ?? {};
|
|
42
|
+
const childProcess = spawn(command, [], {
|
|
43
|
+
cwd,
|
|
44
|
+
shell: this.shell,
|
|
45
|
+
env: env ? { ...globalThis.process.env, ...env } : undefined,
|
|
46
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
47
|
+
});
|
|
48
|
+
const state = {
|
|
49
|
+
info: {
|
|
50
|
+
id,
|
|
51
|
+
command,
|
|
52
|
+
status: 'running',
|
|
53
|
+
startTime: new Date(),
|
|
54
|
+
cwd,
|
|
55
|
+
},
|
|
56
|
+
process: childProcess,
|
|
57
|
+
stdoutBuffer: [],
|
|
58
|
+
stderrBuffer: [],
|
|
59
|
+
readIndex: { stdout: 0, stderr: 0 },
|
|
60
|
+
};
|
|
61
|
+
// Collect stdout (guaranteed non-null due to stdio: ['ignore', 'pipe', 'pipe'])
|
|
62
|
+
childProcess.stdout.on('data', (chunk) => {
|
|
63
|
+
this.appendToBuffer(state.stdoutBuffer, chunk.toString());
|
|
64
|
+
});
|
|
65
|
+
// Collect stderr (guaranteed non-null due to stdio: ['ignore', 'pipe', 'pipe'])
|
|
66
|
+
childProcess.stderr.on('data', (chunk) => {
|
|
67
|
+
this.appendToBuffer(state.stderrBuffer, chunk.toString());
|
|
68
|
+
});
|
|
69
|
+
// Handle completion
|
|
70
|
+
childProcess.on('close', (code) => {
|
|
71
|
+
state.info.status = code === 0 ? 'completed' : 'failed';
|
|
72
|
+
state.info.exitCode = code ?? undefined;
|
|
73
|
+
state.info.endTime = new Date();
|
|
74
|
+
this.scheduleCleanup(id);
|
|
75
|
+
});
|
|
76
|
+
childProcess.on('error', (error) => {
|
|
77
|
+
state.info.status = 'failed';
|
|
78
|
+
state.info.endTime = new Date();
|
|
79
|
+
state.stderrBuffer.push(`Process error: ${error.message}`);
|
|
80
|
+
this.scheduleCleanup(id);
|
|
81
|
+
});
|
|
82
|
+
this.shells.set(id, state);
|
|
83
|
+
return id;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get output from a shell (only new output since last read)
|
|
87
|
+
*/
|
|
88
|
+
getOutput(id, filter) {
|
|
89
|
+
const state = this.shells.get(id);
|
|
90
|
+
if (!state) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
// Get new output since last read
|
|
94
|
+
const newStdout = state.stdoutBuffer.slice(state.readIndex.stdout);
|
|
95
|
+
const newStderr = state.stderrBuffer.slice(state.readIndex.stderr);
|
|
96
|
+
// Update read index
|
|
97
|
+
state.readIndex.stdout = state.stdoutBuffer.length;
|
|
98
|
+
state.readIndex.stderr = state.stderrBuffer.length;
|
|
99
|
+
// Apply filter if provided
|
|
100
|
+
let stdout = newStdout.join('');
|
|
101
|
+
let stderr = newStderr.join('');
|
|
102
|
+
if (filter) {
|
|
103
|
+
stdout = stdout
|
|
104
|
+
.split('\n')
|
|
105
|
+
.filter((line) => filter.test(line))
|
|
106
|
+
.join('\n');
|
|
107
|
+
stderr = stderr
|
|
108
|
+
.split('\n')
|
|
109
|
+
.filter((line) => filter.test(line))
|
|
110
|
+
.join('\n');
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
status: state.info.status,
|
|
115
|
+
stdout,
|
|
116
|
+
stderr,
|
|
117
|
+
hasMore: state.info.status === 'running',
|
|
118
|
+
exitCode: state.info.exitCode,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get all output from a shell (including previously read)
|
|
123
|
+
*/
|
|
124
|
+
getAllOutput(id) {
|
|
125
|
+
const state = this.shells.get(id);
|
|
126
|
+
if (!state) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
id,
|
|
131
|
+
status: state.info.status,
|
|
132
|
+
stdout: state.stdoutBuffer.join(''),
|
|
133
|
+
stderr: state.stderrBuffer.join(''),
|
|
134
|
+
hasMore: state.info.status === 'running',
|
|
135
|
+
exitCode: state.info.exitCode,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Kill a background shell
|
|
140
|
+
*/
|
|
141
|
+
kill(id) {
|
|
142
|
+
const state = this.shells.get(id);
|
|
143
|
+
if (!state) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (state.info.status === 'running') {
|
|
147
|
+
// Immediately update status to prevent race conditions
|
|
148
|
+
state.info.status = 'killed';
|
|
149
|
+
state.info.endTime = new Date();
|
|
150
|
+
// Send SIGTERM
|
|
151
|
+
try {
|
|
152
|
+
state.process.kill('SIGTERM');
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Process may already be dead
|
|
156
|
+
}
|
|
157
|
+
// Force kill after 5 seconds if process still exists
|
|
158
|
+
const forceKillTimer = setTimeout(() => {
|
|
159
|
+
try {
|
|
160
|
+
// Check if process is still alive by sending signal 0
|
|
161
|
+
state.process.kill(0);
|
|
162
|
+
// If we get here, process is still alive - force kill
|
|
163
|
+
state.process.kill('SIGKILL');
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Process is already dead - good
|
|
167
|
+
}
|
|
168
|
+
}, 5000);
|
|
169
|
+
// Don't let the timer prevent process exit
|
|
170
|
+
if (typeof forceKillTimer.unref === 'function') {
|
|
171
|
+
forceKillTimer.unref();
|
|
172
|
+
}
|
|
173
|
+
// Schedule cleanup
|
|
174
|
+
this.scheduleCleanup(id);
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Verify and sync shell status with actual process state.
|
|
180
|
+
* Call this to ensure status reflects reality after potential race conditions.
|
|
181
|
+
*/
|
|
182
|
+
verifyStatus(id) {
|
|
183
|
+
const state = this.shells.get(id);
|
|
184
|
+
if (!state) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
// If marked as running, verify process is actually alive
|
|
188
|
+
if (state.info.status === 'running') {
|
|
189
|
+
try {
|
|
190
|
+
// signal 0 checks if process exists without sending actual signal
|
|
191
|
+
state.process.kill(0);
|
|
192
|
+
// Process is alive
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Process is dead but we thought it was running - update status
|
|
196
|
+
state.info.status = 'completed';
|
|
197
|
+
state.info.endTime = new Date();
|
|
198
|
+
this.scheduleCleanup(id);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return state.info.status;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Verify all shells and sync their status.
|
|
205
|
+
* Returns number of shells whose status was corrected.
|
|
206
|
+
*/
|
|
207
|
+
verifyAllStatus() {
|
|
208
|
+
let corrected = 0;
|
|
209
|
+
for (const [id, state] of this.shells) {
|
|
210
|
+
if (state.info.status === 'running') {
|
|
211
|
+
const verified = this.verifyStatus(id);
|
|
212
|
+
if (verified !== 'running') {
|
|
213
|
+
corrected++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return corrected;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* List all shells
|
|
221
|
+
*/
|
|
222
|
+
list() {
|
|
223
|
+
return Array.from(this.shells.values()).map((s) => ({ ...s.info }));
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* List running shells only
|
|
227
|
+
*/
|
|
228
|
+
listRunning() {
|
|
229
|
+
return this.list().filter((s) => s.status === 'running');
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get shell info
|
|
233
|
+
*/
|
|
234
|
+
get(id) {
|
|
235
|
+
const state = this.shells.get(id);
|
|
236
|
+
return state ? { ...state.info } : null;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Remove a shell from tracking (only if not running)
|
|
240
|
+
*/
|
|
241
|
+
remove(id) {
|
|
242
|
+
const state = this.shells.get(id);
|
|
243
|
+
if (!state || state.info.status === 'running') {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
// Clear cleanup timer if exists
|
|
247
|
+
const timer = this.cleanupTimers.get(id);
|
|
248
|
+
if (timer) {
|
|
249
|
+
clearTimeout(timer);
|
|
250
|
+
this.cleanupTimers.delete(id);
|
|
251
|
+
}
|
|
252
|
+
return this.shells.delete(id);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Clear all completed/failed/killed shells
|
|
256
|
+
*/
|
|
257
|
+
clearCompleted() {
|
|
258
|
+
let count = 0;
|
|
259
|
+
for (const [id, state] of this.shells) {
|
|
260
|
+
if (state.info.status !== 'running') {
|
|
261
|
+
this.remove(id);
|
|
262
|
+
count++;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return count;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Kill all running shells
|
|
269
|
+
*/
|
|
270
|
+
killAll() {
|
|
271
|
+
let count = 0;
|
|
272
|
+
for (const state of this.shells.values()) {
|
|
273
|
+
if (state.info.status === 'running') {
|
|
274
|
+
this.kill(state.info.id);
|
|
275
|
+
count++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return count;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Cleanup - kill all shells and clear state
|
|
282
|
+
*/
|
|
283
|
+
dispose() {
|
|
284
|
+
this.killAll();
|
|
285
|
+
for (const timer of this.cleanupTimers.values()) {
|
|
286
|
+
clearTimeout(timer);
|
|
287
|
+
}
|
|
288
|
+
this.cleanupTimers.clear();
|
|
289
|
+
this.shells.clear();
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Append data to buffer with size limit
|
|
293
|
+
*/
|
|
294
|
+
appendToBuffer(buffer, data) {
|
|
295
|
+
buffer.push(data);
|
|
296
|
+
// Trim buffer if too large
|
|
297
|
+
let totalSize = buffer.reduce((sum, s) => sum + s.length, 0);
|
|
298
|
+
while (totalSize > this.maxBufferSize && buffer.length > 1) {
|
|
299
|
+
const removed = buffer.shift();
|
|
300
|
+
if (removed) {
|
|
301
|
+
totalSize -= removed.length;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Schedule auto-cleanup for a completed shell
|
|
307
|
+
*/
|
|
308
|
+
scheduleCleanup(id) {
|
|
309
|
+
if (this.autoCleanupMs <= 0) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const timer = setTimeout(() => {
|
|
313
|
+
this.remove(id);
|
|
314
|
+
this.cleanupTimers.delete(id);
|
|
315
|
+
}, this.autoCleanupMs);
|
|
316
|
+
this.cleanupTimers.set(id, timer);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Default global shell manager instance
|
|
321
|
+
*/
|
|
322
|
+
let defaultManager = null;
|
|
323
|
+
/**
|
|
324
|
+
* Get or create the default shell manager
|
|
325
|
+
*/
|
|
326
|
+
export function getDefaultShellManager() {
|
|
327
|
+
if (!defaultManager) {
|
|
328
|
+
defaultManager = new ShellManager();
|
|
329
|
+
}
|
|
330
|
+
return defaultManager;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Set a custom default shell manager
|
|
334
|
+
*/
|
|
335
|
+
export function setDefaultShellManager(manager) {
|
|
336
|
+
defaultManager = manager;
|
|
337
|
+
}
|