@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.
Files changed (160) hide show
  1. package/README.md +1277 -0
  2. package/dist/agent.d.ts +1272 -0
  3. package/dist/agent.js +1912 -0
  4. package/dist/anchors/builtin.d.ts +24 -0
  5. package/dist/anchors/builtin.js +61 -0
  6. package/dist/anchors/index.d.ts +6 -0
  7. package/dist/anchors/index.js +5 -0
  8. package/dist/anchors/manager.d.ts +115 -0
  9. package/dist/anchors/manager.js +412 -0
  10. package/dist/anchors/types.d.ts +168 -0
  11. package/dist/anchors/types.js +10 -0
  12. package/dist/context/index.d.ts +12 -0
  13. package/dist/context/index.js +10 -0
  14. package/dist/context/manager.d.ts +224 -0
  15. package/dist/context/manager.js +770 -0
  16. package/dist/context/types.d.ts +377 -0
  17. package/dist/context/types.js +7 -0
  18. package/dist/costs/index.d.ts +8 -0
  19. package/dist/costs/index.js +7 -0
  20. package/dist/costs/tracker.d.ts +121 -0
  21. package/dist/costs/tracker.js +295 -0
  22. package/dist/costs/types.d.ts +157 -0
  23. package/dist/costs/types.js +8 -0
  24. package/dist/errors.d.ts +178 -0
  25. package/dist/errors.js +249 -0
  26. package/dist/guardrails/builtin.d.ts +27 -0
  27. package/dist/guardrails/builtin.js +223 -0
  28. package/dist/guardrails/index.d.ts +6 -0
  29. package/dist/guardrails/index.js +5 -0
  30. package/dist/guardrails/manager.d.ts +117 -0
  31. package/dist/guardrails/manager.js +288 -0
  32. package/dist/guardrails/types.d.ts +159 -0
  33. package/dist/guardrails/types.js +7 -0
  34. package/dist/hooks/index.d.ts +31 -0
  35. package/dist/hooks/index.js +29 -0
  36. package/dist/hooks/manager.d.ts +147 -0
  37. package/dist/hooks/manager.js +600 -0
  38. package/dist/hooks/types.d.ts +368 -0
  39. package/dist/hooks/types.js +12 -0
  40. package/dist/index.d.ts +45 -0
  41. package/dist/index.js +73 -0
  42. package/dist/mcp/client.d.ts +93 -0
  43. package/dist/mcp/client.js +287 -0
  44. package/dist/mcp/errors.d.ts +60 -0
  45. package/dist/mcp/errors.js +78 -0
  46. package/dist/mcp/index.d.ts +43 -0
  47. package/dist/mcp/index.js +45 -0
  48. package/dist/mcp/manager.d.ts +120 -0
  49. package/dist/mcp/manager.js +276 -0
  50. package/dist/mcp/tools.d.ts +54 -0
  51. package/dist/mcp/tools.js +99 -0
  52. package/dist/mcp/types.d.ts +150 -0
  53. package/dist/mcp/types.js +40 -0
  54. package/dist/memory/index.d.ts +8 -0
  55. package/dist/memory/index.js +7 -0
  56. package/dist/memory/loader.d.ts +114 -0
  57. package/dist/memory/loader.js +463 -0
  58. package/dist/memory/types.d.ts +182 -0
  59. package/dist/memory/types.js +8 -0
  60. package/dist/messages/index.d.ts +82 -0
  61. package/dist/messages/index.js +155 -0
  62. package/dist/permissions/index.d.ts +5 -0
  63. package/dist/permissions/index.js +4 -0
  64. package/dist/permissions/manager.d.ts +125 -0
  65. package/dist/permissions/manager.js +379 -0
  66. package/dist/permissions/types.d.ts +162 -0
  67. package/dist/permissions/types.js +7 -0
  68. package/dist/providers/claude.d.ts +90 -0
  69. package/dist/providers/claude.js +348 -0
  70. package/dist/providers/index.d.ts +8 -0
  71. package/dist/providers/index.js +11 -0
  72. package/dist/providers/mock.d.ts +133 -0
  73. package/dist/providers/mock.js +204 -0
  74. package/dist/providers/types.d.ts +168 -0
  75. package/dist/providers/types.js +4 -0
  76. package/dist/rate-limit/index.d.ts +45 -0
  77. package/dist/rate-limit/index.js +47 -0
  78. package/dist/rate-limit/limiter.d.ts +104 -0
  79. package/dist/rate-limit/limiter.js +326 -0
  80. package/dist/rate-limit/provider-wrapper.d.ts +112 -0
  81. package/dist/rate-limit/provider-wrapper.js +201 -0
  82. package/dist/rate-limit/retry.d.ts +108 -0
  83. package/dist/rate-limit/retry.js +287 -0
  84. package/dist/rate-limit/types.d.ts +181 -0
  85. package/dist/rate-limit/types.js +22 -0
  86. package/dist/rehearsal/file-analyzer.d.ts +22 -0
  87. package/dist/rehearsal/file-analyzer.js +351 -0
  88. package/dist/rehearsal/git-analyzer.d.ts +22 -0
  89. package/dist/rehearsal/git-analyzer.js +472 -0
  90. package/dist/rehearsal/index.d.ts +35 -0
  91. package/dist/rehearsal/index.js +36 -0
  92. package/dist/rehearsal/manager.d.ts +100 -0
  93. package/dist/rehearsal/manager.js +290 -0
  94. package/dist/rehearsal/types.d.ts +235 -0
  95. package/dist/rehearsal/types.js +8 -0
  96. package/dist/skills/index.d.ts +160 -0
  97. package/dist/skills/index.js +282 -0
  98. package/dist/state/agent-state.d.ts +41 -0
  99. package/dist/state/agent-state.js +88 -0
  100. package/dist/state/checkpointer.d.ts +110 -0
  101. package/dist/state/checkpointer.js +362 -0
  102. package/dist/state/errors.d.ts +66 -0
  103. package/dist/state/errors.js +88 -0
  104. package/dist/state/index.d.ts +35 -0
  105. package/dist/state/index.js +37 -0
  106. package/dist/state/serializer.d.ts +55 -0
  107. package/dist/state/serializer.js +172 -0
  108. package/dist/state/types.d.ts +312 -0
  109. package/dist/state/types.js +14 -0
  110. package/dist/tools/builtin/bash-output.d.ts +61 -0
  111. package/dist/tools/builtin/bash-output.js +90 -0
  112. package/dist/tools/builtin/bash.d.ts +150 -0
  113. package/dist/tools/builtin/bash.js +354 -0
  114. package/dist/tools/builtin/edit.d.ts +50 -0
  115. package/dist/tools/builtin/edit.js +215 -0
  116. package/dist/tools/builtin/glob.d.ts +62 -0
  117. package/dist/tools/builtin/glob.js +244 -0
  118. package/dist/tools/builtin/grep.d.ts +74 -0
  119. package/dist/tools/builtin/grep.js +363 -0
  120. package/dist/tools/builtin/index.d.ts +44 -0
  121. package/dist/tools/builtin/index.js +69 -0
  122. package/dist/tools/builtin/kill-shell.d.ts +44 -0
  123. package/dist/tools/builtin/kill-shell.js +80 -0
  124. package/dist/tools/builtin/read-file.d.ts +57 -0
  125. package/dist/tools/builtin/read-file.js +184 -0
  126. package/dist/tools/builtin/shell-manager.d.ts +176 -0
  127. package/dist/tools/builtin/shell-manager.js +337 -0
  128. package/dist/tools/builtin/task.d.ts +202 -0
  129. package/dist/tools/builtin/task.js +350 -0
  130. package/dist/tools/builtin/todo.d.ts +207 -0
  131. package/dist/tools/builtin/todo.js +453 -0
  132. package/dist/tools/builtin/utils.d.ts +27 -0
  133. package/dist/tools/builtin/utils.js +70 -0
  134. package/dist/tools/builtin/web-fetch.d.ts +96 -0
  135. package/dist/tools/builtin/web-fetch.js +290 -0
  136. package/dist/tools/builtin/write-file.d.ts +54 -0
  137. package/dist/tools/builtin/write-file.js +147 -0
  138. package/dist/tools/define.d.ts +60 -0
  139. package/dist/tools/define.js +65 -0
  140. package/dist/tools/index.d.ts +10 -0
  141. package/dist/tools/index.js +37 -0
  142. package/dist/tools/registry.d.ts +79 -0
  143. package/dist/tools/registry.js +151 -0
  144. package/dist/tools/types.d.ts +59 -0
  145. package/dist/tools/types.js +4 -0
  146. package/dist/tracing/hooks.d.ts +58 -0
  147. package/dist/tracing/hooks.js +377 -0
  148. package/dist/tracing/index.d.ts +51 -0
  149. package/dist/tracing/index.js +55 -0
  150. package/dist/tracing/logging.d.ts +78 -0
  151. package/dist/tracing/logging.js +310 -0
  152. package/dist/tracing/manager.d.ts +160 -0
  153. package/dist/tracing/manager.js +468 -0
  154. package/dist/tracing/otel.d.ts +102 -0
  155. package/dist/tracing/otel.js +246 -0
  156. package/dist/tracing/types.d.ts +346 -0
  157. package/dist/tracing/types.js +38 -0
  158. package/dist/utils/index.d.ts +23 -0
  159. package/dist/utils/index.js +44 -0
  160. 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
+ }