agent-nuvira 1.37.2 → 1.38.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 (37) hide show
  1. package/dist/agents/agents/runner.d.ts +82 -0
  2. package/dist/agents/agents/runner.d.ts.map +1 -1
  3. package/dist/agents/agents/runner.js +523 -1
  4. package/dist/agents/agents/runner.js.map +1 -1
  5. package/dist/agents/orchestrator.d.ts +30 -0
  6. package/dist/agents/orchestrator.d.ts.map +1 -1
  7. package/dist/agents/orchestrator.js +78 -7
  8. package/dist/agents/orchestrator.js.map +1 -1
  9. package/dist/cli/eval.d.ts +28 -0
  10. package/dist/cli/eval.d.ts.map +1 -0
  11. package/dist/cli/eval.js +224 -0
  12. package/dist/cli/eval.js.map +1 -0
  13. package/dist/cli/router.d.ts.map +1 -1
  14. package/dist/cli/router.js +4 -0
  15. package/dist/cli/router.js.map +1 -1
  16. package/dist/index.d.ts +3 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/learning/error-repair.d.ts +4 -1
  21. package/dist/learning/error-repair.d.ts.map +1 -1
  22. package/dist/learning/error-repair.js +61 -6
  23. package/dist/learning/error-repair.js.map +1 -1
  24. package/dist/learning/eval-framework.d.ts +231 -0
  25. package/dist/learning/eval-framework.d.ts.map +1 -0
  26. package/dist/learning/eval-framework.js +954 -0
  27. package/dist/learning/eval-framework.js.map +1 -0
  28. package/dist/web-dashboard/server.d.ts.map +1 -1
  29. package/dist/web-dashboard/server.js +32 -0
  30. package/dist/web-dashboard/server.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/web-dashboard/public/assets/index-DlQgCJgF.js +135 -0
  33. package/src/web-dashboard/public/assets/{index-WVBA85-3.js.map → index-DlQgCJgF.js.map} +1 -1
  34. package/src/web-dashboard/public/assets/index-GNtw2Ztg.css +1 -0
  35. package/src/web-dashboard/public/index.html +2 -2
  36. package/src/web-dashboard/public/assets/index-B08nDkSB.css +0 -1
  37. package/src/web-dashboard/public/assets/index-WVBA85-3.js +0 -135
@@ -40,6 +40,36 @@ export interface RunResult {
40
40
  duration: number;
41
41
  /** Error message if execSync threw */
42
42
  error?: string;
43
+ /** Whether dependencies were auto-installed before a retry */
44
+ dependencyInstallAttempted?: boolean;
45
+ /** Whether the dependency install succeeded */
46
+ dependencyInstallSucceeded?: boolean;
47
+ /** Package manager / tool used for the install (e.g. 'npm', 'brew', 'winget') */
48
+ dependencyInstallTool?: string;
49
+ /** Whether the tool itself had to be installed first (e.g. Homebrew) */
50
+ dependencyInstallToolInstalled?: boolean;
51
+ }
52
+ /** A detected dependency-install plan for a project */
53
+ export interface InstallPlan {
54
+ /** The package-manager tool to run (e.g. 'npm', 'pip', 'brew', 'cargo') */
55
+ tool: string;
56
+ /** The full install command to execute */
57
+ command: string;
58
+ /** The manifest file that triggered the plan */
59
+ manifest: string;
60
+ }
61
+ /** Result of a dependency-install attempt (including tool bootstrapping) */
62
+ export interface DependencyInstallResult {
63
+ /** Whether the install succeeded */
64
+ success: boolean;
65
+ /** The install command that was attempted */
66
+ command: string;
67
+ /** The package-manager tool used */
68
+ tool?: string;
69
+ /** Whether the tool itself was installed first */
70
+ toolInstalled?: boolean;
71
+ /** Human-readable detail for logs */
72
+ message?: string;
43
73
  }
44
74
  /**
45
75
  * RunnerAgent — Executes shell commands and captures output.
@@ -73,6 +103,58 @@ export declare class RunnerAgent extends Agent {
73
103
  * Execute a command directly on the host machine.
74
104
  * Validates the command first, and falls back to LLM suggestion if the command is not available.
75
105
  */
106
+ /**
107
+ * Heuristic: does this failure look like a missing dependency?
108
+ * Matches common "Cannot find module", "command not found", and ENOENT errors.
109
+ */
110
+ private looksLikeMissingDependency;
111
+ /**
112
+ * Detect which package manager a project needs based on its manifest files.
113
+ * Supports npm/yarn/pnpm, pip (requirements/setup/pyproject), bundler,
114
+ * cargo, go, composer, and dart pub.
115
+ */
116
+ private detectInstallPlan;
117
+ /**
118
+ * Check whether a CLI tool is available on PATH (cross-platform).
119
+ */
120
+ private commandExists;
121
+ /**
122
+ * Bootstrap-install a missing package-manager tool so that the project's
123
+ * dependencies can be installed. Handles Homebrew, winget, choco, npm,
124
+ * pip, cargo, and more — installing the tool itself if it is missing.
125
+ */
126
+ private installTool;
127
+ /** Install Node.js (which bundles npm) via the platform package manager. */
128
+ private installNodeViaPlatform;
129
+ /** Install Python via the platform package manager (so pip can be bootstrapped). */
130
+ private installPythonViaPlatform;
131
+ /** Install Ruby via the platform package manager. */
132
+ private installRubyViaPlatform;
133
+ /** Install PHP via the platform package manager. */
134
+ private installPhpViaPlatform;
135
+ /** Install Go via the platform package manager. */
136
+ private installGoViaPlatform;
137
+ /**
138
+ * Run an install command and return its outcome.
139
+ */
140
+ private runInstallCommand;
141
+ /**
142
+ * When no manifest exists, detect a missing interpreter/tool from the failed
143
+ * command itself (e.g. "python3 script.py" → python3 → install Python).
144
+ * This lets the runner install bare tools even in manifest-less directories.
145
+ */
146
+ private detectToolFromCommand;
147
+ /**
148
+ * Install dependencies for the project using the appropriate package manager
149
+ * (npm, pip, brew, cargo, etc.). If the package manager itself is missing,
150
+ * it is bootstrap-installed first (e.g. Homebrew on macOS, winget on Windows).
151
+ * When no manifest is present, falls back to installing the missing
152
+ * interpreter/tool referenced by the failed command.
153
+ *
154
+ * Controlled by context.metadata.autoInstallTools !== false — set to false
155
+ * to only attempt the install command without installing missing tools.
156
+ */
157
+ private installDependencies;
76
158
  private executeOnHost;
77
159
  /**
78
160
  * Fallback: ask the LLM what command to run based on the project context.
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/agents/agents/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAgB7C;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,6CAA6C;IAC7C,OAAO,EAAE,OAAO,CAAC;IACjB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,WAAW,iDAAiD;IAErE,+DAA+D;IAC/D,OAAO,CAAC,QAAQ,CAAC,CAAY;IAEvB,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;IAoC9E;;;;;;OAMG;YACW,gBAAgB;IAyB9B;;;OAGG;YACW,iBAAiB;IAmG/B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAgC1B;;;OAGG;YACW,aAAa;IA+G3B;;;OAGG;YACW,gBAAgB;CAqE/B"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/agents/agents/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAMH,OAAO,EAAE,KAAK,EAAE,KAAK,YAAY,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAsB7C;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,6CAA6C;IAC7C,OAAO,EAAE,OAAO,CAAC;IACjB,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,+CAA+C;IAC/C,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,iFAAiF;IACjF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,wEAAwE;IACxE,8BAA8B,CAAC,EAAE,OAAO,CAAC;CAC1C;AAED,uDAAuD;AACvD,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,4EAA4E;AAC5E,MAAM,WAAW,uBAAuB;IACtC,oCAAoC;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,oCAAoC;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qCAAqC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,WAAW,iDAAiD;IAErE,+DAA+D;IAC/D,OAAO,CAAC,QAAQ,CAAC,CAAY;IAEvB,OAAO,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;IAoC9E;;;;;;OAMG;YACW,gBAAgB;IAyB9B;;;OAGG;YACW,iBAAiB;IAmG/B;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAgC1B;;;OAGG;IACH;;;OAGG;IACH,OAAO,CAAC,0BAA0B;IAyBlC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAoDzB;;OAEG;IACH,OAAO,CAAC,aAAa;IAYrB;;;;OAIG;IACH,OAAO,CAAC,WAAW;IA+HnB,4EAA4E;IAC5E,OAAO,CAAC,sBAAsB;IAyC9B,oFAAoF;IACpF,OAAO,CAAC,wBAAwB;IA+BhC,qDAAqD;IACrD,OAAO,CAAC,sBAAsB;IA0B9B,oDAAoD;IACpD,OAAO,CAAC,qBAAqB;IA2B7B,mDAAmD;IACnD,OAAO,CAAC,oBAAoB;IAoB5B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAgBzB;;;;OAIG;IACH,OAAO,CAAC,qBAAqB;IAkC7B;;;;;;;;;OASG;IACH,OAAO,CAAC,mBAAmB;YAwEb,aAAa;IA4J3B;;;OAGG;YACW,gBAAgB;CAqE/B"}
@@ -35,6 +35,10 @@ const MAX_OUTPUT_LENGTH = 10_000;
35
35
  const DEFAULT_TIMEOUT_MS = 120_000;
36
36
  /** Maximum number of fallback attempts when command validation fails */
37
37
  const MAX_FALLBACK_ATTEMPTS = 2;
38
+ /** Maximum number of auto-dependency-install + retry cycles */
39
+ const MAX_DEP_INSTALL_RETRIES = 1;
40
+ /** Timeout for installing a missing package-manager tool itself (10 min) */
41
+ const TOOL_INSTALL_TIMEOUT_MS = 600_000;
38
42
  /**
39
43
  * RunnerAgent — Executes shell commands and captures output.
40
44
  */
@@ -228,7 +232,483 @@ export class RunnerAgent extends Agent {
228
232
  * Execute a command directly on the host machine.
229
233
  * Validates the command first, and falls back to LLM suggestion if the command is not available.
230
234
  */
231
- async executeOnHost(context, command, fallbackAttempts = 0) {
235
+ /**
236
+ * Heuristic: does this failure look like a missing dependency?
237
+ * Matches common "Cannot find module", "command not found", and ENOENT errors.
238
+ */
239
+ looksLikeMissingDependency(command, stdout, stderr, execError) {
240
+ const haystack = `${command}\n${stdout}\n${stderr}\n${execError || ''}`.toLowerCase();
241
+ const signals = [
242
+ 'cannot find module',
243
+ 'module not found',
244
+ 'command not found',
245
+ 'is not recognized',
246
+ 'not recognized as an internal',
247
+ 'enoent',
248
+ 'no such file',
249
+ 'could not resolve',
250
+ 'cannot find package',
251
+ 'missing script: test',
252
+ 'npm error',
253
+ 'pip: command not found',
254
+ 'moduleerror',
255
+ 'unable to resolve',
256
+ 'could not find',
257
+ 'is not installed',
258
+ 'not found in path',
259
+ 'cannot be found',
260
+ ];
261
+ return signals.some((s) => haystack.includes(s));
262
+ }
263
+ /**
264
+ * Detect which package manager a project needs based on its manifest files.
265
+ * Supports npm/yarn/pnpm, pip (requirements/setup/pyproject), bundler,
266
+ * cargo, go, composer, and dart pub.
267
+ */
268
+ detectInstallPlan(workingDir) {
269
+ // JavaScript / TypeScript — check lockfiles FIRST because a pnpm/yarn
270
+ // project also contains a package.json. Lockfile presence wins.
271
+ if (existsSync(join(workingDir, 'pnpm-lock.yaml'))) {
272
+ return { tool: 'pnpm', command: 'pnpm install', manifest: 'pnpm-lock.yaml' };
273
+ }
274
+ if (existsSync(join(workingDir, 'yarn.lock'))) {
275
+ return { tool: 'yarn', command: 'yarn install --frozen-lockfile', manifest: 'yarn.lock' };
276
+ }
277
+ if (existsSync(join(workingDir, 'package.json'))) {
278
+ return { tool: 'npm', command: 'npm install --no-audit --no-fund', manifest: 'package.json' };
279
+ }
280
+ // Python
281
+ if (existsSync(join(workingDir, 'requirements.txt'))) {
282
+ return { tool: 'pip', command: 'pip install -r requirements.txt', manifest: 'requirements.txt' };
283
+ }
284
+ if (existsSync(join(workingDir, 'pyproject.toml'))) {
285
+ return { tool: 'pip', command: 'pip install -e .', manifest: 'pyproject.toml' };
286
+ }
287
+ if (existsSync(join(workingDir, 'setup.py'))) {
288
+ return { tool: 'pip', command: 'pip install -e .', manifest: 'setup.py' };
289
+ }
290
+ // Ruby
291
+ if (existsSync(join(workingDir, 'Gemfile'))) {
292
+ return { tool: 'bundle', command: 'bundle install', manifest: 'Gemfile' };
293
+ }
294
+ // Rust
295
+ if (existsSync(join(workingDir, 'Cargo.toml'))) {
296
+ return { tool: 'cargo', command: 'cargo build', manifest: 'Cargo.toml' };
297
+ }
298
+ // Go
299
+ if (existsSync(join(workingDir, 'go.mod'))) {
300
+ return { tool: 'go', command: 'go mod download', manifest: 'go.mod' };
301
+ }
302
+ // PHP
303
+ if (existsSync(join(workingDir, 'composer.json'))) {
304
+ return { tool: 'composer', command: 'composer install', manifest: 'composer.json' };
305
+ }
306
+ // Dart / Flutter
307
+ if (existsSync(join(workingDir, 'pubspec.yaml'))) {
308
+ return { tool: 'dart', command: 'dart pub get', manifest: 'pubspec.yaml' };
309
+ }
310
+ return null;
311
+ }
312
+ /**
313
+ * Check whether a CLI tool is available on PATH (cross-platform).
314
+ */
315
+ commandExists(tool) {
316
+ try {
317
+ execSync(process.platform === 'win32' ? `where ${tool}` : `which ${tool}`, { stdio: 'ignore', timeout: 5000, shell: getHostShell() });
318
+ return true;
319
+ }
320
+ catch {
321
+ return false;
322
+ }
323
+ }
324
+ /**
325
+ * Bootstrap-install a missing package-manager tool so that the project's
326
+ * dependencies can be installed. Handles Homebrew, winget, choco, npm,
327
+ * pip, cargo, and more — installing the tool itself if it is missing.
328
+ */
329
+ installTool(tool) {
330
+ const platform = process.platform;
331
+ // ── npm / yarn / pnpm ────────────────────────────────────────────────
332
+ if (tool === 'npm' || tool === 'yarn' || tool === 'pnpm') {
333
+ // npm ships with Node.js. Only bootstrap Node when npm is actually
334
+ // missing — never reinstall an existing toolchain.
335
+ if (!this.commandExists('npm')) {
336
+ const nodeInstall = this.installNodeViaPlatform(platform);
337
+ if (!nodeInstall.success)
338
+ return nodeInstall;
339
+ }
340
+ if (tool === 'npm') {
341
+ // npm should now exist; verify in case the install didn't refresh PATH
342
+ return this.commandExists('npm')
343
+ ? { success: true, command: 'npm is now available', toolInstalled: true, message: 'npm is now available' }
344
+ : { success: false, command: 'npm was installed but is not on PATH', message: 'npm was installed but is not on PATH for this process — open a new terminal and retry.' };
345
+ }
346
+ // yarn / pnpm are installed via npm (which we just ensured exists)
347
+ return this.runInstallCommand(`npm install -g ${tool}`, process.cwd());
348
+ }
349
+ // ── pip ──────────────────────────────────────────────────────────────
350
+ if (tool === 'pip') {
351
+ if (this.commandExists('python3') || this.commandExists('python')) {
352
+ // Python exists but pip may not — bootstrap pip via ensurepip
353
+ const python = this.commandExists('python3') ? 'python3' : 'python';
354
+ return this.runInstallCommand(`${python} -m ensurepip --upgrade`, process.cwd());
355
+ }
356
+ // No Python at all — install it first
357
+ const pyInstall = this.installPythonViaPlatform(platform);
358
+ if (!pyInstall.success)
359
+ return pyInstall;
360
+ const python = this.commandExists('python3') ? 'python3' : 'python';
361
+ return this.runInstallCommand(`${python} -m ensurepip --upgrade`, process.cwd());
362
+ }
363
+ // ── Homebrew (macOS) ────────────────────────────────────────────────
364
+ if (tool === 'brew') {
365
+ // Install Homebrew itself — the official install script.
366
+ // NONINTERACTIVE=1 prevents the script from blocking on sudo/confirm
367
+ // prompts when stdio is piped.
368
+ return this.runInstallCommand('NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"', process.cwd());
369
+ }
370
+ // ── bundle (Ruby) ────────────────────────────────────────────────────
371
+ if (tool === 'bundle') {
372
+ if (this.commandExists('gem')) {
373
+ return this.runInstallCommand('gem install bundler', process.cwd());
374
+ }
375
+ const rb = this.installRubyViaPlatform(platform);
376
+ if (!rb.success)
377
+ return rb;
378
+ return this.runInstallCommand('gem install bundler', process.cwd());
379
+ }
380
+ // ── cargo (Rust) ─────────────────────────────────────────────────────
381
+ if (tool === 'cargo') {
382
+ // Rustup is the standard bootstrap installer
383
+ return this.runInstallCommand('curl --proto \'=https\' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y', process.cwd());
384
+ }
385
+ // ── go ───────────────────────────────────────────────────────────────
386
+ if (tool === 'go') {
387
+ if (platform === 'darwin' || platform === 'linux') {
388
+ return this.installGoViaPlatform(platform);
389
+ }
390
+ if (platform === 'win32') {
391
+ // winget ships the official Go installer
392
+ return this.runInstallCommand('winget install GoLang.Go --silent --accept-package-agreements --accept-source-agreements', process.cwd());
393
+ }
394
+ }
395
+ // ── composer (PHP) ───────────────────────────────────────────────────
396
+ if (tool === 'composer') {
397
+ // Always install to a user-writable dir ($HOME/.local/bin, or
398
+ // USERPROFILE on Windows) instead of /usr/local/bin, which requires
399
+ // sudo and doesn't exist on Apple Silicon. HOME is unset on Windows.
400
+ const home = process.env.HOME || process.env.USERPROFILE;
401
+ const localBin = home ? `${home}/.local/bin` : '.';
402
+ if (!this.commandExists('php')) {
403
+ // PHP missing — install it first (via brew/apt/winget)
404
+ const phpInstall = this.installPhpViaPlatform(platform);
405
+ if (!phpInstall.success)
406
+ return phpInstall;
407
+ }
408
+ return this.runInstallCommand(`mkdir -p "${localBin}" && curl -sS https://getcomposer.org/installer | php -- --install-dir="${localBin}" --filename=composer`, process.cwd());
409
+ }
410
+ // ── dart ─────────────────────────────────────────────────────────────
411
+ if (tool === 'dart') {
412
+ if (platform === 'darwin') {
413
+ // Bootstrap Homebrew first if missing (consistent with other tools)
414
+ const brewCmd = this.commandExists('brew')
415
+ ? 'brew install dart-lang/dart/dart'
416
+ : 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" && brew install dart-lang/dart/dart';
417
+ return this.runInstallCommand(brewCmd, process.cwd());
418
+ }
419
+ if (platform === 'linux') {
420
+ // Dart is NOT in stock Ubuntu/Debian repos — add Google's apt repo first
421
+ const dartCmd = [
422
+ 'apt-get update && apt-get install -y apt-transport-https wget gnupg',
423
+ 'wget -qO- https://dl-ssl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/dart.gpg',
424
+ 'echo "deb [signed-by=/usr/share/keyrings/dart.gpg] https://storage.googleapis.com/download.dartlang.org/linux/debian stable main" > /etc/apt/sources.list.d/dart.list',
425
+ 'apt-get update && apt-get install -y dart',
426
+ ].join(' && ');
427
+ return this.runInstallCommand(dartCmd, process.cwd());
428
+ }
429
+ if (platform === 'win32') {
430
+ return this.runInstallCommand('winget install Dart.Dart --silent --accept-package-agreements --accept-source-agreements', process.cwd());
431
+ }
432
+ }
433
+ return { success: false, command: '', message: `No bootstrap strategy for tool '${tool}' on ${platform}` };
434
+ }
435
+ /** Install Node.js (which bundles npm) via the platform package manager. */
436
+ installNodeViaPlatform(platform) {
437
+ if (platform === 'darwin') {
438
+ // macOS: prefer Homebrew; bootstrap Homebrew itself if missing
439
+ if (this.commandExists('brew')) {
440
+ return this.runInstallCommand('brew install node', process.cwd());
441
+ }
442
+ return this.runInstallCommand('NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" && brew install node', process.cwd());
443
+ }
444
+ if (platform === 'linux') {
445
+ // Linux: use the distro package manager, with NodeSource as a fallback
446
+ const candidates = [
447
+ 'apt-get update && apt-get install -y nodejs npm',
448
+ 'dnf install -y nodejs npm',
449
+ 'yum install -y nodejs npm',
450
+ 'curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && apt-get install -y nodejs',
451
+ ];
452
+ for (const cmd of candidates) {
453
+ const res = this.runInstallCommand(cmd, process.cwd());
454
+ if (res.success)
455
+ return res;
456
+ }
457
+ return { success: false, command: candidates.join(' | '), message: 'Could not install Node.js on Linux' };
458
+ }
459
+ if (platform === 'win32') {
460
+ // Windows: winget (preferred) → choco → MSI download
461
+ const candidates = [
462
+ 'winget install OpenJS.NodeJS.LTS --silent --accept-package-agreements --accept-source-agreements',
463
+ 'choco install nodejs -y',
464
+ 'powershell -NoProfile -Command "Invoke-WebRequest -Uri https://nodejs.org/dist/latest/node-v22.14.0-x64.msi -OutFile $env:TEMP\\node.msi; Start-Process msiexec -ArgumentList \'/i $env:TEMP\\node.msi /quiet\' -Wait"',
465
+ ];
466
+ for (const cmd of candidates) {
467
+ const res = this.runInstallCommand(cmd, process.cwd());
468
+ if (res.success)
469
+ return res;
470
+ }
471
+ return { success: false, command: candidates.join(' | '), message: 'Could not install Node.js on Windows' };
472
+ }
473
+ return { success: false, command: '', message: `Unsupported platform: ${platform}` };
474
+ }
475
+ /** Install Python via the platform package manager (so pip can be bootstrapped). */
476
+ installPythonViaPlatform(platform) {
477
+ if (platform === 'darwin') {
478
+ return this.commandExists('brew')
479
+ ? this.runInstallCommand('brew install python3', process.cwd())
480
+ : this.runInstallCommand('NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" && brew install python3', process.cwd());
481
+ }
482
+ if (platform === 'linux') {
483
+ const candidates = [
484
+ 'apt-get update && apt-get install -y python3 python3-pip',
485
+ 'dnf install -y python3 python3-pip',
486
+ ];
487
+ for (const cmd of candidates) {
488
+ const res = this.runInstallCommand(cmd, process.cwd());
489
+ if (res.success)
490
+ return res;
491
+ }
492
+ return { success: false, command: candidates.join(' | '), message: 'Could not install Python on Linux' };
493
+ }
494
+ if (platform === 'win32') {
495
+ const candidates = [
496
+ 'winget install Python.Python.3.12 --silent --accept-package-agreements --accept-source-agreements',
497
+ 'choco install python -y',
498
+ ];
499
+ for (const cmd of candidates) {
500
+ const res = this.runInstallCommand(cmd, process.cwd());
501
+ if (res.success)
502
+ return res;
503
+ }
504
+ return { success: false, command: candidates.join(' | '), message: 'Could not install Python on Windows' };
505
+ }
506
+ return { success: false, command: '', message: `Unsupported platform: ${platform}` };
507
+ }
508
+ /** Install Ruby via the platform package manager. */
509
+ installRubyViaPlatform(platform) {
510
+ if (platform === 'darwin') {
511
+ return this.commandExists('brew')
512
+ ? this.runInstallCommand('brew install ruby', process.cwd())
513
+ : this.runInstallCommand('NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" && brew install ruby', process.cwd());
514
+ }
515
+ if (platform === 'linux') {
516
+ const candidates = [
517
+ 'apt-get update && apt-get install -y ruby-full',
518
+ 'dnf install -y ruby',
519
+ ];
520
+ for (const cmd of candidates) {
521
+ const res = this.runInstallCommand(cmd, process.cwd());
522
+ if (res.success)
523
+ return res;
524
+ }
525
+ return { success: false, command: candidates.join(' | '), message: 'Could not install Ruby on Linux' };
526
+ }
527
+ if (platform === 'win32') {
528
+ return this.runInstallCommand('winget install RubyInstallerTeam.Ruby.3.2 --silent --accept-package-agreements --accept-source-agreements', process.cwd());
529
+ }
530
+ return { success: false, command: '', message: `Unsupported platform: ${platform}` };
531
+ }
532
+ /** Install PHP via the platform package manager. */
533
+ installPhpViaPlatform(platform) {
534
+ if (platform === 'darwin') {
535
+ const brewCmd = this.commandExists('brew')
536
+ ? 'brew install php'
537
+ : 'NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" && brew install php';
538
+ return this.runInstallCommand(brewCmd, process.cwd());
539
+ }
540
+ if (platform === 'linux') {
541
+ const candidates = [
542
+ 'apt-get update && apt-get install -y php-cli',
543
+ 'dnf install -y php-cli',
544
+ ];
545
+ for (const cmd of candidates) {
546
+ const res = this.runInstallCommand(cmd, process.cwd());
547
+ if (res.success)
548
+ return res;
549
+ }
550
+ return { success: false, command: candidates.join(' | '), message: 'Could not install PHP on Linux' };
551
+ }
552
+ if (platform === 'win32') {
553
+ return this.runInstallCommand('winget install PHP.PHP.8.3 --silent --accept-package-agreements --accept-source-agreements', process.cwd());
554
+ }
555
+ return { success: false, command: '', message: `Unsupported platform: ${platform}` };
556
+ }
557
+ /** Install Go via the platform package manager. */
558
+ installGoViaPlatform(platform) {
559
+ if (platform === 'darwin') {
560
+ return this.commandExists('brew')
561
+ ? this.runInstallCommand('brew install go', process.cwd())
562
+ : this.runInstallCommand('NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" && brew install go', process.cwd());
563
+ }
564
+ if (platform === 'linux') {
565
+ const candidates = [
566
+ 'apt-get update && apt-get install -y golang-go',
567
+ 'dnf install -y golang',
568
+ ];
569
+ for (const cmd of candidates) {
570
+ const res = this.runInstallCommand(cmd, process.cwd());
571
+ if (res.success)
572
+ return res;
573
+ }
574
+ return { success: false, command: candidates.join(' | '), message: 'Could not install Go on Linux' };
575
+ }
576
+ return { success: false, command: '', message: `Unsupported platform: ${platform}` };
577
+ }
578
+ /**
579
+ * Run an install command and return its outcome.
580
+ */
581
+ runInstallCommand(command, cwd) {
582
+ try {
583
+ execSync(command, {
584
+ cwd,
585
+ timeout: TOOL_INSTALL_TIMEOUT_MS,
586
+ stdio: 'pipe',
587
+ encoding: 'utf-8',
588
+ shell: getHostShell(),
589
+ maxBuffer: 2 * 1024 * 1024,
590
+ });
591
+ return { success: true, command, toolInstalled: true, message: `Installed via: ${command}` };
592
+ }
593
+ catch (err) {
594
+ return { success: false, command, message: err instanceof Error ? err.message : String(err) };
595
+ }
596
+ }
597
+ /**
598
+ * When no manifest exists, detect a missing interpreter/tool from the failed
599
+ * command itself (e.g. "python3 script.py" → python3 → install Python).
600
+ * This lets the runner install bare tools even in manifest-less directories.
601
+ */
602
+ detectToolFromCommand(command) {
603
+ if (!command)
604
+ return null;
605
+ const firstWord = command.trim().split(/\s+/)[0]?.toLowerCase() || '';
606
+ const tool = firstWord.split(/[\\/]/).pop() || firstWord; // handle paths like /usr/bin/node
607
+ const toolMap = {
608
+ node: 'npm',
609
+ npm: 'npm',
610
+ npx: 'npm',
611
+ python: 'pip',
612
+ python3: 'pip',
613
+ pip: 'pip',
614
+ pip3: 'pip',
615
+ go: 'go',
616
+ cargo: 'cargo',
617
+ rustc: 'cargo',
618
+ bundle: 'bundle',
619
+ bundler: 'bundle',
620
+ ruby: 'bundle',
621
+ composer: 'composer',
622
+ php: 'composer',
623
+ dart: 'dart',
624
+ flutter: 'dart',
625
+ yarn: 'yarn',
626
+ pnpm: 'pnpm',
627
+ brew: 'brew',
628
+ };
629
+ const mapped = toolMap[tool];
630
+ // Only install if the tool is genuinely missing (avoids re-installs)
631
+ if (mapped && !this.commandExists(mapped)) {
632
+ return mapped;
633
+ }
634
+ return null;
635
+ }
636
+ /**
637
+ * Install dependencies for the project using the appropriate package manager
638
+ * (npm, pip, brew, cargo, etc.). If the package manager itself is missing,
639
+ * it is bootstrap-installed first (e.g. Homebrew on macOS, winget on Windows).
640
+ * When no manifest is present, falls back to installing the missing
641
+ * interpreter/tool referenced by the failed command.
642
+ *
643
+ * Controlled by context.metadata.autoInstallTools !== false — set to false
644
+ * to only attempt the install command without installing missing tools.
645
+ */
646
+ installDependencies(workingDir, autoInstallTools = true, failedCommand) {
647
+ const plan = this.detectInstallPlan(workingDir);
648
+ if (!plan) {
649
+ // No manifest — try to bootstrap a missing interpreter/tool referenced
650
+ // by the failed command (e.g. "python3 script.py" when python3 is absent).
651
+ if (autoInstallTools && failedCommand) {
652
+ const missingTool = this.detectToolFromCommand(failedCommand);
653
+ if (missingTool) {
654
+ const installResult = this.installTool(missingTool);
655
+ return {
656
+ success: installResult.success,
657
+ command: failedCommand,
658
+ tool: missingTool,
659
+ toolInstalled: installResult.success,
660
+ message: installResult.success
661
+ ? `Auto-installed missing tool '${missingTool}' from command`
662
+ : `Missing tool '${missingTool}' could not be auto-installed: ${installResult.message}`,
663
+ };
664
+ }
665
+ }
666
+ return { success: false, command: '', message: 'No supported dependency manifest detected' };
667
+ }
668
+ // ── Ensure the package manager tool exists ─────────────────────────
669
+ if (!this.commandExists(plan.tool)) {
670
+ if (autoInstallTools) {
671
+ const installResult = this.installTool(plan.tool);
672
+ if (!installResult.success) {
673
+ return {
674
+ success: false,
675
+ command: plan.command,
676
+ tool: plan.tool,
677
+ toolInstalled: false,
678
+ message: `Package manager '${plan.tool}' is missing and could not be auto-installed: ${installResult.message}`,
679
+ };
680
+ }
681
+ // Tool was installed — retry the actual install command
682
+ const attempt = this.runInstallCommand(plan.command, workingDir);
683
+ return {
684
+ success: attempt.success,
685
+ command: plan.command,
686
+ tool: plan.tool,
687
+ toolInstalled: true,
688
+ message: attempt.success
689
+ ? `Installed missing tool '${plan.tool}', then ${plan.command}`
690
+ : `Tool installed but install failed: ${attempt.message}`,
691
+ };
692
+ }
693
+ return {
694
+ success: false,
695
+ command: plan.command,
696
+ tool: plan.tool,
697
+ toolInstalled: false,
698
+ message: `Package manager '${plan.tool}' is not installed (auto-install of tools is disabled)`,
699
+ };
700
+ }
701
+ // ── Tool exists — just run the install command ─────────────────────
702
+ const attempt = this.runInstallCommand(plan.command, workingDir);
703
+ return {
704
+ success: attempt.success,
705
+ command: plan.command,
706
+ tool: plan.tool,
707
+ toolInstalled: false,
708
+ message: attempt.success ? undefined : attempt.message,
709
+ };
710
+ }
711
+ async executeOnHost(context, command, fallbackAttempts = 0, depRetries = 0) {
232
712
  // Validate the command before executing
233
713
  const validation = this.isCommandAvailable(command, context.workingDirectory);
234
714
  if (!validation.available) {
@@ -282,6 +762,40 @@ export class RunnerAgent extends Agent {
282
762
  execError = error.message;
283
763
  }
284
764
  const duration = Date.now() - startTime;
765
+ // ── Auto-install missing dependencies and retry once ───────────────
766
+ // If the command failed because a module/command is missing, try to install
767
+ // dependencies (npm install / pip install / brew install / etc.) using the
768
+ // project's package manager — and bootstrap-install the package manager
769
+ // itself if it is missing. Then re-run the command. This lets the agent
770
+ // close tasks that need `npm install` (or any platform's toolchain) before
771
+ // they can run.
772
+ let depInstallAttempted = false;
773
+ let depInstallSucceeded = false;
774
+ let depInstallTool;
775
+ let depInstallToolInstalled = false;
776
+ if (exitCode !== 0 && depRetries < MAX_DEP_INSTALL_RETRIES && this.looksLikeMissingDependency(command, stdout, stderr, execError)) {
777
+ if (context.metadata.verboseLogging) {
778
+ logger.info(' 📦 Command failed — missing dependency detected, installing...');
779
+ }
780
+ // autoInstallTools defaults to true; set metadata.autoInstallTools=false
781
+ // to only run the install command without bootstrapping missing tools.
782
+ const autoInstallTools = context.metadata.autoInstallTools !== false;
783
+ const installResult = this.installDependencies(context.workingDirectory, autoInstallTools, command);
784
+ depInstallAttempted = true;
785
+ depInstallSucceeded = installResult.success;
786
+ depInstallTool = installResult.tool;
787
+ depInstallToolInstalled = installResult.toolInstalled === true;
788
+ if (context.metadata.verboseLogging) {
789
+ if (installResult.toolInstalled) {
790
+ logger.info(` 🛠️ Auto-installed missing tool '${installResult.tool}'`);
791
+ }
792
+ logger.info(` 📦 Dependency install ${installResult.success ? 'succeeded' : 'failed'}: ${installResult.command || installResult.message}`);
793
+ }
794
+ if (installResult.success) {
795
+ // Retry the original command once after a successful install
796
+ return this.executeOnHost(context, command, fallbackAttempts, depRetries + 1);
797
+ }
798
+ }
285
799
  const runResult = {
286
800
  success: exitCode === 0,
287
801
  command,
@@ -290,8 +804,16 @@ export class RunnerAgent extends Agent {
290
804
  stderr: stderr.slice(0, MAX_OUTPUT_LENGTH),
291
805
  duration,
292
806
  error: execError,
807
+ dependencyInstallAttempted: depInstallAttempted,
808
+ dependencyInstallSucceeded: depInstallSucceeded,
809
+ dependencyInstallTool: depInstallTool,
810
+ dependencyInstallToolInstalled: depInstallToolInstalled,
293
811
  };
294
812
  context.metadata['runResult'] = runResult;
813
+ context.metadata['dependencyInstallAttempted'] = depInstallAttempted;
814
+ context.metadata['dependencyInstallSucceeded'] = depInstallSucceeded;
815
+ context.metadata['dependencyInstallTool'] = depInstallTool;
816
+ context.metadata['dependencyInstallToolInstalled'] = depInstallToolInstalled;
295
817
  const lines = [];
296
818
  lines.push(`Command: ${command}`);
297
819
  lines.push(`Exit code: ${exitCode}`);