skimpyclaw 0.3.14 → 0.4.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 (222) hide show
  1. package/README.md +47 -37
  2. package/dist/__tests__/adapter-types.test.d.ts +4 -0
  3. package/dist/__tests__/adapter-types.test.js +63 -0
  4. package/dist/__tests__/anthropic-adapter.test.d.ts +4 -0
  5. package/dist/__tests__/anthropic-adapter.test.js +264 -0
  6. package/dist/__tests__/api.test.js +0 -1
  7. package/dist/__tests__/cli.integration.test.js +2 -4
  8. package/dist/__tests__/cli.test.js +0 -1
  9. package/dist/__tests__/code-agents-notifications.test.js +137 -0
  10. package/dist/__tests__/code-agents-parser.test.js +19 -1
  11. package/dist/__tests__/code-agents-preflight.test.js +3 -28
  12. package/dist/__tests__/code-agents-utils.test.js +34 -9
  13. package/dist/__tests__/code-agents-worktrees.test.js +116 -0
  14. package/dist/__tests__/codex-adapter.test.js +184 -0
  15. package/dist/__tests__/codex-auth.test.js +66 -0
  16. package/dist/__tests__/codex-provider-gating.test.js +35 -0
  17. package/dist/__tests__/codex-unified-loop.test.js +111 -0
  18. package/dist/__tests__/config-security.test.js +127 -0
  19. package/dist/__tests__/config.test.js +23 -0
  20. package/dist/__tests__/context-manager.test.js +243 -164
  21. package/dist/__tests__/cron-run.test.js +250 -0
  22. package/dist/__tests__/cron.test.js +12 -38
  23. package/dist/__tests__/digests.test.js +67 -0
  24. package/dist/__tests__/discord-attachments.test.js +211 -0
  25. package/dist/__tests__/discord-docs.test.d.ts +1 -0
  26. package/dist/__tests__/discord-docs.test.js +27 -0
  27. package/dist/__tests__/discord-thread-agents.test.d.ts +1 -0
  28. package/dist/__tests__/discord-thread-agents.test.js +115 -0
  29. package/dist/__tests__/discord-thread-context.test.d.ts +1 -0
  30. package/dist/__tests__/discord-thread-context.test.js +42 -0
  31. package/dist/__tests__/doctor.formatters.test.js +4 -4
  32. package/dist/__tests__/doctor.index.test.js +1 -1
  33. package/dist/__tests__/doctor.runner.test.js +3 -15
  34. package/dist/__tests__/env-sanitizer.test.d.ts +1 -0
  35. package/dist/__tests__/env-sanitizer.test.js +45 -0
  36. package/dist/__tests__/exec-approval.test.js +61 -0
  37. package/dist/__tests__/fetch-tool.test.d.ts +1 -0
  38. package/dist/__tests__/fetch-tool.test.js +85 -0
  39. package/dist/__tests__/gateway-status-auth.test.d.ts +1 -0
  40. package/dist/__tests__/gateway-status-auth.test.js +72 -0
  41. package/dist/__tests__/heartbeat.test.js +3 -3
  42. package/dist/__tests__/interactive-sessions.test.d.ts +1 -0
  43. package/dist/__tests__/interactive-sessions.test.js +96 -0
  44. package/dist/__tests__/langfuse.test.js +6 -18
  45. package/dist/__tests__/model-selection.test.js +3 -4
  46. package/dist/__tests__/providers-init.test.js +2 -8
  47. package/dist/__tests__/providers-routing.test.js +1 -1
  48. package/dist/__tests__/providers-utils.test.js +13 -3
  49. package/dist/__tests__/sessions.test.js +14 -10
  50. package/dist/__tests__/setup.test.js +12 -29
  51. package/dist/__tests__/skills.test.js +10 -7
  52. package/dist/__tests__/stream-formatter.test.d.ts +1 -0
  53. package/dist/__tests__/stream-formatter.test.js +114 -0
  54. package/dist/__tests__/token-efficiency.test.js +131 -15
  55. package/dist/__tests__/tool-loop.test.d.ts +4 -0
  56. package/dist/__tests__/tool-loop.test.js +505 -0
  57. package/dist/__tests__/tools.test.js +101 -276
  58. package/dist/__tests__/utils.test.d.ts +1 -0
  59. package/dist/__tests__/utils.test.js +14 -0
  60. package/dist/__tests__/voice.test.js +21 -0
  61. package/dist/agent.js +35 -4
  62. package/dist/api.js +113 -37
  63. package/dist/channels/discord/attachments.d.ts +50 -0
  64. package/dist/channels/discord/attachments.js +137 -0
  65. package/dist/channels/discord/delegation.d.ts +5 -0
  66. package/dist/channels/discord/delegation.js +136 -0
  67. package/dist/channels/discord/handlers.js +694 -7
  68. package/dist/channels/discord/index.d.ts +16 -1
  69. package/dist/channels/discord/index.js +64 -1
  70. package/dist/channels/discord/thread-agents.d.ts +54 -0
  71. package/dist/channels/discord/thread-agents.js +323 -0
  72. package/dist/channels/discord/threads.d.ts +58 -0
  73. package/dist/channels/discord/threads.js +192 -0
  74. package/dist/channels/discord/types.js +4 -2
  75. package/dist/channels/discord/utils.d.ts +16 -0
  76. package/dist/channels/discord/utils.js +86 -6
  77. package/dist/channels/telegram/index.d.ts +1 -1
  78. package/dist/channels/telegram/types.js +1 -1
  79. package/dist/channels/telegram/utils.js +9 -3
  80. package/dist/channels.d.ts +1 -1
  81. package/dist/cli.js +20 -400
  82. package/dist/code-agents/executor.d.ts +1 -1
  83. package/dist/code-agents/executor.js +101 -45
  84. package/dist/code-agents/index.d.ts +2 -7
  85. package/dist/code-agents/index.js +111 -80
  86. package/dist/code-agents/interactive-resume.d.ts +6 -0
  87. package/dist/code-agents/interactive-resume.js +98 -0
  88. package/dist/code-agents/interactive-sessions.d.ts +20 -0
  89. package/dist/code-agents/interactive-sessions.js +132 -0
  90. package/dist/code-agents/parser.js +5 -1
  91. package/dist/code-agents/registry.d.ts +7 -1
  92. package/dist/code-agents/registry.js +11 -23
  93. package/dist/code-agents/stream-formatter.d.ts +8 -0
  94. package/dist/code-agents/stream-formatter.js +92 -0
  95. package/dist/code-agents/types.d.ts +16 -24
  96. package/dist/code-agents/utils.d.ts +35 -11
  97. package/dist/code-agents/utils.js +349 -95
  98. package/dist/code-agents/worktrees.d.ts +37 -0
  99. package/dist/code-agents/worktrees.js +116 -0
  100. package/dist/config.d.ts +2 -4
  101. package/dist/config.js +123 -23
  102. package/dist/cron.d.ts +1 -6
  103. package/dist/cron.js +175 -82
  104. package/dist/dashboard/assets/index-B345aOO-.js +65 -0
  105. package/dist/dashboard/assets/index-ZWK4dalJ.css +1 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/digests.d.ts +1 -0
  108. package/dist/digests.js +132 -42
  109. package/dist/doctor/checks.d.ts +0 -3
  110. package/dist/doctor/checks.js +1 -108
  111. package/dist/doctor/runner.js +1 -4
  112. package/dist/env-sanitizer.d.ts +2 -0
  113. package/dist/env-sanitizer.js +61 -0
  114. package/dist/exec-approval.d.ts +11 -1
  115. package/dist/exec-approval.js +17 -4
  116. package/dist/gateway.d.ts +3 -1
  117. package/dist/gateway.js +17 -7
  118. package/dist/heartbeat.js +1 -6
  119. package/dist/langfuse.js +3 -29
  120. package/dist/model-selection.js +3 -1
  121. package/dist/providers/adapter.d.ts +118 -0
  122. package/dist/providers/adapter.js +6 -0
  123. package/dist/providers/adapters/anthropic-adapter.d.ts +22 -0
  124. package/dist/providers/adapters/anthropic-adapter.js +204 -0
  125. package/dist/providers/adapters/codex-adapter.d.ts +26 -0
  126. package/dist/providers/adapters/codex-adapter.js +203 -0
  127. package/dist/providers/anthropic.d.ts +1 -0
  128. package/dist/providers/anthropic.js +10 -272
  129. package/dist/providers/codex.d.ts +21 -0
  130. package/dist/providers/codex.js +149 -330
  131. package/dist/providers/content.d.ts +1 -1
  132. package/dist/providers/content.js +2 -2
  133. package/dist/providers/context-manager.d.ts +18 -6
  134. package/dist/providers/context-manager.js +199 -223
  135. package/dist/providers/index.d.ts +9 -1
  136. package/dist/providers/index.js +73 -64
  137. package/dist/providers/loop-utils.d.ts +20 -0
  138. package/dist/providers/loop-utils.js +30 -0
  139. package/dist/providers/tool-loop.d.ts +12 -0
  140. package/dist/providers/tool-loop.js +251 -0
  141. package/dist/providers/utils.d.ts +19 -3
  142. package/dist/providers/utils.js +100 -29
  143. package/dist/secure-store.d.ts +8 -0
  144. package/dist/secure-store.js +80 -0
  145. package/dist/service.js +3 -28
  146. package/dist/sessions.d.ts +3 -0
  147. package/dist/sessions.js +147 -18
  148. package/dist/setup-templates.js +13 -25
  149. package/dist/setup.d.ts +10 -6
  150. package/dist/setup.js +84 -292
  151. package/dist/skills.js +3 -11
  152. package/dist/tools/agent-delegation.d.ts +19 -0
  153. package/dist/tools/agent-delegation.js +49 -0
  154. package/dist/tools/bash-tool.js +89 -34
  155. package/dist/tools/definitions.d.ts +199 -302
  156. package/dist/tools/definitions.js +70 -123
  157. package/dist/tools/execute-context.d.ts +13 -4
  158. package/dist/tools/fetch-tool.js +109 -13
  159. package/dist/tools/file-tools.js +7 -1
  160. package/dist/tools.d.ts +7 -7
  161. package/dist/tools.js +133 -151
  162. package/dist/types.d.ts +37 -30
  163. package/dist/utils.js +4 -6
  164. package/dist/voice.d.ts +1 -1
  165. package/dist/voice.js +17 -4
  166. package/package.json +33 -23
  167. package/templates/TOOLS.md +0 -27
  168. package/dist/__tests__/audit.test.js +0 -122
  169. package/dist/__tests__/code-agents-orchestrator.test.js +0 -216
  170. package/dist/__tests__/code-agents-sandbox.test.js +0 -163
  171. package/dist/__tests__/orchestrator.test.js +0 -425
  172. package/dist/__tests__/sandbox-bridge.test.js +0 -116
  173. package/dist/__tests__/sandbox-manager.test.js +0 -144
  174. package/dist/__tests__/sandbox-mount-security.test.js +0 -139
  175. package/dist/__tests__/sandbox-runtime.test.js +0 -176
  176. package/dist/__tests__/subagent.test.js +0 -240
  177. package/dist/__tests__/telegram.test.js +0 -42
  178. package/dist/code-agents/orchestrator.d.ts +0 -29
  179. package/dist/code-agents/orchestrator.js +0 -694
  180. package/dist/code-agents/worktree.d.ts +0 -40
  181. package/dist/code-agents/worktree.js +0 -215
  182. package/dist/dashboard/assets/index-BoTHPby4.js +0 -65
  183. package/dist/dashboard/assets/index-D4mufvBg.css +0 -1
  184. package/dist/dashboard.d.ts +0 -8
  185. package/dist/dashboard.js +0 -4071
  186. package/dist/discord.d.ts +0 -8
  187. package/dist/discord.js +0 -792
  188. package/dist/mcp-context-a8c.d.ts +0 -13
  189. package/dist/mcp-context-a8c.js +0 -34
  190. package/dist/orchestrator.d.ts +0 -15
  191. package/dist/orchestrator.js +0 -676
  192. package/dist/providers/openai.d.ts +0 -10
  193. package/dist/providers/openai.js +0 -355
  194. package/dist/sandbox/bridge.d.ts +0 -5
  195. package/dist/sandbox/bridge.js +0 -63
  196. package/dist/sandbox/index.d.ts +0 -5
  197. package/dist/sandbox/index.js +0 -4
  198. package/dist/sandbox/manager.d.ts +0 -7
  199. package/dist/sandbox/manager.js +0 -100
  200. package/dist/sandbox/mount-security.d.ts +0 -12
  201. package/dist/sandbox/mount-security.js +0 -122
  202. package/dist/sandbox/runtime.d.ts +0 -39
  203. package/dist/sandbox/runtime.js +0 -192
  204. package/dist/sandbox-utils.d.ts +0 -6
  205. package/dist/sandbox-utils.js +0 -36
  206. package/dist/subagent.d.ts +0 -19
  207. package/dist/subagent.js +0 -407
  208. package/dist/telegram.d.ts +0 -2
  209. package/dist/telegram.js +0 -11
  210. package/dist/tools/browser-tool.d.ts +0 -3
  211. package/dist/tools/browser-tool.js +0 -266
  212. package/sandbox/Dockerfile +0 -40
  213. /package/dist/__tests__/{audit.test.d.ts → code-agents-notifications.test.d.ts} +0 -0
  214. /package/dist/__tests__/{code-agents-orchestrator.test.d.ts → code-agents-worktrees.test.d.ts} +0 -0
  215. /package/dist/__tests__/{code-agents-sandbox.test.d.ts → codex-adapter.test.d.ts} +0 -0
  216. /package/dist/__tests__/{orchestrator.test.d.ts → codex-auth.test.d.ts} +0 -0
  217. /package/dist/__tests__/{sandbox-bridge.test.d.ts → codex-provider-gating.test.d.ts} +0 -0
  218. /package/dist/__tests__/{sandbox-manager.test.d.ts → codex-unified-loop.test.d.ts} +0 -0
  219. /package/dist/__tests__/{sandbox-mount-security.test.d.ts → config-security.test.d.ts} +0 -0
  220. /package/dist/__tests__/{sandbox-runtime.test.d.ts → cron-run.test.d.ts} +0 -0
  221. /package/dist/__tests__/{subagent.test.d.ts → digests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{telegram.test.d.ts → discord-attachments.test.d.ts} +0 -0
package/dist/skills.js CHANGED
@@ -64,17 +64,10 @@ export function checkEligibility(skill, toolConfig) {
64
64
  if (!toolConfig?.enabled) {
65
65
  return { eligible: false, reason: `Tools not enabled (needs: ${reqs.tools.join(', ')})` };
66
66
  }
67
- const missing = [];
68
- for (const tool of reqs.tools) {
69
- const t = tool.toLowerCase();
70
- if (t === 'browser') {
71
- if (!toolConfig.browser?.enabled)
72
- missing.push(tool);
73
- }
74
- // built-in tools are available whenever tools.enabled is true
75
- }
67
+ const supportedTools = new Set(['read', 'write', 'glob', 'bash', 'fetch', 'code_with_agent', 'check_code_agent', 'delegate_to_agent']);
68
+ const missing = reqs.tools.filter(tool => !supportedTools.has(tool.toLowerCase()));
76
69
  if (missing.length > 0) {
77
- return { eligible: false, reason: `Tools not enabled (needs: ${missing.join(', ')})` };
70
+ return { eligible: false, reason: `Unsupported tools requested: ${missing.join(', ')}` };
78
71
  }
79
72
  }
80
73
  return { eligible: true };
@@ -156,7 +149,6 @@ export function loadSkills(skillConfig, toolConfig) {
156
149
  entries: skillConfig?.entries,
157
150
  enabled: skillConfig?.enabled,
158
151
  toolEnabled: toolConfig?.enabled,
159
- browserEnabled: toolConfig?.browser?.enabled,
160
152
  });
161
153
  const cached = skillsCache.get(cacheKey);
162
154
  if (cached)
@@ -0,0 +1,19 @@
1
+ import type { Config } from '../types.js';
2
+ import type { ExecuteToolContext } from './execute-context.js';
3
+ export interface DelegateToAgentInput {
4
+ alias?: string;
5
+ task?: string;
6
+ mode?: string;
7
+ wait?: boolean;
8
+ allowSelf?: boolean;
9
+ }
10
+ export interface NormalizedDelegateToAgentInput {
11
+ alias: string;
12
+ task: string;
13
+ mode: 'new_thread';
14
+ wait: boolean;
15
+ allowSelf: boolean;
16
+ }
17
+ export type DelegateToAgentHandler = (input: NormalizedDelegateToAgentInput, config: Config, context?: ExecuteToolContext) => Promise<string>;
18
+ export declare function registerDelegateToAgentHandler(nextHandler: DelegateToAgentHandler | null): void;
19
+ export declare function executeDelegateToAgent(input: DelegateToAgentInput, config: Config, context?: ExecuteToolContext): Promise<string>;
@@ -0,0 +1,49 @@
1
+ const MAX_DELEGATION_DEPTH = 2;
2
+ const ALIAS_RE = /^[a-z][a-z0-9_-]{0,63}$/;
3
+ let handler = null;
4
+ function normalizeAlias(value) {
5
+ const normalized = typeof value === 'string'
6
+ ? value.trim().replace(/^@/, '').toLowerCase()
7
+ : '';
8
+ return ALIAS_RE.test(normalized) ? normalized : null;
9
+ }
10
+ export function registerDelegateToAgentHandler(nextHandler) {
11
+ handler = nextHandler;
12
+ }
13
+ export async function executeDelegateToAgent(input, config, context) {
14
+ const alias = normalizeAlias(input.alias);
15
+ if (!alias) {
16
+ return 'Error: alias must start with a letter and use only letters, numbers, underscore, or dash.';
17
+ }
18
+ const task = typeof input.task === 'string' ? input.task.trim() : '';
19
+ if (!task) {
20
+ return 'Error: task is required.';
21
+ }
22
+ const mode = input.mode?.trim() || 'new_thread';
23
+ if (mode !== 'new_thread') {
24
+ return 'Error: delegate_to_agent currently supports mode "new_thread" only.';
25
+ }
26
+ if (context?.channel !== 'discord') {
27
+ return 'Error: delegate_to_agent is currently Discord-only.';
28
+ }
29
+ if (context?.isDm) {
30
+ return 'Error: delegate_to_agent requires a Discord server channel. Direct messages do not support task threads.';
31
+ }
32
+ const depth = context?.delegationDepth ?? 0;
33
+ if (depth >= MAX_DELEGATION_DEPTH) {
34
+ return `Error: maximum agent delegation depth reached (${MAX_DELEGATION_DEPTH}).`;
35
+ }
36
+ if (!input.allowSelf && context?.threadAgentAlias === alias) {
37
+ return `Error: @${alias} cannot delegate to itself. Choose another agent profile.`;
38
+ }
39
+ if (!handler) {
40
+ return 'Error: Discord agent delegation is not available.';
41
+ }
42
+ return handler({
43
+ alias,
44
+ task,
45
+ mode,
46
+ wait: input.wait === true,
47
+ allowSelf: input.allowSelf === true,
48
+ }, config, context);
49
+ }
@@ -1,33 +1,51 @@
1
- import { exec } from 'child_process';
1
+ import { spawn } from 'child_process';
2
2
  import { isBashCommandSafe } from '../security.js';
3
3
  import { classifyCommandRisk, requiresApproval, createApprovalRequest, waitForApproval, } from '../exec-approval.js';
4
4
  import { isPathAllowed } from './path-utils.js';
5
5
  import { validateBashPaths } from './bash-path-validation.js';
6
- /** Env var name patterns that should never be exposed to model-executed commands. */
7
- const SENSITIVE_ENV_PATTERNS = [
8
- /api.?key/i, /token/i, /secret/i, /password/i, /credential/i,
9
- /^ANTHROPIC_/i, /^OPENAI_/i, /^CLAUDE/i, /^CODEX_/i, /^MINIMAX_/i,
10
- /^KIMI_/i, /^TOGETHER_/i, /^GROQ_/i, /^OPENROUTER_/i,
11
- ];
12
- /** Env vars that match SENSITIVE_ENV_PATTERNS but should be kept (e.g. tool auth). */
13
- const SENSITIVE_ENV_ALLOWLIST = new Set(['GH_TOKEN']);
14
- /** Common tool paths that may be missing when launched as a service/daemon. */
15
- const EXTRA_PATH_DIRS = ['/opt/homebrew/bin', '/opt/homebrew/sbin', '/usr/local/bin'];
16
- /** Create a sanitized copy of process.env with secrets stripped. */
17
- function sanitizeEnv() {
18
- const env = { ...process.env };
19
- for (const key of Object.keys(env)) {
20
- if (!SENSITIVE_ENV_ALLOWLIST.has(key) && SENSITIVE_ENV_PATTERNS.some(p => p.test(key))) {
21
- delete env[key];
6
+ import { sanitizeExecEnv } from '../env-sanitizer.js';
7
+ const SHELL_CONTROL_PATTERN = /[|&;<>()`$\n]/;
8
+ function tokenizeCommand(command) {
9
+ const tokens = [];
10
+ let current = '';
11
+ let quote = null;
12
+ for (let i = 0; i < command.length; i++) {
13
+ const ch = command[i];
14
+ if (ch === '\\' && quote !== "'") {
15
+ const next = command[i + 1];
16
+ if (next) {
17
+ current += next;
18
+ i++;
19
+ }
20
+ continue;
22
21
  }
22
+ if (quote) {
23
+ if (ch === quote) {
24
+ quote = null;
25
+ }
26
+ else {
27
+ current += ch;
28
+ }
29
+ continue;
30
+ }
31
+ if (ch === '"' || ch === "'") {
32
+ quote = ch;
33
+ continue;
34
+ }
35
+ if (/\s/.test(ch)) {
36
+ if (current) {
37
+ tokens.push(current);
38
+ current = '';
39
+ }
40
+ continue;
41
+ }
42
+ current += ch;
23
43
  }
24
- // Ensure common tool directories are in PATH (daemon/service launches often have a minimal PATH)
25
- const currentPath = env.PATH || '';
26
- const missing = EXTRA_PATH_DIRS.filter(d => !currentPath.includes(d));
27
- if (missing.length > 0) {
28
- env.PATH = `${currentPath}:${missing.join(':')}`;
29
- }
30
- return env;
44
+ if (quote)
45
+ return null;
46
+ if (current)
47
+ tokens.push(current);
48
+ return tokens;
31
49
  }
32
50
  export async function executeBash(command, cwd, config, context) {
33
51
  // Hard block: existing safety filter (always enforced)
@@ -79,21 +97,58 @@ export async function executeBash(command, cwd, config, context) {
79
97
  // Approved — fall through to execution below
80
98
  }
81
99
  }
100
+ if (SHELL_CONTROL_PATTERN.test(command)) {
101
+ return 'Error: Shell control operators are blocked in safe mode. Run a single executable with explicit arguments (no pipes, redirects, chaining, or subshell expansion).';
102
+ }
103
+ const argv = tokenizeCommand(command);
104
+ if (!argv || argv.length === 0) {
105
+ return 'Error: Invalid command syntax.';
106
+ }
82
107
  const timeout = config.bashTimeout || 30_000;
108
+ const [executable, ...args] = argv;
83
109
  return new Promise((res) => {
84
- exec(command, {
110
+ const child = spawn(executable, args, {
85
111
  cwd: cwd || undefined,
86
- timeout,
87
- env: sanitizeEnv(),
88
- maxBuffer: 5 * 1024 * 1024,
89
- }, (error, stdout, stderr) => {
90
- if (error) {
91
- const parts = [stdout, stderr, `Exit code: ${error.code ?? 'unknown'}`].filter(Boolean);
92
- res(parts.join('\n').slice(0, 50_000));
112
+ env: sanitizeExecEnv(),
113
+ stdio: ['ignore', 'pipe', 'pipe'],
114
+ shell: false,
115
+ });
116
+ let stdout = '';
117
+ let stderr = '';
118
+ let timedOut = false;
119
+ const timer = setTimeout(() => {
120
+ timedOut = true;
121
+ child.kill('SIGTERM');
122
+ setTimeout(() => child.kill('SIGKILL'), 1000);
123
+ }, timeout);
124
+ child.stdout.on('data', (chunk) => {
125
+ stdout += chunk.toString();
126
+ if (stdout.length > 60_000)
127
+ stdout = stdout.slice(-60_000);
128
+ });
129
+ child.stderr.on('data', (chunk) => {
130
+ stderr += chunk.toString();
131
+ if (stderr.length > 60_000)
132
+ stderr = stderr.slice(-60_000);
133
+ });
134
+ child.on('error', (error) => {
135
+ clearTimeout(timer);
136
+ res(`Error: ${error.message}`);
137
+ });
138
+ child.on('close', (code, signal) => {
139
+ clearTimeout(timer);
140
+ if (timedOut) {
141
+ const output = [stdout, stderr].filter(Boolean).join('\n');
142
+ res((output ? `${output}\n` : '') + `Exit code: timeout after ${timeout}ms`);
143
+ return;
144
+ }
145
+ const output = [stdout, stderr].filter(Boolean).join('\n').slice(0, 50_000);
146
+ if (code && code !== 0) {
147
+ const reason = signal ? `signal ${signal}` : `Exit code: ${code}`;
148
+ res((output ? `${output}\n` : '') + reason);
93
149
  return;
94
150
  }
95
- const output = [stdout, stderr].filter(Boolean).join('\n');
96
- res(output.slice(0, 50_000) || '(no output)');
151
+ res(output || '(no output)');
97
152
  });
98
153
  });
99
154
  }