@desklink/axi 0.3.0

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.
@@ -0,0 +1,28 @@
1
+ // Per-subcommand --help rendered from CommandSpec (AXI principle 10).
2
+ import { emitBlock, emitList } from "../output/toon.js";
3
+ export function renderHelp(tool, spec) {
4
+ const parts = [];
5
+ parts.push(`command: ${[tool, spec.name].filter(Boolean).join(" ")}`);
6
+ parts.push(`summary: ${spec.summary}`);
7
+ const args = spec.args ?? [];
8
+ if (args.length > 0) {
9
+ parts.push(emitList("args", args.map((a) => ({
10
+ name: a.name,
11
+ required: a.required ? "yes" : "no",
12
+ description: a.description,
13
+ })), ["name", "required", "description"]));
14
+ }
15
+ const flagRows = spec.flags.map((f) => ({
16
+ flag: `--${f.name}`,
17
+ default: f.default === undefined ? "" : String(f.default),
18
+ description: f.values
19
+ ? `${f.description} (${f.values.join("|")})`
20
+ : f.description,
21
+ }));
22
+ flagRows.push({ flag: "--help", default: "", description: "show this help" });
23
+ parts.push(emitList("flags", flagRows, ["flag", "default", "description"]));
24
+ if (spec.examples.length > 0) {
25
+ parts.push(emitBlock("examples", spec.examples));
26
+ }
27
+ return parts.join("\n");
28
+ }
@@ -0,0 +1,74 @@
1
+ // Noun-verb command router. Generic over a Registry so scaffolded projects
2
+ // reuse it verbatim with their own command set.
3
+ import { parseArgs } from "./args.js";
4
+ import { renderHelp } from "./help.js";
5
+ import { AxiError, renderError, UsageError } from "../output/errors.js";
6
+ import { emitList, print } from "../output/toon.js";
7
+ function topLevelCommands(reg) {
8
+ const tops = new Set();
9
+ for (const key of Object.keys(reg.commands))
10
+ tops.add(key.split(" ")[0]);
11
+ return [...tops].sort().join(", ");
12
+ }
13
+ export async function dispatch(reg, argv) {
14
+ try {
15
+ const words = [];
16
+ for (const tok of argv) {
17
+ if (tok.startsWith("-"))
18
+ break;
19
+ words.push(tok);
20
+ }
21
+ let key = "";
22
+ let consumed = 0;
23
+ for (let n = Math.min(2, words.length); n >= 1; n--) {
24
+ const candidate = words.slice(0, n).join(" ");
25
+ if (reg.commands[candidate]) {
26
+ key = candidate;
27
+ consumed = n;
28
+ break;
29
+ }
30
+ }
31
+ if (!key) {
32
+ if (words.length === 0) {
33
+ const parsed = parseArgs(argv, reg.root.spec);
34
+ if (parsed.help) {
35
+ print(reg.rootHelp());
36
+ return 0;
37
+ }
38
+ return await reg.root.run(parsed);
39
+ }
40
+ const first = words[0];
41
+ const children = Object.keys(reg.commands).filter((c) => c.startsWith(first + " "));
42
+ if (children.length > 0) {
43
+ if (argv.includes("--help")) {
44
+ print(emitList("commands", children.map((c) => ({
45
+ command: `${reg.tool} ${c}`,
46
+ summary: reg.commands[c].spec.summary,
47
+ })), ["command", "summary"]));
48
+ return 0;
49
+ }
50
+ throw new UsageError(`'${first}' requires a subcommand`, `valid: ${children.map((c) => `${reg.tool} ${c}`).join(", ")}`);
51
+ }
52
+ throw new UsageError(`unknown command '${words.join(" ")}'`, `valid commands: ${topLevelCommands(reg)}`);
53
+ }
54
+ const mod = reg.commands[key];
55
+ const parsed = parseArgs(argv.slice(consumed), mod.spec);
56
+ if (parsed.help) {
57
+ print(renderHelp(reg.tool, mod.spec));
58
+ return 0;
59
+ }
60
+ return await mod.run(parsed);
61
+ }
62
+ catch (err) {
63
+ if (err instanceof AxiError) {
64
+ print(renderError(err));
65
+ return err.exitCode;
66
+ }
67
+ const message = err instanceof Error ? err.message : String(err);
68
+ print(renderError(new AxiError(`unexpected failure: ${message}`, "re-run with the same arguments; report if it persists")));
69
+ if (err instanceof Error && err.stack) {
70
+ process.stderr.write(err.stack + "\n");
71
+ }
72
+ return 1;
73
+ }
74
+ }
@@ -0,0 +1,3 @@
1
+ // CommandSpec is the single source of truth for a command: the parser
2
+ // validates against it, --help renders from it, and SKILL.md docs cite it.
3
+ export {};
@@ -0,0 +1,31 @@
1
+ import { print } from "../output/toon.js";
2
+ import { renderHome, rootHelpText } from "../skill/content.js";
3
+ import { call } from '../bridge.js';
4
+ import { readFileSync } from 'node:fs';
5
+ const { version } = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
6
+ export const homeCommand = {
7
+ spec: {
8
+ name: "",
9
+ summary: "Home view: live content first (AXI principle 8)",
10
+ flags: [
11
+ { name: "version", type: "boolean", description: "print the tool version" },
12
+ ],
13
+ examples: ["desklink-axi", "desklink-axi --version"],
14
+ },
15
+ async run(parsed) {
16
+ if (parsed.flags["version"]) {
17
+ print(`desklink-axi: ${version}`);
18
+ return 0;
19
+ }
20
+ try {
21
+ print(`${renderHome(process.argv[1] ?? 'desklink-axi')}\n${await call('home')}`);
22
+ }
23
+ catch {
24
+ print(`${renderHome(process.argv[1] ?? 'desklink-axi')}\nsession: closed — run desklink-axi start`);
25
+ }
26
+ return 0;
27
+ },
28
+ };
29
+ export function rootHelp() {
30
+ return rootHelpText();
31
+ }
package/dist/hooks.js ADDED
@@ -0,0 +1,18 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export function installHooks() {
4
+ const directory = join(process.cwd(), '.claude');
5
+ mkdirSync(directory, { recursive: true });
6
+ const path = join(directory, 'settings.local.json');
7
+ const settings = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : {};
8
+ const hooks = settings.hooks ?? {};
9
+ const session = hooks.SessionStart ?? [];
10
+ const command = 'desklink-axi --session-hook';
11
+ if (!session.some((entry) => entry.hooks?.some(h => h.command === command))) {
12
+ session.push({ hooks: [{ type: 'command', command }] });
13
+ hooks.SessionStart = session;
14
+ settings.hooks = hooks;
15
+ writeFileSync(path, JSON.stringify(settings, null, 2) + '\n');
16
+ }
17
+ return `hooks: installed ${path}\nhelp[1]:\n desklink-axi start --help`;
18
+ }
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ import { dispatch } from './cli/router.js';
2
+ import { homeCommand, rootHelp } from './commands/home.js';
3
+ import { call, serve, socketPath } from './bridge.js';
4
+ import { browserCommands } from './browser-lane.js';
5
+ import { writeFileSync } from 'node:fs';
6
+ import { print } from './output/toon.js';
7
+ import { installHooks } from './hooks.js';
8
+ import { UsageError } from './output/errors.js';
9
+ if (process.argv[2] === '--bridge') {
10
+ try {
11
+ await serve(process.argv.slice(3));
12
+ }
13
+ catch (error) {
14
+ writeFileSync(`${socketPath}.error`, error.message);
15
+ process.exitCode = 1;
16
+ }
17
+ }
18
+ else if (process.argv[2] === '--session-hook') {
19
+ print('desklink-axi: desktop session available on request; run desklink-axi to check status');
20
+ }
21
+ else {
22
+ const definitions = [
23
+ ['start', [], ['control', 'source', 'display', 'timeout']], ['stop', [], []], ['health', [], []],
24
+ ['screen', [], ['query', 'region', 'full', 'fields']], ['diff', [], ['include-animating', 'fields']],
25
+ ['tree', [], ['query', 'full', 'fields']], ['marks', [], ['region', 'out']],
26
+ ['look', [], ['region', 'out']], ['click', ['target'], ['button', 'double', 'wait']],
27
+ ['type', ['text'], ['into', 'submit', 'wait']], ['press', ['key'], ['wait']],
28
+ ['scroll', ['direction'], ['at', 'amount', 'wait']], ['drag', ['from', 'to'], ['wait']],
29
+ ['batch', ['steps'], []],
30
+ ['wait', [], ['timeout']], ['clipboard read', [], ['full']],
31
+ ['clipboard write', ['text'], []], ['setup hooks', [], []],
32
+ ];
33
+ const commands = {};
34
+ Object.assign(commands, browserCommands());
35
+ for (const [name, required, flags] of definitions)
36
+ commands[name] = {
37
+ spec: { name, summary: `${name} on the live desktop`, args: required.map(arg => ({ name: arg, required: true, description: arg })), flags: flags.map(flag => ({ name: flag, type: ['control', 'full', 'double', 'submit', 'include-animating'].includes(flag) ? 'boolean' : 'string', values: flag === 'source' ? ['auto', 'portal', 'x11', 'display'] : flag === 'button' ? ['left', 'right'] : undefined, description: flag })), examples: [`desklink-axi ${name} ${required.map(arg => `<${arg}>`).join(' ')}`] },
38
+ async run(parsed) {
39
+ if (name === 'scroll' && !['up', 'down'].includes(parsed.positionals[0]))
40
+ throw new UsageError('scroll direction must be up or down');
41
+ if (name === 'marks' && parsed.flags.region && !/^-?\d+,-?\d+,-?\d+,-?\d+$/.test(String(parsed.flags.region)))
42
+ throw new UsageError('--region must be x,y,w,h integers');
43
+ if (typeof parsed.flags.wait === 'string' && !['none', 'change', 'settle'].includes(parsed.flags.wait) && !/^\d+$/.test(parsed.flags.wait))
44
+ throw new UsageError('--wait must be none, change, settle, or milliseconds');
45
+ for (const key of ['timeout', 'amount'])
46
+ if (typeof parsed.flags[key] === 'string' && (!/^\d+$/.test(parsed.flags[key]) || Number(parsed.flags[key]) <= 0))
47
+ throw new UsageError(`--${key} must be a positive integer`);
48
+ const args = [...parsed.positionals];
49
+ for (const [key, value] of Object.entries(parsed.flags))
50
+ if (value === true)
51
+ args.push(`--${key}`);
52
+ else if (typeof value === 'string')
53
+ args.push(`--${key}`, value);
54
+ if (name === 'setup hooks') {
55
+ print(installHooks());
56
+ return 0;
57
+ }
58
+ try {
59
+ const result = await call(name.startsWith('clipboard') ? 'clipboard' : name, name.startsWith('clipboard') ? [name.split(' ')[1], ...args] : args);
60
+ print(result);
61
+ return result.startsWith('error:') ? 1 : 0;
62
+ }
63
+ catch (error) {
64
+ print(`error: ${error.message}\nsuggestion: desklink-axi start --help`);
65
+ return 1;
66
+ }
67
+ },
68
+ };
69
+ const registry = { tool: 'desklink-axi', root: homeCommand, rootHelp, commands };
70
+ process.exit(await dispatch(registry, process.argv.slice(2)));
71
+ }
package/dist/marks.js ADDED
@@ -0,0 +1,75 @@
1
+ // Set-of-marks overlay: numbered boxes drawn onto an RGBA crop buffer.
2
+ // Pure buffer math so it is unit-testable without a display; the caller
3
+ // encodes the PNG (pngjs is already a dependency).
4
+ // 3x5 pixel digits, rows packed as 3-bit lines, top row first.
5
+ const DIGITS = {
6
+ '0': [7, 5, 5, 5, 7],
7
+ '1': [2, 6, 2, 2, 7],
8
+ '2': [7, 1, 7, 4, 7],
9
+ '3': [7, 1, 7, 1, 7],
10
+ '4': [5, 5, 7, 1, 1],
11
+ '5': [7, 4, 7, 1, 7],
12
+ '6': [7, 4, 7, 5, 7],
13
+ '7': [7, 1, 1, 1, 1],
14
+ '8': [7, 5, 7, 5, 7],
15
+ '9': [7, 5, 7, 1, 7],
16
+ };
17
+ export const MARK_BORDER = [255, 59, 48]; // red
18
+ export const MARK_CHIP = [255, 214, 10]; // yellow
19
+ export function estimateMarksCost(width, height) {
20
+ return Math.round(width * height / 750);
21
+ }
22
+ function putPixel(data, width, height, x, y, color) {
23
+ if (x < 0 || y < 0 || x >= width || y >= height)
24
+ return;
25
+ const at = (y * width + x) * 4;
26
+ data[at] = color[2];
27
+ data[at + 1] = color[1];
28
+ data[at + 2] = color[0];
29
+ data[at + 3] = 255;
30
+ }
31
+ function fillRect(data, width, height, x, y, w, h, color) {
32
+ for (let row = 0; row < h; row++)
33
+ for (let col = 0; col < w; col++)
34
+ putPixel(data, width, height, x + col, y + row, color);
35
+ }
36
+ function drawDigit(data, width, height, x, y, glyph, scale) {
37
+ const rows = DIGITS[glyph];
38
+ if (!rows)
39
+ return 0;
40
+ for (let r = 0; r < 5; r++)
41
+ for (let c = 0; c < 3; c++) {
42
+ if (rows[r] & (1 << (2 - c)))
43
+ fillRect(data, width, height, x + c * scale, y + r * scale, scale, scale, [0, 0, 0]);
44
+ }
45
+ return 3 * scale;
46
+ }
47
+ /** Draw numbered boxes (2px border + numbered chip above each box). Mutates data. */
48
+ export function drawMarks(data, width, height, boxes, scale = 2) {
49
+ for (const box of boxes) {
50
+ const { x, y, w, h } = box;
51
+ if (w <= 0 || h <= 0)
52
+ continue;
53
+ for (let t = 0; t < 2; t++) {
54
+ fillRect(data, width, height, x + t, y + t, w - 2 * t, 1, MARK_BORDER);
55
+ fillRect(data, width, height, x + t, y + h - 1 - t, w - 2 * t, 1, MARK_BORDER);
56
+ fillRect(data, width, height, x + t, y + t, 1, h - 2 * t, MARK_BORDER);
57
+ fillRect(data, width, height, x + w - 1 - t, y + t, 1, h - 2 * t, MARK_BORDER);
58
+ }
59
+ const label = String(box.mark);
60
+ const textW = label.length * 3 * scale + 2 * scale;
61
+ const chipX = Math.min(Math.max(0, x), Math.max(0, width - textW));
62
+ const chipY = y >= 9 * scale + 2 ? y - 9 * scale - 2 : y;
63
+ fillRect(data, width, height, chipX, chipY, textW, 7 * scale + 2 * scale, MARK_CHIP);
64
+ let cursor = chipX + scale;
65
+ for (const glyph of label)
66
+ cursor += drawDigit(data, width, height, cursor, chipY + scale, glyph, scale) + scale;
67
+ }
68
+ }
69
+ /** Drop OCR candidates whose center falls inside an AX box (the AX ref is richer). */
70
+ export function dedupeCandidates(axBoxes, ocr) {
71
+ return ocr.filter(item => {
72
+ const cx = item.x + item.w / 2, cy = item.y + item.h / 2;
73
+ return !axBoxes.some(b => cx >= b.x && cx <= b.x + b.w && cy >= b.y && cy <= b.y + b.h);
74
+ });
75
+ }
@@ -0,0 +1,22 @@
1
+ // Structured errors rendered as TOON on stdout (AXI principle 6).
2
+ // Exit codes: 0 success/no-op, 1 error, 2 usage error.
3
+ export class AxiError extends Error {
4
+ exitCode = 1;
5
+ suggestion;
6
+ constructor(message, suggestion) {
7
+ super(message);
8
+ this.suggestion = suggestion;
9
+ }
10
+ }
11
+ export class UsageError extends AxiError {
12
+ constructor(message, suggestion) {
13
+ super(message, suggestion);
14
+ this.exitCode = 2;
15
+ }
16
+ }
17
+ export function renderError(err) {
18
+ const lines = [`error: ${err.message}`];
19
+ if (err.suggestion)
20
+ lines.push(`suggestion: ${err.suggestion}`);
21
+ return lines.join("\n");
22
+ }
@@ -0,0 +1,28 @@
1
+ // TOON (Token-Oriented Object Notation) emitter. All stdout flows through
2
+ // this module so every command stays consistent (AXI principle 1).
3
+ export function print(text) {
4
+ process.stdout.write(text + "\n");
5
+ }
6
+ export function toonValue(value) {
7
+ if (value === null || value === undefined)
8
+ return "";
9
+ const s = String(value);
10
+ if (/[,"\n]/.test(s))
11
+ return '"' + s.replace(/"/g, '""') + '"';
12
+ return s;
13
+ }
14
+ export function emitList(name, rows, fields, opts = {}) {
15
+ const count = opts.total !== undefined && opts.total !== rows.length
16
+ ? `${rows.length} of ${opts.total} total`
17
+ : String(rows.length);
18
+ const header = `${name}[${count}]{${fields.join(",")}}:`;
19
+ const lines = rows.map((row) => " " + fields.map((f) => toonValue(row[f])).join(","));
20
+ return [header, ...lines].join("\n");
21
+ }
22
+ /** A named block of pre-formatted lines, e.g. help[2]: or next[3]:. */
23
+ export function emitBlock(name, lines) {
24
+ return [`${name}[${lines.length}]:`, ...lines.map((l) => " " + l)].join("\n");
25
+ }
26
+ export function emitKV(pairs) {
27
+ return pairs.map(([k, v]) => `${k}: ${toonValue(v) === "" ? "" : String(v)}`.trimEnd()).join("\n");
28
+ }
@@ -0,0 +1,8 @@
1
+ import { homedir } from 'node:os';
2
+ import { emitList } from '../output/toon.js';
3
+ export const DESCRIPTION = "See and drive a live Linux or macOS desktop and an opted-in browser through desklink; text and changes by default, pixels on request";
4
+ export const SPEC_VERSION = 'axi/1.0-2026-07';
5
+ const commands = ['start', 'stop', 'health', 'screen', 'tree', 'marks', 'diff', 'look', 'click', 'type', 'press', 'scroll', 'drag', 'batch', 'wait', 'clipboard read', 'clipboard write', 'setup hooks', 'browser attach', 'browser launch', 'browser tabs', 'browser select', 'browser open', 'browser navigate', 'browser close', 'browser snapshot', 'browser click', 'browser fill', 'browser press', 'browser upload', 'browser detach'];
6
+ export function renderHome(binPath) { return `desklink-axi: ${binPath.replace(homedir(), '~')} — ${DESCRIPTION}\nhelp[5]:\n desklink-axi start --control --source x11 --display :97\n desklink-axi screen --query "<words>"\n desklink-axi tree --query "<words>"\n desklink-axi look @r1\n desklink-axi browser snapshot`; }
7
+ export function rootHelpText() { return [`desklink-axi: ${DESCRIPTION}`, emitList('commands', commands.map(command => ({ command, summary: command.startsWith('browser') ? `${command} on the attached browser` : `${command} on the live desktop` })), ['command', 'summary']), 'flags[1]{flag,description}:\n --help,show help for any command', 'examples[2]:\n desklink-axi start --control --source x11 --display :97\n desklink-axi screen --query "<words>"'].join('\n'); }
8
+ export function renderSkill() { return `---\nname: desklink-axi\ndescription: "${DESCRIPTION}"\n---\n\n# desklink-axi\n\n${DESCRIPTION} (${SPEC_VERSION}).\n\n\`\`\`\n${renderHome('desklink-axi')}\n\`\`\`\n\nTwo lanes: the desktop lane (capture, OCR refs, AT-SPI tree, input) and the browser lane (semantic web control through an opted-in Chromium). On the desktop, \`tree --query "<words>"\` reads the AT-SPI accessibility tree into generation-tagged \`@a<gen>.<n>\` refs (values included), and \`marks\` overlays numbered candidates for icon-only controls. For web pages use the browser lane: \`desklink-axi browser attach --cdp 127.0.0.1:<port>\` for a browser you started with \`--remote-debugging-port\` (loopback only), or \`desklink-axi browser launch\` for an isolated task-owned profile. Then \`browser tabs\`, \`browser snapshot\` (compact accessibility refs, no screenshots), \`browser click/fill/upload @e<N>\`, \`browser select <tab>\`. Page selection (selected) and the browser's actually activated tab (front) are reported separately. OS-level dialogs are not-supported in the browser lane: fall back to the desktop lane (click/type on the dialog).\n\nRead with screen --query or --region before screen --full. Actions acknowledge input immediately; add --wait change|settle|<milliseconds> when needed. Batch sequential steps in one socket round trip: desklink-axi batch '[["press","Meta+l"],["type","https://example.com"],["press","Return"],["wait","change"]]'. Steps may be {verb,args?,name?,wait?} objects; a named snapshot binds OCR refs for later @name.n steps, and assert steps (\`["assert","text"]\`, \`["assert","--absent","text"]\`) make the batch outcome depend on observed screen text. The footer reports effects: asserted[n] or effects: unverified (input acknowledgements only) — an input ack is never a verified outcome.\n\nEvery command supports --help. Exit 0 success, 1 operational error, 2 usage error. No screen text at startup unless requested.\n`; }
package/dist/token.js ADDED
@@ -0,0 +1,44 @@
1
+ import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ export function tokenPath() {
6
+ return process.env.DESKLINK_AXI_RESTORE_TOKEN_FILE ?? join(process.env.XDG_STATE_HOME ?? join(homedir(), '.local', 'state'), 'desklink-axi', 'portal-token');
7
+ }
8
+ // A portal token is single-use. Remove it from the reusable name before open;
9
+ // a failed open must prompt again rather than replay a possibly consumed grant.
10
+ export function takeToken(path) {
11
+ const claimed = `${path}.${randomUUID()}.used`;
12
+ try {
13
+ renameSync(path, claimed);
14
+ }
15
+ catch (error) {
16
+ if (error.code === 'ENOENT')
17
+ return undefined;
18
+ throw error;
19
+ }
20
+ try {
21
+ return readFileSync(claimed, 'utf8').trim() || undefined;
22
+ }
23
+ finally {
24
+ rmSync(claimed);
25
+ }
26
+ }
27
+ export function saveToken(path, token) {
28
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
29
+ const temporary = `${path}.${randomUUID()}.tmp`;
30
+ const fd = openSync(temporary, 'wx', 0o600);
31
+ try {
32
+ writeFileSync(fd, token);
33
+ closeSync(fd);
34
+ renameSync(temporary, path);
35
+ }
36
+ catch (error) {
37
+ try {
38
+ closeSync(fd);
39
+ }
40
+ catch { /* already closed */ }
41
+ rmSync(temporary, { force: true });
42
+ throw error;
43
+ }
44
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@desklink/axi",
3
+ "version": "0.3.0",
4
+ "description": "Agent-facing desktop control CLI",
5
+ "type": "module",
6
+ "bin": {
7
+ "desklink-axi": "bin/desklink-axi.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=20"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "dist",
15
+ "skills",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "npm run build --workspace=@desklink/host && tsc",
20
+ "pretest": "npm run build",
21
+ "test": "vitest run",
22
+ "smoke": "node test/smoke.mjs",
23
+ "skill:gen": "node scripts/gen-skill.mjs",
24
+ "skill:check": "node scripts/gen-skill.mjs --check"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22.10.0",
28
+ "@types/pngjs": "^6.0.5",
29
+ "typescript": "^5.6.0"
30
+ },
31
+ "dependencies": {
32
+ "@desklink/host": "0.3.0",
33
+ "node-datachannel": "^0.33.4",
34
+ "playwright-core": "1.62.1",
35
+ "pngjs": "^7.0.0"
36
+ },
37
+ "license": "Apache-2.0"
38
+ }
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: desklink-axi
3
+ description: "See and drive a live Linux or macOS desktop and an opted-in browser through desklink; text and changes by default, pixels on request"
4
+ ---
5
+
6
+ # desklink-axi
7
+
8
+ See and drive a live Linux or macOS desktop and an opted-in browser through desklink; text and changes by default, pixels on request (axi/1.0-2026-07).
9
+
10
+ ```
11
+ desklink-axi: desklink-axi — See and drive a live Linux or macOS desktop and an opted-in browser through desklink; text and changes by default, pixels on request
12
+ help[5]:
13
+ desklink-axi start --control --source x11 --display :97
14
+ desklink-axi screen --query "<words>"
15
+ desklink-axi tree --query "<words>"
16
+ desklink-axi look @r1
17
+ desklink-axi browser snapshot
18
+ ```
19
+
20
+ Two lanes: the desktop lane (capture, OCR refs, AT-SPI tree, input) and the browser lane (semantic web control through an opted-in Chromium). On the desktop, `tree --query "<words>"` reads the AT-SPI accessibility tree into generation-tagged `@a<gen>.<n>` refs (values included), and `marks` overlays numbered candidates for icon-only controls. For web pages use the browser lane: `desklink-axi browser attach --cdp 127.0.0.1:<port>` for a browser you started with `--remote-debugging-port` (loopback only), or `desklink-axi browser launch` for an isolated task-owned profile. Then `browser tabs`, `browser snapshot` (compact accessibility refs, no screenshots), `browser click/fill/upload @e<N>`, `browser select <tab>`. Page selection (selected) and the browser's actually activated tab (front) are reported separately. OS-level dialogs are not-supported in the browser lane: fall back to the desktop lane (click/type on the dialog).
21
+
22
+ Read with screen --query or --region before screen --full. Actions acknowledge input immediately; add --wait change|settle|<milliseconds> when needed. Batch sequential steps in one socket round trip: desklink-axi batch '[["press","Meta+l"],["type","https://example.com"],["press","Return"],["wait","change"]]'. Steps may be {verb,args?,name?,wait?} objects; a named snapshot binds OCR refs for later @name.n steps, and assert steps (`["assert","text"]`, `["assert","--absent","text"]`) make the batch outcome depend on observed screen text. The footer reports effects: asserted[n] or effects: unverified (input acknowledgements only) — an input ack is never a verified outcome.
23
+
24
+ Every command supports --help. Exit 0 success, 1 operational error, 2 usage error. No screen text at startup unless requested.