@aryacode/dsh-plugin-arya-computer-use 1.0.4 → 1.0.6
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 +9 -8
- package/client.js +2 -2
- package/docs/PLAN.md +2 -1
- package/mcp/adapter-server.mjs +216 -19
- package/mcp/settings.mjs +4 -1
- package/package.json +4 -1
- package/skills/arya-desktop/SKILL.md +13 -10
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Replaces [dsh-computer-use-win](https://github.com/Yu-tao-Li/dsh-computer-use-wi
|
|
|
8
8
|
|
|
9
9
|
| wincu complaint | Arya Computer Use |
|
|
10
10
|
| --- | --- |
|
|
11
|
-
| Slow (PowerShell + UIA backend) | Native Rust engine: snapshot
|
|
11
|
+
| Slow (PowerShell + UIA backend) | Native Rust engine: ~90 ms per call measured on this devbox (snapshot 90 ms, ref action 85 ms, `list-windows` 56 ms). Every CLI call costs ~85 ms of process start plus UIA init, so the number of calls, not the tool, sets the latency — `batch` runs up to 20 steps in one call (6 steps: 332 ms engine vs ~510 ms step by step) |
|
|
12
12
|
| Pixel alignment off | Ref-based semantic UIA — coordinates are never the targeting mechanism; identical twins are refused, not mis-clicked |
|
|
13
13
|
| Chinese UI/docs | English throughout |
|
|
14
14
|
| (kept) DSH integration + setup UI | Settings page, chat step rows, takeover overlay, Esc kill, tool presets |
|
|
@@ -31,12 +31,12 @@ Replaces [dsh-computer-use-win](https://github.com/Yu-tao-Li/dsh-computer-use-wi
|
|
|
31
31
|
| Phase | Deliverable | Status |
|
|
32
32
|
| --- | --- | --- |
|
|
33
33
|
| 0 | Evaluation spike: raw `agent-desktop mcp` row, drive Notepad/Explorer/Chrome — speed ✓, readable trees ✓, refs-not-pixels ✓ | ✅ accepted, spike removed |
|
|
34
|
-
| 1 | Adapter MCP server: Lite
|
|
34
|
+
| 1 | Adapter MCP server: Lite 22 tools, per-conversation session lifecycle, headed toggle + foreground promotion, screenshot→file path, WinRT OCR, skill | ✅ built, installed, driven in-host |
|
|
35
35
|
| 2 | Takeover UI: window rim + banner + Esc kill, configurable ghost cursor, settings page, chat step rows, driver lock, English UI | ✅ built + verified live |
|
|
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
|
|
39
|
+
Lite 22 tool surface: `snapshot, find, get, is, click, focus, double_click, right_click, expand, collapse, toggle, 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**; `expand`/`collapse`/`toggle` are semantic UIA patterns and stay headless; `focus`, `double_click` and `right-click` have no headless path on Windows, so each one drives the real pointer for that single call and never latches the conversation; `type` likewise synthesizes real keystrokes, which on Windows is physical input; `tools` manages headed/overlay/session state).
|
|
40
40
|
|
|
41
41
|
Full plan: [docs/PLAN.md](docs/PLAN.md).
|
|
42
42
|
|
|
@@ -47,7 +47,7 @@ package.json Cordis bundle manifest (dsh.bundle.patch) + pinned engine
|
|
|
47
47
|
cordis.patch.yml rows: arya-computer-use (Host), mcp-arya-desktop, mcp-arya-browser
|
|
48
48
|
index.js Host plugin entry (settings, driver lock, takeover mgmt, status route)
|
|
49
49
|
client.js Settings page + chat step rows (client plugin)
|
|
50
|
-
mcp/adapter-server.mjs adapter MCP server (Lite
|
|
50
|
+
mcp/adapter-server.mjs adapter MCP server (Lite 22 tool surface)
|
|
51
51
|
scripts/windows-takeover.cs window rim + banner + progress + ghost cursor + global Esc
|
|
52
52
|
scripts/ocr.ps1 WinRT OCR helper (Phase 1)
|
|
53
53
|
skills/arya-desktop/ observe-act loop guide (Phase 1)
|
|
@@ -77,10 +77,11 @@ 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
|
-
|
|
|
83
|
-
|
|
|
82
|
+
| Progress panel detail | `summary` | What the on-screen step panel prints: `summary` = short action lines, `source` = the assistant's raw text stream (the old default — it paints model text on your desktop) |
|
|
83
|
+
| Stop shortcut | Esc | Global kill switch; synthetic keystrokes are ignored, so it needs a real keypress. It now also gates the driver: desktop input tools return `COMPUTER_USE_STOPPED_BY_USER` for that turn (reads keep working) |
|
|
84
|
+
| Tool preset | `lite` | `lite` lists all 22 desktop tools; `lazy` starts with tools/snapshot/click and reveals the rest on demand |
|
|
84
85
|
| Tool surface (web) | `core` | agent-browser tool profile: core 29, network 9, state 27, debug 40, tabs 13, react 8, mobile 15, webmcp 4, all 156 |
|
|
85
86
|
| Chrome profile / Session | – / `arya` | Reuse a Chrome login state and name the browser session |
|
|
86
87
|
| Connect to my browser | – | `--cdp` port or URL to attach to a Chrome you already have open |
|
|
@@ -110,7 +111,7 @@ repair them itself at start-up - the status route reports the outcome under `mcp
|
|
|
110
111
|
|
|
111
112
|
## Tool presets
|
|
112
113
|
|
|
113
|
-
- `lite` (default) — all
|
|
114
|
+
- `lite` (default) — all 22 desktop tools listed up front.
|
|
114
115
|
- `lazy` — starts with `tools`, `snapshot`, `click`; everything else appears after `tools {operation:"load", names:[...]}`. The adapter advertises `tools.listChanged` and emits `notifications/tools/list_changed`, which DSH's MCP client re-syncs (`listChanged.onChanged → refreshTools`), so the new tools become callable immediately.
|
|
115
116
|
|
|
116
117
|
## License
|
package/client.js
CHANGED
|
@@ -37,7 +37,7 @@ window.__ModuleLoader__.load({
|
|
|
37
37
|
progressPosition: '浮窗位置', progress_topRight: '右上', progress_topLeft: '左上', progress_bottomRight: '右下', progress_bottomLeft: '左下', progress_custom: '自定义',
|
|
38
38
|
progressX: 'X 坐标', progressY: 'Y 坐标', progressShortcut: '浮窗解锁快捷键', progressShortcutHint: '按下后临时解锁浮窗以便拖动。留空表示不设置。',
|
|
39
39
|
toolSurfaceTitle: '工具范围', toolPreset: '工具预设', toolPresetHint: 'lite 直接列出全部 16 个工具;lazy 只暴露 tools / snapshot / click,其余按需加载。修改后需重启 MCP 生效。', toolPresetLite: 'lite · 全部 16 个', toolPresetLazy: 'lazy · 按需加载',
|
|
40
|
-
behaviorTitle: '操作方式', headedDefault: '
|
|
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 生效。',
|
|
@@ -89,7 +89,7 @@ window.__ModuleLoader__.load({
|
|
|
89
89
|
progressPosition: 'Overlay position', progress_topRight: 'Top right', progress_topLeft: 'Top left', progress_bottomRight: 'Bottom right', progress_bottomLeft: 'Bottom left', progress_custom: 'Custom',
|
|
90
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.',
|
|
91
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, which briefly focuses the target window.',
|
|
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.',
|
|
93
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.",
|
|
94
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.',
|
|
95
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.',
|
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/mcp/adapter-server.mjs
CHANGED
|
@@ -62,14 +62,79 @@ 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');
|
|
68
|
+
// The host publishes the current turn's activity lease here. The adapter reads it
|
|
69
|
+
// to recompute the same series key the takeover lane blocks a stopped series on.
|
|
70
|
+
const ACTIVITY_FILE = join(PLUGIN_DIR, 'control', 'activity.json');
|
|
65
71
|
|
|
66
72
|
const VERSION = '0.1.0';
|
|
67
73
|
const MAX_BATCH = 20;
|
|
68
74
|
|
|
75
|
+
// The engine's own batch runs entries in one process, through the same policy
|
|
76
|
+
// checks and dispatch as the CLI. Entry keys are the command's own field names: a
|
|
77
|
+
// positional is named after itself, a flag drops `--` and writes `-` as `_`
|
|
78
|
+
// (skills/agent-desktop/references/commands-system.md:170). Unknown keys are
|
|
79
|
+
// rejected, so each action declares the keys it forwards.
|
|
80
|
+
const NATIVE_BATCH_COMMANDS = new Map([
|
|
81
|
+
['snapshot', 'snapshot'], ['find', 'find'], ['get', 'get'], ['is', 'is'],
|
|
82
|
+
['click', 'click'], ['set_value', 'set-value'], ['press', 'press'],
|
|
83
|
+
['scroll', 'scroll'], ['wait', 'wait'], ['focus', 'focus'],
|
|
84
|
+
['double_click', 'double-click'], ['right_click', 'right-click'],
|
|
85
|
+
['expand', 'expand'], ['collapse', 'collapse'], ['toggle', 'toggle'],
|
|
86
|
+
]);
|
|
87
|
+
// The engine's --headed is global and applies to every batch entry, so a step
|
|
88
|
+
// that needs physical input cannot share one native batch with semantic steps:
|
|
89
|
+
// those run through the adapter one at a time, each with its own scoped flag.
|
|
90
|
+
const LOOP_BATCH_ACTIONS = new Set(['type', 'screenshot', 'ocr', 'focus', 'double_click', 'right_click']);
|
|
91
|
+
const BATCH_ARG_KEYS = {
|
|
92
|
+
snapshot: ['app', 'window_id', 'compact', 'skeleton', 'interactive_only', 'max_depth', 'root', 'timeout_ms'],
|
|
93
|
+
find: ['app', 'window_id', 'name', 'role', 'text', 'value', 'native_id', 'description', 'first', 'last', 'nth', 'limit', 'count', 'exact', 'root', 'timeout_ms'],
|
|
94
|
+
get: ['ref_id', 'property'],
|
|
95
|
+
is: ['ref_id', 'property'],
|
|
96
|
+
click: ['ref_id', 'timeout_ms'],
|
|
97
|
+
focus: ['ref_id', 'timeout_ms'],
|
|
98
|
+
double_click: ['ref_id', 'timeout_ms'],
|
|
99
|
+
right_click: ['ref_id', 'timeout_ms'],
|
|
100
|
+
expand: ['ref_id', 'timeout_ms'],
|
|
101
|
+
collapse: ['ref_id', 'timeout_ms'],
|
|
102
|
+
toggle: ['ref_id', 'timeout_ms'],
|
|
103
|
+
set_value: ['ref_id', 'value', 'timeout_ms'],
|
|
104
|
+
press: ['combo', 'app', 'window_id', 'force'],
|
|
105
|
+
scroll: ['ref_id', 'direction', 'amount', 'timeout_ms'],
|
|
106
|
+
wait: ['ms', 'app', 'window', 'element', 'text', 'timeout'],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function batchEntryArgs(action, args) {
|
|
110
|
+
const entry = {};
|
|
111
|
+
for (const key of BATCH_ARG_KEYS[action] ?? []) {
|
|
112
|
+
const value = action === 'wait' && key === 'timeout' ? (args.timeout ?? args.timeout_ms) : args[key];
|
|
113
|
+
if (value === undefined || value === null) continue;
|
|
114
|
+
entry[key] = value;
|
|
115
|
+
}
|
|
116
|
+
return entry;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// A batch entry keeps its payload — a find step must yield refs — but a snapshot
|
|
120
|
+
// tree is the largest thing the engine can return, and the batch cap allows 4 MB.
|
|
121
|
+
function trimBatchData(data) {
|
|
122
|
+
if (!data || !Array.isArray(data.results)) return data;
|
|
123
|
+
return {
|
|
124
|
+
...data,
|
|
125
|
+
results: data.results.map((entry) => (
|
|
126
|
+
entry?.command === 'snapshot' && entry.data?.tree
|
|
127
|
+
? { ...entry, data: { ...entry.data, tree: undefined, tree_omitted: true } }
|
|
128
|
+
: entry
|
|
129
|
+
)),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
69
133
|
const state = {
|
|
70
134
|
sessionId: `arya-${randomBytes(4).toString('hex')}`,
|
|
71
135
|
sessionStarted: false,
|
|
72
136
|
headedOverride: null, // tools {operation:'headed'} per-conversation toggle
|
|
137
|
+
headedExpiresAt: 0, // headedOverride self-releases: a latch left on is a mouse/keyboard takeover
|
|
73
138
|
overlayOverride: null,
|
|
74
139
|
headedDefault: false,
|
|
75
140
|
overlayDefault: true,
|
|
@@ -92,13 +157,26 @@ function notifyToolsChanged() {
|
|
|
92
157
|
// swaps its registered tool generation.
|
|
93
158
|
send({ jsonrpc: '2.0', method: 'notifications/tools/list_changed' });
|
|
94
159
|
}
|
|
95
|
-
|
|
160
|
+
// A headed latch left on makes EVERY later call physical (runCli appends
|
|
161
|
+
// --headed to all of them), so it releases on its own. Per-call --headed is the
|
|
162
|
+
// normal path; the latch is only an escape hatch for surfaces that need a
|
|
163
|
+
// sequence of physical actions.
|
|
164
|
+
const HEADED_LEASE_MS = 5 * 60 * 1000;
|
|
165
|
+
const effectiveHeaded = () => {
|
|
166
|
+
if (state.headedOverride === true && Date.now() > state.headedExpiresAt) {
|
|
167
|
+
state.headedOverride = null;
|
|
168
|
+
state.headedExpiresAt = 0;
|
|
169
|
+
}
|
|
170
|
+
return state.headedOverride ?? state.headedDefault;
|
|
171
|
+
};
|
|
96
172
|
const effectiveOverlay = () => state.overlayOverride ?? state.overlayDefault;
|
|
97
173
|
|
|
98
174
|
setInterval(() => {
|
|
99
175
|
try {
|
|
100
176
|
const settings = JSON.parse(readFileSync(SETTINGS_FILE, 'utf8'));
|
|
101
|
-
|
|
177
|
+
// Strict: a settings file that omits the key must mean OFF. `!== false`
|
|
178
|
+
// read a missing key as ON, which is a silent mouse/keyboard takeover.
|
|
179
|
+
state.headedDefault = settings.headedDefault === true;
|
|
102
180
|
state.overlayDefault = settings.overlayDefault !== false;
|
|
103
181
|
const preset = settings.toolPreset === 'lazy' ? 'lazy' : 'lite';
|
|
104
182
|
if (preset !== state.toolPreset) {
|
|
@@ -109,8 +187,59 @@ setInterval(() => {
|
|
|
109
187
|
} catch {
|
|
110
188
|
// Settings file not written yet (plugin not loaded): keep the defaults.
|
|
111
189
|
}
|
|
190
|
+
try {
|
|
191
|
+
// Esc / stop lives in the host lane. A stopped series means the user pushed
|
|
192
|
+
// back: release the headed latch so the next series starts headless.
|
|
193
|
+
const takeover = JSON.parse(readFileSync(TAKEOVER_STATE_FILE, 'utf8'));
|
|
194
|
+
if (takeover?.paused === true && state.headedOverride === true) {
|
|
195
|
+
state.headedOverride = null;
|
|
196
|
+
state.headedExpiresAt = 0;
|
|
197
|
+
}
|
|
198
|
+
} catch {
|
|
199
|
+
// No takeover state yet.
|
|
200
|
+
}
|
|
112
201
|
}, 1000).unref();
|
|
113
202
|
|
|
203
|
+
/* ---------------------------------------------------------- user stop */
|
|
204
|
+
|
|
205
|
+
// Esc lives in the host takeover lane: it blocks the series it was acting for,
|
|
206
|
+
// keyed by [sessionId, turn] from the activity lease (mcp/takeover.mjs:111,184).
|
|
207
|
+
// The adapter is a separate process, so it recomputes that key and refuses every
|
|
208
|
+
// input action for a blocked series. The banner says "Esc Stop"; until now the
|
|
209
|
+
// driver never honoured it. Reads stay available — the user asked to stop being
|
|
210
|
+
// driven, not to be observed. A new user turn is a new key, so the block releases
|
|
211
|
+
// exactly as it does in the host lane, with no settings change.
|
|
212
|
+
const STOPPED_BY_USER = 'COMPUTER_USE_STOPPED_BY_USER: The user stopped this operation series with Esc or their custom shortcut. Do not retry desktop input in this turn. A new user turn, including a request to continue, can use the desktop automatically; no settings change is required.';
|
|
213
|
+
const INPUT_TOOLS = new Set(['click', 'set_value', 'type', 'press', 'scroll', 'launch', 'focus', 'double_click', 'right_click', 'expand', 'collapse', 'toggle', 'batch']);
|
|
214
|
+
|
|
215
|
+
function stoppedSeriesMessage() {
|
|
216
|
+
try {
|
|
217
|
+
const activity = JSON.parse(readFileSync(ACTIVITY_FILE, 'utf8'));
|
|
218
|
+
if (!(activity.active === true && Number.isFinite(activity.expiresAt) && activity.expiresAt > Date.now())) return null;
|
|
219
|
+
const saved = JSON.parse(readFileSync(TAKEOVER_STATE_FILE, 'utf8'));
|
|
220
|
+
const key = JSON.stringify([activity.sessionId, activity.turn]);
|
|
221
|
+
if (saved?.blockedSeries && Object.prototype.hasOwnProperty.call(saved.blockedSeries, key)) return saved.blockedSeries[key];
|
|
222
|
+
return saved?.paused === true && saved?.stoppedSeriesKey === key ? (saved.pauseMessage || STOPPED_BY_USER) : null;
|
|
223
|
+
} catch {
|
|
224
|
+
return null; // no lease published yet: nothing has been stopped
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function stoppedResult(name, message) {
|
|
229
|
+
return envelopeResult({
|
|
230
|
+
version: '2.6',
|
|
231
|
+
ok: false,
|
|
232
|
+
command: name,
|
|
233
|
+
error: {
|
|
234
|
+
code: 'POLICY_DENIED',
|
|
235
|
+
message,
|
|
236
|
+
suggestion: 'Do not retry desktop input in this turn. Report that the user stopped the takeover; a new user turn can drive the desktop again.',
|
|
237
|
+
details: { kind: 'stopped_by_user' },
|
|
238
|
+
disposition: { delivery: 'not_delivered', retry: 'safe' },
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
114
243
|
/* ------------------------------------------------------- overlay target */
|
|
115
244
|
|
|
116
245
|
// The overlay frames the app window that is being driven and puts the ghost
|
|
@@ -181,7 +310,10 @@ async function noteTarget(name, args, result) {
|
|
|
181
310
|
if (name === 'list_windows') rememberWindows(data);
|
|
182
311
|
if (name === 'snapshot') rememberWindows(data);
|
|
183
312
|
if (args?.window_id) state.windowId = args.window_id;
|
|
184
|
-
|
|
313
|
+
// target.json feeds the rim and the ghost cursor only. With the overlay off,
|
|
314
|
+
// skip the CLI calls they cost (element bounds, sometimes a window list):
|
|
315
|
+
// ~85-140 ms per action spent on presentation nobody is looking at.
|
|
316
|
+
if (!effectiveOverlay() || !state.windowId) return;
|
|
185
317
|
const window = await windowRectFor(state.windowId);
|
|
186
318
|
const ref = args?.ref_id ?? data?.match?.ref_id ?? null;
|
|
187
319
|
let pointer = ref ? await elementPoint(ref) : null;
|
|
@@ -323,8 +455,14 @@ const TOOLS = [
|
|
|
323
455
|
tool('get', 'Read one control: property text|value|title|bounds|role|states.', { ref_id: str, property: str }),
|
|
324
456
|
tool('is', 'Check one control: visible|enabled|checked|focused|expanded|selected.', { ref_id: str, property: str }),
|
|
325
457
|
tool('click', 'Press a control via its semantic action (no pixels). Verify by re-snapshot.', { ref_id: str, timeout_ms: int }),
|
|
326
|
-
tool('
|
|
327
|
-
tool('
|
|
458
|
+
tool('focus', 'Focus a control through UIA SetFocus. On Windows SetFocus takes the desktop foreground window, so the engine refuses it headless: the adapter runs it as physical input for this one call (same rule as type) without latching the conversation. For text entry prefer set_value, which focuses the field itself.', { ref_id: str, timeout_ms: int }),
|
|
459
|
+
tool('double_click', 'Double-click a control. Windows has no headless double-click, so this drives the real pointer for this one call (same rule as type). Verify by re-snapshot.', { ref_id: str, timeout_ms: int }),
|
|
460
|
+
tool('right_click', 'Open a control\u2019s context menu. Windows has no headless right-click, so this drives the real pointer for this one call (same rule as type). The menu is a separate surface: snapshot it after.', { ref_id: str, timeout_ms: int }),
|
|
461
|
+
tool('expand', 'Expand an expander, tree item, or dropdown via ExpandCollapsePattern (no pixels).', { ref_id: str, timeout_ms: int }),
|
|
462
|
+
tool('collapse', 'Collapse an expander or tree item via ExpandCollapsePattern (no pixels).', { ref_id: str, timeout_ms: int }),
|
|
463
|
+
tool('toggle', 'Flip a checkbox or toggle button via TogglePattern. Prefer it over click: it reports the resulting state, and click on a checked control can be refused.', { ref_id: str, timeout_ms: int }),
|
|
464
|
+
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 }),
|
|
465
|
+
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
466
|
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
467
|
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
468
|
tool('list_windows', 'List top-level windows (ids, titles, pids, responsiveness).', { app: str, timeout_ms: int }),
|
|
@@ -332,7 +470,7 @@ const TOOLS = [
|
|
|
332
470
|
tool('wait', 'Pause (ms) or wait for a condition: window/app/text/element before acting. Prevents acting on half-settled windows.', { ms: int, app: str, window: str, element: str, text: str, timeout_ms: int }),
|
|
333
471
|
tool('screenshot', 'Capture a window/screen PNG to a file; returns the file path. Use for visual evidence only — trees are the primary signal.', { app: str, window_id: str, screen: int }),
|
|
334
472
|
tool('ocr', 'Read text from a screenshot via Windows OCR (for canvas/games/no-a11y-tree UIs). Returns lines with SCREEN coordinates (not refs). query filters lines.', { app: str, window_id: str, screen: int, query: str }),
|
|
335
|
-
tool('batch', `Run 1-${MAX_BATCH} planned actions on one target; stops on first error, then observes once. Each step: {action, arguments}.
|
|
473
|
+
tool('batch', `Run 1-${MAX_BATCH} planned actions on one target; stops on first error, then observes once. Each step: {action, arguments}. Native single-process actions (fast path): snapshot,find,get,is,click,set_value,press,scroll,wait,focus,double_click,right_click,expand,collapse,toggle. type,screenshot,ocr run step by step.`, { commands_json: str, stop_on_error: bool, timeout_ms: int }, ['commands_json']),
|
|
336
474
|
tool('tools',
|
|
337
475
|
'Manage the adapter: operation list|describe (tool surface + state), load {names} (reveal tools when the lazy preset is on), headed {on} (per-conversation headed toggle — use when headless is not an option: cursor-moving input, notifications, shell surfaces), overlay {on} (presentation cursor overlay), session_end {operation} (end this conversation\'s driver session).',
|
|
338
476
|
{ operation: str, name: str, names: { type: 'array', items: str }, on: bool }),
|
|
@@ -360,6 +498,10 @@ function flagArgs(pairs) {
|
|
|
360
498
|
}
|
|
361
499
|
|
|
362
500
|
async function callTool(name, args = {}) {
|
|
501
|
+
if (INPUT_TOOLS.has(name)) {
|
|
502
|
+
const stopped = stoppedSeriesMessage();
|
|
503
|
+
if (stopped) return stoppedResult(name, stopped);
|
|
504
|
+
}
|
|
363
505
|
switch (name) {
|
|
364
506
|
case 'snapshot':
|
|
365
507
|
return enqueue(async () => {
|
|
@@ -391,18 +533,38 @@ async function callTool(name, args = {}) {
|
|
|
391
533
|
await ensureSession();
|
|
392
534
|
return parseEnvelope(await runCli(['click', args.ref_id, ...flagArgs([['--timeout-ms', args.timeout_ms]])]));
|
|
393
535
|
});
|
|
536
|
+
case 'expand':
|
|
537
|
+
case 'collapse':
|
|
538
|
+
case 'toggle':
|
|
539
|
+
return enqueue(async () => {
|
|
540
|
+
await ensureSession();
|
|
541
|
+
return parseEnvelope(await runCli([NATIVE_BATCH_COMMANDS.get(name), args.ref_id, ...flagArgs([['--timeout-ms', args.timeout_ms]])]));
|
|
542
|
+
});
|
|
543
|
+
case 'focus':
|
|
544
|
+
case 'double_click':
|
|
545
|
+
case 'right_click':
|
|
546
|
+
return enqueue(async () => {
|
|
547
|
+
await ensureSession();
|
|
548
|
+
// Engine fact on Windows (platform skill): right-click, double-click and
|
|
549
|
+
// focus are physical or take the desktop foreground, so they return
|
|
550
|
+
// POLICY_DENIED headless. --headed is scoped to THIS call, exactly like
|
|
551
|
+
// `type`; the conversation is never latched into physical input.
|
|
552
|
+
return parseEnvelope(await runCli([NATIVE_BATCH_COMMANDS.get(name), args.ref_id, ...flagArgs([['--timeout-ms', args.timeout_ms]])], { headed: true }));
|
|
553
|
+
});
|
|
394
554
|
case 'set_value':
|
|
395
555
|
return enqueue(async () => parseEnvelope(await runCli(['set-value', args.ref_id, String(args.value ?? ''), ...flagArgs([['--timeout-ms', args.timeout_ms]])])));
|
|
396
556
|
case 'type':
|
|
397
557
|
return enqueue(async () => {
|
|
398
558
|
await ensureSession();
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
if (
|
|
559
|
+
// Engine fact on Windows: headless `type` is POLICY_DENIED — keystroke
|
|
560
|
+
// synthesis is headed-only. `type` is therefore physical by nature, so
|
|
561
|
+
// --headed is scoped to THIS call and the conversation is never latched
|
|
562
|
+
// into it. Latching was what made every later click/scroll move the
|
|
563
|
+
// user's real pointer.
|
|
564
|
+
const promote = await ensureForeground(args.ref_id);
|
|
565
|
+
if (promote) return envelopeResult(promote);
|
|
566
|
+
const env = parseEnvelope(await runCli(['type', args.ref_id, args.text ?? '', ...flagArgs([['--interval-ms', args.interval_ms], ['--timeout-ms', args.timeout_ms]])], { headed: true }));
|
|
567
|
+
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
568
|
return env;
|
|
407
569
|
});
|
|
408
570
|
case 'press':
|
|
@@ -437,13 +599,35 @@ async function callTool(name, args = {}) {
|
|
|
437
599
|
if (!Array.isArray(commands) || commands.length < 1 || commands.length > MAX_BATCH) {
|
|
438
600
|
return { content: [{ type: 'text', text: `batch needs 1-${MAX_BATCH} steps (got ${Array.isArray(commands) ? commands.length : 'non-array'}).` }], isError: true };
|
|
439
601
|
}
|
|
602
|
+
const actions = commands.map((step) => String(step?.action || ''));
|
|
603
|
+
const unknown = actions.filter((action) => !NATIVE_BATCH_COMMANDS.has(action) && !LOOP_BATCH_ACTIONS.has(action));
|
|
604
|
+
if (unknown.length > 0) {
|
|
605
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ok: false, unknown, supported: [...NATIVE_BATCH_COMMANDS.keys(), ...LOOP_BATCH_ACTIONS] }) }], isError: true };
|
|
606
|
+
}
|
|
607
|
+
if (!actions.some((action) => LOOP_BATCH_ACTIONS.has(action))) {
|
|
608
|
+
// One engine process for the whole batch. Every CLI call costs ~85 ms of
|
|
609
|
+
// spawn plus UIA cold start, and the engine runs the same policy checks and
|
|
610
|
+
// dispatch per entry, so this is ~1.7x faster than the loop and keeps each
|
|
611
|
+
// entry's payload — a find step yields refs instead of "ok".
|
|
612
|
+
const entries = commands.map((step) => {
|
|
613
|
+
const action = String(step?.action || '');
|
|
614
|
+
return { command: NATIVE_BATCH_COMMANDS.get(action), args: batchEntryArgs(action, step?.arguments || {}) };
|
|
615
|
+
});
|
|
616
|
+
return enqueue(async () => {
|
|
617
|
+
await ensureSession();
|
|
618
|
+
const cli = ['batch', JSON.stringify(entries)];
|
|
619
|
+
if (args.stop_on_error !== false) cli.push('--stop-on-error');
|
|
620
|
+
if (args.timeout_ms !== undefined && args.timeout_ms !== null) cli.push('--timeout-ms', String(args.timeout_ms));
|
|
621
|
+
const envelope = parseEnvelope(await runCli(cli));
|
|
622
|
+
if (envelope.ok === true) envelope.data = trimBatchData(envelope.data);
|
|
623
|
+
return envelopeResult(envelope);
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
// type promotes the window to the foreground first, and screenshot/ocr own an
|
|
627
|
+
// adapter-side file path, so a batch containing them runs step by step.
|
|
440
628
|
const results = [];
|
|
441
629
|
for (const step of commands) {
|
|
442
630
|
const action = String(step?.action || '');
|
|
443
|
-
if (!TOOLS_BY_NAME.has(action) || action === 'batch' || action === 'tools') {
|
|
444
|
-
results.push({ action, error: `unsupported step action: ${action}` });
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
631
|
const out = await callTool(action, step?.arguments || {});
|
|
448
632
|
const envelope = out.structuredContent ?? out;
|
|
449
633
|
results.push({ action, ok: envelope.ok === true, summary: summarizeEnvelope(envelope) });
|
|
@@ -460,7 +644,8 @@ async function callTool(name, args = {}) {
|
|
|
460
644
|
}
|
|
461
645
|
if (op === 'headed') {
|
|
462
646
|
state.headedOverride = args.on === true;
|
|
463
|
-
|
|
647
|
+
state.headedExpiresAt = args.on === true ? Date.now() + HEADED_LEASE_MS : 0;
|
|
648
|
+
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
649
|
}
|
|
465
650
|
if (op === 'overlay') {
|
|
466
651
|
state.overlayOverride = args.on === true;
|
|
@@ -506,6 +691,9 @@ async function callTool(name, args = {}) {
|
|
|
506
691
|
await runCli(['session', 'gc']);
|
|
507
692
|
state.sessionStarted = false;
|
|
508
693
|
state.sessionId = `arya-${randomBytes(4).toString('hex')}`;
|
|
694
|
+
// Ending the driver session also releases the physical-input latch.
|
|
695
|
+
state.headedOverride = null;
|
|
696
|
+
state.headedExpiresAt = 0;
|
|
509
697
|
return { content: [{ type: 'text', text: JSON.stringify({ ok: env.ok === true, note: 'new session starts on next action', engine: env }) }] };
|
|
510
698
|
}
|
|
511
699
|
return {
|
|
@@ -531,6 +719,13 @@ function summarizeEnvelope(envelope) {
|
|
|
531
719
|
const d = envelope.data ?? {};
|
|
532
720
|
const bits = [];
|
|
533
721
|
if (d.ref_count !== undefined) bits.push(`refs ${d.ref_count}`);
|
|
722
|
+
// A step that locates something is useless without the ref it found, and a
|
|
723
|
+
// read step without its value is the same. Report the payload, not just "ok".
|
|
724
|
+
if (d.match?.ref_id) bits.push(`ref ${d.match.ref_id}${d.match.name ? ` (${d.match.name})` : ''}`);
|
|
725
|
+
if (Array.isArray(d.matches) && d.matches.length) bits.push(`refs ${d.matches.slice(0, 8).map((m) => m.ref_id).join(',')}`);
|
|
726
|
+
if (d.snapshot_id) bits.push(`snapshot ${d.snapshot_id}`);
|
|
727
|
+
if (d.value !== undefined) bits.push(`value ${JSON.stringify(d.value)}`);
|
|
728
|
+
if (d.result !== undefined) bits.push(`result ${JSON.stringify(d.result)}`);
|
|
534
729
|
if (d.window?.id) bits.push(`window ${d.window.id}`);
|
|
535
730
|
if (d.target?.process?.pid) bits.push(`pid ${d.target.process.pid}`);
|
|
536
731
|
if (d.disposition?.delivery) bits.push(`delivery ${d.disposition.delivery}`);
|
|
@@ -583,11 +778,13 @@ const INSTRUCTIONS = [
|
|
|
583
778
|
'Arya Computer Use: drive Windows apps through semantic UIA refs, not pixels.',
|
|
584
779
|
'Loop: snapshot (skeleton/compact first) -> act on @refs -> re-snapshot to verify.',
|
|
585
780
|
'Refs go stale after window changes: on STALE_REF/AMBIGUOUS_TARGET, re-snapshot.',
|
|
586
|
-
'
|
|
781
|
+
'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.',
|
|
782
|
+
'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
783
|
'Chromium windows settle slowly: snapshot with timeout_ms 10000-25000.',
|
|
588
784
|
'Verify stateful actions by re-read; delivered_verified = confirmed, delivered_unverified retry:unsafe = do not blind-repeat.',
|
|
589
785
|
'On ACTION_FAILED post_action_verification the action landed: read data.post_state, do NOT retry.',
|
|
590
786
|
'This host must run elevated (Administrator); lease_directory_untrusted means restart AryaCode as Administrator.',
|
|
787
|
+
'Esc (or the stop shortcut) stops the series: input tools then return COMPUTER_USE_STOPPED_BY_USER for this turn. Reads still work; do not retry input until a new user turn.',
|
|
591
788
|
'Destructive actions (delete, send, pay, close unsaved) require user confirmation first.',
|
|
592
789
|
'No accessibility tree (games/canvas): use ocr for text and headed mouse coordinates for input.',
|
|
593
790
|
].join('\n');
|
package/mcp/settings.mjs
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// switches for the adapter (headedDefault, overlayDefault, driverLockEnabled).
|
|
4
4
|
|
|
5
5
|
export const DEFAULT_SETTINGS = Object.freeze({
|
|
6
|
-
|
|
6
|
+
// 'source' paints the assistant's raw text stream on the user's desktop. The
|
|
7
|
+
// default is the condensed lane; 'source' stays available for anyone who wants
|
|
8
|
+
// the verbatim panel.
|
|
9
|
+
progressOverlayEnabled: true, progressOverlayMode: 'summary',
|
|
7
10
|
progressOverlayOpacity: 60, progressOverlayWidth: 360, progressOverlayHeight: 240, progressOverlayPosition: 'top-right', progressOverlayX: 80, progressOverlayY: 80, progressOverlayLocked: true, progressLockShortcut: '', progressPreviewToken: 0,
|
|
8
11
|
indicatorEnabled: true, cursorIndicatorEnabled: true, remoteIndicatorVisible: true, indicatorText: 'Arya is controlling your computer', pointerLabel: 'Arya',
|
|
9
12
|
indicatorPosition: 'top', indicatorIdleMs: 800, stopShortcut: '',
|
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.6",
|
|
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.
|