@aryacode/dsh-plugin-arya-computer-use 1.0.3 → 1.0.5
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/README.md +2 -2
- package/client.js +10 -7
- package/docs/PLAN.md +2 -1
- package/index.js +1 -0
- package/mcp/adapter-server.mjs +48 -13
- package/mcp/browser-launcher.mjs +69 -2
- package/mcp/settings.mjs +7 -4
- package/package.json +4 -1
- package/skills/arya-desktop/SKILL.md +13 -10
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Replaces [dsh-computer-use-win](https://github.com/Yu-tao-Li/dsh-computer-use-wi
|
|
|
36
36
|
| 3 | Web lane (`arya_browser` row), sign-in profiles, attach-to-your-Chrome, **wincu retirement** | planned |
|
|
37
37
|
| 4 | Lazy preset, notifications/clipboard/trace, npm publish | planned |
|
|
38
38
|
|
|
39
|
-
Lite 16 tool surface: `snapshot, find, get, is, click, set_value, type, press, scroll, list_windows, launch, wait, screenshot, ocr, batch, tools` (superset of wincu Lite 12; `set_value` fills a field through the accessibility value pattern without touching the pointer
|
|
39
|
+
Lite 16 tool surface: `snapshot, find, get, is, click, set_value, type, press, scroll, list_windows, launch, wait, screenshot, ocr, batch, tools` (superset of wincu Lite 12; `set_value` fills a field through the accessibility value pattern without touching the pointer — **the headless text path, use it for text entry**; `type` synthesizes real keystrokes, which on Windows is physical input, so it moves the pointer and focuses the window; `tools` manages headed/overlay/session state).
|
|
40
40
|
|
|
41
41
|
Full plan: [docs/PLAN.md](docs/PLAN.md).
|
|
42
42
|
|
|
@@ -77,7 +77,7 @@ The published package is `@aryacode/dsh-plugin-arya-computer-use` (npm, public).
|
|
|
77
77
|
| Show a ghost cursor | on | Draw a synthetic cursor where the agent acts; your pointer is never followed |
|
|
78
78
|
| Agent name | `Arya` | Drives the ghost-cursor tag and the progress-panel header (`Arya · Step 3 · acting`), 1–16 characters |
|
|
79
79
|
| Banner text | `Arya is controlling your computer` | One line, 1–120 characters |
|
|
80
|
-
| Real mouse and keyboard | off | Opted-in headed input for one conversation
|
|
80
|
+
| Real mouse and keyboard | off | Opted-in headed input for one conversation: **every** action then drives the real pointer and keyboard. It releases on `session_end` and expires after 5 minutes. Text entry never needs it — `set_value` is headless; `type` is physical on its own. |
|
|
81
81
|
| One conversation drives at a time | on | Refuses desktop tools in other conversations while a drive is live |
|
|
82
82
|
| Stop shortcut | Esc | Global kill switch; synthetic keystrokes are ignored, so it needs a real keypress |
|
|
83
83
|
| Tool preset | `lite` | `lite` lists all 16 desktop tools; `lazy` starts with tools/snapshot/click and reveals the rest on demand |
|
package/client.js
CHANGED
|
@@ -14,8 +14,8 @@ window.__ModuleLoader__.load({
|
|
|
14
14
|
remoteIndicatorVisible: true, indicatorText: 'Arya is controlling your computer', pointerLabel: 'Arya', indicatorPosition: 'top',
|
|
15
15
|
indicatorIdleMs: 800, stopShortcut: '', resumeToken: 0, previewToken: 0,
|
|
16
16
|
headedDefault: false, overlayDefault: true, driverLockEnabled: true,
|
|
17
|
-
toolPreset: 'lite',
|
|
18
|
-
browserTools: 'core', browserProfile: '', browserSession: '', browserStateFile: '', browserCdp: '', browserHeaded: false,
|
|
17
|
+
toolPreset: 'lite',
|
|
18
|
+
browserTools: 'core', browserProfile: '', browserSession: '', browserStateFile: '', browserCdp: '', browserHeaded: false, browserToolFlags: 'strip',
|
|
19
19
|
};
|
|
20
20
|
const equal = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
21
21
|
const dictionaries = {
|
|
@@ -36,11 +36,12 @@ window.__ModuleLoader__.load({
|
|
|
36
36
|
progressOpacity: '背景不透明度(%)', progressWidth: '宽度(像素)', progressHeight: '高度(像素)',
|
|
37
37
|
progressPosition: '浮窗位置', progress_topRight: '右上', progress_topLeft: '左上', progress_bottomRight: '右下', progress_bottomLeft: '左下', progress_custom: '自定义',
|
|
38
38
|
progressX: 'X 坐标', progressY: 'Y 坐标', progressShortcut: '浮窗解锁快捷键', progressShortcutHint: '按下后临时解锁浮窗以便拖动。留空表示不设置。',
|
|
39
|
-
toolSurfaceTitle: '工具范围', toolPreset: '工具预设', toolPresetHint: 'lite 直接列出全部 16 个工具;lazy 只暴露 tools / snapshot / click,其余按需加载。修改后需重启 MCP 生效。', toolPresetLite: 'lite · 全部 16 个', toolPresetLazy: 'lazy · 按需加载',
|
|
40
|
-
behaviorTitle: '操作方式', headedDefault: '
|
|
39
|
+
toolSurfaceTitle: '工具范围', toolPreset: '工具预设', toolPresetHint: 'lite 直接列出全部 16 个工具;lazy 只暴露 tools / snapshot / click,其余按需加载。修改后需重启 MCP 生效。', toolPresetLite: 'lite · 全部 16 个', toolPresetLazy: 'lazy · 按需加载',
|
|
40
|
+
behaviorTitle: '操作方式', headedDefault: '真实鼠标和键盘', headedDefaultHint: '默认关闭:输入通过后台无障碍模式完成,不会移动你的指针,也不会抢占焦点。开启后所有操作都使用真实键盘和鼠标,并会短暂聚焦目标窗口。type 工具始终使用真实按键并移动指针;set_value 在后台写入文本。',
|
|
41
41
|
overlayDefault: '引擎会话叠加层', overlayDefaultHint: '启动桌面会话时启用引擎自带的光标叠加层。Arya 的虚拟光标不依赖它。',
|
|
42
42
|
driverLock: '单会话驱动', driverLockHint: '同一时间只允许一个对话操作桌面;其他对话调用桌面工具会收到 BUSY 提示。关闭后最近使用的对话自动接管。',
|
|
43
43
|
browserTitle: '网页通道', browserTools: '工具范围', browserToolsHint: 'agent-browser 向 MCP 暴露的工具集。修改后需重启 MCP 生效。',
|
|
44
|
+
browserFlags: '精简工具定义', browserFlagsHint: '插件启动网页通道时已经设好域名、证书、会话等全局参数,每个工具里重复的这些参数可以删掉:strip 全部删除(约省 1.8 万 token),lean 保留会话与状态恢复相关项,off 保持 agent-browser 原样。修改后需重启 MCP 生效。', browserFlagsStrip: 'strip · 全部删除', browserFlagsLean: 'lean · 保留会话项', browserFlagsOff: 'off · 保持原样',
|
|
44
45
|
browserProfile: 'Chrome 配置', browserProfileHint: '配置名称(如 Default)或目录路径,让网页通道复用你的登录状态。',
|
|
45
46
|
browserSession: '会话名', browserSessionHint: '隔离的会话键;不同会话名的 Cookie 与标签页彼此独立。',
|
|
46
47
|
browserCdp: '连接我的浏览器', browserCdpHint: 'CDP 地址,例如 http://127.0.0.1:9222。用 --remote-debugging-port=9222 启动 Chrome 即可共享已登录的窗口。',
|
|
@@ -87,11 +88,12 @@ window.__ModuleLoader__.load({
|
|
|
87
88
|
progressOpacity: 'Background opacity (%)', progressWidth: 'Width (px)', progressHeight: 'Height (px)',
|
|
88
89
|
progressPosition: 'Overlay position', progress_topRight: 'Top right', progress_topLeft: 'Top left', progress_bottomRight: 'Bottom right', progress_bottomLeft: 'Bottom left', progress_custom: 'Custom',
|
|
89
90
|
progressX: 'X coordinate', progressY: 'Y coordinate', progressShortcut: 'Overlay unlock shortcut', progressShortcutHint: 'Press to temporarily unlock the overlay so you can drag it. Empty means none.',
|
|
90
|
-
toolSurfaceTitle: 'Tool surface', toolPreset: 'Tool preset', toolPresetHint: 'lite lists all 16 tools up front; lazy exposes tools / snapshot / click and loads the rest on demand. Changes apply after the next MCP restart.', toolPresetLite: 'lite · all 16', toolPresetLazy: 'lazy · load on demand',
|
|
91
|
-
behaviorTitle: 'Control style', headedDefault: 'Real mouse and keyboard', headedDefaultHint: 'Off by default: input goes through background accessibility patterns and never moves your pointer or steals focus. Turn on to use real keyboard/mouse input, which briefly focuses the target window.',
|
|
91
|
+
toolSurfaceTitle: 'Tool surface', toolPreset: 'Tool preset', toolPresetHint: 'lite lists all 16 tools up front; lazy exposes tools / snapshot / click and loads the rest on demand. Changes apply after the next MCP restart.', toolPresetLite: 'lite · all 16', toolPresetLazy: 'lazy · load on demand',
|
|
92
|
+
behaviorTitle: 'Control style', headedDefault: 'Real mouse and keyboard', headedDefaultHint: 'Off by default: input goes through background accessibility patterns and never moves your pointer or steals focus. Turn on to use real keyboard/mouse input for every action, which briefly focuses the target window. The type tool always uses real keystrokes and moves the pointer; set_value writes text in the background.',
|
|
92
93
|
overlayDefault: 'Engine session overlay', overlayDefaultHint: "Starts the desktop session with the engine's own cursor overlay. Arya's ghost cursor does not depend on it; leave it off unless you want the engine marker too.",
|
|
93
94
|
driverLock: 'One conversation drives at a time', driverLockHint: 'While one conversation is driving, desktop calls from other conversations receive a BUSY notice. Turn off to let the most recent conversation take over automatically.',
|
|
94
95
|
browserTitle: 'Web lane', browserTools: 'Tool surface', browserToolsHint: 'Which agent-browser tool profiles the MCP server exposes. Changing this applies on the next MCP restart.',
|
|
96
|
+
browserFlags: 'Trim tool schemas', browserFlagsHint: 'The plugin already sets domains, certificates, session and other host-wide flags when it starts the web lane, so the copies repeated inside every tool can go: strip removes them all (about 18k tokens saved), lean keeps the session and state-restore ones, off leaves the agent-browser schemas untouched. Changes apply after the next MCP restart.', browserFlagsStrip: 'strip · remove all', browserFlagsLean: 'lean · keep session flags', browserFlagsOff: 'off · keep as shipped',
|
|
95
97
|
browserProfile: 'Chrome profile', browserProfileHint: 'A profile name such as Default, or a directory path, so the web lane reuses your sign-ins.',
|
|
96
98
|
browserSession: 'Session', browserSessionHint: 'An isolated session key; cookies and tabs stay separate per name.',
|
|
97
99
|
browserCdp: 'Connect to my browser', browserCdpHint: 'A CDP endpoint such as http://127.0.0.1:9222. Start Chrome with --remote-debugging-port=9222 to share a signed-in window.',
|
|
@@ -350,12 +352,13 @@ window.__ModuleLoader__.load({
|
|
|
350
352
|
h('div', { className: 'arya-grid' },
|
|
351
353
|
switchItem('headedDefault', 'headedDefault', 'headedDefaultHint', arts.keyboard(draft.headedDefault)),
|
|
352
354
|
switchItem('overlayDefault', 'overlayDefault', 'overlayDefaultHint', arts.cursor(draft.overlayDefault)),
|
|
353
|
-
switchItem('driverLockEnabled', 'driverLock', 'driverLockHint', arts.shield(draft.driverLockEnabled)),
|
|
355
|
+
switchItem('driverLockEnabled', 'driverLock', 'driverLockHint', arts.shield(draft.driverLockEnabled)),
|
|
354
356
|
field(title(t('toolPreset'), 'toolPresetHint', { htmlFor: idPrefix + '-toolPreset' }), h('select', { id: idPrefix + '-toolPreset', value: draft.toolPreset, disabled, onChange: (e) => change('toolPreset', e.target.value) }, h('option', { value: 'lite' }, t('toolPresetLite')), h('option', { value: 'lazy' }, t('toolPresetLazy')))))),
|
|
355
357
|
h('section', { className: 'arya-section', 'aria-labelledby': idPrefix + '-browser-title' },
|
|
356
358
|
head(idPrefix + '-browser-title', 'browserTitle'),
|
|
357
359
|
h('div', { className: 'arya-grid' },
|
|
358
360
|
field(title(t('browserTools'), 'browserToolsHint', { htmlFor: idPrefix + '-browserTools' }), h('select', { id: idPrefix + '-browserTools', value: draft.browserTools, disabled, onChange: (e) => change('browserTools', e.target.value) }, ...['core', 'network', 'state', 'debug', 'tabs', 'react', 'mobile', 'all'].map((key) => h('option', { key, value: key }, key)))),
|
|
361
|
+
field(title(t('browserFlags'), 'browserFlagsHint', { htmlFor: idPrefix + '-browserToolFlags' }), h('select', { id: idPrefix + '-browserToolFlags', value: draft.browserToolFlags, disabled, onChange: (e) => change('browserToolFlags', e.target.value) }, ...[['strip', 'browserFlagsStrip'], ['lean', 'browserFlagsLean'], ['off', 'browserFlagsOff']].map(([value, label]) => h('option', { key: value, value }, t(label))))),
|
|
359
362
|
switchItem('browserHeaded', 'browserHeaded', 'browserHeadedHint', arts.monitor && arts.monitor(draft.browserHeaded) || arts.cursor(draft.browserHeaded))),
|
|
360
363
|
h('div', { className: 'arya-fields' },
|
|
361
364
|
field(title(t('browserProfile'), 'browserProfileHint', { htmlFor: idPrefix + '-browserProfile' }), h('input', { id: idPrefix + '-browserProfile', type: 'text', maxLength: 200, placeholder: 'Default', value: draft.browserProfile, disabled, onChange: (e) => change('browserProfile', e.target.value) })),
|
package/docs/PLAN.md
CHANGED
|
@@ -69,6 +69,7 @@ Web lane tools (second row `arya_browser` → `mcp__arya_browser__*`): open, sna
|
|
|
69
69
|
- First desktop call in a conversation → adapter runs `agent-desktop session start` (with `--cursor` when overlay enabled) → session id; every call injects `--session <id> --agent-id <conversation-id>` → refs are namespaced per conversation (no cross-conversation collisions).
|
|
70
70
|
- Turn end / Esc / session end → `cursor-overlay disable` + `session end` (renderer self-reclaims ≤3000 ms; held input released via SIGTERM semantics).
|
|
71
71
|
- **Headless default.** Per-conversation toggle: `arya_tools {operation:"headed", on:true}` — adapter injects `--headed` for that conversation. Skill documents when headed is required: `type`, right-click, double/triple-click, focus, cursor-moving input, notifications, shell surfaces (start-menu/tray/action-center).
|
|
72
|
+
- **Headed is a latch, so it must expire.** `runCli` appends `--headed` to *every* command, so a latch left on turns all later clicks/scrolls/launches into physical input — that is a mouse/keyboard takeover. `type` therefore passes `--headed` **per call** (headless `type` is `POLICY_DENIED` on Windows, so it is physical by nature and must not latch the conversation), the latch is released by `session_end`, and `HEADED_LEASE_MS` (5 min) self-releases it. Text entry should use `set_value`, which is headless.
|
|
72
73
|
- State root: `AGENT_DESKTOP_HOME=%LOCALAPPDATA%\dsh-plugin-arya-desktop\state` (set in row env).
|
|
73
74
|
|
|
74
75
|
## 5. Takeover UI (parity checklist)
|
|
@@ -88,7 +89,7 @@ Overlay cost: ~355 ms per headless action (click 427→782 ms). Setting default:
|
|
|
88
89
|
|
|
89
90
|
- Elevated/admin windows: UIPI boundary fails closed natively (agent-desktop) — same practical effect as wincu's admin protection; UAC secure desktop unaddressable by both.
|
|
90
91
|
- Occlusion/hidden/disabled targets: agent-desktop actionability checks (stronger than wincu POINT_COVERED: identity corroboration immediately before each write; `AMBIGUOUS_TARGET` refuses identical twins instead of mis-clicking).
|
|
91
|
-
- Foreground
|
|
92
|
+
- Foreground: headed actions **steal** the foreground and `ensureForeground()` promotes with a headed `mouse-click --xy` at the target's bounds center. **Borrow-and-return is NOT implemented** — no code restores the user's foreground window, so a headed action leaves the target focused. Mitigated (not fixed) by scoping `type` to one headed call, releasing the latch on `session_end`, and the 5-minute lease. A real restore is open work.
|
|
92
93
|
- Batch: native `desktop_batch` + adapter guard (1–20 steps, stop on first error/dialog/new-window, per-step delivery disposition).
|
|
93
94
|
- Blocked system shortcuts (alt+f4, win+l, alt+tab, …) refused natively without `--force`; `--force` not exposed by default.
|
|
94
95
|
- Delivery honesty: `delivered_verified / delivered_unverified / not_delivered` + `retry: safe|unsafe` — skill teaches: never retry unsafe; read `details.post_state`.
|
package/index.js
CHANGED
|
@@ -223,6 +223,7 @@ const fields = {
|
|
|
223
223
|
driverLockEnabled: z.boolean().default(true).description('One conversation drives the desktop at a time'),
|
|
224
224
|
toolPreset: z.union(['lite', 'lazy'].map(value => z.const(value))).default('lite').description('Tool surface size: lite lists all 16 tools; lazy starts with tools/snapshot/click and reveals the rest on demand'),
|
|
225
225
|
browserTools: z.union(['core', 'network', 'state', 'debug', 'tabs', 'react', 'mobile', 'all'].map(value => z.const(value))).default('core').description('agent-browser tool profiles exposed on the MCP row'),
|
|
226
|
+
browserToolFlags: z.union(['strip', 'lean', 'off'].map(value => z.const(value))).default('strip').description('Web lane schema size: strip drops the host/launch flags the plugin sets at launch from every tool, lean keeps session/restore/timeout, off keeps the agent-browser schemas unchanged'),
|
|
226
227
|
browserProfile: z.string().default('').description('Chrome profile name or directory the web lane reuses for sign-ins'),
|
|
227
228
|
browserSession: z.string().default('').description('Isolated browser session key'),
|
|
228
229
|
browserStateFile: z.string().default('').description('Saved sign-in state file for the web lane'),
|
package/mcp/adapter-server.mjs
CHANGED
|
@@ -62,6 +62,9 @@ const SETTINGS_FILE = join(PLUGIN_DIR, 'control', 'settings.json');
|
|
|
62
62
|
// The overlay host reads this to frame the app window that is being driven and
|
|
63
63
|
// to place the ghost cursor where the harness acts — never the real pointer.
|
|
64
64
|
const TARGET_FILE = join(PLUGIN_DIR, 'control', 'target.json');
|
|
65
|
+
// The host takeover lane persists its paused/stopped series here. The adapter
|
|
66
|
+
// watches it so the user's Esc stop also drops the physical-input latch.
|
|
67
|
+
const TAKEOVER_STATE_FILE = join(PLUGIN_DIR, 'control', 'state.json');
|
|
65
68
|
|
|
66
69
|
const VERSION = '0.1.0';
|
|
67
70
|
const MAX_BATCH = 20;
|
|
@@ -70,6 +73,7 @@ const state = {
|
|
|
70
73
|
sessionId: `arya-${randomBytes(4).toString('hex')}`,
|
|
71
74
|
sessionStarted: false,
|
|
72
75
|
headedOverride: null, // tools {operation:'headed'} per-conversation toggle
|
|
76
|
+
headedExpiresAt: 0, // headedOverride self-releases: a latch left on is a mouse/keyboard takeover
|
|
73
77
|
overlayOverride: null,
|
|
74
78
|
headedDefault: false,
|
|
75
79
|
overlayDefault: true,
|
|
@@ -92,13 +96,26 @@ function notifyToolsChanged() {
|
|
|
92
96
|
// swaps its registered tool generation.
|
|
93
97
|
send({ jsonrpc: '2.0', method: 'notifications/tools/list_changed' });
|
|
94
98
|
}
|
|
95
|
-
|
|
99
|
+
// A headed latch left on makes EVERY later call physical (runCli appends
|
|
100
|
+
// --headed to all of them), so it releases on its own. Per-call --headed is the
|
|
101
|
+
// normal path; the latch is only an escape hatch for surfaces that need a
|
|
102
|
+
// sequence of physical actions.
|
|
103
|
+
const HEADED_LEASE_MS = 5 * 60 * 1000;
|
|
104
|
+
const effectiveHeaded = () => {
|
|
105
|
+
if (state.headedOverride === true && Date.now() > state.headedExpiresAt) {
|
|
106
|
+
state.headedOverride = null;
|
|
107
|
+
state.headedExpiresAt = 0;
|
|
108
|
+
}
|
|
109
|
+
return state.headedOverride ?? state.headedDefault;
|
|
110
|
+
};
|
|
96
111
|
const effectiveOverlay = () => state.overlayOverride ?? state.overlayDefault;
|
|
97
112
|
|
|
98
113
|
setInterval(() => {
|
|
99
114
|
try {
|
|
100
115
|
const settings = JSON.parse(readFileSync(SETTINGS_FILE, 'utf8'));
|
|
101
|
-
|
|
116
|
+
// Strict: a settings file that omits the key must mean OFF. `!== false`
|
|
117
|
+
// read a missing key as ON, which is a silent mouse/keyboard takeover.
|
|
118
|
+
state.headedDefault = settings.headedDefault === true;
|
|
102
119
|
state.overlayDefault = settings.overlayDefault !== false;
|
|
103
120
|
const preset = settings.toolPreset === 'lazy' ? 'lazy' : 'lite';
|
|
104
121
|
if (preset !== state.toolPreset) {
|
|
@@ -109,6 +126,17 @@ setInterval(() => {
|
|
|
109
126
|
} catch {
|
|
110
127
|
// Settings file not written yet (plugin not loaded): keep the defaults.
|
|
111
128
|
}
|
|
129
|
+
try {
|
|
130
|
+
// Esc / stop lives in the host lane. A stopped series means the user pushed
|
|
131
|
+
// back: release the headed latch so the next series starts headless.
|
|
132
|
+
const takeover = JSON.parse(readFileSync(TAKEOVER_STATE_FILE, 'utf8'));
|
|
133
|
+
if (takeover?.paused === true && state.headedOverride === true) {
|
|
134
|
+
state.headedOverride = null;
|
|
135
|
+
state.headedExpiresAt = 0;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// No takeover state yet.
|
|
139
|
+
}
|
|
112
140
|
}, 1000).unref();
|
|
113
141
|
|
|
114
142
|
/* ------------------------------------------------------- overlay target */
|
|
@@ -323,8 +351,8 @@ const TOOLS = [
|
|
|
323
351
|
tool('get', 'Read one control: property text|value|title|bounds|role|states.', { ref_id: str, property: str }),
|
|
324
352
|
tool('is', 'Check one control: visible|enabled|checked|focused|expanded|selected.', { ref_id: str, property: str }),
|
|
325
353
|
tool('click', 'Press a control via its semantic action (no pixels). Verify by re-snapshot.', { ref_id: str, timeout_ms: int }),
|
|
326
|
-
tool('set_value', 'Set a control value via UIA SetValue (no keystrokes). Some edits reject
|
|
327
|
-
tool('type', 'Type keystrokes into a text target.
|
|
354
|
+
tool('set_value', 'Set a control value via UIA SetValue (no keystrokes, no pointer movement). This is the headless text path — use it first for any text entry. Some edits reject it — only then use type.', { ref_id: str, value: str, timeout_ms: int }),
|
|
355
|
+
tool('type', 'Type real keystrokes into a text target. On Windows keystroke synthesis is physical: this moves the pointer and briefly focuses the window. Prefer set_value (headless UIA write) for text entry.', { ref_id: str, text: str, interval_ms: int, timeout_ms: int }),
|
|
328
356
|
tool('press', 'Send a key chord (e.g. ctrl+s) to an app/window. Foreground queue: for non-frontmost apps expect delivered_unverified.', { combo: str, app: str, window_id: str, force: bool }),
|
|
329
357
|
tool('scroll', 'Scroll a scrollable control: direction up|down|left|right, amount in lines.', { ref_id: str, direction: str, amount: int, timeout_ms: int }),
|
|
330
358
|
tool('list_windows', 'List top-level windows (ids, titles, pids, responsiveness).', { app: str, timeout_ms: int }),
|
|
@@ -396,13 +424,15 @@ async function callTool(name, args = {}) {
|
|
|
396
424
|
case 'type':
|
|
397
425
|
return enqueue(async () => {
|
|
398
426
|
await ensureSession();
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
if (
|
|
427
|
+
// Engine fact on Windows: headless `type` is POLICY_DENIED — keystroke
|
|
428
|
+
// synthesis is headed-only. `type` is therefore physical by nature, so
|
|
429
|
+
// --headed is scoped to THIS call and the conversation is never latched
|
|
430
|
+
// into it. Latching was what made every later click/scroll move the
|
|
431
|
+
// user's real pointer.
|
|
432
|
+
const promote = await ensureForeground(args.ref_id);
|
|
433
|
+
if (promote) return envelopeResult(promote);
|
|
434
|
+
const env = parseEnvelope(await runCli(['type', args.ref_id, args.text ?? '', ...flagArgs([['--interval-ms', args.interval_ms], ['--timeout-ms', args.timeout_ms]])], { headed: true }));
|
|
435
|
+
if (env && env.ok !== true) env.hint = 'Keystroke synthesis is physical: it moves the pointer and focuses the window. Prefer set_value, which writes through the UIA value pattern headless.';
|
|
406
436
|
return env;
|
|
407
437
|
});
|
|
408
438
|
case 'press':
|
|
@@ -460,7 +490,8 @@ async function callTool(name, args = {}) {
|
|
|
460
490
|
}
|
|
461
491
|
if (op === 'headed') {
|
|
462
492
|
state.headedOverride = args.on === true;
|
|
463
|
-
|
|
493
|
+
state.headedExpiresAt = args.on === true ? Date.now() + HEADED_LEASE_MS : 0;
|
|
494
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: true, headed: effectiveHeaded(), headedDefault: state.headedDefault, lease_ms: args.on === true ? HEADED_LEASE_MS : 0, note: 'Headed drives the real mouse and keyboard for every action in this conversation. It releases on session_end and expires on its own. Prefer set_value for text so headed is not needed at all.' }) }] };
|
|
464
495
|
}
|
|
465
496
|
if (op === 'overlay') {
|
|
466
497
|
state.overlayOverride = args.on === true;
|
|
@@ -506,6 +537,9 @@ async function callTool(name, args = {}) {
|
|
|
506
537
|
await runCli(['session', 'gc']);
|
|
507
538
|
state.sessionStarted = false;
|
|
508
539
|
state.sessionId = `arya-${randomBytes(4).toString('hex')}`;
|
|
540
|
+
// Ending the driver session also releases the physical-input latch.
|
|
541
|
+
state.headedOverride = null;
|
|
542
|
+
state.headedExpiresAt = 0;
|
|
509
543
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: env.ok === true, note: 'new session starts on next action', engine: env }) }] };
|
|
510
544
|
}
|
|
511
545
|
return {
|
|
@@ -583,7 +617,8 @@ const INSTRUCTIONS = [
|
|
|
583
617
|
'Arya Computer Use: drive Windows apps through semantic UIA refs, not pixels.',
|
|
584
618
|
'Loop: snapshot (skeleton/compact first) -> act on @refs -> re-snapshot to verify.',
|
|
585
619
|
'Refs go stale after window changes: on STALE_REF/AMBIGUOUS_TARGET, re-snapshot.',
|
|
586
|
-
'
|
|
620
|
+
'set_value is the headless text path (UIA value pattern, pointer stays put). type synthesizes real keystrokes: it moves the pointer and focuses the window, scoped to that one call.',
|
|
621
|
+
'Headed mode (tools {operation:"headed",on:true}) makes EVERY action in this conversation physical input. Use it only when background input is refused; it releases on session_end and expires after 5 minutes.',
|
|
587
622
|
'Chromium windows settle slowly: snapshot with timeout_ms 10000-25000.',
|
|
588
623
|
'Verify stateful actions by re-read; delivered_verified = confirmed, delivered_unverified retry:unsafe = do not blind-repeat.',
|
|
589
624
|
'On ACTION_FAILED post_action_verification the action landed: read data.post_state, do NOT retry.',
|
package/mcp/browser-launcher.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
20
|
import { homedir, platform, arch } from 'node:os';
|
|
21
21
|
import { dirname, join } from 'node:path';
|
|
22
|
+
import { Transform } from 'node:stream';
|
|
22
23
|
|
|
23
24
|
const require = createRequire(import.meta.url);
|
|
24
25
|
const SHIM = require.resolve('agent-browser/bin/agent-browser.js');
|
|
@@ -57,6 +58,69 @@ function readSettings() {
|
|
|
57
58
|
|
|
58
59
|
const text = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
59
60
|
|
|
61
|
+
// Every agent-browser tool repeats the same host/launch flags in its schema, and
|
|
62
|
+
// the plugin already sets them on the server command line below. They are worth
|
|
63
|
+
// about 630 tokens per tool, so the relay rewrites tools/list responses to drop
|
|
64
|
+
// them. agent-browser rejects undeclared argument keys with an invalid-params
|
|
65
|
+
// error naming the key, so a caller that passes a dropped flag is told, not
|
|
66
|
+
// silently mis-ruled.
|
|
67
|
+
export const FLAG_KEYS_HOST = ['extraArgs', 'caCert', 'clearCaCert', 'useSystemCa', 'allowedDomains', 'namespace', 'idleTimeout'];
|
|
68
|
+
export const FLAG_KEYS_REST = ['restore', 'restoreSave', 'restoreCheckFn', 'restoreCheckText', 'restoreCheckUrl', 'session', 'timeoutMs'];
|
|
69
|
+
|
|
70
|
+
export function flagKeys(mode) {
|
|
71
|
+
if (mode === 'lean') return FLAG_KEYS_HOST;
|
|
72
|
+
if (mode === 'off') return [];
|
|
73
|
+
return [...FLAG_KEYS_HOST, ...FLAG_KEYS_REST];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function stripToolFlags(tools, keys) {
|
|
77
|
+
const drop = new Set(keys);
|
|
78
|
+
let removed = 0;
|
|
79
|
+
for (const tool of Array.isArray(tools) ? tools : []) {
|
|
80
|
+
const schema = tool?.inputSchema;
|
|
81
|
+
if (schema?.properties) {
|
|
82
|
+
for (const key of drop) if (key in schema.properties) { delete schema.properties[key]; removed++; }
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(schema?.required)) {
|
|
85
|
+
const kept = schema.required.filter((key) => !drop.has(key));
|
|
86
|
+
removed += schema.required.length - kept.length;
|
|
87
|
+
schema.required = kept;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return removed;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// MCP stdio is one JSON message per line, so the relay transforms line by line.
|
|
94
|
+
// Anything that is not a tools/list response is forwarded byte-for-byte.
|
|
95
|
+
export function transformLine(line, drop) {
|
|
96
|
+
if (!line.includes('"tools"')) return line;
|
|
97
|
+
let message;
|
|
98
|
+
try { message = JSON.parse(line); } catch { return line; }
|
|
99
|
+
if (!Array.isArray(message?.result?.tools)) return line;
|
|
100
|
+
return stripToolFlags(message.result.tools, drop) ? JSON.stringify(message) : line;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function createFlagStripTransform(keys) {
|
|
104
|
+
const drop = new Set(keys);
|
|
105
|
+
let pending = '';
|
|
106
|
+
return new Transform({
|
|
107
|
+
transform(chunk, encoding, callback) {
|
|
108
|
+
pending += chunk.toString('utf8');
|
|
109
|
+
let cut = pending.indexOf('\n');
|
|
110
|
+
while (cut >= 0) {
|
|
111
|
+
this.push(`${transformLine(pending.slice(0, cut), drop)}\n`);
|
|
112
|
+
pending = pending.slice(cut + 1);
|
|
113
|
+
cut = pending.indexOf('\n');
|
|
114
|
+
}
|
|
115
|
+
callback();
|
|
116
|
+
},
|
|
117
|
+
flush(callback) {
|
|
118
|
+
if (pending) this.push(transformLine(pending, drop));
|
|
119
|
+
callback();
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
60
124
|
export function browserArgs(settings = {}) {
|
|
61
125
|
const args = [SHIM, 'mcp'];
|
|
62
126
|
const tools = text(settings.browserTools);
|
|
@@ -74,7 +138,9 @@ export function browserArgs(settings = {}) {
|
|
|
74
138
|
}
|
|
75
139
|
|
|
76
140
|
function main() {
|
|
77
|
-
const
|
|
141
|
+
const settings = readSettings();
|
|
142
|
+
const args = browserArgs(settings);
|
|
143
|
+
const flagKeys_ = flagKeys(text(settings.browserToolFlags));
|
|
78
144
|
const native = nativeBin();
|
|
79
145
|
const child = native
|
|
80
146
|
? spawn(native, args.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true })
|
|
@@ -85,7 +151,8 @@ function main() {
|
|
|
85
151
|
});
|
|
86
152
|
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
87
153
|
process.stdin.pipe(child.stdin);
|
|
88
|
-
child.stdout.pipe(process.stdout);
|
|
154
|
+
if (flagKeys_.length) child.stdout.pipe(createFlagStripTransform(flagKeys_)).pipe(process.stdout);
|
|
155
|
+
else child.stdout.pipe(process.stdout);
|
|
89
156
|
child.stderr.pipe(process.stderr);
|
|
90
157
|
child.stdin.on('error', () => { /* child gone mid-write; the exit handler ends us */ });
|
|
91
158
|
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
package/mcp/settings.mjs
CHANGED
|
@@ -10,6 +10,7 @@ export const DEFAULT_SETTINGS = Object.freeze({
|
|
|
10
10
|
resumeToken: 0, previewToken: 0,
|
|
11
11
|
headedDefault: false, overlayDefault: true, driverLockEnabled: true, toolPreset: 'lite',
|
|
12
12
|
browserTools: 'core', browserProfile: '', browserSession: '', browserStateFile: '', browserCdp: '', browserHeaded: false,
|
|
13
|
+
browserToolFlags: 'strip',
|
|
13
14
|
});
|
|
14
15
|
|
|
15
16
|
export const STOP_SHORTCUT_PATTERN = /^(?:(?:Ctrl\+)?(?:Alt\+)?(?:Shift\+)?(?:Win\+)?(?:[A-Z0-9]|F(?:[1-9]|1[0-9]|2[0-4])|Space|Enter|Tab|Backspace|Delete|Insert|Home|End|PageUp|PageDown|Up|Down|Left|Right|Pause|CapsLock|NumLock|ScrollLock))?$/;
|
|
@@ -37,6 +38,7 @@ const SETTING_SWITCHES = ['progressOverlayEnabled', 'indicatorEnabled', 'cursorI
|
|
|
37
38
|
SETTING_CHOICES.progressOverlayPosition = ['top-right','top-left','bottom-right','bottom-left','custom'];
|
|
38
39
|
SETTING_CHOICES.progressOverlayMode = ['source','summary'];
|
|
39
40
|
SETTING_CHOICES.browserTools = ['core', 'network', 'state', 'debug', 'tabs', 'react', 'mobile', 'all'];
|
|
41
|
+
SETTING_CHOICES.browserToolFlags = ['strip', 'lean', 'off'];
|
|
40
42
|
SETTING_CHOICES.toolPreset = ['lite', 'lazy'];
|
|
41
43
|
export function repairStaleSettings(input) {
|
|
42
44
|
if (!input || typeof input !== 'object' || Array.isArray(input)) return { value: input, repaired: [] };
|
|
@@ -56,10 +58,10 @@ export function normalizeSettings(input = {}) {
|
|
|
56
58
|
if (typeof value.indicatorText !== 'string' || !value.indicatorText.trim() || value.indicatorText.length > 120 || /[\r\n\x00-\x1f]/.test(value.indicatorText)) {
|
|
57
59
|
throw new Error('indicatorText must be one line of 1–120 characters');
|
|
58
60
|
}
|
|
59
|
-
if (typeof value.pointerLabel !== 'string' || !value.pointerLabel.trim() || value.pointerLabel.trim().length > 16 || /[\r\n\x00-\x1f]/.test(value.pointerLabel)) {
|
|
60
|
-
throw new Error('pointerLabel must be one line of 1–16 characters');
|
|
61
|
-
}
|
|
62
|
-
value.pointerLabel = value.pointerLabel.trim();
|
|
61
|
+
if (typeof value.pointerLabel !== 'string' || !value.pointerLabel.trim() || value.pointerLabel.trim().length > 16 || /[\r\n\x00-\x1f]/.test(value.pointerLabel)) {
|
|
62
|
+
throw new Error('pointerLabel must be one line of 1–16 characters');
|
|
63
|
+
}
|
|
64
|
+
value.pointerLabel = value.pointerLabel.trim();
|
|
63
65
|
if (!['top', 'bottom'].includes(value.indicatorPosition)) throw new Error('indicatorPosition must be top or bottom');
|
|
64
66
|
if (!Number.isInteger(value.indicatorIdleMs) || value.indicatorIdleMs < 200 || value.indicatorIdleMs > 3000) throw new Error('indicatorIdleMs must be 200–3000');
|
|
65
67
|
value.stopShortcut = normalizeStopShortcut(value.stopShortcut);
|
|
@@ -72,6 +74,7 @@ export function normalizeSettings(input = {}) {
|
|
|
72
74
|
if (!SETTING_CHOICES.progressOverlayMode.includes(value.progressOverlayMode)) throw new Error('Invalid progressOverlayMode');
|
|
73
75
|
if (!SETTING_CHOICES.toolPreset.includes(value.toolPreset)) throw new Error('toolPreset must be one of ' + SETTING_CHOICES.toolPreset.join(', '));
|
|
74
76
|
if (!SETTING_CHOICES.browserTools.includes(value.browserTools)) throw new Error('browserTools must be one of ' + SETTING_CHOICES.browserTools.join(', '));
|
|
77
|
+
if (!SETTING_CHOICES.browserToolFlags.includes(value.browserToolFlags)) throw new Error('browserToolFlags must be one of ' + SETTING_CHOICES.browserToolFlags.join(', '));
|
|
75
78
|
for (const key of ['browserProfile', 'browserSession', 'browserStateFile', 'browserCdp']) {
|
|
76
79
|
if (typeof value[key] !== 'string') throw new Error(`${key} must be a string`);
|
|
77
80
|
if (/[\r\n\x00-\x1f]/.test(value[key])) throw new Error(`${key} must be one line`);
|
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.5",
|
|
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",
|
|
@@ -58,6 +58,9 @@
|
|
|
58
58
|
"bundle": {
|
|
59
59
|
"patch": "./cordis.patch.yml"
|
|
60
60
|
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"dsh": ">=0.2.1-rc.3 <0.3.0-0"
|
|
63
|
+
},
|
|
61
64
|
"client": {
|
|
62
65
|
"platform": "web",
|
|
63
66
|
"inject": [
|
|
@@ -18,11 +18,11 @@ Screenshots are evidence, not input.
|
|
|
18
18
|
thin and grow: pass `timeout_ms: 10000`–`25000` and re-snapshot after the
|
|
19
19
|
first contact.
|
|
20
20
|
2. **Act** — one action on one ref: `click`, `set_value`, `type`, `press`,
|
|
21
|
-
`scroll`.
|
|
22
|
-
value pattern and never touches the pointer. `type`
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
`scroll`. **Use `set_value` for every text entry**: it writes through the
|
|
22
|
+
accessibility value pattern, headless, and never touches the pointer. `type`
|
|
23
|
+
synthesizes real keystrokes — on Windows that is physical input, so it moves
|
|
24
|
+
the pointer and briefly focuses the window. It is the last resort, only for
|
|
25
|
+
an edit that rejects `set_value`.
|
|
26
26
|
3. **Verify** — re-`snapshot` (or `get`/`is` on the ref). The envelope tells
|
|
27
27
|
you what happened:
|
|
28
28
|
- `delivered_verified` — confirmed by readback.
|
|
@@ -43,11 +43,14 @@ Screenshots are evidence, not input.
|
|
|
43
43
|
`lease_directory_untrusted`, tell the user: restart AryaCode as Administrator.
|
|
44
44
|
- **Headed toggle.** Default is headless (no mouse/keyboard takeover): semantic
|
|
45
45
|
UIA input goes straight to the control and never moves the user's pointer.
|
|
46
|
-
|
|
47
|
-
menu, tray,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
Headed is an **escape hatch for surfaces that refuse background input**
|
|
47
|
+
(notifications, start menu, tray, right-click, drag, double-click). It is not
|
|
48
|
+
the answer to a text field — `set_value` does that headless. Turning it on
|
|
49
|
+
makes **every** action in this conversation physical: clicks move the real
|
|
50
|
+
pointer, keystrokes go to the foreground window. So: warn the user before
|
|
51
|
+
switching it on, call `tools {operation:"headed",on:false}` when the surface
|
|
52
|
+
is done, and end with `tools {operation:"session_end"}`, which releases it.
|
|
53
|
+
A latch left on also expires by itself after 5 minutes.
|
|
51
54
|
- **Launch.** Bare names resolve only under System32 (`notepad` works,
|
|
52
55
|
`chrome` does not). Use the absolute exe path. If many instances run, the
|
|
53
56
|
engine refuses to guess — that is correct; ask the user.
|