@openagents-org/agent-launcher 0.2.147 → 0.2.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.147",
3
+ "version": "0.2.149",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -404,30 +404,18 @@ class ClaudeAdapter extends BaseAdapter {
404
404
  throw new Error('claude CLI not found. Install with: curl -fsSL https://claude.ai/install.sh | bash');
405
405
  }
406
406
 
407
- let systemPrompt = '\n' + buildClaudeSystemPrompt({
407
+ // The builder emits the correct tool-reference block for toolMode directly
408
+ // (mcp → workspace_* tools, skills → openagents-workspace skill), so there
409
+ // is no post-hoc string replacement to drift out of sync.
410
+ const systemPrompt = '\n' + buildClaudeSystemPrompt({
408
411
  agentName: this.agentName,
409
412
  workspaceId: this.workspaceId,
410
413
  channelName,
411
414
  mode: this._mode,
412
415
  browserEnabled,
416
+ toolMode: this.toolMode,
413
417
  });
414
418
 
415
- // In skills mode, replace MCP tool references with curl-based instructions
416
- if (this.toolMode === 'skills') {
417
- systemPrompt = systemPrompt
418
- .replace(
419
- 'Use workspace_get_history to read previous messages.\n' +
420
- 'Use workspace_get_agents to see other agents.\n' +
421
- 'Use workspace_put_todos to track your progress. ALWAYS create a to-do list when given multiple tasks or multi-step work.\n' +
422
- 'Use workspace_create_timer to set a reminder that wakes you up later.\n' +
423
- 'Use workspace_create_routine to set up recurring scheduled tasks (e.g. daily reviews).\n',
424
- 'Use the openagents-workspace skill (Bash + curl) for workspace operations:\n' +
425
- 'reading message history, discovering agents, sharing files, browsing,\n' +
426
- 'managing to-do lists, setting timers, and creating routines.\n' +
427
- 'Refer to the skill instructions for the exact curl commands.\n'
428
- );
429
- }
430
-
431
419
  const cmd = [claudeBin, '-p', prompt, '--output-format', 'stream-json', '--verbose'];
432
420
 
433
421
  cmd.push('--append-system-prompt', systemPrompt);
@@ -18,7 +18,7 @@ const { execSync, spawn } = require('child_process');
18
18
  const BaseAdapter = require('./base');
19
19
  const { formatAttachmentsForPrompt } = require('./utils');
20
20
  const { buildOpenCodeSkillMd, buildOpenCodeSystemPrompt } = require('./workspace-prompt');
21
- const { whichBinary, whereBinary } = require('../paths');
21
+ const { whichBinary, whereBinary, getEnhancedEnv } = require('../paths');
22
22
 
23
23
  const IS_WINDOWS = process.platform === 'win32';
24
24
 
@@ -58,8 +58,26 @@ function buildBrowserDirective(browserEnabled) {
58
58
 
59
59
  /**
60
60
  * Build the identity section common to all adapters.
61
+ *
62
+ * `toolMode` controls how the "read prior context" hint is phrased:
63
+ * - `'mcp'` → names the native `workspace_get_history` MCP tool.
64
+ * - `'skills'` → points at the openagents-workspace skill (Bash + curl),
65
+ * since no MCP server is spawned and that tool would not exist.
61
66
  */
62
- function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'execute') {
67
+ function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'execute', toolMode = 'mcp') {
68
+ const priorContext = toolMode === 'skills'
69
+ ? (
70
+ 'When you need prior context, use the openagents-workspace skill to ' +
71
+ `read this channel's recent messages (with \`channel="${channelName}"\`). ` +
72
+ 'Always specify the channel — the default may be different from where ' +
73
+ 'you are.\n'
74
+ )
75
+ : (
76
+ 'When you need prior context, call `workspace_get_history` with ' +
77
+ `\`channel="${channelName}"\` (the current channel). Without the ` +
78
+ 'channel argument the tool falls back to a default channel that may ' +
79
+ 'be different from where you are.\n'
80
+ );
63
81
  return (
64
82
  `You are agent '${agentName}' connected to an OpenAgents workspace.\n` +
65
83
  'Your text responses are automatically posted to the workspace chat ' +
@@ -68,17 +86,29 @@ function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'exe
68
86
  `- Workspace ID: ${workspaceId}\n` +
69
87
  `- Channel: ${channelName} (this is the channel you are currently speaking in)\n` +
70
88
  `- Mode: ${mode}\n\n` +
71
- 'When you need prior context, call `workspace_get_history` with ' +
72
- `\`channel="${channelName}"\` (the current channel). Without the ` +
73
- 'channel argument the tool falls back to a default channel that may ' +
74
- 'be different from where you are.\n'
89
+ priorContext
75
90
  );
76
91
  }
77
92
 
78
93
  /**
79
94
  * Build the multi-agent collaboration instructions.
95
+ *
96
+ * `toolMode` tailors the discovery pointer: `skills` mode sends the agent to
97
+ * the skill's "Discover Agents" section (Bash + curl), `mcp` mode names the
98
+ * native tool.
80
99
  */
81
- function buildCollaborationPrompt() {
100
+ function buildCollaborationPrompt(toolMode = 'mcp') {
101
+ const discover = toolMode === 'skills'
102
+ ? (
103
+ 'Before delegating, use the openagents-workspace skill (see its ' +
104
+ '"Discover Agents" section) to list the agents in this channel and read ' +
105
+ 'each one\'s description, then @mention the best-matched agent.\n'
106
+ )
107
+ : (
108
+ 'Before delegating, discover who is available and what they do: call ' +
109
+ '`workspace_get_agents`, or the discover endpoint for full descriptions, ' +
110
+ 'and match the task to an agent\'s description before @mentioning it.\n'
111
+ );
82
112
  return (
83
113
  '\n## Multi-Agent Collaboration\n' +
84
114
  'To delegate work to another agent, @mention them in your response. ' +
@@ -87,8 +117,7 @@ function buildCollaborationPrompt() {
87
117
  '— that wakes them up for nothing. Only @mention when you need them ' +
88
118
  'to do work. When the task is complete, report results to the user ' +
89
119
  'without @mentioning other agents.\n\n' +
90
- 'To discover available agents, use the workspace discover endpoint ' +
91
- 'or the workspace_get_agents tool (if available).\n'
120
+ discover
92
121
  );
93
122
  }
94
123
 
@@ -219,6 +248,17 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
219
248
  if (!disabled.has('browser')) {
220
249
  let s = '\n### Shared Browser\n\n';
221
250
 
251
+ s += (
252
+ 'The shared browser is a real cloud browser (backed by BrowserFabric) ' +
253
+ 'that all users and agents in the workspace share and can watch live. ' +
254
+ 'Drive it ONLY through these `/v1/browser` endpoints — never a local ' +
255
+ 'browser. Every tab is server-side; you interact by tab id.\n\n' +
256
+ 'When you open a tab, the JSON response includes an `id` (use it in every ' +
257
+ 'later call) and, in the cloud, a `live_url` — a link to the live, ' +
258
+ 'interactive view of that tab. Share the `live_url` with the user when ' +
259
+ 'they may want to watch or take over (e.g. a login or a CAPTCHA).\n\n'
260
+ );
261
+
222
262
  if (!isPlan) {
223
263
  s += (
224
264
  '**To browse a website**, exec these steps (use exec for each):\n' +
@@ -231,7 +271,7 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
231
271
  `${curl} -s -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID/snapshot\n` +
232
272
  `Step 3 — close tab: ` +
233
273
  `${curl} -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID\n` +
234
- '(Replace TAB_ID with the id from step 1 response)\n\n'
274
+ '(Replace TAB_ID with the `id` from the step 1 response)\n\n'
235
275
  );
236
276
  }
237
277
 
@@ -262,9 +302,31 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
262
302
  '**Type text:**\n' +
263
303
  `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
264
304
  ` ${baseUrl}/v1/browser/tabs/{tab_id}/type` +
265
- ` -d '{"selector":"CSS_SELECTOR","text":"TEXT"}'\`\n\n` +
305
+ ` -d '{"selector":"CSS_SELECTOR","text":"TEXT"}'\`\n` +
306
+ '(add `"append":true` to keep existing text instead of replacing it)\n\n' +
307
+ '**Press a key** (e.g. submit a form with Enter):\n' +
308
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
309
+ ` ${baseUrl}/v1/browser/tabs/{tab_id}/press_key` +
310
+ ` -d '{"key":"Enter"}'\`\n\n` +
311
+ '**Run JavaScript** in the page (returns the result):\n' +
312
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
313
+ ` ${baseUrl}/v1/browser/tabs/{tab_id}/evaluate` +
314
+ ` -d '{"expression":"document.title"}'\`\n\n` +
266
315
  '**Close tab:**\n' +
267
- `\`${curl} -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}\`\n`
316
+ `\`${curl} -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}\`\n\n` +
317
+ '**Clicks/typing use CSS selectors only** (no pixel coordinates). If a ' +
318
+ 'selector is hard to find, use `evaluate` to inspect the DOM first.\n\n' +
319
+ '**Persistent logins (contexts):** to reuse cookies/login across tabs ' +
320
+ 'and sessions, save the current tab as a named context, then open ' +
321
+ 'future tabs with that `context_id`:\n' +
322
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
323
+ ` ${baseUrl}/v1/browser/tabs/{tab_id}/persist -d '{"name":"my-login"}'\`\n` +
324
+ `\`${curl} -s -H "${h}" ${baseUrl}/v1/browser/contexts?network=${workspaceId}\`` +
325
+ ` (list saved contexts and their ids)\n` +
326
+ `\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
327
+ ` ${baseUrl}/v1/browser/tabs -d '{"url":"URL","context_id":"CONTEXT_ID",` +
328
+ `"network":"${workspaceId}","source":"openagents:${agentName}"}'\`` +
329
+ ' (open a tab already logged in)\n'
268
330
  );
269
331
  }
270
332
 
@@ -416,8 +478,24 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
416
478
  // Discovery
417
479
  sections.push(
418
480
  '\n### Discover Agents\n\n' +
419
- '**List all agents in the workspace (with status and role):**\n' +
420
- `\`${curl} -s -H "${h}" ${baseUrl}/v1/discover?network=${workspaceId}\`\n`
481
+ 'Before delegating, look up who is available and what they do — match the ' +
482
+ 'task to an agent by its `description`.\n\n' +
483
+ '**List all agents in the workspace (with descriptions, roles, status):**\n' +
484
+ `\`${curl} -s -H "${h}" ${baseUrl}/v1/discover?network=${workspaceId}\`\n` +
485
+ 'The response has `agents[]` (each with `address` `openagents:<name>`, ' +
486
+ '`description`, `role`, `status`, `agent_type`) and `channels[]` (each with ' +
487
+ 'an `address` `channel/<name>` and a `participants` list of agent names).\n\n' +
488
+ '**To list the agents in THIS channel with their descriptions:** call ' +
489
+ 'discover, find the entry in `channels[]` whose `address` is ' +
490
+ `\`channel/${channelName}\`, then keep the \`agents[]\` whose name is in ` +
491
+ 'that channel\'s `participants`. Example:\n' +
492
+ `\`${curl} -s -H "${h}" ${baseUrl}/v1/discover?network=${workspaceId} | ` +
493
+ 'jq --arg ch "channel/' + channelName + '" \'' +
494
+ '(.data.channels[]|select(.address==$ch).participants) as $p | ' +
495
+ '.data.agents[]|select((.address|sub("openagents:";"")) as $n|$p|index($n))|' +
496
+ '{name:(.address|sub("openagents:";"")),description,role,status}\'`\n' +
497
+ '(Discovery is workspace-wide — there is no per-channel discover endpoint, ' +
498
+ 'so cross-reference `participants` yourself as shown.)\n'
421
499
  );
422
500
 
423
501
  return sections.join('\n');
@@ -444,13 +522,11 @@ function buildGuardrails() {
444
522
  }
445
523
 
446
524
  /**
447
- * Build the system prompt for Claude adapter (MCP-based).
448
- * Claude gets identity + collaboration instructions but NOT API skills.
525
+ * The MCP tool-reference block for Claude in `mcp` tool mode. Each line names
526
+ * a native `workspace_*` MCP tool the agent can call directly.
449
527
  */
450
- function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = 'execute', browserEnabled = false }) {
451
- const parts = [];
452
- parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode));
453
- parts.push(
528
+ function buildClaudeMcpToolBlock() {
529
+ return (
454
530
  'Use workspace_get_history to read previous messages.\n' +
455
531
  'Use workspace_get_agents to see other agents.\n' +
456
532
  'Use workspace_put_todos to track your progress. ALWAYS create a to-do list when given multiple tasks or multi-step work.\n' +
@@ -460,8 +536,45 @@ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = '
460
536
  'Use workspace_write_knowledge to create or update shared knowledge base entries that persist across conversations.\n' +
461
537
  'Use workspace_read_knowledge to read knowledge entries by ID or slug (from @knowledge:slug mentions).\n'
462
538
  );
539
+ }
540
+
541
+ /**
542
+ * The skills tool-reference block for Claude in `skills` tool mode. In this
543
+ * mode there is no MCP server — every workspace operation goes through the
544
+ * openagents-workspace skill (Bash + curl), so we must NOT name any
545
+ * `workspace_*` MCP tool here.
546
+ */
547
+ function buildClaudeSkillsToolBlock() {
548
+ return (
549
+ 'IMPORTANT: READ the openagents-workspace skill FIRST, before any workspace\n' +
550
+ 'action. It is your ONLY interface to the workspace — every operation goes\n' +
551
+ 'through the exact Bash + curl commands documented there. Do not guess\n' +
552
+ 'endpoints or improvise; open and follow the skill instructions.\n' +
553
+ 'The openagents-workspace skill (Bash + curl) covers all workspace operations:\n' +
554
+ 'reading message history, discovering agents (and who is in this channel),\n' +
555
+ 'sharing files, browsing the shared browser, managing to-do lists, setting\n' +
556
+ 'timers, creating routines, sending inbox notifications, and reading/writing\n' +
557
+ 'the shared knowledge base.\n'
558
+ );
559
+ }
560
+
561
+ /**
562
+ * Build the system prompt for the Claude adapter.
563
+ *
564
+ * `toolMode` selects how the agent reaches workspace resources:
565
+ * - `'mcp'` → native `workspace_*` MCP tools (an MCP server is spawned).
566
+ * - `'skills'` → the openagents-workspace skill (Bash + curl); no MCP server.
567
+ *
568
+ * The tool-reference block is emitted directly for the chosen mode so it can
569
+ * never drift out of sync (previously the adapter string-replaced the MCP
570
+ * block, which silently leaked stale MCP tool names when the list changed).
571
+ */
572
+ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = 'execute', browserEnabled = false, toolMode = 'mcp' }) {
573
+ const parts = [];
574
+ parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, toolMode));
575
+ parts.push(toolMode === 'skills' ? buildClaudeSkillsToolBlock() : buildClaudeMcpToolBlock());
463
576
  parts.push(buildBrowserDirective(browserEnabled));
464
- parts.push(buildCollaborationPrompt());
577
+ parts.push(buildCollaborationPrompt(toolMode));
465
578
 
466
579
  if (mode === 'plan') {
467
580
  parts.push(
@@ -480,9 +593,9 @@ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = '
480
593
  */
481
594
  function buildOpenclawSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false }) {
482
595
  const parts = [];
483
- parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode));
596
+ parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills'));
484
597
  parts.push(buildBrowserDirective(browserEnabled));
485
- parts.push(buildCollaborationPrompt());
598
+ parts.push(buildCollaborationPrompt('skills'));
486
599
  parts.push(buildModePrompt(mode));
487
600
  parts.push(buildApiSkillsPrompt({
488
601
  endpoint, workspaceId, token, agentName, channelName, disabledModules, mode,
@@ -499,9 +612,9 @@ function buildOpenclawSkillMd({ endpoint, workspaceId, token, agentName, channel
499
612
  endpoint, workspaceId, token, agentName, channelName, disabledModules, mode: 'execute',
500
613
  });
501
614
 
502
- const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute');
615
+ const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute', 'skills');
503
616
  const directive = buildBrowserDirective(browserEnabled);
504
- const collab = buildCollaborationPrompt();
617
+ const collab = buildCollaborationPrompt('skills');
505
618
 
506
619
  const frontmatter = (
507
620
  '---\n' +
@@ -524,9 +637,9 @@ function buildOpenclawSkillMd({ endpoint, workspaceId, token, agentName, channel
524
637
  * Build system prompt for OpenCode adapter.
525
638
  */
526
639
  function buildOpenCodeSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false }) {
527
- const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode);
640
+ const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills');
528
641
  const directive = buildBrowserDirective(browserEnabled);
529
- const collab = buildCollaborationPrompt();
642
+ const collab = buildCollaborationPrompt('skills');
530
643
  const modePrompt = buildModePrompt(mode);
531
644
  const api = buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channelName, disabledModules, mode });
532
645
  return identity + directive + '\n' + collab + '\n' + modePrompt + '\n' + api + '\n' + buildGuardrails();
@@ -571,9 +684,9 @@ function buildClaudeSkillMd({ endpoint, workspaceId, token, agentName, channelNa
571
684
  mode: 'execute',
572
685
  });
573
686
 
574
- const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute');
687
+ const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute', 'skills');
575
688
  const directive = buildBrowserDirective(browserEnabled);
576
- const collab = buildCollaborationPrompt();
689
+ const collab = buildCollaborationPrompt('skills');
577
690
 
578
691
  const frontmatter =
579
692
  '---\n' +
@@ -602,9 +715,9 @@ function buildCursorSkillMd({ endpoint, workspaceId, token, agentName, channelNa
602
715
  mode: 'execute',
603
716
  });
604
717
 
605
- const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute');
718
+ const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, 'execute', 'skills');
606
719
  const directive = buildBrowserDirective(browserEnabled);
607
- const collab = buildCollaborationPrompt();
720
+ const collab = buildCollaborationPrompt('skills');
608
721
 
609
722
  const frontmatter =
610
723
  '---\n' +
@@ -626,6 +739,8 @@ module.exports = {
626
739
  buildModePrompt,
627
740
  buildGuardrails,
628
741
  buildApiSkillsPrompt,
742
+ buildClaudeMcpToolBlock,
743
+ buildClaudeSkillsToolBlock,
629
744
  buildClaudeSystemPrompt,
630
745
  buildOpenclawSystemPrompt,
631
746
  buildOpenclawSkillMd,
package/src/installer.js CHANGED
@@ -471,7 +471,7 @@ class Installer {
471
471
  // Prefer the resolved absolute binary path (quoted) over the bare name:
472
472
  // it's unambiguous and lets `--version` succeed for binaries resolved via
473
473
  // the package-bin fallback (which are not on PATH).
474
- const versionCmd = checkCmd || `"${binary}" --version`;
474
+ const versionCmd = checkCmd || this._versionProbeCommand(binary);
475
475
 
476
476
  const version = this._detectVersion(binary, versionCmd);
477
477
  const readiness = this._evaluateReadiness(agentType, entry, binary);
@@ -504,6 +504,56 @@ class Installer {
504
504
  };
505
505
  }
506
506
 
507
+ /**
508
+ * Build the `<binary> --version` probe command for a resolved binary path.
509
+ *
510
+ * When the resolved binary is a JS entry (`.mjs`/`.cjs`/`.js` — e.g. OpenClaw,
511
+ * whose package `bin` is `openclaw.mjs` and is what `_resolvePackageBin`
512
+ * returns when the `.cmd` shim isn't on PATH), it MUST be run through node.
513
+ * execSync goes through a shell; on Windows `cmd.exe "C:\…\openclaw.mjs"
514
+ * --version` finds no PATHEXT association for `.mjs`, so it ShellExecutes the
515
+ * file and the OS pops the "choose an app to open this .mjs file" dialog. The
516
+ * launcher polls health/version on a timer, so that dialog reappears over and
517
+ * over — even at idle, and stacked (one per poll). Prefixing node makes the
518
+ * probe run headless. Harmless on Unix (node runs a .mjs regardless of any
519
+ * shebang), so it's applied on every platform for consistency.
520
+ */
521
+ _versionProbeCommand(binary) {
522
+ if (/\.(mjs|cjs|js)$/i.test(binary)) {
523
+ return `"${this._nodeBinary()}" "${binary}" --version`;
524
+ }
525
+ return `"${binary}" --version`;
526
+ }
527
+
528
+ /**
529
+ * Resolve a node executable to run JS-entry CLIs with. Prefers the launcher's
530
+ * portable node, then a PATH lookup, then the bare name. Deliberately does NOT
531
+ * use process.execPath: when the core runs inside the Electron main process
532
+ * that path is the Electron binary, and `electron foo.mjs` would launch the
533
+ * app instead of running the script.
534
+ */
535
+ _nodeBinary() {
536
+ const isWin = process.platform === 'win32';
537
+ const nodeName = isWin ? 'node.exe' : 'node';
538
+ const portableDir = path.join(os.homedir(), '.openagents', 'nodejs');
539
+ for (const candidate of [
540
+ path.join(portableDir, nodeName),
541
+ path.join(portableDir, 'bin', nodeName),
542
+ ]) {
543
+ if (fs.existsSync(candidate)) return candidate;
544
+ }
545
+ try {
546
+ const found = execSync(isWin ? 'where node.exe' : 'which node', {
547
+ encoding: 'utf-8',
548
+ timeout: 5000,
549
+ stdio: ['pipe', 'pipe', 'pipe'],
550
+ env: getEnhancedEnv(),
551
+ }).split(/\r?\n/)[0].trim();
552
+ if (found) return found;
553
+ } catch {}
554
+ return 'node';
555
+ }
556
+
507
557
  /**
508
558
  * Detect a binary's version string, cached briefly. Returns null on failure.
509
559
  */