@skillful-agents/agent-computer 0.0.4 → 0.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.
Files changed (50) hide show
  1. package/bin/ac-core-darwin-arm64 +0 -0
  2. package/bin/ac-core-darwin-x64 +0 -0
  3. package/bin/ac-core-win32-arm64.exe +0 -0
  4. package/bin/ac-core-win32-x64.exe +0 -0
  5. package/dist/src/platform/resolve.d.ts.map +1 -1
  6. package/dist/src/platform/resolve.js +5 -3
  7. package/dist/src/platform/resolve.js.map +1 -1
  8. package/dist-cjs/bin/ac.js +127 -0
  9. package/dist-cjs/package.json +1 -0
  10. package/dist-cjs/src/bridge.js +693 -0
  11. package/dist-cjs/src/cdp/ax-tree.js +162 -0
  12. package/dist-cjs/src/cdp/bounds.js +66 -0
  13. package/dist-cjs/src/cdp/client.js +272 -0
  14. package/dist-cjs/src/cdp/connection.js +285 -0
  15. package/dist-cjs/src/cdp/diff.js +55 -0
  16. package/dist-cjs/src/cdp/discovery.js +91 -0
  17. package/dist-cjs/src/cdp/index.js +27 -0
  18. package/dist-cjs/src/cdp/interactions.js +301 -0
  19. package/dist-cjs/src/cdp/port-manager.js +68 -0
  20. package/dist-cjs/src/cdp/role-map.js +102 -0
  21. package/dist-cjs/src/cdp/types.js +2 -0
  22. package/dist-cjs/src/cli/commands/apps.js +63 -0
  23. package/dist-cjs/src/cli/commands/batch.js +37 -0
  24. package/dist-cjs/src/cli/commands/click.js +61 -0
  25. package/dist-cjs/src/cli/commands/clipboard.js +31 -0
  26. package/dist-cjs/src/cli/commands/dialog.js +45 -0
  27. package/dist-cjs/src/cli/commands/drag.js +26 -0
  28. package/dist-cjs/src/cli/commands/find.js +99 -0
  29. package/dist-cjs/src/cli/commands/menu.js +36 -0
  30. package/dist-cjs/src/cli/commands/screenshot.js +27 -0
  31. package/dist-cjs/src/cli/commands/scroll.js +77 -0
  32. package/dist-cjs/src/cli/commands/session.js +27 -0
  33. package/dist-cjs/src/cli/commands/snapshot.js +24 -0
  34. package/dist-cjs/src/cli/commands/type.js +69 -0
  35. package/dist-cjs/src/cli/commands/windowmgmt.js +62 -0
  36. package/dist-cjs/src/cli/commands/windows.js +10 -0
  37. package/dist-cjs/src/cli/commands.js +215 -0
  38. package/dist-cjs/src/cli/output.js +253 -0
  39. package/dist-cjs/src/cli/parser.js +128 -0
  40. package/dist-cjs/src/config.js +79 -0
  41. package/dist-cjs/src/daemon.js +183 -0
  42. package/dist-cjs/src/errors.js +118 -0
  43. package/dist-cjs/src/index.js +24 -0
  44. package/dist-cjs/src/platform/index.js +16 -0
  45. package/dist-cjs/src/platform/resolve.js +71 -0
  46. package/dist-cjs/src/refs.js +91 -0
  47. package/dist-cjs/src/sdk.js +288 -0
  48. package/dist-cjs/src/types.js +11 -0
  49. package/package.json +4 -2
  50. package/scripts/fix-cjs-resolve.js +27 -0
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DaemonManager = void 0;
4
+ const child_process_1 = require("child_process");
5
+ const net_1 = require("net");
6
+ const fs_1 = require("fs");
7
+ const index_js_1 = require("./platform/index.js");
8
+ const resolve_js_1 = require("./platform/resolve.js");
9
+ class DaemonManager {
10
+ binaryPath;
11
+ constructor(binaryPath) {
12
+ this.binaryPath = binaryPath ?? (0, resolve_js_1.resolveBinary)();
13
+ }
14
+ async start() {
15
+ // Check if already running
16
+ const current = this.readDaemonInfo();
17
+ if (current && this.isProcessAlive(current.pid)) {
18
+ return this.buildStatus(current, true);
19
+ }
20
+ // Clean up stale files
21
+ this.cleanupStale();
22
+ // Spawn daemon
23
+ (0, fs_1.mkdirSync)(index_js_1.AC_DIR, { recursive: true });
24
+ const proc = (0, child_process_1.spawn)(this.binaryPath, ['--daemon'], {
25
+ detached: true,
26
+ stdio: ['ignore', 'ignore', 'pipe'],
27
+ });
28
+ proc.unref();
29
+ // Wait for daemon to be ready
30
+ const start = Date.now();
31
+ if (index_js_1.IS_NAMED_PIPE) {
32
+ // Named pipes: wait for daemon.json to appear (pipe has no file)
33
+ while (Date.now() - start < 5000) {
34
+ if ((0, fs_1.existsSync)(index_js_1.DAEMON_JSON_PATH)) {
35
+ await sleep(50);
36
+ break;
37
+ }
38
+ await sleep(50);
39
+ }
40
+ if (!(0, fs_1.existsSync)(index_js_1.DAEMON_JSON_PATH)) {
41
+ throw new Error('Daemon failed to start: daemon.json not created within 5s');
42
+ }
43
+ }
44
+ else {
45
+ // Unix socket: wait for socket file to appear
46
+ while (Date.now() - start < 5000) {
47
+ if ((0, fs_1.existsSync)(index_js_1.SOCKET_PATH)) {
48
+ await sleep(50);
49
+ break;
50
+ }
51
+ await sleep(50);
52
+ }
53
+ if (!(0, fs_1.existsSync)(index_js_1.SOCKET_PATH)) {
54
+ throw new Error('Daemon failed to start: socket not created within 5s');
55
+ }
56
+ }
57
+ const info = this.readDaemonInfo();
58
+ if (!info) {
59
+ throw new Error('Daemon started but daemon.json not found');
60
+ }
61
+ return this.buildStatus(info, true);
62
+ }
63
+ async stop() {
64
+ const info = this.readDaemonInfo();
65
+ if (!info)
66
+ return;
67
+ if (this.isProcessAlive(info.pid)) {
68
+ // On Windows, SIGTERM is a hard kill — try graceful RPC shutdown first
69
+ if (index_js_1.IS_NAMED_PIPE) {
70
+ try {
71
+ await this.sendShutdownRPC(info.socket);
72
+ }
73
+ catch { /* fall through to SIGTERM */ }
74
+ }
75
+ // Wait for process to exit (may already be gone from RPC shutdown)
76
+ let start = Date.now();
77
+ while (Date.now() - start < 3000) {
78
+ if (!this.isProcessAlive(info.pid))
79
+ break;
80
+ await sleep(50);
81
+ }
82
+ // Send SIGTERM if still alive
83
+ if (this.isProcessAlive(info.pid)) {
84
+ try {
85
+ process.kill(info.pid, 'SIGTERM');
86
+ }
87
+ catch { /* ok */ }
88
+ start = Date.now();
89
+ while (Date.now() - start < 3000) {
90
+ if (!this.isProcessAlive(info.pid))
91
+ break;
92
+ await sleep(50);
93
+ }
94
+ }
95
+ // Force kill if still alive
96
+ if (this.isProcessAlive(info.pid)) {
97
+ try {
98
+ process.kill(info.pid, 'SIGKILL');
99
+ }
100
+ catch { /* ok */ }
101
+ }
102
+ }
103
+ this.cleanupStale();
104
+ }
105
+ sendShutdownRPC(socketPath) {
106
+ return new Promise((resolve, reject) => {
107
+ const sock = (0, net_1.connect)({ path: socketPath });
108
+ const timeout = setTimeout(() => { sock.destroy(); reject(new Error('shutdown timeout')); }, 2000);
109
+ sock.on('connect', () => {
110
+ const req = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'shutdown', params: {} }) + '\n';
111
+ sock.write(req, () => {
112
+ clearTimeout(timeout);
113
+ // Give daemon a moment to process before closing
114
+ setTimeout(() => { sock.destroy(); resolve(); }, 100);
115
+ });
116
+ });
117
+ sock.on('error', (err) => { clearTimeout(timeout); reject(err); });
118
+ });
119
+ }
120
+ async status() {
121
+ const info = this.readDaemonInfo();
122
+ if (!info) {
123
+ return { running: false };
124
+ }
125
+ const alive = this.isProcessAlive(info.pid);
126
+ if (!alive) {
127
+ this.cleanupStale();
128
+ return { running: false };
129
+ }
130
+ return this.buildStatus(info, true);
131
+ }
132
+ async restart() {
133
+ await this.stop();
134
+ return this.start();
135
+ }
136
+ readDaemonInfo() {
137
+ try {
138
+ if (!(0, fs_1.existsSync)(index_js_1.DAEMON_JSON_PATH))
139
+ return null;
140
+ const raw = (0, fs_1.readFileSync)(index_js_1.DAEMON_JSON_PATH, 'utf-8');
141
+ return JSON.parse(raw);
142
+ }
143
+ catch {
144
+ return null;
145
+ }
146
+ }
147
+ isProcessAlive(pid) {
148
+ try {
149
+ process.kill(pid, 0);
150
+ return true;
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ }
156
+ cleanupStale() {
157
+ if (!index_js_1.IS_NAMED_PIPE) {
158
+ try {
159
+ (0, fs_1.unlinkSync)(index_js_1.SOCKET_PATH);
160
+ }
161
+ catch { /* ok */ }
162
+ }
163
+ try {
164
+ (0, fs_1.unlinkSync)(index_js_1.DAEMON_JSON_PATH);
165
+ }
166
+ catch { /* ok */ }
167
+ }
168
+ buildStatus(info, running) {
169
+ const startedAt = new Date(info.started_at);
170
+ const uptime = Date.now() - startedAt.getTime();
171
+ return {
172
+ running,
173
+ pid: info.pid,
174
+ socket: info.socket,
175
+ started_at: info.started_at,
176
+ uptime_ms: Math.max(0, uptime),
177
+ };
178
+ }
179
+ }
180
+ exports.DaemonManager = DaemonManager;
181
+ function sleep(ms) {
182
+ return new Promise((resolve) => setTimeout(resolve, ms));
183
+ }
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InvalidParamsError = exports.MethodNotFoundError = exports.InvalidRequestError = exports.OCRFallbackFailedError = exports.InvalidRefError = exports.WindowNotFoundError = exports.AppNotFoundError = exports.TimeoutError = exports.PermissionDeniedError = exports.ElementNotFoundError = exports.ACError = void 0;
4
+ exports.errorFromCode = errorFromCode;
5
+ exports.exitCodeFromErrorCode = exitCodeFromErrorCode;
6
+ class ACError extends Error {
7
+ code;
8
+ exitCode;
9
+ data;
10
+ constructor(message, code, exitCode, data) {
11
+ super(message);
12
+ this.code = code;
13
+ this.exitCode = exitCode;
14
+ this.data = data;
15
+ this.name = 'ACError';
16
+ }
17
+ }
18
+ exports.ACError = ACError;
19
+ class ElementNotFoundError extends ACError {
20
+ constructor(message, data) {
21
+ super(message, -32001, 1, data);
22
+ this.name = 'ELEMENT_NOT_FOUND';
23
+ }
24
+ }
25
+ exports.ElementNotFoundError = ElementNotFoundError;
26
+ class PermissionDeniedError extends ACError {
27
+ constructor(message, data) {
28
+ super(message, -32002, 2, data);
29
+ this.name = 'PERMISSION_DENIED';
30
+ }
31
+ }
32
+ exports.PermissionDeniedError = PermissionDeniedError;
33
+ class TimeoutError extends ACError {
34
+ constructor(message, data) {
35
+ super(message, -32003, 3, data);
36
+ this.name = 'TIMEOUT';
37
+ }
38
+ }
39
+ exports.TimeoutError = TimeoutError;
40
+ class AppNotFoundError extends ACError {
41
+ constructor(message, data) {
42
+ super(message, -32004, 4, data);
43
+ this.name = 'APP_NOT_FOUND';
44
+ }
45
+ }
46
+ exports.AppNotFoundError = AppNotFoundError;
47
+ class WindowNotFoundError extends ACError {
48
+ constructor(message, data) {
49
+ super(message, -32005, 5, data);
50
+ this.name = 'WINDOW_NOT_FOUND';
51
+ }
52
+ }
53
+ exports.WindowNotFoundError = WindowNotFoundError;
54
+ class InvalidRefError extends ACError {
55
+ constructor(message, data) {
56
+ super(message, -32006, 6, data);
57
+ this.name = 'INVALID_REF';
58
+ }
59
+ }
60
+ exports.InvalidRefError = InvalidRefError;
61
+ class OCRFallbackFailedError extends ACError {
62
+ constructor(message, data) {
63
+ super(message, -32007, 7, data);
64
+ this.name = 'OCR_FALLBACK_FAILED';
65
+ }
66
+ }
67
+ exports.OCRFallbackFailedError = OCRFallbackFailedError;
68
+ // JSON-RPC standard errors
69
+ class InvalidRequestError extends ACError {
70
+ constructor(message, data) {
71
+ super(message, -32600, 126, data);
72
+ this.name = 'INVALID_REQUEST';
73
+ }
74
+ }
75
+ exports.InvalidRequestError = InvalidRequestError;
76
+ class MethodNotFoundError extends ACError {
77
+ constructor(message, data) {
78
+ super(message, -32601, 126, data);
79
+ this.name = 'METHOD_NOT_FOUND';
80
+ }
81
+ }
82
+ exports.MethodNotFoundError = MethodNotFoundError;
83
+ class InvalidParamsError extends ACError {
84
+ constructor(message, data) {
85
+ super(message, -32602, 126, data);
86
+ this.name = 'INVALID_PARAMS';
87
+ }
88
+ }
89
+ exports.InvalidParamsError = InvalidParamsError;
90
+ // Error code → error class mapping
91
+ const ERROR_MAP = {
92
+ [-32001]: ElementNotFoundError,
93
+ [-32002]: PermissionDeniedError,
94
+ [-32003]: TimeoutError,
95
+ [-32004]: AppNotFoundError,
96
+ [-32005]: WindowNotFoundError,
97
+ [-32006]: InvalidRefError,
98
+ [-32007]: OCRFallbackFailedError,
99
+ [-32600]: InvalidRequestError,
100
+ [-32601]: MethodNotFoundError,
101
+ [-32602]: InvalidParamsError,
102
+ };
103
+ function errorFromCode(code, message, data) {
104
+ const ErrorClass = ERROR_MAP[code];
105
+ if (ErrorClass) {
106
+ return new ErrorClass(message, data);
107
+ }
108
+ return new ACError(message, code, 126, data);
109
+ }
110
+ function exitCodeFromErrorCode(code) {
111
+ const instance = ERROR_MAP[code];
112
+ if (instance) {
113
+ // Create a temporary instance to get the exit code
114
+ const temp = new instance('');
115
+ return temp.exitCode;
116
+ }
117
+ return 126;
118
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ // @skillful-agents/ac — TypeScript SDK for macOS desktop automation
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.CDPInteractions = exports.CDPAXTree = exports.CDPConnection = exports.CDPClient = exports.AC = exports.Bridge = exports.TimeoutError = exports.PermissionDeniedError = exports.ElementNotFoundError = exports.ACError = exports.REF_PREFIXES = exports.refToRole = exports.isValidRef = exports.parseRef = void 0;
5
+ var refs_js_1 = require("./refs.js");
6
+ Object.defineProperty(exports, "parseRef", { enumerable: true, get: function () { return refs_js_1.parseRef; } });
7
+ Object.defineProperty(exports, "isValidRef", { enumerable: true, get: function () { return refs_js_1.isValidRef; } });
8
+ Object.defineProperty(exports, "refToRole", { enumerable: true, get: function () { return refs_js_1.refToRole; } });
9
+ Object.defineProperty(exports, "REF_PREFIXES", { enumerable: true, get: function () { return refs_js_1.REF_PREFIXES; } });
10
+ var errors_js_1 = require("./errors.js");
11
+ Object.defineProperty(exports, "ACError", { enumerable: true, get: function () { return errors_js_1.ACError; } });
12
+ Object.defineProperty(exports, "ElementNotFoundError", { enumerable: true, get: function () { return errors_js_1.ElementNotFoundError; } });
13
+ Object.defineProperty(exports, "PermissionDeniedError", { enumerable: true, get: function () { return errors_js_1.PermissionDeniedError; } });
14
+ Object.defineProperty(exports, "TimeoutError", { enumerable: true, get: function () { return errors_js_1.TimeoutError; } });
15
+ var bridge_js_1 = require("./bridge.js");
16
+ Object.defineProperty(exports, "Bridge", { enumerable: true, get: function () { return bridge_js_1.Bridge; } });
17
+ var sdk_js_1 = require("./sdk.js");
18
+ Object.defineProperty(exports, "AC", { enumerable: true, get: function () { return sdk_js_1.AC; } });
19
+ // CDP support
20
+ var index_js_1 = require("./cdp/index.js");
21
+ Object.defineProperty(exports, "CDPClient", { enumerable: true, get: function () { return index_js_1.CDPClient; } });
22
+ Object.defineProperty(exports, "CDPConnection", { enumerable: true, get: function () { return index_js_1.CDPConnection; } });
23
+ Object.defineProperty(exports, "CDPAXTree", { enumerable: true, get: function () { return index_js_1.CDPAXTree; } });
24
+ Object.defineProperty(exports, "CDPInteractions", { enumerable: true, get: function () { return index_js_1.CDPInteractions; } });
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IS_NAMED_PIPE = exports.SOCKET_PATH = exports.SNAPSHOTS_DIR = exports.DAEMON_JSON_PATH = exports.AC_DIR = void 0;
4
+ const path_1 = require("path");
5
+ const os_1 = require("os");
6
+ const os = (0, os_1.platform)();
7
+ exports.AC_DIR = (0, path_1.join)((0, os_1.homedir)(), '.ac');
8
+ exports.DAEMON_JSON_PATH = (0, path_1.join)(exports.AC_DIR, 'daemon.json');
9
+ exports.SNAPSHOTS_DIR = (0, path_1.join)(exports.AC_DIR, 'snapshots');
10
+ // macOS: Unix domain socket file on disk
11
+ // Windows: Named pipe (kernel object, no file on disk)
12
+ exports.SOCKET_PATH = os === 'win32'
13
+ ? '\\\\.\\pipe\\ac-daemon'
14
+ : (0, path_1.join)(exports.AC_DIR, 'daemon.sock');
15
+ // Named pipes don't exist as files — connection-based detection needed
16
+ exports.IS_NAMED_PIPE = os === 'win32';
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveBinary = resolveBinary;
4
+ const os_1 = require("os");
5
+ const path_1 = require("path");
6
+ const fs_1 = require("fs");
7
+ // ESM: use import.meta.url directly (CJS build patches this line — see scripts/fix-cjs-resolve.js)
8
+ // @ts-ignore — import.meta.url is valid in ESM; CJS tsconfig rejects it but post-build script fixes it
9
+ const _dirname = __dirname;
10
+ /**
11
+ * Find the package root by walking up from __dirname until we find package.json.
12
+ * Works whether running from source (src/platform/) or compiled (dist/src/platform/).
13
+ */
14
+ function findProjectRoot() {
15
+ let dir = _dirname;
16
+ for (let i = 0; i < 5; i++) {
17
+ if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'package.json')))
18
+ return dir;
19
+ dir = (0, path_1.dirname)(dir);
20
+ }
21
+ return (0, path_1.join)(_dirname, '..', '..');
22
+ }
23
+ function resolveBinary() {
24
+ const os = (0, os_1.platform)();
25
+ const cpu = (0, os_1.arch)();
26
+ if (os !== 'darwin' && os !== 'win32') {
27
+ throw new Error(`Unsupported platform: ${os}. agent-computer supports macOS and Windows.`);
28
+ }
29
+ const ext = os === 'win32' ? '.exe' : '';
30
+ const key = `${os}-${cpu === 'arm64' ? 'arm64' : 'x64'}`;
31
+ const projectRoot = findProjectRoot();
32
+ // Bundled platform-specific binary (npm package)
33
+ const bundledPath = (0, path_1.join)(projectRoot, 'bin', `ac-core-${key}${ext}`);
34
+ if ((0, fs_1.existsSync)(bundledPath)) {
35
+ return bundledPath;
36
+ }
37
+ if (os === 'darwin') {
38
+ // Development: locally-built Swift binary (release)
39
+ const devBinaryPath = (0, path_1.join)(projectRoot, 'native', 'macos', '.build', 'release', 'ac-core');
40
+ if ((0, fs_1.existsSync)(devBinaryPath))
41
+ return devBinaryPath;
42
+ // Development: Swift debug build
43
+ const debugBinaryPath = (0, path_1.join)(projectRoot, 'native', 'macos', '.build', 'debug', 'ac-core');
44
+ if ((0, fs_1.existsSync)(debugBinaryPath))
45
+ return debugBinaryPath;
46
+ }
47
+ if (os === 'win32') {
48
+ const rid = cpu === 'arm64' ? 'win-arm64' : 'win-x64';
49
+ const tfms = ['net9.0-windows', 'net9.0'];
50
+ const base = (0, path_1.join)(projectRoot, 'native', 'windows', 'ACCore', 'bin');
51
+ for (const tfm of tfms) {
52
+ // Development: self-contained release publish
53
+ const publishPath = (0, path_1.join)(base, 'Release', tfm, rid, 'publish', 'ac-core.exe');
54
+ if ((0, fs_1.existsSync)(publishPath))
55
+ return publishPath;
56
+ // Development: release build (framework-dependent)
57
+ const releasePath = (0, path_1.join)(base, 'Release', tfm, 'ac-core.exe');
58
+ if ((0, fs_1.existsSync)(releasePath))
59
+ return releasePath;
60
+ // Development: debug build (framework-dependent)
61
+ const debugPath = (0, path_1.join)(base, 'Debug', tfm, 'ac-core.exe');
62
+ if ((0, fs_1.existsSync)(debugPath))
63
+ return debugPath;
64
+ }
65
+ }
66
+ const buildHint = os === 'win32'
67
+ ? 'cd native/windows && dotnet build'
68
+ : 'cd native/macos && swift build -c release';
69
+ throw new Error(`Native binary not found for ${key}. ` +
70
+ `Build from source: ${buildHint}`);
71
+ }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REF_PREFIXES = void 0;
4
+ exports.roleToPrefix = roleToPrefix;
5
+ exports.parseRef = parseRef;
6
+ exports.isValidRef = isValidRef;
7
+ exports.refToRole = refToRole;
8
+ // Single-letter prefix → role mapping
9
+ const SINGLE_LETTER_PREFIXES = {
10
+ b: 'button',
11
+ t: 'textfield',
12
+ l: 'link',
13
+ m: 'menuitem',
14
+ c: 'checkbox',
15
+ r: 'radio',
16
+ s: 'slider',
17
+ d: 'dropdown',
18
+ i: 'image',
19
+ g: 'group',
20
+ w: 'window',
21
+ x: 'table',
22
+ o: 'row',
23
+ a: 'tab',
24
+ e: 'generic',
25
+ };
26
+ // Two-letter prefix → role mapping (for less common roles)
27
+ const TWO_LETTER_PREFIXES = {
28
+ cb: 'combobox',
29
+ sa: 'scrollarea',
30
+ st: 'stepper',
31
+ sp: 'splitgroup',
32
+ tl: 'timeline',
33
+ pg: 'progress',
34
+ tv: 'treeview',
35
+ wb: 'webarea',
36
+ };
37
+ exports.REF_PREFIXES = {
38
+ ...SINGLE_LETTER_PREFIXES,
39
+ ...TWO_LETTER_PREFIXES,
40
+ };
41
+ // Reverse mapping: role → prefix
42
+ const ROLE_TO_PREFIX = {};
43
+ for (const [prefix, role] of Object.entries(exports.REF_PREFIXES)) {
44
+ // Prefer shorter prefix if role already mapped
45
+ if (!ROLE_TO_PREFIX[role] || prefix.length < ROLE_TO_PREFIX[role].length) {
46
+ ROLE_TO_PREFIX[role] = prefix;
47
+ }
48
+ }
49
+ // textfield and textarea both map to 't' — textarea gets 't' by convention
50
+ ROLE_TO_PREFIX['textarea'] = 't';
51
+ function roleToPrefix(role) {
52
+ return ROLE_TO_PREFIX[role] ?? 'e';
53
+ }
54
+ const ALL_PREFIXES = new Set(Object.keys(exports.REF_PREFIXES));
55
+ // Ref pattern: @<prefix><positive integer>
56
+ // Try two-letter prefix first, then single-letter
57
+ const REF_REGEX = /^@([a-z]{1,2})([1-9]\d*)$/;
58
+ function parseRef(input) {
59
+ const match = REF_REGEX.exec(input);
60
+ if (!match) {
61
+ throw new Error(`Invalid ref format: "${input}". Expected @<prefix><number> (e.g., @b1, @cb3)`);
62
+ }
63
+ const rawPrefix = match[1];
64
+ const id = parseInt(match[2], 10);
65
+ // Try two-letter prefix first (e.g., "@cb1" → prefix "cb", not "c" with "b1")
66
+ if (rawPrefix.length === 2 && ALL_PREFIXES.has(rawPrefix)) {
67
+ return { prefix: rawPrefix, role: exports.REF_PREFIXES[rawPrefix], id };
68
+ }
69
+ // Try single-letter prefix
70
+ if (rawPrefix.length === 1 && ALL_PREFIXES.has(rawPrefix)) {
71
+ return { prefix: rawPrefix, role: exports.REF_PREFIXES[rawPrefix], id };
72
+ }
73
+ // Two-letter prefix but only first letter is valid (e.g., "@bx1" is not valid)
74
+ if (rawPrefix.length === 2) {
75
+ // Check if first letter alone is a valid prefix — if so, reject because the second char is part of the number parsing issue
76
+ throw new Error(`Invalid ref prefix: "${rawPrefix}" in "${input}". Valid prefixes: ${[...ALL_PREFIXES].sort().join(', ')}`);
77
+ }
78
+ throw new Error(`Unknown ref prefix: "${rawPrefix}" in "${input}". Valid prefixes: ${[...ALL_PREFIXES].sort().join(', ')}`);
79
+ }
80
+ function isValidRef(input) {
81
+ try {
82
+ parseRef(input);
83
+ return true;
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ function refToRole(prefix) {
90
+ return exports.REF_PREFIXES[prefix];
91
+ }