@dexto/tools-process 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/LICENSE +44 -0
  2. package/dist/bash-exec-tool.cjs +130 -0
  3. package/dist/bash-exec-tool.d.cts +17 -0
  4. package/dist/bash-exec-tool.d.ts +17 -0
  5. package/dist/bash-exec-tool.js +96 -0
  6. package/dist/bash-output-tool.cjs +49 -0
  7. package/dist/bash-output-tool.d.cts +16 -0
  8. package/dist/bash-output-tool.d.ts +16 -0
  9. package/dist/bash-output-tool.js +25 -0
  10. package/dist/command-validator.cjs +554 -0
  11. package/dist/command-validator.d.cts +52 -0
  12. package/dist/command-validator.d.ts +52 -0
  13. package/dist/command-validator.js +530 -0
  14. package/dist/error-codes.cjs +47 -0
  15. package/dist/error-codes.d.cts +26 -0
  16. package/dist/error-codes.d.ts +26 -0
  17. package/dist/error-codes.js +23 -0
  18. package/dist/errors.cjs +243 -0
  19. package/dist/errors.d.cts +90 -0
  20. package/dist/errors.d.ts +90 -0
  21. package/dist/errors.js +219 -0
  22. package/dist/index.cjs +49 -0
  23. package/dist/index.d.cts +11 -0
  24. package/dist/index.d.ts +11 -0
  25. package/dist/index.js +18 -0
  26. package/dist/kill-process-tool.cjs +47 -0
  27. package/dist/kill-process-tool.d.cts +16 -0
  28. package/dist/kill-process-tool.d.ts +16 -0
  29. package/dist/kill-process-tool.js +23 -0
  30. package/dist/process-service.cjs +544 -0
  31. package/dist/process-service.d.cts +96 -0
  32. package/dist/process-service.d.ts +96 -0
  33. package/dist/process-service.js +510 -0
  34. package/dist/tool-provider.cjs +96 -0
  35. package/dist/tool-provider.d.cts +72 -0
  36. package/dist/tool-provider.d.ts +72 -0
  37. package/dist/tool-provider.js +72 -0
  38. package/dist/types.cjs +16 -0
  39. package/dist/types.d.cts +108 -0
  40. package/dist/types.d.ts +108 -0
  41. package/dist/types.js +0 -0
  42. package/package.json +38 -0
@@ -0,0 +1,11 @@
1
+ export { processToolsProvider } from './tool-provider.js';
2
+ export { ProcessService } from './process-service.js';
3
+ export { CommandValidator } from './command-validator.js';
4
+ export { ProcessError } from './errors.js';
5
+ export { ProcessErrorCode } from './error-codes.js';
6
+ export { CommandValidation, ExecuteOptions, OutputBuffer, ProcessConfig, ProcessHandle, ProcessInfo, ProcessOutput, ProcessResult } from './types.js';
7
+ export { createBashExecTool } from './bash-exec-tool.js';
8
+ export { createBashOutputTool } from './bash-output-tool.js';
9
+ export { createKillProcessTool } from './kill-process-tool.js';
10
+ import 'zod';
11
+ import '@dexto/core';
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ import { processToolsProvider } from "./tool-provider.js";
2
+ import { ProcessService } from "./process-service.js";
3
+ import { CommandValidator } from "./command-validator.js";
4
+ import { ProcessError } from "./errors.js";
5
+ import { ProcessErrorCode } from "./error-codes.js";
6
+ import { createBashExecTool } from "./bash-exec-tool.js";
7
+ import { createBashOutputTool } from "./bash-output-tool.js";
8
+ import { createKillProcessTool } from "./kill-process-tool.js";
9
+ export {
10
+ CommandValidator,
11
+ ProcessError,
12
+ ProcessErrorCode,
13
+ ProcessService,
14
+ createBashExecTool,
15
+ createBashOutputTool,
16
+ createKillProcessTool,
17
+ processToolsProvider
18
+ };
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var kill_process_tool_exports = {};
20
+ __export(kill_process_tool_exports, {
21
+ createKillProcessTool: () => createKillProcessTool
22
+ });
23
+ module.exports = __toCommonJS(kill_process_tool_exports);
24
+ var import_zod = require("zod");
25
+ const KillProcessInputSchema = import_zod.z.object({
26
+ process_id: import_zod.z.string().describe("Process ID of the background process to terminate")
27
+ }).strict();
28
+ function createKillProcessTool(processService) {
29
+ return {
30
+ id: "kill_process",
31
+ description: "Terminate a background process started with bash_exec. Sends SIGTERM signal first, then SIGKILL if process doesn't terminate within 5 seconds. Only works on processes started by this agent. Returns success status and whether the process was running. Does not require additional approval (process was already approved when started).",
32
+ inputSchema: KillProcessInputSchema,
33
+ execute: async (input, _context) => {
34
+ const { process_id } = input;
35
+ await processService.killProcess(process_id);
36
+ return {
37
+ success: true,
38
+ process_id,
39
+ message: `Termination signal sent to process ${process_id}`
40
+ };
41
+ }
42
+ };
43
+ }
44
+ // Annotate the CommonJS export names for ESM import in node:
45
+ 0 && (module.exports = {
46
+ createKillProcessTool
47
+ });
@@ -0,0 +1,16 @@
1
+ import { InternalTool } from '@dexto/core';
2
+ import { ProcessService } from './process-service.cjs';
3
+ import './types.cjs';
4
+
5
+ /**
6
+ * Kill Process Tool
7
+ *
8
+ * Internal tool for terminating background processes
9
+ */
10
+
11
+ /**
12
+ * Create the kill_process internal tool
13
+ */
14
+ declare function createKillProcessTool(processService: ProcessService): InternalTool;
15
+
16
+ export { createKillProcessTool };
@@ -0,0 +1,16 @@
1
+ import { InternalTool } from '@dexto/core';
2
+ import { ProcessService } from './process-service.js';
3
+ import './types.js';
4
+
5
+ /**
6
+ * Kill Process Tool
7
+ *
8
+ * Internal tool for terminating background processes
9
+ */
10
+
11
+ /**
12
+ * Create the kill_process internal tool
13
+ */
14
+ declare function createKillProcessTool(processService: ProcessService): InternalTool;
15
+
16
+ export { createKillProcessTool };
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ const KillProcessInputSchema = z.object({
3
+ process_id: z.string().describe("Process ID of the background process to terminate")
4
+ }).strict();
5
+ function createKillProcessTool(processService) {
6
+ return {
7
+ id: "kill_process",
8
+ description: "Terminate a background process started with bash_exec. Sends SIGTERM signal first, then SIGKILL if process doesn't terminate within 5 seconds. Only works on processes started by this agent. Returns success status and whether the process was running. Does not require additional approval (process was already approved when started).",
9
+ inputSchema: KillProcessInputSchema,
10
+ execute: async (input, _context) => {
11
+ const { process_id } = input;
12
+ await processService.killProcess(process_id);
13
+ return {
14
+ success: true,
15
+ process_id,
16
+ message: `Termination signal sent to process ${process_id}`
17
+ };
18
+ }
19
+ };
20
+ }
21
+ export {
22
+ createKillProcessTool
23
+ };
@@ -0,0 +1,544 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var process_service_exports = {};
30
+ __export(process_service_exports, {
31
+ ProcessService: () => ProcessService
32
+ });
33
+ module.exports = __toCommonJS(process_service_exports);
34
+ var import_node_child_process = require("node:child_process");
35
+ var crypto = __toESM(require("node:crypto"), 1);
36
+ var path = __toESM(require("node:path"), 1);
37
+ var import_command_validator = require("./command-validator.js");
38
+ var import_errors = require("./errors.js");
39
+ var import_core = require("@dexto/core");
40
+ const DEFAULT_TIMEOUT = 12e4;
41
+ class ProcessService {
42
+ config;
43
+ commandValidator;
44
+ initialized = false;
45
+ initPromise = null;
46
+ backgroundProcesses = /* @__PURE__ */ new Map();
47
+ logger;
48
+ /**
49
+ * Create a new ProcessService with validated configuration.
50
+ *
51
+ * @param config - Fully-validated configuration from provider schema.
52
+ * All required fields have values, defaults already applied.
53
+ * @param logger - Logger instance for this service
54
+ */
55
+ constructor(config, logger) {
56
+ this.config = config;
57
+ this.logger = logger.createChild(import_core.DextoLogComponent.PROCESS);
58
+ this.commandValidator = new import_command_validator.CommandValidator(this.config, this.logger);
59
+ }
60
+ /**
61
+ * Initialize the service.
62
+ * Safe to call multiple times - subsequent calls return the same promise.
63
+ */
64
+ initialize() {
65
+ if (this.initPromise) {
66
+ return this.initPromise;
67
+ }
68
+ this.initPromise = this.doInitialize();
69
+ return this.initPromise;
70
+ }
71
+ /**
72
+ * Internal initialization logic.
73
+ */
74
+ async doInitialize() {
75
+ if (this.initialized) {
76
+ this.logger.debug("ProcessService already initialized");
77
+ return;
78
+ }
79
+ this.backgroundProcesses.clear();
80
+ this.initialized = true;
81
+ this.logger.info("ProcessService initialized successfully");
82
+ }
83
+ /**
84
+ * Ensure the service is initialized before use.
85
+ * Tools should call this at the start of their execute methods.
86
+ * Safe to call multiple times - will await the same initialization promise.
87
+ */
88
+ async ensureInitialized() {
89
+ if (this.initialized) {
90
+ return;
91
+ }
92
+ await this.initialize();
93
+ }
94
+ /**
95
+ * Execute a command
96
+ */
97
+ async executeCommand(command, options = {}) {
98
+ await this.ensureInitialized();
99
+ const validation = this.commandValidator.validateCommand(command);
100
+ if (!validation.isValid || !validation.normalizedCommand) {
101
+ throw import_errors.ProcessError.invalidCommand(command, validation.error || "Unknown error");
102
+ }
103
+ const normalizedCommand = validation.normalizedCommand;
104
+ const rawTimeout = options.timeout !== void 0 && Number.isFinite(options.timeout) ? options.timeout : DEFAULT_TIMEOUT;
105
+ const timeout = Math.max(1, Math.min(rawTimeout, this.config.maxTimeout));
106
+ const cwd = this.resolveSafeCwd(options.cwd);
107
+ const env = {};
108
+ for (const [key, value] of Object.entries({
109
+ ...process.env,
110
+ ...this.config.environment,
111
+ ...options.env
112
+ })) {
113
+ if (value !== void 0) {
114
+ env[key] = value;
115
+ }
116
+ }
117
+ if (options.runInBackground) {
118
+ return await this.executeInBackground(normalizedCommand, options);
119
+ }
120
+ return await this.executeForeground(normalizedCommand, {
121
+ cwd,
122
+ timeout,
123
+ env,
124
+ ...options.description !== void 0 && { description: options.description },
125
+ ...options.abortSignal !== void 0 && { abortSignal: options.abortSignal }
126
+ });
127
+ }
128
+ static SIGKILL_TIMEOUT_MS = 200;
129
+ /**
130
+ * Kill a process tree (process group on Unix, taskkill on Windows)
131
+ */
132
+ async killProcessTree(pid, child) {
133
+ if (process.platform === "win32") {
134
+ await new Promise((resolve) => {
135
+ const killer = (0, import_node_child_process.spawn)("taskkill", ["/pid", String(pid), "/f", "/t"], {
136
+ stdio: "ignore"
137
+ });
138
+ killer.once("exit", () => resolve());
139
+ killer.once("error", () => resolve());
140
+ });
141
+ } else {
142
+ try {
143
+ process.kill(-pid, "SIGTERM");
144
+ await new Promise((res) => setTimeout(res, ProcessService.SIGKILL_TIMEOUT_MS));
145
+ if (child.exitCode === null) {
146
+ process.kill(-pid, "SIGKILL");
147
+ }
148
+ } catch {
149
+ child.kill("SIGTERM");
150
+ await new Promise((res) => setTimeout(res, ProcessService.SIGKILL_TIMEOUT_MS));
151
+ if (child.exitCode === null) {
152
+ child.kill("SIGKILL");
153
+ }
154
+ }
155
+ }
156
+ }
157
+ /**
158
+ * Execute command in foreground with timeout and abort support
159
+ */
160
+ executeForeground(command, options) {
161
+ return new Promise((resolve, reject) => {
162
+ const startTime = Date.now();
163
+ let stdout = "";
164
+ let stderr = "";
165
+ let stdoutBytes = 0;
166
+ let stderrBytes = 0;
167
+ let outputTruncated = false;
168
+ let killed = false;
169
+ let aborted = false;
170
+ let closed = false;
171
+ const maxBuffer = this.config.maxOutputBuffer;
172
+ if (options.abortSignal?.aborted) {
173
+ this.logger.debug(`Command cancelled before execution: ${command}`);
174
+ resolve({
175
+ stdout: "",
176
+ stderr: "(Command was cancelled)",
177
+ exitCode: 130,
178
+ // Standard exit code for SIGINT
179
+ duration: 0
180
+ });
181
+ return;
182
+ }
183
+ this.logger.debug(`Executing command: ${command}`);
184
+ const child = (0, import_node_child_process.spawn)(command, {
185
+ cwd: options.cwd,
186
+ env: options.env,
187
+ shell: true,
188
+ detached: process.platform !== "win32"
189
+ // Create process group on Unix
190
+ });
191
+ const timeoutHandle = setTimeout(() => {
192
+ killed = true;
193
+ if (child.pid) {
194
+ void this.killProcessTree(child.pid, child);
195
+ } else {
196
+ child.kill("SIGTERM");
197
+ }
198
+ }, options.timeout);
199
+ const abortHandler = () => {
200
+ if (closed) return;
201
+ aborted = true;
202
+ this.logger.debug(`Command cancelled by user: ${command}`);
203
+ clearTimeout(timeoutHandle);
204
+ if (child.pid) {
205
+ void this.killProcessTree(child.pid, child);
206
+ } else {
207
+ child.kill("SIGTERM");
208
+ }
209
+ };
210
+ options.abortSignal?.addEventListener("abort", abortHandler, { once: true });
211
+ child.stdout?.on("data", (data) => {
212
+ if (outputTruncated) return;
213
+ const chunk = data.toString();
214
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
215
+ if (stdoutBytes + stderrBytes + chunkBytes <= maxBuffer) {
216
+ stdout += chunk;
217
+ stdoutBytes += chunkBytes;
218
+ } else {
219
+ const remaining = maxBuffer - stdoutBytes - stderrBytes;
220
+ if (remaining > 0) {
221
+ stdout += chunk.slice(0, remaining);
222
+ stdoutBytes += remaining;
223
+ }
224
+ stdout += "\n...[truncated]";
225
+ outputTruncated = true;
226
+ this.logger.warn(`Output buffer full for command: ${command}`);
227
+ }
228
+ });
229
+ child.stderr?.on("data", (data) => {
230
+ if (outputTruncated) return;
231
+ const chunk = data.toString();
232
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
233
+ if (stdoutBytes + stderrBytes + chunkBytes <= maxBuffer) {
234
+ stderr += chunk;
235
+ stderrBytes += chunkBytes;
236
+ } else {
237
+ const remaining = maxBuffer - stdoutBytes - stderrBytes;
238
+ if (remaining > 0) {
239
+ stderr += chunk.slice(0, remaining);
240
+ stderrBytes += remaining;
241
+ }
242
+ stderr += "\n...[truncated]";
243
+ outputTruncated = true;
244
+ this.logger.warn(`Output buffer full for command: ${command}`);
245
+ }
246
+ });
247
+ child.on("close", (code, signal) => {
248
+ closed = true;
249
+ clearTimeout(timeoutHandle);
250
+ options.abortSignal?.removeEventListener("abort", abortHandler);
251
+ const duration = Date.now() - startTime;
252
+ if (aborted) {
253
+ stdout += "\n\n(Command was cancelled)";
254
+ this.logger.debug(`Command cancelled after ${duration}ms: ${command}`);
255
+ resolve({
256
+ stdout,
257
+ stderr,
258
+ exitCode: 130,
259
+ // Standard exit code for SIGINT
260
+ duration
261
+ });
262
+ return;
263
+ }
264
+ if (killed) {
265
+ reject(import_errors.ProcessError.timeout(command, options.timeout));
266
+ return;
267
+ }
268
+ let exitCode = typeof code === "number" ? code : 1;
269
+ if (code === null) {
270
+ stderr += `
271
+ Process terminated by signal ${signal ?? "UNKNOWN"}`;
272
+ }
273
+ this.logger.debug(
274
+ `Command completed with exit code ${exitCode} in ${duration}ms: ${command}`
275
+ );
276
+ resolve({
277
+ stdout,
278
+ stderr,
279
+ exitCode,
280
+ duration
281
+ });
282
+ });
283
+ child.on("error", (error) => {
284
+ clearTimeout(timeoutHandle);
285
+ options.abortSignal?.removeEventListener("abort", abortHandler);
286
+ if (error.code === "ENOENT") {
287
+ reject(import_errors.ProcessError.commandNotFound(command));
288
+ } else if (error.code === "EACCES") {
289
+ reject(import_errors.ProcessError.permissionDenied(command));
290
+ } else {
291
+ reject(import_errors.ProcessError.executionFailed(command, error.message));
292
+ }
293
+ });
294
+ });
295
+ }
296
+ /**
297
+ * Execute command in background
298
+ */
299
+ async executeInBackground(command, options) {
300
+ const runningCount = Array.from(this.backgroundProcesses.values()).filter(
301
+ (p) => p.status === "running"
302
+ ).length;
303
+ if (runningCount >= this.config.maxConcurrentProcesses) {
304
+ throw import_errors.ProcessError.tooManyProcesses(runningCount, this.config.maxConcurrentProcesses);
305
+ }
306
+ const processId = crypto.randomBytes(4).toString("hex");
307
+ const cwd = this.resolveSafeCwd(options.cwd);
308
+ const env = {};
309
+ for (const [key, value] of Object.entries({
310
+ ...process.env,
311
+ ...this.config.environment,
312
+ ...options.env
313
+ })) {
314
+ if (value !== void 0) {
315
+ env[key] = value;
316
+ }
317
+ }
318
+ this.logger.debug(`Starting background process ${processId}: ${command}`);
319
+ const child = (0, import_node_child_process.spawn)(command, {
320
+ cwd,
321
+ env,
322
+ shell: true,
323
+ detached: false
324
+ });
325
+ const outputBuffer = {
326
+ stdout: [],
327
+ stderr: [],
328
+ complete: false,
329
+ lastRead: Date.now(),
330
+ bytesUsed: 0,
331
+ truncated: false
332
+ };
333
+ const bgProcess = {
334
+ processId,
335
+ command,
336
+ child,
337
+ startedAt: /* @__PURE__ */ new Date(),
338
+ status: "running",
339
+ outputBuffer,
340
+ description: options.description
341
+ };
342
+ this.backgroundProcesses.set(processId, bgProcess);
343
+ const bgTimeout = Math.max(
344
+ 1,
345
+ Math.min(options.timeout || DEFAULT_TIMEOUT, this.config.maxTimeout)
346
+ );
347
+ let killEscalationTimer = null;
348
+ const killTimer = setTimeout(() => {
349
+ if (bgProcess.status === "running") {
350
+ this.logger.warn(
351
+ `Background process ${processId} timed out after ${bgTimeout}ms, sending SIGTERM`
352
+ );
353
+ child.kill("SIGTERM");
354
+ killEscalationTimer = setTimeout(() => {
355
+ if (bgProcess.status === "running") {
356
+ this.logger.warn(
357
+ `Background process ${processId} did not respond to SIGTERM, sending SIGKILL`
358
+ );
359
+ child.kill("SIGKILL");
360
+ }
361
+ }, 5e3);
362
+ }
363
+ }, bgTimeout);
364
+ child.stdout?.on("data", (data) => {
365
+ const chunk = data.toString();
366
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
367
+ if (outputBuffer.bytesUsed + chunkBytes <= this.config.maxOutputBuffer) {
368
+ outputBuffer.stdout.push(chunk);
369
+ outputBuffer.bytesUsed += chunkBytes;
370
+ } else {
371
+ if (!outputBuffer.truncated) {
372
+ outputBuffer.truncated = true;
373
+ this.logger.warn(`Output buffer full for process ${processId}`);
374
+ }
375
+ }
376
+ });
377
+ child.stderr?.on("data", (data) => {
378
+ const chunk = data.toString();
379
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
380
+ if (outputBuffer.bytesUsed + chunkBytes <= this.config.maxOutputBuffer) {
381
+ outputBuffer.stderr.push(chunk);
382
+ outputBuffer.bytesUsed += chunkBytes;
383
+ } else {
384
+ if (!outputBuffer.truncated) {
385
+ outputBuffer.truncated = true;
386
+ this.logger.warn(`Error buffer full for process ${processId}`);
387
+ }
388
+ }
389
+ });
390
+ child.on("close", (code) => {
391
+ clearTimeout(killTimer);
392
+ if (killEscalationTimer) clearTimeout(killEscalationTimer);
393
+ bgProcess.status = code === 0 ? "completed" : "failed";
394
+ bgProcess.exitCode = code ?? void 0;
395
+ bgProcess.completedAt = /* @__PURE__ */ new Date();
396
+ bgProcess.outputBuffer.complete = true;
397
+ this.logger.debug(`Background process ${processId} completed with exit code ${code}`);
398
+ });
399
+ child.on("error", (error) => {
400
+ clearTimeout(killTimer);
401
+ if (killEscalationTimer) clearTimeout(killEscalationTimer);
402
+ bgProcess.status = "failed";
403
+ bgProcess.completedAt = /* @__PURE__ */ new Date();
404
+ bgProcess.outputBuffer.complete = true;
405
+ const chunk = `Error: ${error.message}`;
406
+ const chunkBytes = Buffer.byteLength(chunk, "utf8");
407
+ if (bgProcess.outputBuffer.bytesUsed + chunkBytes <= this.config.maxOutputBuffer) {
408
+ bgProcess.outputBuffer.stderr.push(chunk);
409
+ bgProcess.outputBuffer.bytesUsed += chunkBytes;
410
+ } else {
411
+ if (!bgProcess.outputBuffer.truncated) {
412
+ bgProcess.outputBuffer.truncated = true;
413
+ this.logger.warn(`Error buffer full for process ${processId}`);
414
+ }
415
+ }
416
+ this.logger.error(`Background process ${processId} failed: ${error.message}`);
417
+ });
418
+ return {
419
+ processId,
420
+ command,
421
+ pid: child.pid,
422
+ startedAt: bgProcess.startedAt,
423
+ description: options.description
424
+ };
425
+ }
426
+ /**
427
+ * Get output from a background process
428
+ */
429
+ async getProcessOutput(processId) {
430
+ await this.ensureInitialized();
431
+ const bgProcess = this.backgroundProcesses.get(processId);
432
+ if (!bgProcess) {
433
+ throw import_errors.ProcessError.processNotFound(processId);
434
+ }
435
+ const stdout = bgProcess.outputBuffer.stdout.join("");
436
+ const stderr = bgProcess.outputBuffer.stderr.join("");
437
+ bgProcess.outputBuffer.stdout = [];
438
+ bgProcess.outputBuffer.stderr = [];
439
+ bgProcess.outputBuffer.lastRead = Date.now();
440
+ bgProcess.outputBuffer.bytesUsed = 0;
441
+ return {
442
+ stdout,
443
+ stderr,
444
+ status: bgProcess.status,
445
+ exitCode: bgProcess.exitCode,
446
+ duration: bgProcess.completedAt ? bgProcess.completedAt.getTime() - bgProcess.startedAt.getTime() : void 0
447
+ };
448
+ }
449
+ /**
450
+ * Kill a background process
451
+ */
452
+ async killProcess(processId) {
453
+ await this.ensureInitialized();
454
+ const bgProcess = this.backgroundProcesses.get(processId);
455
+ if (!bgProcess) {
456
+ throw import_errors.ProcessError.processNotFound(processId);
457
+ }
458
+ if (bgProcess.status !== "running") {
459
+ this.logger.debug(`Process ${processId} is not running (status: ${bgProcess.status})`);
460
+ return;
461
+ }
462
+ try {
463
+ bgProcess.child.kill("SIGTERM");
464
+ setTimeout(() => {
465
+ if (bgProcess.child.exitCode === null) {
466
+ bgProcess.child.kill("SIGKILL");
467
+ }
468
+ }, 5e3);
469
+ this.logger.debug(`Process ${processId} sent SIGTERM`);
470
+ } catch (error) {
471
+ throw import_errors.ProcessError.killFailed(
472
+ processId,
473
+ error instanceof Error ? error.message : String(error)
474
+ );
475
+ }
476
+ }
477
+ /**
478
+ * List all background processes
479
+ */
480
+ async listProcesses() {
481
+ await this.ensureInitialized();
482
+ return Array.from(this.backgroundProcesses.values()).map((bgProcess) => ({
483
+ processId: bgProcess.processId,
484
+ command: bgProcess.command,
485
+ pid: bgProcess.child.pid,
486
+ status: bgProcess.status,
487
+ startedAt: bgProcess.startedAt,
488
+ completedAt: bgProcess.completedAt,
489
+ exitCode: bgProcess.exitCode,
490
+ description: bgProcess.description
491
+ }));
492
+ }
493
+ /**
494
+ * Get buffer size in bytes
495
+ */
496
+ getBufferSize(buffer) {
497
+ const stdoutSize = buffer.stdout.reduce((sum, line) => sum + line.length, 0);
498
+ const stderrSize = buffer.stderr.reduce((sum, line) => sum + line.length, 0);
499
+ return stdoutSize + stderrSize;
500
+ }
501
+ /**
502
+ * Get service configuration
503
+ */
504
+ getConfig() {
505
+ return { ...this.config };
506
+ }
507
+ /**
508
+ * Resolve and confine cwd to the configured working directory
509
+ */
510
+ resolveSafeCwd(cwd) {
511
+ const baseDir = this.config.workingDirectory || process.cwd();
512
+ if (!cwd) return baseDir;
513
+ const candidate = path.isAbsolute(cwd) ? path.resolve(cwd) : path.resolve(baseDir, cwd);
514
+ const rel = path.relative(baseDir, candidate);
515
+ const outside = rel.startsWith("..") || path.isAbsolute(rel);
516
+ if (outside) {
517
+ throw import_errors.ProcessError.invalidWorkingDirectory(
518
+ cwd,
519
+ `Working directory must be within ${baseDir}`
520
+ );
521
+ }
522
+ return candidate;
523
+ }
524
+ /**
525
+ * Cleanup completed processes
526
+ */
527
+ async cleanup() {
528
+ const now = Date.now();
529
+ const CLEANUP_AGE = 36e5;
530
+ for (const [processId, bgProcess] of this.backgroundProcesses.entries()) {
531
+ if (bgProcess.status !== "running" && bgProcess.completedAt) {
532
+ const age = now - bgProcess.completedAt.getTime();
533
+ if (age > CLEANUP_AGE) {
534
+ this.backgroundProcesses.delete(processId);
535
+ this.logger.debug(`Cleaned up old process ${processId}`);
536
+ }
537
+ }
538
+ }
539
+ }
540
+ }
541
+ // Annotate the CommonJS export names for ESM import in node:
542
+ 0 && (module.exports = {
543
+ ProcessService
544
+ });