@aryacode/dsh-plugin-arya-computer-use 1.0.2 → 1.0.3

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/index.js CHANGED
@@ -19,7 +19,7 @@
19
19
  import z from '@deepseek-ai/schemastery';
20
20
  import os from 'node:os';
21
21
  import { readFile, readFileSync, readdirSync, statSync } from 'node:fs';
22
- import { execFileSync } from 'node:child_process';
22
+ import { execFileSync, spawn } from 'node:child_process';
23
23
  import { dirname, join } from 'node:path';
24
24
  import { createRequire } from 'node:module';
25
25
  import { fileURLToPath, pathToFileURL } from 'node:url';
@@ -33,6 +33,7 @@ export const inject = ['tools'];
33
33
  const HERE = dirname(fileURLToPath(import.meta.url));
34
34
  const SKILL_DIR = join(HERE, 'skills', 'arya-desktop');
35
35
  const SKILL_FILE = join(SKILL_DIR, 'SKILL.md');
36
+ const CONSOLE_HIDER_SCRIPT = join(HERE, 'scripts', 'hide-consoles.ps1');
36
37
  const PROVIDER_NAME = 'arya-computer-use';
37
38
  const RANK = 350;
38
39
  const STATUS_PATH = '/plugins/arya/status';
@@ -295,7 +296,47 @@ export { ensureMarketSource as __ensureMarketSource };
295
296
 
296
297
  /* -------------------------------------------------------------------- main */
297
298
 
299
+ // Windows gives a console window titled after its executable to any console
300
+ // child that was not created with CREATE_NO_WINDOW. Both engines ship a JS shim
301
+ // (agent-browser/bin/agent-browser.js, agent-desktop/bin/agent-desktop) that
302
+ // runs the native helper with `stdio: 'inherit'` and `windowsHide: false`, so a
303
+ // black console stays on the desktop for as long as the helper lives - the
304
+ // browser daemon keeps it for the whole session - and closing it kills the
305
+ // helper, which is why it comes straight back. The plugin spawns the natives
306
+ // itself wherever the call site allows it; this watcher covers whatever else
307
+ // starts them (a lane restart, an older launcher, a helper started outside the
308
+ // plugin). It hides only classic console windows whose title or image names one
309
+ // of the two engine helpers, and hiding leaves stdio alone, so the helper keeps
310
+ // working.
311
+ function startConsoleHider(ctx) {
312
+ if (process.platform !== 'win32') return () => {};
313
+ try {
314
+ if (!statSync(CONSOLE_HIDER_SCRIPT).isFile()) return () => {};
315
+ } catch {
316
+ return () => {};
317
+ }
318
+ let child;
319
+ try {
320
+ child = spawn(
321
+ 'powershell.exe',
322
+ ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', CONSOLE_HIDER_SCRIPT, '-ParentPid', String(process.pid)],
323
+ { stdio: 'ignore', windowsHide: true },
324
+ );
325
+ } catch (error) {
326
+ ctx.logger.warn('dsh-arya-computer-use: could not start the console watcher: %s', error?.message ?? String(error));
327
+ return () => {};
328
+ }
329
+ child.on('error', error => {
330
+ ctx.logger.warn('dsh-arya-computer-use: console watcher stopped: %s', error?.message ?? String(error));
331
+ });
332
+ child.unref?.();
333
+ return () => {
334
+ try { child.kill(); } catch { /* already gone */ }
335
+ };
336
+ }
337
+
298
338
  export async function apply(ctx, config) {
339
+ const stopConsoleHider = startConsoleHider(ctx);
299
340
  registerSkillProvider(ctx);
300
341
  ensureMarketSource(ctx);
301
342
  void (async () => {
@@ -438,7 +479,9 @@ export async function apply(ctx, config) {
438
479
  state: readJson(runtime.stateFile),
439
480
  });
440
481
  }
441
- return () => {};
482
+ return () => {
483
+ stopConsoleHider();
484
+ };
442
485
  }
443
486
 
444
487
  function isLoopbackAddress(address) {
@@ -25,6 +25,25 @@ const require = createRequire(import.meta.url);
25
25
  const CLI_BIN = require.resolve('agent-desktop/bin/agent-desktop');
26
26
  const OCR_PS1 = join(HERE, '..', 'scripts', 'ocr.ps1');
27
27
 
28
+ // Windows console-window trap, the same one browser-launcher.mjs documents and
29
+ // avoids: bin/agent-desktop (the npm shim) spawns the native exe with
30
+ // stdio:'inherit' + windowsHide:false, so every tool call pops a console window
31
+ // on the user's desktop. Spawn the native binary directly instead; stdio stays
32
+ // piped and windowsHide holds. Null when the platform has no released binary,
33
+ // in which case the shim is the only way to run the CLI.
34
+ const NATIVE_BIN = (() => {
35
+ try {
36
+ const pkgDir = dirname(require.resolve('agent-desktop/package.json'));
37
+ const { resolve } = require('agent-desktop/lib/platform');
38
+ const entry = resolve(process.platform, process.arch);
39
+ if (!entry || !entry.released || !entry.binaryName) return null;
40
+ const bin = join(pkgDir, 'bin', entry.binaryName);
41
+ return existsSync(bin) ? bin : null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ })();
46
+
28
47
  // Engine state root: the engine's own default (~/.agent-desktop) — it creates
29
48
  // that root with its protected DACL and passes its private-file checks.
30
49
  // Do NOT point AGENT_DESKTOP_HOME at AppData\Local: inherited AppContainer ACEs
@@ -179,11 +198,17 @@ function runCli(args, { headed = effectiveHeaded() } = {}) {
179
198
  const env = { ...process.env };
180
199
  if (state.sessionStarted) env.AGENT_DESKTOP_SESSION = state.sessionId;
181
200
  return new Promise((resolve) => {
182
- const child = spawn(process.execPath, [CLI_BIN, ...argv], {
183
- stdio: ['ignore', 'pipe', 'pipe'],
184
- windowsHide: true,
185
- env,
186
- });
201
+ const child = NATIVE_BIN
202
+ ? spawn(NATIVE_BIN, argv, {
203
+ stdio: ['ignore', 'pipe', 'pipe'],
204
+ windowsHide: true,
205
+ env,
206
+ })
207
+ : spawn(process.execPath, [CLI_BIN, ...argv], {
208
+ stdio: ['ignore', 'pipe', 'pipe'],
209
+ windowsHide: true,
210
+ env,
211
+ });
187
212
  let stdout = '';
188
213
  let stderr = '';
189
214
  child.stdout.on('data', (d) => (stdout += d));
@@ -3,18 +3,41 @@
3
3
  // DSH launches this file instead of agent-browser itself so the browser lane can
4
4
  // follow the plugin's settings (tool surface, Chrome profile, session, CDP
5
5
  // endpoint, headed window) without editing the Cordis patch by hand. The real
6
- // server's stdio is inherited, so this process is a transparent pipe: stdin and
6
+ // server's stdio is piped through this process, a transparent relay: stdin and
7
7
  // stdout carry the MCP protocol untouched, and settings changes are picked up on
8
8
  // the next MCP restart.
9
+ //
10
+ // Two window traps on Windows, both avoided here:
11
+ // - Node ignores windowsHide when stdio is 'inherit', so stdio is piped.
12
+ // - bin/agent-browser.js (the npm CLI shim) spawns the native exe with
13
+ // stdio:'inherit' + windowsHide:false, which pops a console window for the
14
+ // grandchild on the user's desktop. We skip the shim and spawn the native
15
+ // exe directly; it speaks MCP over stdio itself.
9
16
 
10
17
  import { spawn } from 'node:child_process';
11
18
  import { existsSync, readFileSync } from 'node:fs';
12
19
  import { createRequire } from 'node:module';
13
- import { homedir } from 'node:os';
14
- import { join } from 'node:path';
20
+ import { homedir, platform, arch } from 'node:os';
21
+ import { dirname, join } from 'node:path';
15
22
 
16
23
  const require = createRequire(import.meta.url);
17
- const BIN = require.resolve('agent-browser/bin/agent-browser.js');
24
+ const SHIM = require.resolve('agent-browser/bin/agent-browser.js');
25
+
26
+ // Resolve the platform native binary shipped inside the agent-browser package,
27
+ // mirroring bin/agent-browser.js's naming (with the win32-arm64 → x64 fallback).
28
+ function nativeBin() {
29
+ const dir = dirname(require.resolve('agent-browser/package.json'));
30
+ const osKey = { darwin: 'darwin', linux: 'linux', win32: 'win32' }[platform()];
31
+ const archKey = { x64: 'x64', arm64: 'arm64' }[arch()];
32
+ if (!osKey || !archKey) return null;
33
+ const ext = osKey === 'win32' ? '.exe' : '';
34
+ let name = `agent-browser-${osKey}-${archKey}${ext}`;
35
+ if (osKey === 'win32' && archKey === 'arm64' && !existsSync(join(dir, name))) {
36
+ name = 'agent-browser-win32-x64.exe';
37
+ }
38
+ const bin = join(dir, name);
39
+ return existsSync(bin) ? bin : null;
40
+ }
18
41
 
19
42
  function controlDir() {
20
43
  const local = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local');
@@ -35,7 +58,7 @@ function readSettings() {
35
58
  const text = (value) => (typeof value === 'string' ? value.trim() : '');
36
59
 
37
60
  export function browserArgs(settings = {}) {
38
- const args = [BIN, 'mcp'];
61
+ const args = [SHIM, 'mcp'];
39
62
  const tools = text(settings.browserTools);
40
63
  if (tools) args.push('--tools', tools);
41
64
  const cdp = text(settings.browserCdp);
@@ -52,12 +75,19 @@ export function browserArgs(settings = {}) {
52
75
 
53
76
  function main() {
54
77
  const args = browserArgs(readSettings());
55
- const child = spawn(process.execPath, args, { stdio: 'inherit', windowsHide: true });
78
+ const native = nativeBin();
79
+ const child = native
80
+ ? spawn(native, args.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true })
81
+ : spawn(process.execPath, args, { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
56
82
  child.on('error', (error) => {
57
83
  process.stderr.write(`arya browser lane: could not start agent-browser: ${error.message}\n`);
58
84
  process.exit(1);
59
85
  });
60
86
  child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
87
+ process.stdin.pipe(child.stdin);
88
+ child.stdout.pipe(process.stdout);
89
+ child.stderr.pipe(process.stderr);
90
+ child.stdin.on('error', () => { /* child gone mid-write; the exit handler ends us */ });
61
91
  for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
62
92
  process.on(signal, () => { try { child.kill(signal); } catch { /* already gone */ } });
63
93
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aryacode/dsh-plugin-arya-computer-use",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Computer use for DSH/AryaCode: agent-desktop UIA engine, agent-browser web lane, takeover UI with a ghost cursor",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,6 +29,7 @@
29
29
  "scripts/windows-takeover.ps1",
30
30
  "scripts/windows-takeover.cs",
31
31
  "scripts/ocr.ps1",
32
+ "scripts/hide-consoles.ps1",
32
33
  "skills/arya-desktop/SKILL.md",
33
34
  "locale/en.json",
34
35
  "icon.svg",
@@ -0,0 +1,120 @@
1
+ # Arya Computer Use - hide the console windows the automation sidecars hand to
2
+ # the desktop.
3
+ #
4
+ # Windows gives a console window named after the executable to any console
5
+ # process that was not started with CREATE_NO_WINDOW. Both engines ship a JS
6
+ # shim (agent-browser/bin/agent-browser.js, agent-desktop/bin/agent-desktop)
7
+ # that runs the native helper with `stdio: 'inherit'` and `windowsHide: false`,
8
+ # and a native helper that outlives the call (the browser daemon) keeps its
9
+ # console for the whole session. The plugin starts the natives itself wherever
10
+ # it can, so the shim never runs, but a lane restart or a helper started by
11
+ # something else still puts a black console on screen - and closing it kills the
12
+ # helper, which is why it comes back.
13
+ #
14
+ # This watcher hides those windows instead of closing them. Hiding only removes
15
+ # the chrome: stdio is piped, the helper keeps running and stays reachable.
16
+ # Nothing else is touched - the window must be a classic console AND its title
17
+ # or its image path must name one of the two engine helpers.
18
+ #
19
+ # Started hidden by index.js for the lifetime of the host process
20
+ # (-ParentPid). -Once -Report prints what would be hidden and changes nothing.
21
+
22
+ param(
23
+ [int]$ParentPid = 0,
24
+ [int]$IntervalMs = 600,
25
+ [switch]$Once,
26
+ [switch]$Report
27
+ )
28
+
29
+ $ErrorActionPreference = 'Stop'
30
+
31
+ # Console window titles on Windows default to the application path, and the
32
+ # vendor shim does not overwrite them. A few releases rename the helper, so the
33
+ # match stays on the package name plus its bin directory.
34
+ $script:TitlePattern = 'agent-(browser|desktop)(-win32-(x64|arm64))?\.exe'
35
+ $script:PathPattern = '\\node_modules\\(agent-browser|agent-desktop)\\bin\\'
36
+
37
+ Add-Type -Namespace AryaCU -Name Win32 -MemberDefinition @'
38
+ public delegate bool EnumWindowsProc(System.IntPtr hWnd, System.IntPtr lParam);
39
+
40
+ [System.Runtime.InteropServices.DllImport("user32.dll")]
41
+ public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, System.IntPtr lParam);
42
+
43
+ [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
44
+ public static extern int GetWindowTextW(System.IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
45
+
46
+ [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
47
+ public static extern int GetClassNameW(System.IntPtr hWnd, System.Text.StringBuilder lpClassName, int nMaxCount);
48
+
49
+ [System.Runtime.InteropServices.DllImport("user32.dll")]
50
+ public static extern bool IsWindowVisible(System.IntPtr hWnd);
51
+
52
+ [System.Runtime.InteropServices.DllImport("user32.dll")]
53
+ public static extern bool ShowWindow(System.IntPtr hWnd, int nCmdShow);
54
+
55
+ [System.Runtime.InteropServices.DllImport("user32.dll")]
56
+ public static extern uint GetWindowThreadProcessId(System.IntPtr hWnd, out uint lpdwProcessId);
57
+ '@
58
+
59
+ # Returns the visible console windows that belong to one of the engine helpers.
60
+ function Get-EngineConsoles {
61
+ $found = New-Object System.Collections.ArrayList
62
+
63
+ $callback = [AryaCU.Win32+EnumWindowsProc] {
64
+ param([IntPtr]$hWnd, [IntPtr]$lParam)
65
+
66
+ if (-not [AryaCU.Win32]::IsWindowVisible($hWnd)) { return $true }
67
+
68
+ $class = New-Object System.Text.StringBuilder 256
69
+ [void][AryaCU.Win32]::GetClassNameW($hWnd, $class, 256)
70
+ # ConPTY terminals and Windows Terminal use their own classes; only the
71
+ # classic console host is a candidate.
72
+ if ($class.ToString() -ne 'ConsoleWindowClass') { return $true }
73
+
74
+ $title = New-Object System.Text.StringBuilder 1024
75
+ [void][AryaCU.Win32]::GetWindowTextW($hWnd, $title, 1024)
76
+ $text = $title.ToString()
77
+
78
+ $ownerPid = 0
79
+ [void][AryaCU.Win32]::GetWindowThreadProcessId($hWnd, [ref]$ownerPid)
80
+
81
+ $hit = $text -match $script:TitlePattern
82
+ if (-not $hit -and $ownerPid -gt 0) {
83
+ # The title can be blanked by the shim's parent; fall back to the image
84
+ # path when the process is readable (the host runs elevated, so it is).
85
+ try {
86
+ $path = (Get-Process -Id $ownerPid -ErrorAction Stop).Path
87
+ if ($path -and $path -match $script:PathPattern) { $hit = $true }
88
+ } catch { }
89
+ }
90
+
91
+ if ($hit) {
92
+ [void]$found.Add([pscustomobject]@{ Handle = $hWnd; Title = $text; OwnerPid = $ownerPid })
93
+ }
94
+ return $true
95
+ }
96
+
97
+ [void][AryaCU.Win32]::EnumWindows($callback, [IntPtr]::Zero)
98
+ return @($found.ToArray())
99
+ }
100
+
101
+ while ($true) {
102
+ try {
103
+ foreach ($window in Get-EngineConsoles) {
104
+ if ($Report) {
105
+ '{0} {1} {2}' -f $window.OwnerPid, $window.Handle, $window.Title
106
+ } else {
107
+ [void][AryaCU.Win32]::ShowWindow($window.Handle, 0) # SW_HIDE
108
+ }
109
+ }
110
+ } catch {
111
+ if ($Once) { throw }
112
+ }
113
+
114
+ if ($Once) { break }
115
+
116
+ Start-Sleep -Milliseconds ([Math]::Max(200, $IntervalMs))
117
+
118
+ # Leave with the host: an orphaned watcher would keep polling forever.
119
+ if ($ParentPid -gt 0 -and -not (Get-Process -Id $ParentPid -ErrorAction SilentlyContinue)) { break }
120
+ }