@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 +45 -2
- package/mcp/adapter-server.mjs +30 -5
- package/mcp/browser-launcher.mjs +36 -6
- package/package.json +2 -1
- package/scripts/hide-consoles.ps1 +120 -0
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) {
|
package/mcp/adapter-server.mjs
CHANGED
|
@@ -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 =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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));
|
package/mcp/browser-launcher.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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 = [
|
|
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
|
|
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.
|
|
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
|
+
}
|